Search code examples
pythoninheritancemetaclass

Pass parent attribute in metaclass


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.


Solution

  • 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 filters out attributes in the namespaces that have dunder identifiers.

    Since you can't change the meta class you "borrowed" a custom one is derived from it. This revised version of my earlier answer 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'})