Home > OS >  Pass parent attribute in metaclass
Pass parent attribute in metaclass

Time:02-14

The metaclass is defined as below:

class MyMeta(type):
    def __new__(metacls, name, bases, namespace, **kwargs):
        # uses attribute a,b,c,d
        a = namespace["a"]
        b = namespace["b"]
        c = namespace["c"]
        d = namespace["d"]
        ...
        return super().__new__(metacls, name, bases, namespace, **kwargs)

This metaclass uses attributes a, b, c, d. Now classes can be created with this metaclass as:

class A(metaclass=MyMeta):
    a = "etc/dev"
    b = 2
    c = "c"
    d = "d"

class B(metaclass=MyMeta):
    a = "etc/dev/null"
    b = 2
    c = "c"
    d = "d"

The attribute a is variable for each class. I want to extract the constant attributes in a base class to avoid duplication:

class BaseClass:
    b = 2
    c = "c"
    d = "d"

and create classes using the metaclass as:

class A(BaseClass, metaclass=MyMeta):
    a = "/etc/dev"

class B(BaseClass, metaclass=MyMeta):
    a = "/etc/dev/null"

The metaclass complains that it can not find attribute b from parent class.

test_mode = namespace["b"]
KeyError: 'b'

Is there a way to pass attributes from parent to a child metaclass ?

Constraint: Can not update the MyMeta metaclass as it's been borrowed/imported it from an existing library.

CodePudding user response:

You can do it by making the metaclass __new__() method also check the namespaces of all the base classes of the class being defined. Here's one way of doing it. Note that it skips attributes in the namespaces that have dunder identifiers.

Since you can't change the meta class you "borrowed" and custom one is derived from it. This revised version also uses the collection.ChainMap class to make all the namespaces be considered without actually combining them all.

from collections import ChainMap
import re

class BorrowedMeta(type):
    def __new__(metacls, name, bases, namespace, **kwargs):
        # uses attribute a,b,c,d
        a = namespace["a"]
        b = namespace["b"]
        c = namespace["c"]
        d = namespace["d"]
        ...
        return super().__new__(metacls, name, bases, namespace, **kwargs)


def is_dunder(name):
    """Determine if name is a Python dunder identifier."""
    return re.match(r'^__[^\d\W]\w*\Z__$', name, re.UNICODE)


class MyMeta(BorrowedMeta):
    def __new__(metacls, name, bases, namespace, **kwargs):
        chainmap = ChainMap(*[namespace]  
                             [{k: v for (k, v) in vars(base).items() if not(is_dunder(k))}
                                                    for base in bases])

        return super().__new__(metacls, name, bases, {**chainmap}, **kwargs)


class BaseClass:
    b = 2
    c = "c"
    d = "d"

class A(BaseClass, metaclass=MyMeta):
    a = "/etc/dev"

class B(BaseClass, metaclass=MyMeta):
    a = "/etc/dev/null"


from pprint import pprint
pprint(vars(A))
print()
pprint(vars(B))

Output:

mappingproxy({'__doc__': None,
              '__module__': '__main__',
              'a': '/etc/dev',
              'b': 2,
              'c': 'c',
              'd': 'd'})

mappingproxy({'__doc__': None,
              '__module__': '__main__',
              'a': '/etc/dev/null',
              'b': 2,
              'c': 'c',
              'd': 'd'})

CodePudding user response:

No, you can't do what you want without changing the metaclass's behavior.

The problem is that the namespace argument that MyMeta.__new__ is looking in contains only the values defined in the class statement for the new class. It doesn't contain anything inherited, since the class and it's bases haven't been combined yet (that's what MyMeta.__new__ does when it calls super().__new__ at the bottom of its implementation, after it has done its thing with the a/b/c variables).

  • Related