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).