Coverage for src/chat_limiter/utils.py: 84%

38 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-09-18 21:15 +0100

1""" 

2Utilities for handling asyncio execution contexts. 

3 

4Fail-fast, minimal helpers to bridge async code into sync callers. 

5""" 

6 

7import asyncio 

8import concurrent.futures 

9from functools import lru_cache 

10from typing import Any, TypeVar 

11 

12T = TypeVar("T") 

13 

14 

15@lru_cache(maxsize=1) 

16def _get_background_executor() -> concurrent.futures.ThreadPoolExecutor: 

17 """Return a singleton background executor for bridging from running loops.""" 

18 return concurrent.futures.ThreadPoolExecutor(max_workers=1) 

19 

20 

21def run_coro_blocking(coro: Any) -> T: 

22 """Run the given coroutine to completion, regardless of event loop state. 

23 

24 - If no loop is running, call asyncio.run(coro). 

25 - If a loop is running (e.g., Jupyter), execute asyncio.run(coro) in a new thread. 

26 """ 

27 try: 

28 asyncio.get_running_loop() 

29 in_async_context = True 

30 except RuntimeError: 

31 in_async_context = False 

32 

33 if not in_async_context: 

34 try: 

35 return asyncio.run(coro) # type: ignore[no-any-return] 

36 finally: 

37 # Ensure the coroutine is closed even if asyncio.run was mocked 

38 # and did not consume it, to avoid "coroutine was never awaited" warnings. 

39 try: 

40 coro.close() # type: ignore[attr-defined] 

41 except AttributeError: 

42 pass 

43 

44 def _runner() -> T: 

45 return asyncio.run(coro) # type: ignore[no-any-return] 

46 

47 try: 

48 executor = _get_background_executor() 

49 except Exception: 

50 # Ensure the coroutine is closed to avoid 'was never awaited' warnings 

51 try: 

52 coro.close() # type: ignore[attr-defined] 

53 except AttributeError: 

54 pass 

55 raise 

56 try: 

57 future: concurrent.futures.Future[T] = executor.submit(_runner) 

58 return future.result() 

59 finally: 

60 # Close the coroutine in case the background runner did not 

61 # actually execute it (e.g., asyncio.run mocked in tests). 

62 try: 

63 coro.close() # type: ignore[attr-defined] 

64 except AttributeError: 

65 pass 

66 

67