SJ Palt SJ Palt - 16 days ago 9
Python Question

Overriding __bases__ in a metaclass

Is it possible to override the

__bases__
field of a metaclass (i.e. class deriving from
type
) using a get-set property? The following code works for getting
C.__bases__
, but not setting it:

class Meta(type):
@property
def __bases__(cls):
print('Getting __bases__ via Meta.__bases__')
return super().__bases__

@__bases__.setter
def __bases__(cls, value):
print('Setting __bases__ via Meta.__bases__')
super().__bases__ = value

class A: pass
class B: pass
class C(A, B, metaclass=Meta): pass

# >>> C.__bases__
# Getting __bases__ via Meta.__bases__
# (<class '__main__.A'>, <class '__main__.B'>)
# >>> C.__bases__ = (B, A)
# Setting __bases__ via Meta.__bases__
# AttributeError: 'super' object has no attribute '__bases__'


I've tried a few substitutions for
super()
in the setter function, but none of them work:

type.__setattr__(cls, '__bases__', value)
leads to recursion.

object.__setattr__(cls, '__bases__', value)
gives
TypeError: can't apply this __setattr__ to type object


So, what this boils down to is how to set the
cls.__bases__
field when it is
shadowed by a property on the metaclass. Any ideas?




(Yes, I am aware that defining the
__bases__
property has no effect on the actual
__mro__
of the class, although that can be arranged by overriding
mro()
)

Answer

super() doesn't support data descriptors, only plain descriptors, as only super().__get__ is implemented.

Put differently, the assignment

super().__bases__ = value

fails because the super() proxy object does not implement the descriptor.__set__() method, and thus that assignment tries to set __bases__ as an attribute on that proxy object.

You'd have to manually access the descriptor on the type object:

class Meta(type):
    @property
    def __bases__(cls):
        print('Getting __bases__ via Meta.__bases__')
        return type.__dict__['__bases__'].__get__(cls)

    @__bases__.setter
    def __bases__(cls, value):
        print('Setting __bases__ via Meta.__bases__')
        return type.__dict__['__bases__'].__set__(cls, value)

For symmetry's sake, I've used the same manual descriptor access in the getter, although super().__bases__ would work too.

Above, I hardcoded type rather than search the MRO; you could also use a helper function to find the right descriptor with a full MRO search:

class Meta(type):
    def _find_super(cls, name):
        mro = type(cls).__mro__
        idx = mro.index(__class__)
        for base in mro[idx + 1:]:
            if name in base.__dict__:
                return base.__dict__[name]
        return

    @property
    def __bases__(cls):
        print('Getting __bases__ via Meta.__bases__')
        return cls._find_super('__bases__').__get__(cls)

    @__bases__.setter
    def __bases__(cls, value):
        print('Setting __bases__ via Meta.__bases__')
        return cls._find_super('__bases__').__set__(cls, value)

Either way, now you can intercept __bases__ being set:

>>> class A: pass
...
>>> class B: pass
...
>>> class C(A, B, metaclass=Meta): pass
...
>>> C.__bases__
Getting __bases__ via Meta.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
>>> C.__bases__ = (B, A)
Setting __bases__ via Meta.__bases__
>>> C.__bases__
Getting __bases__ via Meta.__bases__
(<class '__main__.B'>, <class '__main__.A'>)