Reputation: 520
I have a metaclass that defines a class level attribute which should be unique for each subclass but shared across instances of each subclass.
class MetaValidator(type):
def __new__(
cls, name, bases, dct
):
new_cls = super().__new__(cls, name, bases, dct)
new_cls.valid_funcs = []
return new_cls
Now I'd like to implement a decorator that appends the decorated class method to valid_funcs within the derived class. However because the derived class is still being defined I don't have a reference to the derived decorator so I end up appending to the base class. Here's my code:
class Validator(object, metaclass=MetaValidator):
@classmethod
def add(cls, f):
cls.valid_funcs.append(f)
return f
def _validate(self, **kwargs):
for f in self.valid_funcs:
params = inspect.signature(f).parameters.keys()
f_kwargs = {name: kwargs[name] for name in params}
f(**f_kwargs)
def validate(self, **kwargs):
self._validate(**kwargs)
class A(Validator):
@staticmethod
@Validator.add
def test_func(x):
return x
class B(Validator):
@staticmethod
@Validator.add
def test_func(x, y):
return x, y
a = A()
a.validate(x="In A")
b = B()
b.validate(x="In B", y=" Called with arg y")
print(Validator.valid_funcs)
print(a.valid_funcs)
print(b.valid_funcs)
This prints:
[<function A.test_func at 0x7f0189d4fc80>,
<function B.test_func at 0x7f0189d4fd08>]
[]
[]
I want:
[]
[<function A.test_func at 0x7f0189d4fc80>]
[<function B.test_func at 0x7f0189d4fd08>]
Upvotes: 3
Views: 570
Reputation: 1121834
There is no class object yet when decorators on functions in the class body are executed. The class body is executed first, then the class is created.
Instead of having the decorator look for a class attribute to mutate, add an attribute to a decorated function object. The metaclass, or the _validate()
implementation then looks for any objects with this attribute and adds them to the list once the class object has been created.
I'm going to assume you'd want to retain the order in which the decorators would have added the decorated items to the list:
from itertools import count
class Validator(metaclass=MetaValidator):
@classmethod
def add(cls, f):
_count = getattr(Validator.add, '_count', None)
if _count is None:
_count = Validator.add.__func__._count = count()
f._validator_function_id = next(_count)
return f
and in the metaclass:
class MetaValidator(type):
def __new__(cls, name, bases, dct):
new_cls = super().__new__(cls, name, bases, dct)
registered = []
for v in dct.values():
id = getattr(v, '_validator_function_id', None)
if id is None and isinstance(v, (staticmethod, classmethod)):
# unwrap staticmethod or classmethod decorators
id = getattr(v.__func__, '_validator_function_id', None)
if id is not None:
registered.append((id, v))
new_cls.valid_funcs = [f for _, f in sorted(registered)]
return new_cls
Note that if you are using Python 3.6 or newer, then you don't need a metaclass at all any more. You can put the same logic into the class.__init_subclass__
method.
Note that this registers unbound objects. For staticmethod
objects, that means the call will fail with:
TypeError: <staticmethod object at 0x10d1b7048> is not a callable object
You perhaps want to register the __func__
attribute in that case, or use .__get__
to 'bind' the object to something (a staticmethod
ignores the binding context anyway)`.
If you bind explicitly, in the _validate()
method, then you don't actually have to use staticmethod
objects:
def _validate(self, **kwargs):
for f in self.valid_funcs:
bound = f.__get__(self)
signature = inspect.signature(bound)
bound(**{name: kwargs[name] for name in signature.parameters})
Now @validator.add
will work with staticmethod
, classmethod
and regular functions.
And if you have the _validate()
method look for the methods, then binding can be done for you. You can choose to support inheritance here by just using dir()
and getattr()
:
from operator import itemgetter
from itertools import count
class Validator:
@classmethod
def add(cls, f):
_count = getattr(Validator.add, '_count', None)
if _count is None:
_count = Validator.add.__func__._count = count()
f._validator_function_id = next(_count)
return f
def _list_validators(self):
objects = (getattr(self, name) for name in dir(self))
return sorted(
(o for o in objects if hasattr(o, '_validator_function_id')),
key=attrgetter('_validator_function_id'))
def _validate(self, **kwargs):
for f in self._list_validators():
signature = inspect.signature(f)
f(**{name: kwargs[name] for name in signature.parameters})
getattr()
gives you a bound object, no further binding necessary.
Upvotes: 1
Reputation: 280564
While having the metaclass __new__
handle adding functions to valid_funcs
is an option, another option would be to inject valid_funcs
into the namespace of the class body before the class even exists, using __prepare__
:
class MetaValidator(type):
@classmethod
def __prepare__(cls, name, bases, **kwds):
ns = super().__prepare__(name, bases, **kwds)
ns['valid_funcs'] = []
return ns
def register(func_list):
def inner_register(func):
func_list.append(func)
return func
return inner_register
class A(metaclass=MetaValidator):
@register(valid_funcs)
def method(self):
...
I'd probably skip all the metaclass stuff and require classes to do valid_funcs = []
themselves, though. The additional complexity of a metaclass isn't worth it just to save one line of boilerplate per class.
Upvotes: 0