]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Version 0.21 (#1935) 0.21.0
authorTom Christie <tom@tomchristie.com>
Mon, 15 Nov 2021 14:30:54 +0000 (14:30 +0000)
committerGitHub <noreply@github.com>
Mon, 15 Nov 2021 14:30:54 +0000 (14:30 +0000)
* Integrate with httpcore 0.14

* Fix pool timeout test

* Add request extensions to API

* Add certificate and connection info to client, using 'trace' extension

* Fix test_pool_timeout flakiness

httpx/__version__.py
httpx/_client.py
httpx/_main.py
httpx/_transports/default.py
setup.py
tests/client/test_proxies.py
tests/test_exceptions.py
tests/test_main.py
tests/test_timeouts.py

index 448f37f4ada4c77ffc8b5919d5acfa67c6377459..94a5a3310e2f7a7d261d5153592e4a182212ba2f 100644 (file)
@@ -1,3 +1,3 @@
 __title__ = "httpx"
 __description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.20.0"
+__version__ = "0.21.0"
index 537a4b9158b0501ba43a22d0a389d895cbe94739..021128d58456911dbc3c4549a191bbaca033eb67 100644 (file)
@@ -323,6 +323,7 @@ class BaseClient:
         headers: HeaderTypes = None,
         cookies: CookieTypes = None,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Request:
         """
         Build and return a request instance.
@@ -339,9 +340,14 @@ class BaseClient:
         headers = self._merge_headers(headers)
         cookies = self._merge_cookies(cookies)
         params = self._merge_queryparams(params)
-        timeout = (
-            self.timeout if isinstance(timeout, UseClientDefault) else Timeout(timeout)
-        )
+        extensions = {} if extensions is None else extensions
+        if "timeout" not in extensions:
+            timeout = (
+                self.timeout
+                if isinstance(timeout, UseClientDefault)
+                else Timeout(timeout)
+            )
+            extensions["timeout"] = timeout.as_dict()
         return Request(
             method,
             url,
@@ -352,7 +358,7 @@ class BaseClient:
             params=params,
             headers=headers,
             cookies=cookies,
-            extensions={"timeout": timeout.as_dict()},
+            extensions=extensions,
         )
 
     def _merge_url(self, url: URLTypes) -> URL:
@@ -459,7 +465,12 @@ class BaseClient:
         stream = self._redirect_stream(request, method)
         cookies = Cookies(self.cookies)
         return Request(
-            method=method, url=url, headers=headers, cookies=cookies, stream=stream
+            method=method,
+            url=url,
+            headers=headers,
+            cookies=cookies,
+            stream=stream,
+            extensions=request.extensions,
         )
 
     def _redirect_method(self, request: Request, response: Response) -> str:
@@ -749,6 +760,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Build and send a request.
@@ -785,6 +797,7 @@ class Client(BaseClient):
             headers=headers,
             cookies=cookies,
             timeout=timeout,
+            extensions=extensions,
         )
         return self.send(request, auth=auth, follow_redirects=follow_redirects)
 
@@ -804,6 +817,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> typing.Iterator[Response]:
         """
         Alternative to `httpx.request()` that streams the response body
@@ -826,6 +840,7 @@ class Client(BaseClient):
             headers=headers,
             cookies=cookies,
             timeout=timeout,
+            extensions=extensions,
         )
         response = self.send(
             request=request,
@@ -1000,6 +1015,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `GET` request.
@@ -1015,6 +1031,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def options(
@@ -1027,6 +1044,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send an `OPTIONS` request.
@@ -1042,6 +1060,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def head(
@@ -1054,6 +1073,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `HEAD` request.
@@ -1069,6 +1089,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def post(
@@ -1085,6 +1106,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `POST` request.
@@ -1104,6 +1126,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def put(
@@ -1120,6 +1143,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `PUT` request.
@@ -1139,6 +1163,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def patch(
@@ -1155,6 +1180,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `PATCH` request.
@@ -1174,6 +1200,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def delete(
@@ -1186,6 +1213,7 @@ class Client(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `DELETE` request.
@@ -1201,6 +1229,7 @@ class Client(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     def close(self) -> None:
@@ -1450,6 +1479,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Build and send a request.
@@ -1478,6 +1508,7 @@ class AsyncClient(BaseClient):
             headers=headers,
             cookies=cookies,
             timeout=timeout,
+            extensions=extensions,
         )
         return await self.send(request, auth=auth, follow_redirects=follow_redirects)
 
@@ -1497,6 +1528,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> typing.AsyncIterator[Response]:
         """
         Alternative to `httpx.request()` that streams the response body
@@ -1519,6 +1551,7 @@ class AsyncClient(BaseClient):
             headers=headers,
             cookies=cookies,
             timeout=timeout,
+            extensions=extensions,
         )
         response = await self.send(
             request=request,
@@ -1693,6 +1726,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `GET` request.
@@ -1708,6 +1742,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def options(
@@ -1720,6 +1755,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send an `OPTIONS` request.
@@ -1735,6 +1771,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def head(
@@ -1747,6 +1784,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `HEAD` request.
@@ -1762,6 +1800,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def post(
@@ -1778,6 +1817,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `POST` request.
@@ -1797,6 +1837,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def put(
@@ -1813,6 +1854,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `PUT` request.
@@ -1832,6 +1874,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def patch(
@@ -1848,6 +1891,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `PATCH` request.
@@ -1867,6 +1911,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def delete(
@@ -1879,6 +1924,7 @@ class AsyncClient(BaseClient):
         auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
         follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
         timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
+        extensions: dict = None,
     ) -> Response:
         """
         Send a `DELETE` request.
@@ -1894,6 +1940,7 @@ class AsyncClient(BaseClient):
             auth=auth,
             follow_redirects=follow_redirects,
             timeout=timeout,
+            extensions=extensions,
         )
 
     async def aclose(self) -> None:
index 6c299ba050c5f310f8fa6a86746be5d0ccc02eb0..42426c7a71bd309338543f8b0985a4307c533dab 100644 (file)
@@ -4,6 +4,7 @@ import sys
 import typing
 
 import click
+import httpcore
 import pygments.lexers
 import pygments.util
 import rich.console
@@ -12,7 +13,8 @@ import rich.syntax
 
 from ._client import Client
 from ._exceptions import RequestError
-from ._models import Request, Response
+from ._models import Response
+from ._status_codes import codes
 
 
 def print_help() -> None:
@@ -102,29 +104,38 @@ def get_lexer_for_response(response: Response) -> str:
     return ""  # pragma: nocover
 
 
-def format_request_headers(request: Request, http2: bool = False) -> str:
+def format_request_headers(request: httpcore.Request, http2: bool = False) -> str:
     version = "HTTP/2" if http2 else "HTTP/1.1"
     headers = [
-        (name.lower() if http2 else name, value) for name, value in request.headers.raw
+        (name.lower() if http2 else name, value) for name, value in request.headers
     ]
-    target = request.url.raw[-1].decode("ascii")
-    lines = [f"{request.method} {target} {version}"] + [
+    method = request.method.decode("ascii")
+    target = request.url.target.decode("ascii")
+    lines = [f"{method} {target} {version}"] + [
         f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
     ]
     return "\n".join(lines)
 
 
-def format_response_headers(response: Response) -> str:
-    lines = [
-        f"{response.http_version} {response.status_code} {response.reason_phrase}"
-    ] + [
-        f"{name.decode('ascii')}: {value.decode('ascii')}"
-        for name, value in response.headers.raw
+def format_response_headers(
+    http_version: bytes,
+    status: int,
+    reason_phrase: typing.Optional[bytes],
+    headers: typing.List[typing.Tuple[bytes, bytes]],
+) -> str:
+    version = http_version.decode("ascii")
+    reason = (
+        codes.get_reason_phrase(status)
+        if reason_phrase is None
+        else reason_phrase.decode("ascii")
+    )
+    lines = [f"{version} {status} {reason}"] + [
+        f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
     ]
     return "\n".join(lines)
 
 
-def print_request_headers(request: Request, http2: bool = False) -> None:
+def print_request_headers(request: httpcore.Request, http2: bool = False) -> None:
     console = rich.console.Console()
     http_text = format_request_headers(request, http2=http2)
     syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
@@ -133,26 +144,20 @@ def print_request_headers(request: Request, http2: bool = False) -> None:
     console.print(syntax)
 
 
-def print_response_headers(response: Response) -> None:
+def print_response_headers(
+    http_version: bytes,
+    status: int,
+    reason_phrase: typing.Optional[bytes],
+    headers: typing.List[typing.Tuple[bytes, bytes]],
+) -> None:
     console = rich.console.Console()
-    http_text = format_response_headers(response)
+    http_text = format_response_headers(http_version, status, reason_phrase, headers)
     syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
     console.print(syntax)
-
-
-def print_delimiter() -> None:
-    console = rich.console.Console()
     syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
     console.print(syntax)
 
 
-def print_redirects(response: Response) -> None:
-    if response.has_redirect_location:
-        response.read()
-        print_response_headers(response)
-        print_response(response)
-
-
 def print_response(response: Response) -> None:
     console = rich.console.Console()
     lexer_name = get_lexer_for_response(response)
@@ -171,6 +176,61 @@ def print_response(response: Response) -> None:
         console.print(response.text)
 
 
+def format_certificate(cert: dict) -> str:  # pragma: nocover
+    lines = []
+    for key, value in cert.items():
+        if isinstance(value, (list, tuple)):
+            lines.append(f"*   {key}:")
+            for item in value:
+                if key in ("subject", "issuer"):
+                    for sub_item in item:
+                        lines.append(f"*     {sub_item[0]}: {sub_item[1]!r}")
+                elif isinstance(item, tuple) and len(item) == 2:
+                    lines.append(f"*     {item[0]}: {item[1]!r}")
+                else:
+                    lines.append(f"*     {item!r}")
+        else:
+            lines.append(f"*   {key}: {value!r}")
+    return "\n".join(lines)
+
+
+def trace(name: str, info: dict, verbose: bool = False) -> None:
+    console = rich.console.Console()
+    if name == "connection.connect_tcp.started" and verbose:
+        host = info["host"]
+        console.print(f"* Connecting to {host!r}")
+    elif name == "connection.connect_tcp.complete" and verbose:
+        stream = info["return_value"]
+        server_addr = stream.get_extra_info("server_addr")
+        console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}")
+    elif name == "connection.start_tls.complete" and verbose:  # pragma: nocover
+        stream = info["return_value"]
+        ssl_object = stream.get_extra_info("ssl_object")
+        version = ssl_object.version()
+        cipher = ssl_object.cipher()
+        server_cert = ssl_object.getpeercert()
+        alpn = ssl_object.selected_alpn_protocol()
+        console.print(f"* SSL established using {version!r} / {cipher[0]!r}")
+        console.print(f"* Selected ALPN protocol: {alpn!r}")
+        if server_cert:
+            console.print("* Server certificate:")
+            console.print(format_certificate(server_cert))
+    elif name == "http11.send_request_headers.started" and verbose:
+        request = info["request"]
+        print_request_headers(request, http2=False)
+    elif name == "http2.send_request_headers.started" and verbose:  # pragma: nocover
+        request = info["request"]
+        print_request_headers(request, http2=True)
+    elif name == "http11.receive_response_headers.complete":
+        http_version, status, reason_phrase, headers = info["return_value"]
+        print_response_headers(http_version, status, reason_phrase, headers)
+    elif name == "http2.receive_response_headers.complete":  # pragma: nocover
+        status, headers = info["return_value"]
+        http_version = b"HTTP/2"
+        reason_phrase = None
+        print_response_headers(http_version, status, reason_phrase, headers)
+
+
 def download_response(response: Response, download: typing.BinaryIO) -> None:
     console = rich.console.Console()
     syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
@@ -397,19 +457,12 @@ def main(
     if not method:
         method = "POST" if content or data or files or json else "GET"
 
-    event_hooks: typing.Dict[str, typing.List[typing.Callable]] = {}
-    if verbose:
-        event_hooks["request"] = [functools.partial(print_request_headers, http2=http2)]
-    if follow_redirects:
-        event_hooks["response"] = [print_redirects]
-
     try:
         with Client(
             proxies=proxies,
             timeout=timeout,
             verify=verify,
             http2=http2,
-            event_hooks=event_hooks,
         ) as client:
             with client.stream(
                 method,
@@ -423,20 +476,18 @@ def main(
                 cookies=dict(cookies),
                 auth=auth,
                 follow_redirects=follow_redirects,
+                extensions={"trace": functools.partial(trace, verbose=verbose)},
             ) as response:
-                print_response_headers(response)
-
                 if download is not None:
                     download_response(response, download)
                 else:
                     response.read()
                     if response.content:
-                        print_delimiter()
                         print_response(response)
 
     except RequestError as exc:
         console = rich.console.Console()
-        console.print(f"{type(exc).__name__}: {exc}")
+        console.print(f"[red]{type(exc).__name__}[/red]: {str(exc)}")
         sys.exit(1)
 
     sys.exit(0 if response.is_success else 1)
index 22328ef60fe2ff4587cf90397e298dac48cb8808..4b400d8de973f51e4dd681e807b49885f06f1315 100644 (file)
@@ -6,7 +6,6 @@ The following additional keyword arguments are currently supported by httpcore..
 * uds: str
 * local_address: str
 * retries: int
-* backend: str ("auto", "asyncio", "trio", "curio", "anyio", "sync")
 
 Example usages...
 
@@ -32,7 +31,6 @@ import httpcore
 
 from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
 from .._exceptions import (
-    CloseError,
     ConnectError,
     ConnectTimeout,
     LocalProtocolError,
@@ -89,7 +87,6 @@ HTTPCORE_EXC_MAP = {
     httpcore.ConnectError: ConnectError,
     httpcore.ReadError: ReadError,
     httpcore.WriteError: WriteError,
-    httpcore.CloseError: CloseError,
     httpcore.ProxyError: ProxyError,
     httpcore.UnsupportedProtocol: UnsupportedProtocol,
     httpcore.ProtocolError: ProtocolError,
@@ -99,7 +96,7 @@ HTTPCORE_EXC_MAP = {
 
 
 class ResponseStream(SyncByteStream):
-    def __init__(self, httpcore_stream: httpcore.SyncByteStream):
+    def __init__(self, httpcore_stream: typing.Iterable[bytes]):
         self._httpcore_stream = httpcore_stream
 
     def __iter__(self) -> typing.Iterator[bytes]:
@@ -108,8 +105,8 @@ class ResponseStream(SyncByteStream):
                 yield part
 
     def close(self) -> None:
-        with map_httpcore_exceptions():
-            self._httpcore_stream.close()
+        if hasattr(self._httpcore_stream, "close"):
+            self._httpcore_stream.close()  # type: ignore
 
 
 class HTTPTransport(BaseTransport):
@@ -125,12 +122,11 @@ class HTTPTransport(BaseTransport):
         uds: str = None,
         local_address: str = None,
         retries: int = 0,
-        backend: str = "sync",
     ) -> None:
         ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
 
         if proxy is None:
-            self._pool = httpcore.SyncConnectionPool(
+            self._pool = httpcore.ConnectionPool(
                 ssl_context=ssl_context,
                 max_connections=limits.max_connections,
                 max_keepalive_connections=limits.max_keepalive_connections,
@@ -140,18 +136,20 @@ class HTTPTransport(BaseTransport):
                 uds=uds,
                 local_address=local_address,
                 retries=retries,
-                backend=backend,
             )
         else:
-            self._pool = httpcore.SyncHTTPProxy(
-                proxy_url=proxy.url.raw,
+            self._pool = httpcore.HTTPProxy(
+                proxy_url=httpcore.URL(
+                    scheme=proxy.url.raw_scheme,
+                    host=proxy.url.raw_host,
+                    port=proxy.url.port,
+                    target=proxy.url.raw_path,
+                ),
                 proxy_headers=proxy.headers.raw,
                 ssl_context=ssl_context,
                 max_connections=limits.max_connections,
                 max_keepalive_connections=limits.max_keepalive_connections,
                 keepalive_expiry=limits.keepalive_expiry,
-                http2=http2,
-                backend=backend,
             )
 
     def __enter__(self: T) -> T:  # Use generics for subclass support.
@@ -173,19 +171,28 @@ class HTTPTransport(BaseTransport):
     ) -> Response:
         assert isinstance(request.stream, SyncByteStream)
 
+        req = httpcore.Request(
+            method=request.method,
+            url=httpcore.URL(
+                scheme=request.url.raw_scheme,
+                host=request.url.raw_host,
+                port=request.url.port,
+                target=request.url.raw_path,
+            ),
+            headers=request.headers.raw,
+            content=request.stream,
+            extensions=request.extensions,
+        )
         with map_httpcore_exceptions():
-            status_code, headers, byte_stream, extensions = self._pool.handle_request(
-                method=request.method.encode("ascii"),
-                url=request.url.raw,
-                headers=request.headers.raw,
-                stream=httpcore.IteratorByteStream(iter(request.stream)),
-                extensions=request.extensions,
-            )
+            resp = self._pool.handle_request(req)
 
-        stream = ResponseStream(byte_stream)
+        assert isinstance(resp.stream, typing.Iterable)
 
         return Response(
-            status_code, headers=headers, stream=stream, extensions=extensions
+            status_code=resp.status,
+            headers=resp.headers,
+            stream=ResponseStream(resp.stream),
+            extensions=resp.extensions,
         )
 
     def close(self) -> None:
@@ -193,7 +200,7 @@ class HTTPTransport(BaseTransport):
 
 
 class AsyncResponseStream(AsyncByteStream):
-    def __init__(self, httpcore_stream: httpcore.AsyncByteStream):
+    def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]):
         self._httpcore_stream = httpcore_stream
 
     async def __aiter__(self) -> typing.AsyncIterator[bytes]:
@@ -202,8 +209,8 @@ class AsyncResponseStream(AsyncByteStream):
                 yield part
 
     async def aclose(self) -> None:
-        with map_httpcore_exceptions():
-            await self._httpcore_stream.aclose()
+        if hasattr(self._httpcore_stream, "aclose"):
+            await self._httpcore_stream.aclose()  # type: ignore
 
 
 class AsyncHTTPTransport(AsyncBaseTransport):
@@ -219,7 +226,6 @@ class AsyncHTTPTransport(AsyncBaseTransport):
         uds: str = None,
         local_address: str = None,
         retries: int = 0,
-        backend: str = "auto",
     ) -> None:
         ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
 
@@ -234,18 +240,20 @@ class AsyncHTTPTransport(AsyncBaseTransport):
                 uds=uds,
                 local_address=local_address,
                 retries=retries,
-                backend=backend,
             )
         else:
             self._pool = httpcore.AsyncHTTPProxy(
-                proxy_url=proxy.url.raw,
+                proxy_url=httpcore.URL(
+                    scheme=proxy.url.raw_scheme,
+                    host=proxy.url.raw_host,
+                    port=proxy.url.port,
+                    target=proxy.url.raw_path,
+                ),
                 proxy_headers=proxy.headers.raw,
                 ssl_context=ssl_context,
                 max_connections=limits.max_connections,
                 max_keepalive_connections=limits.max_keepalive_connections,
                 keepalive_expiry=limits.keepalive_expiry,
-                http2=http2,
-                backend=backend,
             )
 
     async def __aenter__(self: A) -> A:  # Use generics for subclass support.
@@ -267,24 +275,28 @@ class AsyncHTTPTransport(AsyncBaseTransport):
     ) -> Response:
         assert isinstance(request.stream, AsyncByteStream)
 
+        req = httpcore.Request(
+            method=request.method,
+            url=httpcore.URL(
+                scheme=request.url.raw_scheme,
+                host=request.url.raw_host,
+                port=request.url.port,
+                target=request.url.raw_path,
+            ),
+            headers=request.headers.raw,
+            content=request.stream,
+            extensions=request.extensions,
+        )
         with map_httpcore_exceptions():
-            (
-                status_code,
-                headers,
-                byte_stream,
-                extensions,
-            ) = await self._pool.handle_async_request(
-                method=request.method.encode("ascii"),
-                url=request.url.raw,
-                headers=request.headers.raw,
-                stream=httpcore.AsyncIteratorByteStream(request.stream.__aiter__()),
-                extensions=request.extensions,
-            )
+            resp = await self._pool.handle_async_request(req)
 
-        stream = AsyncResponseStream(byte_stream)
+        assert isinstance(resp.stream, typing.AsyncIterable)
 
         return Response(
-            status_code, headers=headers, stream=stream, extensions=extensions
+            status_code=resp.status,
+            headers=resp.headers,
+            stream=AsyncResponseStream(resp.stream),
+            extensions=resp.extensions,
         )
 
     async def aclose(self) -> None:
index e22afc6f135d6f191d4dc34935723d81977d7e01..e72c5be2ec688cb67111036f1b1d1f2f15e1390e 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -60,7 +60,7 @@ setup(
         "charset_normalizer",
         "sniffio",
         "rfc3986[idna2008]>=1.3,<2",
-        "httpcore>=0.13.3,<0.14.0",
+        "httpcore>=0.14.0,<0.15.0",
         "async_generator; python_version < '3.7'"
     ],
     extras_require={
index 2817d202b9d2cb51f49074d10fe19368fd8df06e..893e35cf08621cbed9174ce47835f19a91eaa7dd 100644 (file)
@@ -10,11 +10,8 @@ def url_to_origin(url: str):
     Given a URL string, return the origin in the raw tuple format that
     `httpcore` uses for it's representation.
     """
-    DEFAULT_PORTS = {b"http": 80, b"https": 443}
-    scheme, host, explicit_port = httpx.URL(url).raw[:3]
-    default_port = DEFAULT_PORTS[scheme]
-    port = default_port if explicit_port is None else explicit_port
-    return scheme, host, port
+    scheme, host, port = httpx.URL(url).raw[:3]
+    return httpcore.URL(scheme=scheme, host=host, port=port, target="/")
 
 
 @pytest.mark.parametrize(
@@ -44,8 +41,8 @@ def test_proxies_parameter(proxies, expected_proxies):
         assert pattern in client._mounts
         proxy = client._mounts[pattern]
         assert isinstance(proxy, httpx.HTTPTransport)
-        assert isinstance(proxy._pool, httpcore.SyncHTTPProxy)
-        assert proxy._pool.proxy_origin == url_to_origin(url)
+        assert isinstance(proxy._pool, httpcore.HTTPProxy)
+        assert proxy._pool._proxy_url == url_to_origin(url)
 
     assert len(expected_proxies) == len(client._mounts)
 
@@ -117,8 +114,8 @@ def test_transport_for_request(url, proxies, expected):
         assert transport is client._transport
     else:
         assert isinstance(transport, httpx.HTTPTransport)
-        assert isinstance(transport._pool, httpcore.SyncHTTPProxy)
-        assert transport._pool.proxy_origin == url_to_origin(expected)
+        assert isinstance(transport._pool, httpcore.HTTPProxy)
+        assert transport._pool._proxy_url == url_to_origin(expected)
 
 
 @pytest.mark.asyncio
@@ -253,7 +250,7 @@ def test_proxies_environ(monkeypatch, client_class, url, env, expected):
     if expected is None:
         assert transport == client._transport
     else:
-        assert transport._pool.proxy_origin == url_to_origin(expected)
+        assert transport._pool._proxy_url == url_to_origin(expected)
 
 
 @pytest.mark.parametrize(
index 8d28fda0d746d8589b4d9b154814073d820ae8a4..1356cd22fc9e11b0cd87a2585ebe48daf973f3c1 100644 (file)
@@ -18,6 +18,7 @@ def test_httpcore_all_exceptions_mapped() -> None:
         if isinstance(value, type)
         and issubclass(value, Exception)
         and value not in HTTPCORE_EXC_MAP
+        and value is not httpcore.ConnectionNotAvailable
     ]
 
     if not_mapped:  # pragma: nocover
@@ -39,33 +40,21 @@ def test_httpcore_exception_mapping(server) -> None:
         def close(self):
             pass
 
-    class CloseFailedStream:
-        def __iter__(self):
-            yield b""
-
-        def close(self):
-            raise httpcore.CloseError()
-
     with mock.patch(
-        "httpcore.SyncConnectionPool.handle_request", side_effect=connect_failed
+        "httpcore.ConnectionPool.handle_request", side_effect=connect_failed
     ):
         with pytest.raises(httpx.ConnectError):
             httpx.get(server.url)
 
     with mock.patch(
-        "httpcore.SyncConnectionPool.handle_request",
-        return_value=(200, [], TimeoutStream(), {}),
+        "httpcore.ConnectionPool.handle_request",
+        return_value=httpcore.Response(
+            200, headers=[], content=TimeoutStream(), extensions={}
+        ),
     ):
         with pytest.raises(httpx.ReadTimeout):
             httpx.get(server.url)
 
-    with mock.patch(
-        "httpcore.SyncConnectionPool.handle_request",
-        return_value=(200, [], CloseFailedStream(), {}),
-    ):
-        with pytest.raises(httpx.CloseError):
-            httpx.get(server.url)
-
 
 def test_httpx_exceptions_exposed() -> None:
     """
index 1afd538e3f453fb5e468035eaeeac14c17905c9a..e9f56df8da04f605267005d39b0a08d667b92777 100644 (file)
@@ -62,6 +62,7 @@ def test_redirects(server):
         "server: uvicorn",
         "location: /",
         "Transfer-Encoding: chunked",
+        "",
     ]
 
 
@@ -106,6 +107,8 @@ def test_verbose(server):
     result = runner.invoke(httpx.main, [url, "-v"])
     assert result.exit_code == 0
     assert remove_date_header(splitlines(result.output)) == [
+        "* Connecting to '127.0.0.1'",
+        "* Connected to '127.0.0.1' on port 8000",
         "GET / HTTP/1.1",
         f"Host: {server.url.netloc.decode('ascii')}",
         "Accept: */*",
@@ -129,6 +132,8 @@ def test_auth(server):
     print(result.output)
     assert result.exit_code == 0
     assert remove_date_header(splitlines(result.output)) == [
+        "* Connecting to '127.0.0.1'",
+        "* Connected to '127.0.0.1' on port 8000",
         "GET / HTTP/1.1",
         f"Host: {server.url.netloc.decode('ascii')}",
         "Accept: */*",
index c7a665c3bb10d01586cb725cae57d9b3bea61de1..dee17df2f74c54df18876fa759a94529da18cb3c 100644 (file)
@@ -39,6 +39,6 @@ async def test_pool_timeout(server):
     timeout = httpx.Timeout(None, pool=1e-4)
 
     async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
-        async with client.stream("GET", server.url):
-            with pytest.raises(httpx.PoolTimeout):
-                await client.get("http://localhost:8000/")
+        with pytest.raises(httpx.PoolTimeout):
+            async with client.stream("GET", server.url):
+                await client.get(server.url)