Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-asgiref for openSUSE:Factory checked in at 2021-01-18 11:27:13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-asgiref (Old) and /work/SRC/openSUSE:Factory/.python-asgiref.new.28504 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asgiref" Mon Jan 18 11:27:13 2021 rev:3 rq:863007 version:3.3.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-asgiref/python-asgiref.changes 2020-07-02 23:54:49.800572868 +0200 +++ /work/SRC/openSUSE:Factory/.python-asgiref.new.28504/python-asgiref.changes 2021-01-18 11:30:28.096340415 +0100 @@ -1,0 +2,10 @@ +Thu Jan 14 04:31:06 UTC 2021 - Steve Kowalik <[email protected]> + +- Update to 3.3.1 + * Updated StatelessServer to use ASGI v3 single-callable applications. + * sync_to_async now defaults to thread-sensitive mode being on + * async_to_sync now works inside of forked processes + * WsgiToAsgi now correctly clamps its response body when Content-Length + is set + +------------------------------------------------------------------- Old: ---- asgiref-3.2.10.tar.gz New: ---- asgiref-3.3.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-asgiref.spec ++++++ --- /var/tmp/diff_new_pack.ZIWFMI/_old 2021-01-18 11:30:28.760340790 +0100 +++ /var/tmp/diff_new_pack.ZIWFMI/_new 2021-01-18 11:30:28.764340792 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-asgiref # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,11 +19,10 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-asgiref -Version: 3.2.10 +Version: 3.3.1 Release: 0 Summary: ASGI specs, helper code, and adapters License: BSD-3-Clause -Group: Development/Languages/Python URL: https://github.com/django/asgiref/ Source: https://files.pythonhosted.org/packages/source/a/asgiref/asgiref-%{version}.tar.gz BuildRequires: %{python_module base >= 3.5} ++++++ asgiref-3.2.10.tar.gz -> asgiref-3.3.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/PKG-INFO new/asgiref-3.3.1/PKG-INFO --- old/asgiref-3.2.10/PKG-INFO 2020-06-18 20:49:06.533756500 +0200 +++ new/asgiref-3.3.1/PKG-INFO 2020-11-09 16:55:38.710000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.2.10 +Version: 3.3.1 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -54,7 +54,11 @@ Note that exactly what threads things run in is very specific, and aimed to keep maximum compatibility with old synchronous code. See - "Synchronous code & Threads" below for a full explanation. + "Synchronous code & Threads" below for a full explanation. By default, + ``sync_to_async`` will run all synchronous code in the program in the same + thread for safety reasons; you can disable this for more performance with + ``@sync_to_async(thread_sensitive=False)``, but make sure that your code does + not rely on anything bound to threads (like database connections) when you do. Threadlocal replacement @@ -177,7 +181,7 @@ This means you now have two basic states: * If the outermost layer of your program is synchronous, then all async code - run through ``AsyncToSync`` will run in a per-call event loop in arbitary + run through ``AsyncToSync`` will run in a per-call event loop in arbitrary sub-threads, while all ``thread_sensitive`` code will run in the main thread. * If the outermost layer of your program is asynchronous, then all async code @@ -220,6 +224,7 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Requires-Python: >=3.5 Provides-Extra: tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/README.rst new/asgiref-3.3.1/README.rst --- old/asgiref-3.2.10/README.rst 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/README.rst 2020-11-09 16:53:04.000000000 +0100 @@ -43,7 +43,11 @@ Note that exactly what threads things run in is very specific, and aimed to keep maximum compatibility with old synchronous code. See -"Synchronous code & Threads" below for a full explanation. +"Synchronous code & Threads" below for a full explanation. By default, +``sync_to_async`` will run all synchronous code in the program in the same +thread for safety reasons; you can disable this for more performance with +``@sync_to_async(thread_sensitive=False)``, but make sure that your code does +not rely on anything bound to threads (like database connections) when you do. Threadlocal replacement @@ -166,7 +170,7 @@ This means you now have two basic states: * If the outermost layer of your program is synchronous, then all async code - run through ``AsyncToSync`` will run in a per-call event loop in arbitary + run through ``AsyncToSync`` will run in a per-call event loop in arbitrary sub-threads, while all ``thread_sensitive`` code will run in the main thread. * If the outermost layer of your program is asynchronous, then all async code diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref/__init__.py new/asgiref-3.3.1/asgiref/__init__.py --- old/asgiref-3.2.10/asgiref/__init__.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/asgiref/__init__.py 2020-11-09 16:55:08.000000000 +0100 @@ -1 +1 @@ -__version__ = "3.2.10" +__version__ = "3.3.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref/server.py new/asgiref-3.3.1/asgiref/server.py --- old/asgiref-3.2.10/asgiref/server.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/asgiref/server.py 2020-11-09 16:53:04.000000000 +0100 @@ -3,6 +3,8 @@ import time import traceback +from .compatibility import guarantee_single_callable + logger = logging.getLogger(__name__) @@ -84,10 +86,11 @@ self.delete_oldest_application_instance() # Make an instance of the application input_queue = asyncio.Queue() - application_instance = self.application(scope=scope) + application_instance = guarantee_single_callable(self.application) # Run it, and stash the future for later checking future = asyncio.ensure_future( application_instance( + scope=scope, receive=input_queue.get, send=lambda message: self.application_send(scope, message), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref/sync.py new/asgiref-3.3.1/asgiref/sync.py --- old/asgiref-3.2.10/asgiref/sync.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/asgiref/sync.py 2020-11-09 16:53:04.000000000 +0100 @@ -61,9 +61,17 @@ except RuntimeError: # There's no event loop in this thread. Look for the threadlocal if # we're inside SyncToAsync - self.main_event_loop = getattr( - SyncToAsync.threadlocal, "main_event_loop", None + main_event_loop_pid = getattr( + SyncToAsync.threadlocal, "main_event_loop_pid", None ) + # We make sure the parent loop is from the same process - if + # they've forked, this is not going to be valid any more (#194) + if main_event_loop_pid and main_event_loop_pid == os.getpid(): + self.main_event_loop = getattr( + SyncToAsync.threadlocal, "main_event_loop", None + ) + else: + self.main_event_loop = None def __call__(self, *args, **kwargs): # You can't call AsyncToSync from a thread with a running event loop @@ -198,7 +206,7 @@ if exc_info[1]: try: raise exc_info[1] - except: + except Exception: result = await self.awaitable(*args, **kwargs) else: result = await self.awaitable(*args, **kwargs) @@ -247,7 +255,7 @@ # Single-thread executor for thread-sensitive code single_thread_executor = ThreadPoolExecutor(max_workers=1) - def __init__(self, func, thread_sensitive=False): + def __init__(self, func, thread_sensitive=True): self.func = func functools.update_wrapper(self, func) self._thread_sensitive = thread_sensitive @@ -312,6 +320,7 @@ """ # Set the threadlocal for AsyncToSync self.threadlocal.main_event_loop = loop + self.threadlocal.main_event_loop_pid = os.getpid() # Set the task mapping (used for the locals module) current_thread = threading.current_thread() if AsyncToSync.launch_map.get(source_task) == current_thread: @@ -328,7 +337,7 @@ if exc_info[1]: try: raise exc_info[1] - except: + except Exception: return func(*args, **kwargs) else: return func(*args, **kwargs) @@ -356,6 +365,11 @@ return None -# Lowercase is more sensible for most things -sync_to_async = SyncToAsync +# Lowercase aliases (and decorator friendliness) async_to_sync = AsyncToSync + + +def sync_to_async(func=None, thread_sensitive=True): + if func is None: + return lambda f: SyncToAsync(f, thread_sensitive=thread_sensitive) + return SyncToAsync(func, thread_sensitive=thread_sensitive) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref/wsgi.py new/asgiref-3.3.1/asgiref/wsgi.py --- old/asgiref-3.2.10/asgiref/wsgi.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/asgiref/wsgi.py 2020-10-09 18:25:45.000000000 +0200 @@ -29,6 +29,7 @@ def __init__(self, wsgi_application): self.wsgi_application = wsgi_application self.response_started = False + self.response_content_length = None async def __call__(self, scope, receive, send): if scope["type"] != "http": @@ -55,8 +56,8 @@ """ environ = { "REQUEST_METHOD": scope["method"], - "SCRIPT_NAME": scope.get("root_path", ""), - "PATH_INFO": scope["path"], + "SCRIPT_NAME": scope.get("root_path", "").encode("utf8").decode("latin1"), + "PATH_INFO": scope["path"].encode("utf8").decode("latin1"), "QUERY_STRING": scope["query_string"].decode("ascii"), "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], "wsgi.version": (1, 0), @@ -114,6 +115,11 @@ (name.lower().encode("ascii"), value.encode("ascii")) for name, value in response_headers ] + # Extract content-length + self.response_content_length = None + for name, value in response_headers: + if name.lower() == "content-length": + self.response_content_length = int(value) # Build and send response start message. self.response_start = { "type": "http.response.start", @@ -130,14 +136,25 @@ # Translate the scope and incoming request body into a WSGI environ environ = self.build_environ(self.scope, body) # Run the WSGI app + bytes_sent = 0 for output in self.wsgi_application(environ, self.start_response): # If this is the first response, include the response headers if not self.response_started: self.response_started = True self.sync_send(self.response_start) + # If the application supplies a Content-Length header + if self.response_content_length is not None: + # The server should not transmit more bytes to the client than the header allows + bytes_allowed = self.response_content_length - bytes_sent + if len(output) > bytes_allowed: + output = output[:bytes_allowed] self.sync_send( {"type": "http.response.body", "body": output, "more_body": True} ) + bytes_sent += len(output) + # The server should stop iterating over the response when enough data has been sent + if bytes_sent == self.response_content_length: + break # Close connection if not self.response_started: self.response_started = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref.egg-info/PKG-INFO new/asgiref-3.3.1/asgiref.egg-info/PKG-INFO --- old/asgiref-3.2.10/asgiref.egg-info/PKG-INFO 2020-06-18 20:49:06.000000000 +0200 +++ new/asgiref-3.3.1/asgiref.egg-info/PKG-INFO 2020-11-09 16:55:38.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.2.10 +Version: 3.3.1 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -54,7 +54,11 @@ Note that exactly what threads things run in is very specific, and aimed to keep maximum compatibility with old synchronous code. See - "Synchronous code & Threads" below for a full explanation. + "Synchronous code & Threads" below for a full explanation. By default, + ``sync_to_async`` will run all synchronous code in the program in the same + thread for safety reasons; you can disable this for more performance with + ``@sync_to_async(thread_sensitive=False)``, but make sure that your code does + not rely on anything bound to threads (like database connections) when you do. Threadlocal replacement @@ -177,7 +181,7 @@ This means you now have two basic states: * If the outermost layer of your program is synchronous, then all async code - run through ``AsyncToSync`` will run in a per-call event loop in arbitary + run through ``AsyncToSync`` will run in a per-call event loop in arbitrary sub-threads, while all ``thread_sensitive`` code will run in the main thread. * If the outermost layer of your program is asynchronous, then all async code @@ -220,6 +224,7 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Requires-Python: >=3.5 Provides-Extra: tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/asgiref.egg-info/SOURCES.txt new/asgiref-3.3.1/asgiref.egg-info/SOURCES.txt --- old/asgiref-3.2.10/asgiref.egg-info/SOURCES.txt 2020-06-18 20:49:06.000000000 +0200 +++ new/asgiref-3.3.1/asgiref.egg-info/SOURCES.txt 2020-11-09 16:55:38.000000000 +0100 @@ -20,6 +20,7 @@ asgiref.egg-info/top_level.txt tests/test_compatibility.py tests/test_local.py +tests/test_server.py tests/test_sync.py tests/test_sync_contextvars.py tests/test_testing.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/setup.cfg new/asgiref-3.3.1/setup.cfg --- old/asgiref-3.2.10/setup.cfg 2020-06-18 20:49:06.537758600 +0200 +++ new/asgiref-3.3.1/setup.cfg 2020-11-09 16:55:38.710000000 +0100 @@ -20,6 +20,7 @@ Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Internet :: WWW/HTTP project_urls = Documentation = https://asgi.readthedocs.io/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/tests/test_compatibility.py new/asgiref-3.3.1/tests/test_compatibility.py --- old/asgiref-3.2.10/tests/test_compatibility.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/tests/test_compatibility.py 2020-11-09 16:53:04.000000000 +0100 @@ -67,11 +67,11 @@ """ Tests that the signature matcher works as expected. """ - assert is_double_callable(double_application_function) == True - assert is_double_callable(DoubleApplicationClass) == True - assert is_double_callable(DoubleApplicationClassNestedFunction()) == True - assert is_double_callable(single_application_function) == False - assert is_double_callable(SingleApplicationClass()) == False + assert is_double_callable(double_application_function) is True + assert is_double_callable(DoubleApplicationClass) is True + assert is_double_callable(DoubleApplicationClassNestedFunction()) is True + assert is_double_callable(single_application_function) is False + assert is_double_callable(SingleApplicationClass()) is False def test_double_to_single_signature(): @@ -80,7 +80,7 @@ """ assert ( is_double_callable(double_to_single_callable(double_application_function)) - == False + is False ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/tests/test_local.py new/asgiref-3.3.1/tests/test_local.py --- old/asgiref-3.2.10/tests/test_local.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/tests/test_local.py 2020-11-09 16:53:04.000000000 +0100 @@ -53,6 +53,7 @@ # Unassigned should be an error with pytest.raises(AttributeError): test_local.foo + # Assign and check it does not persist inside the thread class TestThread(threading.Thread): # Failure reason @@ -132,6 +133,7 @@ # Set up the local test_local = Local() test_local.foo = 3 + # Look at it in a sync context def sync_function(): assert test_local.foo == 3 @@ -149,6 +151,7 @@ # Set up the local test_local = Local() test_local.foo = 12 + # Look at it in an async context async def async_function(): assert test_local.foo == 12 @@ -168,6 +171,7 @@ # Set up the local test_local = Local() test_local.foo = 756 + # Look at it in an async context inside a sync context def sync_function(): async def async_function(): @@ -189,6 +193,7 @@ # Set up the local test_local = Local() test_local.foo = 8374 + # Make sure we go between each world at least twice def sync_function(): async def async_function(): @@ -217,6 +222,7 @@ # Set up the local test_local = Local(thread_critical=True) test_local.foo = 86 + # Look at it in a sync context def sync_function(): with pytest.raises(AttributeError): @@ -237,6 +243,7 @@ # Set up the local test_local = Local(thread_critical=True) test_local.foo = 89 + # Look at it in an async context async def async_function(): with pytest.raises(AttributeError): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/tests/test_server.py new/asgiref-3.3.1/tests/test_server.py --- old/asgiref-3.2.10/tests/test_server.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asgiref-3.3.1/tests/test_server.py 2020-11-09 16:53:04.000000000 +0100 @@ -0,0 +1,11 @@ +from asgiref.server import StatelessServer + + +def test_stateless_server(): + """StatlessServer can be instantiated with an ASGI 3 application.""" + + async def app(scope, receive, send): + pass + + server = StatelessServer(app) + server.get_or_create_application_instance("scope_id", {}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/tests/test_sync.py new/asgiref-3.3.1/tests/test_sync.py --- old/asgiref-3.2.10/tests/test_sync.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/tests/test_sync.py 2020-11-09 16:53:00.000000000 +0100 @@ -1,4 +1,5 @@ import asyncio +import multiprocessing import threading import time from concurrent.futures import ThreadPoolExecutor @@ -279,11 +280,10 @@ await inner() # Inner sync function + @sync_to_async def inner(): result["thread"] = threading.current_thread() - inner = sync_to_async(inner, thread_sensitive=True) - # Run it middle() assert result["thread"] == threading.current_thread() @@ -300,22 +300,20 @@ result_2 = {} # Outer sync function + @sync_to_async def outer(result): middle(result) - outer = sync_to_async(outer, thread_sensitive=True) - # Middle async function @async_to_sync async def middle(result): await inner(result) # Inner sync function + @sync_to_async def inner(result): result["thread"] = threading.current_thread() - inner = sync_to_async(inner, thread_sensitive=True) - # Run it (in supposed parallel!) await asyncio.wait([outer(result_1), inner(result_2)]) @@ -338,22 +336,20 @@ await level2() # Sync level 2 + @sync_to_async def level2(): level3() - level2 = sync_to_async(level2, thread_sensitive=True) - # Async level 3 @async_to_sync async def level3(): await level4() # Sync level 2 + @sync_to_async def level4(): result["thread"] = threading.current_thread() - level4 = sync_to_async(level4, thread_sensitive=True) - # Run it level1() assert result["thread"] == threading.current_thread() @@ -369,22 +365,20 @@ result = {} # Sync level 1 + @sync_to_async def level1(): level2() - level1 = sync_to_async(level1, thread_sensitive=True) - # Async level 2 @async_to_sync async def level2(): await level3() # Sync level 3 + @sync_to_async def level3(): level4() - level3 = sync_to_async(level3, thread_sensitive=True) - # Async level 4 @async_to_sync async def level4(): @@ -395,6 +389,29 @@ assert result["thread"] == threading.current_thread() +def test_thread_sensitive_disabled(): + """ + Tests that we can disable thread sensitivity and make things run in + separate threads. + """ + + result = {} + + # Middle async function + @async_to_sync + async def middle(): + await inner() + + # Inner sync function + @sync_to_async(thread_sensitive=False) + def inner(): + result["thread"] = threading.current_thread() + + # Run it + middle() + assert result["thread"] != threading.current_thread() + + class ASGITest(TestCase): """ Tests collection of async cases inside classes @@ -415,3 +432,32 @@ assert not asyncio.iscoroutinefunction(sync_to_async) assert asyncio.iscoroutinefunction(sync_to_async(sync_func)) + + [email protected] +async def test_multiprocessing(): + """ + Tests that a forked process can use async_to_sync without it looking for + the event loop from the parent process. + """ + + test_queue = multiprocessing.Queue() + + async def async_process(): + test_queue.put(42) + + def sync_process(): + """Runs async_process synchronously""" + async_to_sync(async_process)() + + def fork_first(): + """Forks process before running sync_process""" + fork = multiprocessing.Process(target=sync_process) + fork.start() + fork.join(3) + # Force cleanup in failed test case + if fork.is_alive(): + fork.terminate() + return test_queue.get(True, 1) + + assert await sync_to_async(fork_first)() == 42 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.2.10/tests/test_wsgi.py new/asgiref-3.3.1/tests/test_wsgi.py --- old/asgiref-3.2.10/tests/test_wsgi.py 2020-06-18 20:48:49.000000000 +0200 +++ new/asgiref-3.3.1/tests/test_wsgi.py 2020-10-09 18:25:45.000000000 +0200 @@ -1,3 +1,5 @@ +import sys + import pytest from asgiref.testing import ApplicationCommunicator @@ -51,6 +53,48 @@ @pytest.mark.asyncio +async def test_wsgi_path_encoding(): + """ + Makes sure the WSGI wrapper has basic functionality. + """ + # Define WSGI app + def wsgi_application(environ, start_response): + assert environ["SCRIPT_NAME"] == "/??????".encode("utf8").decode("latin-1") + assert environ["PATH_INFO"] == "/??????".encode("utf8").decode("latin-1") + start_response("200 OK", []) + yield b"" + + # Wrap it + application = WsgiToAsgi(wsgi_application) + # Launch it as a test application + instance = ApplicationCommunicator( + application, + { + "type": "http", + "http_version": "1.0", + "method": "GET", + "path": "/??????", + "root_path": "/??????", + "query_string": b"bar=baz", + "headers": [], + }, + ) + await instance.send_input({"type": "http.request"}) + # Check they send stuff + assert (await instance.receive_output(1)) == { + "type": "http.response.start", + "status": 200, + "headers": [], + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"", + "more_body": True, + } + assert (await instance.receive_output(1)) == {"type": "http.response.body"} + + [email protected] async def test_wsgi_empty_body(): """ Makes sure WsgiToAsgi handles an empty body response correctly @@ -84,6 +128,130 @@ assert (await instance.receive_output(1)) == {"type": "http.response.body"} [email protected] +async def test_wsgi_clamped_body(): + """ + Makes sure WsgiToAsgi clamps a body response longer than Content-Length + """ + + def wsgi_application(environ, start_response): + start_response("200 OK", [("Content-Length", "8")]) + return [b"0123", b"45", b"6789"] + + application = WsgiToAsgi(wsgi_application) + instance = ApplicationCommunicator( + application, + { + "type": "http", + "http_version": "1.0", + "method": "GET", + "path": "/", + "query_string": b"", + "headers": [], + }, + ) + await instance.send_input({"type": "http.request"}) + assert (await instance.receive_output(1)) == { + "type": "http.response.start", + "status": 200, + "headers": [(b"content-length", b"8")], + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"0123", + "more_body": True, + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"45", + "more_body": True, + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"67", + "more_body": True, + } + assert (await instance.receive_output(1)) == {"type": "http.response.body"} + + [email protected] +async def test_wsgi_stops_iterating_after_content_length_bytes(): + """ + Makes sure WsgiToAsgi does not iterate after than Content-Length bytes + """ + + def wsgi_application(environ, start_response): + start_response("200 OK", [("Content-Length", "4")]) + yield b"0123" + pytest.fail("WsgiToAsgi should not iterate after Content-Length bytes") + yield b"4567" + + application = WsgiToAsgi(wsgi_application) + instance = ApplicationCommunicator( + application, + { + "type": "http", + "http_version": "1.0", + "method": "GET", + "path": "/", + "query_string": b"", + "headers": [], + }, + ) + await instance.send_input({"type": "http.request"}) + assert (await instance.receive_output(1)) == { + "type": "http.response.start", + "status": 200, + "headers": [(b"content-length", b"4")], + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"0123", + "more_body": True, + } + assert (await instance.receive_output(1)) == {"type": "http.response.body"} + + [email protected] +async def test_wsgi_multiple_start_response(): + """ + Makes sure WsgiToAsgi only keep Content-Length from the last call to start_response + """ + + def wsgi_application(environ, start_response): + start_response("200 OK", [("Content-Length", "5")]) + try: + raise ValueError("Application Error") + except ValueError: + start_response("500 Server Error", [], sys.exc_info()) + return [b"Some long error message"] + + application = WsgiToAsgi(wsgi_application) + instance = ApplicationCommunicator( + application, + { + "type": "http", + "http_version": "1.0", + "method": "GET", + "path": "/", + "query_string": b"", + "headers": [], + }, + ) + await instance.send_input({"type": "http.request"}) + assert (await instance.receive_output(1)) == { + "type": "http.response.start", + "status": 500, + "headers": [], + } + assert (await instance.receive_output(1)) == { + "type": "http.response.body", + "body": b"Some long error message", + "more_body": True, + } + assert (await instance.receive_output(1)) == {"type": "http.response.body"} + + @pytest.mark.asyncio async def test_wsgi_multi_body(): """
