7

In my django project (django 1.6 - soon upgrading to 1.9, python 2.7),

I'd like to apply a decorator on all my project's model classes (20-30 classes). All of these classes already inherit from a parent class called 'LinkableModel', which I wrote for a certain (non-related) purpose.

Now, I'd like to apply a class decorator to all these models. (specifically I'm referring to decorator 'python_2_unicode_compatible': https://docs.djangoproject.com/en/1.9/ref/utils/#django.utils.encoding.python_2_unicode_compatible).

When I add this decorator to their parent class 'LinkableModel', it's not inherited. Is there any way to apply a decorator to multiple classes, without adding it to each and every class?

(Theoretically I even don't mind if this decorator will be by default applied to all classes in my project...)

Code snippet:

@python_2_unicode_compatible
class LinkableModel(models.Model):
    ...
    ...
    ...

class MyModel1(LinkableModel):
    ...
    ...
    ...

class MyModel2(LinkableModel):
    ...
    ...
    ...
o_c
  • 3,965
  • 1
  • 22
  • 22

3 Answers3

9

In Python 3.7 now you can do it this way:

  class ParentClass:
      def __init_subclass__(cls, **kwargs):
          return your_decorator(_cls=cls)

it will apply decorator for each subclass of ParentClass

UPDATED: full example:

def your_decorator(_cls):
    print("Hello, I'm decor!")
    def wrapper():
        return _cls()
    return wrapper


class ParentClass:
    def __init_subclass__(cls, **kwargs):
        return your_decorator(_cls=cls)


class A(ParentClass):
    pass

a = A()
  • 1
    I really want this to work, but alas it seems not to in general. It seems to work using dataclasses.dataclass as the decorator (but the return statement from __init_subclass__ is not necessary, it's not clear to me what that return statement achieves), but not when I use any decorator of my own (this must be due to some behaviour of the dataclass framework). I get the feeling this is not designed to work this way, __init_subclass__ is not intended to allow for modification of the child class, rather the parent class. – kerzane Apr 07 '20 at 11:58
  • 1
    @kerzane, hi! Not sure that cause the problem. Maybe full example will be helpful. I updated the answer, pls check. Be sure, that you runs code with Python 3.7 Will be more useful if you attach code that does not work for you – Yuliya Volkova Apr 26 '20 at 09:30
  • thanks for your reply! My query is based on the fact that when I add a print statement in the wrapper function: ` def wrapper(): print("Hello, I'm wrapper!") return _cls()` That this print statement is not triggered on instantiating an object of class A. Separately, when I remove the return statement from the __init_subclass__ method, the behaviour of the code is not changed, so I'm not sure the object returned from __init_subclass__ is having the desired result. – kerzane Apr 27 '20 at 10:23
  • For what it's worth, this is 3.6, not 3.7 – Ehsan Kia Jun 12 '21 at 08:31
  • How would this look if I wanted to pass arguments to the decorator? Or if the decorator I want to apply was written by someone else and doesn't have the _cls? – Apreche Dec 30 '21 at 18:49
  • `__init_subclass__` isn’t supposed to return anything, and the return value is ignored. This only works if the decorator *modifies* `cls`. If it returns a new instance instead, the code will fail. In particular, the concrete example code in this answer does nothing. – Konrad Rudolph Apr 07 '22 at 07:36
3

There is (as far as I know) no simple way, because you cannot inherit decorators.

The simplest solution I can imagine is:

globals_ = globals()
for name, cls in globals_.items():
    if subclass(cls, Base):
        globals_[name] = decorator(cls)

It simply iterates over every global variable already defined in current module, and if it happens to be class inheriting from Base (or Base itself), it decorates it with decorator.

Note that subclass will not be decorated if:

  • it's created after this snippet,
  • it's created in another module,
  • it's not defined in global namespace.

Alternatively, you can use metaclass:

class Decorate(type):
    def __new__(mcls, name, bases, attrs):
        return decorator(super().__new__(name, bases, attrs))

class Base(metaclass=Decorate):
    pass

When you write class Base(metaclass=Decorate):, Python uses Decorate to create Base and its subclasses.

All that Decorate does, is to decorate class using decorator before returning it.

If you use this, you will probably have a problem if you try to inherit from 2 (or more) classes, each with different metaclass.

GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
1

I used the answer by @GingerPlusPlus, and created the following function, to apply a decorator to all subclasses of a class:

 def apply_decorator_to_all_subclasses(globals_, base_class, decorator):
    """
    Given a 'globals_' dictionary, a base class, and a decorator - this function applies the decorator to all the defined classes that derive from the base class
    Note!: this function should be called only *after* the subclassess were declared
    :param globals_: the given output of globals(), in the caller's context
    :param base_class: the class whose descendants require the decorator
    :param decorator: the decorator to apply
    """
    for name, cls in globals_.items():
        # Applying only on *class* items, that are descandants of base_class
        if inspect.isclass(cls) and issubclass(cls, base_class) and cls != base_class:
            globals_[name] = decorator(cls)
o_c
  • 3,965
  • 1
  • 22
  • 22