Force implementation of a method in all inheriting classes
Asked Answered
B

2

7

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.

Bellerophon answered 3/9, 2014 at 17:2 Comment(5)
Maybe you could throw a NotImplementedException in the base class.Equitant
@Equitant -- Nope, because B's very_specific_method doesn't call the base class.Questioning
My bad, I didn't notice C was extending B and not A. You can check if a method was implemented in a class with 'very_specific_method' in vars(C). I don't know much about metaclasses but using them you can check for that during the class creation.Equitant
Whether it is possible or not I don't know. However, wanting to enforce this implementation, you are defeating the inheritance paradigm where B inherits from A means "B is an A". B is no more an A if it cannot do its very_specific_method the same way A does.Sexy
@DidierTrosset C, B and A could all implement this method and this would still be proper inheritance. The only difference is in enforcing this. In any case, and respectfully, OK, so I'm breaking the inheritance paradigm.Bellerophon
H
3

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
Holocene answered 3/9, 2014 at 18:15 Comment(4)
Thanks for the answer. But could you please look at the clarification added to the question? I'm looking for a way which doesn't mess up abstractmethod, but rather adds a different kind of method.Bellerophon
@Korem Check the edit, may be that's what you're looking for.Holocene
Why return super(ABCMeta, cls).__call__(*args, **kwargs) and not return super(EditedABCMeta, cls).__call__(*args, **kwargs)?Bellerophon
@Korem Good catch, as ABCMeta has no __call__ method we can definitely change that.Holocene
Q
3

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__))
Questioning answered 3/9, 2014 at 17:21 Comment(1)
__abstractmethods__ and AttributeError was what I was thinking of.Mongolic
H
3

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
Holocene answered 3/9, 2014 at 18:15 Comment(4)
Thanks for the answer. But could you please look at the clarification added to the question? I'm looking for a way which doesn't mess up abstractmethod, but rather adds a different kind of method.Bellerophon
@Korem Check the edit, may be that's what you're looking for.Holocene
Why return super(ABCMeta, cls).__call__(*args, **kwargs) and not return super(EditedABCMeta, cls).__call__(*args, **kwargs)?Bellerophon
@Korem Good catch, as ABCMeta has no __call__ method we can definitely change that.Holocene

© 2022 - 2024 — McMap. All rights reserved.