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.
Files changed (34) hide show
  1. {asgiref-3.10.0/asgiref.egg-info → asgiref-3.11.1}/PKG-INFO +1 -1
  2. asgiref-3.11.1/asgiref/__init__.py +1 -0
  3. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/sync.py +13 -3
  4. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/wsgi.py +38 -7
  5. {asgiref-3.10.0 → asgiref-3.11.1/asgiref.egg-info}/PKG-INFO +1 -1
  6. asgiref-3.11.1/tests/test_sync_contextvars.py +161 -0
  7. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_wsgi.py +65 -0
  8. asgiref-3.10.0/asgiref/__init__.py +0 -1
  9. asgiref-3.10.0/tests/test_sync_contextvars.py +0 -76
  10. {asgiref-3.10.0 → asgiref-3.11.1}/LICENSE +0 -0
  11. {asgiref-3.10.0 → asgiref-3.11.1}/MANIFEST.in +0 -0
  12. {asgiref-3.10.0 → asgiref-3.11.1}/README.rst +0 -0
  13. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/compatibility.py +0 -0
  14. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/current_thread_executor.py +0 -0
  15. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/local.py +0 -0
  16. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/py.typed +0 -0
  17. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/server.py +0 -0
  18. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/testing.py +0 -0
  19. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/timeout.py +0 -0
  20. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref/typing.py +0 -0
  21. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/SOURCES.txt +0 -0
  22. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/dependency_links.txt +0 -0
  23. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/not-zip-safe +0 -0
  24. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/requires.txt +0 -0
  25. {asgiref-3.10.0 → asgiref-3.11.1}/asgiref.egg-info/top_level.txt +0 -0
  26. {asgiref-3.10.0 → asgiref-3.11.1}/setup.cfg +0 -0
  27. {asgiref-3.10.0 → asgiref-3.11.1}/setup.py +0 -0
  28. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_compatibility.py +0 -0
  29. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_garbage_collection.py +0 -0
  30. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_local.py +0 -0
  31. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_server.py +0 -0
  32. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_sync.py +0 -0
  33. {asgiref-3.10.0 → asgiref-3.11.1}/tests/test_testing.py +0 -0
  34. {asgiref-3.10.0 → asgiref-3.11.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.10.0
3
+ Version: 3.11.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
@@ -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
- self.func = func
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
- _restore_context(context)
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)(scope, receive, send)
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 corrected_name in environ:
98
- value = environ[corrected_name] + "," + value
99
- environ[corrected_name] = value
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
- environ = self.build_environ(self.scope, body)
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asgiref
3
- Version: 3.10.0
3
+ Version: 3.11.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
@@ -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