Deprecate app=... in favor of explicit WSGITransport/ASGITransport. (#3050)
* 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
This commit is contained in:
parent
6f461522a5
commit
cabd1c095e
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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).
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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!"]))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user