45
votes

Is it at all possible to monkey patch the value of a @property of an instance of a class that I do not control?

class Foo:
    @property
    def bar(self):
        return here().be['dragons']

f = Foo()
print(f.bar)  # baz
f.bar = 42    # MAGIC!
print(f.bar)  # 42

Obviously the above would produce an error when trying to assign to f.bar. Is # MAGIC! possible in any way? The implementation details of the @property are a black box and not indirectly monkey-patchable. The entire method call needs to be replaced. It needs to affect a single instance only (class-level patching is okay if inevitable, but the changed behaviour must only selectively affect a given instance, not all instances of that class).

7
Do you mean that you want to make f.bar not a property any more (and if so, without affecting other instances?) Note that properties, like other descriptors, are stored on the class.jonrsharpe
Yes, I want to affect the value which is returned when accessing f.bar on this one instance only.deceze♦
I think the only way to do that would be to alter f.__dict__ directly, providing a new bar attribute that will get looked up before the property is, but only for that single instance.jonrsharpe
My solution was to make copy of the class, change the descriptor and then replace the instance's class: stackoverflow.com/a/30578922/248296warvariuc
@deceze You can change the class using Foo().__class__ = SubFooMarkus Meskanen

7 Answers

25
votes

Subclass the base class (Foo) and change single instance's class to match the new subclass using __class__ attribute:

>>> class Foo:
...     @property
...     def bar(self):
...         return 'Foo.bar'
...
>>> f = Foo()
>>> f.bar
'Foo.bar'
>>> class _SubFoo(Foo):
...     bar = 0
...
>>> f.__class__ = _SubFoo
>>> f.bar
0
>>> f.bar = 42
>>> f.bar
42
18
votes
from module import ClassToPatch

def get_foo(self):
    return 'foo'

setattr(ClassToPatch, 'foo', property(get_foo))
9
votes

To monkey patch a property, there is an even simpler way:

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)
2
votes

Idea: replace property descriptor to allow setting on certain objects. Unless a value is explicitly set this way, original property getter is called.

The problem is how to store the explicitly set values. We cannot use a dict keyed by patched objects, since 1) they are not necessarily comparable by identity; 2) this prevents patched objects from being garbage-collected. For 1) we could write a Handle that wraps objects and overrides comparison semantics by identity and for 2) we could use weakref.WeakKeyDictionary. However, I couldn't make these two work together.

Therefore we use a different approach of storing the explicitly set values on the object itself, using a "very unlikely attribute name". It is of course still possible that this name would collide with something, but that's pretty much inherent to languages such as Python.

This won't work on objects that lack a __dict__ slot. Similar problem would arise for weakrefs though.

class Foo:
    @property
    def bar (self):
        return 'original'

class Handle:
    def __init__(self, obj):
        self._obj = obj

    def __eq__(self, other):
        return self._obj is other._obj

    def __hash__(self):
        return id (self._obj)


_monkey_patch_index = 0
_not_set            = object ()
def monkey_patch (prop):
    global _monkey_patch_index, _not_set
    special_attr = '$_prop_monkey_patch_{}'.format (_monkey_patch_index)
    _monkey_patch_index += 1

    def getter (self):
        value = getattr (self, special_attr, _not_set)
        return prop.fget (self) if value is _not_set else value

    def setter (self, value):
        setattr (self, special_attr, value)

    return property (getter, setter)

Foo.bar = monkey_patch (Foo.bar)

f = Foo()
print (Foo.bar.fset)
print(f.bar)  # baz
f.bar = 42    # MAGIC!
print(f.bar)  # 42
1
votes

It looks like you need to move on from properties to the realms of data descriptors and non-data descriptors. Properties are just a specialised version of data descriptors. Functions are an example of non-data descriptors -- when you retrieve them from an instance they return a method rather than the function itself.

A non-data descriptor is just an instance of a class that has a __get__ method. The only difference with a data descriptor is that it has a __set__ method as well. Properties initially have a __set__ method that throws an error unless you provide a setter function.

You can achieve what you want really easily just by writing your own trivial non-data descriptor.

class nondatadescriptor:
    """generic nondata descriptor decorator to replace @property with"""
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objclass):
        if obj is not None:
            # instance based access
            return self.func(obj)
        else:
            # class based access
            return self

class Foo:
    @nondatadescriptor
    def bar(self):
        return "baz"

foo = Foo()
another_foo = Foo()

assert foo.bar == "baz"
foo.bar = 42
assert foo.bar == 42
assert another_foo.bar == "baz"
del foo.bar
assert foo.bar == "baz"
print(Foo.bar)

What makes all this work is that logic under the hood __getattribute__. I can't find the appropriate documentation at the moment, but order of retrieval is:

  1. Data descriptors defined on the class are given the highest priority (objects with both __get__ and __set__), and their __get__ method is invoked.
  2. Any attribute of the object itself.
  3. Non-data descriptors defined on the class (objects with only a __get__ method).
  4. All other attributes defined on the class.
  5. Finally the __getattr__ method of the object is invoked as a last resort (if defined).
1
votes

You can also patch property setters. Using @fralau 's answer:

from module import ClassToPatch

def foo(self, new_foo):
    self._foo = new_foo

ClassToPatch.foo = ClassToPatch.foo.setter(foo)

reference

0
votes

In case someone needs to patch a property while being able to call the original implementation, here is an example:

@property
def _cursor_args(self, __orig=mongoengine.queryset.base.BaseQuerySet._cursor_args):
    # TODO: remove this hack when we upgrade MongoEngine
    # https://github.com/MongoEngine/mongoengine/pull/2160
    cursor_args = __orig.__get__(self)
    if self._timeout:
        cursor_args.pop("no_cursor_timeout", None)
    return cursor_args


mongoengine.queryset.base.BaseQuerySet._cursor_args = _cursor_args