Auto-register class methods using decorator

Question:

I want to be able to create a python decorator that automatically “registers” class methods in a global repository (with some properties).

Example code:

class my_class(object):

    @register(prop1,prop2)
    def my_method( arg1,arg2 ):
       # method code here...

    @register(prop3,prop4)
    def my_other_method( arg1,arg2 ):
       # method code here...

I want that when loading is done, somewhere there will be a dict containing:

{ "my_class.my_method"       : ( prop1, prop2 )
  "my_class.my_other_method" : ( prop3, prop4 ) }

Is this possible?

Asked By: adamk

||

Answers:

No. The decorator receives the function before it has become a method, so you don’t know what class it is on.

Not easy, but if you are using Python 3 this should work:

registry = {}

class MetaRegistry(type):

    @classmethod
    def __prepare__(mcl, name, bases):
        def register(*props):
            def deco(f):
                registry[name + "." + f.__name__] = props
                return f
            return deco
        d = dict()
        d['register'] = register
        return d

    def __new__(mcl, name, bases, dct):
        del dct['register']
        cls = super().__new__(mcl, name, bases, dct)
        return cls

class my_class(object, metaclass=MetaRegistry):

    @register('prop1','prop2')
    def my_method( arg1,arg2 ):
       pass # method code here...

    @register('prop3','prop4')
    def my_other_method( arg1,arg2 ):
       pass # method code here...

print(registry)

Note that you can not have method names equal to the decorator name in the meta-typed class, because they are automatically deleted by the del command in the metaclass’s __new__ method.

For Python 2.6 I think you would have to explicitly tell the decorator the class name to use.

Answered By: Duncan

Not with just a decorator, no. But a metaclass can automatically work with a class after its been created. If your register decorator just makes notes about what the metaclass should do, you can do the following:

registry = {}

class RegisteringType(type):
    def __init__(cls, name, bases, attrs):
        for key, val in attrs.iteritems():
            properties = getattr(val, 'register', None)
            if properties is not None:
                registry['%s.%s' % (name, key)] = properties

def register(*args):
    def decorator(f):
        f.register = tuple(args)
        return f
    return decorator

class MyClass(object):
    __metaclass__ = RegisteringType
    @register('prop1','prop2')
    def my_method( arg1,arg2 ):
        pass

    @register('prop3','prop4')
    def my_other_method( arg1,arg2 ):
        pass

print registry

printing

{'MyClass.my_other_method': ('prop3', 'prop4'), 'MyClass.my_method': ('prop1', 'prop2')}
Answered By: Matt Anderson

Here’s a little love for class decorators. I think the syntax is slightly simpler than that required for metaclasses.

def class_register(cls):
    cls._propdict = {}
    for methodname in dir(cls):
        method = getattr(cls, methodname)
        if hasattr(method, '_prop'):
            cls._propdict.update(
                {cls.__name__ + '.' + methodname: method._prop})
    return cls


def register(*args):
    def wrapper(func):
        func._prop = args
        return func
    return wrapper


@class_register
class MyClass(object):

    @register('prop1', 'prop2')
    def my_method(self, arg1, arg2):
        pass

    @register('prop3', 'prop4')
    def my_other_method(self, arg1, arg2):
        pass

myclass = MyClass()
print(myclass._propdict)
# {'MyClass.my_other_method': ('prop3', 'prop4'), 'MyClass.my_method': ('prop1', 'prop2')}
Answered By: unutbu

Not as beautiful or elegant, but probably the simplest way if you only need this in one class only:

_registry = {}
class MyClass(object):
    def register(*prop):
        def decorator(meth):
            _registry[MyClass.__name__ + '.' + meth.__name__] = prop
        return decorator

    @register('prop1', 'prop2')
    def my_method(self, arg1, arg2):
        pass
    @register('prop3', 'prop4')
    def my_other_method(self, arg1, arg2):
        pass

    del register
Answered By: Lie Ryan

If you need the classes name, use Matt’s solution. However, if you’re ok with just having the methods name — or a reference to the method — in the registry, this might be a simpler way of doing it:

class Registry:
    r = {}

    @classmethod
    def register(cls, *args):
        def decorator(fn):
            cls.r[fn.__name__] = args
            return fn
        return decorator

class MyClass(object):

    @Registry.register("prop1","prop2")
    def my_method( arg1,arg2 ):
        pass

    @Registry.register("prop3","prop4")
    def my_other_method( arg1,arg2 ):
        pass

print Registry.r

print

{'my_other_method': ('prop3', 'prop4'), 'my_method': ('prop1', 'prop2')}
Answered By: Manuel Ebert

To summarize, update and explain the existing answers, you have two options:

  1. Using Class Decorators (suggested by @unutbu)
  2. Using a Metaclass (suggested by @Matt Anderson)

However, both of them rely on giving the function an attribute so it can be identified:

def register(*args):
    """
    Creates an attribute on the method, so it can
    be discovered by the metaclass
    """

    def decorator(f):
        f._register = args
        return f

    return decorator

1. The Class Decorator Approach

import inspect

def class_register(cls):
    for method_name, _ in inspect.getmembers(cls):
        method = getattr(cls, method_name)
        if hasattr(method, "_prop"):
            cls._propdict.update({f"{cls.__name__}.{method_name}": method._prop})
    return cls


@class_register
class MyClass:

    _propdict = {}

    @register("prop1", "prop2")
    def my_method(self, arg1, arg2):
        pass

    @register("prop3", "prop4")
    def my_other_method(self, arg1, arg2):
        pass


print(MyClass._propdict)

2. The Metaclass Approach

registry = {}


class RegisteringType(type):
    def __init__(cls, name, bases, attrs):
        for key, val in attrs.items():
            properties = getattr(val, "_register", None)
            if properties is not None:
                registry[f"{name}.{key}"] = properties


class MyClass(metaclass=RegisteringType):
    @register("prop1", "prop2")
    def my_method(self, arg1, arg2):
        pass

    @register("prop3", "prop4")
    def my_other_method(self, arg1, arg2):
        pass


print(registry)
Answered By: Seanny123