Forced implementation of a method in all inheriting classes

I have a situation where I want to enforce every class that inherits from a certain (abstract) class to implement a method. This is what I would normally achieve with @abstractmethod. However, given this multiple inheritance situation:

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 apply C

to implement a method. I want every class that inherits A to be forced to implement this method directly or indirectly. Is it possible?

Clarification . I want this to be applicable to a specific method, not all abstract methods. abstract methods should continue to work the same way, but maybe a new decorator should be created to signal different methods.

Side note: I used abc in the question because it seems to be related to the problem. I understand how abstract methods usually work and use them regularly. This is a completely different situation and I don't mind if it is not done with abc.

+3


source to share


2 answers


A modified version of ABCMeta should do the trick.

Here, instead of checking methods from __isabstractmethod__

to True

only in base classes, we can check that it is in the MRO class, and if it is found in any of the class in MRO and it is not in the current class, then we can add that 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 custom decorator @enforcedmethod

that can satisfy 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

      

+3


source


I'm sure this isn't a great idea, but I think you can do it. Checking 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__))

      

+3


source







All Articles