asgiref 3.9.1__tar.gz → 3.10.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.1/asgiref.egg-info → asgiref-3.10.0}/PKG-INFO +1 -1
- asgiref-3.10.0/asgiref/__init__.py +1 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/sync.py +65 -2
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/wsgi.py +2 -2
- {asgiref-3.9.1 → asgiref-3.10.0/asgiref.egg-info}/PKG-INFO +1 -1
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_sync.py +94 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tox.ini +1 -1
- asgiref-3.9.1/asgiref/__init__.py +0 -1
- {asgiref-3.9.1 → asgiref-3.10.0}/LICENSE +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/MANIFEST.in +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/README.rst +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/compatibility.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/current_thread_executor.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/local.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/py.typed +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/server.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/testing.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/timeout.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref/typing.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref.egg-info/SOURCES.txt +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref.egg-info/dependency_links.txt +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref.egg-info/not-zip-safe +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref.egg-info/requires.txt +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/asgiref.egg-info/top_level.txt +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/setup.cfg +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/setup.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_compatibility.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_garbage_collection.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_local.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_server.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_sync_contextvars.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_testing.py +0 -0
- {asgiref-3.9.1 → asgiref-3.10.0}/tests/test_wsgi.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.10.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)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import sys
|
|
2
2
|
from tempfile import SpooledTemporaryFile
|
|
3
3
|
|
|
4
4
|
from asgiref.sync import AsyncToSync, sync_to_async
|
|
@@ -67,7 +67,7 @@ class WsgiToAsgiInstance:
|
|
|
67
67
|
"wsgi.version": (1, 0),
|
|
68
68
|
"wsgi.url_scheme": scope.get("scheme", "http"),
|
|
69
69
|
"wsgi.input": body,
|
|
70
|
-
"wsgi.errors":
|
|
70
|
+
"wsgi.errors": sys.stderr,
|
|
71
71
|
"wsgi.multithread": True,
|
|
72
72
|
"wsgi.multiprocess": True,
|
|
73
73
|
"wsgi.run_once": False,
|
|
@@ -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 +0,0 @@
|
|
|
1
|
-
__version__ = "3.9.1"
|
|
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
|