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,48 @@ def __len__(self):
2434
2535 def __iter__ (self ):
2636 return iter (self ._dict )
37+
38+
39+ T = t .TypeVar ("T" )
40+ U = t .TypeVar ("U" )
41+ V = t .TypeVar ("V" )
42+
43+
44+ def _async_in_context (
45+ f : Callable [..., t .Coroutine [T , U , V ]], context : Context | None = None
46+ ) -> Callable [..., t .Coroutine [T , U , V ]]:
47+ """
48+ Wrapper to run a coroutine in a persistent ContextVar Context.
49+
50+ Backports asyncio.create_task(context=...) behavior from Python 3.11
51+ """
52+ if context is None :
53+ context = copy_context ()
54+
55+ if sys .version_info >= (3 , 11 ):
56+
57+ @wraps (f )
58+ async def run_in_context (* args , ** kwargs ):
59+ coro = f (* args , ** kwargs )
60+ return await asyncio .create_task (coro , context = context )
61+
62+ return run_in_context
63+
64+ # don't need this backport when we require 3.11
65+ # context_holder so we have a modifiable container for later calls
66+ context_holder = [context ] # type: ignore[unreachable]
67+
68+ async def preserve_context (f , * args , ** kwargs ):
69+ """call a coroutine, preserving the context after it is called"""
70+ try :
71+ return await f (* args , ** kwargs )
72+ finally :
73+ # persist changes to the context for future calls
74+ context_holder [0 ] = copy_context ()
75+
76+ @wraps (f )
77+ async def run_in_context_pre311 (* args , ** kwargs ):
78+ ctx = context_holder [0 ]
79+ return await ctx .run (partial (asyncio .create_task , preserve_context (f , * args , ** kwargs )))
80+
81+ return run_in_context_pre311
0 commit comments