]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Update to httpcore 0.10 (#1126)
authorTom Christie <tom@tomchristie.com>
Fri, 7 Aug 2020 13:14:11 +0000 (14:14 +0100)
committerGitHub <noreply@github.com>
Fri, 7 Aug 2020 13:14:11 +0000 (14:14 +0100)
* Keep HTTPError as a base class for .request() and .raise_for_status()

* Updates for httpcore 0.10

* Update httpx/_exceptions.py

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
* Use httpcore.SimpleByteStream/httpcore.IteratorByteStream

* Use httpcore.PlainByteStream

* Merge master

* Update to httpcore 0.10.x

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
19 files changed:
httpx/__init__.py
httpx/_client.py
httpx/_config.py
httpx/_exceptions.py
httpx/_transports/asgi.py
httpx/_transports/urllib3.py
httpx/_transports/wsgi.py
httpx/_utils.py
setup.py
tests/client/test_async_client.py
tests/client/test_auth.py
tests/client/test_client.py
tests/client/test_cookies.py
tests/client/test_headers.py
tests/client/test_proxies.py
tests/client/test_queryparams.py
tests/client/test_redirects.py
tests/test_config.py
tests/test_multipart.py

index 25c70e4bad6c09425f868bd585e94b6057fcc2c9..d9ae3513adc52706b89342b2f808e8f78581fb1b 100644 (file)
@@ -12,6 +12,7 @@ from ._exceptions import (
     HTTPError,
     HTTPStatusError,
     InvalidURL,
+    LocalProtocolError,
     NetworkError,
     NotRedirectResponse,
     PoolTimeout,
@@ -19,6 +20,7 @@ from ._exceptions import (
     ProxyError,
     ReadError,
     ReadTimeout,
+    RemoteProtocolError,
     RequestBodyUnavailable,
     RequestError,
     RequestNotRead,
@@ -29,6 +31,7 @@ from ._exceptions import (
     TimeoutException,
     TooManyRedirects,
     TransportError,
+    UnsupportedProtocol,
     WriteError,
     WriteTimeout,
 )
@@ -72,6 +75,9 @@ __all__ = [
     "HTTPError",
     "HTTPStatusError",
     "InvalidURL",
+    "UnsupportedProtocol",
+    "LocalProtocolError",
+    "RemoteProtocolError",
     "NetworkError",
     "NotRedirectResponse",
     "PoolTimeout",
index 7f4d257e748531ce647c401cbb2a6f5cbba96409..d319ed81ac9faba1fa6fbf9006d59f177dceb338 100644 (file)
@@ -19,7 +19,6 @@ from ._config import (
 from ._content_streams import ContentStream
 from ._exceptions import (
     HTTPCORE_EXC_MAP,
-    InvalidURL,
     RequestBodyUnavailable,
     TooManyRedirects,
     map_exceptions,
@@ -44,7 +43,6 @@ from ._types import (
 from ._utils import (
     NetRCInfo,
     URLPattern,
-    enforce_http_url,
     get_environment_proxies,
     get_logger,
     same_origin,
@@ -344,11 +342,6 @@ class BaseClient:
 
         url = URL(location)
 
-        # Check that we can handle the scheme
-        if url.scheme and url.scheme not in ("http", "https"):
-            message = f'Scheme "{url.scheme}" not supported.'
-            raise InvalidURL(message, request=request)
-
         # Handle malformed 'Location' headers that are "absolute" form, have no host.
         # See: https://github.com/encode/httpx/issues/771
         if url.scheme and not url.host:
@@ -540,8 +533,8 @@ class Client(BaseClient):
 
         return httpcore.SyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=limits.max_keepalive,
             max_connections=limits.max_connections,
+            max_keepalive_connections=limits.max_keepalive_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
@@ -562,20 +555,17 @@ class Client(BaseClient):
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=limits.max_keepalive,
             max_connections=limits.max_connections,
+            max_keepalive_connections=limits.max_keepalive_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
 
-    def _transport_for_url(self, request: Request) -> httpcore.SyncHTTPTransport:
+    def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
         """
         Returns the transport instance that should be used for a given URL.
         This will either be the standard connection pool, or a proxy.
         """
-        url = request.url
-        enforce_http_url(request)
-
         for pattern, transport in self._proxies.items():
             if pattern.matches(url):
                 return self._transport if transport is None else transport
@@ -620,10 +610,6 @@ class Client(BaseClient):
         allow_redirects: bool = True,
         timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
     ) -> Response:
-        if request.url.scheme not in ("http", "https"):
-            message = 'URL scheme must be "http" or "https".'
-            raise InvalidURL(message, request=request)
-
         timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
 
         auth = self._build_auth(request, auth)
@@ -714,7 +700,7 @@ class Client(BaseClient):
         """
         Sends a single request, without handling any redirections.
         """
-        transport = self._transport_for_url(request)
+        transport = self._transport_for_url(request.url)
 
         with map_exceptions(HTTPCORE_EXC_MAP, request=request):
             (
@@ -1072,8 +1058,8 @@ class AsyncClient(BaseClient):
 
         return httpcore.AsyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=limits.max_keepalive,
             max_connections=limits.max_connections,
+            max_keepalive_connections=limits.max_keepalive_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
@@ -1094,20 +1080,17 @@ class AsyncClient(BaseClient):
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=limits.max_keepalive,
             max_connections=limits.max_connections,
+            max_keepalive_connections=limits.max_keepalive_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
 
-    def _transport_for_url(self, request: Request) -> httpcore.AsyncHTTPTransport:
+    def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
         """
         Returns the transport instance that should be used for a given URL.
         This will either be the standard connection pool, or a proxy.
         """
-        url = request.url
-        enforce_http_url(request)
-
         for pattern, transport in self._proxies.items():
             if pattern.matches(url):
                 return self._transport if transport is None else transport
@@ -1245,7 +1228,7 @@ class AsyncClient(BaseClient):
         """
         Sends a single request, without handling any redirections.
         """
-        transport = self._transport_for_url(request)
+        transport = self._transport_for_url(request.url)
 
         with map_exceptions(HTTPCORE_EXC_MAP, request=request):
             (
index c140e12d3662b8cd1f60e4c0bac1e0e560304e81..37bb75a6e2e43763f0bcd24917bd44f93d931f02 100644 (file)
@@ -323,42 +323,53 @@ class Limits:
 
     **Parameters:**
 
-    * **max_keepalive** - Allow the connection pool to maintain keep-alive connections
-                       below this point.
     * **max_connections** - The maximum number of concurrent connections that may be
-                       established.
+            established.
+    * **max_keepalive_connections** - Allow the connection pool to maintain
+            keep-alive connections below this point. Should be less than or equal
+            to `max_connections`.
     """
 
     def __init__(
-        self, *, max_keepalive: int = None, max_connections: int = None,
+        self,
+        *,
+        max_connections: int = None,
+        max_keepalive_connections: int = None,
+        # Deprecated parameter naming, in favour of more explicit version:
+        max_keepalive: int = None,
     ):
-        self.max_keepalive = max_keepalive
+        if max_keepalive is not None:
+            warnings.warn(
+                "'max_keepalive' is deprecated. Use 'max_keepalive_connections'.",
+                DeprecationWarning,
+            )
+            max_keepalive_connections = max_keepalive
+
         self.max_connections = max_connections
+        self.max_keepalive_connections = max_keepalive_connections
 
     def __eq__(self, other: typing.Any) -> bool:
         return (
             isinstance(other, self.__class__)
-            and self.max_keepalive == other.max_keepalive
             and self.max_connections == other.max_connections
+            and self.max_keepalive_connections == other.max_keepalive_connections
         )
 
     def __repr__(self) -> str:
         class_name = self.__class__.__name__
         return (
-            f"{class_name}(max_keepalive={self.max_keepalive}, "
-            f"max_connections={self.max_connections})"
+            f"{class_name}(max_connections={self.max_connections}, "
+            f"max_keepalive_connections={self.max_keepalive_connections})"
         )
 
 
 class PoolLimits(Limits):
-    def __init__(
-        self, *, max_keepalive: int = None, max_connections: int = None,
-    ) -> None:
+    def __init__(self, **kwargs: typing.Any) -> None:
         warn_deprecated(
             "httpx.PoolLimits(...) is deprecated and will raise errors in the future. "
             "Use httpx.Limits(...) instead."
         )
-        super().__init__(max_keepalive=max_keepalive, max_connections=max_connections)
+        super().__init__(**kwargs)
 
 
 class Proxy:
index 4c22dd7351ea32fa6a23657ae90e606140be162d..465d558df0c70b65fed039fafba79bb5fa8d1e39 100644 (file)
@@ -15,11 +15,13 @@ Our exception hierarchy:
         · WriteError
         · CloseError
       - ProtocolError
+        · LocalProtocolError
+        · RemoteProtocolError
       - ProxyError
+      - UnsupportedProtocol
     + DecodingError
     + TooManyRedirects
     + RequestBodyUnavailable
-    + InvalidURL
   x HTTPStatusError
 * NotRedirectResponse
 * CookieConflict
@@ -153,9 +155,35 @@ class ProxyError(TransportError):
     """
 
 
+class UnsupportedProtocol(TransportError):
+    """
+    Attempted to make a request to an unsupported protocol.
+
+    For example issuing a request to `ftp://www.example.com`.
+    """
+
+
 class ProtocolError(TransportError):
     """
-    A protocol was violated by the server.
+    The protocol was violated.
+    """
+
+
+class LocalProtocolError(ProtocolError):
+    """
+    A protocol was violated by the client.
+
+    For example if the user instantiated a `Request` instance explicitly,
+    failed to include the mandatory `Host:` header, and then issued it directly
+    using `client.send()`.
+    """
+
+
+class RemoteProtocolError(ProtocolError):
+    """
+    The protocol was violated by the server.
+
+    For exaample, returning malformed HTTP.
     """
 
 
@@ -181,12 +209,6 @@ class RequestBodyUnavailable(RequestError):
     """
 
 
-class InvalidURL(RequestError):
-    """
-    URL was missing a hostname, or was not one of HTTP/HTTPS.
-    """
-
-
 # Client errors
 
 
@@ -297,6 +319,14 @@ class ResponseClosed(StreamError):
         super().__init__(message)
 
 
+# The `InvalidURL` class is no longer required. It was being used to enforce only
+# 'http'/'https' URLs being requested, but is now treated instead at the
+# transport layer using `UnsupportedProtocol()`.`
+
+# We are currently still exposing this class, but it will be removed in 1.0.
+InvalidURL = UnsupportedProtocol
+
+
 @contextlib.contextmanager
 def map_exceptions(
     mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
@@ -335,5 +365,8 @@ HTTPCORE_EXC_MAP = {
     httpcore.WriteError: WriteError,
     httpcore.CloseError: CloseError,
     httpcore.ProxyError: ProxyError,
+    httpcore.UnsupportedProtocol: UnsupportedProtocol,
     httpcore.ProtocolError: ProtocolError,
+    httpcore.LocalProtocolError: LocalProtocolError,
+    httpcore.RemoteProtocolError: RemoteProtocolError,
 }
index 051662946757b3f22bf56b5ff14d8c53f99c8b80..a58e10a6d68a8a712e318ad71ffdec99b8e11955 100644 (file)
@@ -1,13 +1,4 @@
-from typing import (
-    TYPE_CHECKING,
-    AsyncIterator,
-    Callable,
-    Dict,
-    List,
-    Optional,
-    Tuple,
-    Union,
-)
+from typing import TYPE_CHECKING, Callable, List, Mapping, Optional, Tuple, Union
 
 import httpcore
 import sniffio
@@ -31,10 +22,6 @@ def create_event() -> "Event":
         return asyncio.Event()
 
 
-async def async_byte_iterator(bytestring: bytes) -> AsyncIterator[bytes]:
-    yield bytestring
-
-
 class ASGITransport(httpcore.AsyncHTTPTransport):
     """
     A custom AsyncTransport that handles sending requests directly to an ASGI app.
@@ -86,14 +73,10 @@ class ASGITransport(httpcore.AsyncHTTPTransport):
         url: Tuple[bytes, bytes, Optional[int], bytes],
         headers: List[Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: Dict[str, Optional[float]] = None,
+        timeout: Mapping[str, Optional[float]] = None,
     ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream]:
         headers = [] if headers is None else headers
-        stream = (
-            httpcore.AsyncByteStream(async_byte_iterator(b""))
-            if stream is None
-            else stream
-        )
+        stream = httpcore.PlainByteStream(content=b"") if stream is None else stream
 
         # ASGI scope.
         scheme, host, port, full_path = url
@@ -170,8 +153,6 @@ class ASGITransport(httpcore.AsyncHTTPTransport):
         assert status_code is not None
         assert response_headers is not None
 
-        response_body = b"".join(body_parts)
-
-        stream = httpcore.AsyncByteStream(async_byte_iterator(response_body))
+        stream = httpcore.PlainByteStream(content=b"".join(body_parts))
 
         return (b"HTTP/1.1", status_code, b"", response_headers, stream)
index 7f076d2b38bfce04647fb4b7c4485215742c2a9a..c5b7af6cc2e6a31b717af96933aeae6565f7b31b 100644 (file)
@@ -1,5 +1,5 @@
 import socket
-from typing import Dict, Iterator, List, Optional, Tuple
+from typing import Iterator, List, Mapping, Optional, Tuple
 
 import httpcore
 
@@ -45,7 +45,7 @@ class URLLib3Transport(httpcore.SyncHTTPTransport):
         url: Tuple[bytes, bytes, Optional[int], bytes],
         headers: List[Tuple[bytes, bytes]] = None,
         stream: httpcore.SyncByteStream = None,
-        timeout: Dict[str, Optional[float]] = None,
+        timeout: Mapping[str, Optional[float]] = None,
     ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.SyncByteStream]:
         headers = [] if headers is None else headers
         stream = ByteStream(b"") if stream is None else stream
index 81d801d002955fdc29eac736ccd39a364668a720..0573c9cf4ccfe4c8add89c83e7a841bd3d3837a0 100644 (file)
@@ -64,7 +64,7 @@ class WSGITransport(httpcore.SyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.SyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes,
         int,
@@ -73,11 +73,7 @@ class WSGITransport(httpcore.SyncHTTPTransport):
         httpcore.SyncByteStream,
     ]:
         headers = [] if headers is None else headers
-        stream = (
-            httpcore.SyncByteStream(chunk for chunk in [b""])
-            if stream is None
-            else stream
-        )
+        stream = httpcore.PlainByteStream(content=b"") if stream is None else stream
 
         scheme, host, port, full_path = url
         path, _, query = full_path.partition(b"?")
@@ -130,6 +126,6 @@ class WSGITransport(httpcore.SyncHTTPTransport):
             (key.encode("ascii"), value.encode("ascii"))
             for key, value in seen_response_headers
         ]
-        stream = httpcore.SyncByteStream(chunk for chunk in result)
+        stream = httpcore.IteratorByteStream(iterator=result)
 
         return (b"HTTP/1.1", status_code, b"", headers, stream)
index 476407e7d75ae06f3439728f64eb9e1642c225ac..8080f63a4663587cfd24b219f9f82a08dcd3f562 100644 (file)
@@ -14,11 +14,10 @@ from time import perf_counter
 from types import TracebackType
 from urllib.request import getproxies
 
-from ._exceptions import InvalidURL
 from ._types import PrimitiveData
 
 if typing.TYPE_CHECKING:  # pragma: no cover
-    from ._models import URL, Request
+    from ._models import URL
 
 
 _HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
@@ -265,23 +264,6 @@ def get_logger(name: str) -> Logger:
     return typing.cast(Logger, logger)
 
 
-def enforce_http_url(request: "Request") -> None:
-    """
-    Raise an appropriate InvalidURL for any non-HTTP URLs.
-    """
-    url = request.url
-
-    if not url.scheme:
-        message = "No scheme included in URL."
-        raise InvalidURL(message, request=request)
-    if not url.host:
-        message = "No host included in URL."
-        raise InvalidURL(message, request=request)
-    if url.scheme not in ("http", "https"):
-        message = 'URL scheme must be "http" or "https".'
-        raise InvalidURL(message, request=request)
-
-
 def port_or_default(url: "URL") -> typing.Optional[int]:
     if url.port is not None:
         return url.port
index 4ce68e113f1402bc3b5af0bedd7fa0ee63403247..d8b545c5a32998a0afa5fda522dc850538d3d547 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -60,7 +60,7 @@ setup(
         "chardet==3.*",
         "idna==2.*",
         "rfc3986>=1.3,<2",
-        "httpcore==0.9.*",
+        "httpcore[http2]==0.10.*",
     ],
     classifiers=[
         "Development Status :: 4 - Beta",
index b50565a72345dd772fc71580ce2060098ccdeef0..ae3a0c07cf5de8cee128c9cc66c52370cfecc37c 100644 (file)
@@ -32,7 +32,7 @@ async def test_get(server):
 @pytest.mark.usefixtures("async_environment")
 async def test_get_invalid_url(server, url):
     async with httpx.AsyncClient() as client:
-        with pytest.raises(httpx.InvalidURL):
+        with pytest.raises((httpx.UnsupportedProtocol, httpx.LocalProtocolError)):
             await client.get(url)
 
 
index 29b6f20bf146be4bf08c182e52c6ee30703f9aae..32b1720ff1ccd54e27cb3685a7ae4edc5259574f 100644 (file)
@@ -40,7 +40,7 @@ class MockTransport:
         url: typing.Tuple[bytes, bytes, int, bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]],
         stream: ContentStream,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
@@ -90,7 +90,7 @@ class MockDigestAuthTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
index 03f3d788ed0f47bc98040af1d7c52317456fbfa9..a936e051945070b54a63c474f80baeb91cd71fce 100644 (file)
@@ -35,7 +35,7 @@ def test_get(server):
 )
 def test_get_invalid_url(server, url):
     with httpx.Client() as client:
-        with pytest.raises(httpx.InvalidURL):
+        with pytest.raises((httpx.UnsupportedProtocol, httpx.LocalProtocolError)):
             client.get(url)
 
 
index 68b6c64cf50a691f8da6a15a5e2cccacb1ceb107..77bf77e83021bf182659350c145111744d753a80 100644 (file)
@@ -23,7 +23,7 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
index 2f87c38a1f2cbc94a5a5e8826d647852aa2679a9..81b369b6d610fb0565b7b1e0f42417e68bd8a752 100755 (executable)
@@ -16,7 +16,7 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
index 45ff01743c3146125cd0339433a6e4e55950670a..4677ff0e52d68523ebc83947e60f78cabc6650ce 100644 (file)
@@ -111,8 +111,7 @@ PROXY_URL = "http://[::1]"
 )
 def test_transport_for_request(url, proxies, expected):
     client = httpx.AsyncClient(proxies=proxies)
-    request = httpx.Request(method="GET", url=url)
-    transport = client._transport_for_url(request)
+    transport = client._transport_for_url(httpx.URL(url))
 
     if expected is None:
         assert transport is client._transport
@@ -240,8 +239,7 @@ def test_proxies_environ(monkeypatch, client_class, url, env, expected):
         monkeypatch.setenv(name, value)
 
     client = client_class()
-    request = httpx.Request(method="GET", url=url)
-    transport = client._transport_for_url(request)
+    transport = client._transport_for_url(httpx.URL(url))
 
     if expected is None:
         assert transport == client._transport
index 10a03539e2f64413d6d04ae634435396d83d0131..1fcbe59448337b01eec24a0a897dabab17a25238 100644 (file)
@@ -14,7 +14,7 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
index 61e600cbb006ee65f40aaa81744bcf91e7b6264a..e699bb6945c73b2e5fe11ebbddd197a8c98ff943 100644 (file)
@@ -9,10 +9,10 @@ from httpx import (
     URL,
     AsyncClient,
     Client,
-    InvalidURL,
     NotRedirectResponse,
     RequestBodyUnavailable,
     TooManyRedirects,
+    UnsupportedProtocol,
     codes,
 )
 from httpx._content_streams import AsyncIteratorStream, ByteStream, ContentStream
@@ -33,11 +33,14 @@ class MockTransport:
         url: typing.Tuple[bytes, bytes, int, bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]],
         stream: ContentStream,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
         scheme, host, port, path = url
+        if scheme not in (b"http", b"https"):
+            raise httpcore.UnsupportedProtocol(f"Scheme {scheme!r} not supported.")
+
         path, _, query = path.partition(b"?")
         if path == b"/no_redirect":
             return b"HTTP/1.1", codes.OK, b"OK", [], ByteStream(b"")
@@ -405,7 +408,7 @@ class MockCookieTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
     ]:
@@ -481,6 +484,6 @@ async def test_redirect_cookie_behavior():
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_custom_scheme():
     client = AsyncClient(transport=AsyncMockTransport())
-    with pytest.raises(InvalidURL) as e:
+    with pytest.raises(UnsupportedProtocol) as e:
         await client.post("https://example.org/redirect_custom_scheme")
-    assert str(e.value) == 'Scheme "market" not supported.'
+    assert str(e.value) == "Scheme b'market' not supported."
index de9f4bc85b2e3dd93ca4324b28cbc9c337ca0c45..d3d391e20c8b85293071cbe4b182063e50b626e3 100644 (file)
@@ -102,7 +102,7 @@ def test_create_ssl_context_with_get_request(server, cert_pem_file):
 
 def test_limits_repr():
     limits = httpx.Limits(max_connections=100)
-    assert repr(limits) == "Limits(max_keepalive=None, max_connections=100)"
+    assert repr(limits) == "Limits(max_connections=100, max_keepalive_connections=None)"
 
 
 def test_limits_eq():
index 7d6f8e025d4c2d32cd985d41e53952a888c073fa..3230f927eb14ce2c0dbe964e4682342fa295c641 100644 (file)
@@ -19,7 +19,7 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
         headers: typing.List[typing.Tuple[bytes, bytes]] = None,
         stream: httpcore.AsyncByteStream = None,
-        timeout: typing.Dict[str, typing.Optional[float]] = None,
+        timeout: typing.Mapping[str, typing.Optional[float]] = None,
     ) -> typing.Tuple[
         bytes,
         int,