Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method chaining with asyncio coroutines

I want to implement method chaining, but not for usual functions - for asyncio coroutines.

import asyncio


class Browser:
    @asyncio.coroutine
    def go(self):
        # some actions
        return self

    @asyncio.coroutine
    def click(self):
        # some actions
        return self

"Intuitive" way to call chain wouldn't work, because single method returns coroutine (generator), not self:

@asyncio.coroutine
def main():
    br = yield from Browser().go().click()  # this will fail

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

Correct way to call chain is:

br = yield from (yield from Browser().go()).click()

But it looks ugly and becomes unreadable when chain grows.

Is there any way to do this better? Any ideas are welcome.

like image 629
Mikhail Gerasimov Avatar asked Apr 29 '26 04:04

Mikhail Gerasimov


1 Answers

I created solution, that do a job close to the needed. Idea is to use wrapper for Browser() which uses __getattr__ and __call__ to collect action (like getting attribute or call) and return self to catch next one action. After all actions collected, we "catch" yiled from wrapper using __iter__ and process all collected actions.

import asyncio


def chain(obj):
    """
    Enables coroutines chain for obj.
    Usage: text = yield from chain(obj).go().click().attr
    Note: Returns not coroutine, but object that can be yield from.
    """
    class Chain:
        _obj = obj
        _queue = []

        # Collect getattr of call to queue:
        def __getattr__(self, name):
            Chain._queue.append({'type': 'getattr', 'name': name})
            return self

        def __call__(self, *args, **kwargs):
            Chain._queue.append({'type': 'call', 'params': [args, kwargs]})
            return self

        # On iter process queue:
        def __iter__(self):
            res = Chain._obj
            while Chain._queue:
                action = Chain._queue.pop(0)
                if action['type'] == 'getattr':
                    res = getattr(res, action['name'])
                elif action['type'] == 'call':
                    args, kwargs = action['params']
                    res = res(*args, **kwargs)
                if asyncio.iscoroutine(res):
                    res = yield from res
            return res
    return Chain()

Usage:

class Browser:
    @asyncio.coroutine
    def go(self):
        print('go')
        return self

    @asyncio.coroutine
    def click(self):
        print('click')
        return self

    def text(self):
        print('text')
        return 5


@asyncio.coroutine
def main():
    text = yield from chain(Browser()).go().click().go().text()
    print(text)


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

Output:

go
click
go
text
5

Note, that chain() doesn't return real coroutine, but object that can be used like coroutine on yield from. We should wrap result of chain() to get normal coroutine, which can be passed to any asyncio function that requires coroutine:

@asyncio.coroutine
def chain_to_coro(chain):
    return (yield from chain)


@asyncio.coroutine
def main():
    ch = chain(Browser()).go().click().go().text()
    coro = chain_to_coro(ch)

    results = yield from asyncio.gather(*[coro], return_exceptions=True)
    print(results)

Output:

go
click
go
text
[5]
like image 115
Mikhail Gerasimov Avatar answered May 03 '26 17:05

Mikhail Gerasimov



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!