Coverage for src/chat_limiter/utils.py: 85%
27 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-09-15 12:11 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-09-15 12:11 +0100
1"""
2Utilities for handling asyncio execution contexts.
4Fail-fast, minimal helpers to bridge async code into sync callers.
5"""
7import asyncio
8import concurrent.futures
9from typing import Any, TypeVar
11T = TypeVar("T")
14def run_coro_blocking(coro: Any) -> T:
15 """Run the given coroutine to completion, regardless of event loop state.
17 - If no loop is running, call asyncio.run(coro).
18 - If a loop is running (e.g., Jupyter), execute asyncio.run(coro) in a new thread.
19 """
20 try:
21 asyncio.get_running_loop()
22 in_async_context = True
23 except RuntimeError:
24 in_async_context = False
26 if not in_async_context:
27 try:
28 return asyncio.run(coro) # type: ignore[no-any-return]
29 finally:
30 # Ensure the coroutine is closed even if asyncio.run was mocked
31 # and did not consume it, to avoid "coroutine was never awaited" warnings.
32 try:
33 coro.close() # type: ignore[attr-defined]
34 except Exception:
35 pass
37 def _runner() -> T:
38 return asyncio.run(coro) # type: ignore[no-any-return]
40 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
41 try:
42 future: concurrent.futures.Future[T] = executor.submit(_runner)
43 return future.result()
44 finally:
45 # Close the coroutine in case the background runner did not
46 # actually execute it (e.g., asyncio.run mocked in tests).
47 try:
48 coro.close() # type: ignore[attr-defined]
49 except Exception:
50 pass