BlockingPortalProvider fails on 2nd call with RuntimeError: Event loop is closed
#743
|
from __future__ import annotations
import trio
import anyio
from ipaddress import IPv4Address
import httpx
from anyio.from_thread import BlockingPortalProvider
class AsyncAPI:
_client: httpx.AsyncClient
def __init__(self):
self._client = httpx.AsyncClient()
@property
def sync(self) -> SyncAPI:
return SyncAPI(self)
async def get_ipaddress(self) -> IPv4Address:
url = "https://ifconfig.me/ip"
response = await self._client.get(url)
response.raise_for_status()
return IPv4Address(response.text)
class SyncAPI:
_portal_provider: BlockingPortalProvider
_async_api: AsyncAPI
def __init__(self, api: AsyncAPI):
self._portal_provider = BlockingPortalProvider()
self._async_api = api
def get_ipaddress(self) -> IPv4Address:
with self._portal_provider as portal:
return portal.call(self._async_api.get_ipaddress)
api = AsyncAPI()
print('#'*80)
print('# First Call')
print('#'*80)
ipaddress = api.sync.get_ipaddress()
print(repr(ipaddress))
print()
print('#'*80)
print('# Second Call')
print('#'*80)
ipaddress = api.sync.get_ipaddress()
print(repr(ipaddress)) |
Replies: 10 comments 28 replies
|
I've opened a discussion as it's likely user error, but I'm not sure what I'm doing wrong above? |
|
Note It does seem to work fine if I instead use |
|
Ideally, you should have the provider as a global, so that other instances of your classes can take advantage of it. If you have a private instance, then it may be pointless to use the provider vs just opening a regular blocking portal. |
|
AFAICT this usage mirrors the example in the docs: from anyio.to_thread import BlockingPortalProvider
class MyAPI:
def __init__(self, async_obj) -> None:
self._async_obj = async_obj
self._portal_provider = BlockingPortalProvider()
def do_stuff(self) -> None:
with self._portal_provider as portal:
portal.call(async_obj.do_async_stuff)https://anyio.readthedocs.io/en/stable/threads.html#sharing-a-blocking-portal-on-demand |
|
If I make portal_provider = BlockingPortalProvider('asyncio')
class SyncAPI:
_async_api: AsyncAPI
def __init__(self, api: AsyncAPI):
self._async_api = api
def get_ipaddress(self) -> IPv4Address:
with portal_provider as portal:
return portal.call(self._async_api.get_ipaddress) |
|
storing a class SyncAPI:
_async_api: AsyncAPI
def __init__(self, api: AsyncAPI):
self._async_api = api
self._portal = portal_provider.__enter__()
def get_ipaddress(self) -> IPv4Address:
return self._portal.call(self._async_api.get_ipaddress)>>> api.sync.get_ipaddress()
IPv4Address('<redacted>')
>>> api.sync.get_ipaddress()
IPv4Address('<redacted>')
>>> api.sync.get_ipaddress()
IPv4Address('<redacted>')So, maybe I just need to manually create a |
|
Thanks for your help @agronholm. I've gone ahead and marked this as answered since I now know why it's happening and how it could be worked around. For now I'll use the trio backend as it appears to work out of the box so is the path of least resistance. I'll make sure to come back and warn against it if I live to rue that decision! 😅 |
|
Thanks again for all your help understanding the nuances! It has given me a lot to think about! 😅 |
|
xref: An example using |
|
Hello ! I seems this issue is the same as encode/httpcore#659 which snowballs to pydantic/pydantic-ai#748 Just wanted GitHub to keep a link between those related issues :) Also: if the same issue keeps beeing seen downstream, does it mean comething should change in anyio ?
I'm interested in an opinion of who did "wrong" (with all sorts of quotes) in the pydantic case according to anyio's knowledgeable folks: httpcore or pydantic ? (or the end user ? But I don't think so) |
If 1)
httpx.AsyncClientdefers the code path selection to the first use of the APIs, and 2) you never use both async and sync APIs from the same process, then you should be fine.