Seperate content=... and data=... parameters (#1266)

* Seperate content=... and data=... parameters

* Update compatibility.md
This commit is contained in:
Tom Christie 2020-09-15 13:36:10 +01:00 committed by GitHub
parent 54f7708e2b
commit feb404f86b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 169 additions and 55 deletions

View File

@ -9,6 +9,28 @@ This documentation outlines places where the API differs...
Accessing `response.url` will return a `URL` instance, rather than a string.
Use `str(response.url)` if you need a string instance.
## Request Content
For uploading raw text or binary content we prefer to use a `content` parameter,
in order to better separate this usage from the case of uploading form data.
For example, using `content=...` to upload raw content:
```python
# Uploading text, bytes, or a bytes iterator.
httpx.post(..., content=b"Hello, world")
```
And using `data=...` to send form data:
```python
# Uploading form data.
httpx.post(..., data={"message": "Hello, world"})
```
If you're using a type checking tool such as `mypy`, you'll see warnings issues if using test/byte content with the `data` argument.
However, for compatibility reasons with `requests`, we do still handle the case where `data=...` is used with raw binary and text contents.
## Status Codes
In our documentation we prefer the uppercased versions, such as `codes.NOT_FOUND`, but also provide lower-cased versions for API compatibility with `requests`.

View File

@ -249,13 +249,18 @@ For more complicated data structures you'll often want to use JSON encoding inst
## Sending Binary Request Data
For other encodings, you should use either a `bytes` type or a generator
that yields `bytes`.
For other encodings, you should use the `content=...` parameter, passing
either a `bytes` type or a generator that yields `bytes`.
You'll probably also want to set a custom `Content-Type` header when uploading
```pycon
>>> content = b'Hello, world'
>>> r = httpx.post("https://httpbin.org/post", content=content)
```
You may also want to set a custom `Content-Type` header when uploading
binary data.
## Response Status Codes
## Response Status Codes
We can inspect the HTTP status code of the response:

View File

@ -10,6 +10,7 @@ from ._types import (
HeaderTypes,
ProxiesTypes,
QueryParamTypes,
RequestContent,
RequestData,
RequestFiles,
TimeoutTypes,
@ -23,6 +24,7 @@ def request(
url: URLTypes,
*,
params: QueryParamTypes = None,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -46,8 +48,10 @@ def request(
* **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.
* **data** - *(optional)* Data to include in the body of the request, as a
dictionary
* **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,
as a dictionary.
* **files** - *(optional)* A dictionary of upload files to include in the
body of the request.
* **json** - *(optional)* A JSON serializable object to include in the body
@ -89,6 +93,7 @@ def request(
return client.request(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
@ -105,6 +110,7 @@ def stream(
url: URLTypes,
*,
params: QueryParamTypes = None,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -133,6 +139,7 @@ def stream(
method=method,
url=url,
params=params,
content=content,
data=data,
files=files,
json=json,
@ -266,6 +273,7 @@ def head(
def post(
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -288,6 +296,7 @@ def post(
return request(
"POST",
url,
content=content,
data=data,
files=files,
json=json,
@ -307,6 +316,7 @@ def post(
def put(
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -329,6 +339,7 @@ def put(
return request(
"PUT",
url,
content=content,
data=data,
files=files,
json=json,
@ -348,6 +359,7 @@ def put(
def patch(
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -370,6 +382,7 @@ def patch(
return request(
"PATCH",
url,
content=content,
data=data,
files=files,
json=json,

View File

@ -39,6 +39,7 @@ from ._types import (
HeaderTypes,
ProxiesTypes,
QueryParamTypes,
RequestContent,
RequestData,
RequestFiles,
TimeoutTypes,
@ -226,6 +227,7 @@ class BaseClient:
method: str,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -249,6 +251,7 @@ class BaseClient:
request = self.build_request(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
@ -269,6 +272,7 @@ class BaseClient:
method: str,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -294,6 +298,7 @@ class BaseClient:
return Request(
method,
url,
content=content,
data=data,
files=files,
json=json,
@ -679,6 +684,7 @@ class Client(BaseClient):
method: str,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -708,6 +714,7 @@ class Client(BaseClient):
request = self.build_request(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
@ -962,6 +969,7 @@ class Client(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -980,6 +988,7 @@ class Client(BaseClient):
return self.request(
"POST",
url,
content=content,
data=data,
files=files,
json=json,
@ -995,6 +1004,7 @@ class Client(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1013,6 +1023,7 @@ class Client(BaseClient):
return self.request(
"PUT",
url,
content=content,
data=data,
files=files,
json=json,
@ -1028,6 +1039,7 @@ class Client(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1046,6 +1058,7 @@ class Client(BaseClient):
return self.request(
"PATCH",
url,
content=content,
data=data,
files=files,
json=json,
@ -1313,6 +1326,7 @@ class AsyncClient(BaseClient):
method: str,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1342,6 +1356,7 @@ class AsyncClient(BaseClient):
request = self.build_request(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
@ -1599,6 +1614,7 @@ class AsyncClient(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1617,6 +1633,7 @@ class AsyncClient(BaseClient):
return await self.request(
"POST",
url,
content=content,
data=data,
files=files,
json=json,
@ -1632,6 +1649,7 @@ class AsyncClient(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1650,6 +1668,7 @@ class AsyncClient(BaseClient):
return await self.request(
"PUT",
url,
content=content,
data=data,
files=files,
json=json,
@ -1665,6 +1684,7 @@ class AsyncClient(BaseClient):
self,
url: URLTypes,
*,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -1683,6 +1703,7 @@ class AsyncClient(BaseClient):
return await self.request(
"PATCH",
url,
content=content,
data=data,
files=files,
json=json,

View File

@ -8,7 +8,14 @@ from urllib.parse import urlencode
import httpcore
from ._exceptions import StreamConsumed
from ._types import FileContent, FileTypes, RequestData, RequestFiles, ResponseContent
from ._types import (
FileContent,
FileTypes,
RequestContent,
RequestData,
RequestFiles,
ResponseContent,
)
from ._utils import (
format_form_param,
guess_content_type,
@ -357,35 +364,52 @@ class MultipartStream(ContentStream):
def encode(
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
boundary: bytes = None,
) -> ContentStream:
"""
Handles encoding the given `data`, `files`, and `json`, returning
a `ContentStream` implementation.
Handles encoding the given `content`, `data`, `files`, and `json`,
returning a `ContentStream` implementation.
"""
if not data:
if json is not None:
return JSONStream(json=json)
elif files:
return MultipartStream(data={}, files=files, boundary=boundary)
if data is not None and not isinstance(data, dict):
# We prefer to seperate `content=<bytes|byte iterator|bytes aiterator>`
# for raw request content, and `data=<form data>` for url encoded or
# multipart form content.
#
# However for compat with requests, we *do* still support
# `data=<bytes...>` usages. We deal with that case here, treating it
# as if `content=<...>` had been supplied instead.
content = data
data = None
if content is not None:
if isinstance(content, (str, bytes)):
return ByteStream(body=content)
elif hasattr(content, "__aiter__"):
content = typing.cast(typing.AsyncIterator[bytes], content)
return AsyncIteratorStream(aiterator=content)
elif hasattr(content, "__iter__"):
content = typing.cast(typing.Iterator[bytes], content)
return IteratorStream(iterator=content)
else:
return ByteStream(body=b"")
elif isinstance(data, dict):
raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
elif data:
if files:
return MultipartStream(data=data, files=files, boundary=boundary)
else:
return URLEncodedStream(data=data)
elif isinstance(data, (str, bytes)):
return ByteStream(body=data)
elif isinstance(data, typing.AsyncIterator):
return AsyncIteratorStream(aiterator=data)
elif isinstance(data, typing.Iterator):
return IteratorStream(iterator=data)
raise TypeError(f"Unexpected type for 'data', {type(data)!r}")
elif files:
return MultipartStream(data={}, files=files, boundary=boundary)
elif json is not None:
return JSONStream(json=json)
return ByteStream(body=b"")
def encode_response(content: ResponseContent = None) -> ContentStream:

View File

@ -41,6 +41,7 @@ from ._types import (
HeaderTypes,
PrimitiveData,
QueryParamTypes,
RequestContent,
RequestData,
RequestFiles,
ResponseContent,
@ -590,6 +591,7 @@ class Request:
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
@ -604,7 +606,7 @@ class Request:
if stream is not None:
self.stream = stream
else:
self.stream = encode(data, files, json)
self.stream = encode(content, data, files, json)
self._prepare()

View File

@ -64,9 +64,10 @@ AuthTypes = Union[
None,
]
RequestContent = Union[str, bytes, Iterator[bytes], AsyncIterator[bytes]]
ResponseContent = Union[bytes, Iterator[bytes], AsyncIterator[bytes]]
RequestData = Union[dict, str, bytes, Iterator[bytes], AsyncIterator[bytes]]
RequestData = dict
FileContent = Union[IO[str], IO[bytes], str, bytes]
FileTypes = Union[

View File

@ -56,7 +56,7 @@ async def test_build_request(server):
async def test_post(server):
url = server.url
async with httpx.AsyncClient() as client:
response = await client.post(url, data=b"Hello, world!")
response = await client.post(url, content=b"Hello, world!")
assert response.status_code == 200
@ -97,7 +97,7 @@ async def test_stream_request(server):
yield b"world!"
async with httpx.AsyncClient() as client:
response = await client.request("POST", server.url, data=hello_world())
response = await client.request("POST", server.url, content=hello_world())
assert response.status_code == 200
@ -136,14 +136,14 @@ async def test_head(server):
@pytest.mark.usefixtures("async_environment")
async def test_put(server):
async with httpx.AsyncClient() as client:
response = await client.put(server.url, data=b"Hello, world!")
response = await client.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
@pytest.mark.usefixtures("async_environment")
async def test_patch(server):
async with httpx.AsyncClient() as client:
response = await client.patch(server.url, data=b"Hello, world!")
response = await client.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
@ -158,15 +158,15 @@ async def test_delete(server):
@pytest.mark.usefixtures("async_environment")
async def test_100_continue(server):
headers = {"Expect": "100-continue"}
data = b"Echo request body"
content = b"Echo request body"
async with httpx.AsyncClient() as client:
response = await client.post(
server.url.copy_with(path="/echo_body"), headers=headers, data=data
server.url.copy_with(path="/echo_body"), headers=headers, content=content
)
assert response.status_code == 200
assert response.content == data
assert response.content == content
@pytest.mark.usefixtures("async_environment")

View File

@ -73,7 +73,7 @@ def test_build_post_request(server):
def test_post(server):
with httpx.Client() as client:
response = client.post(server.url, data=b"Hello, world!")
response = client.post(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
@ -148,14 +148,14 @@ def test_head(server):
def test_put(server):
with httpx.Client() as client:
response = client.put(server.url, data=b"Hello, world!")
response = client.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_patch(server):
with httpx.Client() as client:
response = client.patch(server.url, data=b"Hello, world!")
response = client.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"

View File

@ -298,8 +298,8 @@ def test_body_redirect():
"""
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_body"
data = b"Example request body"
response = client.post(url, data=data)
content = b"Example request body"
response = client.post(url, content=content)
assert response.url == "https://example.org/redirect_body_target"
assert response.json()["body"] == "Example request body"
assert "content-length" in response.json()["headers"]
@ -311,8 +311,8 @@ def test_no_body_redirect():
"""
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_no_body"
data = b"Example request body"
response = client.post(url, data=data)
content = b"Example request body"
response = client.post(url, content=content)
assert response.url == "https://example.org/redirect_body_target"
assert response.json()["body"] == ""
assert "content-length" not in response.json()["headers"]
@ -335,7 +335,7 @@ def test_cannot_redirect_streaming_body():
yield b"Example request body" # pragma: nocover
with pytest.raises(httpx.RequestBodyUnavailable):
client.post(url, data=streaming_body())
client.post(url, content=streaming_body())
def test_cross_subdomain_redirect():

View File

@ -14,7 +14,7 @@ def test_no_content():
def test_content_length_header():
request = httpx.Request("POST", "http://example.org", data=b"test 123")
request = httpx.Request("POST", "http://example.org", content=b"test 123")
assert request.headers["Content-Length"] == "8"

View File

@ -12,7 +12,7 @@ def test_get(server):
def test_post(server):
response = httpx.post(server.url, data=b"Hello, world!")
response = httpx.post(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
@ -23,7 +23,7 @@ def test_post_byte_iterator(server):
yield b", "
yield b"world!"
response = httpx.post(server.url, data=data())
response = httpx.post(server.url, content=data())
assert response.status_code == 200
assert response.reason_phrase == "OK"
@ -41,13 +41,13 @@ def test_head(server):
def test_put(server):
response = httpx.put(server.url, data=b"Hello, world!")
response = httpx.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_patch(server):
response = httpx.patch(server.url, data=b"Hello, world!")
response = httpx.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"

View File

@ -51,7 +51,7 @@ async def test_asgi():
@pytest.mark.usefixtures("async_environment")
async def test_asgi_upload():
async with httpx.AsyncClient(app=echo_body) as client:
response = await client.post("http://www.example.org/", data=b"example")
response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
@ -99,7 +99,7 @@ async def test_asgi_disconnect_after_response_complete():
disconnect = message.get("type") == "http.disconnect"
async with httpx.AsyncClient(app=read_body) as client:
response = await client.post("http://www.example.org/", data=b"example")
response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert disconnect

View File

@ -32,7 +32,17 @@ async def test_empty_content():
@pytest.mark.asyncio
async def test_bytes_content():
stream = encode(data=b"Hello, world!")
stream = encode(content=b"Hello, world!")
sync_content = b"".join([part for part in stream])
async_content = b"".join([part async for part in stream])
assert stream.can_replay()
assert stream.get_headers() == {"Content-Length": "13"}
assert sync_content == b"Hello, world!"
assert async_content == b"Hello, world!"
# Support 'data' for compat with requests.
stream = encode(data=b"Hello, world!") # type: ignore
sync_content = b"".join([part for part in stream])
async_content = b"".join([part async for part in stream])
@ -48,7 +58,7 @@ async def test_iterator_content():
yield b"Hello, "
yield b"world!"
stream = encode(data=hello_world())
stream = encode(content=hello_world())
content = b"".join([part for part in stream])
assert not stream.can_replay()
@ -61,6 +71,14 @@ async def test_iterator_content():
with pytest.raises(StreamConsumed):
[part for part in stream]
# Support 'data' for compat with requests.
stream = encode(data=hello_world()) # type: ignore
content = b"".join([part for part in stream])
assert not stream.can_replay()
assert stream.get_headers() == {"Transfer-Encoding": "chunked"}
assert content == b"Hello, world!"
@pytest.mark.asyncio
async def test_aiterator_content():
@ -68,7 +86,7 @@ async def test_aiterator_content():
yield b"Hello, "
yield b"world!"
stream = encode(data=hello_world())
stream = encode(content=hello_world())
content = b"".join([part async for part in stream])
assert not stream.can_replay()
@ -81,6 +99,14 @@ async def test_aiterator_content():
with pytest.raises(StreamConsumed):
[part async for part in stream]
# Support 'data' for compat with requests.
stream = encode(data=hello_world()) # type: ignore
content = b"".join([part async for part in stream])
assert not stream.can_replay()
assert stream.get_headers() == {"Transfer-Encoding": "chunked"}
assert content == b"Hello, world!"
@pytest.mark.asyncio
async def test_json_content():

View File

@ -19,7 +19,7 @@ async def test_write_timeout(server):
async with httpx.AsyncClient(timeout=timeout) as client:
with pytest.raises(httpx.WriteTimeout):
data = b"*" * 1024 * 1024 * 100
await client.put(server.url.copy_with(path="/slow_response"), data=data)
await client.put(server.url.copy_with(path="/slow_response"), content=data)
@pytest.mark.usefixtures("async_environment")

View File

@ -78,14 +78,14 @@ def test_wsgi():
def test_wsgi_upload():
client = httpx.Client(app=echo_body)
response = client.post("http://www.example.org/", data=b"example")
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)
response = client.post("http://www.example.org/", data=b"example")
response = client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"

View File

@ -43,7 +43,7 @@ class MockTransport(httpcore.SyncHTTPTransport):
path = raw_path.decode("ascii")
request_headers = httpx.Headers(headers)
data = (
content = (
(item for item in stream)
if stream
and (
@ -57,7 +57,7 @@ class MockTransport(httpcore.SyncHTTPTransport):
method=method.decode("ascii"),
url=f"{scheme}://{host}{port_str}{path}",
headers=request_headers,
data=data,
content=content,
)
request.read()
response = self.handler(request)