From 86964054d60da7796c65e501ba021440b14cc36c Mon Sep 17 00:00:00 2001 From: SarunasAzna Date: Sat, 12 Dec 2020 18:38:37 +0100 Subject: [PATCH 01/10] Allow tuple as input of query parameters. (#1426) * Allow tuple as input of query parameters. In the documentation it is stated that params can be dict, string or two tuples. This allows to used two tuples. Previously it was possible to use only tuple inside a list. * tests for two tuples * use isinstance to check the type of query params * change list|tuple to in Sequence * update documentation * fix typing --- httpx/_api.py | 2 +- httpx/_client.py | 4 ++-- httpx/_models.py | 2 +- httpx/_types.py | 1 + tests/models/test_queryparams.py | 2 ++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/httpx/_api.py b/httpx/_api.py index 985e2ab938..8cfaf6dfda 100644 --- a/httpx/_api.py +++ b/httpx/_api.py @@ -47,7 +47,7 @@ def request( `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`. * **url** - URL for the new `Request` object. * **params** - *(optional)* Query parameters to include in the URL, as a - string, dictionary, or list of two-tuples. + string, dictionary, or sequence of two-tuples. * **content** - *(optional)* Binary content to include in the body of the request, as bytes or a byte iterator. * **data** - *(optional)* Form data to include in the body of the request, diff --git a/httpx/_client.py b/httpx/_client.py index 0ae5f2b1fa..4f457c79dc 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -520,7 +520,7 @@ class Client(BaseClient): * **auth** - *(optional)* An authentication class to use when sending requests. * **params** - *(optional)* Query parameters to include in request URLs, as - a string, dictionary, or list of two-tuples. + a string, dictionary, or sequence of two-tuples. * **headers** - *(optional)* Dictionary of HTTP headers to include when sending requests. * **cookies** - *(optional)* Dictionary of Cookie items to include when @@ -1161,7 +1161,7 @@ class AsyncClient(BaseClient): * **auth** - *(optional)* An authentication class to use when sending requests. * **params** - *(optional)* Query parameters to include in request URLs, as - a string, dictionary, or list of two-tuples. + a string, dictionary, or sequence of two-tuples. * **headers** - *(optional)* Dictionary of HTTP headers to include when sending requests. * **cookies** - *(optional)* Dictionary of Cookie items to include when diff --git a/httpx/_models.py b/httpx/_models.py index a77f7a5790..2d11888254 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -441,7 +441,7 @@ def __init__(self, *args: QueryParamTypes, **kwargs: typing.Any) -> None: items = parse_qsl(value) elif isinstance(value, QueryParams): items = value.multi_items() - elif isinstance(value, list): + elif isinstance(value, (list, tuple)): items = value else: items = flatten_queryparams(value) diff --git a/httpx/_types.py b/httpx/_types.py index 776df1d8dc..7768bac11b 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -35,6 +35,7 @@ "QueryParams", Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]], List[Tuple[str, PrimitiveData]], + Tuple[Tuple[str, PrimitiveData], ...], str, bytes, None, diff --git a/tests/models/test_queryparams.py b/tests/models/test_queryparams.py index d591eded8c..7031a65cb9 100644 --- a/tests/models/test_queryparams.py +++ b/tests/models/test_queryparams.py @@ -9,6 +9,8 @@ "a=123&a=456&b=789", {"a": ["123", "456"], "b": 789}, {"a": ("123", "456"), "b": 789}, + [("a", "123"), ("a", "456"), ("b", "789")], + (("a", "123"), ("a", "456"), ("b", "789")), ], ) def test_queryparams(source): From 3c89b91d6b912825dacf54113df4f1926dc3ca42 Mon Sep 17 00:00:00 2001 From: Gerhard van Andel Date: Sun, 20 Dec 2020 19:43:33 +0000 Subject: [PATCH 02/10] Update exceptions.md (#1432) --- docs/exceptions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/exceptions.md b/docs/exceptions.md index 1bc86100db..949ac47a19 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -11,7 +11,7 @@ while issuing an HTTP request. These exceptions include a `.request` attribute. try: response = httpx.get("https://www.example.com/") except httpx.RequestError as exc: - print(f"An error occured while requesting {exc.request.url!r}.") + print(f"An error occurred while requesting {exc.request.url!r}.") ``` The `HTTPStatusError` class is raised by `response.raise_for_status()` on 4xx and 5xx responses. @@ -45,7 +45,7 @@ try: response = httpx.get("https://www.example.com/") response.raise_for_status() except httpx.RequestError as exc: - print(f"An error occured while requesting {exc.request.url!r}.") + print(f"An error occurred while requesting {exc.request.url!r}.") except httpx.HTTPStatusError as exc: print(f"Error response {exc.response.status_code} while requesting {exc.request.url!r}.") ``` From e3a7b6d7318f943b2289437f74028cb36b5b02e4 Mon Sep 17 00:00:00 2001 From: Colton Eakins Date: Wed, 23 Dec 2020 13:09:43 -0600 Subject: [PATCH 03/10] Updates compatibility guide to address event hooks (#1436) * Updates compatibility guide to address event hooks In `requests`, event hook callbacks can mutate response/request objects. In HTTPX, this is not the case. Added text to address this difference, and added a link to the best alternate HTTPX offers in this circumstance. More context: https://github.com/encode/httpx/issues/1343#issuecomment-703223097 * Apply suggestions from code review Co-authored-by: Florimond Manca Co-authored-by: Florimond Manca --- docs/compatibility.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/compatibility.md b/docs/compatibility.md index 564767118b..c8a074e491 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -135,3 +135,11 @@ while request is not None: response = client.send(request, allow_redirects=False) request = response.next_request ``` + +## Event Hooks + +`requests` allows event hooks to mutate `Request` and `Response` objects. See [examples](https://requests.readthedocs.io/en/master/user/advanced/#event-hooks) given in the documentation for `requests`. + +In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response. + +If you are looking for more control, consider checking out [Custom Transports](advanced.md#custom-transports). From f4165e9e091f7ca622a996ce531a2e8638e941a0 Mon Sep 17 00:00:00 2001 From: shan7030 <42472191+shan7030@users.noreply.github.com> Date: Fri, 25 Dec 2020 21:27:14 +0530 Subject: [PATCH 04/10] Add curio docs in Supported async environments (#1437) Fixes: #1418 --- docs/async.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/async.md b/docs/async.md index 3238097a82..69a1fb419f 100644 --- a/docs/async.md +++ b/docs/async.md @@ -156,6 +156,27 @@ trio.run(main) !!! important The `trio` package must be installed to use the Trio backend. + +### [Curio](https://github.com/dabeaz/curio) + +Curio is a [coroutine-based library](https://curio.readthedocs.io/en/latest/tutorial.html) +for concurrent Python systems programming. + +```python +import httpx +import curio + +async def main(): + async with httpx.AsyncClient() as client: + response = await client.get('https://www.example.com/') + print(response) + +curio.run(main) +``` + +!!! important + The `curio` package must be installed to use the Curio backend. + ## Calling into Python Web Apps Just as `httpx.Client` allows you to call directly into WSGI web applications, From 7f9bb5f32d9af77badcbe4347040cbf6c97242cd Mon Sep 17 00:00:00 2001 From: Florimond Manca Date: Tue, 29 Dec 2020 13:38:04 +0100 Subject: [PATCH 05/10] Remove stale reference to "Travis" (#1440) --- docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index bb9eaa86bc..9732c81059 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -87,7 +87,7 @@ To run the documentation site locally (useful for previewing changes), use: $ scripts/docs ``` -## Resolving Build / Travis Failures +## Resolving Build / CI Failures Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub. If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed. From 25781a7625d4b0eae598cb338a1501c71ae52e75 Mon Sep 17 00:00:00 2001 From: Florimond Manca Date: Tue, 29 Dec 2020 13:40:53 +0100 Subject: [PATCH 06/10] Add troubleshooting guide, with initial proxies entries (#1435) * Add troubleshooting guide, with initial proxies entries * Drop unrelated issue --- docs/advanced.md | 4 +++ docs/troubleshooting.md | 61 +++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 66 insertions(+) create mode 100644 docs/troubleshooting.md diff --git a/docs/advanced.md b/docs/advanced.md index 59c04ca1e2..837acd4941 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -583,6 +583,10 @@ with httpx.Client(proxies=proxies) as client: r = client.get("http://example.com") ``` +### Troubleshooting proxies + +If you encounter issues when setting up proxies, please refer to our [Troubleshooting guide](troubleshooting.md#proxies). + ## Timeout Configuration HTTPX is careful to enforce timeouts everywhere by default. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000000..6461574974 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,61 @@ +# Troubleshooting + +This page lists some common problems or issues you could encounter while developing with HTTPX, as well as possible solutions. + +## Proxies + +--- + +### "`The handshake operation timed out`" on HTTPS requests when using a proxy + +**Description**: When using a proxy and making an HTTPS request, you see an exception looking like this: + +```console +httpx.ProxyError: _ssl.c:1091: The handshake operation timed out +``` + +**Similar issues**: [encode/httpx#1412](https://github.com/encode/httpx/issues/1412), [encode/httpx#1433](https://github.com/encode/httpx/issues/1433) + +**Resolution**: it is likely that you've set up your proxies like this... + +```python +proxies = { + "http": "http://myproxy.org", + "https": "https://myproxy.org", +} +``` + +Using this setup, you're telling HTTPX to connect to the proxy using HTTP for HTTP requests, and using HTTPS for HTTPS requests. + +But if you get the error above, it is likely that your proxy doesn't support connecting via HTTPS. Don't worry: that's a [common gotcha](advanced.md#example). + +Change the scheme of your HTTPS proxy to `http://...` instead of `https://...`: + +```python +proxies = { + "http": "http://myproxy.org", + "https": "http://myproxy.org", +} +``` + +This can be simplified to: + +```python +proxies = "http://myproxy.org" +``` + +For more information, see [Proxies: FORWARD vs TUNNEL](advanced.md#forward-vs-tunnel). + +--- + +### Error when making requests to an HTTPS proxy + +**Description**: your proxy _does_ support connecting via HTTPS, but you are seeing errors along the lines of... + +```console +httpx.ProxyError: [SSL: PRE_MAC_LENGTH_TOO_LONG] invalid alert (_ssl.c:1091) +``` + +**Similar issues**: [encode/httpx#1424](https://github.com/encode/httpx/issues/1424). + +**Resolution**: HTTPX does not properly support HTTPS proxies at this time. If that's something you're interested in having, please see [encode/httpx#1434](https://github.com/encode/httpx/issues/1434) and consider lending a hand there. diff --git a/mkdocs.yml b/mkdocs.yml index a6fbb8e6aa..2f1f94b819 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Requests Compatibility: 'compatibility.md' - Developer Interface: 'api.md' - Exceptions: 'exceptions.md' + - Troubleshooting: 'troubleshooting.md' - Third Party Packages: 'third-party-packages.md' - Contributing: 'contributing.md' From 3bf18637c1289575f3cc00467fbde1a34bbdf332 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 30 Dec 2020 17:21:35 +0100 Subject: [PATCH 07/10] Remove double "then" in docs/http2.md (#1442) --- docs/http2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/http2.md b/docs/http2.md index 84148c3b33..56b032dd09 100644 --- a/docs/http2.md +++ b/docs/http2.md @@ -31,7 +31,7 @@ the optional HTTP/2 dependencies... $ pip install httpx[http2] ``` -And then then instantiating a client with HTTP/2 support enabled: +And then instantiating a client with HTTP/2 support enabled: ```python client = httpx.AsyncClient(http2=True) From 9c7c2ace99ce6732377e18d35bcdca1a8f1d37e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Jan 2021 11:04:26 +0000 Subject: [PATCH 08/10] Add `httpx.MockTransport()` (#1401) * Add httpx.MockTransport * Add docs on MockTransport * Add pointer to RESPX * Add note on pytest-httpx * Tweak existing docs example to use 'httpx.MockTransport' Co-authored-by: Florimond Manca --- docs/advanced.md | 30 ++++++++++++- httpx/__init__.py | 2 + httpx/_transports/mock.py | 56 ++++++++++++++++++++++++ tests/client/test_async_client.py | 11 +++-- tests/client/test_auth.py | 73 +++++++++++++++++-------------- tests/client/test_client.py | 15 ++++--- tests/client/test_cookies.py | 13 +++--- tests/client/test_event_hooks.py | 19 +++++--- tests/client/test_headers.py | 19 ++++---- tests/client/test_queryparams.py | 3 +- tests/client/test_redirects.py | 51 +++++++++++---------- tests/test_multipart.py | 9 ++-- tests/utils.py | 55 ----------------------- 13 files changed, 198 insertions(+), 158 deletions(-) create mode 100644 httpx/_transports/mock.py diff --git a/docs/advanced.md b/docs/advanced.md index 837acd4941..5463250f11 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1062,6 +1062,31 @@ Which we can use in the same way: {"text": "Hello, world!"} ``` +### Mock transports + +During testing it can often be useful to be able to mock out a transport, +and return pre-determined responses, rather than making actual network requests. + +The `httpx.MockTransport` class accepts a handler function, which can be used +to map requests onto pre-determined responses: + +```python +def handler(request): + return httpx.Response(200, json={"text": "Hello, world!"}) + + +# Switch to a mock transport, if the TESTING environment variable is set. +if os.environ['TESTING'].upper() == "TRUE": + transport = httpx.MockTransport(handler) +else: + transport = httpx.HTTPTransport() + +client = httpx.Client(transport=transport) +``` + +For more advanced use-cases you might want to take a look at either [the third-party +mocking library, RESPX](https://lundberg.github.io/respx/), or the [pytest-httpx library](https://github.com/Colin-b/pytest_httpx). + ### Mounting transports You can also mount transports against given schemes or domains, to control @@ -1101,7 +1126,10 @@ Mocking requests to a given domain: ```python # All requests to "example.org" should be mocked out. # Other requests occur as usual. -mounts = {"all://example.org": MockTransport()} +def handler(request): + return httpx.Response(200, json={"text": "Hello, World!"}) + +mounts = {"all://example.org": httpx.MockTransport(handler)} client = httpx.Client(mounts=mounts) ``` diff --git a/httpx/__init__.py b/httpx/__init__.py index 489ffeae4f..8a6d9b32ce 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -36,6 +36,7 @@ from ._models import URL, Cookies, Headers, QueryParams, Request, Response from ._status_codes import StatusCode, codes from ._transports.asgi import ASGITransport +from ._transports.mock import MockTransport from ._transports.wsgi import WSGITransport __all__ = [ @@ -65,6 +66,7 @@ "InvalidURL", "Limits", "LocalProtocolError", + "MockTransport", "NetworkError", "options", "patch", diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py new file mode 100644 index 0000000000..6f9ebc1e0f --- /dev/null +++ b/httpx/_transports/mock.py @@ -0,0 +1,56 @@ +from typing import Callable, List, Optional, Tuple + +import httpcore + +from .._models import Request + + +class MockTransport(httpcore.SyncHTTPTransport, httpcore.AsyncHTTPTransport): + def __init__(self, handler: Callable) -> None: + self.handler = handler + + def request( + self, + method: bytes, + url: Tuple[bytes, bytes, Optional[int], bytes], + headers: List[Tuple[bytes, bytes]] = None, + stream: httpcore.SyncByteStream = None, + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.SyncByteStream, dict]: + request = Request( + method=method, + url=url, + headers=headers, + stream=stream, + ) + request.read() + response = self.handler(request) + return ( + response.status_code, + response.headers.raw, + response.stream, + response.ext, + ) + + async def arequest( + self, + method: bytes, + url: Tuple[bytes, bytes, Optional[int], bytes], + headers: List[Tuple[bytes, bytes]] = None, + stream: httpcore.AsyncByteStream = None, + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: + request = Request( + method=method, + url=url, + headers=headers, + stream=stream, + ) + await request.aread() + response = self.handler(request) + return ( + response.status_code, + response.headers.raw, + response.stream, + response.ext, + ) diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index 696f202cff..0cc9d8a4ff 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -5,7 +5,6 @@ import pytest import httpx -from tests.utils import MockTransport @pytest.mark.usefixtures("async_environment") @@ -247,7 +246,7 @@ def hello_world(request): @pytest.mark.usefixtures("async_environment") async def test_client_closed_state_using_implicit_open(): - client = httpx.AsyncClient(transport=MockTransport(hello_world)) + client = httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) assert not client.is_closed await client.get("http://example.com") @@ -262,7 +261,7 @@ async def test_client_closed_state_using_implicit_open(): @pytest.mark.usefixtures("async_environment") async def test_client_closed_state_using_with_block(): - async with httpx.AsyncClient(transport=MockTransport(hello_world)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) as client: assert not client.is_closed await client.get("http://example.com") @@ -273,7 +272,7 @@ async def test_client_closed_state_using_with_block(): @pytest.mark.usefixtures("async_environment") async def test_deleting_unclosed_async_client_causes_warning(): - client = httpx.AsyncClient(transport=MockTransport(hello_world)) + client = httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) await client.get("http://example.com") with pytest.warns(UserWarning): del client @@ -291,8 +290,8 @@ def mounted(request: httpx.Request) -> httpx.Response: @pytest.mark.usefixtures("async_environment") async def test_mounted_transport(): - transport = MockTransport(unmounted) - mounts = {"custom://": MockTransport(mounted)} + transport = httpx.MockTransport(unmounted) + mounts = {"custom://": httpx.MockTransport(mounted)} async with httpx.AsyncClient(transport=transport, mounts=mounts) as client: response = await client.get("https://www.example.com") diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 677fb9f619..c41afeff87 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -13,7 +13,6 @@ import httpx from httpx import URL, Auth, BasicAuth, DigestAuth, ProtocolError, Request, Response -from tests.utils import MockTransport from ..common import FIXTURES_DIR @@ -155,7 +154,7 @@ async def test_basic_auth() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -171,7 +170,9 @@ async def test_basic_auth_with_stream() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: async with client.stream("GET", url) as response: await response.aread() @@ -184,7 +185,7 @@ async def test_basic_auth_in_url() -> None: url = "https://tomchristie:password123@example.org/" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -197,7 +198,9 @@ async def test_basic_auth_on_session() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: response = await client.get(url) assert response.status_code == 200 @@ -213,7 +216,7 @@ def auth(request: Request) -> Request: request.headers["Authorization"] = "Token 123" return request - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -226,7 +229,7 @@ async def test_netrc_auth() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -241,7 +244,7 @@ async def test_auth_header_has_priority_over_netrc() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, headers={"Authorization": "Override"}) assert response.status_code == 200 @@ -255,7 +258,7 @@ async def test_trust_env_auth() -> None: app = App() async with httpx.AsyncClient( - transport=MockTransport(app), trust_env=False + transport=httpx.MockTransport(app), trust_env=False ) as client: response = await client.get(url) @@ -263,7 +266,7 @@ async def test_trust_env_auth() -> None: assert response.json() == {"auth": None} async with httpx.AsyncClient( - transport=MockTransport(app), trust_env=True + transport=httpx.MockTransport(app), trust_env=True ) as client: response = await client.get(url) @@ -279,7 +282,9 @@ async def test_auth_disable_per_request() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: response = await client.get(url, auth=None) assert response.status_code == 200 @@ -299,7 +304,7 @@ async def test_auth_hidden_header() -> None: auth = ("example-username", "example-password") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert "'authorization': '[secure]'" in str(response.request.headers) @@ -309,7 +314,7 @@ async def test_auth_hidden_header() -> None: async def test_auth_property() -> None: app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: assert client.auth is None client.auth = ("tomchristie", "password123") # type: ignore @@ -327,11 +332,11 @@ async def test_auth_invalid_type() -> None: with pytest.raises(TypeError): client = httpx.AsyncClient( - transport=MockTransport(app), + transport=httpx.MockTransport(app), auth="not a tuple, not a callable", # type: ignore ) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(TypeError): await client.get(auth="not a tuple, not a callable") # type: ignore @@ -345,7 +350,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -359,7 +364,7 @@ def test_digest_auth_returns_no_auth_if_alternate_auth_scheme() -> None: auth_header = "Token ..." app = App(auth_header=auth_header, status_code=401) - client = httpx.Client(transport=MockTransport(app)) + client = httpx.Client(transport=httpx.MockTransport(app)) response = client.get(url, auth=auth) assert response.status_code == 401 @@ -374,7 +379,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -388,7 +393,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -417,7 +422,7 @@ async def test_digest_auth( auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(algorithm=algorithm) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -448,7 +453,7 @@ async def test_digest_auth_no_specified_qop() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -480,7 +485,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -493,7 +498,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(NotImplementedError): await client.get(url, auth=auth) @@ -504,7 +509,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -515,7 +520,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -537,7 +542,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -556,7 +561,7 @@ def test_sync_digest_auth_raises_protocol_error_on_malformed_header( auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header=auth_header, status_code=401) - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): client.get(url, auth=auth) @@ -571,7 +576,7 @@ async def test_async_auth_history() -> None: auth = RepeatAuth(repeat=2) app = App(auth_header="abc") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -597,7 +602,7 @@ def test_sync_auth_history() -> None: auth = RepeatAuth(repeat=2) app = App(auth_header="abc") - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 @@ -623,7 +628,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=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(httpx.StreamConsumed): await client.post(url, data=streaming_body(), auth=auth) @@ -638,7 +643,7 @@ async def test_async_auth_reads_response_body() -> None: auth = ResponseBodyAuth("xyz") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -654,7 +659,7 @@ def test_sync_auth_reads_response_body() -> None: auth = ResponseBodyAuth("xyz") app = App() - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 @@ -672,7 +677,7 @@ async def test_async_auth() -> None: auth = SyncOrAsyncAuth() app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -687,7 +692,7 @@ def test_sync_auth() -> None: auth = SyncOrAsyncAuth() app = App() - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 diff --git a/tests/client/test_client.py b/tests/client/test_client.py index e4bf46b9a4..13bb7f03ad 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -5,7 +5,6 @@ import pytest import httpx -from tests.utils import MockTransport def test_get(server): @@ -292,7 +291,7 @@ def hello_world(request): def test_client_closed_state_using_implicit_open(): - client = httpx.Client(transport=MockTransport(hello_world)) + client = httpx.Client(transport=httpx.MockTransport(hello_world)) assert not client.is_closed client.get("http://example.com") @@ -306,7 +305,7 @@ def test_client_closed_state_using_implicit_open(): def test_client_closed_state_using_with_block(): - with httpx.Client(transport=MockTransport(hello_world)) as client: + with httpx.Client(transport=httpx.MockTransport(hello_world)) as client: assert not client.is_closed client.get("http://example.com") @@ -330,7 +329,9 @@ def test_raw_client_header(): url = "http://example.org/echo_headers" headers = {"Example-Header": "example-value"} - client = httpx.Client(transport=MockTransport(echo_raw_headers), headers=headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_raw_headers), headers=headers + ) response = client.get(url) assert response.status_code == 200 @@ -355,8 +356,8 @@ def mounted(request: httpx.Request) -> httpx.Response: def test_mounted_transport(): - transport = MockTransport(unmounted) - mounts = {"custom://": MockTransport(mounted)} + transport = httpx.MockTransport(unmounted) + mounts = {"custom://": httpx.MockTransport(mounted)} client = httpx.Client(transport=transport, mounts=mounts) @@ -370,7 +371,7 @@ def test_mounted_transport(): def test_all_mounted_transport(): - mounts = {"all://": MockTransport(mounted)} + mounts = {"all://": httpx.MockTransport(mounted)} client = httpx.Client(mounts=mounts) diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py index feb26ac436..fe9125fa06 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookies.py @@ -1,7 +1,6 @@ from http.cookiejar import Cookie, CookieJar import httpx -from tests.utils import MockTransport def get_and_set_cookies(request: httpx.Request) -> httpx.Response: @@ -21,7 +20,7 @@ def test_set_cookie() -> None: url = "http://example.org/echo_cookies" cookies = {"example-name": "example-value"} - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -56,7 +55,7 @@ def test_set_cookie_with_cookiejar() -> None: ) cookies.set_cookie(cookie) - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -91,7 +90,7 @@ def test_setting_client_cookies_to_cookiejar() -> None: ) cookies.set_cookie(cookie) - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) client.cookies = cookies # type: ignore response = client.get(url) @@ -108,7 +107,7 @@ def test_set_cookie_with_cookies_model() -> None: cookies = httpx.Cookies() cookies["example-name"] = "example-value" - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -118,7 +117,7 @@ def test_set_cookie_with_cookies_model() -> None: def test_get_cookie() -> None: url = "http://example.org/set_cookie" - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url) assert response.status_code == 200 @@ -130,7 +129,7 @@ def test_cookie_persistence() -> None: """ Ensure that Client instances persist cookies between requests. """ - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get("http://example.org/echo_cookies") assert response.status_code == 200 diff --git a/tests/client/test_event_hooks.py b/tests/client/test_event_hooks.py index a81f31e1e5..24dc8dd70b 100644 --- a/tests/client/test_event_hooks.py +++ b/tests/client/test_event_hooks.py @@ -1,7 +1,6 @@ import pytest import httpx -from tests.utils import MockTransport def app(request: httpx.Request) -> httpx.Response: @@ -25,7 +24,9 @@ def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: http.get("http://127.0.0.1:8000/", auth=("username", "password")) assert events == [ @@ -53,7 +54,9 @@ def raise_on_4xx_5xx(response): event_hooks = {"response": [raise_on_4xx_5xx]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: try: http.get("http://127.0.0.1:8000/status/400") except httpx.HTTPStatusError as exc: @@ -73,7 +76,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/", auth=("username", "password")) @@ -104,7 +107,7 @@ async def raise_on_4xx_5xx(response): event_hooks = {"response": [raise_on_4xx_5xx]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: try: await http.get("http://127.0.0.1:8000/status/400") @@ -127,7 +130,9 @@ def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: http.get("http://127.0.0.1:8000/redirect", auth=("username", "password")) assert events == [ @@ -166,7 +171,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/redirect", auth=("username", "password")) diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py index 556cd1df14..ba862fd521 100755 --- a/tests/client/test_headers.py +++ b/tests/client/test_headers.py @@ -3,7 +3,6 @@ import pytest import httpx -from tests.utils import MockTransport def echo_headers(request: httpx.Request) -> httpx.Response: @@ -18,7 +17,7 @@ def test_client_header(): url = "http://example.org/echo_headers" headers = {"Example-Header": "example-value"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=headers) + client = httpx.Client(transport=httpx.MockTransport(echo_headers), headers=headers) response = client.get(url) assert response.status_code == 200 @@ -38,7 +37,9 @@ def test_header_merge(): url = "http://example.org/echo_headers" client_headers = {"User-Agent": "python-myclient/0.2.1"} request_headers = {"X-Auth-Token": "FooBarBazToken"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_headers), headers=client_headers + ) response = client.get(url, headers=request_headers) assert response.status_code == 200 @@ -58,7 +59,9 @@ def test_header_merge_conflicting_headers(): url = "http://example.org/echo_headers" client_headers = {"X-Auth-Token": "FooBar"} request_headers = {"X-Auth-Token": "BazToken"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_headers), headers=client_headers + ) response = client.get(url, headers=request_headers) assert response.status_code == 200 @@ -76,7 +79,7 @@ def test_header_merge_conflicting_headers(): def test_header_update(): url = "http://example.org/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) first_response = client.get(url) client.headers.update( {"User-Agent": "python-myclient/0.2.1", "Another-Header": "AThing"} @@ -113,7 +116,7 @@ def test_remove_default_header(): """ url = "http://example.org/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) del client.headers["User-Agent"] response = client.get(url) @@ -143,7 +146,7 @@ def test_host_with_auth_and_port_in_url(): """ url = "http://username:password@example.org:80/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) response = client.get(url) assert response.status_code == 200 @@ -166,7 +169,7 @@ def test_host_with_non_default_port_in_url(): """ url = "http://username:password@example.org:123/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) response = client.get(url) assert response.status_code == 200 diff --git a/tests/client/test_queryparams.py b/tests/client/test_queryparams.py index 6d3a9d5b5d..e5acb0ba20 100644 --- a/tests/client/test_queryparams.py +++ b/tests/client/test_queryparams.py @@ -1,5 +1,4 @@ import httpx -from tests.utils import MockTransport def hello_world(request: httpx.Request) -> httpx.Response: @@ -28,7 +27,7 @@ def test_client_queryparams_echo(): client_queryparams = "first=str" request_queryparams = {"second": "dict"} client = httpx.Client( - transport=MockTransport(hello_world), params=client_queryparams + transport=httpx.MockTransport(hello_world), params=client_queryparams ) response = client.get(url, params=request_queryparams) diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index 99610c7715..84d371e9fa 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -2,7 +2,6 @@ import pytest import httpx -from tests.utils import MockTransport def redirects(request: httpx.Request) -> httpx.Response: @@ -116,7 +115,7 @@ def redirects(request: httpx.Request) -> httpx.Response: def test_redirect_301(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.post("https://example.org/redirect_301") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -124,7 +123,7 @@ def test_redirect_301(): def test_redirect_302(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.post("https://example.org/redirect_302") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -132,7 +131,7 @@ def test_redirect_302(): def test_redirect_303(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/redirect_303") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -140,7 +139,7 @@ def test_redirect_303(): def test_next_request(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) request = client.build_request("POST", "https://example.org/redirect_303") response = client.send(request, allow_redirects=False) assert response.status_code == httpx.codes.SEE_OTHER @@ -155,7 +154,7 @@ def test_next_request(): @pytest.mark.usefixtures("async_environment") async def test_async_next_request(): - async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(redirects)) as client: request = client.build_request("POST", "https://example.org/redirect_303") response = await client.send(request, allow_redirects=False) assert response.status_code == httpx.codes.SEE_OTHER @@ -172,7 +171,7 @@ def test_head_redirect(): """ Contrary to Requests, redirects remain enabled by default for HEAD requests. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.head("https://example.org/redirect_302") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -182,7 +181,7 @@ def test_head_redirect(): def test_relative_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/relative_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -191,7 +190,7 @@ def test_relative_redirect(): def test_malformed_redirect(): # https://github.com/encode/httpx/issues/771 - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("http://example.org/malformed_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org:443/" @@ -199,13 +198,13 @@ def test_malformed_redirect(): def test_invalid_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.RemoteProtocolError): client.get("http://example.org/invalid_redirect") def test_no_scheme_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/no_scheme_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -213,7 +212,7 @@ def test_no_scheme_redirect(): def test_fragment_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/relative_redirect#fragment") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/#fragment" @@ -221,7 +220,7 @@ def test_fragment_redirect(): def test_multiple_redirects(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/multiple_redirects?count=20") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/multiple_redirects" @@ -234,25 +233,25 @@ def test_multiple_redirects(): @pytest.mark.usefixtures("async_environment") async def test_async_too_many_redirects(): - async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(redirects)) as client: with pytest.raises(httpx.TooManyRedirects): await client.get("https://example.org/multiple_redirects?count=21") def test_sync_too_many_redirects(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.TooManyRedirects): client.get("https://example.org/multiple_redirects?count=21") def test_redirect_loop(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.TooManyRedirects): client.get("https://example.org/redirect_loop") def test_cross_domain_redirect_with_auth_header(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_domain" headers = {"Authorization": "abc"} response = client.get(url, headers=headers) @@ -261,7 +260,7 @@ def test_cross_domain_redirect_with_auth_header(): def test_cross_domain_redirect_with_auth(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_domain" response = client.get(url, auth=("user", "pass")) assert response.url == "https://example.org/cross_domain_target" @@ -269,7 +268,7 @@ def test_cross_domain_redirect_with_auth(): def test_same_domain_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/cross_domain" headers = {"Authorization": "abc"} response = client.get(url, headers=headers) @@ -281,7 +280,7 @@ def test_body_redirect(): """ A 308 redirect should preserve the request body. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_body" content = b"Example request body" response = client.post(url, content=content) @@ -294,7 +293,7 @@ def test_no_body_redirect(): """ A 303 redirect should remove the request body. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_no_body" content = b"Example request body" response = client.post(url, content=content) @@ -304,7 +303,7 @@ def test_no_body_redirect(): def test_can_stream_if_no_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_301" with client.stream("GET", url, allow_redirects=False) as response: assert not response.is_closed @@ -313,7 +312,7 @@ def test_can_stream_if_no_redirect(): def test_cannot_redirect_streaming_body(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_body" def streaming_body(): @@ -324,7 +323,7 @@ def streaming_body(): def test_cross_subdomain_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_subdomain" response = client.get(url) assert response.url == "https://www.example.org/cross_subdomain" @@ -364,7 +363,7 @@ def cookie_sessions(request: httpx.Request) -> httpx.Response: def test_redirect_cookie_behavior(): - client = httpx.Client(transport=MockTransport(cookie_sessions)) + client = httpx.Client(transport=httpx.MockTransport(cookie_sessions)) # The client is not logged in. response = client.get("https://example.com/") @@ -393,7 +392,7 @@ def test_redirect_cookie_behavior(): def test_redirect_custom_scheme(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.UnsupportedProtocol) as e: client.post("https://example.org/redirect_custom_scheme") assert str(e.value) == "Scheme 'market' not supported." diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 8852c9f905..94813932a8 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -9,7 +9,6 @@ import httpx from httpx._content import encode_request from httpx._utils import format_form_param -from tests.utils import MockTransport def echo_request_content(request: httpx.Request) -> httpx.Response: @@ -18,7 +17,7 @@ def echo_request_content(request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc"))) def test_multipart(value, output): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) # Test with a single-value 'data' argument, and a plain file 'files' argument. data = {"text": value} @@ -44,7 +43,7 @@ def test_multipart(value, output): @pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) def test_multipart_invalid_key(key): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) data = {key: "abc"} files = {"file": io.BytesIO(b"")} @@ -60,7 +59,7 @@ def test_multipart_invalid_key(key): @pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"})) def test_multipart_invalid_value(value): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) data = {"text": value} files = {"file": io.BytesIO(b"")} @@ -70,7 +69,7 @@ def test_multipart_invalid_value(value): def test_multipart_file_tuple(): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) # Test with a list of values 'data' argument, # and a tuple style 'files' argument. diff --git a/tests/utils.py b/tests/utils.py index 75157dee7e..e2636a535c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,7 @@ import contextlib import logging import os -from typing import Callable, List, Optional, Tuple -import httpcore - -import httpx from httpx import _utils @@ -22,54 +18,3 @@ def override_log_level(log_level: str): finally: # Reset the logger so we don't have verbose output in all unit tests logging.getLogger("httpx").handlers = [] - - -class MockTransport(httpcore.SyncHTTPTransport, httpcore.AsyncHTTPTransport): - def __init__(self, handler: Callable) -> None: - self.handler = handler - - def request( - self, - method: bytes, - url: Tuple[bytes, bytes, Optional[int], bytes], - headers: List[Tuple[bytes, bytes]] = None, - stream: httpcore.SyncByteStream = None, - ext: dict = None, - ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.SyncByteStream, dict]: - request = httpx.Request( - method=method, - url=url, - headers=headers, - stream=stream, - ) - request.read() - response = self.handler(request) - return ( - response.status_code, - response.headers.raw, - response.stream, - response.ext, - ) - - async def arequest( - self, - method: bytes, - url: Tuple[bytes, bytes, Optional[int], bytes], - headers: List[Tuple[bytes, bytes]] = None, - stream: httpcore.AsyncByteStream = None, - ext: dict = None, - ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: - request = httpx.Request( - method=method, - url=url, - headers=headers, - stream=stream, - ) - await request.aread() - response = self.handler(request) - return ( - response.status_code, - response.headers.raw, - response.stream, - response.ext, - ) From 181639322e2f68c00393435ef9e2359ff2691331 Mon Sep 17 00:00:00 2001 From: nkitsaini <74284503+nkitsaini@users.noreply.github.com> Date: Thu, 7 Jan 2021 12:27:25 +0530 Subject: [PATCH 09/10] Update README to reflect new estimate for v1.0 release (#1445) Co-authored-by: Ankit Co-authored-by: Florimond Manca --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d641ef849..60ca24540f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ HTTPX is a fully featured HTTP client for Python 3, which provides sync and asyn **Note**: _HTTPX should be considered in beta. We believe we've got the public API to a stable point now, but would strongly recommend pinning your dependencies to the `0.16.*` -release, so that you're able to properly review [API changes between package updates](https://github.com/encode/httpx/blob/master/CHANGELOG.md). A 1.0 release is expected to be issued sometime in late 2020._ +release, so that you're able to properly review [API changes between package updates](https://github.com/encode/httpx/blob/master/CHANGELOG.md). A 1.0 release is expected to be issued sometime in 2021._ --- diff --git a/docs/index.md b/docs/index.md index fa005e7ce9..abdc9c7d33 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,7 +29,7 @@ HTTPX is a fully featured HTTP client for Python 3, which provides sync and asyn We believe we've got the public API to a stable point now, but would strongly recommend pinning your dependencies to the `0.16.*` release, so that you're able to properly review [API changes between package updates](https://github.com/encode/httpx/blob/master/CHANGELOG.md). - A 1.0 release is expected to be issued sometime in late 2020. + A 1.0 release is expected to be issued sometime in 2021. --- From 89fb0cbc69ea07b123dd7b36dc1ed9151c5d398f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Jan 2021 10:23:56 +0000 Subject: [PATCH 10/10] Add `HTTPTransport` and `AsyncHTTPTransport` (#1399) * Add keepalive_expiry to Limits config * keepalive_expiry should be optional. In line with httpcore. * HTTPTransport and AsyncHTTPTransport * Update docs for httpx.HTTPTransport() * Update type hints * Fix docs typo * Additional mount example * Tweak context manager methods * Add 'httpx.HTTPTransport(proxy=...)' * Use explicit keyword arguments throughout httpx.HTTPTransport Co-authored-by: Florimond Manca --- docs/advanced.md | 56 +++++------ docs/async.md | 13 +++ httpx/__init__.py | 3 + httpx/_client.py | 54 ++++------- httpx/_transports/default.py | 174 +++++++++++++++++++++++++++++++++++ tests/client/test_proxies.py | 12 ++- 6 files changed, 242 insertions(+), 70 deletions(-) create mode 100644 httpx/_transports/default.py diff --git a/docs/advanced.md b/docs/advanced.md index 5463250f11..61bf4c1938 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -971,46 +971,36 @@ sending of the requests. ### Usage For some advanced configuration you might need to instantiate a transport -class directly, and pass it to the client instance. The `httpcore` package -provides a `local_address` configuration that is only available via this -low-level API. +class directly, and pass it to the client instance. One example is the +`local_address` configuration which is only available via this low-level API. ```pycon ->>> import httpx, httpcore ->>> ssl_context = httpx.create_ssl_context() ->>> transport = httpcore.SyncConnectionPool( -... ssl_context=ssl_context, -... max_connections=100, -... max_keepalive_connections=20, -... keepalive_expiry=5.0, -... local_address="0.0.0.0" -... ) # Use the standard HTTPX defaults, but with an IPv4 only 'local_address'. +>>> import httpx +>>> transport = httpx.HTTPTransport(local_address="0.0.0.0") >>> client = httpx.Client(transport=transport) ``` -Similarly, `httpcore` provides a `uds` option for connecting via a Unix Domain Socket that is only available via this low-level API: +Connection retries are also available via this interface. -```python ->>> import httpx, httpcore ->>> ssl_context = httpx.create_ssl_context() ->>> transport = httpcore.SyncConnectionPool( -... ssl_context=ssl_context, -... max_connections=100, -... max_keepalive_connections=20, -... keepalive_expiry=5.0, -... uds="/var/run/docker.sock", -... ) # Connect to the Docker API via a Unix Socket. +```pycon +>>> import httpx +>>> transport = httpx.HTTPTransport(retries=1) +>>> client = httpx.Client(transport=transport) +``` + +Similarly, instantiating a transport directly provides a `uds` option for +connecting via a Unix Domain Socket that is only available via this low-level API: + +```pycon +>>> import httpx +>>> # Connect to the Docker API via a Unix Socket. +>>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock") >>> client = httpx.Client(transport=transport) >>> response = client.get("http://docker/info") >>> response.json() {"ID": "...", "Containers": 4, "Images": 74, ...} ``` -Unlike the `httpx.Client()`, the lower-level `httpcore` transport instances -do not include any default values for configuring aspects such as the -connection pooling details, so you'll need to provide more explicit -configuration when using this API. - ### urllib3 transport This [public gist](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e) provides a transport that uses the excellent [`urllib3` library](https://urllib3.readthedocs.io/en/latest/), and can be used with the sync `Client`... @@ -1121,6 +1111,16 @@ client = httpx.Client(mounts=mounts) A couple of other sketches of how you might take advantage of mounted transports... +Disabling HTTP/2 on a single given domain... + +```python +mounts = { + "all://": httpx.HTTPTransport(http2=True), + "all://*example.org": httpx.HTTPTransport() +} +client = httpx.Client(mounts=mounts) +``` + Mocking requests to a given domain: ```python diff --git a/docs/async.md b/docs/async.md index 69a1fb419f..8ddee956ae 100644 --- a/docs/async.md +++ b/docs/async.md @@ -112,6 +112,19 @@ async def upload_bytes(): await client.post(url, data=upload_bytes()) ``` +### Explicit transport instances + +When instantiating a transport instance directly, you need to use `httpx.AsyncHTTPTransport`. + +For instance: + +```pycon +>>> import httpx +>>> transport = httpx.AsyncHTTPTransport(retries=1) +>>> async with httpx.AsyncClient(transport=transport) as client: +>>> ... +``` + ## Supported async environments HTTPX supports either `asyncio` or `trio` as an async environment. diff --git a/httpx/__init__.py b/httpx/__init__.py index 8a6d9b32ce..96d9e0c2f8 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -36,6 +36,7 @@ from ._models import URL, Cookies, Headers, QueryParams, Request, Response from ._status_codes import StatusCode, codes from ._transports.asgi import ASGITransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport from ._transports.mock import MockTransport from ._transports.wsgi import WSGITransport @@ -45,6 +46,7 @@ "__version__", "ASGITransport", "AsyncClient", + "AsyncHTTPTransport", "Auth", "BasicAuth", "Client", @@ -63,6 +65,7 @@ "Headers", "HTTPError", "HTTPStatusError", + "HTTPTransport", "InvalidURL", "Limits", "LocalProtocolError", diff --git a/httpx/_client.py b/httpx/_client.py index 4f457c79dc..b300cccb1f 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -17,7 +17,6 @@ Proxy, Timeout, UnsetType, - create_ssl_context, ) from ._decoders import SUPPORTED_DECODERS from ._exceptions import ( @@ -30,6 +29,7 @@ from ._models import URL, Cookies, Headers, QueryParams, Request, Response from ._status_codes import codes from ._transports.asgi import ASGITransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport from ._transports.wsgi import WSGITransport from ._types import ( AuthTypes, @@ -649,14 +649,8 @@ def _init_transport( if app is not None: return WSGITransport(app=app) - ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) - - return httpcore.SyncConnectionPool( - ssl_context=ssl_context, - max_connections=limits.max_connections, - max_keepalive_connections=limits.max_keepalive_connections, - keepalive_expiry=limits.keepalive_expiry, - http2=http2, + return HTTPTransport( + verify=verify, cert=cert, http2=http2, limits=limits, trust_env=trust_env ) def _init_proxy_transport( @@ -668,17 +662,13 @@ def _init_proxy_transport( limits: Limits = DEFAULT_LIMITS, trust_env: bool = True, ) -> httpcore.SyncHTTPTransport: - ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) - - return httpcore.SyncHTTPProxy( - proxy_url=proxy.url.raw, - proxy_headers=proxy.headers.raw, - proxy_mode=proxy.mode, - ssl_context=ssl_context, - max_connections=limits.max_connections, - max_keepalive_connections=limits.max_keepalive_connections, - keepalive_expiry=limits.keepalive_expiry, + return HTTPTransport( + verify=verify, + cert=cert, http2=http2, + limits=limits, + trust_env=trust_env, + proxy=proxy, ) def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport: @@ -1292,14 +1282,8 @@ def _init_transport( if app is not None: return ASGITransport(app=app) - ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) - - return httpcore.AsyncConnectionPool( - ssl_context=ssl_context, - max_connections=limits.max_connections, - max_keepalive_connections=limits.max_keepalive_connections, - keepalive_expiry=limits.keepalive_expiry, - http2=http2, + return AsyncHTTPTransport( + verify=verify, cert=cert, http2=http2, limits=limits, trust_env=trust_env ) def _init_proxy_transport( @@ -1311,17 +1295,13 @@ def _init_proxy_transport( limits: Limits = DEFAULT_LIMITS, trust_env: bool = True, ) -> httpcore.AsyncHTTPTransport: - ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) - - return httpcore.AsyncHTTPProxy( - proxy_url=proxy.url.raw, - proxy_headers=proxy.headers.raw, - proxy_mode=proxy.mode, - ssl_context=ssl_context, - max_connections=limits.max_connections, - max_keepalive_connections=limits.max_keepalive_connections, - keepalive_expiry=limits.keepalive_expiry, + return AsyncHTTPTransport( + verify=verify, + cert=cert, http2=http2, + limits=limits, + trust_env=trust_env, + proxy=proxy, ) def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport: diff --git a/httpx/_transports/default.py b/httpx/_transports/default.py new file mode 100644 index 0000000000..84aeb26be8 --- /dev/null +++ b/httpx/_transports/default.py @@ -0,0 +1,174 @@ +""" +Custom transports, with nicely configured defaults. + +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... + +# Disable HTTP/2 on a single specfic domain. +mounts = { + "all://": httpx.HTTPTransport(http2=True), + "all://*example.org": httpx.HTTPTransport() +} + +# Using advanced httpcore configuration, with connection retries. +transport = httpx.HTTPTransport(retries=1) +client = httpx.Client(transport=transport) + +# Using advanced httpcore configuration, with unix domain sockets. +transport = httpx.HTTPTransport(uds="socket.uds") +client = httpx.Client(transport=transport) +""" +import typing +from types import TracebackType + +import httpcore + +from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context +from .._types import CertTypes, VerifyTypes + +T = typing.TypeVar("T", bound="HTTPTransport") +A = typing.TypeVar("A", bound="AsyncHTTPTransport") +Headers = typing.List[typing.Tuple[bytes, bytes]] +URL = typing.Tuple[bytes, bytes, typing.Optional[int], bytes] + + +class HTTPTransport(httpcore.SyncHTTPTransport): + def __init__( + self, + verify: VerifyTypes = True, + cert: CertTypes = None, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + proxy: Proxy = None, + 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( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + backend=backend, + ) + else: + self._pool = httpcore.SyncHTTPProxy( + proxy_url=proxy.url.raw, + proxy_headers=proxy.headers.raw, + proxy_mode=proxy.mode, + 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. + self._pool.__enter__() + return self + + def __exit__( + self, + exc_type: typing.Type[BaseException] = None, + exc_value: BaseException = None, + traceback: TracebackType = None, + ) -> None: + self._pool.__exit__(exc_type, exc_value, traceback) + + def request( + self, + method: bytes, + url: URL, + headers: Headers = None, + stream: httpcore.SyncByteStream = None, + ext: dict = None, + ) -> typing.Tuple[int, Headers, httpcore.SyncByteStream, dict]: + return self._pool.request(method, url, headers=headers, stream=stream, ext=ext) + + def close(self) -> None: + self._pool.close() + + +class AsyncHTTPTransport(httpcore.AsyncHTTPTransport): + def __init__( + self, + verify: VerifyTypes = True, + cert: CertTypes = None, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + trust_env: bool = True, + proxy: Proxy = None, + 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) + + if proxy is None: + self._pool = httpcore.AsyncConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + backend=backend, + ) + else: + self._pool = httpcore.AsyncHTTPProxy( + proxy_url=proxy.url.raw, + proxy_headers=proxy.headers.raw, + proxy_mode=proxy.mode, + 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. + await self._pool.__aenter__() + return self + + async def __aexit__( + self, + exc_type: typing.Type[BaseException] = None, + exc_value: BaseException = None, + traceback: TracebackType = None, + ) -> None: + await self._pool.__aexit__(exc_type, exc_value, traceback) + + async def arequest( + self, + method: bytes, + url: URL, + headers: Headers = None, + stream: httpcore.AsyncByteStream = None, + ext: dict = None, + ) -> typing.Tuple[int, Headers, httpcore.AsyncByteStream, dict]: + return await self._pool.arequest( + method, url, headers=headers, stream=stream, ext=ext + ) + + async def aclose(self) -> None: + await self._pool.aclose() diff --git a/tests/client/test_proxies.py b/tests/client/test_proxies.py index a2d21e9429..b491213dae 100644 --- a/tests/client/test_proxies.py +++ b/tests/client/test_proxies.py @@ -43,8 +43,9 @@ def test_proxies_parameter(proxies, expected_proxies): pattern = URLPattern(proxy_key) assert pattern in client._mounts proxy = client._mounts[pattern] - assert isinstance(proxy, httpcore.SyncHTTPProxy) - assert proxy.proxy_origin == url_to_origin(url) + assert isinstance(proxy, httpx.HTTPTransport) + assert isinstance(proxy._pool, httpcore.SyncHTTPProxy) + assert proxy._pool.proxy_origin == url_to_origin(url) assert len(expected_proxies) == len(client._mounts) @@ -116,8 +117,9 @@ def test_transport_for_request(url, proxies, expected): if expected is None: assert transport is client._transport else: - assert isinstance(transport, httpcore.SyncHTTPProxy) - assert transport.proxy_origin == url_to_origin(expected) + assert isinstance(transport, httpx.HTTPTransport) + assert isinstance(transport._pool, httpcore.SyncHTTPProxy) + assert transport._pool.proxy_origin == url_to_origin(expected) @pytest.mark.asyncio @@ -250,7 +252,7 @@ def test_proxies_environ(monkeypatch, client_class, url, env, expected): if expected is None: assert transport == client._transport else: - assert transport.proxy_origin == url_to_origin(expected) + assert transport._pool.proxy_origin == url_to_origin(expected) @pytest.mark.parametrize(