python-trio / unasync

The async transformation code.
Other
91 stars 13 forks source link

Provide common replacement utils / functions many projects will need #65

Open sethmlarson opened 4 years ago

sethmlarson commented 4 years ago

Some utility functions I was thinking about:

def azip(*iterables):  # Convert any iterables and async iterables into a zipped async iterable
    iterators = [aiter(x) for x in iterables]

    async def generator():
        while True:
            try:
                yield tuple([await x.__anext__() for x in iterators])
            except StopAsyncIteration:
                break

    return generator().__aiter__()

def aiter(x):  # Convert any iterable or async iterable into an async iterator
    if hasattr(x, "__aiter__"):
        return x.__aiter__()
    elif hasattr(x, "__anext__"):
        return x

    async def aiter_wrapper():
        for item in x:
            yield item

    return aiter_wrapper().__aiter__()

async def anext(i):  # Advance an async iterator, good for mapping to `next()`
    return await i.__anext__()

async def await_or_return(x):  # Maps to 'return_()'?
    if iscoroutine(x):
        return await x
    return x

# All of the sync variations of the above functions
next = next
iter = iter
zip = zip
def return_(x):
    return x

Could also have azip_longest -> zip_longest, etc.

All of the async functions could be available only on Python 3.6+ so this could be used on projects that support 3.5 or less.

Could also provide type hints that are effected by unasync properly as well.

AsyncOrSyncIterable[X] = Union[AsyncIterable[X], Iterable[X]]
SyncIterable[X] = Iterable[X]

This should probably be a separate project to unasync as unasync is primarily a build tool, raising it here because it'd make sense for the tool to live under python-trio, maybe unasync-utils?

pquentin commented 4 years ago

Thanks for opening this issue!

I'm a bit surprised here because my understanding is that unasync exists specifically so that you don't need functions like await_or_return. Instead return await x in the async version of a package becomes return x in the sync version.

But since you know that, what am I missing here?

sethmlarson commented 4 years ago

I'm imaging a situation where your API takes a callable from the user and you'd like to support both Union[Callable[[...], X], Callable[[...], Awaitable[X]]] on the async implementation but only support Callable[[...], X] on the sync side. To do that you'd need a conditional await I think?

Maybe await_if_coro() or something like this is a better name for this interface?

sethmlarson commented 4 years ago

An example where this would be useful in Hip: When given a body that has read() we want to call await f.read() for async files and f.read() on sync files.

sethmlarson commented 4 years ago

Also anext() would be useful in a similar place in Hip for generating the async iterators of bytes to send.

pquentin commented 4 years ago

Right, this is needed when your API accepts both async and sync callables. Even though unasync is only a build dependency, I don't really mind adding utilities like that and making it useful as a runtime dependency too