diff --git a/httpcore/adapters/redirects.py b/httpcore/adapters/redirects.py index 0a79b4c9..08639cd8 100644 --- a/httpcore/adapters/redirects.py +++ b/httpcore/adapters/redirects.py @@ -65,7 +65,7 @@ class RedirectAdapter(Adapter): url = self.redirect_url(request, response) headers = self.redirect_headers(request, url) content = self.redirect_content(request, method) - return Request(method=method, url=url, headers=headers, content=content) + return Request(method=method, url=url, headers=headers, data=content) def redirect_method(self, request: Request, response: Response) -> str: """ diff --git a/httpcore/backends/sync.py b/httpcore/backends/sync.py index ae831941..2bb582a5 100644 --- a/httpcore/backends/sync.py +++ b/httpcore/backends/sync.py @@ -14,10 +14,11 @@ from ..config import ( ) from ..models import ( URL, - ByteOrByteStream, Headers, HeaderTypes, + QueryParamTypes, Request, + RequestData, Response, URLTypes, ) @@ -100,14 +101,17 @@ class SyncClient: method: str, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, ssl: SSLConfig = None, timeout: TimeoutConfig = None, ) -> SyncResponse: - request = Request(method, url, headers=headers, content=content) + request = Request( + method, url, data=data, query_params=query_params, headers=headers + ) self.prepare_request(request) response = self.send( request, @@ -122,6 +126,7 @@ class SyncClient: self, url: URLTypes, *, + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -142,6 +147,7 @@ class SyncClient: self, url: URLTypes, *, + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -162,6 +168,7 @@ class SyncClient: self, url: URLTypes, *, + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = False, #  Note: Differs to usual default. @@ -182,7 +189,8 @@ class SyncClient: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -192,7 +200,7 @@ class SyncClient: return self.request( "POST", url, - content=content, + data=data, headers=headers, stream=stream, allow_redirects=allow_redirects, @@ -204,7 +212,8 @@ class SyncClient: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -214,7 +223,7 @@ class SyncClient: return self.request( "PUT", url, - content=content, + data=data, headers=headers, stream=stream, allow_redirects=allow_redirects, @@ -226,7 +235,8 @@ class SyncClient: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -236,7 +246,7 @@ class SyncClient: return self.request( "PATCH", url, - content=content, + data=data, headers=headers, stream=stream, allow_redirects=allow_redirects, @@ -248,7 +258,8 @@ class SyncClient: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, allow_redirects: bool = True, @@ -258,7 +269,7 @@ class SyncClient: return self.request( "DELETE", url, - content=content, + data=data, headers=headers, stream=stream, allow_redirects=allow_redirects, diff --git a/httpcore/client.py b/httpcore/client.py index a3e31f1d..16925b31 100644 --- a/httpcore/client.py +++ b/httpcore/client.py @@ -17,10 +17,10 @@ from .config import ( from .dispatch.connection_pool import ConnectionPool from .models import ( URL, - ByteOrByteStream, HeaderTypes, QueryParamTypes, Request, + RequestData, Response, URLTypes, ) @@ -49,7 +49,7 @@ class Client: method: str, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, @@ -58,7 +58,7 @@ class Client: timeout: TimeoutConfig = None, ) -> Response: request = Request( - method, url, query_params=query_params, headers=headers, content=content + method, url, data=data, query_params=query_params, headers=headers ) self.prepare_request(request) response = await self.send( @@ -140,7 +140,7 @@ class Client: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, @@ -151,7 +151,7 @@ class Client: return await self.request( "POST", url, - content=content, + data=data, query_params=query_params, headers=headers, stream=stream, @@ -164,7 +164,7 @@ class Client: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, @@ -175,7 +175,7 @@ class Client: return await self.request( "PUT", url, - content=content, + data=data, query_params=query_params, headers=headers, stream=stream, @@ -188,7 +188,7 @@ class Client: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, @@ -199,7 +199,7 @@ class Client: return await self.request( "PATCH", url, - content=content, + data=data, query_params=query_params, headers=headers, stream=stream, @@ -212,7 +212,7 @@ class Client: self, url: URLTypes, *, - content: ByteOrByteStream = b"", + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, stream: bool = False, @@ -223,7 +223,7 @@ class Client: return await self.request( "DELETE", url, - content=content, + data=data, query_params=query_params, headers=headers, stream=stream, diff --git a/httpcore/interfaces.py b/httpcore/interfaces.py index 2ada76c8..5903c454 100644 --- a/httpcore/interfaces.py +++ b/httpcore/interfaces.py @@ -2,7 +2,15 @@ import typing from types import TracebackType from .config import TimeoutConfig -from .models import URL, ByteOrByteStream, HeaderTypes, Request, Response, URLTypes +from .models import ( + URL, + HeaderTypes, + QueryParamTypes, + Request, + RequestData, + Response, + URLTypes, +) OptionalTimeout = typing.Optional[TimeoutConfig] @@ -21,11 +29,14 @@ class Adapter: method: str, url: URLTypes, *, + data: RequestData = b"", + query_params: QueryParamTypes = None, headers: HeaderTypes = None, - content: ByteOrByteStream = b"", **options: typing.Any, ) -> Response: - request = Request(method, url, headers=headers, content=content) + request = Request( + method, url, data=data, query_params=query_params, headers=headers + ) self.prepare_request(request) response = await self.send(request, **options) return response diff --git a/httpcore/models.py b/httpcore/models.py index 5eeec460..251f6265 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -44,7 +44,9 @@ HeaderTypes = typing.Union[ typing.List[typing.Tuple[typing.AnyStr, typing.AnyStr]], ] -ByteOrByteStream = typing.Union[bytes, typing.AsyncIterator[bytes]] +RequestData = typing.Union[dict, bytes, typing.AsyncIterator[bytes]] + +ResponseContent = typing.Union[bytes, typing.AsyncIterator[bytes]] class URL: @@ -197,7 +199,7 @@ class Origin: return hash((self.is_ssl, self.host, self.port)) -class QueryParams(typing.Mapping): +class QueryParams(typing.Mapping[str, str]): """ URL query parameters, as a multi-dict. """ @@ -456,19 +458,24 @@ class Request: method: str, url: typing.Union[str, URL], *, + data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, - content: ByteOrByteStream = b"", ): self.method = method.upper() self.url = URL(url, query_params=query_params) - if isinstance(content, bytes): + self.headers = Headers(headers) + + if isinstance(data, bytes): self.is_streaming = False - self.content = content + self.content = data + elif isinstance(data, dict): + self.is_streaming = False + self.content = urlencode(data, doseq=True).encode("utf-8") + self.headers["Content-Type"] = "application/x-www-form-urlencoded" else: self.is_streaming = True - self.content_aiter = content - self.headers = Headers(headers) + self.content_aiter = data async def read(self) -> bytes: """ @@ -532,7 +539,7 @@ class Response: reason_phrase: str = None, protocol: str = None, headers: HeaderTypes = None, - content: ByteOrByteStream = b"", + content: ResponseContent = b"", on_close: typing.Callable = None, request: Request = None, history: typing.List["Response"] = None, diff --git a/tests/adapters/test_redirects.py b/tests/adapters/test_redirects.py index 574b6dc5..94e5a745 100644 --- a/tests/adapters/test_redirects.py +++ b/tests/adapters/test_redirects.py @@ -222,8 +222,8 @@ async def test_same_domain_redirect(): async def test_body_redirect(): client = RedirectAdapter(MockDispatch()) url = "https://example.org/redirect_body" - content = b"Example request body" - response = await client.request("POST", url, content=content) + data = b"Example request body" + response = await client.request("POST", url, data=data) data = json.loads(response.content.decode()) assert response.url == URL("https://example.org/redirect_body_target") assert data == {"body": "Example request body"} @@ -238,4 +238,4 @@ async def test_cannot_redirect_streaming_body(): yield b"Example request body" with pytest.raises(RedirectBodyUnavailable): - await client.request("POST", url, content=streaming_body()) + await client.request("POST", url, data=streaming_body()) diff --git a/tests/dispatch/test_http2.py b/tests/dispatch/test_http2.py index 0adf7b8a..b9bf8ccf 100644 --- a/tests/dispatch/test_http2.py +++ b/tests/dispatch/test_http2.py @@ -92,7 +92,7 @@ async def test_http2_get_request(): async def test_http2_post_request(): server = MockServer() async with httpcore.HTTP2Connection(reader=server, writer=server) as conn: - response = await conn.request("POST", "http://example.org", content=b"") + response = await conn.request("POST", "http://example.org", data=b"") assert response.status_code == 200 assert json.loads(response.content) == { "method": "POST", diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index 98eea510..f010db8c 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -17,7 +17,7 @@ def test_host_header(): def test_content_length_header(): - request = httpcore.Request("POST", "http://example.org", content=b"test 123") + request = httpcore.Request("POST", "http://example.org", data=b"test 123") request.prepare() assert request.headers == httpcore.Headers( [ @@ -28,13 +28,27 @@ def test_content_length_header(): ) +def test_url_encoded_data(): + request = httpcore.Request("POST", "http://example.org", data={"test": "123"}) + request.prepare() + assert request.headers == httpcore.Headers( + [ + (b"host", b"example.org"), + (b"content-length", b"8"), + (b"accept-encoding", b"deflate, gzip, br"), + (b"content-type", b"application/x-www-form-urlencoded"), + ] + ) + assert request.content == b"test=123" + + def test_transfer_encoding_header(): async def streaming_body(data): yield data # pragma: nocover - content = streaming_body(b"test 123") + data = streaming_body(b"test 123") - request = httpcore.Request("POST", "http://example.org", content=content) + request = httpcore.Request("POST", "http://example.org", data=data) request.prepare() assert request.headers == httpcore.Headers( [ @@ -69,12 +83,10 @@ def test_override_content_length_header(): async def streaming_body(data): yield data # pragma: nocover - content = streaming_body(b"test 123") + data = streaming_body(b"test 123") headers = [(b"content-length", b"8")] - request = httpcore.Request( - "POST", "http://example.org", content=content, headers=headers - ) + request = httpcore.Request("POST", "http://example.org", data=data, headers=headers) request.prepare() assert request.headers == httpcore.Headers( [ diff --git a/tests/test_client.py b/tests/test_client.py index 1d33379c..cd6437ae 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,7 +16,7 @@ async def test_get(server): async def test_post(server): url = "http://127.0.0.1:8000/" async with httpcore.Client() as client: - response = await client.post(url, content=b"Hello, world!") + response = await client.post(url, data=b"Hello, world!") assert response.status_code == 200 @@ -47,7 +47,7 @@ async def test_stream_request(server): async with httpcore.Client() as client: response = await client.request( - "POST", "http://127.0.0.1:8000/", content=hello_world() + "POST", "http://127.0.0.1:8000/", data=hello_world() ) assert response.status_code == 200 diff --git a/tests/test_sync.py b/tests/test_sync.py index 99b6c188..b5f55cef 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -35,7 +35,7 @@ def test_get(server): @threadpool def test_post(server): with httpcore.SyncClient() as http: - response = http.post("http://127.0.0.1:8000/", content=b"Hello, world!") + response = http.post("http://127.0.0.1:8000/", data=b"Hello, world!") assert response.status_code == 200 assert response.reason_phrase == "OK"