2

An upstream interface was given to me with all of its functions defined as non-abstract when in reality they should be decorated with @abstractmethods. I want to receive an error when I did not implement one of its functions when it's called. To do this, I would create a wrapper class and manually go through each of its defined functions and do something like this:

from abc import ABC, abstractmethod

class Foo(object):
    def foo(self):
        print("Foo")

class AbstractFoo(Foo, ABC):
    @abstractmethod
    def foo(self):
        return super().foo()

class ConcreteFoo(AbstractFoo):
    def foo(self):
        print("Concrete Foo")
        super().foo()

f = ConcreteFoo()
f.foo()

Which outputs:

Concrete Foo
Foo

I would like some way of just doing this to all functions defined by Foo. Obviously, inherited magic functions like __str__ and __repr__ should be forwarded appropriately.

Does anyone know a nice, pythonic way of doing this?

OneRaynyDay
  • 3,658
  • 2
  • 23
  • 56

2 Answers2

2
def validate_base_class_implemntation(cls):
    base_cls_funcs = []
    for attr in cls.__bases__[0].__dict__:
        if callable(getattr(cls, attr)):
            base_cls_funcs.append(attr)

    cls_funcs = []
    for attr in cls.__dict__:
        if callable(getattr(cls, attr)):
            cls_funcs.append(attr)

    missing_funcs = [x for x in base_cls_funcs if x not in cls_funcs]

    if len(missing_funcs) > 0:
        print("Not implemented functions are: {}".format(','.join(missing_funcs)))
        raise Exception("Not implement function exception!")

    return cls


class Foo(object):
    def foo(self):
        print("Foo")

    def boo(self):
        print("Wow")


@validate_base_class_implemntation
class ConcreteFoo(Foo):
    def foo(self):
        print("Concrete Foo")
        super().foo()


f = ConcreteFoo()
f.foo()

Not sure in 100% if that what you meant.

this decorator checks that the class decorated implements all the base class function(in your case, they are not decorated with abstract). if there is a function that your decorated class does not implement, it raises exception.

nonamer92
  • 1,887
  • 1
  • 13
  • 24
  • This works for me! Thanks so much for this implementation. I wonder if there's a more pythonic way to do this, but this is definitely sufficient for me :) cheers! – OneRaynyDay Nov 18 '19 at 18:21
  • @OneRaynyDay I'll think about it – nonamer92 Nov 18 '19 at 19:21
  • 1
    @OneRaynyDay More pythonic: (1) Using `set` instead of `list` for the funcs, then `missing_funcs = base_cls_funcs - cls_funcs`, (2) Checking `if missing_funcs` is sufficient, (3) Using `cls.mro()[1]` instead of `cls.__bases__[0]`, (4) Using `vars(...)` instead of `__dict__`, (5) Using a comprehension instead of the two `for` loops, (6) Raise a `TypeError` instead of `Exception`. Also `callable` will include non-function objects so `isinstance(..., types.FunctionType)` is more appropriate. – a_guest Nov 18 '19 at 21:03
1

You can modify the original class Foo and turn all its methods into abstract methods and then define a blank subclass of Foo with metaclass=ABCMeta in order to handle the checks:

from abc import ABCMeta, abstractmethod
from types import FunctionType


class AbstractFoo(Foo, metaclass=ABCMeta):
    pass


names = set()
for k, v in vars(Foo).items():
    if k.startswith('__') and k.endswith('__'):
        continue
    elif isinstance(v, FunctionType):
        names.add(k)
        v.__isabstractmethod__ = True

AbstractFoo.__abstractmethods__ = frozenset(names)

Side note: This approach relies on dunder attributes being used by abc and as such can break without deprecation.

a_guest
  • 34,165
  • 12
  • 64
  • 118
  • Thanks for this implementation! In the problem statement I should've mentioned that `Foo` may not be directly inheriting `object`, which means there may be non-dunders that I don't want to make abstract. This would apply to every function. I have upvoted your answer though! Apologies for the incomplete specification – OneRaynyDay Nov 18 '19 at 18:17
  • @OneRaynyDay I'm not sure I understand what you mean. `vars(Foo)` checks only the *immediate* attributes of `Foo` excluding any base classes. In case `Foo` itself defines methods that you don't want to turn abstract then, for any algorithm, you need to provide some specification for them. – a_guest Nov 18 '19 at 20:53