From 1bd6a737f602cec243d177005b0b28db714cbae8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Sep 2020 13:34:56 +0100 Subject: [PATCH 1/9] Version 0.15.0 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ httpx/__version__.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc3579a75..c7d58b1b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 0.15.0 + +### Added + +* Added support for event hooks. (Pull #1246) +* Added support for authentication flows which require either sync or async I/O. (Pull #1217) +* Added support for moniotoring download progress with `response.num_bytes_downloaded`. (Pull #1268) +* Added `Request(content=...)` for byte content, instead of overloading `Request(data=...)` (Pull #1266) +* Added support for all URL components as parameter names when using `url.copy_with(...)`. (Pull #1285) +* Neater split between automatically populated headers on `Request` instances, vs default `client.headers`. (Pull #1248) +* Unclosed `AsyncClient` instances will now raise warnings if garbage collected. (Pull #1197) +* Support `Response(content=..., text=..., html=..., json=...)` for creating usable response instances in code. (Pull #1265, #1297) +* Support instantiating requests from the low-level transport API. (Pull #1293) +* Raise errors on invalid URL types. (Pull #1259) + +### Changed + +* Cleaned up expected behaviour for URL escaping. `url.path` is now URL escaped. (Pull #1285) +* Cleaned up expected behaviour for bytes vs str in URL components. `url.userinfo` and `url.query` are not URL escaped, and so return bytes. (Pull #1285) +* Drop `url.authority` property in favour of `url.netloc`, since "authority" was semantically incorrect. (Pull #1285) +* Drop `url.full_path` property in favour of `url.raw_path`, for better consistency with other parts of the API. (Pull #1285) +* No longer use the `chardet` library for auto-detecting charsets, instead defaulting to a simpler approach when no charset is specified. (#1269) + +### Fixed + +* Swapped ordering of redirects and authentication flow. (Pull #1267) +* `.netrc` lookups should use host, not host+port. (Pull #1298) + +### Removed + +* The `URLLib3Transport` class no longer exists. We've published it instead as an example of [a custom transport class](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e). (Pull #1182) +* Drop `request.timer` attribute, which was being used internally to set `response.elapsed`. (Pull #1249) +* Drop `response.decoder` attribute, which was being used internally. (Pull #1276) +* `Request.prepare()` is now a private method. (Pull #1284) + ## 0.14.3 (September 2nd, 2020) ### Added diff --git a/httpx/__version__.py b/httpx/__version__.py index aff7043ec1..117eed369e 100644 --- a/httpx/__version__.py +++ b/httpx/__version__.py @@ -1,3 +1,3 @@ __title__ = "httpx" __description__ = "A next generation HTTP client, for Python 3." -__version__ = "0.14.3" +__version__ = "0.15.0" From d21cd7fc52d6bc7b8891feb3318608dc1f77278b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Sep 2020 15:42:16 +0100 Subject: [PATCH 2/9] Update CHANGELOG.md Co-authored-by: Jamie Hewland --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d58b1b3c..f20264aa55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added support for event hooks. (Pull #1246) * Added support for authentication flows which require either sync or async I/O. (Pull #1217) -* Added support for moniotoring download progress with `response.num_bytes_downloaded`. (Pull #1268) +* Added support for monitoring download progress with `response.num_bytes_downloaded`. (Pull #1268) * Added `Request(content=...)` for byte content, instead of overloading `Request(data=...)` (Pull #1266) * Added support for all URL components as parameter names when using `url.copy_with(...)`. (Pull #1285) * Neater split between automatically populated headers on `Request` instances, vs default `client.headers`. (Pull #1248) From 876182586b9db7a0042a9875dc6ce0d7921a783b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:31:00 +0100 Subject: [PATCH 3/9] Escalate deprecations into removals. --- httpx/_models.py | 19 ------------------- tests/client/test_client.py | 2 -- tests/models/test_headers.py | 3 --- tests/models/test_queryparams.py | 3 --- 4 files changed, 27 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index 83e644c2fa..5c5ae12b31 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -5,7 +5,6 @@ import json as jsonlib import typing import urllib.request -import warnings from collections.abc import MutableMapping from http.cookiejar import Cookie, CookieJar from urllib.parse import parse_qsl, quote, unquote, urlencode @@ -272,12 +271,6 @@ def raw(self) -> RawURL: self.raw_path, ) - @property - def is_ssl(self) -> bool: - message = 'URL.is_ssl() is pending deprecation. Use url.scheme == "https"' - warnings.warn(message, DeprecationWarning) - return self.scheme == "https" - @property def is_absolute_url(self) -> bool: """ @@ -525,13 +518,6 @@ def __repr__(self) -> str: query_string = str(self) return f"{class_name}({query_string!r})" - def getlist(self, key: typing.Any) -> typing.List[str]: - message = ( - "QueryParams.getlist() is pending deprecation. Use QueryParams.get_list()" - ) - warnings.warn(message, DeprecationWarning) - return self.get_list(key) - class Headers(typing.MutableMapping[str, str]): """ @@ -757,11 +743,6 @@ def __repr__(self) -> str: return f"{class_name}({as_dict!r}{encoding_str})" return f"{class_name}({as_list!r}{encoding_str})" - def getlist(self, key: str, split_commas: bool = False) -> typing.List[str]: - message = "Headers.getlist() is pending deprecation. Use Headers.get_list()" - warnings.warn(message, DeprecationWarning) - return self.get_list(key, split_commas=split_commas) - class Request: def __init__( diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 67c7faae50..54c67a366b 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -179,8 +179,6 @@ def test_merge_absolute_url(): client = httpx.Client(base_url="https://www.example.com/") request = client.build_request("GET", "http://www.example.com/") assert request.url == "http://www.example.com/" - with pytest.warns(DeprecationWarning): - assert not request.url.is_ssl def test_merge_relative_url(): diff --git a/tests/models/test_headers.py b/tests/models/test_headers.py index 8c1042105d..263db11920 100644 --- a/tests/models/test_headers.py +++ b/tests/models/test_headers.py @@ -15,9 +15,6 @@ def test_headers(): assert h.get("nope", default=None) is None assert h.get_list("a") == ["123", "456"] - with pytest.warns(DeprecationWarning): - assert h.getlist("a") == ["123", "456"] - assert list(h.keys()) == ["a", "b"] assert list(h.values()) == ["123, 456", "789"] assert list(h.items()) == [("a", "123, 456"), ("b", "789")] diff --git a/tests/models/test_queryparams.py b/tests/models/test_queryparams.py index b5ae86e9d0..d591eded8c 100644 --- a/tests/models/test_queryparams.py +++ b/tests/models/test_queryparams.py @@ -21,9 +21,6 @@ def test_queryparams(source): assert q.get("nope", default=None) is None assert q.get_list("a") == ["123", "456"] - with pytest.warns(DeprecationWarning): - assert q.getlist("a") == ["123", "456"] - assert list(q.keys()) == ["a", "b"] assert list(q.values()) == ["456", "789"] assert list(q.items()) == [("a", "456"), ("b", "789")] From 81a62ac1038190d0361403f0c8fbdd424306b128 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:33:15 +0100 Subject: [PATCH 4/9] Deprecate overly verbose timeout parameter names --- httpx/_config.py | 37 ------------------------------------- tests/test_timeouts.py | 14 -------------- 2 files changed, 51 deletions(-) diff --git a/httpx/_config.py b/httpx/_config.py index 8d589eadec..74d92890d6 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -212,44 +212,7 @@ def __init__( read: typing.Union[None, float, UnsetType] = UNSET, write: typing.Union[None, float, UnsetType] = UNSET, pool: typing.Union[None, float, UnsetType] = UNSET, - # Deprecated aliases. - connect_timeout: typing.Union[None, float, UnsetType] = UNSET, - read_timeout: typing.Union[None, float, UnsetType] = UNSET, - write_timeout: typing.Union[None, float, UnsetType] = UNSET, - pool_timeout: typing.Union[None, float, UnsetType] = UNSET, ): - if not isinstance(connect_timeout, UnsetType): - warn_deprecated( - "httpx.Timeout(..., connect_timeout=...) is deprecated and will " - "raise errors in a future version. " - "Use httpx.Timeout(..., connect=...) instead." - ) - connect = connect_timeout - - if not isinstance(read_timeout, UnsetType): - warn_deprecated( - "httpx.Timeout(..., read_timeout=...) is deprecated and will " - "raise errors in a future version. " - "Use httpx.Timeout(..., write=...) instead." - ) - read = read_timeout - - if not isinstance(write_timeout, UnsetType): - warn_deprecated( - "httpx.Timeout(..., write_timeout=...) is deprecated and will " - "raise errors in a future version. " - "Use httpx.Timeout(..., write=...) instead." - ) - write = write_timeout - - if not isinstance(pool_timeout, UnsetType): - warn_deprecated( - "httpx.Timeout(..., pool_timeout=...) is deprecated and will " - "raise errors in a future version. " - "Use httpx.Timeout(..., pool=...) instead." - ) - pool = pool_timeout - if isinstance(timeout, Timeout): # Passed as a single explicit Timeout. assert connect is UNSET diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py index 034b42f314..46a8bee8fc 100644 --- a/tests/test_timeouts.py +++ b/tests/test_timeouts.py @@ -41,17 +41,3 @@ async def test_pool_timeout(server): async with client.stream("GET", server.url): with pytest.raises(httpx.PoolTimeout): await client.get("http://localhost:8000/") - - -def test_deprecated_verbose_timeout_params(): - with pytest.warns(DeprecationWarning): - httpx.Timeout(None, read_timeout=1.0) - - with pytest.warns(DeprecationWarning): - httpx.Timeout(None, write_timeout=1.0) - - with pytest.warns(DeprecationWarning): - httpx.Timeout(None, connect_timeout=1.0) - - with pytest.warns(DeprecationWarning): - httpx.Timeout(None, pool_timeout=1.0) From 7946a3314d34e9ba0324dc5ccf38cfe1cb6cddb9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:35:27 +0100 Subject: [PATCH 5/9] Fully deprecate max_keepalive in favour of explicit max_keepalive_connections --- httpx/_config.py | 9 --------- tests/test_config.py | 5 ----- 2 files changed, 14 deletions(-) diff --git a/httpx/_config.py b/httpx/_config.py index 74d92890d6..c14786317f 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -298,16 +298,7 @@ def __init__( *, max_connections: int = None, max_keepalive_connections: int = None, - # Deprecated parameter naming, in favour of more explicit version: - max_keepalive: int = None, ): - 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 diff --git a/tests/test_config.py b/tests/test_config.py index 5c68badba3..d3d391e20c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -115,11 +115,6 @@ def test_pool_limits_deprecated(): httpx.PoolLimits() -def test_max_keepalive_deprecated(): - with pytest.warns(DeprecationWarning): - httpx.Limits(max_keepalive=50) - - def test_timeout_eq(): timeout = httpx.Timeout(timeout=5.0) assert timeout == httpx.Timeout(timeout=5.0) From 35767f808bebec7a391867258f63db65101ec18d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:36:27 +0100 Subject: [PATCH 6/9] Fully deprecate PoolLimits in favour of Limits --- httpx/__init__.py | 3 +-- httpx/_config.py | 9 --------- tests/test_config.py | 5 ----- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/httpx/__init__.py b/httpx/__init__.py index 842532142c..3e1fdc4520 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -2,7 +2,7 @@ from ._api import delete, get, head, options, patch, post, put, request, stream from ._auth import Auth, BasicAuth, DigestAuth from ._client import AsyncClient, Client -from ._config import Limits, PoolLimits, Proxy, Timeout, create_ssl_context +from ._config import Limits, Proxy, Timeout, create_ssl_context from ._exceptions import ( CloseError, ConnectError, @@ -70,7 +70,6 @@ "NotRedirectResponse", "options", "patch", - "PoolLimits", "PoolTimeout", "post", "ProtocolError", diff --git a/httpx/_config.py b/httpx/_config.py index c14786317f..6da8e422d9 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -317,15 +317,6 @@ def __repr__(self) -> str: ) -class PoolLimits(Limits): - 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__(**kwargs) - - class Proxy: def __init__( self, url: URLTypes, *, headers: HeaderTypes = None, mode: str = "DEFAULT" diff --git a/tests/test_config.py b/tests/test_config.py index d3d391e20c..11faf1cb18 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -110,11 +110,6 @@ def test_limits_eq(): assert limits == httpx.Limits(max_connections=100) -def test_pool_limits_deprecated(): - with pytest.warns(DeprecationWarning): - httpx.PoolLimits() - - def test_timeout_eq(): timeout = httpx.Timeout(timeout=5.0) assert timeout == httpx.Timeout(timeout=5.0) From 191fd74e8776a33bb6e4c77a7a12d6f4ac90a009 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:45:17 +0100 Subject: [PATCH 7/9] Deprecate instantiating 'Timeout' without fully explicit values --- httpx/_config.py | 10 +++------- tests/test_config.py | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/httpx/_config.py b/httpx/_config.py index 6da8e422d9..623392f47e 100644 --- a/httpx/_config.py +++ b/httpx/_config.py @@ -1,7 +1,6 @@ import os import ssl import typing -import warnings from base64 import b64encode from pathlib import Path @@ -9,7 +8,7 @@ from ._models import URL, Headers from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes -from ._utils import get_ca_bundle_from_env, get_logger, warn_deprecated +from ._utils import get_ca_bundle_from_env, get_logger DEFAULT_CIPHERS = ":".join( [ @@ -241,13 +240,10 @@ def __init__( self.pool = pool else: if isinstance(timeout, UnsetType): - warnings.warn( + raise ValueError( "httpx.Timeout must either include a default, or set all " - "four parameters explicitly. Omitting the default argument " - "is deprecated and will raise errors in a future version.", - DeprecationWarning, + "four parameters explicitly." ) - timeout = None self.connect = timeout if isinstance(connect, UnsetType) else connect self.read = timeout if isinstance(read, UnsetType) else read self.write = timeout if isinstance(write, UnsetType) else write diff --git a/tests/test_config.py b/tests/test_config.py index 11faf1cb18..23f29e00c6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -149,9 +149,8 @@ def test_timeout_from_one_value_and_default(): def test_timeout_missing_default(): - with pytest.warns(DeprecationWarning): - timeout = httpx.Timeout(pool=60.0) - assert timeout == httpx.Timeout(timeout=(None, None, None, 60.0)) + with pytest.raises(ValueError): + httpx.Timeout(pool=60.0) def test_timeout_from_tuple(): From e2c3e743a51803e79e7ddba38dcf28a3565af070 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 09:52:29 +0100 Subject: [PATCH 8/9] Include deprecation notes in changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f20264aa55..c889b4407f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Drop `request.timer` attribute, which was being used internally to set `response.elapsed`. (Pull #1249) * Drop `response.decoder` attribute, which was being used internally. (Pull #1276) * `Request.prepare()` is now a private method. (Pull #1284) +* The `Headers.getlist()` method had previously been deprecated in favour of `Headers.get_list()`. It is now fully removed. +* The `QueryParams.getlist()` method had previously been deprecated in favour of `QueryParams.get_list()`. It is now fully removed. +* The `URL.is_ssl` property had previously been deprecated in favour of `URL.scheme == "https"`. It is now fully removed. +* The `httpx.PoolLimits` class had previously been deprecated in favour of `httpx.Limits`. It is now fully removed. +* The `max_keepalive` setting had previously been deprecated in favour of the more explicit `max_keepalive_connections`. It is now fully removed. +* The verbose `httpx.Timeout(5.0, connect_timeout=60.0)` style had previously been deprecated in favour of `httpx.Timeout(5.0, connect=60.0)`. It is now fully removed. +* Support for instantiating a timeout config missing some defaults, such as `httpx.Timeout(connect=60.0)`, had previously been deprecated in favour of enforcing a more explicit style, such as `httpx.Timeout(5.0, connect=60.0)`. This is now strictly enforced. ## 0.14.3 (September 2nd, 2020) From f6b4ccde96531d0f26cb4b0c28025e1f8c7cc5a1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Sep 2020 11:10:32 +0100 Subject: [PATCH 9/9] Use httpcore 0.11.x --- httpx/_client.py | 24 ++++--------- httpx/_models.py | 10 ++++-- httpx/_transports/asgi.py | 11 +++--- httpx/_transports/wsgi.py | 11 +++--- setup.py | 2 +- tests/client/test_auth.py | 60 +++++++++++++++----------------- tests/client/test_event_hooks.py | 8 ++--- tests/client/test_redirects.py | 6 ++-- tests/utils.py | 25 +++++-------- 9 files changed, 68 insertions(+), 89 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index f797a2e33c..6208535f0b 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -850,18 +850,12 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response: timer.sync_start() with map_exceptions(HTTPCORE_EXC_MAP, request=request): - ( - http_version, - status_code, - reason_phrase, - headers, - stream, - ) = transport.request( + (status_code, headers, stream, ext) = transport.request( request.method.encode(), request.url.raw, headers=request.headers.raw, stream=request.stream, # type: ignore - timeout=timeout.as_dict(), + ext={"timeout": timeout.as_dict()}, ) def on_close(response: Response) -> None: @@ -871,9 +865,9 @@ def on_close(response: Response) -> None: response = Response( status_code, - http_version=http_version.decode("ascii"), headers=headers, stream=stream, # type: ignore + ext=ext, request=request, on_close=on_close, ) @@ -1501,18 +1495,12 @@ async def _send_single_request( await timer.async_start() with map_exceptions(HTTPCORE_EXC_MAP, request=request): - ( - http_version, - status_code, - reason_phrase, - headers, - stream, - ) = await transport.request( + (status_code, headers, stream, ext,) = await transport.arequest( request.method.encode(), request.url.raw, headers=request.headers.raw, stream=request.stream, # type: ignore - timeout=timeout.as_dict(), + ext={"timeout": timeout.as_dict()}, ) async def on_close(response: Response) -> None: @@ -1522,9 +1510,9 @@ async def on_close(response: Response) -> None: response = Response( status_code, - http_version=http_version.decode("ascii"), headers=headers, stream=stream, # type: ignore + ext=ext, request=request, on_close=on_close, ) diff --git a/httpx/_models.py b/httpx/_models.py index 5c5ae12b31..03635f3e38 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -864,19 +864,19 @@ def __init__( html: str = None, json: typing.Any = None, stream: ByteStream = None, - http_version: str = None, request: Request = None, + ext: dict = None, history: typing.List["Response"] = None, on_close: typing.Callable = None, ): self.status_code = status_code - self.http_version = http_version self.headers = Headers(headers) self._request: typing.Optional[Request] = request self.call_next: typing.Optional[typing.Callable] = None + self.ext = {} if ext is None else ext self.history = [] if history is None else list(history) self._on_close = on_close @@ -945,9 +945,13 @@ def request(self) -> Request: def request(self, value: Request) -> None: self._request = value + @property + def http_version(self) -> str: + return self.ext.get("http_version", "HTTP/1.1") + @property def reason_phrase(self) -> str: - return codes.get_reason_phrase(self.status_code) + return self.ext.get("reason", codes.get_reason_phrase(self.status_code)) @property def url(self) -> typing.Optional[URL]: diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py index a58e10a6d6..94976f3aad 100644 --- a/httpx/_transports/asgi.py +++ b/httpx/_transports/asgi.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Callable, List, Mapping, Optional, Tuple, Union +from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union import httpcore import sniffio @@ -67,14 +67,14 @@ def __init__( self.root_path = root_path self.client = client - async def request( + async def arequest( self, method: bytes, url: Tuple[bytes, bytes, Optional[int], bytes], headers: List[Tuple[bytes, bytes]] = None, stream: httpcore.AsyncByteStream = None, - timeout: Mapping[str, Optional[float]] = None, - ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream]: + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: headers = [] if headers is None else headers stream = httpcore.PlainByteStream(content=b"") if stream is None else stream @@ -154,5 +154,6 @@ async def send(message: dict) -> None: assert response_headers is not None stream = httpcore.PlainByteStream(content=b"".join(body_parts)) + ext = {} - return (b"HTTP/1.1", status_code, b"", response_headers, stream) + return (status_code, response_headers, stream, ext) diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index 0573c9cf4c..5169d92e70 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -64,13 +64,9 @@ def request( url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes], headers: typing.List[typing.Tuple[bytes, bytes]] = None, stream: httpcore.SyncByteStream = None, - timeout: typing.Mapping[str, typing.Optional[float]] = None, + ext: dict = None, ) -> typing.Tuple[ - bytes, - int, - bytes, - typing.List[typing.Tuple[bytes, bytes]], - httpcore.SyncByteStream, + int, typing.List[typing.Tuple[bytes, bytes]], httpcore.SyncByteStream, dict ]: headers = [] if headers is None else headers stream = httpcore.PlainByteStream(content=b"") if stream is None else stream @@ -127,5 +123,6 @@ def start_response( for key, value in seen_response_headers ] stream = httpcore.IteratorByteStream(iterator=result) + ext = {} - return (b"HTTP/1.1", status_code, b"", headers, stream) + return (status_code, headers, stream, ext) diff --git a/setup.py b/setup.py index d7006dd805..482c27208e 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def get_packages(package): "certifi", "sniffio", "rfc3986[idna2008]>=1.3,<2", - "httpcore==0.10.*", + "httpcore==0.11.*", ], extras_require={ "http2": "h2==3.*", diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 59777b8c69..e3d328c6fa 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -13,7 +13,7 @@ import httpx from httpx import URL, Auth, BasicAuth, DigestAuth, ProtocolError, Request, Response -from tests.utils import AsyncMockTransport, MockTransport +from tests.utils import MockTransport from ..common import FIXTURES_DIR @@ -155,7 +155,7 @@ async def test_basic_auth() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -167,7 +167,7 @@ async def test_basic_auth_in_url() -> None: url = "https://tomchristie:password123@example.org/" app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -180,9 +180,7 @@ async def test_basic_auth_on_session() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient( - transport=AsyncMockTransport(app), auth=auth - ) as client: + async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: response = await client.get(url) assert response.status_code == 200 @@ -198,7 +196,7 @@ def auth(request: Request) -> Request: request.headers["Authorization"] = "Token 123" return request - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -211,7 +209,7 @@ async def test_netrc_auth() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -226,7 +224,7 @@ async def test_auth_header_has_priority_over_netrc() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, headers={"Authorization": "Override"}) assert response.status_code == 200 @@ -240,7 +238,7 @@ async def test_trust_env_auth() -> None: app = App() async with httpx.AsyncClient( - transport=AsyncMockTransport(app), trust_env=False + transport=MockTransport(app), trust_env=False ) as client: response = await client.get(url) @@ -248,7 +246,7 @@ async def test_trust_env_auth() -> None: assert response.json() == {"auth": None} async with httpx.AsyncClient( - transport=AsyncMockTransport(app), trust_env=True + transport=MockTransport(app), trust_env=True ) as client: response = await client.get(url) @@ -264,9 +262,7 @@ async def test_auth_disable_per_request() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient( - transport=AsyncMockTransport(app), auth=auth - ) as client: + async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: response = await client.get(url, auth=None) assert response.status_code == 200 @@ -286,7 +282,7 @@ async def test_auth_hidden_header() -> None: auth = ("example-username", "example-password") app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert "'authorization': '[secure]'" in str(response.request.headers) @@ -296,7 +292,7 @@ async def test_auth_hidden_header() -> None: async def test_auth_property() -> None: app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: assert client.auth is None client.auth = ("tomchristie", "password123") # type: ignore @@ -314,11 +310,11 @@ async def test_auth_invalid_type() -> None: with pytest.raises(TypeError): client = httpx.AsyncClient( - transport=AsyncMockTransport(app), + transport=MockTransport(app), auth="not a tuple, not a callable", # type: ignore ) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: with pytest.raises(TypeError): await client.get(auth="not a tuple, not a callable") # type: ignore @@ -332,7 +328,7 @@ async def test_digest_auth_returns_no_auth_if_no_digest_header_in_response() -> auth = DigestAuth(username="tomchristie", password="password123") app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -361,7 +357,7 @@ async def test_digest_auth_200_response_including_digest_auth_header() -> None: auth_header = 'Digest realm="realm@host.com",qop="auth",nonce="abc",opaque="xyz"' app = App(auth_header=auth_header, status_code=200) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -375,7 +371,7 @@ async def test_digest_auth_401_response_without_digest_auth_header() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header="", status_code=401) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -404,7 +400,7 @@ async def test_digest_auth( auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(algorithm=algorithm) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -435,7 +431,7 @@ async def test_digest_auth_no_specified_qop() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="") - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -467,7 +463,7 @@ async def test_digest_auth_qop_including_spaces_and_auth_returns_auth(qop: str) auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop=qop) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -480,7 +476,7 @@ async def test_digest_auth_qop_auth_int_not_implemented() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="auth-int") - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: with pytest.raises(NotImplementedError): await client.get(url, auth=auth) @@ -491,7 +487,7 @@ async def test_digest_auth_qop_must_be_auth_or_auth_int() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="not-auth") - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -502,7 +498,7 @@ async def test_digest_auth_incorrect_credentials() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(send_response_after_attempt=2) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -524,7 +520,7 @@ async def test_async_digest_auth_raises_protocol_error_on_malformed_header( auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header=auth_header, status_code=401) - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -558,7 +554,7 @@ async def test_async_auth_history() -> None: auth = RepeatAuth(repeat=2) app = App(auth_header="abc") - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -610,7 +606,7 @@ async def test_digest_auth_unavailable_streaming_body(): async def streaming_body(): yield b"Example request body" # pragma: nocover - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: with pytest.raises(httpx.StreamConsumed): await client.post(url, data=streaming_body(), auth=auth) @@ -625,7 +621,7 @@ async def test_async_auth_reads_response_body() -> None: auth = ResponseBodyAuth("xyz") app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -659,7 +655,7 @@ async def test_async_auth() -> None: auth = SyncOrAsyncAuth() app = App() - async with httpx.AsyncClient(transport=AsyncMockTransport(app)) as client: + async with httpx.AsyncClient(transport=MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 diff --git a/tests/client/test_event_hooks.py b/tests/client/test_event_hooks.py index a2cfa936a4..a81f31e1e5 100644 --- a/tests/client/test_event_hooks.py +++ b/tests/client/test_event_hooks.py @@ -1,7 +1,7 @@ import pytest import httpx -from tests.utils import AsyncMockTransport, MockTransport +from tests.utils import MockTransport def app(request: httpx.Request) -> httpx.Response: @@ -73,7 +73,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=AsyncMockTransport(app) + event_hooks=event_hooks, transport=MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/", auth=("username", "password")) @@ -104,7 +104,7 @@ async def raise_on_4xx_5xx(response): event_hooks = {"response": [raise_on_4xx_5xx]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=AsyncMockTransport(app) + event_hooks=event_hooks, transport=MockTransport(app) ) as http: try: await http.get("http://127.0.0.1:8000/status/400") @@ -166,7 +166,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=AsyncMockTransport(app) + event_hooks=event_hooks, transport=MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/redirect", auth=("username", "password")) diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index f32512bbf9..1c7911bc35 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -2,7 +2,7 @@ import pytest import httpx -from tests.utils import AsyncMockTransport, MockTransport +from tests.utils import MockTransport def redirects(request: httpx.Request) -> httpx.Response: @@ -232,14 +232,14 @@ def test_multiple_redirects(): @pytest.mark.usefixtures("async_environment") async def test_async_too_many_redirects(): - async with httpx.AsyncClient(transport=AsyncMockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: with pytest.raises(httpx.TooManyRedirects): await client.get("https://example.org/multiple_redirects?count=21") @pytest.mark.usefixtures("async_environment") async def test_async_too_many_redirects_calling_next(): - async with httpx.AsyncClient(transport=AsyncMockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: url = "https://example.org/multiple_redirects?count=21" response = await client.get(url, allow_redirects=False) with pytest.raises(httpx.TooManyRedirects): diff --git a/tests/utils.py b/tests/utils.py index ba8a188e78..75157dee7e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ import contextlib import logging import os -from typing import Callable, List, Mapping, Optional, Tuple +from typing import Callable, List, Optional, Tuple import httpcore @@ -24,7 +24,7 @@ def override_log_level(log_level: str): logging.getLogger("httpx").handlers = [] -class MockTransport(httpcore.SyncHTTPTransport): +class MockTransport(httpcore.SyncHTTPTransport, httpcore.AsyncHTTPTransport): def __init__(self, handler: Callable) -> None: self.handler = handler @@ -34,8 +34,8 @@ def request( url: Tuple[bytes, bytes, Optional[int], bytes], headers: List[Tuple[bytes, bytes]] = None, stream: httpcore.SyncByteStream = None, - timeout: Mapping[str, Optional[float]] = None, - ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.SyncByteStream]: + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.SyncByteStream, dict]: request = httpx.Request( method=method, url=url, @@ -45,26 +45,20 @@ def request( request.read() response = self.handler(request) return ( - (response.http_version or "HTTP/1.1").encode("ascii"), response.status_code, - response.reason_phrase.encode("ascii"), response.headers.raw, response.stream, + response.ext, ) - -class AsyncMockTransport(httpcore.AsyncHTTPTransport): - def __init__(self, handler: Callable) -> None: - self.handler = handler - - async def request( + async def arequest( self, method: bytes, url: Tuple[bytes, bytes, Optional[int], bytes], headers: List[Tuple[bytes, bytes]] = None, stream: httpcore.AsyncByteStream = None, - timeout: Mapping[str, Optional[float]] = None, - ) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream]: + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: request = httpx.Request( method=method, url=url, @@ -74,9 +68,8 @@ async def request( await request.aread() response = self.handler(request) return ( - (response.http_version or "HTTP/1.1").encode("ascii"), response.status_code, - response.reason_phrase.encode("ascii"), response.headers.raw, response.stream, + response.ext, )