From: Tom Christie Date: Fri, 7 Aug 2020 13:14:11 +0000 (+0100) Subject: Update to httpcore 0.10 (#1126) X-Git-Tag: 0.14.0~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=876e722b2419047e518ace030f2c9505df960049;p=thirdparty%2Fhttpx.git Update to httpcore 0.10 (#1126) * 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 * Use httpcore.SimpleByteStream/httpcore.IteratorByteStream * Use httpcore.PlainByteStream * Merge master * Update to httpcore 0.10.x Co-authored-by: Stephen Brown II --- diff --git a/httpx/__init__.py b/httpx/__init__.py index 25c70e4b..d9ae3513 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -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", diff --git a/httpx/_client.py b/httpx/_client.py index 7f4d257e..d319ed81 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -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): ( diff --git a/httpx/_config.py b/httpx/_config.py index c140e12d..37bb75a6 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -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: diff --git a/httpx/_exceptions.py b/httpx/_exceptions.py index 4c22dd73..465d558d 100644 --- a/httpx/_exceptions.py +++ b/httpx/_exceptions.py @@ -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, } diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py index 05166294..a58e10a6 100644 --- a/httpx/_transports/asgi.py +++ b/httpx/_transports/asgi.py @@ -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) diff --git a/httpx/_transports/urllib3.py b/httpx/_transports/urllib3.py index 7f076d2b..c5b7af6c 100644 --- a/httpx/_transports/urllib3.py +++ b/httpx/_transports/urllib3.py @@ -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 diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index 81d801d0..0573c9cf 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -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) diff --git a/httpx/_utils.py b/httpx/_utils.py index 476407e7..8080f63a 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -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 diff --git a/setup.py b/setup.py index 4ce68e11..d8b545c5 100644 --- 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", diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index b50565a7..ae3a0c07 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -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) diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 29b6f20b..32b1720f 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -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 ]: diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 03f3d788..a936e051 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -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) diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py index 68b6c64c..77bf77e8 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookies.py @@ -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 ]: diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py index 2f87c38a..81b369b6 100755 --- a/tests/client/test_headers.py +++ b/tests/client/test_headers.py @@ -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 ]: diff --git a/tests/client/test_proxies.py b/tests/client/test_proxies.py index 45ff0174..4677ff0e 100644 --- a/tests/client/test_proxies.py +++ b/tests/client/test_proxies.py @@ -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 diff --git a/tests/client/test_queryparams.py b/tests/client/test_queryparams.py index 10a03539..1fcbe594 100644 --- a/tests/client/test_queryparams.py +++ b/tests/client/test_queryparams.py @@ -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 ]: diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index 61e600cb..e699bb69 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -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." diff --git a/tests/test_config.py b/tests/test_config.py index de9f4bc8..d3d391e2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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(): diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 7d6f8e02..3230f927 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -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,