I have a situation in which I want to enforce each and every class inheriting from a certain (abstract) class to implement a method. This is something I would normally achieve using @abstractmethod. However, considering this situation of multiple inheritance:
from abc import ABCMeta, abstractmethod
class A(object):
__metaclass__ = ABCMeta
@abstractmethod
def very_specific_method(self):
pass
class B(A):
def very_specific_method(self):
print 'doing something in B'
class C(B):
pass
I want to enforce C
to implement the method as well. I want each and every class that inherits A either directly or indirectly to be forced to implement the method. Is this possible?
Clarification: I want this to apply for a specific method, not to all abstract methods. abstract methods should continue to work the same, but perhaps a new decorator signaling a different kind of methods should be created.
Side note: I used abc in the question because this seems like the most related to the issue. I understand how abstract methods usually work and use them regularly. This is a different situation, and I don't mind if it's not done via abc.
A modified version of ABCMeta should do the trick.
Here instead of checking for methods with __isabstractmethod__
set to True
only in base classes we can check for this is in class's MRO, and if it is found in any of the class in MRO and it is not present in current class then we can add this to the set abstracts
.
from abc import ABCMeta, abstractmethod
from _weakrefset import WeakSet
class EditedABCMeta(ABCMeta):
def __new__(mcls, name, bases, namespace):
cls = type.__new__(mcls, name, bases, namespace)
# Compute set of abstract method names
abstracts = set(name
for name, value in namespace.items()
if getattr(value, "__isabstractmethod__", False))
for base in cls.__mro__:
for name, value in base.__dict__.items():
if getattr(value, "__isabstractmethod__", False) and name not in cls.__dict__:
abstracts.add(name)
cls.__abstractmethods__ = frozenset(abstracts)
# Set up inheritance registry
cls._abc_registry = WeakSet()
cls._abc_cache = WeakSet()
cls._abc_negative_cache = WeakSet()
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
return cls
class A(object):
__metaclass__ = EditedABCMeta
@abstractmethod
def veryspecificmethod(self):
pass
class B(A):
def veryspecificmethod(self):
print 'doing something in B'
@abstractmethod
def foo(self):
print 'foo from B'
class C(B):
def foo(self):
pass
class D(C, B):
pass
if __name__ == '__main__':
for cls in (C, D):
try:
cls().veryspecificmethod
except TypeError as e:
print e.message
print '-'*20
for cls in (C, D):
try:
cls().foo
except TypeError as e:
print e.message
Output:
Can't instantiate abstract class C with abstract methods veryspecificmethod
Can't instantiate abstract class D with abstract methods foo, veryspecificmethod
--------------------
Can't instantiate abstract class C with abstract methods veryspecificmethod
Can't instantiate abstract class D with abstract methods foo, veryspecificmethod
EDIT:
Adding a special decorator @enforcedmethod
that can meet your requirements without affecting @abstractmethod
:
from abc import ABCMeta, abstractmethod
def enforcedmethod(func):
func.__enforcedmethod__ = True
return func
class EditedABCMeta(ABCMeta):
def __call__(cls, *args, **kwargs):
enforcedmethods = set()
for base in cls.__mro__:
for name, value in base.__dict__.items():
if getattr(value, "__enforcedmethod__", False) and name not in cls.__dict__:
enforcedmethods.add(name)
if enforcedmethods:
raise TypeError("Can't instantiate abstract class {} "
"with enforced methods {}".format(
cls.__name__, ', '.join(enforcedmethods)))
else:
return super(EditedABCMeta, cls).__call__(*args, **kwargs)
class A(object):
__metaclass__ = EditedABCMeta
@enforcedmethod
def veryspecificmethod(self):
pass
@abstractmethod
def simplemethod(self):
pass
class B(A):
def veryspecificmethod(self):
print 'doing something in B'
def simplemethod(self):
pass
class C(B):
pass
class D(C):
def veryspecificmethod(self):
print 'doing something in D'
Output:
>>> D().veryspecificmethod()
doing something in D
>>> C().veryspecificmethod()
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
C().veryspecificmethod()
File "C:\Python27\so.py", line 19, in __call__
cls.__name__, ', '.join(enforcedmethods)))
TypeError: Can't instantiate abstract class C with enforced methods veryspecificmethod
I'm pretty sure that this isn't a great idea, but I think that you can do this. Checking out the ABCMeta
implementation for inspiration:
from abc import ABCMeta
def always_override(func):
func._always_override = True
return func
class always_override_property(property):
_always_override = True
class CrazyABCMeta(ABCMeta):
def __new__(mcls, name, bases, namespace):
cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
abstracts = set()
# first, get all abstracts from the base classes
for base in bases:
abstracts.update(getattr(base, "_all_always_override", set()))
all_abstracts = abstracts.copy()
# Now add abstracts from this class and remove abstracts that this class defines
for name, value in namespace.items():
always_override = getattr(value, '_always_override', False)
if always_override:
abstracts.add(name)
all_abstracts.add(name)
elif name in abstracts:
abstracts.remove(name)
cls._all_always_override = frozenset(all_abstracts)
cls._always_override = frozenset(abstracts)
return cls
def __call__(cls, *args, **kwargs):
if cls._always_override:
raise TypeError(
'The following methods/properties must '
'be overridden {}'.format(cls._all_always_override))
return super(CrazyABCMeta, cls).__call__(*args, **kwargs)
# # # # # # # # # # #
# TESTS!
# # # # # # # # # # #
class A(object):
__metaclass__ = CrazyABCMeta
@always_override
def foo(self):
pass
@always_override_property
def bar(self):
pass
class B(A):
def foo(self):
pass
bar = 1
class C(B):
pass
class D(C):
pass
class E(D):
def foo(self):
pass
@property
def bar(self):
return 6
for cls in (B, E):
cls()
print ("Pass {}".format(cls.__name__))
for cls in (C, D):
try:
print cls()
except TypeError:
print ("Pass {}".format(cls.__name__))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With