Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create_blocking returns a function that blocks when called from a coroutine function #141

Open
spencerwilson opened this issue Apr 11, 2024 · 1 comment

Comments

@spencerwilson
Copy link

spencerwilson commented Apr 11, 2024

Hello 👋

Setup

Adding some assert statements to the code in the 0.6.6. README:

import asyncio

from synchronicity import Synchronizer

synchronizer = Synchronizer()

@synchronizer.create_blocking
async def f(x):
    await asyncio.sleep(1.0)
    return x**2


# Running f in a synchronous context blocks until the result is available
assert type(f(42)) == int


async def g():
    # Running f in an asynchronous context works the normal way
    ret = f(42)
    assert type(await ret) == int

asyncio.run(g())

Expected behavior

  • No assertion errors.
  • f returns an int when called from a sync context
  • f returns an Awaitable[int] when called from a coroutine function

Actual behavior

Running this on synchronicity 0.6.6 in Python 3.11 one observes the following:

Traceback (most recent call last):
  File "/test.py", line 23, in <module>
    asyncio.run(g())
  File "/opt/homebrew/Cellar/[email protected]/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/[email protected]/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/[email protected]/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/test.py", line 20, in g
    assert type(await ret) == int
                ^^^^^^^^^
TypeError: object int can't be used in 'await' expression

More info

It seems that when calling a wrapped function from inside a coroutine function (in the example case: calling f from g), the wrapped function does not return a coroutine as suggested; instead the wrapped function blocks and returns the non-coroutine value. AFAICT it does this because it takes a code path that leads here:

value = fut.result()

The prose in the README also suggests that when calling a wrapped function from a coroutine function, the value returned should be an Awaitable:

When you call anything, it will detect if you're running in a synchronous or asynchronous context, and behave correspondingly.

In the synchronous case, it will simply block until the result is available (note that you can make it return a future as well, see later)
In the asynchronous case, it works just like the usual business of calling asynchronous code

It's curious because the README also documents how one can pass _future=True to a wrapped function to coax it into returning an awaitable. It's a bit confusing that this is opt in after having just read both the sample code (which throws the above TypeError) and the words "In the asynchronous case, it works just like the usual business of calling asynchronous code".

@spencerwilson
Copy link
Author

Another gotcha: When using _future=True the object returned is a concurrent.futures.Future, which is not an awaitable. This type is distinct from asyncio.Future, which is awaitable.

The former can in most cases be adapted into the latter using asyncio.wrap_future. The complete working code looks like:

import asyncio

from synchronicity import Synchronizer

synchronizer = Synchronizer()

@synchronizer.create_blocking
async def f(x):
    await asyncio.sleep(1.0)
    return x**2


# Running f in a synchronous context blocks until the result is available
assert type(f(42)) == int


async def g():
    # Running f in an asynchronous context works the normal way
    ret = asyncio.wrap_future(f(42, _future=True))
    assert type(await ret) == int

asyncio.run(g())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant