David Callanan
David Callanan

Reputation: 5968

Python - Accessing parent class decorators inside class declaration

Let's say I have this class:

class Foo:
    @classmethod
    def some_decorator(cls, ...):
        ...

And then I create a subclass which uses the parent class decorator:

class Bar(Foo):
    @Foo.some_decorator(...)
    def some_function(...)
        ...

How do I remove the need for Foo. before the decorator name? The below code doesn't work:

class Bar(Foo):
    @some_decorator(...)
    def some_function(...)
        ...

I believe this is possible, because the sly library does this.

See their example:

from sly import Lexer, Parser

class CalcLexer(Lexer):
    ...

    @_(r'\d+')
    def NUMBER(self, t):
        t.value = int(t.value)
        return t

    ...

As you can see, you can type in @_(...) instead of @Lexer._(...).

How do they accomplish this?

Upvotes: 2

Views: 194

Answers (2)

Aran-Fey
Aran-Fey

Reputation: 43166

This is done with a metaclass that implements a __prepare__ method. Excerpt from the docs:

3.3.3.4. Preparing the class namespace

Once the appropriate metaclass has been identified, then the class namespace is prepared. If the metaclass has a __prepare__ attribute, it is called as namespace = metaclass.__prepare__(name, bases, **kwds) (where the additional keyword arguments, if any, come from the class definition).

To put it in simple terms: You make your __prepare__ method return a dictionary that contains an entry for the decorator. Proof of concept:

class MyMeta(type):
    def __prepare__(name, bases):
        return {'x': 'foobar'}

class MyClass(metaclass=MyMeta):
    print(x)  # output: foobar

Upvotes: 3

Alexandru Martin
Alexandru Martin

Reputation: 236

I have looked inside the library you are talking about and the Lexer class inherits a metaclass:

class Lexer(metaclass=LexerMeta):

Inside the LexerMeta you can find the following:

@classmethod
    def __prepare__(meta, name, bases):
        d = LexerMetaDict()

        def _(pattern, *extra):
            patterns = [pattern, *extra]
            def decorate(func):
                pattern = '|'.join(f'({pat})' for pat in patterns )
                if hasattr(func, 'pattern'):
                    func.pattern = pattern + '|' + func.pattern
                else:
                    func.pattern = pattern
                return func
            return decorate

        d['_'] = _
        d['before'] = _Before
        return d

A metaclass is used to create the class object which then is used to instantiate objects. From what i can see in that method is that here d['_'] = _ that metaclass dynamically attaches the _ method to the class you are going to use.

This means that what they are doing is not much different from:

class Bar:
    @staticmethod
    def some_decorator(f):
        ...

    @some_decorator
    def some_function(self):
        ...

Upvotes: 3

Related Questions