asgiref 3.11.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 (33) hide show
  1. {asgiref-3.11.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.11.0 → asgiref-3.11.1}/asgiref/sync.py +3 -1
  4. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/wsgi.py +38 -7
  5. {asgiref-3.11.0 → asgiref-3.11.1/asgiref.egg-info}/PKG-INFO +1 -1
  6. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_sync_contextvars.py +21 -0
  7. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_wsgi.py +65 -0
  8. asgiref-3.11.0/asgiref/__init__.py +0 -1
  9. {asgiref-3.11.0 → asgiref-3.11.1}/LICENSE +0 -0
  10. {asgiref-3.11.0 → asgiref-3.11.1}/MANIFEST.in +0 -0
  11. {asgiref-3.11.0 → asgiref-3.11.1}/README.rst +0 -0
  12. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/compatibility.py +0 -0
  13. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/current_thread_executor.py +0 -0
  14. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/local.py +0 -0
  15. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/py.typed +0 -0
  16. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/server.py +0 -0
  17. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/testing.py +0 -0
  18. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/timeout.py +0 -0
  19. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref/typing.py +0 -0
  20. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref.egg-info/SOURCES.txt +0 -0
  21. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref.egg-info/dependency_links.txt +0 -0
  22. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref.egg-info/not-zip-safe +0 -0
  23. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref.egg-info/requires.txt +0 -0
  24. {asgiref-3.11.0 → asgiref-3.11.1}/asgiref.egg-info/top_level.txt +0 -0
  25. {asgiref-3.11.0 → asgiref-3.11.1}/setup.cfg +0 -0
  26. {asgiref-3.11.0 → asgiref-3.11.1}/setup.py +0 -0
  27. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_compatibility.py +0 -0
  28. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_garbage_collection.py +0 -0
  29. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_local.py +0 -0
  30. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_server.py +0 -0
  31. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_sync.py +0 -0
  32. {asgiref-3.11.0 → asgiref-3.11.1}/tests/test_testing.py +0 -0
  33. {asgiref-3.11.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.11.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"
@@ -432,9 +432,11 @@ class SyncToAsync(Generic[_P, _R]):
432
432
  or iscoroutinefunction(getattr(func, "__call__", func))
433
433
  ):
434
434
  raise TypeError("sync_to_async can only be applied to sync functions.")
435
+
436
+ functools.update_wrapper(self, func)
435
437
  self.func = func
436
438
  self.context = context
437
- functools.update_wrapper(self, func)
439
+
438
440
  self._thread_sensitive = thread_sensitive
439
441
  markcoroutinefunction(self)
440
442
  if thread_sensitive and executor is not None:
@@ -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.11.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
@@ -138,3 +138,24 @@ def test_async_to_sync_contextvars():
138
138
  sync_function = async_to_sync(async_function)
139
139
  assert sync_function() == 42
140
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.11.0"
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