GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.03k stars 231 forks source link

Methods of proxied objects are bound to the original object, not the proxy #242

Closed lazorchakp closed 1 year ago

lazorchakp commented 1 year ago

I'm attempting to use a custom ObjectProxy to modify the behavior of a specific bound method of an object. This seems to work normally when I call the method directly, but it fails when my target method is called from within another bound method. Here's an example:

import wrapt

class Thing:
    def inner(self):
        return "normal"

    def outer(self):
        return self.inner()

class Proxy(wrapt.ObjectProxy):
    def inner(self):
        return "modified"

thing = Thing()
proxied_thing = Proxy(thing)

assert thing.inner() == "normal"
assert thing.outer() == "normal"
assert proxied_thing.inner() == "modified"
assert proxied_thing.outer() == "modified"  # AssertionError

The issue seems to stem from the fact that the self passed to bound methods of a proxied object is the original object, not the proxy. I tried messing around with ObjectProxy.__getattr__ to change this behavior, but I haven't been able to come up with a safe / stable solution.

I recognize that this may be expected behavior, but it feels a bit counterintuitive. I'd really appreciate your thoughts or suggestions on this!

GrahamDumpleton commented 1 year ago

The proxy wrapper is just that, a wrapper, the inner wrapped object doesn't know anything about what may wrap it.

For the sorts of changes you seem to want to make, you need to monkey patch the original object and for that there are two options.

The first if the change is to be applied to all instances of the type to be modified, is to monkey patch the type object for the class itself.

The second if the change is to be applied only to selected instances of the type, is to monkey patch just the instances you want to be modified.

Which of these two are you needing to do? Let me know and then can direct you as to how to do it.

lazorchakp commented 1 year ago

Thanks for the quick response. That makes sense; I guess I'm not thinking about ObjectProxy correctly.

I definitely fall into the second case here - I have a specific instance I need to modify. I imagine I'd be able to use something along the lines of

@wrapt.function_wrapper
def inner_wrapper(wrapped, instance, args, kwargs):
    return "modified"

thing.inner = inner_wrapper(thing.inner)

This at least makes my example test case pass.

My concern with using this approach for my particular application is that I actually know very little about the object I'm wrapping, and the method I'd like to monkeypatch might be read-only. Using an ObjectProxy appeared to offer a nice workaround to that limitation. Do you know of any way around this (admittedly strange/specific) restriction?

GrahamDumpleton commented 1 year ago

Not being able to monkey patch something only generally comes up in special cases where a class is actually implemented as a C extension. There are also certain special Python methods, eg., __enter__ and __exit__, where you need to use special tricks to monkey patch it on an instance.

What is it about the object type makes you think it may be read only?

lazorchakp commented 1 year ago

The object might be an instance of a class defined in a C extension. After having thought about this a bit more, however, I realized I'm more worried about this case than I need to be. It's quite unlikely to occur and the consequences are not particularly high, so I think the risk is tolerable.

I'm going to go ahead with the function_wrapper and monkeypatching solution. Thanks for your help on this!