Skip to content

Commit cabd1c0

Browse files
authored
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
1 parent 6f46152 commit cabd1c0

File tree

6 files changed

+145
-72
lines changed

6 files changed

+145
-72
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## Unreleased
88

9+
### Deprecated
10+
11+
* The `app=...` shortcut has been deprecated. Use the explicit style of `transport=httpx.WSGITransport()` or `transport=httpx.ASGITransport()` instead.
12+
913
### Fixed
1014

1115
* Respect the `http1` argument while configuring proxy transports. (#3023)

docs/advanced/transports.md

+70-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ You can configure an `httpx` client to call directly into a Python web applicati
4242
This is particularly useful for two main use-cases:
4343

4444
* Using `httpx` as a client inside test cases.
45-
* Mocking out external services during tests or in dev/staging environments.
45+
* Mocking out external services during tests or in dev or staging environments.
46+
47+
### Example
4648

4749
Here's an example of integrating against a Flask application:
4850

@@ -57,12 +59,15 @@ app = Flask(__name__)
5759
def hello():
5860
return "Hello World!"
5961

60-
with httpx.Client(app=app, base_url="http://testserver") as client:
62+
transport = httpx.WSGITransport(app=app)
63+
with httpx.Client(transport=transport, base_url="http://testserver") as client:
6164
r = client.get("/")
6265
assert r.status_code == 200
6366
assert r.text == "Hello World!"
6467
```
6568

69+
### Configuration
70+
6671
For some more complex cases you might need to customize the WSGI transport. This allows you to:
6772

6873
* 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:
7883
...
7984
```
8085

86+
## ASGITransport
87+
88+
You can configure an `httpx` client to call directly into an async Python web application using the ASGI protocol.
89+
90+
This is particularly useful for two main use-cases:
91+
92+
* Using `httpx` as a client inside test cases.
93+
* Mocking out external services during tests or in dev or staging environments.
94+
95+
### Example
96+
97+
Let's take this Starlette application as an example:
98+
99+
```python
100+
from starlette.applications import Starlette
101+
from starlette.responses import HTMLResponse
102+
from starlette.routing import Route
103+
104+
105+
async def hello(request):
106+
return HTMLResponse("Hello World!")
107+
108+
109+
app = Starlette(routes=[Route("/", hello)])
110+
```
111+
112+
We can make requests directly against the application, like so:
113+
114+
```python
115+
transport = httpx.ASGITransport(app=app)
116+
117+
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
118+
r = await client.get("/")
119+
assert r.status_code == 200
120+
assert r.text == "Hello World!"
121+
```
122+
123+
### Configuration
124+
125+
For some more complex cases you might need to customise the ASGI transport. This allows you to:
126+
127+
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
128+
* Mount the ASGI application at a subpath by setting `root_path`.
129+
* Use a given client address for requests by setting `client`.
130+
131+
For example:
132+
133+
```python
134+
# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
135+
# on port 123.
136+
transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
137+
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
138+
...
139+
```
140+
141+
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.
142+
143+
### ASGI startup and shutdown
144+
145+
It is not in the scope of HTTPX to trigger ASGI lifespan events of your app.
146+
147+
However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
148+
81149
## Custom transports
82150

83151
A transport instance must implement the low-level Transport API, which deals

docs/async.md

+1-51
Original file line numberDiff line numberDiff line change
@@ -191,54 +191,4 @@ anyio.run(main, backend='trio')
191191

192192
## Calling into Python Web Apps
193193

194-
Just as `httpx.Client` allows you to call directly into WSGI web applications,
195-
the `httpx.AsyncClient` class allows you to call directly into ASGI web applications.
196-
197-
Let's take this Starlette application as an example:
198-
199-
```python
200-
from starlette.applications import Starlette
201-
from starlette.responses import HTMLResponse
202-
from starlette.routing import Route
203-
204-
205-
async def hello(request):
206-
return HTMLResponse("Hello World!")
207-
208-
209-
app = Starlette(routes=[Route("/", hello)])
210-
```
211-
212-
We can make requests directly against the application, like so:
213-
214-
```pycon
215-
>>> import httpx
216-
>>> async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
217-
... r = await client.get("/")
218-
... assert r.status_code == 200
219-
... assert r.text == "Hello World!"
220-
```
221-
222-
For some more complex cases you might need to customise the ASGI transport. This allows you to:
223-
224-
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
225-
* Mount the ASGI application at a subpath by setting `root_path`.
226-
* Use a given client address for requests by setting `client`.
227-
228-
For example:
229-
230-
```python
231-
# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
232-
# on port 123.
233-
transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
234-
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
235-
...
236-
```
237-
238-
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.
239-
240-
## Startup/shutdown of ASGI apps
241-
242-
It is not in the scope of HTTPX to trigger lifespan events of your app.
243-
244-
However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
194+
For details on calling directly into ASGI applications, see [the `ASGITransport` docs](../advanced/transports#asgitransport).

httpx/_client.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,13 @@ def __init__(
672672
if proxy:
673673
raise RuntimeError("Use either `proxy` or 'proxies', not both.")
674674

675+
if app:
676+
message = (
677+
"The 'app' shortcut is now deprecated."
678+
" Use the explicit style 'transport=WSGITransport(app=...)' instead."
679+
)
680+
warnings.warn(message, DeprecationWarning)
681+
675682
allow_env_proxies = trust_env and app is None and transport is None
676683
proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
677684

@@ -1411,7 +1418,14 @@ def __init__(
14111418
if proxy:
14121419
raise RuntimeError("Use either `proxy` or 'proxies', not both.")
14131420

1414-
allow_env_proxies = trust_env and app is None and transport is None
1421+
if app:
1422+
message = (
1423+
"The 'app' shortcut is now deprecated."
1424+
" Use the explicit style 'transport=ASGITransport(app=...)' instead."
1425+
)
1426+
warnings.warn(message, DeprecationWarning)
1427+
1428+
allow_env_proxies = trust_env and transport is None
14151429
proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
14161430

14171431
self._transport = self._init_transport(

tests/test_asgi.py

+28-9
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ async def test_asgi_transport_no_body():
9292

9393
@pytest.mark.anyio
9494
async def test_asgi():
95-
async with httpx.AsyncClient(app=hello_world) as client:
95+
transport = httpx.ASGITransport(app=hello_world)
96+
async with httpx.AsyncClient(transport=transport) as client:
9697
response = await client.get("http://www.example.org/")
9798

9899
assert response.status_code == 200
@@ -101,7 +102,8 @@ async def test_asgi():
101102

102103
@pytest.mark.anyio
103104
async def test_asgi_urlencoded_path():
104-
async with httpx.AsyncClient(app=echo_path) as client:
105+
transport = httpx.ASGITransport(app=echo_path)
106+
async with httpx.AsyncClient(transport=transport) as client:
105107
url = httpx.URL("http://www.example.org/").copy_with(path="/[email protected]")
106108
response = await client.get(url)
107109

@@ -111,7 +113,8 @@ async def test_asgi_urlencoded_path():
111113

112114
@pytest.mark.anyio
113115
async def test_asgi_raw_path():
114-
async with httpx.AsyncClient(app=echo_raw_path) as client:
116+
transport = httpx.ASGITransport(app=echo_raw_path)
117+
async with httpx.AsyncClient(transport=transport) as client:
115118
url = httpx.URL("http://www.example.org/").copy_with(path="/[email protected]")
116119
response = await client.get(url)
117120

@@ -124,7 +127,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
124127
"""
125128
See https://github.com/encode/httpx/issues/2810
126129
"""
127-
async with httpx.AsyncClient(app=echo_raw_path) as client:
130+
transport = httpx.ASGITransport(app=echo_raw_path)
131+
async with httpx.AsyncClient(transport=transport) as client:
128132
url = httpx.URL("http://www.example.org/path?query")
129133
response = await client.get(url)
130134

@@ -134,7 +138,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
134138

135139
@pytest.mark.anyio
136140
async def test_asgi_upload():
137-
async with httpx.AsyncClient(app=echo_body) as client:
141+
transport = httpx.ASGITransport(app=echo_body)
142+
async with httpx.AsyncClient(transport=transport) as client:
138143
response = await client.post("http://www.example.org/", content=b"example")
139144

140145
assert response.status_code == 200
@@ -143,7 +148,8 @@ async def test_asgi_upload():
143148

144149
@pytest.mark.anyio
145150
async def test_asgi_headers():
146-
async with httpx.AsyncClient(app=echo_headers) as client:
151+
transport = httpx.ASGITransport(app=echo_headers)
152+
async with httpx.AsyncClient(transport=transport) as client:
147153
response = await client.get("http://www.example.org/")
148154

149155
assert response.status_code == 200
@@ -160,14 +166,16 @@ async def test_asgi_headers():
160166

161167
@pytest.mark.anyio
162168
async def test_asgi_exc():
163-
async with httpx.AsyncClient(app=raise_exc) as client:
169+
transport = httpx.ASGITransport(app=raise_exc)
170+
async with httpx.AsyncClient(transport=transport) as client:
164171
with pytest.raises(RuntimeError):
165172
await client.get("http://www.example.org/")
166173

167174

168175
@pytest.mark.anyio
169176
async def test_asgi_exc_after_response():
170-
async with httpx.AsyncClient(app=raise_exc_after_response) as client:
177+
transport = httpx.ASGITransport(app=raise_exc_after_response)
178+
async with httpx.AsyncClient(transport=transport) as client:
171179
with pytest.raises(RuntimeError):
172180
await client.get("http://www.example.org/")
173181

@@ -199,7 +207,8 @@ async def read_body(scope, receive, send):
199207
message = await receive()
200208
disconnect = message.get("type") == "http.disconnect"
201209

202-
async with httpx.AsyncClient(app=read_body) as client:
210+
transport = httpx.ASGITransport(app=read_body)
211+
async with httpx.AsyncClient(transport=transport) as client:
203212
response = await client.post("http://www.example.org/", content=b"example")
204213

205214
assert response.status_code == 200
@@ -213,3 +222,13 @@ async def test_asgi_exc_no_raise():
213222
response = await client.get("http://www.example.org/")
214223

215224
assert response.status_code == 500
225+
226+
227+
@pytest.mark.anyio
228+
async def test_deprecated_shortcut():
229+
"""
230+
The `app=...` shortcut is now deprecated.
231+
Use the explicit transport style instead.
232+
"""
233+
with pytest.warns(DeprecationWarning):
234+
httpx.AsyncClient(app=hello_world)

tests/test_wsgi.py

+27-9
Original file line numberDiff line numberDiff line change
@@ -92,49 +92,56 @@ def log_to_wsgi_log_buffer(environ, start_response):
9292

9393

9494
def test_wsgi():
95-
client = httpx.Client(app=application_factory([b"Hello, World!"]))
95+
transport = httpx.WSGITransport(app=application_factory([b"Hello, World!"]))
96+
client = httpx.Client(transport=transport)
9697
response = client.get("http://www.example.org/")
9798
assert response.status_code == 200
9899
assert response.text == "Hello, World!"
99100

100101

101102
def test_wsgi_upload():
102-
client = httpx.Client(app=echo_body)
103+
transport = httpx.WSGITransport(app=echo_body)
104+
client = httpx.Client(transport=transport)
103105
response = client.post("http://www.example.org/", content=b"example")
104106
assert response.status_code == 200
105107
assert response.text == "example"
106108

107109

108110
def test_wsgi_upload_with_response_stream():
109-
client = httpx.Client(app=echo_body_with_response_stream)
111+
transport = httpx.WSGITransport(app=echo_body_with_response_stream)
112+
client = httpx.Client(transport=transport)
110113
response = client.post("http://www.example.org/", content=b"example")
111114
assert response.status_code == 200
112115
assert response.text == "example"
113116

114117

115118
def test_wsgi_exc():
116-
client = httpx.Client(app=raise_exc)
119+
transport = httpx.WSGITransport(app=raise_exc)
120+
client = httpx.Client(transport=transport)
117121
with pytest.raises(ValueError):
118122
client.get("http://www.example.org/")
119123

120124

121125
def test_wsgi_http_error():
122-
client = httpx.Client(app=partial(raise_exc, exc=RuntimeError))
126+
transport = httpx.WSGITransport(app=partial(raise_exc, exc=RuntimeError))
127+
client = httpx.Client(transport=transport)
123128
with pytest.raises(RuntimeError):
124129
client.get("http://www.example.org/")
125130

126131

127132
def test_wsgi_generator():
128133
output = [b"", b"", b"Some content", b" and more content"]
129-
client = httpx.Client(app=application_factory(output))
134+
transport = httpx.WSGITransport(app=application_factory(output))
135+
client = httpx.Client(transport=transport)
130136
response = client.get("http://www.example.org/")
131137
assert response.status_code == 200
132138
assert response.text == "Some content and more content"
133139

134140

135141
def test_wsgi_generator_empty():
136142
output = [b"", b"", b"", b""]
137-
client = httpx.Client(app=application_factory(output))
143+
transport = httpx.WSGITransport(app=application_factory(output))
144+
client = httpx.Client(transport=transport)
138145
response = client.get("http://www.example.org/")
139146
assert response.status_code == 200
140147
assert response.text == ""
@@ -170,7 +177,8 @@ def app(environ, start_response):
170177
server_port = environ["SERVER_PORT"]
171178
return hello_world_app(environ, start_response)
172179

173-
client = httpx.Client(app=app)
180+
transport = httpx.WSGITransport(app=app)
181+
client = httpx.Client(transport=transport)
174182
response = client.get(url)
175183
assert response.status_code == 200
176184
assert response.text == "Hello, World!"
@@ -186,9 +194,19 @@ def app(environ, start_response):
186194
start_response("200 OK", [("Content-Type", "text/plain")])
187195
return [b"success"]
188196

189-
with httpx.Client(app=app, base_url="http://testserver") as client:
197+
transport = httpx.WSGITransport(app=app)
198+
with httpx.Client(transport=transport, base_url="http://testserver") as client:
190199
response = client.get("/")
191200

192201
assert response.status_code == 200
193202
assert response.text == "success"
194203
assert server_protocol == "HTTP/1.1"
204+
205+
206+
def test_deprecated_shortcut():
207+
"""
208+
The `app=...` shortcut is now deprecated.
209+
Use the explicit transport style instead.
210+
"""
211+
with pytest.warns(DeprecationWarning):
212+
httpx.Client(app=application_factory([b"Hello, World!"]))

0 commit comments

Comments
 (0)