asgiref 3.9.2__tar.gz → 3.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asgiref-3.9.2/asgiref.egg-info → asgiref-3.11.0}/PKG-INFO +1 -1
- asgiref-3.11.0/asgiref/__init__.py +1 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/sync.py +75 -4
- {asgiref-3.9.2 → asgiref-3.11.0/asgiref.egg-info}/PKG-INFO +1 -1
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_sync.py +94 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_sync_contextvars.py +65 -1
- asgiref-3.9.2/asgiref/__init__.py +0 -1
- {asgiref-3.9.2 → asgiref-3.11.0}/LICENSE +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/MANIFEST.in +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/README.rst +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/compatibility.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/current_thread_executor.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/local.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/py.typed +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/server.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/testing.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/timeout.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/typing.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref/wsgi.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/SOURCES.txt +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/dependency_links.txt +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/not-zip-safe +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/requires.txt +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/asgiref.egg-info/top_level.txt +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/setup.cfg +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/setup.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_compatibility.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_garbage_collection.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_local.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_server.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_testing.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tests/test_wsgi.py +0 -0
- {asgiref-3.9.2 → asgiref-3.11.0}/tox.ini +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.11.0"
|
|
@@ -69,6 +69,45 @@ else:
|
|
|
69
69
|
return func
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
class AsyncSingleThreadContext:
|
|
73
|
+
"""Context manager to run async code inside the same thread.
|
|
74
|
+
|
|
75
|
+
Normally, AsyncToSync functions run either inside a separate ThreadPoolExecutor or
|
|
76
|
+
the main event loop if it exists. This context manager ensures that all AsyncToSync
|
|
77
|
+
functions execute within the same thread.
|
|
78
|
+
|
|
79
|
+
This context manager is re-entrant, so only the outer-most call to
|
|
80
|
+
AsyncSingleThreadContext will set the context.
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
|
|
84
|
+
>>> import asyncio
|
|
85
|
+
>>> with AsyncSingleThreadContext():
|
|
86
|
+
... async_to_sync(asyncio.sleep(1))()
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self.token = None
|
|
91
|
+
|
|
92
|
+
def __enter__(self):
|
|
93
|
+
try:
|
|
94
|
+
AsyncToSync.async_single_thread_context.get()
|
|
95
|
+
except LookupError:
|
|
96
|
+
self.token = AsyncToSync.async_single_thread_context.set(self)
|
|
97
|
+
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, exc, value, tb):
|
|
101
|
+
if not self.token:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
executor = AsyncToSync.context_to_thread_executor.pop(self, None)
|
|
105
|
+
if executor:
|
|
106
|
+
executor.shutdown()
|
|
107
|
+
|
|
108
|
+
AsyncToSync.async_single_thread_context.reset(self.token)
|
|
109
|
+
|
|
110
|
+
|
|
72
111
|
class ThreadSensitiveContext:
|
|
73
112
|
"""Async context manager to manage context for thread sensitive mode
|
|
74
113
|
|
|
@@ -131,6 +170,14 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
131
170
|
# inside create_task, we'll look it up here from the running event loop.
|
|
132
171
|
loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {}
|
|
133
172
|
|
|
173
|
+
async_single_thread_context: "contextvars.ContextVar[AsyncSingleThreadContext]" = (
|
|
174
|
+
contextvars.ContextVar("async_single_thread_context")
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
context_to_thread_executor: "weakref.WeakKeyDictionary[AsyncSingleThreadContext, ThreadPoolExecutor]" = (
|
|
178
|
+
weakref.WeakKeyDictionary()
|
|
179
|
+
)
|
|
180
|
+
|
|
134
181
|
def __init__(
|
|
135
182
|
self,
|
|
136
183
|
awaitable: Union[
|
|
@@ -246,8 +293,24 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
246
293
|
running_in_main_event_loop = False
|
|
247
294
|
|
|
248
295
|
if not running_in_main_event_loop:
|
|
249
|
-
|
|
250
|
-
|
|
296
|
+
loop_executor = None
|
|
297
|
+
|
|
298
|
+
if self.async_single_thread_context.get(None):
|
|
299
|
+
single_thread_context = self.async_single_thread_context.get()
|
|
300
|
+
|
|
301
|
+
if single_thread_context in self.context_to_thread_executor:
|
|
302
|
+
loop_executor = self.context_to_thread_executor[
|
|
303
|
+
single_thread_context
|
|
304
|
+
]
|
|
305
|
+
else:
|
|
306
|
+
loop_executor = ThreadPoolExecutor(max_workers=1)
|
|
307
|
+
self.context_to_thread_executor[
|
|
308
|
+
single_thread_context
|
|
309
|
+
] = loop_executor
|
|
310
|
+
else:
|
|
311
|
+
# Make our own event loop - in a new thread - and run inside that.
|
|
312
|
+
loop_executor = ThreadPoolExecutor(max_workers=1)
|
|
313
|
+
|
|
251
314
|
loop_future = loop_executor.submit(asyncio.run, new_loop_wrap())
|
|
252
315
|
# Run the CurrentThreadExecutor until the future is done.
|
|
253
316
|
current_executor.run_until_future(loop_future)
|
|
@@ -361,6 +424,7 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
361
424
|
func: Callable[_P, _R],
|
|
362
425
|
thread_sensitive: bool = True,
|
|
363
426
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
427
|
+
context: Optional[contextvars.Context] = None,
|
|
364
428
|
) -> None:
|
|
365
429
|
if (
|
|
366
430
|
not callable(func)
|
|
@@ -369,6 +433,7 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
369
433
|
):
|
|
370
434
|
raise TypeError("sync_to_async can only be applied to sync functions.")
|
|
371
435
|
self.func = func
|
|
436
|
+
self.context = context
|
|
372
437
|
functools.update_wrapper(self, func)
|
|
373
438
|
self._thread_sensitive = thread_sensitive
|
|
374
439
|
markcoroutinefunction(self)
|
|
@@ -417,7 +482,7 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
417
482
|
# Use the passed in executor, or the loop's default if it is None
|
|
418
483
|
executor = self._executor
|
|
419
484
|
|
|
420
|
-
context = contextvars.copy_context()
|
|
485
|
+
context = contextvars.copy_context() if self.context is None else self.context
|
|
421
486
|
child = functools.partial(self.func, *args, **kwargs)
|
|
422
487
|
func = context.run
|
|
423
488
|
task_context: List[asyncio.Task[Any]] = []
|
|
@@ -455,7 +520,8 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
455
520
|
exec_coro.cancel()
|
|
456
521
|
ret = await exec_coro
|
|
457
522
|
finally:
|
|
458
|
-
|
|
523
|
+
if self.context is None:
|
|
524
|
+
_restore_context(context)
|
|
459
525
|
self.deadlock_context.set(False)
|
|
460
526
|
|
|
461
527
|
return ret
|
|
@@ -548,6 +614,7 @@ def sync_to_async(
|
|
|
548
614
|
*,
|
|
549
615
|
thread_sensitive: bool = True,
|
|
550
616
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
617
|
+
context: Optional[contextvars.Context] = None,
|
|
551
618
|
) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
|
|
552
619
|
...
|
|
553
620
|
|
|
@@ -558,6 +625,7 @@ def sync_to_async(
|
|
|
558
625
|
*,
|
|
559
626
|
thread_sensitive: bool = True,
|
|
560
627
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
628
|
+
context: Optional[contextvars.Context] = None,
|
|
561
629
|
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
562
630
|
...
|
|
563
631
|
|
|
@@ -567,6 +635,7 @@ def sync_to_async(
|
|
|
567
635
|
*,
|
|
568
636
|
thread_sensitive: bool = True,
|
|
569
637
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
638
|
+
context: Optional[contextvars.Context] = None,
|
|
570
639
|
) -> Union[
|
|
571
640
|
Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
|
|
572
641
|
Callable[_P, Coroutine[Any, Any, _R]],
|
|
@@ -576,9 +645,11 @@ def sync_to_async(
|
|
|
576
645
|
f,
|
|
577
646
|
thread_sensitive=thread_sensitive,
|
|
578
647
|
executor=executor,
|
|
648
|
+
context=context,
|
|
579
649
|
)
|
|
580
650
|
return SyncToAsync(
|
|
581
651
|
func,
|
|
582
652
|
thread_sensitive=thread_sensitive,
|
|
583
653
|
executor=executor,
|
|
654
|
+
context=context,
|
|
584
655
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextvars
|
|
2
3
|
import functools
|
|
3
4
|
import multiprocessing
|
|
4
5
|
import sys
|
|
@@ -13,6 +14,7 @@ from unittest import TestCase
|
|
|
13
14
|
import pytest
|
|
14
15
|
|
|
15
16
|
from asgiref.sync import (
|
|
17
|
+
AsyncSingleThreadContext,
|
|
16
18
|
ThreadSensitiveContext,
|
|
17
19
|
async_to_sync,
|
|
18
20
|
iscoroutinefunction,
|
|
@@ -544,6 +546,98 @@ async def test_thread_sensitive_outside_async():
|
|
|
544
546
|
assert result_1["thread"] == result_2["thread"]
|
|
545
547
|
|
|
546
548
|
|
|
549
|
+
def test_async_single_thread_context_matches():
|
|
550
|
+
"""
|
|
551
|
+
Tests that functions wrapped with async_to_sync and executed within an
|
|
552
|
+
AsyncSingleThreadContext run on the same thread, even without a main_event_loop.
|
|
553
|
+
"""
|
|
554
|
+
result_1 = {}
|
|
555
|
+
result_2 = {}
|
|
556
|
+
|
|
557
|
+
async def store_thread_async(result):
|
|
558
|
+
result["thread"] = threading.current_thread()
|
|
559
|
+
|
|
560
|
+
with AsyncSingleThreadContext():
|
|
561
|
+
async_to_sync(store_thread_async)(result_1)
|
|
562
|
+
async_to_sync(store_thread_async)(result_2)
|
|
563
|
+
|
|
564
|
+
# They should not have run in the main thread, and on the same threads
|
|
565
|
+
assert result_1["thread"] != threading.current_thread()
|
|
566
|
+
assert result_1["thread"] == result_2["thread"]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def test_async_single_thread_nested_context():
|
|
570
|
+
"""
|
|
571
|
+
Tests that behavior remains the same when using nested context managers.
|
|
572
|
+
"""
|
|
573
|
+
result_1 = {}
|
|
574
|
+
result_2 = {}
|
|
575
|
+
|
|
576
|
+
@async_to_sync
|
|
577
|
+
async def store_thread(result):
|
|
578
|
+
result["thread"] = threading.current_thread()
|
|
579
|
+
|
|
580
|
+
with AsyncSingleThreadContext():
|
|
581
|
+
store_thread(result_1)
|
|
582
|
+
|
|
583
|
+
with AsyncSingleThreadContext():
|
|
584
|
+
store_thread(result_2)
|
|
585
|
+
|
|
586
|
+
# They should not have run in the main thread, and on the same threads
|
|
587
|
+
assert result_1["thread"] != threading.current_thread()
|
|
588
|
+
assert result_1["thread"] == result_2["thread"]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_async_single_thread_context_without_async_work():
|
|
592
|
+
"""
|
|
593
|
+
Tests everything works correctly without any async_to_sync calls.
|
|
594
|
+
"""
|
|
595
|
+
with AsyncSingleThreadContext():
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_async_single_thread_context_success_share_context():
|
|
600
|
+
"""
|
|
601
|
+
Tests that we share context between different async_to_sync functions.
|
|
602
|
+
"""
|
|
603
|
+
connection = contextvars.ContextVar("connection")
|
|
604
|
+
connection.set(0)
|
|
605
|
+
|
|
606
|
+
async def handler():
|
|
607
|
+
connection.set(connection.get(0) + 1)
|
|
608
|
+
|
|
609
|
+
with AsyncSingleThreadContext():
|
|
610
|
+
async_to_sync(handler)()
|
|
611
|
+
async_to_sync(handler)()
|
|
612
|
+
|
|
613
|
+
assert connection.get() == 2
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@pytest.mark.asyncio
|
|
617
|
+
async def test_async_single_thread_context_matches_from_async_thread():
|
|
618
|
+
"""
|
|
619
|
+
Tests that we use main_event_loop for running async_to_sync functions executed
|
|
620
|
+
within an AsyncSingleThreadContext.
|
|
621
|
+
"""
|
|
622
|
+
result_1 = {}
|
|
623
|
+
result_2 = {}
|
|
624
|
+
|
|
625
|
+
@async_to_sync
|
|
626
|
+
async def store_thread_async(result):
|
|
627
|
+
result["thread"] = threading.current_thread()
|
|
628
|
+
|
|
629
|
+
def inner():
|
|
630
|
+
with AsyncSingleThreadContext():
|
|
631
|
+
store_thread_async(result_1)
|
|
632
|
+
store_thread_async(result_2)
|
|
633
|
+
|
|
634
|
+
await sync_to_async(inner)()
|
|
635
|
+
|
|
636
|
+
# They should both have run in the current thread.
|
|
637
|
+
assert result_1["thread"] == threading.current_thread()
|
|
638
|
+
assert result_1["thread"] == result_2["thread"]
|
|
639
|
+
|
|
640
|
+
|
|
547
641
|
@pytest.mark.asyncio
|
|
548
642
|
async def test_thread_sensitive_with_context_matches():
|
|
549
643
|
result_1 = {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextvars
|
|
3
|
+
import sys
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
|
|
@@ -55,13 +56,76 @@ async def test_sync_to_async_contextvars():
|
|
|
55
56
|
assert foo.get() == "baz"
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_sync_to_async_contextvars_with_custom_context():
|
|
61
|
+
"""
|
|
62
|
+
Passing a custom context to `sync_to_async` ensures that changes to context
|
|
63
|
+
variables within the synchronous function are isolated to the provided
|
|
64
|
+
context and do not affect the caller's context. Specifically, verifies that
|
|
65
|
+
modifications to a context variable inside the sync function are reflected
|
|
66
|
+
only in the custom context and not in the outer context.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def sync_function():
|
|
70
|
+
time.sleep(1)
|
|
71
|
+
assert foo.get() == "bar"
|
|
72
|
+
foo.set("baz")
|
|
73
|
+
return 42
|
|
74
|
+
|
|
75
|
+
foo.set("bar")
|
|
76
|
+
context = contextvars.copy_context()
|
|
77
|
+
|
|
78
|
+
async_function = sync_to_async(sync_function, context=context)
|
|
79
|
+
assert await async_function() == 42
|
|
80
|
+
|
|
81
|
+
# Current context remains unchanged.
|
|
82
|
+
assert foo.get() == "bar"
|
|
83
|
+
|
|
84
|
+
# Custom context reflects the changes made within the sync function.
|
|
85
|
+
assert context.get(foo) == "baz"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11")
|
|
90
|
+
async def test_sync_to_async_contextvars_with_custom_context_and_parallel_tasks():
|
|
91
|
+
"""
|
|
92
|
+
Using a custom context with `sync_to_async` and asyncio tasks isolates
|
|
93
|
+
contextvars changes, leaving the original context unchanged and reflecting
|
|
94
|
+
all modifications in the custom context.
|
|
95
|
+
"""
|
|
96
|
+
foo.set("")
|
|
97
|
+
|
|
98
|
+
def sync_function():
|
|
99
|
+
foo.set(foo.get() + "1")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
async def async_function():
|
|
103
|
+
foo.set(foo.get() + "1")
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
context = contextvars.copy_context()
|
|
107
|
+
|
|
108
|
+
await asyncio.gather(
|
|
109
|
+
sync_to_async(sync_function, context=context)(),
|
|
110
|
+
sync_to_async(sync_function, context=context)(),
|
|
111
|
+
asyncio.create_task(async_function(), context=context),
|
|
112
|
+
asyncio.create_task(async_function(), context=context),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Current context remains unchanged
|
|
116
|
+
assert foo.get() == ""
|
|
117
|
+
|
|
118
|
+
# Custom context reflects the changes made within all the gathered tasks.
|
|
119
|
+
assert context.get(foo) == "1111"
|
|
120
|
+
|
|
121
|
+
|
|
58
122
|
def test_async_to_sync_contextvars():
|
|
59
123
|
"""
|
|
60
124
|
Tests to make sure that contextvars from the calling context are
|
|
61
125
|
present in the called context, and that any changes in the called context
|
|
62
126
|
are then propagated back to the calling context.
|
|
63
127
|
"""
|
|
64
|
-
# Define
|
|
128
|
+
# Define async function
|
|
65
129
|
async def async_function():
|
|
66
130
|
await asyncio.sleep(1)
|
|
67
131
|
assert foo.get() == "bar"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.9.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|