wrapt icon indicating copy to clipboard operation
wrapt copied to clipboard

Question on decorating @property methods

Open harlowja opened this issue 10 years ago • 6 comments

A question recently came up on https://review.openstack.org/#/c/206253/ about using the universal wrapt decorator on @property methods, and this doesn't seem possible (see comments in that review); I'm wondering if u know anyway that it is possible to make it work? If not that's fine to, just thought u might have some thoughts there?

That code @ https://github.com/openstack/debtcollector/blob/master/debtcollector/removals.py#L28 would be the 'universal remove/deprecate thing function' but in that review it doesn't seem possible to use it on @property methods. Thoughts?

harlowja avatar Jul 29 '15 15:07 harlowja

You just need to apply the decorator before you apply the property decorator.

I guess that theoretically you could retrieve thew original function from the property object and decorate that and then redecorate everything with property if you REALLY needed this functionality, but I would argue that you are doing it wrong if you want to do that

colonelpanic8 avatar Jul 29 '15 20:07 colonelpanic8

Ya, that's what I've seen/tried to, but it seems like that will be a common PITA to do; making sure people get told the order has to be this or that, vs any order just working.

harlowja avatar Jul 29 '15 21:07 harlowja

hmmm. If you really understand decorators and descriptors, its not really that much of a pain in the ass. All you have to understand is that the property decorator does not actually return a callable object, but a descriptor. You can't decorate a descriptor in the same way as a function, so its really as simple as that.

At some point I think you just have to expect developers to understand the abstractions/language features that they are using.

colonelpanic8 avatar Jul 29 '15 21:07 colonelpanic8

Sorry for the slow reply on this. One may be able to put together a wrapper which would be smart enough to behave the right way even when applied to a decorator, but there are a few issues around this. It isn't something I think I could sensibly do as part of the wrapt decorator default wrapper because I don't know how people would use it, but since your remove() function hides the fact that wrapt.decorator is used and you only use its basic functionality, you could possibly replace use of wrapt.decorator such that a derived version of FunctionWrapper is used which adds descriptor support.

The things to consider are as follows.

When the decorator is placed inside of @property, the wrapper would only be created once. If placed outside and give descriptor functionality, it is likely the FunctionWrapper instance would need to be created on each access of the property, adding the cost of the wrapper creation.

You have to deal with setter and deleter being used on the getter property, and thus the wrapper, to add functions for those as well. If the getter is marked as removed, then presumably these are also marked as removal.

Thinking about it, it may all up not be too bad. The remove() function when given the thing to be wrapped can do an isinstance() check for type property and totally adjust how it works and rather than use a decorator, use an ObjectProxy which overrides the parts of property to allow interception of access to add the decorator on each access.

I'll try and put together an example.

GrahamDumpleton avatar Aug 12 '15 07:08 GrahamDumpleton

First attempt. This only deals with it being used on a getter property. I have to think through the setter and deleter cases as when dealing with those, even knowing which it is is which is a problem.

from __future__ import print_function

import wrapt
import functools

def remove(wrapped=None, message=None):
    if wrapped is None:
        return functools.partial(remove, message=message)

    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print('wrapper', wrapped, message, args, kwargs)
        return wrapped(*args, **kwargs)

    # Assume only used on getter for now.

    if isinstance(wrapped, property):
        return property(wrapper(wrapped.fget))

    return wrapper(wrapped)

@remove
def func():
    pass

func()

class C(object):
    @remove
    def method(self):
        pass

    @property
    @remove
    def attribute_1(self):
        print('attribute_1.getter')
        pass

    @remove
    @property
    def attribute_2(self):
        print('attribute_2.getter')
        pass

c = C()

c.method()

c.attribute_1

c.attribute_2

GrahamDumpleton avatar Aug 12 '15 08:08 GrahamDumpleton

Better version.

If you use remove() inside of @property for getter, must be inside of the decorator for any setter and deleter as well, as per third attribute example.

If you use remove() outside of @property for the getter, you do not need to use it on setter and deleter and instead they will inherit it automatically from that for the getter, as per second attribute example.

Will that do what you want?

from __future__ import print_function

import wrapt
import functools

def remove(wrapped=None, message=None):
    if wrapped is None:
        return functools.partial(remove, message=message)

    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print('wrapper', wrapped, message, args, kwargs)
        return wrapped(*args, **kwargs)

    class Property(property):
        def getter(self, fget):
            print('getter', fget)
            return type(self)(wrapper(fget), self.fset, self.fdel)
        def setter(self, fset):
            print('setter', fset)
            return type(self)(self.fget, wrapper(fset), self.fdel)
        def deleter(self, fdel):
            print('deleter', fdel)
            return type(self)(self.fget, self.fset, wrapper(fdel))

    # Assume that if get exact property type that is getter.

    if type(wrapped) == property:
        return Property(wrapper(wrapped.fget), wrapped.fset, wrapped.fdel)

    return wrapper(wrapped)

@remove
def func():
    pass

func()

class C(object):
    @remove
    def method(self):
        pass

    @property
    @remove
    def attribute_1(self):
        print('attribute_1.getter')
        pass

    @remove
    @property
    def attribute_2(self):
        print('getting#2')
        pass

    @attribute_2.setter
    def attribute_2(self, value):
        print('setting#2', value)

    @attribute_2.deleter
    def attribute_2(self):
        print('deleting#2')

    @property
    @remove
    def attribute_3(self):
        print('getting#3')
        pass

    @attribute_3.setter
    @remove
    def attribute_3(self, value):
        print('setting#3', value)

    @attribute_3.deleter
    @remove
    def attribute_3(self):
        print('deleting#3')

c = C()

print('---')

c.method()

print('---')

c.attribute_1

print('---')

c.attribute_2

print('---')

c.attribute_2 = 1

print('---')

del c.attribute_2

print('---')

c.attribute_3

print('---')

c.attribute_3 = 1

print('---')

del c.attribute_3

GrahamDumpleton avatar Aug 12 '15 11:08 GrahamDumpleton