Mojimi
Mojimi

Reputation: 3161

Make subclasses automatically call superclass method

The context here is simple, I'm creating a base class that other developers will subclass it and they're supposed to implement the test method.

Now I don't want them to call super().test() on their implementation, how can I do this?

Bellow is what I've tried, but it does not work.

class Base():
  def __init__(self):
    self._test()

  def _test(self):
    try:
      super()._test()
    except AttributeError:
      pass
    self.test()

  def test(self):
    print('Base')

class One(Base):
  def test(self):
    print('One')

class Two(One):
  def test(self):
    print('Two!')

two = Two()

What I want is the code above to print

Base
One
Two!

without One or Two calling super().

I understand a solution is using __init_subclass_ and keep an array of methods, but that seems way too hacky

Upvotes: 1

Views: 181

Answers (1)

Serge Ballesta
Serge Ballesta

Reputation: 149085

If you only want to force an automatic call of some methods from the base class, __init_subclass__ is indeed the way to go. You just override the required methods there. I would use something close to:

from functools import update_wrapper
from inspect import signature


class Base:
    # will be overriden in subclasses
    _SPECIAL_METHODS = ['test', 'test2']
    def test(self):
        print('Base')
    def test2(self, i):
        print(f'{i} in Base')
    def __init_subclass__(cls):
        def do_wrap(cls, method):
            orig = getattr(cls, method)
            # ignore methods not explicitely defined in cls
            if not orig.__qualname__.startswith(cls.__qualname__): return
            def wrapper(*args, **kwargs):
                # first calls the method from Base with passed arguments
                getattr(Base, method)(*args, **kwargs)
                # then the one from the subclass
                return orig(*args, **kwargs)
            # keep the original signature, docstring, etc.
            wrapper.__signature__ = signature(orig)
            setattr(cls, method, update_wrapper(wrapper, orig))
        for i in Base._SPECIAL_METHODS:
            do_wrap(cls, i)


class One(Base):
    def test(self):
        # super().test()
        print('One')
    def test2(self, i):
        print(f'{i} in One')

class Two(One):
    def test2(self, j):
        print('In Two:', j)

It gives:

>>> a = One()
>>> a.test()
Base
One
>>> a.test2(2)
2 in Base
2 in One
>>> b = Two()
>>> b.test()
Base
One
>>> b.test2(5)
5 in Base
In Two: 5

This example calls the base method with the arguments passed to the child class one, but this would be easy to adapt.

Upvotes: 2

Related Questions