Search code examples
pythonpython-decorators

Can a decorator returning a non-callable take additional arguments?


I have subclassed the built-in property class (call it SpecialProperty) in order to add more fields to it:

class SpecialProperty(property):
    extra_field_1 = None
    extra_field_2 = None
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        super().__init__(fget, fset, fdel, doc)

def make_special_property(func):
    prop = SerialisableProperty(fget=func)
    return prop

and I am able to use it in the same fashion as the built-in property() decorator:

@my_module.make_special_property
def get_my_property(self): return self._my_property

I now want to further specialise my SpecialProperty instances populating one of the extra fields I have added to the class with an arbitrary value.

Is it possible, in Python, to write a decorator that will return a property with also accepting extra parameters?

I'd like to do it via the decorator because this is where and when the information is most relevant, however I'm finding myself stuck. I suspect this falls under the domain of decorators with arguments that have been well documented (Decorators with arguments? (Stack Overflow), or Python Decorators II: Decorator Arguments (artima.com) to only cite a couple sources), however I find myself unable to apply the same pattern to my case.

Here's how I'm trying to write it:

@my_module.make_special_property("example string")
def get_my_property(self): return self._my_property

And on the class declaring get_my_property:

>>> DeclaringClass.my_property
<SpecialProperty object at 0x...>
>>> DeclaringClass.my_property.extra_field_1
'example string'

Since I am making properties, the decorated class member should be swapped with an instance of SpecialProperty, and hence should not be a callable anymore -- thus, I am unable to apply the "nested wrapper" pattern for allowing a decorator with arguments.

Non working example:

def make_special_property(custom_arg_1):
    def wrapper(func):
        prop = SerialisableProperty(fget=func)
        prop.extra_field_1 = custom_arg_1
        return prop
    return wrapper # this returns a callable (function)

I shouldn't have a callable be returned here, if I want a property I should have a SpecialProperty instance be returned, but I can't call return wrapper(func) for obvious reasons.


Solution

  • Your decorator doesn't return a callable. Your decorator factory returns a decorator, which returns a property. You might understand better if you rename the functions:

    def make_decorator(custom_arg_1):
        def decorator(func):
            prop = SerialisableProperty(fget=func)
            prop.extra_field_1 = custom_arg_1
            return prop
        return decorator
    

    When you decorate with make_decorator, it is called with an argument, and decorator is returned and called on the decorated function.