asgiref 3.8.1__tar.gz → 3.9.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.8.1/asgiref.egg-info → asgiref-3.9.1}/PKG-INFO +8 -7
- {asgiref-3.8.1 → asgiref-3.9.1}/README.rst +2 -2
- asgiref-3.9.1/asgiref/__init__.py +1 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/current_thread_executor.py +42 -34
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/local.py +5 -2
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/server.py +18 -2
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/sync.py +41 -70
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/testing.py +47 -13
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/typing.py +1 -0
- {asgiref-3.8.1 → asgiref-3.9.1/asgiref.egg-info}/PKG-INFO +8 -7
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref.egg-info/SOURCES.txt +1 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref.egg-info/requires.txt +1 -1
- {asgiref-3.8.1 → asgiref-3.9.1}/setup.cfg +4 -3
- asgiref-3.9.1/tests/test_garbage_collection.py +61 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_local.py +53 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_server.py +24 -32
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_sync.py +107 -0
- asgiref-3.9.1/tests/test_testing.py +90 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tox.ini +1 -1
- asgiref-3.8.1/asgiref/__init__.py +0 -1
- asgiref-3.8.1/tests/test_testing.py +0 -45
- {asgiref-3.8.1 → asgiref-3.9.1}/LICENSE +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/MANIFEST.in +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/compatibility.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/py.typed +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/timeout.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref/wsgi.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref.egg-info/dependency_links.txt +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref.egg-info/not-zip-safe +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/asgiref.egg-info/top_level.txt +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/setup.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_compatibility.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_sync_contextvars.py +0 -0
- {asgiref-3.8.1 → asgiref-3.9.1}/tests/test_wsgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: asgiref
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.1
|
|
4
4
|
Summary: ASGI specs, helper code, and adapters
|
|
5
5
|
Home-page: https://github.com/django/asgiref/
|
|
6
6
|
Author: Django Software Foundation
|
|
@@ -17,19 +17,20 @@ Classifier: Operating System :: OS Independent
|
|
|
17
17
|
Classifier: Programming Language :: Python
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: typing_extensions>=4; python_version < "3.11"
|
|
29
29
|
Provides-Extra: tests
|
|
30
30
|
Requires-Dist: pytest; extra == "tests"
|
|
31
31
|
Requires-Dist: pytest-asyncio; extra == "tests"
|
|
32
|
-
Requires-Dist: mypy>=0
|
|
32
|
+
Requires-Dist: mypy>=1.14.0; extra == "tests"
|
|
33
|
+
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
asgiref
|
|
35
36
|
=======
|
|
@@ -129,7 +130,7 @@ file handles for incoming POST bodies).
|
|
|
129
130
|
Dependencies
|
|
130
131
|
------------
|
|
131
132
|
|
|
132
|
-
``asgiref`` requires Python 3.
|
|
133
|
+
``asgiref`` requires Python 3.9 or higher.
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
Contributing
|
|
@@ -179,7 +180,7 @@ Then, build and push the packages::
|
|
|
179
180
|
|
|
180
181
|
python -m build
|
|
181
182
|
twine upload dist/*
|
|
182
|
-
rm -r
|
|
183
|
+
rm -r asgiref.egg-info dist
|
|
183
184
|
|
|
184
185
|
|
|
185
186
|
Implementation Details
|
|
@@ -96,7 +96,7 @@ file handles for incoming POST bodies).
|
|
|
96
96
|
Dependencies
|
|
97
97
|
------------
|
|
98
98
|
|
|
99
|
-
``asgiref`` requires Python 3.
|
|
99
|
+
``asgiref`` requires Python 3.9 or higher.
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
Contributing
|
|
@@ -146,7 +146,7 @@ Then, build and push the packages::
|
|
|
146
146
|
|
|
147
147
|
python -m build
|
|
148
148
|
twine upload dist/*
|
|
149
|
-
rm -r
|
|
149
|
+
rm -r asgiref.egg-info dist
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
Implementation Details
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.9.1"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import queue
|
|
2
1
|
import sys
|
|
3
2
|
import threading
|
|
3
|
+
from collections import deque
|
|
4
4
|
from concurrent.futures import Executor, Future
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Callable, TypeVar
|
|
6
6
|
|
|
7
7
|
if sys.version_info >= (3, 10):
|
|
8
8
|
from typing import ParamSpec
|
|
@@ -53,10 +53,12 @@ class CurrentThreadExecutor(Executor):
|
|
|
53
53
|
the thread they came from.
|
|
54
54
|
"""
|
|
55
55
|
|
|
56
|
-
def __init__(self) -> None:
|
|
56
|
+
def __init__(self, old_executor: "CurrentThreadExecutor | None") -> None:
|
|
57
57
|
self._work_thread = threading.current_thread()
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
58
|
+
self._work_ready = threading.Condition(threading.Lock())
|
|
59
|
+
self._work_items = deque[_WorkItem]() # synchronized by _work_ready
|
|
60
|
+
self._broken = False # synchronized by _work_ready
|
|
61
|
+
self._old_executor = old_executor
|
|
60
62
|
|
|
61
63
|
def run_until_future(self, future: "Future[Any]") -> None:
|
|
62
64
|
"""
|
|
@@ -68,24 +70,30 @@ class CurrentThreadExecutor(Executor):
|
|
|
68
70
|
raise RuntimeError(
|
|
69
71
|
"You cannot run CurrentThreadExecutor from a different thread"
|
|
70
72
|
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
def done(future: "Future[Any]") -> None:
|
|
75
|
+
with self._work_ready:
|
|
76
|
+
self._broken = True
|
|
77
|
+
self._work_ready.notify()
|
|
78
|
+
|
|
79
|
+
future.add_done_callback(done)
|
|
80
|
+
# Keep getting and running work items until the future we're waiting for
|
|
81
|
+
# is done and the queue is empty.
|
|
82
|
+
while True:
|
|
83
|
+
with self._work_ready:
|
|
84
|
+
while not self._work_items and not self._broken:
|
|
85
|
+
self._work_ready.wait()
|
|
86
|
+
if not self._work_items:
|
|
87
|
+
break
|
|
76
88
|
# Get a work item and run it
|
|
77
|
-
work_item = self.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assert isinstance(work_item, _WorkItem)
|
|
81
|
-
work_item.run()
|
|
82
|
-
del work_item
|
|
83
|
-
finally:
|
|
84
|
-
self._broken = True
|
|
89
|
+
work_item = self._work_items.popleft()
|
|
90
|
+
work_item.run()
|
|
91
|
+
del work_item
|
|
85
92
|
|
|
86
|
-
def
|
|
93
|
+
def submit(
|
|
87
94
|
self,
|
|
88
95
|
fn: Callable[_P, _R],
|
|
96
|
+
/,
|
|
89
97
|
*args: _P.args,
|
|
90
98
|
**kwargs: _P.kwargs,
|
|
91
99
|
) -> "Future[_R]":
|
|
@@ -94,22 +102,22 @@ class CurrentThreadExecutor(Executor):
|
|
|
94
102
|
raise RuntimeError(
|
|
95
103
|
"You cannot submit onto CurrentThreadExecutor from its own thread"
|
|
96
104
|
)
|
|
97
|
-
# Check they're not too late or the executor errored
|
|
98
|
-
if self._broken:
|
|
99
|
-
raise RuntimeError("CurrentThreadExecutor already quit or is broken")
|
|
100
|
-
# Add to work queue
|
|
101
105
|
f: "Future[_R]" = Future()
|
|
102
106
|
work_item = _WorkItem(f, fn, *args, **kwargs)
|
|
103
|
-
self._work_queue.put(work_item)
|
|
104
|
-
# Return the future
|
|
105
|
-
return f
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
# Walk up the CurrentThreadExecutor stack to find the closest one still
|
|
109
|
+
# running
|
|
110
|
+
executor = self
|
|
111
|
+
while True:
|
|
112
|
+
with executor._work_ready:
|
|
113
|
+
if not executor._broken:
|
|
114
|
+
# Add to work queue
|
|
115
|
+
executor._work_items.append(work_item)
|
|
116
|
+
executor._work_ready.notify()
|
|
117
|
+
break
|
|
118
|
+
if executor._old_executor is None:
|
|
119
|
+
raise RuntimeError("CurrentThreadExecutor already quit or is broken")
|
|
120
|
+
executor = executor._old_executor
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
# Return the future
|
|
123
|
+
return f
|
|
@@ -24,12 +24,12 @@ class _CVar:
|
|
|
24
24
|
if key == "_data":
|
|
25
25
|
return super().__setattr__(key, value)
|
|
26
26
|
|
|
27
|
-
storage_object = self._data.get({})
|
|
27
|
+
storage_object = self._data.get({}).copy()
|
|
28
28
|
storage_object[key] = value
|
|
29
29
|
self._data.set(storage_object)
|
|
30
30
|
|
|
31
31
|
def __delattr__(self, key: str) -> None:
|
|
32
|
-
storage_object = self._data.get({})
|
|
32
|
+
storage_object = self._data.get({}).copy()
|
|
33
33
|
if key in storage_object:
|
|
34
34
|
del storage_object[key]
|
|
35
35
|
self._data.set(storage_object)
|
|
@@ -82,12 +82,15 @@ class Local:
|
|
|
82
82
|
def _lock_storage(self):
|
|
83
83
|
# Thread safe access to storage
|
|
84
84
|
if self._thread_critical:
|
|
85
|
+
is_async = True
|
|
85
86
|
try:
|
|
86
87
|
# this is a test for are we in a async or sync
|
|
87
88
|
# thread - will raise RuntimeError if there is
|
|
88
89
|
# no current loop
|
|
89
90
|
asyncio.get_running_loop()
|
|
90
91
|
except RuntimeError:
|
|
92
|
+
is_async = False
|
|
93
|
+
if not is_async:
|
|
91
94
|
# We are in a sync thread, the storage is
|
|
92
95
|
# just the plain thread local (i.e, "global within
|
|
93
96
|
# this thread" - it doesn't matter where you are
|
|
@@ -57,12 +57,28 @@ class StatelessServer:
|
|
|
57
57
|
Runs the asyncio event loop with our handler loop.
|
|
58
58
|
"""
|
|
59
59
|
event_loop = asyncio.get_event_loop()
|
|
60
|
-
asyncio.ensure_future(self.application_checker())
|
|
61
60
|
try:
|
|
62
|
-
event_loop.run_until_complete(self.
|
|
61
|
+
event_loop.run_until_complete(self.arun())
|
|
63
62
|
except KeyboardInterrupt:
|
|
64
63
|
logger.info("Exiting due to Ctrl-C/interrupt")
|
|
65
64
|
|
|
65
|
+
async def arun(self):
|
|
66
|
+
"""
|
|
67
|
+
Runs the asyncio event loop with our handler loop.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
class Done(Exception):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
async def handle():
|
|
74
|
+
await self.handle()
|
|
75
|
+
raise Done
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
await asyncio.gather(self.application_checker(), handle())
|
|
79
|
+
except Done:
|
|
80
|
+
pass
|
|
81
|
+
|
|
66
82
|
async def handle(self):
|
|
67
83
|
raise NotImplementedError("You must implement handle()")
|
|
68
84
|
|
|
@@ -179,15 +179,14 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
179
179
|
|
|
180
180
|
# You can't call AsyncToSync from a thread with a running event loop
|
|
181
181
|
try:
|
|
182
|
-
|
|
182
|
+
asyncio.get_running_loop()
|
|
183
183
|
except RuntimeError:
|
|
184
184
|
pass
|
|
185
185
|
else:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
"You cannot use AsyncToSync in the same thread as an async event loop - "
|
|
188
|
+
"just await the async function directly."
|
|
189
|
+
)
|
|
191
190
|
|
|
192
191
|
# Make a future for the return information
|
|
193
192
|
call_result: "Future[_R]" = Future()
|
|
@@ -196,7 +195,7 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
196
195
|
# need one for every sync frame, even if there's one above us in the
|
|
197
196
|
# same thread.
|
|
198
197
|
old_executor = getattr(self.executors, "current", None)
|
|
199
|
-
current_executor = CurrentThreadExecutor()
|
|
198
|
+
current_executor = CurrentThreadExecutor(old_executor)
|
|
200
199
|
self.executors.current = current_executor
|
|
201
200
|
|
|
202
201
|
# Wrapping context in list so it can be reassigned from within
|
|
@@ -207,7 +206,6 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
207
206
|
# an asyncio.CancelledError to.
|
|
208
207
|
task_context = getattr(SyncToAsync.threadlocal, "task_context", None)
|
|
209
208
|
|
|
210
|
-
loop = None
|
|
211
209
|
# Use call_soon_threadsafe to schedule a synchronous callback on the
|
|
212
210
|
# main event loop's thread if it's there, otherwise make a new loop
|
|
213
211
|
# in this thread.
|
|
@@ -217,35 +215,45 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
217
215
|
sys.exc_info(),
|
|
218
216
|
task_context,
|
|
219
217
|
context,
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
# prepare an awaitable which can be passed as is to self.main_wrap,
|
|
219
|
+
# so that `args` and `kwargs` don't need to be
|
|
220
|
+
# destructured when passed to self.main_wrap
|
|
221
|
+
# (which is required by `ParamSpec`)
|
|
222
|
+
# as that may cause overlapping arguments
|
|
223
|
+
self.awaitable(*args, **kwargs),
|
|
222
224
|
)
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
loop = asyncio.new_event_loop()
|
|
226
|
+
async def new_loop_wrap() -> None:
|
|
227
|
+
loop = asyncio.get_running_loop()
|
|
227
228
|
self.loop_thread_executors[loop] = current_executor
|
|
229
|
+
try:
|
|
230
|
+
await awaitable
|
|
231
|
+
finally:
|
|
232
|
+
del self.loop_thread_executors[loop]
|
|
233
|
+
|
|
234
|
+
if self.main_event_loop is not None:
|
|
235
|
+
try:
|
|
236
|
+
self.main_event_loop.call_soon_threadsafe(
|
|
237
|
+
self.main_event_loop.create_task, awaitable
|
|
238
|
+
)
|
|
239
|
+
except RuntimeError:
|
|
240
|
+
running_in_main_event_loop = False
|
|
241
|
+
else:
|
|
242
|
+
running_in_main_event_loop = True
|
|
243
|
+
# Run the CurrentThreadExecutor until the future is done.
|
|
244
|
+
current_executor.run_until_future(call_result)
|
|
245
|
+
else:
|
|
246
|
+
running_in_main_event_loop = False
|
|
247
|
+
|
|
248
|
+
if not running_in_main_event_loop:
|
|
249
|
+
# Make our own event loop - in a new thread - and run inside that.
|
|
228
250
|
loop_executor = ThreadPoolExecutor(max_workers=1)
|
|
229
|
-
loop_future = loop_executor.submit(
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
if current_executor:
|
|
233
|
-
# Run the CurrentThreadExecutor until the future is done
|
|
234
|
-
current_executor.run_until_future(loop_future)
|
|
251
|
+
loop_future = loop_executor.submit(asyncio.run, new_loop_wrap())
|
|
252
|
+
# Run the CurrentThreadExecutor until the future is done.
|
|
253
|
+
current_executor.run_until_future(loop_future)
|
|
235
254
|
# Wait for future and/or allow for exception propagation
|
|
236
255
|
loop_future.result()
|
|
237
|
-
else:
|
|
238
|
-
# Call it inside the existing loop
|
|
239
|
-
self.main_event_loop.call_soon_threadsafe(
|
|
240
|
-
self.main_event_loop.create_task, awaitable
|
|
241
|
-
)
|
|
242
|
-
if current_executor:
|
|
243
|
-
# Run the CurrentThreadExecutor until the future is done
|
|
244
|
-
current_executor.run_until_future(call_result)
|
|
245
256
|
finally:
|
|
246
|
-
# Clean up any executor we were running
|
|
247
|
-
if loop is not None:
|
|
248
|
-
del self.loop_thread_executors[loop]
|
|
249
257
|
_restore_context(context[0])
|
|
250
258
|
# Restore old current thread executor state
|
|
251
259
|
self.executors.current = old_executor
|
|
@@ -253,42 +261,6 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
253
261
|
# Wait for results from the future.
|
|
254
262
|
return call_result.result()
|
|
255
263
|
|
|
256
|
-
def _run_event_loop(self, loop, coro):
|
|
257
|
-
"""
|
|
258
|
-
Runs the given event loop (designed to be called in a thread).
|
|
259
|
-
"""
|
|
260
|
-
asyncio.set_event_loop(loop)
|
|
261
|
-
try:
|
|
262
|
-
loop.run_until_complete(coro)
|
|
263
|
-
finally:
|
|
264
|
-
try:
|
|
265
|
-
# mimic asyncio.run() behavior
|
|
266
|
-
# cancel unexhausted async generators
|
|
267
|
-
tasks = asyncio.all_tasks(loop)
|
|
268
|
-
for task in tasks:
|
|
269
|
-
task.cancel()
|
|
270
|
-
|
|
271
|
-
async def gather():
|
|
272
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
273
|
-
|
|
274
|
-
loop.run_until_complete(gather())
|
|
275
|
-
for task in tasks:
|
|
276
|
-
if task.cancelled():
|
|
277
|
-
continue
|
|
278
|
-
if task.exception() is not None:
|
|
279
|
-
loop.call_exception_handler(
|
|
280
|
-
{
|
|
281
|
-
"message": "unhandled exception during loop shutdown",
|
|
282
|
-
"exception": task.exception(),
|
|
283
|
-
"task": task,
|
|
284
|
-
}
|
|
285
|
-
)
|
|
286
|
-
if hasattr(loop, "shutdown_asyncgens"):
|
|
287
|
-
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
288
|
-
finally:
|
|
289
|
-
loop.close()
|
|
290
|
-
asyncio.set_event_loop(self.main_event_loop)
|
|
291
|
-
|
|
292
264
|
def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]:
|
|
293
265
|
"""
|
|
294
266
|
Include self for methods
|
|
@@ -302,8 +274,7 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
302
274
|
exc_info: "OptExcInfo",
|
|
303
275
|
task_context: "Optional[List[asyncio.Task[Any]]]",
|
|
304
276
|
context: List[contextvars.Context],
|
|
305
|
-
|
|
306
|
-
**kwargs: _P.kwargs,
|
|
277
|
+
awaitable: Union[Coroutine[Any, Any, _R], Awaitable[_R]],
|
|
307
278
|
) -> None:
|
|
308
279
|
"""
|
|
309
280
|
Wraps the awaitable with something that puts the result into the
|
|
@@ -326,9 +297,9 @@ class AsyncToSync(Generic[_P, _R]):
|
|
|
326
297
|
try:
|
|
327
298
|
raise exc_info[1]
|
|
328
299
|
except BaseException:
|
|
329
|
-
result = await
|
|
300
|
+
result = await awaitable
|
|
330
301
|
else:
|
|
331
|
-
result = await
|
|
302
|
+
result = await awaitable
|
|
332
303
|
except BaseException as e:
|
|
333
304
|
call_result.set_exception(e)
|
|
334
305
|
else:
|
|
@@ -13,18 +13,40 @@ class ApplicationCommunicator:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
def __init__(self, application, scope):
|
|
16
|
+
self._future = None
|
|
16
17
|
self.application = guarantee_single_callable(application)
|
|
17
18
|
self.scope = scope
|
|
18
|
-
self.
|
|
19
|
-
self.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
self.
|
|
27
|
-
|
|
19
|
+
self._input_queue = None
|
|
20
|
+
self._output_queue = None
|
|
21
|
+
|
|
22
|
+
# For Python 3.9 we need to lazily bind the queues, on 3.10+ they bind the
|
|
23
|
+
# event loop lazily.
|
|
24
|
+
@property
|
|
25
|
+
def input_queue(self):
|
|
26
|
+
if self._input_queue is None:
|
|
27
|
+
self._input_queue = asyncio.Queue()
|
|
28
|
+
return self._input_queue
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def output_queue(self):
|
|
32
|
+
if self._output_queue is None:
|
|
33
|
+
self._output_queue = asyncio.Queue()
|
|
34
|
+
return self._output_queue
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def future(self):
|
|
38
|
+
if self._future is None:
|
|
39
|
+
# Clear context - this ensures that context vars set in the testing scope
|
|
40
|
+
# are not "leaked" into the application which would normally begin with
|
|
41
|
+
# an empty context. In Python >= 3.11 this could also be written as:
|
|
42
|
+
# asyncio.create_task(..., context=contextvars.Context())
|
|
43
|
+
self._future = contextvars.Context().run(
|
|
44
|
+
asyncio.create_task,
|
|
45
|
+
self.application(
|
|
46
|
+
self.scope, self.input_queue.get, self.output_queue.put
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
return self._future
|
|
28
50
|
|
|
29
51
|
async def wait(self, timeout=1):
|
|
30
52
|
"""
|
|
@@ -46,11 +68,15 @@ class ApplicationCommunicator:
|
|
|
46
68
|
pass
|
|
47
69
|
|
|
48
70
|
def stop(self, exceptions=True):
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
future = self._future
|
|
72
|
+
if future is None:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if not future.done():
|
|
76
|
+
future.cancel()
|
|
51
77
|
elif exceptions:
|
|
52
78
|
# Give a chance to raise any exceptions
|
|
53
|
-
|
|
79
|
+
future.result()
|
|
54
80
|
|
|
55
81
|
def __del__(self):
|
|
56
82
|
# Clean up on deletion
|
|
@@ -64,6 +90,10 @@ class ApplicationCommunicator:
|
|
|
64
90
|
"""
|
|
65
91
|
Sends a single message to the application
|
|
66
92
|
"""
|
|
93
|
+
# Make sure there's not an exception to raise from the task
|
|
94
|
+
if self.future.done():
|
|
95
|
+
self.future.result()
|
|
96
|
+
|
|
67
97
|
# Give it the message
|
|
68
98
|
await self.input_queue.put(message)
|
|
69
99
|
|
|
@@ -94,6 +124,10 @@ class ApplicationCommunicator:
|
|
|
94
124
|
"""
|
|
95
125
|
Checks that there is no message to receive in the given time.
|
|
96
126
|
"""
|
|
127
|
+
# Make sure there's not an exception to raise from the task
|
|
128
|
+
if self.future.done():
|
|
129
|
+
self.future.result()
|
|
130
|
+
|
|
97
131
|
# `interval` has precedence over `timeout`
|
|
98
132
|
start = time.monotonic()
|
|
99
133
|
while time.monotonic() - start < timeout:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: asgiref
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.1
|
|
4
4
|
Summary: ASGI specs, helper code, and adapters
|
|
5
5
|
Home-page: https://github.com/django/asgiref/
|
|
6
6
|
Author: Django Software Foundation
|
|
@@ -17,19 +17,20 @@ Classifier: Operating System :: OS Independent
|
|
|
17
17
|
Classifier: Programming Language :: Python
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: typing_extensions>=4; python_version < "3.11"
|
|
29
29
|
Provides-Extra: tests
|
|
30
30
|
Requires-Dist: pytest; extra == "tests"
|
|
31
31
|
Requires-Dist: pytest-asyncio; extra == "tests"
|
|
32
|
-
Requires-Dist: mypy>=0
|
|
32
|
+
Requires-Dist: mypy>=1.14.0; extra == "tests"
|
|
33
|
+
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
asgiref
|
|
35
36
|
=======
|
|
@@ -129,7 +130,7 @@ file handles for incoming POST bodies).
|
|
|
129
130
|
Dependencies
|
|
130
131
|
------------
|
|
131
132
|
|
|
132
|
-
``asgiref`` requires Python 3.
|
|
133
|
+
``asgiref`` requires Python 3.9 or higher.
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
Contributing
|
|
@@ -179,7 +180,7 @@ Then, build and push the packages::
|
|
|
179
180
|
|
|
180
181
|
python -m build
|
|
181
182
|
twine upload dist/*
|
|
182
|
-
rm -r
|
|
183
|
+
rm -r asgiref.egg-info dist
|
|
183
184
|
|
|
184
185
|
|
|
185
186
|
Implementation Details
|
|
@@ -16,11 +16,11 @@ classifiers =
|
|
|
16
16
|
Programming Language :: Python
|
|
17
17
|
Programming Language :: Python :: 3
|
|
18
18
|
Programming Language :: Python :: 3 :: Only
|
|
19
|
-
Programming Language :: Python :: 3.8
|
|
20
19
|
Programming Language :: Python :: 3.9
|
|
21
20
|
Programming Language :: Python :: 3.10
|
|
22
21
|
Programming Language :: Python :: 3.11
|
|
23
22
|
Programming Language :: Python :: 3.12
|
|
23
|
+
Programming Language :: Python :: 3.13
|
|
24
24
|
Topic :: Internet :: WWW/HTTP
|
|
25
25
|
project_urls =
|
|
26
26
|
Documentation = https://asgi.readthedocs.io/
|
|
@@ -28,7 +28,7 @@ project_urls =
|
|
|
28
28
|
Changelog = https://github.com/django/asgiref/blob/master/CHANGELOG.txt
|
|
29
29
|
|
|
30
30
|
[options]
|
|
31
|
-
python_requires = >=3.
|
|
31
|
+
python_requires = >=3.9
|
|
32
32
|
packages = find:
|
|
33
33
|
include_package_data = true
|
|
34
34
|
install_requires =
|
|
@@ -39,11 +39,12 @@ zip_safe = false
|
|
|
39
39
|
tests =
|
|
40
40
|
pytest
|
|
41
41
|
pytest-asyncio
|
|
42
|
-
mypy>=0
|
|
42
|
+
mypy>=1.14.0
|
|
43
43
|
|
|
44
44
|
[tool:pytest]
|
|
45
45
|
testpaths = tests
|
|
46
46
|
asyncio_mode = strict
|
|
47
|
+
asyncio_default_fixture_loop_scope = function
|
|
47
48
|
|
|
48
49
|
[flake8]
|
|
49
50
|
exclude = venv/*,tox/*,specs/*
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from asgiref.local import Local
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def disable_gc_for_garbage_collection_test() -> None:
|
|
10
|
+
# Disable automatic garbage collection. To have control over when
|
|
11
|
+
# garbage collection is performed. This is necessary to ensure that another
|
|
12
|
+
# that thread doesn't accidentally trigger it by simply executing code.
|
|
13
|
+
gc.disable()
|
|
14
|
+
|
|
15
|
+
# Delete the garbage list(`gc.garbage`) to ensure that other tests don't
|
|
16
|
+
# interfere with this test.
|
|
17
|
+
gc.collect()
|
|
18
|
+
|
|
19
|
+
# Set the garbage collection debugging flag to store all unreachable
|
|
20
|
+
# objects in `gc.garbage`. This is necessary to ensure that the
|
|
21
|
+
# garbage list is empty after execute test code. Otherwise, the test
|
|
22
|
+
# will always pass. The garbage list isn't automatically populated
|
|
23
|
+
# because it costs extra CPU cycles
|
|
24
|
+
gc.set_debug(gc.DEBUG_SAVEALL)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clean_up_after_garbage_collection_test() -> None:
|
|
28
|
+
# Clean up the garbage collection settings. Re-enable automatic garbage
|
|
29
|
+
# collection. This step is mandatory to avoid running other tests without
|
|
30
|
+
# automatic garbage collection.
|
|
31
|
+
gc.set_debug(0)
|
|
32
|
+
gc.enable()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.skipif(
|
|
36
|
+
sys.implementation.name == "pypy", reason="Test relies on CPython GC internals"
|
|
37
|
+
)
|
|
38
|
+
def test_thread_critical_Local_remove_all_reference_cycles() -> None:
|
|
39
|
+
try:
|
|
40
|
+
# given
|
|
41
|
+
# Disable automatic garbage collection and set debugging flag.
|
|
42
|
+
disable_gc_for_garbage_collection_test()
|
|
43
|
+
|
|
44
|
+
# when
|
|
45
|
+
# Create thread critical Local object in sync context.
|
|
46
|
+
try:
|
|
47
|
+
getattr(Local(thread_critical=True), "missing")
|
|
48
|
+
except AttributeError:
|
|
49
|
+
pass
|
|
50
|
+
# Enforce garbage collection to populate the garbage list for inspection.
|
|
51
|
+
gc.collect()
|
|
52
|
+
|
|
53
|
+
# then
|
|
54
|
+
# Ensure that the garbage list is empty. The garbage list is only valid
|
|
55
|
+
# until the next collection cycle so we can only make assertions about it
|
|
56
|
+
# before re-enabling automatic collection.
|
|
57
|
+
assert gc.garbage == []
|
|
58
|
+
# Restore garbage collection settings to their original state. This should always be run to avoid interfering
|
|
59
|
+
# with other tests to ensure that code should be executed in the `finally' block.
|
|
60
|
+
finally:
|
|
61
|
+
clean_up_after_garbage_collection_test()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import gc
|
|
3
3
|
import threading
|
|
4
|
+
from threading import Thread
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -338,3 +339,55 @@ def test_thread_critical_local_not_context_dependent_in_sync_thread():
|
|
|
338
339
|
# inner value was set inside a new async context, meaning that
|
|
339
340
|
# we do not see it, as context vars don't propagate up the stack
|
|
340
341
|
assert not hasattr(test_local_not_tc, "test_value")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_visibility_thread_asgiref() -> None:
|
|
345
|
+
"""Check visibility with subthreads."""
|
|
346
|
+
test_local = Local()
|
|
347
|
+
test_local.value = 0
|
|
348
|
+
|
|
349
|
+
def _test() -> None:
|
|
350
|
+
# Local() is cleared when changing thread
|
|
351
|
+
assert not hasattr(test_local, "value")
|
|
352
|
+
setattr(test_local, "value", 1)
|
|
353
|
+
assert test_local.value == 1
|
|
354
|
+
|
|
355
|
+
thread = Thread(target=_test)
|
|
356
|
+
thread.start()
|
|
357
|
+
thread.join()
|
|
358
|
+
|
|
359
|
+
assert test_local.value == 0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.mark.asyncio
|
|
363
|
+
async def test_visibility_task() -> None:
|
|
364
|
+
"""Check visibility with asyncio tasks."""
|
|
365
|
+
test_local = Local()
|
|
366
|
+
test_local.value = 0
|
|
367
|
+
|
|
368
|
+
async def _test() -> None:
|
|
369
|
+
# Local is inherited when changing task
|
|
370
|
+
assert test_local.value == 0
|
|
371
|
+
test_local.value = 1
|
|
372
|
+
assert test_local.value == 1
|
|
373
|
+
|
|
374
|
+
await asyncio.create_task(_test())
|
|
375
|
+
|
|
376
|
+
# Changes should not leak to the caller
|
|
377
|
+
assert test_local.value == 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@pytest.mark.asyncio
|
|
381
|
+
async def test_deletion() -> None:
|
|
382
|
+
"""Check visibility with asyncio tasks."""
|
|
383
|
+
test_local = Local()
|
|
384
|
+
test_local.value = 123
|
|
385
|
+
|
|
386
|
+
async def _test() -> None:
|
|
387
|
+
# Local is inherited when changing task
|
|
388
|
+
assert test_local.value == 123
|
|
389
|
+
del test_local.value
|
|
390
|
+
assert not hasattr(test_local, "value")
|
|
391
|
+
|
|
392
|
+
await asyncio.create_task(_test())
|
|
393
|
+
assert test_local.value == 123
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import socket as sock
|
|
3
|
-
from functools import partial
|
|
4
3
|
|
|
5
4
|
import pytest
|
|
5
|
+
import pytest_asyncio
|
|
6
6
|
|
|
7
7
|
from asgiref.server import StatelessServer
|
|
8
8
|
|
|
@@ -74,8 +74,8 @@ class Client:
|
|
|
74
74
|
self._sock.close()
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
@
|
|
78
|
-
def server():
|
|
77
|
+
@pytest_asyncio.fixture(scope="function")
|
|
78
|
+
async def server():
|
|
79
79
|
async def app(scope, receive, send):
|
|
80
80
|
while True:
|
|
81
81
|
msg = await receive()
|
|
@@ -92,25 +92,12 @@ async def check_client_msg(client, expected_address, expected_msg):
|
|
|
92
92
|
assert server_addr == expected_address
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
function because it is a while True loop without break. Use this method to close
|
|
98
|
-
server automatically."""
|
|
99
|
-
loop = asyncio.get_running_loop()
|
|
100
|
-
task = asyncio.ensure_future(fut, loop=loop)
|
|
101
|
-
await asyncio.sleep(timeout)
|
|
102
|
-
task.cancel()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_stateless_server(server):
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_stateless_server(server):
|
|
106
97
|
"""StatelessServer can be instantiated with an ASGI 3 application."""
|
|
107
98
|
"""Create a UDP Server can register instance based on name from message of client.
|
|
108
99
|
Clients can communicate to other client by name through server"""
|
|
109
100
|
|
|
110
|
-
loop = asyncio.new_event_loop()
|
|
111
|
-
asyncio.set_event_loop(loop)
|
|
112
|
-
server.handle = partial(server_auto_close, fut=server.handle(), timeout=1.0)
|
|
113
|
-
|
|
114
101
|
client1 = Client(name="client1")
|
|
115
102
|
client2 = Client(name="client2")
|
|
116
103
|
|
|
@@ -124,30 +111,35 @@ def test_stateless_server(server):
|
|
|
124
111
|
await check_client_msg(client2, server.address, b"Welcome")
|
|
125
112
|
await check_client_msg(client2, server.address, b"Hello")
|
|
126
113
|
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
class Done(Exception):
|
|
115
|
+
pass
|
|
129
116
|
|
|
130
|
-
|
|
117
|
+
async def do_test():
|
|
118
|
+
await asyncio.gather(check_client1_behavior(), check_client2_behavior())
|
|
119
|
+
raise Done
|
|
131
120
|
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
try:
|
|
122
|
+
await asyncio.gather(server.arun(), do_test())
|
|
123
|
+
except Done:
|
|
124
|
+
pass
|
|
134
125
|
|
|
135
126
|
|
|
136
|
-
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_server_delete_instance(server):
|
|
137
129
|
"""The max_applications of Server is 10. After 20 times register, application number should be 10."""
|
|
138
|
-
loop = asyncio.new_event_loop()
|
|
139
|
-
asyncio.set_event_loop(loop)
|
|
140
|
-
server.handle = partial(server_auto_close, fut=server.handle(), timeout=1.0)
|
|
141
|
-
|
|
142
130
|
client1 = Client(name="client1")
|
|
143
131
|
|
|
132
|
+
class Done(Exception):
|
|
133
|
+
pass
|
|
134
|
+
|
|
144
135
|
async def client1_multiple_register():
|
|
145
136
|
for i in range(20):
|
|
146
137
|
await client1.register(server.address, name=f"client{i}")
|
|
147
138
|
print(f"client{i}")
|
|
148
139
|
await check_client_msg(client1, server.address, b"Welcome")
|
|
140
|
+
raise Done
|
|
149
141
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
142
|
+
try:
|
|
143
|
+
await asyncio.gather(client1_multiple_register(), server.arun())
|
|
144
|
+
except Done:
|
|
145
|
+
pass
|
|
@@ -7,6 +7,7 @@ import time
|
|
|
7
7
|
import warnings
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
9
|
from functools import wraps
|
|
10
|
+
from typing import Any
|
|
10
11
|
from unittest import TestCase
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
@@ -1174,3 +1175,109 @@ async def test_inner_shield_sync_and_async_middleware_sync_task():
|
|
|
1174
1175
|
assert task_complete
|
|
1175
1176
|
|
|
1176
1177
|
assert task_executed
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def test_async_to_sync_overlapping_kwargs() -> None:
|
|
1181
|
+
"""
|
|
1182
|
+
Tests that AsyncToSync correctly passes through kwargs to the wrapped function,
|
|
1183
|
+
particularly in the case where the wrapped function uses same names for the parameters
|
|
1184
|
+
as the wrapper.
|
|
1185
|
+
"""
|
|
1186
|
+
|
|
1187
|
+
@async_to_sync
|
|
1188
|
+
async def test_function(**kwargs: Any) -> None:
|
|
1189
|
+
assert kwargs
|
|
1190
|
+
|
|
1191
|
+
# AsyncToSync.main_wrap has a param named `context`.
|
|
1192
|
+
# So we pass the same argument here to test for the error
|
|
1193
|
+
# "AsyncToSync.main_wrap() got multiple values for argument '<kwarg>'"
|
|
1194
|
+
test_function(context=1)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@pytest.mark.asyncio
|
|
1198
|
+
async def test_sync_to_async_overlapping_kwargs() -> None:
|
|
1199
|
+
"""
|
|
1200
|
+
Tests that SyncToAsync correctly passes through kwargs to the wrapped function,
|
|
1201
|
+
particularly in the case where the wrapped function uses same names for the parameters
|
|
1202
|
+
as the wrapper.
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
@sync_to_async
|
|
1206
|
+
def test_function(**kwargs: Any) -> None:
|
|
1207
|
+
assert kwargs
|
|
1208
|
+
|
|
1209
|
+
# SyncToAsync.__call__.loop.run_in_executor has a param named `task_context`.
|
|
1210
|
+
await test_function(task_context=1)
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def test_nested_task() -> None:
|
|
1214
|
+
async def inner() -> asyncio.Task[None]:
|
|
1215
|
+
return asyncio.create_task(sync_to_async(print)("inner"))
|
|
1216
|
+
|
|
1217
|
+
async def main() -> None:
|
|
1218
|
+
task = await sync_to_async(async_to_sync(inner))()
|
|
1219
|
+
await task
|
|
1220
|
+
|
|
1221
|
+
async_to_sync(main)()
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def test_nested_task_later() -> None:
|
|
1225
|
+
def later(fut: asyncio.Future[asyncio.Task[None]]) -> None:
|
|
1226
|
+
task = asyncio.create_task(sync_to_async(print)("later"))
|
|
1227
|
+
fut.set_result(task)
|
|
1228
|
+
|
|
1229
|
+
async def inner() -> asyncio.Future[asyncio.Task[None]]:
|
|
1230
|
+
loop = asyncio.get_running_loop()
|
|
1231
|
+
fut = loop.create_future()
|
|
1232
|
+
loop.call_later(0.1, later, fut)
|
|
1233
|
+
return fut
|
|
1234
|
+
|
|
1235
|
+
async def main() -> None:
|
|
1236
|
+
fut = await sync_to_async(async_to_sync(inner))()
|
|
1237
|
+
task = await fut
|
|
1238
|
+
await task
|
|
1239
|
+
|
|
1240
|
+
async_to_sync(main)()
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def test_double_nested_task() -> None:
|
|
1244
|
+
async def inner() -> asyncio.Task[None]:
|
|
1245
|
+
return asyncio.create_task(sync_to_async(print)("inner"))
|
|
1246
|
+
|
|
1247
|
+
async def outer() -> asyncio.Task[asyncio.Task[None]]:
|
|
1248
|
+
return asyncio.create_task(sync_to_async(async_to_sync(inner))())
|
|
1249
|
+
|
|
1250
|
+
async def main() -> None:
|
|
1251
|
+
outer_task = await sync_to_async(async_to_sync(outer))()
|
|
1252
|
+
inner_task = await outer_task
|
|
1253
|
+
await inner_task
|
|
1254
|
+
|
|
1255
|
+
async_to_sync(main)()
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
# asyncio.Barrier is new in Python 3.11. Nest definition (rather than using
|
|
1259
|
+
# skipIf) to avoid mypy error.
|
|
1260
|
+
if sys.version_info >= (3, 11):
|
|
1261
|
+
|
|
1262
|
+
def test_two_nested_tasks_with_asyncio_run() -> None:
|
|
1263
|
+
barrier = asyncio.Barrier(3)
|
|
1264
|
+
event = threading.Event()
|
|
1265
|
+
|
|
1266
|
+
async def inner() -> None:
|
|
1267
|
+
task = asyncio.create_task(sync_to_async(event.wait)())
|
|
1268
|
+
await barrier.wait()
|
|
1269
|
+
await task
|
|
1270
|
+
|
|
1271
|
+
async def outer() -> tuple[asyncio.Task[None], asyncio.Task[None]]:
|
|
1272
|
+
task0 = asyncio.create_task(inner())
|
|
1273
|
+
task1 = asyncio.create_task(inner())
|
|
1274
|
+
await barrier.wait()
|
|
1275
|
+
event.set()
|
|
1276
|
+
return task0, task1
|
|
1277
|
+
|
|
1278
|
+
async def main() -> None:
|
|
1279
|
+
task0, task1 = await sync_to_async(async_to_sync(outer))()
|
|
1280
|
+
await task0
|
|
1281
|
+
await task1
|
|
1282
|
+
|
|
1283
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from asgiref.testing import ApplicationCommunicator
|
|
6
|
+
from asgiref.wsgi import WsgiToAsgi
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_receive_nothing():
|
|
11
|
+
"""
|
|
12
|
+
Tests ApplicationCommunicator.receive_nothing to return the correct value.
|
|
13
|
+
"""
|
|
14
|
+
# Get an ApplicationCommunicator instance
|
|
15
|
+
def wsgi_application(environ, start_response):
|
|
16
|
+
start_response("200 OK", [])
|
|
17
|
+
yield b"content"
|
|
18
|
+
|
|
19
|
+
application = WsgiToAsgi(wsgi_application)
|
|
20
|
+
instance = ApplicationCommunicator(
|
|
21
|
+
application,
|
|
22
|
+
{
|
|
23
|
+
"type": "http",
|
|
24
|
+
"http_version": "1.0",
|
|
25
|
+
"method": "GET",
|
|
26
|
+
"path": "/foo/",
|
|
27
|
+
"query_string": b"bar=baz",
|
|
28
|
+
"headers": [],
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# No event
|
|
33
|
+
assert await instance.receive_nothing() is True
|
|
34
|
+
|
|
35
|
+
# Produce 3 events to receive
|
|
36
|
+
await instance.send_input({"type": "http.request"})
|
|
37
|
+
# Start event of the response
|
|
38
|
+
assert await instance.receive_nothing() is False
|
|
39
|
+
await instance.receive_output()
|
|
40
|
+
# First body event of the response announcing further body event
|
|
41
|
+
assert await instance.receive_nothing() is False
|
|
42
|
+
await instance.receive_output()
|
|
43
|
+
# Last body event of the response
|
|
44
|
+
assert await instance.receive_nothing() is False
|
|
45
|
+
await instance.receive_output()
|
|
46
|
+
# Response received completely
|
|
47
|
+
assert await instance.receive_nothing(0.01) is True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_receive_nothing_lazy_loop():
|
|
51
|
+
"""
|
|
52
|
+
Tests ApplicationCommunicator.receive_nothing to return the correct value.
|
|
53
|
+
"""
|
|
54
|
+
# Get an ApplicationCommunicator instance
|
|
55
|
+
def wsgi_application(environ, start_response):
|
|
56
|
+
start_response("200 OK", [])
|
|
57
|
+
yield b"content"
|
|
58
|
+
|
|
59
|
+
application = WsgiToAsgi(wsgi_application)
|
|
60
|
+
instance = ApplicationCommunicator(
|
|
61
|
+
application,
|
|
62
|
+
{
|
|
63
|
+
"type": "http",
|
|
64
|
+
"http_version": "1.0",
|
|
65
|
+
"method": "GET",
|
|
66
|
+
"path": "/foo/",
|
|
67
|
+
"query_string": b"bar=baz",
|
|
68
|
+
"headers": [],
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def test():
|
|
73
|
+
# No event
|
|
74
|
+
assert await instance.receive_nothing() is True
|
|
75
|
+
|
|
76
|
+
# Produce 3 events to receive
|
|
77
|
+
await instance.send_input({"type": "http.request"})
|
|
78
|
+
# Start event of the response
|
|
79
|
+
assert await instance.receive_nothing() is False
|
|
80
|
+
await instance.receive_output()
|
|
81
|
+
# First body event of the response announcing further body event
|
|
82
|
+
assert await instance.receive_nothing() is False
|
|
83
|
+
await instance.receive_output()
|
|
84
|
+
# Last body event of the response
|
|
85
|
+
assert await instance.receive_nothing() is False
|
|
86
|
+
await instance.receive_output()
|
|
87
|
+
# Response received completely
|
|
88
|
+
assert await instance.receive_nothing(0.01) is True
|
|
89
|
+
|
|
90
|
+
asyncio.run(test())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.8.1"
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from asgiref.testing import ApplicationCommunicator
|
|
4
|
-
from asgiref.wsgi import WsgiToAsgi
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@pytest.mark.asyncio
|
|
8
|
-
async def test_receive_nothing():
|
|
9
|
-
"""
|
|
10
|
-
Tests ApplicationCommunicator.receive_nothing to return the correct value.
|
|
11
|
-
"""
|
|
12
|
-
# Get an ApplicationCommunicator instance
|
|
13
|
-
def wsgi_application(environ, start_response):
|
|
14
|
-
start_response("200 OK", [])
|
|
15
|
-
yield b"content"
|
|
16
|
-
|
|
17
|
-
application = WsgiToAsgi(wsgi_application)
|
|
18
|
-
instance = ApplicationCommunicator(
|
|
19
|
-
application,
|
|
20
|
-
{
|
|
21
|
-
"type": "http",
|
|
22
|
-
"http_version": "1.0",
|
|
23
|
-
"method": "GET",
|
|
24
|
-
"path": "/foo/",
|
|
25
|
-
"query_string": b"bar=baz",
|
|
26
|
-
"headers": [],
|
|
27
|
-
},
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# No event
|
|
31
|
-
assert await instance.receive_nothing() is True
|
|
32
|
-
|
|
33
|
-
# Produce 3 events to receive
|
|
34
|
-
await instance.send_input({"type": "http.request"})
|
|
35
|
-
# Start event of the response
|
|
36
|
-
assert await instance.receive_nothing() is False
|
|
37
|
-
await instance.receive_output()
|
|
38
|
-
# First body event of the response announcing further body event
|
|
39
|
-
assert await instance.receive_nothing() is False
|
|
40
|
-
await instance.receive_output()
|
|
41
|
-
# Last body event of the response
|
|
42
|
-
assert await instance.receive_nothing() is False
|
|
43
|
-
await instance.receive_output()
|
|
44
|
-
# Response received completely
|
|
45
|
-
assert await instance.receive_nothing(0.01) is True
|
|
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
|