Skip to content Skip to sidebar Skip to footer

A Lean Interface For Making Python Decorator Classes

I've been looking to make an object-oriented setup to make decorator factories. A simple version can be found in this stackoverflow answer. But I'm not totally satisfied with the s

Solution 1:

We can start with a base such as this:

from functools import partial, update_wrapper

classDecorator:
    """A transparent decorator -- to be subclassed"""def__new__(cls, func=None, **kwargs):
        if func isNone:
            return partial(cls, **kwargs)
        else:
            self = super().__new__(cls)
            self.func = func
            for attr_name, attr_val in kwargs.items():
                setattr(self, attr_name, attr_val)
            return update_wrapper(self, func)

    def__call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

It allows one to define decorator factories by subclassing and defining a __new__ method (that should call its parent).

Then, one can use __init_subclass__ to extract the desired arguments of that __new__ directly from attributes of the subclass, as such:

from inspect import Signature, Parameter

PK, KO = Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY

classLiteral:
    """An object to indicate that the value should be considered literally"""def__init__(self, val):
        self.val = val

classDecora(Decorator):
    _injected_deco_params = ()

    def__init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if'__new__'notin cls.__dict__:  # if __new__ hasn't been defined in the subclass...
            params = ([Parameter('self', PK), Parameter('func', PK, default=None)])
            cls_annots = getattr(cls, '__annotations__', {})
            injected_deco_params = set()
            for attr_name in (a for a in cls.__dict__ ifnot a.startswith('__')):
                attr_obj = cls.__dict__[attr_name]  # get the attributeifnotisinstance(attr_obj, Literal):
                    setattr(cls, attr_name, attr_obj)  # what we would have done anyway...# ... but also add a parameter to the list of params
                    params.append(Parameter(attr_name, KO, default=attr_obj,
                                            annotation=cls_annots.get(attr_name, Parameter.empty)))
                    injected_deco_params.add(attr_name)
                else:  # it is a Literal, sosetattr(cls, attr_name, attr_obj.val)  # just assign the literal value
            cls._injected_deco_params = injected_deco_params

            def__new__(cls, func=None, **kwargs):
                if cls._injected_deco_params andnotset(kwargs).issubset(cls._injected_deco_params):
                    raise TypeError("TypeError: __new__() got unexpected keyword arguments: "f"{kwargs.keys() - cls._injected_deco_params}")
                if func isNone:
                    return partial(cls, **kwargs)
                else:
                    return Decorator.__new__(cls, func, **kwargs)

            __new__.__signature__ = Signature(params)
            cls.__new__ = __new__

It passes the tests listed in the question, but I'm not super experienced with this kind of object oriented magic, so would be curious to see alternative solutions.

Post a Comment for "A Lean Interface For Making Python Decorator Classes"