asgiref 3.8.1__tar.gz → 3.9.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.
Files changed (34) hide show
  1. {asgiref-3.8.1/asgiref.egg-info → asgiref-3.9.0}/PKG-INFO +8 -7
  2. {asgiref-3.8.1 → asgiref-3.9.0}/README.rst +2 -2
  3. asgiref-3.9.0/asgiref/__init__.py +1 -0
  4. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/current_thread_executor.py +42 -34
  5. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/local.py +18 -14
  6. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/server.py +18 -2
  7. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/sync.py +41 -70
  8. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/testing.py +47 -13
  9. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/typing.py +1 -0
  10. {asgiref-3.8.1 → asgiref-3.9.0/asgiref.egg-info}/PKG-INFO +8 -7
  11. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref.egg-info/SOURCES.txt +1 -0
  12. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref.egg-info/requires.txt +1 -1
  13. {asgiref-3.8.1 → asgiref-3.9.0}/setup.cfg +4 -3
  14. asgiref-3.9.0/tests/test_garbage_collection.py +55 -0
  15. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_local.py +37 -0
  16. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_server.py +24 -32
  17. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_sync.py +107 -0
  18. asgiref-3.9.0/tests/test_testing.py +90 -0
  19. {asgiref-3.8.1 → asgiref-3.9.0}/tox.ini +1 -1
  20. asgiref-3.8.1/asgiref/__init__.py +0 -1
  21. asgiref-3.8.1/tests/test_testing.py +0 -45
  22. {asgiref-3.8.1 → asgiref-3.9.0}/LICENSE +0 -0
  23. {asgiref-3.8.1 → asgiref-3.9.0}/MANIFEST.in +0 -0
  24. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/compatibility.py +0 -0
  25. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/py.typed +0 -0
  26. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/timeout.py +0 -0
  27. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref/wsgi.py +0 -0
  28. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref.egg-info/dependency_links.txt +0 -0
  29. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref.egg-info/not-zip-safe +0 -0
  30. {asgiref-3.8.1 → asgiref-3.9.0}/asgiref.egg-info/top_level.txt +0 -0
  31. {asgiref-3.8.1 → asgiref-3.9.0}/setup.py +0 -0
  32. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_compatibility.py +0 -0
  33. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_sync_contextvars.py +0 -0
  34. {asgiref-3.8.1 → asgiref-3.9.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.8.1
3
+ Version: 3.9.0
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.8
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.800; extra == "tests"
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.8 or higher.
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 build/ dist/
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.8 or higher.
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 build/ dist/
149
+ rm -r asgiref.egg-info dist
150
150
 
151
151
 
152
152
  Implementation Details
@@ -0,0 +1 @@
1
+ __version__ = "3.9.0"
@@ -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 TYPE_CHECKING, Any, Callable, TypeVar, Union
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._work_queue: queue.Queue[Union[_WorkItem, "Future[Any]"]] = queue.Queue()
59
- self._broken = False
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
- future.add_done_callback(self._work_queue.put)
72
- # Keep getting and running work items until we get the future we're waiting for
73
- # back via the future's done callback.
74
- try:
75
- while True:
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._work_queue.get()
78
- if work_item is future:
79
- return
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 _submit(
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
- # Python 3.9+ has a new signature for submit with a "/" after `fn`, to enforce
108
- # it to be a positional argument. If we ignore[override] mypy on 3.9+ will be
109
- # happy but 3.8 will say that the ignore comment is unused, even when
110
- # defining them differently based on sys.version_info.
111
- # We should be able to remove this when we drop support for 3.8.
112
- if not TYPE_CHECKING:
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
- def submit(self, fn, *args, **kwargs):
115
- return self._submit(fn, *args, **kwargs)
122
+ # Return the future
123
+ return f
@@ -2,37 +2,38 @@ import asyncio
2
2
  import contextlib
3
3
  import contextvars
4
4
  import threading
5
- from typing import Any, Dict, Union
5
+ from typing import Any, Union
6
6
 
7
7
 
8
8
  class _CVar:
9
9
  """Storage utility for Local."""
10
10
 
11
11
  def __init__(self) -> None:
12
- self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar(
13
- "asgiref.local"
14
- )
12
+ self._data: dict[str, contextvars.ContextVar[Any]] = {}
15
13
 
16
- def __getattr__(self, key):
17
- storage_object = self._data.get({})
14
+ def __getattr__(self, key: str) -> Any:
18
15
  try:
19
- return storage_object[key]
16
+ var = self._data[key]
20
17
  except KeyError:
21
18
  raise AttributeError(f"{self!r} object has no attribute {key!r}")
22
19
 
20
+ try:
21
+ return var.get()
22
+ except LookupError:
23
+ raise AttributeError(f"{self!r} object has no attribute {key!r}")
24
+
23
25
  def __setattr__(self, key: str, value: Any) -> None:
24
26
  if key == "_data":
25
27
  return super().__setattr__(key, value)
26
28
 
27
- storage_object = self._data.get({})
28
- storage_object[key] = value
29
- self._data.set(storage_object)
29
+ var = self._data.get(key)
30
+ if var is None:
31
+ self._data[key] = var = contextvars.ContextVar(key)
32
+ var.set(value)
30
33
 
31
34
  def __delattr__(self, key: str) -> None:
32
- storage_object = self._data.get({})
33
- if key in storage_object:
34
- del storage_object[key]
35
- self._data.set(storage_object)
35
+ if key in self._data:
36
+ del self._data[key]
36
37
  else:
37
38
  raise AttributeError(f"{self!r} object has no attribute {key!r}")
38
39
 
@@ -82,12 +83,15 @@ class Local:
82
83
  def _lock_storage(self):
83
84
  # Thread safe access to storage
84
85
  if self._thread_critical:
86
+ is_async = True
85
87
  try:
86
88
  # this is a test for are we in a async or sync
87
89
  # thread - will raise RuntimeError if there is
88
90
  # no current loop
89
91
  asyncio.get_running_loop()
90
92
  except RuntimeError:
93
+ is_async = False
94
+ if not is_async:
91
95
  # We are in a sync thread, the storage is
92
96
  # just the plain thread local (i.e, "global within
93
97
  # 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.handle())
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
- event_loop = asyncio.get_running_loop()
182
+ asyncio.get_running_loop()
183
183
  except RuntimeError:
184
184
  pass
185
185
  else:
186
- if event_loop.is_running():
187
- raise RuntimeError(
188
- "You cannot use AsyncToSync in the same thread as an async event loop - "
189
- "just await the async function directly."
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
- *args,
221
- **kwargs,
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
- if not (self.main_event_loop and self.main_event_loop.is_running()):
225
- # Make our own event loop - in a new thread - and run inside that.
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
- self._run_event_loop, loop, awaitable
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
- *args: _P.args,
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 self.awaitable(*args, **kwargs)
300
+ result = await awaitable
330
301
  else:
331
- result = await self.awaitable(*args, **kwargs)
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.input_queue = asyncio.Queue()
19
- self.output_queue = asyncio.Queue()
20
- # Clear context - this ensures that context vars set in the testing scope
21
- # are not "leaked" into the application which would normally begin with
22
- # an empty context. In Python >= 3.11 this could also be written as:
23
- # asyncio.create_task(..., context=contextvars.Context())
24
- self.future = contextvars.Context().run(
25
- asyncio.create_task,
26
- self.application(scope, self.input_queue.get, self.output_queue.put),
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
- if not self.future.done():
50
- self.future.cancel()
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
- self.future.result()
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:
@@ -189,6 +189,7 @@ class WebSocketResponseBodyEvent(TypedDict):
189
189
  class WebSocketDisconnectEvent(TypedDict):
190
190
  type: Literal["websocket.disconnect"]
191
191
  code: int
192
+ reason: Optional[str]
192
193
 
193
194
 
194
195
  class WebSocketCloseEvent(TypedDict):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.8.1
3
+ Version: 3.9.0
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.8
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.800; extra == "tests"
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.8 or higher.
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 build/ dist/
183
+ rm -r asgiref.egg-info dist
183
184
 
184
185
 
185
186
  Implementation Details
@@ -22,6 +22,7 @@ asgiref.egg-info/not-zip-safe
22
22
  asgiref.egg-info/requires.txt
23
23
  asgiref.egg-info/top_level.txt
24
24
  tests/test_compatibility.py
25
+ tests/test_garbage_collection.py
25
26
  tests/test_local.py
26
27
  tests/test_server.py
27
28
  tests/test_sync.py
@@ -5,4 +5,4 @@ typing_extensions>=4
5
5
  [tests]
6
6
  pytest
7
7
  pytest-asyncio
8
- mypy>=0.800
8
+ mypy>=1.14.0
@@ -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.8
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.800
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,55 @@
1
+ import gc
2
+
3
+ from asgiref.local import Local
4
+
5
+
6
+ def disable_gc_for_garbage_collection_test() -> None:
7
+ # Disable automatic garbage collection. To have control over when
8
+ # garbage collection is performed. This is necessary to ensure that another
9
+ # that thread doesn't accidentally trigger it by simply executing code.
10
+ gc.disable()
11
+
12
+ # Delete the garbage list(`gc.garbage`) to ensure that other tests don't
13
+ # interfere with this test.
14
+ gc.collect()
15
+
16
+ # Set the garbage collection debugging flag to store all unreachable
17
+ # objects in `gc.garbage`. This is necessary to ensure that the
18
+ # garbage list is empty after execute test code. Otherwise, the test
19
+ # will always pass. The garbage list isn't automatically populated
20
+ # because it costs extra CPU cycles
21
+ gc.set_debug(gc.DEBUG_SAVEALL)
22
+
23
+
24
+ def clean_up_after_garbage_collection_test() -> None:
25
+ # Clean up the garbage collection settings. Re-enable automatic garbage
26
+ # collection. This step is mandatory to avoid running other tests without
27
+ # automatic garbage collection.
28
+ gc.set_debug(0)
29
+ gc.enable()
30
+
31
+
32
+ def test_thread_critical_Local_remove_all_reference_cycles() -> None:
33
+ try:
34
+ # given
35
+ # Disable automatic garbage collection and set debugging flag.
36
+ disable_gc_for_garbage_collection_test()
37
+
38
+ # when
39
+ # Create thread critical Local object in sync context.
40
+ try:
41
+ getattr(Local(thread_critical=True), "missing")
42
+ except AttributeError:
43
+ pass
44
+ # Enforce garbage collection to populate the garbage list for inspection.
45
+ gc.collect()
46
+
47
+ # then
48
+ # Ensure that the garbage list is empty. The garbage list is only valid
49
+ # until the next collection cycle so we can only make assertions about it
50
+ # before re-enabling automatic collection.
51
+ assert gc.garbage == []
52
+ # Restore garbage collection settings to their original state. This should always be run to avoid interfering
53
+ # with other tests to ensure that code should be executed in the `finally' block.
54
+ finally:
55
+ 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,39 @@ 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
@@ -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
- @pytest.fixture(scope="function")
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
- async def server_auto_close(fut, timeout):
96
- """Server run based on run_until_complete. It will block forever with handle
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
- task1 = loop.create_task(check_client1_behavior())
128
- task2 = loop.create_task(check_client2_behavior())
114
+ class Done(Exception):
115
+ pass
129
116
 
130
- server.run()
117
+ async def do_test():
118
+ await asyncio.gather(check_client1_behavior(), check_client2_behavior())
119
+ raise Done
131
120
 
132
- assert task1.done()
133
- assert task2.done()
121
+ try:
122
+ await asyncio.gather(server.arun(), do_test())
123
+ except Done:
124
+ pass
134
125
 
135
126
 
136
- def test_server_delete_instance(server):
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
- task = loop.create_task(client1_multiple_register())
151
- server.run()
152
-
153
- assert task.done()
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,6 +1,6 @@
1
1
  [tox]
2
2
  envlist =
3
- py{38,39,310,311,312}-{test,mypy}
3
+ py{38,39,310,311,312,313}-{test,mypy}
4
4
  qa
5
5
 
6
6
  [testenv]
@@ -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