The problem here is that when abstract methods are calculated for a subclass of one or more abstract classes, only the subclass itself, rather than all the bases, is looked up upon to determine if an abstract method has been defined concretely.
Since we know from PEP-3119 that the names of the abstract methods of an abstract class are stored in the __abstractmethods__
attribute, and that only an abstract method has the __isabstractmethod__
attribute set to True
:
The @abstractmethod
decorator sets the function attribute
__isabstractmethod__
to the value True. The ABCMeta.__new__
method computes the type attribute __abstractmethods__
as the set of all
method names that have an __isabstractmethod__
attribute whose value
is true.
we can override ABCMeta.__new__
with additional logics after instantiating the class to check if methods that are deemed to have remained abstract by the original calculations can actually be found to be defined concretely in any of the base classes, in which case define references to them in the subclass, and remove them from __abstractmethods__
of the subclass:
class ComprehensiveABCMeta(abc.ABCMeta):
_NOTFOUND = object()
def __new__(metacls, name, bases, namespace, /, **kwargs):
cls = super().__new__(metacls, name, bases, namespace, **kwargs)
abstracts = set(cls.__abstractmethods__)
for name in cls.__abstractmethods__:
for base in bases:
value = getattr(base, name, cls._NOTFOUND)
if not (value is cls._NOTFOUND or
getattr(value, '__isabstractmethod__', False)):
setattr(cls, name, value)
abstracts.remove(name)
break
cls.__abstractmethods__ = frozenset(abstracts)
return cls
class ComprehensiveABC(metaclass=ComprehensiveABCMeta):
pass
so that:
class A(ComprehensiveABC):
@abc.abstractmethod
def x(self):
pass
def y(self):
return "A.y"
class B(ComprehensiveABC):
@abc.abstractmethod
def y(self):
pass
def x(self):
return "B.x"
class AB(B, A):
pass
ab = AB()
print(ab.x())
print(ab.y())
outputs:
B.x
A.y
Demo: https://ideone.com/jFMuII