测试gitnore
This commit is contained in:
@@ -16,7 +16,9 @@ from django.core.handlers.asgi import ASGIRequest
|
||||
from django.core.handlers.base import BaseHandler
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.signals import got_request_exception, request_finished, request_started
|
||||
from django.core.signals import (
|
||||
got_request_exception, request_finished, request_started,
|
||||
)
|
||||
from django.db import close_old_connections
|
||||
from django.http import HttpRequest, QueryDict, SimpleCookie
|
||||
from django.test import signals
|
||||
@@ -29,26 +31,20 @@ from django.utils.itercompat import is_iterable
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
__all__ = (
|
||||
"AsyncClient",
|
||||
"AsyncRequestFactory",
|
||||
"Client",
|
||||
"RedirectCycleError",
|
||||
"RequestFactory",
|
||||
"encode_file",
|
||||
"encode_multipart",
|
||||
'AsyncClient', 'AsyncRequestFactory', 'Client', 'RedirectCycleError',
|
||||
'RequestFactory', 'encode_file', 'encode_multipart',
|
||||
)
|
||||
|
||||
|
||||
BOUNDARY = "BoUnDaRyStRiNg"
|
||||
MULTIPART_CONTENT = "multipart/form-data; boundary=%s" % BOUNDARY
|
||||
CONTENT_TYPE_RE = _lazy_re_compile(r".*; charset=([\w\d-]+);?")
|
||||
BOUNDARY = 'BoUnDaRyStRiNg'
|
||||
MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
CONTENT_TYPE_RE = _lazy_re_compile(r'.*; charset=([\w\d-]+);?')
|
||||
# Structured suffix spec: https://tools.ietf.org/html/rfc6838#section-4.2.8
|
||||
JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
|
||||
JSON_CONTENT_TYPE_RE = _lazy_re_compile(r'^application\/(.+\+)?json')
|
||||
|
||||
|
||||
class RedirectCycleError(Exception):
|
||||
"""The test client has been asked to follow a redirect loop."""
|
||||
|
||||
def __init__(self, message, last_response):
|
||||
super().__init__(message)
|
||||
self.last_response = last_response
|
||||
@@ -62,7 +58,6 @@ class FakePayload:
|
||||
length. This makes sure that views can't do anything under the test client
|
||||
that wouldn't work in real life.
|
||||
"""
|
||||
|
||||
def __init__(self, content=None):
|
||||
self.__content = BytesIO()
|
||||
self.__len = 0
|
||||
@@ -79,9 +74,7 @@ class FakePayload:
|
||||
self.read_started = True
|
||||
if num_bytes is None:
|
||||
num_bytes = self.__len or 0
|
||||
assert (
|
||||
self.__len >= num_bytes
|
||||
), "Cannot read more than the available bytes from the HTTP incoming data."
|
||||
assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data."
|
||||
content = self.__content.read(num_bytes)
|
||||
self.__len -= num_bytes
|
||||
return content
|
||||
@@ -99,13 +92,13 @@ def closing_iterator_wrapper(iterable, close):
|
||||
yield from iterable
|
||||
finally:
|
||||
request_finished.disconnect(close_old_connections)
|
||||
close() # will fire request_finished
|
||||
close() # will fire request_finished
|
||||
request_finished.connect(close_old_connections)
|
||||
|
||||
|
||||
def conditional_content_removal(request, response):
|
||||
"""
|
||||
Simulate the behavior of most web servers by removing the content of
|
||||
Simulate the behavior of most Web servers by removing the content of
|
||||
responses for HEAD requests, 1xx, 204, and 304 responses. Ensure
|
||||
compliance with RFC 7230, section 3.3.3.
|
||||
"""
|
||||
@@ -113,22 +106,21 @@ def conditional_content_removal(request, response):
|
||||
if response.streaming:
|
||||
response.streaming_content = []
|
||||
else:
|
||||
response.content = b""
|
||||
if request.method == "HEAD":
|
||||
response.content = b''
|
||||
if request.method == 'HEAD':
|
||||
if response.streaming:
|
||||
response.streaming_content = []
|
||||
else:
|
||||
response.content = b""
|
||||
response.content = b''
|
||||
return response
|
||||
|
||||
|
||||
class ClientHandler(BaseHandler):
|
||||
"""
|
||||
An HTTP Handler that can be used for testing purposes. Use the WSGI
|
||||
A HTTP Handler that can be used for testing purposes. Use the WSGI
|
||||
interface to compose requests, but return the raw HttpResponse object with
|
||||
the originating WSGIRequest attached to its ``wsgi_request`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, enforce_csrf_checks=True, *args, **kwargs):
|
||||
self.enforce_csrf_checks = enforce_csrf_checks
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -152,7 +144,7 @@ class ClientHandler(BaseHandler):
|
||||
# Request goes through middleware.
|
||||
response = self.get_response(request)
|
||||
|
||||
# Simulate behaviors of most web servers.
|
||||
# Simulate behaviors of most Web servers.
|
||||
conditional_content_removal(request, response)
|
||||
|
||||
# Attach the originating request to the response so that it could be
|
||||
@@ -162,11 +154,10 @@ class ClientHandler(BaseHandler):
|
||||
# Emulate a WSGI server by calling the close method on completion.
|
||||
if response.streaming:
|
||||
response.streaming_content = closing_iterator_wrapper(
|
||||
response.streaming_content, response.close
|
||||
)
|
||||
response.streaming_content, response.close)
|
||||
else:
|
||||
request_finished.disconnect(close_old_connections)
|
||||
response.close() # will fire request_finished
|
||||
response.close() # will fire request_finished
|
||||
request_finished.connect(close_old_connections)
|
||||
|
||||
return response
|
||||
@@ -174,7 +165,6 @@ class ClientHandler(BaseHandler):
|
||||
|
||||
class AsyncClientHandler(BaseHandler):
|
||||
"""An async version of ClientHandler."""
|
||||
|
||||
def __init__(self, enforce_csrf_checks=True, *args, **kwargs):
|
||||
self.enforce_csrf_checks = enforce_csrf_checks
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -185,15 +175,13 @@ class AsyncClientHandler(BaseHandler):
|
||||
if self._middleware_chain is None:
|
||||
self.load_middleware(is_async=True)
|
||||
# Extract body file from the scope, if provided.
|
||||
if "_body_file" in scope:
|
||||
body_file = scope.pop("_body_file")
|
||||
if '_body_file' in scope:
|
||||
body_file = scope.pop('_body_file')
|
||||
else:
|
||||
body_file = FakePayload("")
|
||||
body_file = FakePayload('')
|
||||
|
||||
request_started.disconnect(close_old_connections)
|
||||
await sync_to_async(request_started.send, thread_sensitive=False)(
|
||||
sender=self.__class__, scope=scope
|
||||
)
|
||||
await sync_to_async(request_started.send, thread_sensitive=False)(sender=self.__class__, scope=scope)
|
||||
request_started.connect(close_old_connections)
|
||||
request = ASGIRequest(scope, body_file)
|
||||
# Sneaky little hack so that we can easily get round
|
||||
@@ -202,16 +190,14 @@ class AsyncClientHandler(BaseHandler):
|
||||
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||
# Request goes through middleware.
|
||||
response = await self.get_response_async(request)
|
||||
# Simulate behaviors of most web servers.
|
||||
# Simulate behaviors of most Web servers.
|
||||
conditional_content_removal(request, response)
|
||||
# Attach the originating ASGI request to the response so that it could
|
||||
# be later retrieved.
|
||||
response.asgi_request = request
|
||||
# Emulate a server by calling the close method on completion.
|
||||
if response.streaming:
|
||||
response.streaming_content = await sync_to_async(
|
||||
closing_iterator_wrapper, thread_sensitive=False
|
||||
)(
|
||||
response.streaming_content = await sync_to_async(closing_iterator_wrapper, thread_sensitive=False)(
|
||||
response.streaming_content,
|
||||
response.close,
|
||||
)
|
||||
@@ -230,10 +216,10 @@ def store_rendered_templates(store, signal, sender, template, context, **kwargs)
|
||||
The context is copied so that it is an accurate representation at the time
|
||||
of rendering.
|
||||
"""
|
||||
store.setdefault("templates", []).append(template)
|
||||
if "context" not in store:
|
||||
store["context"] = ContextList()
|
||||
store["context"].append(copy(context))
|
||||
store.setdefault('templates', []).append(template)
|
||||
if 'context' not in store:
|
||||
store['context'] = ContextList()
|
||||
store['context'].append(copy(context))
|
||||
|
||||
|
||||
def encode_multipart(boundary, data):
|
||||
@@ -269,33 +255,25 @@ def encode_multipart(boundary, data):
|
||||
if is_file(item):
|
||||
lines.extend(encode_file(boundary, key, item))
|
||||
else:
|
||||
lines.extend(
|
||||
to_bytes(val)
|
||||
for val in [
|
||||
"--%s" % boundary,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
"",
|
||||
item,
|
||||
]
|
||||
)
|
||||
lines.extend(to_bytes(val) for val in [
|
||||
'--%s' % boundary,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
item
|
||||
])
|
||||
else:
|
||||
lines.extend(
|
||||
to_bytes(val)
|
||||
for val in [
|
||||
"--%s" % boundary,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
"",
|
||||
value,
|
||||
]
|
||||
)
|
||||
lines.extend(to_bytes(val) for val in [
|
||||
'--%s' % boundary,
|
||||
'Content-Disposition: form-data; name="%s"' % key,
|
||||
'',
|
||||
value
|
||||
])
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
to_bytes("--%s--" % boundary),
|
||||
b"",
|
||||
]
|
||||
)
|
||||
return b"\r\n".join(lines)
|
||||
lines.extend([
|
||||
to_bytes('--%s--' % boundary),
|
||||
b'',
|
||||
])
|
||||
return b'\r\n'.join(lines)
|
||||
|
||||
|
||||
def encode_file(boundary, key, file):
|
||||
@@ -304,10 +282,10 @@ def encode_file(boundary, key, file):
|
||||
|
||||
# file.name might not be a string. For example, it's an int for
|
||||
# tempfile.TemporaryFile().
|
||||
file_has_string_name = hasattr(file, "name") and isinstance(file.name, str)
|
||||
filename = os.path.basename(file.name) if file_has_string_name else ""
|
||||
file_has_string_name = hasattr(file, 'name') and isinstance(file.name, str)
|
||||
filename = os.path.basename(file.name) if file_has_string_name else ''
|
||||
|
||||
if hasattr(file, "content_type"):
|
||||
if hasattr(file, 'content_type'):
|
||||
content_type = file.content_type
|
||||
elif filename:
|
||||
content_type = mimetypes.guess_type(filename)[0]
|
||||
@@ -315,16 +293,15 @@ def encode_file(boundary, key, file):
|
||||
content_type = None
|
||||
|
||||
if content_type is None:
|
||||
content_type = "application/octet-stream"
|
||||
content_type = 'application/octet-stream'
|
||||
filename = filename or key
|
||||
return [
|
||||
to_bytes("--%s" % boundary),
|
||||
to_bytes(
|
||||
'Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)
|
||||
),
|
||||
to_bytes("Content-Type: %s" % content_type),
|
||||
b"",
|
||||
to_bytes(file.read()),
|
||||
to_bytes('--%s' % boundary),
|
||||
to_bytes('Content-Disposition: form-data; name="%s"; filename="%s"'
|
||||
% (key, filename)),
|
||||
to_bytes('Content-Type: %s' % content_type),
|
||||
b'',
|
||||
to_bytes(file.read())
|
||||
]
|
||||
|
||||
|
||||
@@ -341,7 +318,6 @@ class RequestFactory:
|
||||
Once you have a request object you can pass it to any view function,
|
||||
just as if that view had been hooked up using a URLconf.
|
||||
"""
|
||||
|
||||
def __init__(self, *, json_encoder=DjangoJSONEncoder, **defaults):
|
||||
self.json_encoder = json_encoder
|
||||
self.defaults = defaults
|
||||
@@ -357,26 +333,24 @@ class RequestFactory:
|
||||
# - REMOTE_ADDR: often useful, see #8551.
|
||||
# See https://www.python.org/dev/peps/pep-3333/#environ-variables
|
||||
return {
|
||||
"HTTP_COOKIE": "; ".join(
|
||||
sorted(
|
||||
"%s=%s" % (morsel.key, morsel.coded_value)
|
||||
for morsel in self.cookies.values()
|
||||
)
|
||||
),
|
||||
"PATH_INFO": "/",
|
||||
"REMOTE_ADDR": "127.0.0.1",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"SCRIPT_NAME": "",
|
||||
"SERVER_NAME": "testserver",
|
||||
"SERVER_PORT": "80",
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
"wsgi.version": (1, 0),
|
||||
"wsgi.url_scheme": "http",
|
||||
"wsgi.input": FakePayload(b""),
|
||||
"wsgi.errors": self.errors,
|
||||
"wsgi.multiprocess": True,
|
||||
"wsgi.multithread": False,
|
||||
"wsgi.run_once": False,
|
||||
'HTTP_COOKIE': '; '.join(sorted(
|
||||
'%s=%s' % (morsel.key, morsel.coded_value)
|
||||
for morsel in self.cookies.values()
|
||||
)),
|
||||
'PATH_INFO': '/',
|
||||
'REMOTE_ADDR': '127.0.0.1',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'SCRIPT_NAME': '',
|
||||
'SERVER_NAME': 'testserver',
|
||||
'SERVER_PORT': '80',
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'wsgi.version': (1, 0),
|
||||
'wsgi.url_scheme': 'http',
|
||||
'wsgi.input': FakePayload(b''),
|
||||
'wsgi.errors': self.errors,
|
||||
'wsgi.multiprocess': True,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.run_once': False,
|
||||
**self.defaults,
|
||||
**request,
|
||||
}
|
||||
@@ -402,9 +376,7 @@ class RequestFactory:
|
||||
Return encoded JSON if data is a dict, list, or tuple and content_type
|
||||
is application/json.
|
||||
"""
|
||||
should_encode = JSON_CONTENT_TYPE_RE.match(content_type) and isinstance(
|
||||
data, (dict, list, tuple)
|
||||
)
|
||||
should_encode = JSON_CONTENT_TYPE_RE.match(content_type) and isinstance(data, (dict, list, tuple))
|
||||
return json.dumps(data, cls=self.json_encoder) if should_encode else data
|
||||
|
||||
def _get_path(self, parsed):
|
||||
@@ -416,128 +388,88 @@ class RequestFactory:
|
||||
# Replace the behavior where non-ASCII values in the WSGI environ are
|
||||
# arbitrarily decoded with ISO-8859-1.
|
||||
# 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, **extra):
|
||||
"""Construct a GET request."""
|
||||
data = {} if data is None else data
|
||||
return self.generic(
|
||||
"GET",
|
||||
path,
|
||||
secure=secure,
|
||||
**{
|
||||
"QUERY_STRING": urlencode(data, doseq=True),
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
return self.generic('GET', path, secure=secure, **{
|
||||
'QUERY_STRING': urlencode(data, doseq=True),
|
||||
**extra,
|
||||
})
|
||||
|
||||
def post(
|
||||
self, path, data=None, content_type=MULTIPART_CONTENT, secure=False, **extra
|
||||
):
|
||||
def post(self, path, data=None, content_type=MULTIPART_CONTENT,
|
||||
secure=False, **extra):
|
||||
"""Construct a POST request."""
|
||||
data = self._encode_json({} if data is None else data, content_type)
|
||||
post_data = self._encode_data(data, content_type)
|
||||
|
||||
return self.generic(
|
||||
"POST", path, post_data, content_type, secure=secure, **extra
|
||||
)
|
||||
return self.generic('POST', path, post_data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def head(self, path, data=None, secure=False, **extra):
|
||||
"""Construct a HEAD request."""
|
||||
data = {} if data is None else data
|
||||
return self.generic(
|
||||
"HEAD",
|
||||
path,
|
||||
secure=secure,
|
||||
**{
|
||||
"QUERY_STRING": urlencode(data, doseq=True),
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
return self.generic('HEAD', path, secure=secure, **{
|
||||
'QUERY_STRING': urlencode(data, doseq=True),
|
||||
**extra,
|
||||
})
|
||||
|
||||
def trace(self, path, secure=False, **extra):
|
||||
"""Construct a TRACE request."""
|
||||
return self.generic("TRACE", path, secure=secure, **extra)
|
||||
return self.generic('TRACE', path, secure=secure, **extra)
|
||||
|
||||
def options(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def options(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"Construct an OPTIONS request."
|
||||
return self.generic("OPTIONS", path, data, content_type, secure=secure, **extra)
|
||||
return self.generic('OPTIONS', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def put(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def put(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a PUT request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic("PUT", path, data, content_type, secure=secure, **extra)
|
||||
return self.generic('PUT', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def patch(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def patch(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a PATCH request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic("PATCH", path, data, content_type, secure=secure, **extra)
|
||||
return self.generic('PATCH', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def delete(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a DELETE request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic("DELETE", path, data, content_type, secure=secure, **extra)
|
||||
return self.generic('DELETE', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def generic(
|
||||
self,
|
||||
method,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def generic(self, method, path, data='',
|
||||
content_type='application/octet-stream', secure=False,
|
||||
**extra):
|
||||
"""Construct an arbitrary HTTP request."""
|
||||
parsed = urlparse(str(path)) # path can be lazy
|
||||
data = force_bytes(data, settings.DEFAULT_CHARSET)
|
||||
r = {
|
||||
"PATH_INFO": self._get_path(parsed),
|
||||
"REQUEST_METHOD": method,
|
||||
"SERVER_PORT": "443" if secure else "80",
|
||||
"wsgi.url_scheme": "https" if secure else "http",
|
||||
'PATH_INFO': self._get_path(parsed),
|
||||
'REQUEST_METHOD': method,
|
||||
'SERVER_PORT': '443' if secure else '80',
|
||||
'wsgi.url_scheme': 'https' if secure else 'http',
|
||||
}
|
||||
if data:
|
||||
r.update(
|
||||
{
|
||||
"CONTENT_LENGTH": str(len(data)),
|
||||
"CONTENT_TYPE": content_type,
|
||||
"wsgi.input": FakePayload(data),
|
||||
}
|
||||
)
|
||||
r.update({
|
||||
'CONTENT_LENGTH': str(len(data)),
|
||||
'CONTENT_TYPE': content_type,
|
||||
'wsgi.input': FakePayload(data),
|
||||
})
|
||||
r.update(extra)
|
||||
# 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'):
|
||||
# WSGI requires latin-1 encoded strings. See get_path_info().
|
||||
query_string = parsed[4].encode().decode("iso-8859-1")
|
||||
r["QUERY_STRING"] = query_string
|
||||
query_string = parsed[4].encode().decode('iso-8859-1')
|
||||
r['QUERY_STRING'] = query_string
|
||||
return self.request(**r)
|
||||
|
||||
|
||||
@@ -555,35 +487,30 @@ class AsyncRequestFactory(RequestFactory):
|
||||
a) this makes ASGIRequest subclasses, and
|
||||
b) AsyncTestClient can subclass it.
|
||||
"""
|
||||
|
||||
def _base_scope(self, **request):
|
||||
"""The base scope for a request."""
|
||||
# This is a minimal valid ASGI scope, plus:
|
||||
# - headers['cookie'] for cookie support,
|
||||
# - 'client' often useful, see #8551.
|
||||
scope = {
|
||||
"asgi": {"version": "3.0"},
|
||||
"type": "http",
|
||||
"http_version": "1.1",
|
||||
"client": ["127.0.0.1", 0],
|
||||
"server": ("testserver", "80"),
|
||||
"scheme": "http",
|
||||
"method": "GET",
|
||||
"headers": [],
|
||||
'asgi': {'version': '3.0'},
|
||||
'type': 'http',
|
||||
'http_version': '1.1',
|
||||
'client': ['127.0.0.1', 0],
|
||||
'server': ('testserver', '80'),
|
||||
'scheme': 'http',
|
||||
'method': 'GET',
|
||||
'headers': [],
|
||||
**self.defaults,
|
||||
**request,
|
||||
}
|
||||
scope["headers"].append(
|
||||
(
|
||||
b"cookie",
|
||||
b"; ".join(
|
||||
sorted(
|
||||
("%s=%s" % (morsel.key, morsel.coded_value)).encode("ascii")
|
||||
for morsel in self.cookies.values()
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
scope['headers'].append((
|
||||
b'cookie',
|
||||
b'; '.join(sorted(
|
||||
('%s=%s' % (morsel.key, morsel.coded_value)).encode('ascii')
|
||||
for morsel in self.cookies.values()
|
||||
)),
|
||||
))
|
||||
return scope
|
||||
|
||||
def request(self, **request):
|
||||
@@ -591,52 +518,43 @@ class AsyncRequestFactory(RequestFactory):
|
||||
# This is synchronous, which means all methods on this class are.
|
||||
# AsyncClient, however, has an async request function, which makes all
|
||||
# its methods async.
|
||||
if "_body_file" in request:
|
||||
body_file = request.pop("_body_file")
|
||||
if '_body_file' in request:
|
||||
body_file = request.pop('_body_file')
|
||||
else:
|
||||
body_file = FakePayload("")
|
||||
body_file = FakePayload('')
|
||||
return ASGIRequest(self._base_scope(**request), body_file)
|
||||
|
||||
def generic(
|
||||
self,
|
||||
method,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
secure=False,
|
||||
**extra,
|
||||
self, method, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra,
|
||||
):
|
||||
"""Construct an arbitrary HTTP request."""
|
||||
parsed = urlparse(str(path)) # path can be lazy.
|
||||
data = force_bytes(data, settings.DEFAULT_CHARSET)
|
||||
s = {
|
||||
"method": method,
|
||||
"path": self._get_path(parsed),
|
||||
"server": ("127.0.0.1", "443" if secure else "80"),
|
||||
"scheme": "https" if secure else "http",
|
||||
"headers": [(b"host", b"testserver")],
|
||||
'method': method,
|
||||
'path': self._get_path(parsed),
|
||||
'server': ('127.0.0.1', '443' if secure else '80'),
|
||||
'scheme': 'https' if secure else 'http',
|
||||
'headers': [(b'host', b'testserver')],
|
||||
}
|
||||
if data:
|
||||
s["headers"].extend(
|
||||
[
|
||||
(b"content-length", str(len(data)).encode("ascii")),
|
||||
(b"content-type", content_type.encode("ascii")),
|
||||
]
|
||||
)
|
||||
s["_body_file"] = FakePayload(data)
|
||||
follow = extra.pop("follow", None)
|
||||
s['headers'].extend([
|
||||
(b'content-length', str(len(data)).encode('ascii')),
|
||||
(b'content-type', content_type.encode('ascii')),
|
||||
])
|
||||
s['_body_file'] = FakePayload(data)
|
||||
follow = extra.pop('follow', None)
|
||||
if follow is not None:
|
||||
s["follow"] = follow
|
||||
if query_string := extra.pop("QUERY_STRING", None):
|
||||
s["query_string"] = query_string
|
||||
s["headers"] += [
|
||||
(key.lower().encode("ascii"), value.encode("latin1"))
|
||||
s['follow'] = follow
|
||||
s['headers'] += [
|
||||
(key.lower().encode('ascii'), value.encode('latin1'))
|
||||
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]
|
||||
if not s.get('query_string'):
|
||||
s['query_string'] = parsed[4]
|
||||
return self.request(**s)
|
||||
|
||||
|
||||
@@ -644,7 +562,6 @@ class ClientMixin:
|
||||
"""
|
||||
Mixin with common methods between Client and AsyncClient.
|
||||
"""
|
||||
|
||||
def store_exc_info(self, **kwargs):
|
||||
"""Store exceptions when they are generated by a view."""
|
||||
self.exc_info = sys.exc_info()
|
||||
@@ -682,7 +599,6 @@ class ClientMixin:
|
||||
are incorrect.
|
||||
"""
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
user = authenticate(**credentials)
|
||||
if user:
|
||||
self._login(user)
|
||||
@@ -692,10 +608,9 @@ class ClientMixin:
|
||||
def force_login(self, user, backend=None):
|
||||
def get_backend():
|
||||
from django.contrib.auth import load_backend
|
||||
|
||||
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
||||
backend = load_backend(backend_path)
|
||||
if hasattr(backend, "get_user"):
|
||||
if hasattr(backend, 'get_user'):
|
||||
return backend_path
|
||||
|
||||
if backend is None:
|
||||
@@ -720,18 +635,17 @@ class ClientMixin:
|
||||
session_cookie = settings.SESSION_COOKIE_NAME
|
||||
self.cookies[session_cookie] = request.session.session_key
|
||||
cookie_data = {
|
||||
"max-age": None,
|
||||
"path": "/",
|
||||
"domain": settings.SESSION_COOKIE_DOMAIN,
|
||||
"secure": settings.SESSION_COOKIE_SECURE or None,
|
||||
"expires": None,
|
||||
'max-age': None,
|
||||
'path': '/',
|
||||
'domain': settings.SESSION_COOKIE_DOMAIN,
|
||||
'secure': settings.SESSION_COOKIE_SECURE or None,
|
||||
'expires': None,
|
||||
}
|
||||
self.cookies[session_cookie].update(cookie_data)
|
||||
|
||||
def logout(self):
|
||||
"""Log out the user by removing the cookies and session object."""
|
||||
from django.contrib.auth import get_user, logout
|
||||
|
||||
request = HttpRequest()
|
||||
if self.session:
|
||||
request.session = self.session
|
||||
@@ -743,15 +657,13 @@ class ClientMixin:
|
||||
self.cookies = SimpleCookie()
|
||||
|
||||
def _parse_json(self, response, **extra):
|
||||
if not hasattr(response, "_json"):
|
||||
if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")):
|
||||
if not hasattr(response, '_json'):
|
||||
if not JSON_CONTENT_TYPE_RE.match(response.get('Content-Type')):
|
||||
raise ValueError(
|
||||
'Content-Type header is "%s", not "application/json"'
|
||||
% response.get("Content-Type")
|
||||
% response.get('Content-Type')
|
||||
)
|
||||
response._json = json.loads(
|
||||
response.content.decode(response.charset), **extra
|
||||
)
|
||||
response._json = json.loads(response.content.decode(response.charset), **extra)
|
||||
return response._json
|
||||
|
||||
|
||||
@@ -773,10 +685,7 @@ class Client(ClientMixin, RequestFactory):
|
||||
contexts and templates produced by a view, rather than the
|
||||
HTML rendered to the end-user.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, enforce_csrf_checks=False, raise_request_exception=True, **defaults
|
||||
):
|
||||
def __init__(self, enforce_csrf_checks=False, raise_request_exception=True, **defaults):
|
||||
super().__init__(**defaults)
|
||||
self.handler = ClientHandler(enforce_csrf_checks)
|
||||
self.raise_request_exception = raise_request_exception
|
||||
@@ -812,14 +721,11 @@ class Client(ClientMixin, RequestFactory):
|
||||
response.client = self
|
||||
response.request = request
|
||||
# Add any rendered template detail to the response.
|
||||
response.templates = data.get("templates", [])
|
||||
response.context = data.get("context")
|
||||
response.templates = data.get('templates', [])
|
||||
response.context = data.get('context')
|
||||
response.json = partial(self._parse_json, response)
|
||||
# Attach the ResolverMatch instance to the response.
|
||||
urlconf = getattr(response.wsgi_request, "urlconf", None)
|
||||
response.resolver_match = SimpleLazyObject(
|
||||
lambda: resolve(request["PATH_INFO"], urlconf=urlconf),
|
||||
)
|
||||
response.resolver_match = SimpleLazyObject(lambda: resolve(request['PATH_INFO']))
|
||||
# Flatten a single context. Not really necessary anymore thanks to the
|
||||
# __getattr__ flattening in ContextList, but has some edge case
|
||||
# backwards compatibility implications.
|
||||
@@ -838,24 +744,13 @@ class Client(ClientMixin, RequestFactory):
|
||||
response = self._handle_redirects(response, data=data, **extra)
|
||||
return response
|
||||
|
||||
def post(
|
||||
self,
|
||||
path,
|
||||
data=None,
|
||||
content_type=MULTIPART_CONTENT,
|
||||
follow=False,
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def post(self, path, data=None, content_type=MULTIPART_CONTENT,
|
||||
follow=False, secure=False, **extra):
|
||||
"""Request a response from the server using POST."""
|
||||
self.extra = extra
|
||||
response = super().post(
|
||||
path, data=data, content_type=content_type, secure=secure, **extra
|
||||
)
|
||||
response = super().post(path, data=data, content_type=content_type, secure=secure, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(
|
||||
response, data=data, content_type=content_type, **extra
|
||||
)
|
||||
response = self._handle_redirects(response, data=data, content_type=content_type, **extra)
|
||||
return response
|
||||
|
||||
def head(self, path, data=None, follow=False, secure=False, **extra):
|
||||
@@ -866,87 +761,43 @@ class Client(ClientMixin, RequestFactory):
|
||||
response = self._handle_redirects(response, data=data, **extra)
|
||||
return response
|
||||
|
||||
def options(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
follow=False,
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def options(self, path, data='', content_type='application/octet-stream',
|
||||
follow=False, secure=False, **extra):
|
||||
"""Request a response from the server using OPTIONS."""
|
||||
self.extra = extra
|
||||
response = super().options(
|
||||
path, data=data, content_type=content_type, secure=secure, **extra
|
||||
)
|
||||
response = super().options(path, data=data, content_type=content_type, secure=secure, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(
|
||||
response, data=data, content_type=content_type, **extra
|
||||
)
|
||||
response = self._handle_redirects(response, data=data, content_type=content_type, **extra)
|
||||
return response
|
||||
|
||||
def put(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
follow=False,
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def put(self, path, data='', content_type='application/octet-stream',
|
||||
follow=False, secure=False, **extra):
|
||||
"""Send a resource to the server using PUT."""
|
||||
self.extra = extra
|
||||
response = super().put(
|
||||
path, data=data, content_type=content_type, secure=secure, **extra
|
||||
)
|
||||
response = super().put(path, data=data, content_type=content_type, secure=secure, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(
|
||||
response, data=data, content_type=content_type, **extra
|
||||
)
|
||||
response = self._handle_redirects(response, data=data, content_type=content_type, **extra)
|
||||
return response
|
||||
|
||||
def patch(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
follow=False,
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def patch(self, path, data='', content_type='application/octet-stream',
|
||||
follow=False, secure=False, **extra):
|
||||
"""Send a resource to the server using PATCH."""
|
||||
self.extra = extra
|
||||
response = super().patch(
|
||||
path, data=data, content_type=content_type, secure=secure, **extra
|
||||
)
|
||||
response = super().patch(path, data=data, content_type=content_type, secure=secure, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(
|
||||
response, data=data, content_type=content_type, **extra
|
||||
)
|
||||
response = self._handle_redirects(response, data=data, content_type=content_type, **extra)
|
||||
return response
|
||||
|
||||
def delete(
|
||||
self,
|
||||
path,
|
||||
data="",
|
||||
content_type="application/octet-stream",
|
||||
follow=False,
|
||||
secure=False,
|
||||
**extra,
|
||||
):
|
||||
def delete(self, path, data='', content_type='application/octet-stream',
|
||||
follow=False, secure=False, **extra):
|
||||
"""Send a DELETE request to the server."""
|
||||
self.extra = extra
|
||||
response = super().delete(
|
||||
path, data=data, content_type=content_type, secure=secure, **extra
|
||||
)
|
||||
response = super().delete(path, data=data, content_type=content_type, secure=secure, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(
|
||||
response, data=data, content_type=content_type, **extra
|
||||
)
|
||||
response = self._handle_redirects(response, data=data, content_type=content_type, **extra)
|
||||
return response
|
||||
|
||||
def trace(self, path, data="", follow=False, secure=False, **extra):
|
||||
def trace(self, path, data='', follow=False, secure=False, **extra):
|
||||
"""Send a TRACE request to the server."""
|
||||
self.extra = extra
|
||||
response = super().trace(path, data=data, secure=secure, **extra)
|
||||
@@ -954,7 +805,7 @@ class Client(ClientMixin, RequestFactory):
|
||||
response = self._handle_redirects(response, data=data, **extra)
|
||||
return response
|
||||
|
||||
def _handle_redirects(self, response, data="", content_type="", **extra):
|
||||
def _handle_redirects(self, response, data='', content_type='', **extra):
|
||||
"""
|
||||
Follow any redirects by requesting responses from the server using GET.
|
||||
"""
|
||||
@@ -973,46 +824,36 @@ class Client(ClientMixin, RequestFactory):
|
||||
|
||||
url = urlsplit(response_url)
|
||||
if url.scheme:
|
||||
extra["wsgi.url_scheme"] = url.scheme
|
||||
extra['wsgi.url_scheme'] = url.scheme
|
||||
if url.hostname:
|
||||
extra["SERVER_NAME"] = url.hostname
|
||||
extra['SERVER_NAME'] = url.hostname
|
||||
if url.port:
|
||||
extra["SERVER_PORT"] = str(url.port)
|
||||
extra['SERVER_PORT'] = str(url.port)
|
||||
|
||||
path = url.path
|
||||
# RFC 2616: bare domains without path are treated as the root.
|
||||
if not path and url.netloc:
|
||||
path = "/"
|
||||
# Prepend the request path to handle relative path redirects
|
||||
if not path.startswith("/"):
|
||||
path = urljoin(response.request["PATH_INFO"], path)
|
||||
path = url.path
|
||||
if not path.startswith('/'):
|
||||
path = urljoin(response.request['PATH_INFO'], path)
|
||||
|
||||
if response.status_code in (
|
||||
HTTPStatus.TEMPORARY_REDIRECT,
|
||||
HTTPStatus.PERMANENT_REDIRECT,
|
||||
):
|
||||
if response.status_code in (HTTPStatus.TEMPORARY_REDIRECT, HTTPStatus.PERMANENT_REDIRECT):
|
||||
# Preserve request method and query string (if needed)
|
||||
# post-redirect for 307/308 responses.
|
||||
request_method = response.request["REQUEST_METHOD"].lower()
|
||||
if request_method not in ("get", "head"):
|
||||
extra["QUERY_STRING"] = url.query
|
||||
request_method = response.request['REQUEST_METHOD'].lower()
|
||||
if request_method not in ('get', 'head'):
|
||||
extra['QUERY_STRING'] = url.query
|
||||
request_method = getattr(self, request_method)
|
||||
else:
|
||||
request_method = self.get
|
||||
data = QueryDict(url.query)
|
||||
content_type = None
|
||||
|
||||
response = request_method(
|
||||
path, data=data, content_type=content_type, follow=False, **extra
|
||||
)
|
||||
response = request_method(path, data=data, content_type=content_type, follow=False, **extra)
|
||||
response.redirect_chain = redirect_chain
|
||||
|
||||
if redirect_chain[-1] in redirect_chain[:-1]:
|
||||
# Check that we're not redirecting to somewhere we've already
|
||||
# been to, to prevent loops.
|
||||
raise RedirectCycleError(
|
||||
"Redirect loop detected.", last_response=response
|
||||
)
|
||||
raise RedirectCycleError("Redirect loop detected.", last_response=response)
|
||||
if len(redirect_chain) > 20:
|
||||
# Such a lengthy chain likely also means a loop, but one with
|
||||
# a growing path, changing view, or changing query argument;
|
||||
@@ -1029,10 +870,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
|
||||
|
||||
Does not currently support "follow" on its methods.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, enforce_csrf_checks=False, raise_request_exception=True, **defaults
|
||||
):
|
||||
def __init__(self, enforce_csrf_checks=False, raise_request_exception=True, **defaults):
|
||||
super().__init__(**defaults)
|
||||
self.handler = AsyncClientHandler(enforce_csrf_checks)
|
||||
self.raise_request_exception = raise_request_exception
|
||||
@@ -1046,19 +884,20 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
|
||||
query environment, which can be overridden using the arguments to the
|
||||
request.
|
||||
"""
|
||||
if "follow" in request:
|
||||
if 'follow' in request:
|
||||
raise NotImplementedError(
|
||||
"AsyncClient request methods do not accept the follow parameter."
|
||||
'AsyncClient request methods do not accept the follow '
|
||||
'parameter.'
|
||||
)
|
||||
scope = self._base_scope(**request)
|
||||
# Curry a data dictionary into an instance of the template renderer
|
||||
# callback function.
|
||||
data = {}
|
||||
on_template_render = partial(store_rendered_templates, data)
|
||||
signal_uid = "template-render-%s" % id(request)
|
||||
signal_uid = 'template-render-%s' % id(request)
|
||||
signals.template_rendered.connect(on_template_render, dispatch_uid=signal_uid)
|
||||
# Capture exceptions created by the handler.
|
||||
exception_uid = "request-exception-%s" % id(request)
|
||||
exception_uid = 'request-exception-%s' % id(request)
|
||||
got_request_exception.connect(self.store_exc_info, dispatch_uid=exception_uid)
|
||||
try:
|
||||
response = await self.handler(scope)
|
||||
@@ -1071,14 +910,11 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
|
||||
response.client = self
|
||||
response.request = request
|
||||
# Add any rendered template detail to the response.
|
||||
response.templates = data.get("templates", [])
|
||||
response.context = data.get("context")
|
||||
response.templates = data.get('templates', [])
|
||||
response.context = data.get('context')
|
||||
response.json = partial(self._parse_json, response)
|
||||
# Attach the ResolverMatch instance to the response.
|
||||
urlconf = getattr(response.asgi_request, "urlconf", None)
|
||||
response.resolver_match = SimpleLazyObject(
|
||||
lambda: resolve(request["path"], urlconf=urlconf),
|
||||
)
|
||||
response.resolver_match = SimpleLazyObject(lambda: resolve(request['path']))
|
||||
# Flatten a single context. Not really necessary anymore thanks to the
|
||||
# __getattr__ flattening in ContextList, but has some edge case
|
||||
# backwards compatibility implications.
|
||||
|
||||
Reference in New Issue
Block a user