11"""Utilities"""
22
3+ from __future__ import annotations
4+
5+ import asyncio
6+ import sys
37import typing as t
48from collections .abc import Mapping
9+ from contextvars import copy_context
10+ from functools import partial , wraps
11+
12+ if t .TYPE_CHECKING :
13+ from collections .abc import Callable
14+ from contextvars import Context
515
616
717class LazyDict (Mapping [str , t .Any ]):
@@ -24,3 +34,41 @@ def __len__(self):
2434
2535 def __iter__ (self ):
2636 return iter (self ._dict )
37+
38+
39+ def _async_in_context (f : Callable , context : Context | None = None ) -> Callable :
40+ """
41+ Wrapper to run a coroutine in a persistent ContextVar Context.
42+
43+ Backports asyncio.create_task(context=...) behavior from Python 3.11
44+ """
45+ if context is None :
46+ context = copy_context ()
47+
48+ if sys .version_info >= (3 , 11 ):
49+
50+ @wraps (f )
51+ async def run_in_context (* args , ** kwargs ):
52+ coro = f (* args , ** kwargs )
53+ return await asyncio .create_task (coro , context = context )
54+
55+ return run_in_context
56+
57+ # don't need this backport when we require 3.11
58+ # context_holder so we have a modifiable container for later calls
59+ context_holder = [context ]
60+
61+ async def preserve_context (f , * args , ** kwargs ):
62+ """call a coroutine, preserving the context after it is called"""
63+ try :
64+ return await f (* args , ** kwargs )
65+ finally :
66+ # persist changes to the context for future calls
67+ context_holder [0 ] = copy_context ()
68+
69+ @wraps (f )
70+ async def run_in_context (* args , ** kwargs ):
71+ ctx = context_holder [0 ]
72+ return await ctx .run (partial (asyncio .create_task , preserve_context (f , * args , ** kwargs )))
73+
74+ return run_in_context
0 commit comments