1

Let's assume I have a class that is used to perform I/O operations:

class CommunicationStack:
    def __init__(self, socket):
        self.socket = socket

    def sync_get_data(self):
        ...

    def sync_send_data(self):
        ...

    async def async_get_data(self):
        ...

    async def async_send_data(self):
        ...

As you can see it has sync and async variant for the same operations but it would be slightly inconvenient to write async_get_data or sync_get_data manually. I am looking for a smart way to have the same interface like

def get_data(self):
    ...  # call sync variant or return an awaitable, depending on caller type

def send_data(self):
    ...  # call sync variant or return an awaitable, depending on caller type

so it can be conveniently used like:

stack = CommunicationStack(...)

def thread_procedure():
    data = stack.get_data()  # get_data returns data

async def task_procedure():
    data = await stack.get_data()  # get_data returns an awaitable

I believe it can be done in some tricky way with inspections or some black magic:

def is_caller_coroutine():
    return sys._getframe(2).f_code.co_flags & 0x380

to check if caller is a coroutine or a function but it seems like a bad design to mess with python's guts.


The question is: what is a good way to choose appropriate variant? Or is there a better way to design everything like using adapters or developing two independent AsyncCommunicationStack and SyncCommunicationStack classes?

asikorski
  • 882
  • 6
  • 20
  • 2
    David Beazley uses a more complete version of his **black magic** in `curio`'s `meta.py`: [see `from_coroutine()` here](https://github.com/dabeaz/curio/blob/e2c6ee29d1e988a216e4b76f9d3e3709f8738a36/curio/meta.py#L54) – Jean Monet Apr 25 '20 at 20:48

1 Answers1

4

If you want to call async functions same way as regular ones, you may be interested in using gevent.

asyncio wants you to explicitly mark functions as async and to explicitly use await everywhere async stuff happens. In other words asyncio wants you to have different interfaces for sync and async code.

This is intentional decision made to fight with concurrency problems which is much harder to achieve when async nature of a code is hidden (like in gevent).

So yes - two different independent AsyncCommunicationStack and CommunicationStack classes is a way to go if you want to support both worlds. Although once you have async version you can cast write critical code with it and make it sync just running it with asyncio.run(). Problem only is that you won't be able to return to async world after that.

Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • 1
    Thank you for the answer. I decided to write separate classes for async and sync communication. Dealing with two different *worlds* in one class would be a mess. – asikorski Jun 10 '19 at 22:45