]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Deprecate `app=...` in favor of explicit `WSGITransport`/`ASGITransport`. (#3050)
authorTom Christie <tom@tomchristie.com>
Fri, 2 Feb 2024 13:29:41 +0000 (13:29 +0000)
committerGitHub <noreply@github.com>
Fri, 2 Feb 2024 13:29:41 +0000 (13:29 +0000)
* Deprecate app=... in favour of explicit WSGITransport/ASGITransport

* Linting

* Linting

* Update WSGITransport and ASGITransport docs

* Deprecate app

* Drop deprecation tests

* Add CHANGELOG

* Deprecate 'app=...' shortcut, rather than removing it.

* Update CHANGELOG

* Fix test_asgi.test_deprecated_shortcut

CHANGELOG.md
docs/advanced/transports.md
docs/async.md
httpx/_client.py
tests/test_asgi.py
tests/test_wsgi.py

index 47ac88c8348d72dc4f435ea8f17755d21a8ee6e6..7950a5f320410c8c1f58fb376feaf6f3a969a7c3 100644 (file)
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## Unreleased
 
+### Deprecated
+
+* The `app=...` shortcut has been deprecated. Use the explicit style of `transport=httpx.WSGITransport()` or `transport=httpx.ASGITransport()` instead.
+
 ### Fixed
 
 * Respect the `http1` argument while configuring proxy transports. (#3023)
index 2f3e00690d9e9c92ea4b55263741048fc4f61d91..7e0e21c6f9644caf0da8d1a4291463b4d3422af9 100644 (file)
@@ -42,7 +42,9 @@ You can configure an `httpx` client to call directly into a Python web applicati
 This is particularly useful for two main use-cases:
 
 * Using `httpx` as a client inside test cases.
-* Mocking out external services during tests or in dev/staging environments.
+* Mocking out external services during tests or in dev or staging environments.
+
+### Example
 
 Here's an example of integrating against a Flask application:
 
@@ -57,12 +59,15 @@ app = Flask(__name__)
 def hello():
     return "Hello World!"
 
-with httpx.Client(app=app, base_url="http://testserver") as client:
+transport = httpx.WSGITransport(app=app)
+with httpx.Client(transport=transport, base_url="http://testserver") as client:
     r = client.get("/")
     assert r.status_code == 200
     assert r.text == "Hello World!"
 ```
 
+### Configuration
+
 For some more complex cases you might need to customize the WSGI transport. This allows you to:
 
 * Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
@@ -78,6 +83,69 @@ with httpx.Client(transport=transport, base_url="http://testserver") as client:
     ...
 ```
 
+## ASGITransport
+
+You can configure an `httpx` client to call directly into an async Python web application using the ASGI protocol.
+
+This is particularly useful for two main use-cases:
+
+* Using `httpx` as a client inside test cases.
+* Mocking out external services during tests or in dev or staging environments.
+
+### Example
+
+Let's take this Starlette application as an example:
+
+```python
+from starlette.applications import Starlette
+from starlette.responses import HTMLResponse
+from starlette.routing import Route
+
+
+async def hello(request):
+    return HTMLResponse("Hello World!")
+
+
+app = Starlette(routes=[Route("/", hello)])
+```
+
+We can make requests directly against the application, like so:
+
+```python
+transport = httpx.ASGITransport(app=app)
+
+async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
+    r = await client.get("/")
+    assert r.status_code == 200
+    assert r.text == "Hello World!"
+```
+
+### Configuration
+
+For some more complex cases you might need to customise the ASGI transport. This allows you to:
+
+* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
+* Mount the ASGI application at a subpath by setting `root_path`.
+* Use a given client address for requests by setting `client`.
+
+For example:
+
+```python
+# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
+# on port 123.
+transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
+async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
+    ...
+```
+
+See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
+
+### ASGI startup and shutdown
+
+It is not in the scope of HTTPX to trigger ASGI lifespan events of your app.
+
+However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
+
 ## Custom transports
 
 A transport instance must implement the low-level Transport API, which deals
index d54a353d62447835a1b3fc2778e2ed9468abba20..089d7831915c0b465d0da7de21523e1723cacfa3 100644 (file)
@@ -191,54 +191,4 @@ anyio.run(main, backend='trio')
 
 ## Calling into Python Web Apps
 
-Just as `httpx.Client` allows you to call directly into WSGI web applications,
-the `httpx.AsyncClient` class allows you to call directly into ASGI web applications.
-
-Let's take this Starlette application as an example:
-
-```python
-from starlette.applications import Starlette
-from starlette.responses import HTMLResponse
-from starlette.routing import Route
-
-
-async def hello(request):
-    return HTMLResponse("Hello World!")
-
-
-app = Starlette(routes=[Route("/", hello)])
-```
-
-We can make requests directly against the application, like so:
-
-```pycon
->>> import httpx
->>> async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
-...     r = await client.get("/")
-...     assert r.status_code == 200
-...     assert r.text == "Hello World!"
-```
-
-For some more complex cases you might need to customise the ASGI transport. This allows you to:
-
-* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
-* Mount the ASGI application at a subpath by setting `root_path`.
-* Use a given client address for requests by setting `client`.
-
-For example:
-
-```python
-# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
-# on port 123.
-transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
-async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
-    ...
-```
-
-See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
-
-## Startup/shutdown of ASGI apps
-
-It is not in the scope of HTTPX to trigger lifespan events of your app.
-
-However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
+For details on calling directly into ASGI applications, see [the `ASGITransport` docs](../advanced/transports#asgitransport).
\ No newline at end of file
index 1f2145d12ed902f33510af679fb1b385e9626a50..e2c6702e0c7ecb0d9e12ae41d19a156daaed6981 100644 (file)
@@ -672,6 +672,13 @@ class Client(BaseClient):
             if proxy:
                 raise RuntimeError("Use either `proxy` or 'proxies', not both.")
 
+        if app:
+            message = (
+                "The 'app' shortcut is now deprecated."
+                " Use the explicit style 'transport=WSGITransport(app=...)' instead."
+            )
+            warnings.warn(message, DeprecationWarning)
+
         allow_env_proxies = trust_env and app is None and transport is None
         proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
 
@@ -1411,7 +1418,14 @@ class AsyncClient(BaseClient):
             if proxy:
                 raise RuntimeError("Use either `proxy` or 'proxies', not both.")
 
-        allow_env_proxies = trust_env and app is None and transport is None
+        if app:
+            message = (
+                "The 'app' shortcut is now deprecated."
+                " Use the explicit style 'transport=ASGITransport(app=...)' instead."
+            )
+            warnings.warn(message, DeprecationWarning)
+
+        allow_env_proxies = trust_env and transport is None
         proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
 
         self._transport = self._init_transport(
index 29715060979c0da1426aa709441f998bb603d09f..ccc55266787e9b608293f8a4b913769e5947f9ea 100644 (file)
@@ -92,7 +92,8 @@ async def test_asgi_transport_no_body():
 
 @pytest.mark.anyio
 async def test_asgi():
-    async with httpx.AsyncClient(app=hello_world) as client:
+    transport = httpx.ASGITransport(app=hello_world)
+    async with httpx.AsyncClient(transport=transport) as client:
         response = await client.get("http://www.example.org/")
 
     assert response.status_code == 200
@@ -101,7 +102,8 @@ async def test_asgi():
 
 @pytest.mark.anyio
 async def test_asgi_urlencoded_path():
-    async with httpx.AsyncClient(app=echo_path) as client:
+    transport = httpx.ASGITransport(app=echo_path)
+    async with httpx.AsyncClient(transport=transport) as client:
         url = httpx.URL("http://www.example.org/").copy_with(path="/user@example.org")
         response = await client.get(url)
 
@@ -111,7 +113,8 @@ async def test_asgi_urlencoded_path():
 
 @pytest.mark.anyio
 async def test_asgi_raw_path():
-    async with httpx.AsyncClient(app=echo_raw_path) as client:
+    transport = httpx.ASGITransport(app=echo_raw_path)
+    async with httpx.AsyncClient(transport=transport) as client:
         url = httpx.URL("http://www.example.org/").copy_with(path="/user@example.org")
         response = await client.get(url)
 
@@ -124,7 +127,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
     """
     See https://github.com/encode/httpx/issues/2810
     """
-    async with httpx.AsyncClient(app=echo_raw_path) as client:
+    transport = httpx.ASGITransport(app=echo_raw_path)
+    async with httpx.AsyncClient(transport=transport) as client:
         url = httpx.URL("http://www.example.org/path?query")
         response = await client.get(url)
 
@@ -134,7 +138,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
 
 @pytest.mark.anyio
 async def test_asgi_upload():
-    async with httpx.AsyncClient(app=echo_body) as client:
+    transport = httpx.ASGITransport(app=echo_body)
+    async with httpx.AsyncClient(transport=transport) as client:
         response = await client.post("http://www.example.org/", content=b"example")
 
     assert response.status_code == 200
@@ -143,7 +148,8 @@ async def test_asgi_upload():
 
 @pytest.mark.anyio
 async def test_asgi_headers():
-    async with httpx.AsyncClient(app=echo_headers) as client:
+    transport = httpx.ASGITransport(app=echo_headers)
+    async with httpx.AsyncClient(transport=transport) as client:
         response = await client.get("http://www.example.org/")
 
     assert response.status_code == 200
@@ -160,14 +166,16 @@ async def test_asgi_headers():
 
 @pytest.mark.anyio
 async def test_asgi_exc():
-    async with httpx.AsyncClient(app=raise_exc) as client:
+    transport = httpx.ASGITransport(app=raise_exc)
+    async with httpx.AsyncClient(transport=transport) as client:
         with pytest.raises(RuntimeError):
             await client.get("http://www.example.org/")
 
 
 @pytest.mark.anyio
 async def test_asgi_exc_after_response():
-    async with httpx.AsyncClient(app=raise_exc_after_response) as client:
+    transport = httpx.ASGITransport(app=raise_exc_after_response)
+    async with httpx.AsyncClient(transport=transport) as client:
         with pytest.raises(RuntimeError):
             await client.get("http://www.example.org/")
 
@@ -199,7 +207,8 @@ async def test_asgi_disconnect_after_response_complete():
         message = await receive()
         disconnect = message.get("type") == "http.disconnect"
 
-    async with httpx.AsyncClient(app=read_body) as client:
+    transport = httpx.ASGITransport(app=read_body)
+    async with httpx.AsyncClient(transport=transport) as client:
         response = await client.post("http://www.example.org/", content=b"example")
 
     assert response.status_code == 200
@@ -213,3 +222,13 @@ async def test_asgi_exc_no_raise():
         response = await client.get("http://www.example.org/")
 
         assert response.status_code == 500
+
+
+@pytest.mark.anyio
+async def test_deprecated_shortcut():
+    """
+    The `app=...` shortcut is now deprecated.
+    Use the explicit transport style instead.
+    """
+    with pytest.warns(DeprecationWarning):
+        httpx.AsyncClient(app=hello_world)
index 3565a48c925a320c74a6398283298f7df238dbff..0134bee854f44b2cfc0d41e903963393d6b29356 100644 (file)
@@ -92,41 +92,47 @@ def log_to_wsgi_log_buffer(environ, start_response):
 
 
 def test_wsgi():
-    client = httpx.Client(app=application_factory([b"Hello, World!"]))
+    transport = httpx.WSGITransport(app=application_factory([b"Hello, World!"]))
+    client = httpx.Client(transport=transport)
     response = client.get("http://www.example.org/")
     assert response.status_code == 200
     assert response.text == "Hello, World!"
 
 
 def test_wsgi_upload():
-    client = httpx.Client(app=echo_body)
+    transport = httpx.WSGITransport(app=echo_body)
+    client = httpx.Client(transport=transport)
     response = client.post("http://www.example.org/", content=b"example")
     assert response.status_code == 200
     assert response.text == "example"
 
 
 def test_wsgi_upload_with_response_stream():
-    client = httpx.Client(app=echo_body_with_response_stream)
+    transport = httpx.WSGITransport(app=echo_body_with_response_stream)
+    client = httpx.Client(transport=transport)
     response = client.post("http://www.example.org/", content=b"example")
     assert response.status_code == 200
     assert response.text == "example"
 
 
 def test_wsgi_exc():
-    client = httpx.Client(app=raise_exc)
+    transport = httpx.WSGITransport(app=raise_exc)
+    client = httpx.Client(transport=transport)
     with pytest.raises(ValueError):
         client.get("http://www.example.org/")
 
 
 def test_wsgi_http_error():
-    client = httpx.Client(app=partial(raise_exc, exc=RuntimeError))
+    transport = httpx.WSGITransport(app=partial(raise_exc, exc=RuntimeError))
+    client = httpx.Client(transport=transport)
     with pytest.raises(RuntimeError):
         client.get("http://www.example.org/")
 
 
 def test_wsgi_generator():
     output = [b"", b"", b"Some content", b" and more content"]
-    client = httpx.Client(app=application_factory(output))
+    transport = httpx.WSGITransport(app=application_factory(output))
+    client = httpx.Client(transport=transport)
     response = client.get("http://www.example.org/")
     assert response.status_code == 200
     assert response.text == "Some content and more content"
@@ -134,7 +140,8 @@ def test_wsgi_generator():
 
 def test_wsgi_generator_empty():
     output = [b"", b"", b"", b""]
-    client = httpx.Client(app=application_factory(output))
+    transport = httpx.WSGITransport(app=application_factory(output))
+    client = httpx.Client(transport=transport)
     response = client.get("http://www.example.org/")
     assert response.status_code == 200
     assert response.text == ""
@@ -170,7 +177,8 @@ def test_wsgi_server_port(url: str, expected_server_port: str) -> None:
         server_port = environ["SERVER_PORT"]
         return hello_world_app(environ, start_response)
 
-    client = httpx.Client(app=app)
+    transport = httpx.WSGITransport(app=app)
+    client = httpx.Client(transport=transport)
     response = client.get(url)
     assert response.status_code == 200
     assert response.text == "Hello, World!"
@@ -186,9 +194,19 @@ def test_wsgi_server_protocol():
         start_response("200 OK", [("Content-Type", "text/plain")])
         return [b"success"]
 
-    with httpx.Client(app=app, base_url="http://testserver") as client:
+    transport = httpx.WSGITransport(app=app)
+    with httpx.Client(transport=transport, base_url="http://testserver") as client:
         response = client.get("/")
 
     assert response.status_code == 200
     assert response.text == "success"
     assert server_protocol == "HTTP/1.1"
+
+
+def test_deprecated_shortcut():
+    """
+    The `app=...` shortcut is now deprecated.
+    Use the explicit transport style instead.
+    """
+    with pytest.warns(DeprecationWarning):
+        httpx.Client(app=application_factory([b"Hello, World!"]))