1
0
mirror of https://github.com/django/django.git synced 2025-04-01 03:56:42 +00:00

Fixed #14611 -- Added query_params argument to RequestFactory and Client classes.

This commit is contained in:
Tom Carrick 2023-11-05 16:41:16 +01:00 committed by Mariusz Felisiak
parent e76cc93b01
commit a03593967f
6 changed files with 434 additions and 82 deletions

View File

@ -381,13 +381,22 @@ class RequestFactory:
just as if that view had been hooked up using a URLconf. just as if that view had been hooked up using a URLconf.
""" """
def __init__(self, *, json_encoder=DjangoJSONEncoder, headers=None, **defaults): def __init__(
self,
*,
json_encoder=DjangoJSONEncoder,
headers=None,
query_params=None,
**defaults,
):
self.json_encoder = json_encoder self.json_encoder = json_encoder
self.defaults = defaults self.defaults = defaults
self.cookies = SimpleCookie() self.cookies = SimpleCookie()
self.errors = BytesIO() self.errors = BytesIO()
if headers: if headers:
self.defaults.update(HttpHeaders.to_wsgi_names(headers)) self.defaults.update(HttpHeaders.to_wsgi_names(headers))
if query_params:
self.defaults["QUERY_STRING"] = urlencode(query_params, doseq=True)
def _base_environ(self, **request): def _base_environ(self, **request):
""" """
@ -459,18 +468,21 @@ class RequestFactory:
# Refs comment in `get_bytes_from_wsgi()`. # Refs comment in `get_bytes_from_wsgi()`.
return path.decode("iso-8859-1") return path.decode("iso-8859-1")
def get(self, path, data=None, secure=False, *, headers=None, **extra): def get(
self, path, data=None, secure=False, *, headers=None, query_params=None, **extra
):
"""Construct a GET request.""" """Construct a GET request."""
data = {} if data is None else data if query_params and data:
raise ValueError("query_params and data arguments are mutually exclusive.")
query_params = data or query_params
query_params = {} if query_params is None else query_params
return self.generic( return self.generic(
"GET", "GET",
path, path,
secure=secure, secure=secure,
headers=headers, headers=headers,
**{ query_params=query_params,
"QUERY_STRING": urlencode(data, doseq=True),
**extra, **extra,
},
) )
def post( def post(
@ -481,6 +493,7 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct a POST request.""" """Construct a POST request."""
@ -494,26 +507,37 @@ class RequestFactory:
content_type, content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
def head(self, path, data=None, secure=False, *, headers=None, **extra): def head(
self, path, data=None, secure=False, *, headers=None, query_params=None, **extra
):
"""Construct a HEAD request.""" """Construct a HEAD request."""
data = {} if data is None else data if query_params and data:
raise ValueError("query_params and data arguments are mutually exclusive.")
query_params = data or query_params
query_params = {} if query_params is None else query_params
return self.generic( return self.generic(
"HEAD", "HEAD",
path, path,
secure=secure, secure=secure,
headers=headers, headers=headers,
**{ query_params=query_params,
"QUERY_STRING": urlencode(data, doseq=True),
**extra, **extra,
},
) )
def trace(self, path, secure=False, *, headers=None, **extra): def trace(self, path, secure=False, *, headers=None, query_params=None, **extra):
"""Construct a TRACE request.""" """Construct a TRACE request."""
return self.generic("TRACE", path, secure=secure, headers=headers, **extra) return self.generic(
"TRACE",
path,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
)
def options( def options(
self, self,
@ -523,11 +547,19 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"Construct an OPTIONS request." "Construct an OPTIONS request."
return self.generic( return self.generic(
"OPTIONS", path, data, content_type, secure=secure, headers=headers, **extra "OPTIONS",
path,
data,
content_type,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
def put( def put(
@ -538,12 +570,20 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct a PUT request.""" """Construct a PUT request."""
data = self._encode_json(data, content_type) data = self._encode_json(data, content_type)
return self.generic( return self.generic(
"PUT", path, data, content_type, secure=secure, headers=headers, **extra "PUT",
path,
data,
content_type,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
def patch( def patch(
@ -554,12 +594,20 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct a PATCH request.""" """Construct a PATCH request."""
data = self._encode_json(data, content_type) data = self._encode_json(data, content_type)
return self.generic( return self.generic(
"PATCH", path, data, content_type, secure=secure, headers=headers, **extra "PATCH",
path,
data,
content_type,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
def delete( def delete(
@ -570,12 +618,20 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct a DELETE request.""" """Construct a DELETE request."""
data = self._encode_json(data, content_type) data = self._encode_json(data, content_type)
return self.generic( return self.generic(
"DELETE", path, data, content_type, secure=secure, headers=headers, **extra "DELETE",
path,
data,
content_type,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
def generic( def generic(
@ -587,6 +643,7 @@ class RequestFactory:
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct an arbitrary HTTP request.""" """Construct an arbitrary HTTP request."""
@ -608,6 +665,8 @@ class RequestFactory:
) )
if headers: if headers:
extra.update(HttpHeaders.to_wsgi_names(headers)) extra.update(HttpHeaders.to_wsgi_names(headers))
if query_params:
extra["QUERY_STRING"] = urlencode(query_params, doseq=True)
r.update(extra) r.update(extra)
# If QUERY_STRING is absent or empty, we want to extract it from the URL. # If QUERY_STRING is absent or empty, we want to extract it from the URL.
if not r.get("QUERY_STRING"): if not r.get("QUERY_STRING"):
@ -685,6 +744,7 @@ class AsyncRequestFactory(RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Construct an arbitrary HTTP request.""" """Construct an arbitrary HTTP request."""
@ -705,18 +765,20 @@ class AsyncRequestFactory(RequestFactory):
] ]
) )
s["_body_file"] = FakePayload(data) s["_body_file"] = FakePayload(data)
if query_string := extra.pop("QUERY_STRING", None): if query_params:
s["query_string"] = urlencode(query_params, doseq=True)
elif query_string := extra.pop("QUERY_STRING", None):
s["query_string"] = query_string s["query_string"] = query_string
else:
# If QUERY_STRING is absent or empty, we want to extract it from
# the URL.
s["query_string"] = parsed[4]
if headers: if headers:
extra.update(HttpHeaders.to_asgi_names(headers)) extra.update(HttpHeaders.to_asgi_names(headers))
s["headers"] += [ s["headers"] += [
(key.lower().encode("ascii"), value.encode("latin1")) (key.lower().encode("ascii"), value.encode("latin1"))
for key, value in extra.items() for key, value in extra.items()
] ]
# If QUERY_STRING is absent or empty, we want to extract it from the
# URL.
if not s.get("query_string"):
s["query_string"] = parsed[4]
return self.request(**s) return self.request(**s)
@ -889,7 +951,14 @@ class ClientMixin:
return response._json return response._json
def _follow_redirect( def _follow_redirect(
self, response, *, data="", content_type="", headers=None, **extra self,
response,
*,
data="",
content_type="",
headers=None,
query_params=None,
**extra,
): ):
"""Follow a single redirect contained in response using GET.""" """Follow a single redirect contained in response using GET."""
response_url = response.url response_url = response.url
@ -934,6 +1003,7 @@ class ClientMixin:
content_type=content_type, content_type=content_type,
follow=False, follow=False,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
@ -978,9 +1048,10 @@ class Client(ClientMixin, RequestFactory):
raise_request_exception=True, raise_request_exception=True,
*, *,
headers=None, headers=None,
query_params=None,
**defaults, **defaults,
): ):
super().__init__(headers=headers, **defaults) super().__init__(headers=headers, query_params=query_params, **defaults)
self.handler = ClientHandler(enforce_csrf_checks) self.handler = ClientHandler(enforce_csrf_checks)
self.raise_request_exception = raise_request_exception self.raise_request_exception = raise_request_exception
self.exc_info = None self.exc_info = None
@ -1042,15 +1113,23 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using GET.""" """Request a response from the server using GET."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = super().get(path, data=data, secure=secure, headers=headers, **extra) response = super().get(
path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
)
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1063,6 +1142,7 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using POST.""" """Request a response from the server using POST."""
@ -1074,11 +1154,17 @@ class Client(ClientMixin, RequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1090,17 +1176,23 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using HEAD.""" """Request a response from the server using HEAD."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = super().head( response = super().head(
path, data=data, secure=secure, headers=headers, **extra path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1113,6 +1205,7 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using OPTIONS.""" """Request a response from the server using OPTIONS."""
@ -1124,11 +1217,17 @@ class Client(ClientMixin, RequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1141,6 +1240,7 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a resource to the server using PUT.""" """Send a resource to the server using PUT."""
@ -1152,11 +1252,17 @@ class Client(ClientMixin, RequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1169,6 +1275,7 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a resource to the server using PATCH.""" """Send a resource to the server using PATCH."""
@ -1180,11 +1287,17 @@ class Client(ClientMixin, RequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1197,6 +1310,7 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a DELETE request to the server.""" """Send a DELETE request to the server."""
@ -1208,11 +1322,17 @@ class Client(ClientMixin, RequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1224,17 +1344,23 @@ class Client(ClientMixin, RequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a TRACE request to the server.""" """Send a TRACE request to the server."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = super().trace( response = super().trace(
path, data=data, secure=secure, headers=headers, **extra path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
if follow: if follow:
response = self._handle_redirects( response = self._handle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1244,6 +1370,7 @@ class Client(ClientMixin, RequestFactory):
data="", data="",
content_type="", content_type="",
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
""" """
@ -1257,6 +1384,7 @@ class Client(ClientMixin, RequestFactory):
data=data, data=data,
content_type=content_type, content_type=content_type,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
response.redirect_chain = redirect_chain response.redirect_chain = redirect_chain
@ -1278,9 +1406,10 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
raise_request_exception=True, raise_request_exception=True,
*, *,
headers=None, headers=None,
query_params=None,
**defaults, **defaults,
): ):
super().__init__(headers=headers, **defaults) super().__init__(headers=headers, query_params=query_params, **defaults)
self.handler = AsyncClientHandler(enforce_csrf_checks) self.handler = AsyncClientHandler(enforce_csrf_checks)
self.raise_request_exception = raise_request_exception self.raise_request_exception = raise_request_exception
self.exc_info = None self.exc_info = None
@ -1341,17 +1470,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using GET.""" """Request a response from the server using GET."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = await super().get( response = await super().get(
path, data=data, secure=secure, headers=headers, **extra path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1364,6 +1499,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using POST.""" """Request a response from the server using POST."""
@ -1375,11 +1511,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1391,17 +1533,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using HEAD.""" """Request a response from the server using HEAD."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = await super().head( response = await super().head(
path, data=data, secure=secure, headers=headers, **extra path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1414,6 +1562,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Request a response from the server using OPTIONS.""" """Request a response from the server using OPTIONS."""
@ -1425,11 +1574,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1442,6 +1597,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a resource to the server using PUT.""" """Send a resource to the server using PUT."""
@ -1453,11 +1609,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1470,6 +1632,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a resource to the server using PATCH.""" """Send a resource to the server using PATCH."""
@ -1481,11 +1644,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1498,6 +1667,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a DELETE request to the server.""" """Send a DELETE request to the server."""
@ -1509,11 +1679,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
content_type=content_type, content_type=content_type,
secure=secure, secure=secure,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, content_type=content_type, headers=headers, **extra response,
data=data,
content_type=content_type,
headers=headers,
query_params=query_params,
**extra,
) )
return response return response
@ -1525,17 +1701,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
secure=False, secure=False,
*, *,
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
"""Send a TRACE request to the server.""" """Send a TRACE request to the server."""
self.extra = extra self.extra = extra
self.headers = headers self.headers = headers
response = await super().trace( response = await super().trace(
path, data=data, secure=secure, headers=headers, **extra path,
data=data,
secure=secure,
headers=headers,
query_params=query_params,
**extra,
) )
if follow: if follow:
response = await self._ahandle_redirects( response = await self._ahandle_redirects(
response, data=data, headers=headers, **extra response, data=data, headers=headers, query_params=query_params, **extra
) )
return response return response
@ -1545,6 +1727,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
data="", data="",
content_type="", content_type="",
headers=None, headers=None,
query_params=None,
**extra, **extra,
): ):
""" """
@ -1558,6 +1741,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
data=data, data=data,
content_type=content_type, content_type=content_type,
headers=headers, headers=headers,
query_params=query_params,
**extra, **extra,
) )
response.redirect_chain = redirect_chain response.redirect_chain = redirect_chain

View File

@ -224,6 +224,17 @@ Tests
* The Django test runner now supports a ``--screenshots`` option to save * The Django test runner now supports a ``--screenshots`` option to save
screenshots for Selenium tests. screenshots for Selenium tests.
* The :class:`~django.test.RequestFactory`,
:class:`~django.test.AsyncRequestFactory`, :class:`~django.test.Client`, and
:class:`~django.test.AsyncClient` classes now support the ``query_params``
parameter, which accepts a dictionary of query string keys and values. This
allows setting query strings on any HTTP methods more easily.
.. code-block:: python
self.client.post("/items/1", query_params={"action": "delete"})
await self.async_client.post("/items/1", query_params={"action": "delete"})
URLs URLs
~~~~ ~~~~

View File

@ -32,6 +32,10 @@ restricted subset of the test client API:
attributes must be supplied by the test itself if required attributes must be supplied by the test itself if required
for the view to function properly. for the view to function properly.
.. versionchanged:: 5.1
The ``query_params`` parameter was added.
Example Example
------- -------
@ -85,6 +89,10 @@ difference being that it returns ``ASGIRequest`` instances rather than
Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI
scope. scope.
.. versionchanged:: 5.1
The ``query_params`` parameter was added.
Testing class-based views Testing class-based views
========================= =========================

View File

@ -120,7 +120,7 @@ Making requests
Use the ``django.test.Client`` class to make requests. Use the ``django.test.Client`` class to make requests.
.. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, **defaults) .. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)
A testing HTTP client. Takes several arguments that can customize behavior. A testing HTTP client. Takes several arguments that can customize behavior.
@ -129,6 +129,9 @@ Use the ``django.test.Client`` class to make requests.
client = Client(headers={"user-agent": "curl/7.79.1"}) client = Client(headers={"user-agent": "curl/7.79.1"})
``query_params`` allows you to specify the default query string that will
be set on every request.
Arbitrary keyword arguments in ``**defaults`` set WSGI Arbitrary keyword arguments in ``**defaults`` set WSGI
:pep:`environ variables <3333#environ-variables>`. For example, to set the :pep:`environ variables <3333#environ-variables>`. For example, to set the
script name:: script name::
@ -140,8 +143,8 @@ Use the ``django.test.Client`` class to make requests.
Keyword arguments starting with a ``HTTP_`` prefix are set as headers, Keyword arguments starting with a ``HTTP_`` prefix are set as headers,
but the ``headers`` parameter should be preferred for readability. but the ``headers`` parameter should be preferred for readability.
The values from the ``headers`` and ``extra`` keyword arguments passed to The values from the ``headers``, ``query_params``, and ``extra`` keyword
:meth:`~django.test.Client.get()`, arguments passed to :meth:`~django.test.Client.get()`,
:meth:`~django.test.Client.post()`, etc. have precedence over :meth:`~django.test.Client.post()`, etc. have precedence over
the defaults passed to the class constructor. the defaults passed to the class constructor.
@ -155,21 +158,25 @@ Use the ``django.test.Client`` class to make requests.
The ``json_encoder`` argument allows setting a custom JSON encoder for The ``json_encoder`` argument allows setting a custom JSON encoder for
the JSON serialization that's described in :meth:`post`. the JSON serialization that's described in :meth:`post`.
.. versionchanged:: 5.1
The ``query_params`` argument was added.
Once you have a ``Client`` instance, you can call any of the following Once you have a ``Client`` instance, you can call any of the following
methods: methods:
.. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, **extra) .. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a GET request on the provided ``path`` and returns a ``Response`` Makes a GET request on the provided ``path`` and returns a ``Response``
object, which is documented below. object, which is documented below.
The key-value pairs in the ``data`` dictionary are used to create a GET The key-value pairs in the ``query_params`` dictionary are used to set
data payload. For example: query strings. For example:
.. code-block:: pycon .. code-block:: pycon
>>> c = Client() >>> c = Client()
>>> c.get("/customers/details/", {"name": "fred", "age": 7}) >>> c.get("/customers/details/", query_params={"name": "fred", "age": 7})
...will result in the evaluation of a GET request equivalent to: ...will result in the evaluation of a GET request equivalent to:
@ -177,6 +184,10 @@ Use the ``django.test.Client`` class to make requests.
/customers/details/?name=fred&age=7 /customers/details/?name=fred&age=7
It is also possible to pass these parameters into the ``data``
parameter. However, ``query_params`` is preferred as it works for any
HTTP method.
The ``headers`` parameter can be used to specify headers to be sent in The ``headers`` parameter can be used to specify headers to be sent in
the request. For example: the request. For example:
@ -185,7 +196,7 @@ Use the ``django.test.Client`` class to make requests.
>>> c = Client() >>> c = Client()
>>> c.get( >>> c.get(
... "/customers/details/", ... "/customers/details/",
... {"name": "fred", "age": 7}, ... query_params={"name": "fred", "age": 7},
... headers={"accept": "application/json"}, ... headers={"accept": "application/json"},
... ) ... )
@ -211,8 +222,8 @@ Use the ``django.test.Client`` class to make requests.
>>> c = Client() >>> c = Client()
>>> c.get("/customers/details/?name=fred&age=7") >>> c.get("/customers/details/?name=fred&age=7")
If you provide a URL with both an encoded GET data and a data argument, If you provide a URL with both an encoded GET data and either a
the data argument will take precedence. query_params or data argument these arguments will take precedence.
If you set ``follow`` to ``True`` the client will follow any redirects If you set ``follow`` to ``True`` the client will follow any redirects
and a ``redirect_chain`` attribute will be set in the response object and a ``redirect_chain`` attribute will be set in the response object
@ -230,7 +241,11 @@ Use the ``django.test.Client`` class to make requests.
If you set ``secure`` to ``True`` the client will emulate an HTTPS If you set ``secure`` to ``True`` the client will emulate an HTTPS
request. request.
.. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a POST request on the provided ``path`` and returns a Makes a POST request on the provided ``path`` and returns a
``Response`` object, which is documented below. ``Response`` object, which is documented below.
@ -321,8 +336,8 @@ Use the ``django.test.Client`` class to make requests.
such as an image, this means you will need to open the file in such as an image, this means you will need to open the file in
``rb`` (read binary) mode. ``rb`` (read binary) mode.
The ``headers`` and ``extra`` parameters acts the same as for The ``headers``, ``query_params``, and ``extra`` parameters acts the
:meth:`Client.get`. same as for :meth:`Client.get`.
If the URL you request with a POST contains encoded parameters, these If the URL you request with a POST contains encoded parameters, these
parameters will be made available in the request.GET data. For example, parameters will be made available in the request.GET data. For example,
@ -330,7 +345,9 @@ Use the ``django.test.Client`` class to make requests.
.. code-block:: pycon .. code-block:: pycon
>>> c.post("/login/?visitor=true", {"name": "fred", "passwd": "secret"}) >>> c.post(
... "/login/", {"name": "fred", "passwd": "secret"}, query_params={"visitor": "true"}
... )
... the view handling this request could interrogate request.POST ... the view handling this request could interrogate request.POST
to retrieve the username and password, and could interrogate request.GET to retrieve the username and password, and could interrogate request.GET
@ -343,14 +360,22 @@ Use the ``django.test.Client`` class to make requests.
If you set ``secure`` to ``True`` the client will emulate an HTTPS If you set ``secure`` to ``True`` the client will emulate an HTTPS
request. request.
.. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a HEAD request on the provided ``path`` and returns a Makes a HEAD request on the provided ``path`` and returns a
``Response`` object. This method works just like :meth:`Client.get`, ``Response`` object. This method works just like :meth:`Client.get`,
including the ``follow``, ``secure``, ``headers``, and ``extra`` including the ``follow``, ``secure``, ``headers``, ``query_params``,
parameters, except it does not return a message body. and ``extra`` parameters, except it does not return a message body.
.. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes an OPTIONS request on the provided ``path`` and returns a Makes an OPTIONS request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
@ -358,10 +383,14 @@ Use the ``django.test.Client`` class to make requests.
When ``data`` is provided, it is used as the request body, and When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act The ``follow``, ``secure``, ``headers``, ``query_params``, and
the same as for :meth:`Client.get`. ``extra`` parameters act the same as for :meth:`Client.get`.
.. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a PUT request on the provided ``path`` and returns a Makes a PUT request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
@ -369,18 +398,26 @@ Use the ``django.test.Client`` class to make requests.
When ``data`` is provided, it is used as the request body, and When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act The ``follow``, ``secure``, ``headers``, ``query_params``, and
the same as for :meth:`Client.get`. ``extra`` parameters act the same as for :meth:`Client.get`.
.. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a PATCH request on the provided ``path`` and returns a Makes a PATCH request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act The ``follow``, ``secure``, ``headers``, ``query_params``, and
the same as for :meth:`Client.get`. ``extra`` parameters act the same as for :meth:`Client.get`.
.. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a DELETE request on the provided ``path`` and returns a Makes a DELETE request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
@ -388,10 +425,14 @@ Use the ``django.test.Client`` class to make requests.
When ``data`` is provided, it is used as the request body, and When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act The ``follow``, ``secure``, ``headers``, ``query_params``, and
the same as for :meth:`Client.get`. ``extra`` parameters act the same as for :meth:`Client.get`.
.. method:: Client.trace(path, follow=False, secure=False, *, headers=None, **extra) .. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra)
Makes a TRACE request on the provided ``path`` and returns a Makes a TRACE request on the provided ``path`` and returns a
``Response`` object. Useful for simulating diagnostic probes. ``Response`` object. Useful for simulating diagnostic probes.
@ -400,8 +441,12 @@ Use the ``django.test.Client`` class to make requests.
parameter in order to comply with :rfc:`9110#section-9.3.8`, which parameter in order to comply with :rfc:`9110#section-9.3.8`, which
mandates that TRACE requests must not have a body. mandates that TRACE requests must not have a body.
The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act The ``follow``, ``secure``, ``headers``, ``query_params``, and
the same as for :meth:`Client.get`. ``extra`` parameters act the same as for :meth:`Client.get`.
.. versionchanged:: 5.1
The ``query_params`` argument was added.
.. method:: Client.login(**credentials) .. method:: Client.login(**credentials)
.. method:: Client.alogin(**credentials) .. method:: Client.alogin(**credentials)
@ -1997,7 +2042,7 @@ If you are testing from an asynchronous function, you must also use the
asynchronous test client. This is available as ``django.test.AsyncClient``, asynchronous test client. This is available as ``django.test.AsyncClient``,
or as ``self.async_client`` on any test. or as ``self.async_client`` on any test.
.. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults) .. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, query_params=None, **defaults)
``AsyncClient`` has the same methods and signatures as the synchronous (normal) ``AsyncClient`` has the same methods and signatures as the synchronous (normal)
test client, with the following exceptions: test client, with the following exceptions:
@ -2017,6 +2062,10 @@ test client, with the following exceptions:
Support for the ``follow`` parameter was added to the ``AsyncClient``. Support for the ``follow`` parameter was added to the ``AsyncClient``.
.. versionchanged:: 5.1
The ``query_params`` argument was added.
Using ``AsyncClient`` any method that makes a request must be awaited:: Using ``AsyncClient`` any method that makes a request must be awaited::
async def test_my_thing(self): async def test_my_thing(self):

View File

@ -1002,6 +1002,36 @@ class ClientTest(TestCase):
) )
self.assertEqual(response.content, b"named_temp_file") self.assertEqual(response.content, b"named_temp_file")
def test_query_params(self):
tests = (
"get",
"post",
"put",
"patch",
"delete",
"head",
"options",
"trace",
)
for method in tests:
with self.subTest(method=method):
client_method = getattr(self.client, method)
response = client_method("/get_view/", query_params={"example": "data"})
self.assertEqual(response.wsgi_request.GET["example"], "data")
def test_cannot_use_data_and_query_params_together(self):
tests = ["get", "head"]
msg = "query_params and data arguments are mutually exclusive."
for method in tests:
with self.subTest(method=method):
client_method = getattr(self.client, method)
with self.assertRaisesMessage(ValueError, msg):
client_method(
"/get_view/",
data={"example": "data"},
query_params={"q": "terms"},
)
@override_settings( @override_settings(
MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"], MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
@ -1127,6 +1157,23 @@ class RequestFactoryTest(SimpleTestCase):
self.assertEqual(request.headers["x-another-header"], "some other value") self.assertEqual(request.headers["x-another-header"], "some other value")
self.assertIn("HTTP_X_ANOTHER_HEADER", request.META) self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
def test_request_factory_query_params(self):
tests = (
"get",
"post",
"put",
"patch",
"delete",
"head",
"options",
"trace",
)
for method in tests:
with self.subTest(method=method):
factory = getattr(self.request_factory, method)
request = factory("/somewhere", query_params={"example": "data"})
self.assertEqual(request.GET["example"], "data")
@override_settings(ROOT_URLCONF="test_client.urls") @override_settings(ROOT_URLCONF="test_client.urls")
class AsyncClientTest(TestCase): class AsyncClientTest(TestCase):
@ -1183,6 +1230,25 @@ class AsyncClientTest(TestCase):
response = await self.async_client.get("/post_view/") response = await self.async_client.get("/post_view/")
self.assertContains(response, "Viewing GET page.") self.assertContains(response, "Viewing GET page.")
async def test_query_params(self):
tests = (
"get",
"post",
"put",
"patch",
"delete",
"head",
"options",
"trace",
)
for method in tests:
with self.subTest(method=method):
client_method = getattr(self.async_client, method)
response = await client_method(
"/async_get_view/", query_params={"example": "data"}
)
self.assertEqual(response.asgi_request.GET["example"], "data")
@override_settings(ROOT_URLCONF="test_client.urls") @override_settings(ROOT_URLCONF="test_client.urls")
class AsyncRequestFactoryTest(SimpleTestCase): class AsyncRequestFactoryTest(SimpleTestCase):
@ -1264,3 +1330,33 @@ class AsyncRequestFactoryTest(SimpleTestCase):
request = self.request_factory.get("/somewhere/", {"example": "data"}) request = self.request_factory.get("/somewhere/", {"example": "data"})
self.assertNotIn("Query-String", request.headers) self.assertNotIn("Query-String", request.headers)
self.assertEqual(request.GET["example"], "data") self.assertEqual(request.GET["example"], "data")
def test_request_factory_query_params(self):
tests = (
"get",
"post",
"put",
"patch",
"delete",
"head",
"options",
"trace",
)
for method in tests:
with self.subTest(method=method):
factory = getattr(self.request_factory, method)
request = factory("/somewhere", query_params={"example": "data"})
self.assertEqual(request.GET["example"], "data")
def test_cannot_use_data_and_query_params_together(self):
tests = ["get", "head"]
msg = "query_params and data arguments are mutually exclusive."
for method in tests:
with self.subTest(method=method):
factory = getattr(self.request_factory, method)
with self.assertRaisesMessage(ValueError, msg):
factory(
"/somewhere",
data={"example": "data"},
query_params={"q": "terms"},
)

View File

@ -1197,6 +1197,10 @@ class QueryStringTests(SimpleTestCase):
self.assertEqual(response.context["get-foo"], "whiz") self.assertEqual(response.context["get-foo"], "whiz")
self.assertIsNone(response.context["post-foo"]) self.assertIsNone(response.context["post-foo"])
response = self.client.post("/request_data/", query_params={"foo": "whiz"})
self.assertEqual(response.context["get-foo"], "whiz")
self.assertIsNone(response.context["post-foo"])
# POST data provided in the URL augments actual form data # POST data provided in the URL augments actual form data
response = self.client.post("/request_data/?foo=whiz", data={"foo": "bang"}) response = self.client.post("/request_data/?foo=whiz", data={"foo": "bang"})
self.assertEqual(response.context["get-foo"], "whiz") self.assertEqual(response.context["get-foo"], "whiz")