asgiref 3.10.0__tar.gz → 3.11.1__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.10.0/asgiref.egg-info → asgiref-3.11.1}/PKG-INFO +1 -1
- asgiref-3.11.1/asgiref/__init__.py +1 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/sync.py +13 -3
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/wsgi.py +38 -7
- {asgiref-3.10.0 → asgiref-3.11.1/asgiref.egg-info}/PKG-INFO +1 -1
- asgiref-3.11.1/tests/test_sync_contextvars.py +161 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_wsgi.py +65 -0
- asgiref-3.10.0/asgiref/__init__.py +0 -1
- asgiref-3.10.0/tests/test_sync_contextvars.py +0 -76
- {asgiref-3.10.0 → asgiref-3.11.1}/LICENSE +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/MANIFEST.in +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/README.rst +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/compatibility.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/current_thread_executor.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/local.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/py.typed +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/server.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/testing.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/timeout.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/typing.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/SOURCES.txt +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/dependency_links.txt +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/not-zip-safe +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/requires.txt +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/top_level.txt +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/setup.cfg +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/setup.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_compatibility.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_garbage_collection.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_local.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_server.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_sync.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_testing.py +0 -0
- {asgiref-3.10.0 → asgiref-3.11.1}/tox.ini +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.11.1"
|
|
@@ -424,6 +424,7 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
424
424
|
func: Callable[_P, _R],
|
|
425
425
|
thread_sensitive: bool = True,
|
|
426
426
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
427
|
+
context: Optional[contextvars.Context] = None,
|
|
427
428
|
) -> None:
|
|
428
429
|
if (
|
|
429
430
|
not callable(func)
|
|
@@ -431,8 +432,11 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
431
432
|
or iscoroutinefunction(getattr(func, "__call__", func))
|
|
432
433
|
):
|
|
433
434
|
raise TypeError("sync_to_async can only be applied to sync functions.")
|
|
434
|
-
|
|
435
|
+
|
|
435
436
|
functools.update_wrapper(self, func)
|
|
437
|
+
self.func = func
|
|
438
|
+
self.context = context
|
|
439
|
+
|
|
436
440
|
self._thread_sensitive = thread_sensitive
|
|
437
441
|
markcoroutinefunction(self)
|
|
438
442
|
if thread_sensitive and executor is not None:
|
|
@@ -480,7 +484,7 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
480
484
|
# Use the passed in executor, or the loop's default if it is None
|
|
481
485
|
executor = self._executor
|
|
482
486
|
|
|
483
|
-
context = contextvars.copy_context()
|
|
487
|
+
context = contextvars.copy_context() if self.context is None else self.context
|
|
484
488
|
child = functools.partial(self.func, *args, **kwargs)
|
|
485
489
|
func = context.run
|
|
486
490
|
task_context: List[asyncio.Task[Any]] = []
|
|
@@ -518,7 +522,8 @@ class SyncToAsync(Generic[_P, _R]):
|
|
|
518
522
|
exec_coro.cancel()
|
|
519
523
|
ret = await exec_coro
|
|
520
524
|
finally:
|
|
521
|
-
|
|
525
|
+
if self.context is None:
|
|
526
|
+
_restore_context(context)
|
|
522
527
|
self.deadlock_context.set(False)
|
|
523
528
|
|
|
524
529
|
return ret
|
|
@@ -611,6 +616,7 @@ def sync_to_async(
|
|
|
611
616
|
*,
|
|
612
617
|
thread_sensitive: bool = True,
|
|
613
618
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
619
|
+
context: Optional[contextvars.Context] = None,
|
|
614
620
|
) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
|
|
615
621
|
...
|
|
616
622
|
|
|
@@ -621,6 +627,7 @@ def sync_to_async(
|
|
|
621
627
|
*,
|
|
622
628
|
thread_sensitive: bool = True,
|
|
623
629
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
630
|
+
context: Optional[contextvars.Context] = None,
|
|
624
631
|
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
625
632
|
...
|
|
626
633
|
|
|
@@ -630,6 +637,7 @@ def sync_to_async(
|
|
|
630
637
|
*,
|
|
631
638
|
thread_sensitive: bool = True,
|
|
632
639
|
executor: Optional["ThreadPoolExecutor"] = None,
|
|
640
|
+
context: Optional[contextvars.Context] = None,
|
|
633
641
|
) -> Union[
|
|
634
642
|
Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
|
|
635
643
|
Callable[_P, Coroutine[Any, Any, _R]],
|
|
@@ -639,9 +647,11 @@ def sync_to_async(
|
|
|
639
647
|
f,
|
|
640
648
|
thread_sensitive=thread_sensitive,
|
|
641
649
|
executor=executor,
|
|
650
|
+
context=context,
|
|
642
651
|
)
|
|
643
652
|
return SyncToAsync(
|
|
644
653
|
func,
|
|
645
654
|
thread_sensitive=thread_sensitive,
|
|
646
655
|
executor=executor,
|
|
656
|
+
context=context,
|
|
647
657
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
from tempfile import SpooledTemporaryFile
|
|
3
4
|
|
|
4
5
|
from asgiref.sync import AsyncToSync, sync_to_async
|
|
@@ -9,8 +10,9 @@ class WsgiToAsgi:
|
|
|
9
10
|
Wraps a WSGI application to make it into an ASGI application.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
|
-
def __init__(self, wsgi_application):
|
|
13
|
+
def __init__(self, wsgi_application, duplicate_header_limit=100):
|
|
13
14
|
self.wsgi_application = wsgi_application
|
|
15
|
+
self.duplicate_header_limit = duplicate_header_limit
|
|
14
16
|
|
|
15
17
|
async def __call__(self, scope, receive, send):
|
|
16
18
|
"""
|
|
@@ -18,7 +20,9 @@ class WsgiToAsgi:
|
|
|
18
20
|
We return a new WsgiToAsgiInstance here with the WSGI app
|
|
19
21
|
and the scope, ready to respond when it is __call__ed.
|
|
20
22
|
"""
|
|
21
|
-
await WsgiToAsgiInstance(self.wsgi_application
|
|
23
|
+
await WsgiToAsgiInstance(self.wsgi_application, self.duplicate_header_limit)(
|
|
24
|
+
scope, receive, send
|
|
25
|
+
)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class WsgiToAsgiInstance:
|
|
@@ -26,8 +30,9 @@ class WsgiToAsgiInstance:
|
|
|
26
30
|
Per-socket instance of a wrapped WSGI application
|
|
27
31
|
"""
|
|
28
32
|
|
|
29
|
-
def __init__(self, wsgi_application):
|
|
33
|
+
def __init__(self, wsgi_application, duplicate_header_limit=100):
|
|
30
34
|
self.wsgi_application = wsgi_application
|
|
35
|
+
self.duplicate_header_limit = duplicate_header_limit
|
|
31
36
|
self.response_started = False
|
|
32
37
|
self.response_content_length = None
|
|
33
38
|
|
|
@@ -84,6 +89,7 @@ class WsgiToAsgiInstance:
|
|
|
84
89
|
environ["REMOTE_ADDR"] = scope["client"][0]
|
|
85
90
|
|
|
86
91
|
# Go through headers and make them into environ entries
|
|
92
|
+
_headers = defaultdict(list)
|
|
87
93
|
for name, value in self.scope.get("headers", []):
|
|
88
94
|
name = name.decode("latin1")
|
|
89
95
|
if name == "content-length":
|
|
@@ -94,9 +100,17 @@ class WsgiToAsgiInstance:
|
|
|
94
100
|
corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
|
|
95
101
|
# HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case
|
|
96
102
|
value = value.decode("latin1")
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
if (
|
|
104
|
+
self.duplicate_header_limit
|
|
105
|
+
and len(_headers[corrected_name]) >= self.duplicate_header_limit
|
|
106
|
+
):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Too many duplicate headers: {corrected_name} exceeds limit of"
|
|
109
|
+
f"{self.duplicate_header_limit}"
|
|
110
|
+
)
|
|
111
|
+
_headers[corrected_name].append(value)
|
|
112
|
+
for name, values in _headers.items():
|
|
113
|
+
environ[name] = ",".join(values)
|
|
100
114
|
return environ
|
|
101
115
|
|
|
102
116
|
def start_response(self, status, response_headers, exc_info=None):
|
|
@@ -138,7 +152,24 @@ class WsgiToAsgiInstance:
|
|
|
138
152
|
this so that the start_response callable is called in the same thread.
|
|
139
153
|
"""
|
|
140
154
|
# Translate the scope and incoming request body into a WSGI environ
|
|
141
|
-
|
|
155
|
+
try:
|
|
156
|
+
environ = self.build_environ(self.scope, body)
|
|
157
|
+
except ValueError:
|
|
158
|
+
# Return 400 Bad Request if header limit exceeded
|
|
159
|
+
self.sync_send(
|
|
160
|
+
{
|
|
161
|
+
"type": "http.response.start",
|
|
162
|
+
"status": 400,
|
|
163
|
+
"headers": [(b"content-type", b"text/plain")],
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
self.sync_send(
|
|
167
|
+
{
|
|
168
|
+
"type": "http.response.body",
|
|
169
|
+
"body": b"Bad Request: Too many duplicate headers",
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
return
|
|
142
173
|
# Run the WSGI app
|
|
143
174
|
bytes_sent = 0
|
|
144
175
|
for output in self.wsgi_application(environ, self.start_response):
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async
|
|
10
|
+
|
|
11
|
+
foo: "contextvars.ContextVar[str]" = contextvars.ContextVar("foo")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_thread_sensitive_with_context_different():
|
|
16
|
+
result_1 = {}
|
|
17
|
+
result_2 = {}
|
|
18
|
+
|
|
19
|
+
@sync_to_async
|
|
20
|
+
def store_thread(result):
|
|
21
|
+
result["thread"] = threading.current_thread()
|
|
22
|
+
|
|
23
|
+
async def fn(result):
|
|
24
|
+
async with ThreadSensitiveContext():
|
|
25
|
+
await store_thread(result)
|
|
26
|
+
|
|
27
|
+
# Run it (in true parallel!)
|
|
28
|
+
await asyncio.wait(
|
|
29
|
+
[asyncio.create_task(fn(result_1)), asyncio.create_task(fn(result_2))]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# They should not have run in the main thread, and on different threads
|
|
33
|
+
assert result_1["thread"] != threading.current_thread()
|
|
34
|
+
assert result_1["thread"] != result_2["thread"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_sync_to_async_contextvars():
|
|
39
|
+
"""
|
|
40
|
+
Tests to make sure that contextvars from the calling context are
|
|
41
|
+
present in the called context, and that any changes in the called context
|
|
42
|
+
are then propagated back to the calling context.
|
|
43
|
+
"""
|
|
44
|
+
# Define sync function
|
|
45
|
+
def sync_function():
|
|
46
|
+
time.sleep(1)
|
|
47
|
+
assert foo.get() == "bar"
|
|
48
|
+
foo.set("baz")
|
|
49
|
+
return 42
|
|
50
|
+
|
|
51
|
+
# Ensure outermost detection works
|
|
52
|
+
# Wrap it
|
|
53
|
+
foo.set("bar")
|
|
54
|
+
async_function = sync_to_async(sync_function)
|
|
55
|
+
assert await async_function() == 42
|
|
56
|
+
assert foo.get() == "baz"
|
|
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
|
+
|
|
122
|
+
def test_async_to_sync_contextvars():
|
|
123
|
+
"""
|
|
124
|
+
Tests to make sure that contextvars from the calling context are
|
|
125
|
+
present in the called context, and that any changes in the called context
|
|
126
|
+
are then propagated back to the calling context.
|
|
127
|
+
"""
|
|
128
|
+
# Define async function
|
|
129
|
+
async def async_function():
|
|
130
|
+
await asyncio.sleep(1)
|
|
131
|
+
assert foo.get() == "bar"
|
|
132
|
+
foo.set("baz")
|
|
133
|
+
return 42
|
|
134
|
+
|
|
135
|
+
# Ensure outermost detection works
|
|
136
|
+
# Wrap it
|
|
137
|
+
foo.set("bar")
|
|
138
|
+
sync_function = async_to_sync(async_function)
|
|
139
|
+
assert sync_function() == 42
|
|
140
|
+
assert foo.get() == "baz"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_sync_to_async_contextvars_with_callable_with_context_attribute():
|
|
145
|
+
"""
|
|
146
|
+
Tests that a callable object with a `context` attribute
|
|
147
|
+
can be wrapped with `sync_to_async` without overwriting the `context` attribute
|
|
148
|
+
and still returns the expected result.
|
|
149
|
+
"""
|
|
150
|
+
# Define sync Callable
|
|
151
|
+
class SyncCallable:
|
|
152
|
+
def __init__(self):
|
|
153
|
+
# Should not be copied to the SyncToAsync wrapper.
|
|
154
|
+
self.context = ...
|
|
155
|
+
|
|
156
|
+
def __call__(self):
|
|
157
|
+
return 42
|
|
158
|
+
|
|
159
|
+
async_function = sync_to_async(SyncCallable())
|
|
160
|
+
assert async_function.context is None
|
|
161
|
+
assert await async_function() == 42
|
|
@@ -315,3 +315,68 @@ async def test_wsgi_multi_body():
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
assert (await instance.receive_output(1)) == {"type": "http.response.body"}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_duplicate_header_limit():
|
|
322
|
+
def wsgi_application(environ, start_response):
|
|
323
|
+
start_response("200 OK", [])
|
|
324
|
+
return [b"OK"]
|
|
325
|
+
|
|
326
|
+
application = WsgiToAsgi(wsgi_application, duplicate_header_limit=5)
|
|
327
|
+
instance = ApplicationCommunicator(
|
|
328
|
+
application,
|
|
329
|
+
{
|
|
330
|
+
"type": "http",
|
|
331
|
+
"http_version": "1.0",
|
|
332
|
+
"method": "GET",
|
|
333
|
+
"path": "/",
|
|
334
|
+
"query_string": b"",
|
|
335
|
+
"headers": [[b"x-test", b"value"] for _ in range(10)],
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
await instance.send_input({"type": "http.request"})
|
|
339
|
+
|
|
340
|
+
assert (await instance.receive_output(1)) == {
|
|
341
|
+
"type": "http.response.start",
|
|
342
|
+
"status": 400,
|
|
343
|
+
"headers": [(b"content-type", b"text/plain")],
|
|
344
|
+
}
|
|
345
|
+
assert (await instance.receive_output(1)) == {
|
|
346
|
+
"type": "http.response.body",
|
|
347
|
+
"body": b"Bad Request: Too many duplicate headers",
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.mark.asyncio
|
|
352
|
+
async def test_duplicate_header_limit_disabled():
|
|
353
|
+
def wsgi_application(environ, start_response):
|
|
354
|
+
assert "HTTP_X_TEST" in environ
|
|
355
|
+
start_response("200 OK", [])
|
|
356
|
+
return [b"OK"]
|
|
357
|
+
|
|
358
|
+
application = WsgiToAsgi(wsgi_application, duplicate_header_limit=None)
|
|
359
|
+
instance = ApplicationCommunicator(
|
|
360
|
+
application,
|
|
361
|
+
{
|
|
362
|
+
"type": "http",
|
|
363
|
+
"http_version": "1.0",
|
|
364
|
+
"method": "GET",
|
|
365
|
+
"path": "/",
|
|
366
|
+
"query_string": b"",
|
|
367
|
+
"headers": [[b"x-test", b"value"] for _ in range(200)],
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
await instance.send_input({"type": "http.request"})
|
|
371
|
+
|
|
372
|
+
assert (await instance.receive_output(1)) == {
|
|
373
|
+
"type": "http.response.start",
|
|
374
|
+
"status": 200,
|
|
375
|
+
"headers": [],
|
|
376
|
+
}
|
|
377
|
+
assert (await instance.receive_output(1)) == {
|
|
378
|
+
"type": "http.response.body",
|
|
379
|
+
"body": b"OK",
|
|
380
|
+
"more_body": True,
|
|
381
|
+
}
|
|
382
|
+
assert (await instance.receive_output(1)) == {"type": "http.response.body"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.10.0"
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextvars
|
|
3
|
-
import threading
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async
|
|
9
|
-
|
|
10
|
-
foo: "contextvars.ContextVar[str]" = contextvars.ContextVar("foo")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.mark.asyncio
|
|
14
|
-
async def test_thread_sensitive_with_context_different():
|
|
15
|
-
result_1 = {}
|
|
16
|
-
result_2 = {}
|
|
17
|
-
|
|
18
|
-
@sync_to_async
|
|
19
|
-
def store_thread(result):
|
|
20
|
-
result["thread"] = threading.current_thread()
|
|
21
|
-
|
|
22
|
-
async def fn(result):
|
|
23
|
-
async with ThreadSensitiveContext():
|
|
24
|
-
await store_thread(result)
|
|
25
|
-
|
|
26
|
-
# Run it (in true parallel!)
|
|
27
|
-
await asyncio.wait(
|
|
28
|
-
[asyncio.create_task(fn(result_1)), asyncio.create_task(fn(result_2))]
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# They should not have run in the main thread, and on different threads
|
|
32
|
-
assert result_1["thread"] != threading.current_thread()
|
|
33
|
-
assert result_1["thread"] != result_2["thread"]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@pytest.mark.asyncio
|
|
37
|
-
async def test_sync_to_async_contextvars():
|
|
38
|
-
"""
|
|
39
|
-
Tests to make sure that contextvars from the calling context are
|
|
40
|
-
present in the called context, and that any changes in the called context
|
|
41
|
-
are then propagated back to the calling context.
|
|
42
|
-
"""
|
|
43
|
-
# Define sync function
|
|
44
|
-
def sync_function():
|
|
45
|
-
time.sleep(1)
|
|
46
|
-
assert foo.get() == "bar"
|
|
47
|
-
foo.set("baz")
|
|
48
|
-
return 42
|
|
49
|
-
|
|
50
|
-
# Ensure outermost detection works
|
|
51
|
-
# Wrap it
|
|
52
|
-
foo.set("bar")
|
|
53
|
-
async_function = sync_to_async(sync_function)
|
|
54
|
-
assert await async_function() == 42
|
|
55
|
-
assert foo.get() == "baz"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_async_to_sync_contextvars():
|
|
59
|
-
"""
|
|
60
|
-
Tests to make sure that contextvars from the calling context are
|
|
61
|
-
present in the called context, and that any changes in the called context
|
|
62
|
-
are then propagated back to the calling context.
|
|
63
|
-
"""
|
|
64
|
-
# Define sync function
|
|
65
|
-
async def async_function():
|
|
66
|
-
await asyncio.sleep(1)
|
|
67
|
-
assert foo.get() == "bar"
|
|
68
|
-
foo.set("baz")
|
|
69
|
-
return 42
|
|
70
|
-
|
|
71
|
-
# Ensure outermost detection works
|
|
72
|
-
# Wrap it
|
|
73
|
-
foo.set("bar")
|
|
74
|
-
sync_function = async_to_sync(async_function)
|
|
75
|
-
assert sync_function() == 42
|
|
76
|
-
assert foo.get() == "baz"
|
|
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
|