From 1cec8df876023d9cfd084b66d192c31984fc7986 Mon Sep 17 00:00:00 2001 From: Jamie Hewland Date: Fri, 1 May 2020 19:17:37 +0200 Subject: [PATCH 1/3] asgi: Wait for response to complete before sending disconnect message --- httpx/_concurrency.py | 31 +++++++++++++++++++++++++++++++ httpx/_dispatch/asgi.py | 17 +++++++++++------ requirements.txt | 1 + setup.py | 1 + tests/test_asgi.py | 3 +-- 5 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 httpx/_concurrency.py diff --git a/httpx/_concurrency.py b/httpx/_concurrency.py new file mode 100644 index 0000000000..3a93e87149 --- /dev/null +++ b/httpx/_concurrency.py @@ -0,0 +1,31 @@ +import typing + +import sniffio + +if typing.TYPE_CHECKING: # pragma: no cover + try: + from typing import Protocol + except ImportError: + from typing_extensions import Protocol # type: ignore + + class Event(Protocol): + def set(self) -> None: + ... + + # asyncio wait() returns True, but Trio returns None: ignore the return value. + async def wait(self) -> typing.Any: + ... + + def is_set(self) -> bool: + ... + + +def create_event() -> "Event": + if sniffio.current_async_library() == "trio": + import trio + + return trio.Event() + else: + import asyncio + + return asyncio.Event() diff --git a/httpx/_dispatch/asgi.py b/httpx/_dispatch/asgi.py index 5edca1ed8c..204ffc3db2 100644 --- a/httpx/_dispatch/asgi.py +++ b/httpx/_dispatch/asgi.py @@ -2,6 +2,7 @@ import httpcore +from .._concurrency import create_event from .._content_streams import ByteStream @@ -76,8 +77,9 @@ async def request( status_code = None response_headers = None body_parts = [] + request_complete = False response_started = False - response_complete = False + response_complete = create_event() headers = [] if headers is None else headers stream = ByteStream(b"") if stream is None else stream @@ -85,14 +87,17 @@ async def request( request_body_chunks = stream.__aiter__() async def receive() -> dict: - nonlocal response_complete + nonlocal request_complete, response_complete - if response_complete: + if request_complete: + # Simulate blocking until the response is complete and then disconnect + await response_complete.wait() return {"type": "http.disconnect"} try: body = await request_body_chunks.__anext__() except StopAsyncIteration: + request_complete = True return {"type": "http.request", "body": b"", "more_body": False} return {"type": "http.request", "body": body, "more_body": True} @@ -108,7 +113,7 @@ async def send(message: dict) -> None: response_started = True elif message["type"] == "http.response.body": - assert not response_complete + assert not response_complete.is_set() body = message.get("body", b"") more_body = message.get("more_body", False) @@ -116,7 +121,7 @@ async def send(message: dict) -> None: body_parts.append(body) if not more_body: - response_complete = True + response_complete.set() try: await self.app(scope, receive, send) @@ -124,7 +129,7 @@ async def send(message: dict) -> None: if self.raise_app_exceptions or not response_complete: raise - assert response_complete + assert response_complete.is_set() assert status_code is not None assert response_headers is not None diff --git a/requirements.txt b/requirements.txt index e5ac1a2ed9..dd2409067d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ pytest-asyncio pytest-trio pytest-cov trio +trio-typing trustme uvicorn seed-isort-config diff --git a/setup.py b/setup.py index 02191049f8..fa71f42724 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def get_packages(package): "idna==2.*", "rfc3986>=1.3,<2", "httpcore>=0.8.1", + "sniffio", ], classifiers=[ "Development Status :: 4 - Beta", diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 72a003936e..d225baf411 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -69,8 +69,7 @@ async def test_asgi_exc_after_response(): await client.get("http://www.example.org/") -@pytest.mark.asyncio -async def test_asgi_disconnect_after_response_complete(): +async def test_asgi_disconnect_after_response_complete(async_environment): disconnect = False async def read_body(scope, receive, send): From bf6f194c04e9fc46e632d28a2558b14c9dcf3a3f Mon Sep 17 00:00:00 2001 From: Jamie Hewland Date: Sun, 3 May 2020 14:10:14 +0200 Subject: [PATCH 2/3] Dial back type checking + remove concurrency module --- httpx/_concurrency.py | 31 ------------------------------- httpx/_dispatch/asgi.py | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 32 deletions(-) delete mode 100644 httpx/_concurrency.py diff --git a/httpx/_concurrency.py b/httpx/_concurrency.py deleted file mode 100644 index 3a93e87149..0000000000 --- a/httpx/_concurrency.py +++ /dev/null @@ -1,31 +0,0 @@ -import typing - -import sniffio - -if typing.TYPE_CHECKING: # pragma: no cover - try: - from typing import Protocol - except ImportError: - from typing_extensions import Protocol # type: ignore - - class Event(Protocol): - def set(self) -> None: - ... - - # asyncio wait() returns True, but Trio returns None: ignore the return value. - async def wait(self) -> typing.Any: - ... - - def is_set(self) -> bool: - ... - - -def create_event() -> "Event": - if sniffio.current_async_library() == "trio": - import trio - - return trio.Event() - else: - import asyncio - - return asyncio.Event() diff --git a/httpx/_dispatch/asgi.py b/httpx/_dispatch/asgi.py index 204ffc3db2..c7fdbb6a5f 100644 --- a/httpx/_dispatch/asgi.py +++ b/httpx/_dispatch/asgi.py @@ -1,10 +1,28 @@ +import typing from typing import Callable, Dict, List, Optional, Tuple import httpcore +import sniffio -from .._concurrency import create_event from .._content_streams import ByteStream +if typing.TYPE_CHECKING: # pragma: no cover + import asyncio + import trio + + Event = typing.Union[asyncio.Event, trio.Event] + + +def create_event() -> "Event": + if sniffio.current_async_library() == "trio": + import trio + + return trio.Event() + else: + import asyncio + + return asyncio.Event() + class ASGIDispatch(httpcore.AsyncHTTPTransport): """ From 1979edb9da1ea884f5a95cd9ee1d07a29249493d Mon Sep 17 00:00:00 2001 From: Jamie Hewland Date: Wed, 6 May 2020 20:52:23 +0200 Subject: [PATCH 3/3] Remove somewhat redundant comment --- httpx/_dispatch/asgi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/httpx/_dispatch/asgi.py b/httpx/_dispatch/asgi.py index c7fdbb6a5f..a86969bcca 100644 --- a/httpx/_dispatch/asgi.py +++ b/httpx/_dispatch/asgi.py @@ -108,7 +108,6 @@ async def receive() -> dict: nonlocal request_complete, response_complete if request_complete: - # Simulate blocking until the response is complete and then disconnect await response_complete.wait() return {"type": "http.disconnect"}