]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Drop StreamContextManager in favour of `contextlib.contextmanager`/`asynccontextmanag...
authorTom Christie <tom@tomchristie.com>
Mon, 19 Apr 2021 10:07:07 +0000 (11:07 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Apr 2021 10:07:07 +0000 (11:07 +0100)
* Drop StreamContextManager in favour of using contextlib.contextmanager/asyncontextmanager

* Use type: ignore to avoid mypy errors on 3.6

README.md
docs/index.md
httpx/_api.py
httpx/_client.py
httpx/_compat.py [new file with mode: 0644]
setup.py

index 66b2f8688f7874467c6a5968baee3d394bbe9785..d8251bd138192cc23d6e688a441a9bc28b5bc6ca 100644 (file)
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@ The HTTPX project relies on these excellent libraries:
 * `rfc3986` - URL parsing & normalization.
   * `idna` - Internationalized domain name support.
 * `sniffio` - Async library autodetection.
+* `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
 * `brotlipy` - Decoding for "brotli" compressed responses. *(Optional)*
 
 A huge amount of credit is due to `requests` for the API layout that
index 3fcb33612f2436cc0e28bea1f036c9ed89f7ea17..109ee33088643fc6f581990d31d1716bab369897 100644 (file)
@@ -114,6 +114,7 @@ The HTTPX project relies on these excellent libraries:
 * `rfc3986` - URL parsing & normalization.
   * `idna` - Internationalized domain name support.
 * `sniffio` - Async library autodetection.
+* `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
 * `brotlipy` - Decoding for "brotli" compressed responses. *(Optional)*
 
 A huge amount of credit is due to `requests` for the API layout that
index 7722111dc5e68377c4fa3af035ada7f8ffef87a7..51bc246aa30d1821e807d33d0f1a01fe0eb18631 100644 (file)
@@ -1,8 +1,9 @@
 import typing
+from contextlib import contextmanager
 
-from ._client import Client, StreamContextManager
+from ._client import Client
 from ._config import DEFAULT_TIMEOUT_CONFIG
-from ._models import Request, Response
+from ._models import Response
 from ._types import (
     AuthTypes,
     CertTypes,
@@ -106,6 +107,7 @@ def request(
         )
 
 
+@contextmanager
 def stream(
     method: str,
     url: URLTypes,
@@ -124,7 +126,7 @@ def stream(
     verify: VerifyTypes = True,
     cert: CertTypes = None,
     trust_env: bool = True,
-) -> StreamContextManager:
+) -> typing.Iterator[Response]:
     """
     Alternative to `httpx.request()` that streams the response body
     instead of loading it into memory at once.
@@ -135,26 +137,23 @@ def stream(
 
     [0]: /quickstart#streaming-responses
     """
-    client = Client(proxies=proxies, cert=cert, verify=verify, trust_env=trust_env)
-    request = Request(
-        method=method,
-        url=url,
-        params=params,
-        content=content,
-        data=data,
-        files=files,
-        json=json,
-        headers=headers,
-        cookies=cookies,
-    )
-    return StreamContextManager(
-        client=client,
-        request=request,
-        auth=auth,
-        timeout=timeout,
-        allow_redirects=allow_redirects,
-        close_client=True,
-    )
+    with Client(
+        proxies=proxies, cert=cert, verify=verify, trust_env=trust_env
+    ) as client:
+        with client.stream(
+            method=method,
+            url=url,
+            content=content,
+            data=data,
+            files=files,
+            json=json,
+            params=params,
+            headers=headers,
+            cookies=cookies,
+            auth=auth,
+            allow_redirects=allow_redirects,
+        ) as response:
+            yield response
 
 
 def get(
index 429382fa808097472f9bced75a5a96cafa0eb26f..d0ed0893a10ee077f7df61c7215f648102277f1f 100644 (file)
@@ -2,10 +2,12 @@ import datetime
 import enum
 import typing
 import warnings
+from contextlib import contextmanager
 from types import TracebackType
 
 from .__version__ import __version__
 from ._auth import Auth, BasicAuth, FunctionAuth
+from ._compat import asynccontextmanager
 from ._config import (
     DEFAULT_LIMITS,
     DEFAULT_MAX_REDIRECTS,
@@ -289,51 +291,6 @@ class BaseClient:
     def params(self, params: QueryParamTypes) -> None:
         self._params = QueryParams(params)
 
-    def stream(
-        self,
-        method: str,
-        url: URLTypes,
-        *,
-        content: RequestContent = None,
-        data: RequestData = None,
-        files: RequestFiles = None,
-        json: typing.Any = None,
-        params: QueryParamTypes = None,
-        headers: HeaderTypes = None,
-        cookies: CookieTypes = None,
-        auth: typing.Union[AuthTypes, UnsetType] = UNSET,
-        allow_redirects: bool = True,
-        timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
-    ) -> "StreamContextManager":
-        """
-        Alternative to `httpx.request()` that streams the response body
-        instead of loading it into memory at once.
-
-        **Parameters**: See `httpx.request`.
-
-        See also: [Streaming Responses][0]
-
-        [0]: /quickstart#streaming-responses
-        """
-        request = self.build_request(
-            method=method,
-            url=url,
-            content=content,
-            data=data,
-            files=files,
-            json=json,
-            params=params,
-            headers=headers,
-            cookies=cookies,
-        )
-        return StreamContextManager(
-            client=self,
-            request=request,
-            auth=auth,
-            allow_redirects=allow_redirects,
-            timeout=timeout,
-        )
-
     def build_request(
         self,
         method: str,
@@ -793,6 +750,56 @@ class Client(BaseClient):
             request, auth=auth, allow_redirects=allow_redirects, timeout=timeout
         )
 
+    @contextmanager
+    def stream(
+        self,
+        method: str,
+        url: URLTypes,
+        *,
+        content: RequestContent = None,
+        data: RequestData = None,
+        files: RequestFiles = None,
+        json: typing.Any = None,
+        params: QueryParamTypes = None,
+        headers: HeaderTypes = None,
+        cookies: CookieTypes = None,
+        auth: typing.Union[AuthTypes, UnsetType] = UNSET,
+        allow_redirects: bool = True,
+        timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
+    ) -> typing.Iterator[Response]:
+        """
+        Alternative to `httpx.request()` that streams the response body
+        instead of loading it into memory at once.
+
+        **Parameters**: See `httpx.request`.
+
+        See also: [Streaming Responses][0]
+
+        [0]: /quickstart#streaming-responses
+        """
+        request = self.build_request(
+            method=method,
+            url=url,
+            content=content,
+            data=data,
+            files=files,
+            json=json,
+            params=params,
+            headers=headers,
+            cookies=cookies,
+        )
+        response = self.send(
+            request=request,
+            auth=auth,
+            allow_redirects=allow_redirects,
+            timeout=timeout,
+            stream=True,
+        )
+        try:
+            yield response
+        finally:
+            response.close()
+
     def send(
         self,
         request: Request,
@@ -1430,6 +1437,56 @@ class AsyncClient(BaseClient):
         )
         return response
 
+    @asynccontextmanager
+    async def stream(
+        self,
+        method: str,
+        url: URLTypes,
+        *,
+        content: RequestContent = None,
+        data: RequestData = None,
+        files: RequestFiles = None,
+        json: typing.Any = None,
+        params: QueryParamTypes = None,
+        headers: HeaderTypes = None,
+        cookies: CookieTypes = None,
+        auth: typing.Union[AuthTypes, UnsetType] = UNSET,
+        allow_redirects: bool = True,
+        timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
+    ) -> typing.AsyncIterator[Response]:
+        """
+        Alternative to `httpx.request()` that streams the response body
+        instead of loading it into memory at once.
+
+        **Parameters**: See `httpx.request`.
+
+        See also: [Streaming Responses][0]
+
+        [0]: /quickstart#streaming-responses
+        """
+        request = self.build_request(
+            method=method,
+            url=url,
+            content=content,
+            data=data,
+            files=files,
+            json=json,
+            params=params,
+            headers=headers,
+            cookies=cookies,
+        )
+        response = await self.send(
+            request=request,
+            auth=auth,
+            allow_redirects=allow_redirects,
+            timeout=timeout,
+            stream=True,
+        )
+        try:
+            yield response
+        finally:
+            await response.aclose()
+
     async def send(
         self,
         request: Request,
@@ -1869,64 +1926,3 @@ class AsyncClient(BaseClient):
                 "See https://www.python-httpx.org/async/#opening-and-closing-clients "
                 "for details."
             )
-
-
-class StreamContextManager:
-    def __init__(
-        self,
-        client: BaseClient,
-        request: Request,
-        *,
-        auth: typing.Union[AuthTypes, UnsetType] = UNSET,
-        allow_redirects: bool = True,
-        timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
-        close_client: bool = False,
-    ) -> None:
-        self.client = client
-        self.request = request
-        self.auth = auth
-        self.allow_redirects = allow_redirects
-        self.timeout = timeout
-        self.close_client = close_client
-
-    def __enter__(self) -> "Response":
-        assert isinstance(self.client, Client)
-        self.response = self.client.send(
-            request=self.request,
-            auth=self.auth,
-            allow_redirects=self.allow_redirects,
-            timeout=self.timeout,
-            stream=True,
-        )
-        return self.response
-
-    def __exit__(
-        self,
-        exc_type: typing.Type[BaseException] = None,
-        exc_value: BaseException = None,
-        traceback: TracebackType = None,
-    ) -> None:
-        assert isinstance(self.client, Client)
-        self.response.close()
-        if self.close_client:
-            self.client.close()
-
-    async def __aenter__(self) -> "Response":
-        assert isinstance(self.client, AsyncClient)
-        self.response = await self.client.send(
-            request=self.request,
-            auth=self.auth,
-            allow_redirects=self.allow_redirects,
-            timeout=self.timeout,
-            stream=True,
-        )
-        return self.response
-
-    async def __aexit__(
-        self,
-        exc_type: typing.Type[BaseException] = None,
-        exc_value: BaseException = None,
-        traceback: TracebackType = None,
-    ) -> None:
-        assert isinstance(self.client, AsyncClient)
-        await self.response.aclose()
diff --git a/httpx/_compat.py b/httpx/_compat.py
new file mode 100644 (file)
index 0000000..47c12ba
--- /dev/null
@@ -0,0 +1,6 @@
+# `contextlib.asynccontextmanager` exists from Python 3.7 onwards.
+# For 3.6 we require the `async_generator` package for a backported version.
+try:
+    from contextlib import asynccontextmanager  # type: ignore
+except ImportError:  # pragma: no cover
+    from async_generator import asynccontextmanager  # type: ignore # noqa
index 0f1b2864ffb2d3fa49ebfc28069d1e6ef85b3d30..dfd4d73c944d49e60ff823c0213250aaf9e7c8aa 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -60,6 +60,7 @@ setup(
         "sniffio",
         "rfc3986[idna2008]>=1.3,<2",
         "httpcore>=0.12.1,<0.13",
+        "async_generator; python_version < '3.7'"
     ],
     extras_require={
         "http2": "h2==3.*",