8

How can I require that an abstract base class implement a specific method as a coroutine. For example, consider this ABC:

import abc

class Foo(abc.ABC):
    @abc.abstractmethod
    async def func():
        pass

Now when I subclass and instantiate that:

class Bar(Foo):
    def func():
        pass

b = Bar()

This succeeds, although func is not async, as in the ABC. What can I do so that this only succeeds if func is async?

Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
  • Possible duplicate of [Test if function or method is normal or asynchronous](https://stackoverflow.com/questions/36076619/test-if-function-or-method-is-normal-or-asynchronous) – Elis Byberi Nov 29 '17 at 15:10
  • 1
    That question is about how to test, which is only part of the solution. I want to do this using an abstract base class. – Björn Pollex Nov 29 '17 at 15:13
  • You have to define `async def func()` again in class `Bar`. `@abc.abstractmethod` does not take in considerate if `func()` is async or not. – Elis Byberi Nov 29 '17 at 15:20
  • Yes, I understand that. I'm asking if there is a way to make this work, short of writing a custom meta class. – Björn Pollex Nov 29 '17 at 15:21
  • Testing function `func()` inside class `__init__()` is a way but it is not what you want! – Elis Byberi Nov 29 '17 at 15:28
  • Python doesn't check the signature of overridden method. You can add new parameter or change return type. It's discouraged but possible. *sync*/*async* dichotomy adds nothing new -- controlling overridden signature is up to user. – Andrew Svetlov Nov 30 '17 at 11:28

1 Answers1

6

You may use __new__ and check if and how a child class has override parent's coros.

import asyncio
import abc
import inspect


class A:    

    def __new__(cls, *arg, **kwargs):
        # get all coros of A
        parent_coros = inspect.getmembers(A, predicate=inspect.iscoroutinefunction)

        # check if parent's coros are still coros in a child
        for coro in parent_coros:
            child_method = getattr(cls, coro[0])
            if not inspect.iscoroutinefunction(child_method):
                raise RuntimeError('The method %s must be a coroutine' % (child_method,))

        return super(A, cls).__new__(cls, *arg, **kwargs)

    @abc.abstractmethod
    async def my_func(self):
        pass


class B(A):

    async def my_func(self):
        await asyncio.sleep(1)
        print('bb')


class C(A):

    def my_func(self):
        print('cc')

async def main():
    b = B()
    await b.my_func()

    c = C()  # this will trigger the RuntimeError
    await c.my_func()


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Caveats

  • a child class may override __new__ as well to suppress this constraint
  • not only async may be awaited. For example

    async def _change_in_db(self, key, value):
        # some db logic
        pass
    
    def change(self, key, value):
        if self.is_validate(value):
            raise Exception('Value is not valid')
        return self._change_in_db(key, value)  
    

    it's ok to call change like

    await o.change(key, value)
    

    Not to mention __await__ in objects, other raw Futures, Tasks...

kwarunek
  • 12,141
  • 4
  • 43
  • 48
  • Thanks, that's very useful, both the solution and the caveats! – Björn Pollex Nov 30 '17 at 09:31
  • I think it's important to add that passing `*args` and `**kwargs` to `__new__` instead of `__init__` isn't recommended. as Python 3.3+ will most likely throw an error if you do so. – Charming Robot Nov 23 '18 at 07:32
  • AFAIR https://docs.python.org/3/reference/datamodel.html#object.`__new__` can accept arguments other than the first class (3.3+ doesn't throw any error), `__new__` is a function like any other so why wouldn't is work. The `__init__` is initlializer of an object, while here is the approach that uses class creation - such a different things. – kwarunek Nov 23 '18 at 08:51