plain 0.66.0__py3-none-any.whl → 0.101.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/CHANGELOG.md +684 -0
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -13
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +56 -40
- plain/urls/resolvers.py +38 -28
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/test/client.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
|
-
import sys
|
|
3
|
-
from functools import partial
|
|
4
4
|
from http import HTTPStatus
|
|
5
5
|
from http.cookies import SimpleCookie
|
|
6
6
|
from io import BytesIO, IOBase
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
|
8
9
|
|
|
9
|
-
from plain.http import
|
|
10
|
+
from plain.http import QueryDict, RequestHeaders
|
|
10
11
|
from plain.internal import internalcode
|
|
11
12
|
from plain.internal.handlers.base import BaseHandler
|
|
12
13
|
from plain.internal.handlers.wsgi import WSGIRequest
|
|
13
14
|
from plain.json import PlainJSONEncoder
|
|
14
15
|
from plain.runtime import settings
|
|
15
|
-
from plain.signals import
|
|
16
|
+
from plain.signals import request_started
|
|
16
17
|
from plain.urls import get_resolver
|
|
17
18
|
from plain.utils.encoding import force_bytes
|
|
18
19
|
from plain.utils.functional import SimpleLazyObject
|
|
@@ -22,8 +23,13 @@ from plain.utils.regex_helper import _lazy_re_compile
|
|
|
22
23
|
from .encoding import encode_multipart
|
|
23
24
|
from .exceptions import RedirectCycleError
|
|
24
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from plain.http import ResponseBase
|
|
28
|
+
from plain.urls import ResolverMatch
|
|
29
|
+
|
|
25
30
|
__all__ = (
|
|
26
31
|
"Client",
|
|
32
|
+
"ClientResponse",
|
|
27
33
|
"RequestFactory",
|
|
28
34
|
)
|
|
29
35
|
|
|
@@ -35,6 +41,77 @@ _CONTENT_TYPE_RE = _lazy_re_compile(r".*; charset=([\w-]+);?")
|
|
|
35
41
|
_JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
|
|
36
42
|
|
|
37
43
|
|
|
44
|
+
class ClientResponse:
|
|
45
|
+
"""
|
|
46
|
+
Response wrapper returned by test Client with test-specific attributes.
|
|
47
|
+
|
|
48
|
+
Wraps any ResponseBase subclass and adds attributes useful for testing,
|
|
49
|
+
while delegating all other attribute access to the wrapped response.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
response: ResponseBase,
|
|
55
|
+
client: Client,
|
|
56
|
+
request: dict[str, Any],
|
|
57
|
+
):
|
|
58
|
+
# Store wrapped response in __dict__ directly to avoid __setattr__ recursion
|
|
59
|
+
object.__setattr__(self, "_response", response)
|
|
60
|
+
object.__setattr__(self, "_json_cache", None)
|
|
61
|
+
# Test-specific attributes
|
|
62
|
+
self.client = client
|
|
63
|
+
self.request = request
|
|
64
|
+
self.wsgi_request: WSGIRequest
|
|
65
|
+
self.redirect_chain: list[tuple[str, int]]
|
|
66
|
+
self.resolver_match: SimpleLazyObject | ResolverMatch
|
|
67
|
+
# Optional: set by plain.auth if available
|
|
68
|
+
# self.user: Model
|
|
69
|
+
|
|
70
|
+
def json(self, **extra: Any) -> Any:
|
|
71
|
+
"""Parse response content as JSON."""
|
|
72
|
+
_json_cache = object.__getattribute__(self, "_json_cache")
|
|
73
|
+
if _json_cache is None:
|
|
74
|
+
response = object.__getattribute__(self, "_response")
|
|
75
|
+
content_type = response.headers.get("Content-Type", "")
|
|
76
|
+
if not _JSON_CONTENT_TYPE_RE.match(content_type):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f'Content-Type header is "{content_type}", not "application/json"'
|
|
79
|
+
)
|
|
80
|
+
_json_cache = json.loads(
|
|
81
|
+
response.content.decode(response.charset),
|
|
82
|
+
**extra,
|
|
83
|
+
)
|
|
84
|
+
object.__setattr__(self, "_json_cache", _json_cache)
|
|
85
|
+
return _json_cache
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def url(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Return redirect URL if this is a redirect response.
|
|
91
|
+
|
|
92
|
+
This property exists on RedirectResponse and is added for redirects.
|
|
93
|
+
"""
|
|
94
|
+
response = object.__getattribute__(self, "_response")
|
|
95
|
+
if hasattr(response, "url"):
|
|
96
|
+
return response.url
|
|
97
|
+
# For non-redirect responses, try to get Location header
|
|
98
|
+
if "Location" in response.headers:
|
|
99
|
+
return response.headers["Location"]
|
|
100
|
+
raise AttributeError(f"{response.__class__.__name__} has no attribute 'url'")
|
|
101
|
+
|
|
102
|
+
def __getattr__(self, name: str) -> Any:
|
|
103
|
+
"""Delegate attribute access to the wrapped response."""
|
|
104
|
+
return getattr(object.__getattribute__(self, "_response"), name)
|
|
105
|
+
|
|
106
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
107
|
+
"""Set attributes on the wrapper itself."""
|
|
108
|
+
object.__setattr__(self, name, value)
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
"""Return repr of wrapped response."""
|
|
112
|
+
return repr(object.__getattribute__(self, "_response"))
|
|
113
|
+
|
|
114
|
+
|
|
38
115
|
@internalcode
|
|
39
116
|
class FakePayload(IOBase):
|
|
40
117
|
"""
|
|
@@ -44,17 +121,17 @@ class FakePayload(IOBase):
|
|
|
44
121
|
that wouldn't work in real life.
|
|
45
122
|
"""
|
|
46
123
|
|
|
47
|
-
def __init__(self, initial_bytes=None):
|
|
124
|
+
def __init__(self, initial_bytes: bytes | None = None) -> None:
|
|
48
125
|
self.__content = BytesIO()
|
|
49
126
|
self.__len = 0
|
|
50
127
|
self.read_started = False
|
|
51
128
|
if initial_bytes is not None:
|
|
52
129
|
self.write(initial_bytes)
|
|
53
130
|
|
|
54
|
-
def __len__(self):
|
|
131
|
+
def __len__(self) -> int:
|
|
55
132
|
return self.__len
|
|
56
133
|
|
|
57
|
-
def read(self, size
|
|
134
|
+
def read(self, size: int = -1, /) -> bytes:
|
|
58
135
|
if not self.read_started:
|
|
59
136
|
self.__content.seek(0)
|
|
60
137
|
self.read_started = True
|
|
@@ -67,11 +144,11 @@ class FakePayload(IOBase):
|
|
|
67
144
|
self.__len -= len(content)
|
|
68
145
|
return content
|
|
69
146
|
|
|
70
|
-
def readline(self, size
|
|
147
|
+
def readline(self, size: int | None = -1, /) -> bytes:
|
|
71
148
|
if not self.read_started:
|
|
72
149
|
self.__content.seek(0)
|
|
73
150
|
self.read_started = True
|
|
74
|
-
if size
|
|
151
|
+
if size is None or size == -1:
|
|
75
152
|
size = self.__len
|
|
76
153
|
assert self.__len >= size, (
|
|
77
154
|
"Cannot read more than the available bytes from the HTTP incoming data."
|
|
@@ -80,7 +157,7 @@ class FakePayload(IOBase):
|
|
|
80
157
|
self.__len -= len(content)
|
|
81
158
|
return content
|
|
82
159
|
|
|
83
|
-
def write(self, b, /):
|
|
160
|
+
def write(self, b: bytes | str, /) -> None:
|
|
84
161
|
if self.read_started:
|
|
85
162
|
raise ValueError("Unable to write a payload after it's been read")
|
|
86
163
|
content = force_bytes(b)
|
|
@@ -88,25 +165,28 @@ class FakePayload(IOBase):
|
|
|
88
165
|
self.__len += len(content)
|
|
89
166
|
|
|
90
167
|
|
|
91
|
-
def _conditional_content_removal(
|
|
168
|
+
def _conditional_content_removal(
|
|
169
|
+
request: WSGIRequest, response: ResponseBase
|
|
170
|
+
) -> ResponseBase:
|
|
92
171
|
"""
|
|
93
172
|
Simulate the behavior of most web servers by removing the content of
|
|
94
173
|
responses for HEAD requests, 1xx, 204, and 304 responses. Ensure
|
|
95
174
|
compliance with RFC 9112 Section 6.3.
|
|
96
175
|
"""
|
|
97
176
|
if 100 <= response.status_code < 200 or response.status_code in (204, 304):
|
|
98
|
-
if response.streaming:
|
|
99
|
-
response.streaming_content = []
|
|
177
|
+
if response.streaming: # type: ignore[attr-defined]
|
|
178
|
+
response.streaming_content = [] # type: ignore[attr-defined]
|
|
100
179
|
else:
|
|
101
|
-
response.content = b""
|
|
180
|
+
response.content = b"" # type: ignore[attr-defined]
|
|
102
181
|
if request.method == "HEAD":
|
|
103
|
-
if response.streaming:
|
|
104
|
-
response.streaming_content = []
|
|
182
|
+
if response.streaming: # type: ignore[attr-defined]
|
|
183
|
+
response.streaming_content = [] # type: ignore[attr-defined]
|
|
105
184
|
else:
|
|
106
|
-
response.content = b""
|
|
185
|
+
response.content = b"" # type: ignore[attr-defined]
|
|
107
186
|
return response
|
|
108
187
|
|
|
109
188
|
|
|
189
|
+
@internalcode
|
|
110
190
|
class ClientHandler(BaseHandler):
|
|
111
191
|
"""
|
|
112
192
|
An HTTP Handler that can be used for testing purposes. Use the WSGI
|
|
@@ -114,10 +194,10 @@ class ClientHandler(BaseHandler):
|
|
|
114
194
|
the originating WSGIRequest attached to its ``wsgi_request`` attribute.
|
|
115
195
|
"""
|
|
116
196
|
|
|
117
|
-
def __init__(self, *args, **kwargs):
|
|
197
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
118
198
|
super().__init__(*args, **kwargs)
|
|
119
199
|
|
|
120
|
-
def __call__(self, environ):
|
|
200
|
+
def __call__(self, environ: dict[str, Any]) -> ResponseBase:
|
|
121
201
|
# Set up middleware if needed. We couldn't do this earlier, because
|
|
122
202
|
# settings weren't available.
|
|
123
203
|
if self._middleware_chain is None:
|
|
@@ -134,7 +214,7 @@ class ClientHandler(BaseHandler):
|
|
|
134
214
|
|
|
135
215
|
# Attach the originating request to the response so that it could be
|
|
136
216
|
# later retrieved.
|
|
137
|
-
response.wsgi_request = request
|
|
217
|
+
response.wsgi_request = request # type: ignore[attr-defined]
|
|
138
218
|
|
|
139
219
|
# Emulate a WSGI server by calling the close method on completion.
|
|
140
220
|
response.close()
|
|
@@ -156,15 +236,21 @@ class RequestFactory:
|
|
|
156
236
|
just as if that view had been hooked up using a urlrouter.
|
|
157
237
|
"""
|
|
158
238
|
|
|
159
|
-
def __init__(
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
json_encoder: type[json.JSONEncoder] = PlainJSONEncoder,
|
|
243
|
+
headers: dict[str, str] | None = None,
|
|
244
|
+
**defaults: Any,
|
|
245
|
+
) -> None:
|
|
160
246
|
self.json_encoder = json_encoder
|
|
161
|
-
self.defaults = defaults
|
|
162
|
-
self.cookies = SimpleCookie()
|
|
247
|
+
self.defaults: dict[str, Any] = defaults
|
|
248
|
+
self.cookies: SimpleCookie[str] = SimpleCookie()
|
|
163
249
|
self.errors = BytesIO()
|
|
164
250
|
if headers:
|
|
165
|
-
self.defaults.update(
|
|
251
|
+
self.defaults.update(RequestHeaders.to_wsgi_names(headers))
|
|
166
252
|
|
|
167
|
-
def _base_environ(self, **request):
|
|
253
|
+
def _base_environ(self, **request: Any) -> dict[str, Any]:
|
|
168
254
|
"""
|
|
169
255
|
The base environment for a request.
|
|
170
256
|
"""
|
|
@@ -197,13 +283,13 @@ class RequestFactory:
|
|
|
197
283
|
**request,
|
|
198
284
|
}
|
|
199
285
|
|
|
200
|
-
def request(self, **request):
|
|
286
|
+
def request(self, **request: Any) -> WSGIRequest:
|
|
201
287
|
"Construct a generic request object."
|
|
202
288
|
return WSGIRequest(self._base_environ(**request))
|
|
203
289
|
|
|
204
|
-
def _encode_data(self, data, content_type):
|
|
290
|
+
def _encode_data(self, data: dict[str, Any] | str, content_type: str) -> bytes:
|
|
205
291
|
if content_type is _MULTIPART_CONTENT:
|
|
206
|
-
return encode_multipart(_BOUNDARY, data)
|
|
292
|
+
return encode_multipart(_BOUNDARY, data) # type: ignore[arg-type]
|
|
207
293
|
else:
|
|
208
294
|
# Encode the content so that the byte representation is correct.
|
|
209
295
|
match = _CONTENT_TYPE_RE.match(content_type)
|
|
@@ -213,7 +299,7 @@ class RequestFactory:
|
|
|
213
299
|
charset = settings.DEFAULT_CHARSET
|
|
214
300
|
return force_bytes(data, encoding=charset)
|
|
215
301
|
|
|
216
|
-
def _encode_json(self, data, content_type):
|
|
302
|
+
def _encode_json(self, data: Any, content_type: str) -> Any:
|
|
217
303
|
"""
|
|
218
304
|
Return encoded JSON if data is a dict, list, or tuple and content_type
|
|
219
305
|
is application/json.
|
|
@@ -223,7 +309,7 @@ class RequestFactory:
|
|
|
223
309
|
)
|
|
224
310
|
return json.dumps(data, cls=self.json_encoder) if should_encode else data
|
|
225
311
|
|
|
226
|
-
def _get_path(self, parsed):
|
|
312
|
+
def _get_path(self, parsed: Any) -> str:
|
|
227
313
|
path = parsed.path
|
|
228
314
|
# If there are parameters, add them
|
|
229
315
|
if parsed.params:
|
|
@@ -234,7 +320,15 @@ class RequestFactory:
|
|
|
234
320
|
# Refs comment in `get_bytes_from_wsgi()`.
|
|
235
321
|
return path.decode("iso-8859-1")
|
|
236
322
|
|
|
237
|
-
def get(
|
|
323
|
+
def get(
|
|
324
|
+
self,
|
|
325
|
+
path: str,
|
|
326
|
+
data: dict[str, Any] | None = None,
|
|
327
|
+
secure: bool = True,
|
|
328
|
+
*,
|
|
329
|
+
headers: dict[str, str] | None = None,
|
|
330
|
+
**extra: Any,
|
|
331
|
+
) -> WSGIRequest:
|
|
238
332
|
"""Construct a GET request."""
|
|
239
333
|
data = {} if data is None else data
|
|
240
334
|
return self.generic(
|
|
@@ -250,14 +344,14 @@ class RequestFactory:
|
|
|
250
344
|
|
|
251
345
|
def post(
|
|
252
346
|
self,
|
|
253
|
-
path,
|
|
254
|
-
data=None,
|
|
255
|
-
content_type=_MULTIPART_CONTENT,
|
|
256
|
-
secure=True,
|
|
347
|
+
path: str,
|
|
348
|
+
data: dict[str, Any] | None = None,
|
|
349
|
+
content_type: str = _MULTIPART_CONTENT,
|
|
350
|
+
secure: bool = True,
|
|
257
351
|
*,
|
|
258
|
-
headers=None,
|
|
259
|
-
**extra,
|
|
260
|
-
):
|
|
352
|
+
headers: dict[str, str] | None = None,
|
|
353
|
+
**extra: Any,
|
|
354
|
+
) -> WSGIRequest:
|
|
261
355
|
"""Construct a POST request."""
|
|
262
356
|
data = self._encode_json({} if data is None else data, content_type)
|
|
263
357
|
post_data = self._encode_data(data, content_type)
|
|
@@ -272,7 +366,15 @@ class RequestFactory:
|
|
|
272
366
|
**extra,
|
|
273
367
|
)
|
|
274
368
|
|
|
275
|
-
def head(
|
|
369
|
+
def head(
|
|
370
|
+
self,
|
|
371
|
+
path: str,
|
|
372
|
+
data: dict[str, Any] | None = None,
|
|
373
|
+
secure: bool = True,
|
|
374
|
+
*,
|
|
375
|
+
headers: dict[str, str] | None = None,
|
|
376
|
+
**extra: Any,
|
|
377
|
+
) -> WSGIRequest:
|
|
276
378
|
"""Construct a HEAD request."""
|
|
277
379
|
data = {} if data is None else data
|
|
278
380
|
return self.generic(
|
|
@@ -286,20 +388,27 @@ class RequestFactory:
|
|
|
286
388
|
},
|
|
287
389
|
)
|
|
288
390
|
|
|
289
|
-
def trace(
|
|
391
|
+
def trace(
|
|
392
|
+
self,
|
|
393
|
+
path: str,
|
|
394
|
+
secure: bool = True,
|
|
395
|
+
*,
|
|
396
|
+
headers: dict[str, str] | None = None,
|
|
397
|
+
**extra: Any,
|
|
398
|
+
) -> WSGIRequest:
|
|
290
399
|
"""Construct a TRACE request."""
|
|
291
400
|
return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
|
|
292
401
|
|
|
293
402
|
def options(
|
|
294
403
|
self,
|
|
295
|
-
path,
|
|
296
|
-
data="",
|
|
297
|
-
content_type="application/octet-stream",
|
|
298
|
-
secure=True,
|
|
404
|
+
path: str,
|
|
405
|
+
data: Any = "",
|
|
406
|
+
content_type: str = "application/octet-stream",
|
|
407
|
+
secure: bool = True,
|
|
299
408
|
*,
|
|
300
|
-
headers=None,
|
|
301
|
-
**extra,
|
|
302
|
-
):
|
|
409
|
+
headers: dict[str, str] | None = None,
|
|
410
|
+
**extra: Any,
|
|
411
|
+
) -> WSGIRequest:
|
|
303
412
|
"Construct an OPTIONS request."
|
|
304
413
|
return self.generic(
|
|
305
414
|
"OPTIONS", path, data, content_type, secure=secure, headers=headers, **extra
|
|
@@ -307,14 +416,14 @@ class RequestFactory:
|
|
|
307
416
|
|
|
308
417
|
def put(
|
|
309
418
|
self,
|
|
310
|
-
path,
|
|
311
|
-
data="",
|
|
312
|
-
content_type="application/octet-stream",
|
|
313
|
-
secure=True,
|
|
419
|
+
path: str,
|
|
420
|
+
data: Any = "",
|
|
421
|
+
content_type: str = "application/octet-stream",
|
|
422
|
+
secure: bool = True,
|
|
314
423
|
*,
|
|
315
|
-
headers=None,
|
|
316
|
-
**extra,
|
|
317
|
-
):
|
|
424
|
+
headers: dict[str, str] | None = None,
|
|
425
|
+
**extra: Any,
|
|
426
|
+
) -> WSGIRequest:
|
|
318
427
|
"""Construct a PUT request."""
|
|
319
428
|
data = self._encode_json(data, content_type)
|
|
320
429
|
return self.generic(
|
|
@@ -323,14 +432,14 @@ class RequestFactory:
|
|
|
323
432
|
|
|
324
433
|
def patch(
|
|
325
434
|
self,
|
|
326
|
-
path,
|
|
327
|
-
data="",
|
|
328
|
-
content_type="application/octet-stream",
|
|
329
|
-
secure=True,
|
|
435
|
+
path: str,
|
|
436
|
+
data: Any = "",
|
|
437
|
+
content_type: str = "application/octet-stream",
|
|
438
|
+
secure: bool = True,
|
|
330
439
|
*,
|
|
331
|
-
headers=None,
|
|
332
|
-
**extra,
|
|
333
|
-
):
|
|
440
|
+
headers: dict[str, str] | None = None,
|
|
441
|
+
**extra: Any,
|
|
442
|
+
) -> WSGIRequest:
|
|
334
443
|
"""Construct a PATCH request."""
|
|
335
444
|
data = self._encode_json(data, content_type)
|
|
336
445
|
return self.generic(
|
|
@@ -339,14 +448,14 @@ class RequestFactory:
|
|
|
339
448
|
|
|
340
449
|
def delete(
|
|
341
450
|
self,
|
|
342
|
-
path,
|
|
343
|
-
data="",
|
|
344
|
-
content_type="application/octet-stream",
|
|
345
|
-
secure=True,
|
|
451
|
+
path: str,
|
|
452
|
+
data: Any = "",
|
|
453
|
+
content_type: str = "application/octet-stream",
|
|
454
|
+
secure: bool = True,
|
|
346
455
|
*,
|
|
347
|
-
headers=None,
|
|
348
|
-
**extra,
|
|
349
|
-
):
|
|
456
|
+
headers: dict[str, str] | None = None,
|
|
457
|
+
**extra: Any,
|
|
458
|
+
) -> WSGIRequest:
|
|
350
459
|
"""Construct a DELETE request."""
|
|
351
460
|
data = self._encode_json(data, content_type)
|
|
352
461
|
return self.generic(
|
|
@@ -355,19 +464,19 @@ class RequestFactory:
|
|
|
355
464
|
|
|
356
465
|
def generic(
|
|
357
466
|
self,
|
|
358
|
-
method,
|
|
359
|
-
path,
|
|
360
|
-
data="",
|
|
361
|
-
content_type="application/octet-stream",
|
|
362
|
-
secure=True,
|
|
467
|
+
method: str,
|
|
468
|
+
path: str,
|
|
469
|
+
data: Any = "",
|
|
470
|
+
content_type: str = "application/octet-stream",
|
|
471
|
+
secure: bool = True,
|
|
363
472
|
*,
|
|
364
|
-
headers=None,
|
|
365
|
-
**extra,
|
|
366
|
-
):
|
|
473
|
+
headers: dict[str, str] | None = None,
|
|
474
|
+
**extra: Any,
|
|
475
|
+
) -> WSGIRequest:
|
|
367
476
|
"""Construct an arbitrary HTTP request."""
|
|
368
477
|
parsed = urlparse(str(path)) # path can be lazy
|
|
369
478
|
data = force_bytes(data, settings.DEFAULT_CHARSET)
|
|
370
|
-
r = {
|
|
479
|
+
r: dict[str, Any] = {
|
|
371
480
|
"PATH_INFO": self._get_path(parsed),
|
|
372
481
|
"REQUEST_METHOD": method,
|
|
373
482
|
"SERVER_PORT": "443" if secure else "80",
|
|
@@ -382,7 +491,7 @@ class RequestFactory:
|
|
|
382
491
|
}
|
|
383
492
|
)
|
|
384
493
|
if headers:
|
|
385
|
-
extra.update(
|
|
494
|
+
extra.update(RequestHeaders.to_wsgi_names(headers))
|
|
386
495
|
r.update(extra)
|
|
387
496
|
# If QUERY_STRING is absent or empty, we want to extract it from the URL.
|
|
388
497
|
if not r.get("QUERY_STRING"):
|
|
@@ -392,7 +501,7 @@ class RequestFactory:
|
|
|
392
501
|
return self.request(**r)
|
|
393
502
|
|
|
394
503
|
|
|
395
|
-
class Client
|
|
504
|
+
class Client:
|
|
396
505
|
"""
|
|
397
506
|
A class that can act as a client for testing purposes.
|
|
398
507
|
|
|
@@ -413,71 +522,89 @@ class Client(RequestFactory):
|
|
|
413
522
|
|
|
414
523
|
def __init__(
|
|
415
524
|
self,
|
|
416
|
-
raise_request_exception=True,
|
|
525
|
+
raise_request_exception: bool = True,
|
|
417
526
|
*,
|
|
418
|
-
headers=None,
|
|
419
|
-
**defaults,
|
|
420
|
-
):
|
|
421
|
-
|
|
527
|
+
headers: dict[str, str] | None = None,
|
|
528
|
+
**defaults: Any,
|
|
529
|
+
) -> None:
|
|
530
|
+
self._request_factory = RequestFactory(headers=headers, **defaults)
|
|
422
531
|
self.handler = ClientHandler()
|
|
423
532
|
self.raise_request_exception = raise_request_exception
|
|
424
|
-
self.
|
|
425
|
-
self.
|
|
426
|
-
|
|
533
|
+
self.extra: dict[str, Any] | None = None
|
|
534
|
+
self.headers: dict[str, str] | None = None
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def cookies(self) -> SimpleCookie[str]:
|
|
538
|
+
"""Access the cookies from the request factory."""
|
|
539
|
+
return self._request_factory.cookies
|
|
540
|
+
|
|
541
|
+
@cookies.setter
|
|
542
|
+
def cookies(self, value: SimpleCookie[str]) -> None:
|
|
543
|
+
"""Set the cookies on the request factory."""
|
|
544
|
+
self._request_factory.cookies = value
|
|
427
545
|
|
|
428
|
-
def request(self, **request):
|
|
546
|
+
def request(self, **request: Any) -> ClientResponse:
|
|
429
547
|
"""
|
|
430
548
|
Make a generic request. Compose the environment dictionary and pass
|
|
431
549
|
to the handler, return the result of the handler. Assume defaults for
|
|
432
550
|
the query environment, which can be overridden using the arguments to
|
|
433
551
|
the request.
|
|
434
552
|
"""
|
|
435
|
-
environ = self._base_environ(**request)
|
|
553
|
+
environ = self._request_factory._base_environ(**request)
|
|
554
|
+
|
|
555
|
+
# Make the request
|
|
556
|
+
response = self.handler(environ)
|
|
557
|
+
|
|
558
|
+
# Wrap the response in ClientResponse for test-specific attributes
|
|
559
|
+
client_response = ClientResponse(
|
|
560
|
+
response=response,
|
|
561
|
+
client=self,
|
|
562
|
+
request=request,
|
|
563
|
+
)
|
|
436
564
|
|
|
437
|
-
#
|
|
438
|
-
|
|
439
|
-
|
|
565
|
+
# Re-raise the exception if configured to do so
|
|
566
|
+
# Only 5xx errors have response.exception set
|
|
567
|
+
if client_response.exception and self.raise_request_exception:
|
|
568
|
+
raise client_response.exception
|
|
569
|
+
|
|
570
|
+
# If the request had a user, make it available on the response.
|
|
440
571
|
try:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
self.check_exception(response)
|
|
447
|
-
# Save the client and request that stimulated the response.
|
|
448
|
-
response.client = self
|
|
449
|
-
response.request = request
|
|
450
|
-
response.json = partial(self._parse_json, response)
|
|
451
|
-
|
|
452
|
-
# If the request had a user attached, make it available on the response.
|
|
453
|
-
if hasattr(response.wsgi_request, "user"):
|
|
454
|
-
response.user = response.wsgi_request.user
|
|
572
|
+
from plain.auth.requests import get_request_user
|
|
573
|
+
|
|
574
|
+
client_response.user = get_request_user(client_response.wsgi_request)
|
|
575
|
+
except ImportError:
|
|
576
|
+
pass
|
|
455
577
|
|
|
456
578
|
# Attach the ResolverMatch instance to the response.
|
|
457
579
|
resolver = get_resolver()
|
|
458
|
-
|
|
580
|
+
client_response.resolver_match = SimpleLazyObject(
|
|
459
581
|
lambda: resolver.resolve(request["PATH_INFO"]),
|
|
460
582
|
)
|
|
461
583
|
|
|
462
584
|
# Update persistent cookie data.
|
|
463
|
-
if
|
|
464
|
-
self.cookies.update(
|
|
465
|
-
return
|
|
585
|
+
if client_response.cookies:
|
|
586
|
+
self.cookies.update(client_response.cookies)
|
|
587
|
+
return client_response
|
|
466
588
|
|
|
467
589
|
def get(
|
|
468
590
|
self,
|
|
469
|
-
path,
|
|
470
|
-
data=None,
|
|
471
|
-
follow=False,
|
|
472
|
-
secure=True,
|
|
591
|
+
path: str,
|
|
592
|
+
data: dict[str, Any] | None = None,
|
|
593
|
+
follow: bool = False,
|
|
594
|
+
secure: bool = True,
|
|
473
595
|
*,
|
|
474
|
-
headers=None,
|
|
475
|
-
**extra,
|
|
476
|
-
):
|
|
596
|
+
headers: dict[str, str] | None = None,
|
|
597
|
+
**extra: Any,
|
|
598
|
+
) -> ClientResponse:
|
|
477
599
|
"""Request a response from the server using GET."""
|
|
478
600
|
self.extra = extra
|
|
479
601
|
self.headers = headers
|
|
480
|
-
|
|
602
|
+
# Build the request using the factory
|
|
603
|
+
wsgi_request = self._request_factory.get(
|
|
604
|
+
path, data=data, secure=secure, headers=headers, **extra
|
|
605
|
+
)
|
|
606
|
+
# Execute and get response
|
|
607
|
+
response = self.request(**wsgi_request.environ)
|
|
481
608
|
if follow:
|
|
482
609
|
response = self._handle_redirects(
|
|
483
610
|
response, data=data, headers=headers, **extra
|
|
@@ -486,19 +613,20 @@ class Client(RequestFactory):
|
|
|
486
613
|
|
|
487
614
|
def post(
|
|
488
615
|
self,
|
|
489
|
-
path,
|
|
490
|
-
data=None,
|
|
491
|
-
content_type=_MULTIPART_CONTENT,
|
|
492
|
-
follow=False,
|
|
493
|
-
secure=True,
|
|
616
|
+
path: str,
|
|
617
|
+
data: dict[str, Any] | None = None,
|
|
618
|
+
content_type: str = _MULTIPART_CONTENT,
|
|
619
|
+
follow: bool = False,
|
|
620
|
+
secure: bool = True,
|
|
494
621
|
*,
|
|
495
|
-
headers=None,
|
|
496
|
-
**extra,
|
|
497
|
-
):
|
|
622
|
+
headers: dict[str, str] | None = None,
|
|
623
|
+
**extra: Any,
|
|
624
|
+
) -> ClientResponse:
|
|
498
625
|
"""Request a response from the server using POST."""
|
|
499
626
|
self.extra = extra
|
|
500
627
|
self.headers = headers
|
|
501
|
-
|
|
628
|
+
# Build the request using the factory
|
|
629
|
+
wsgi_request = self._request_factory.post(
|
|
502
630
|
path,
|
|
503
631
|
data=data,
|
|
504
632
|
content_type=content_type,
|
|
@@ -506,6 +634,8 @@ class Client(RequestFactory):
|
|
|
506
634
|
headers=headers,
|
|
507
635
|
**extra,
|
|
508
636
|
)
|
|
637
|
+
# Execute and get response
|
|
638
|
+
response = self.request(**wsgi_request.environ)
|
|
509
639
|
if follow:
|
|
510
640
|
response = self._handle_redirects(
|
|
511
641
|
response, data=data, content_type=content_type, headers=headers, **extra
|
|
@@ -514,20 +644,23 @@ class Client(RequestFactory):
|
|
|
514
644
|
|
|
515
645
|
def head(
|
|
516
646
|
self,
|
|
517
|
-
path,
|
|
518
|
-
data=None,
|
|
519
|
-
follow=False,
|
|
520
|
-
secure=True,
|
|
647
|
+
path: str,
|
|
648
|
+
data: dict[str, Any] | None = None,
|
|
649
|
+
follow: bool = False,
|
|
650
|
+
secure: bool = True,
|
|
521
651
|
*,
|
|
522
|
-
headers=None,
|
|
523
|
-
**extra,
|
|
524
|
-
):
|
|
652
|
+
headers: dict[str, str] | None = None,
|
|
653
|
+
**extra: Any,
|
|
654
|
+
) -> ClientResponse:
|
|
525
655
|
"""Request a response from the server using HEAD."""
|
|
526
656
|
self.extra = extra
|
|
527
657
|
self.headers = headers
|
|
528
|
-
|
|
658
|
+
# Build the request using the factory
|
|
659
|
+
wsgi_request = self._request_factory.head(
|
|
529
660
|
path, data=data, secure=secure, headers=headers, **extra
|
|
530
661
|
)
|
|
662
|
+
# Execute and get response
|
|
663
|
+
response = self.request(**wsgi_request.environ)
|
|
531
664
|
if follow:
|
|
532
665
|
response = self._handle_redirects(
|
|
533
666
|
response, data=data, headers=headers, **extra
|
|
@@ -536,19 +669,20 @@ class Client(RequestFactory):
|
|
|
536
669
|
|
|
537
670
|
def options(
|
|
538
671
|
self,
|
|
539
|
-
path,
|
|
540
|
-
data="",
|
|
541
|
-
content_type="application/octet-stream",
|
|
542
|
-
follow=False,
|
|
543
|
-
secure=True,
|
|
672
|
+
path: str,
|
|
673
|
+
data: Any = "",
|
|
674
|
+
content_type: str = "application/octet-stream",
|
|
675
|
+
follow: bool = False,
|
|
676
|
+
secure: bool = True,
|
|
544
677
|
*,
|
|
545
|
-
headers=None,
|
|
546
|
-
**extra,
|
|
547
|
-
):
|
|
678
|
+
headers: dict[str, str] | None = None,
|
|
679
|
+
**extra: Any,
|
|
680
|
+
) -> ClientResponse:
|
|
548
681
|
"""Request a response from the server using OPTIONS."""
|
|
549
682
|
self.extra = extra
|
|
550
683
|
self.headers = headers
|
|
551
|
-
|
|
684
|
+
# Build the request using the factory
|
|
685
|
+
wsgi_request = self._request_factory.options(
|
|
552
686
|
path,
|
|
553
687
|
data=data,
|
|
554
688
|
content_type=content_type,
|
|
@@ -556,6 +690,8 @@ class Client(RequestFactory):
|
|
|
556
690
|
headers=headers,
|
|
557
691
|
**extra,
|
|
558
692
|
)
|
|
693
|
+
# Execute and get response
|
|
694
|
+
response = self.request(**wsgi_request.environ)
|
|
559
695
|
if follow:
|
|
560
696
|
response = self._handle_redirects(
|
|
561
697
|
response, data=data, content_type=content_type, headers=headers, **extra
|
|
@@ -564,19 +700,20 @@ class Client(RequestFactory):
|
|
|
564
700
|
|
|
565
701
|
def put(
|
|
566
702
|
self,
|
|
567
|
-
path,
|
|
568
|
-
data="",
|
|
569
|
-
content_type="application/octet-stream",
|
|
570
|
-
follow=False,
|
|
571
|
-
secure=True,
|
|
703
|
+
path: str,
|
|
704
|
+
data: Any = "",
|
|
705
|
+
content_type: str = "application/octet-stream",
|
|
706
|
+
follow: bool = False,
|
|
707
|
+
secure: bool = True,
|
|
572
708
|
*,
|
|
573
|
-
headers=None,
|
|
574
|
-
**extra,
|
|
575
|
-
):
|
|
709
|
+
headers: dict[str, str] | None = None,
|
|
710
|
+
**extra: Any,
|
|
711
|
+
) -> ClientResponse:
|
|
576
712
|
"""Send a resource to the server using PUT."""
|
|
577
713
|
self.extra = extra
|
|
578
714
|
self.headers = headers
|
|
579
|
-
|
|
715
|
+
# Build the request using the factory
|
|
716
|
+
wsgi_request = self._request_factory.put(
|
|
580
717
|
path,
|
|
581
718
|
data=data,
|
|
582
719
|
content_type=content_type,
|
|
@@ -584,6 +721,8 @@ class Client(RequestFactory):
|
|
|
584
721
|
headers=headers,
|
|
585
722
|
**extra,
|
|
586
723
|
)
|
|
724
|
+
# Execute and get response
|
|
725
|
+
response = self.request(**wsgi_request.environ)
|
|
587
726
|
if follow:
|
|
588
727
|
response = self._handle_redirects(
|
|
589
728
|
response, data=data, content_type=content_type, headers=headers, **extra
|
|
@@ -592,19 +731,20 @@ class Client(RequestFactory):
|
|
|
592
731
|
|
|
593
732
|
def patch(
|
|
594
733
|
self,
|
|
595
|
-
path,
|
|
596
|
-
data="",
|
|
597
|
-
content_type="application/octet-stream",
|
|
598
|
-
follow=False,
|
|
599
|
-
secure=True,
|
|
734
|
+
path: str,
|
|
735
|
+
data: Any = "",
|
|
736
|
+
content_type: str = "application/octet-stream",
|
|
737
|
+
follow: bool = False,
|
|
738
|
+
secure: bool = True,
|
|
600
739
|
*,
|
|
601
|
-
headers=None,
|
|
602
|
-
**extra,
|
|
603
|
-
):
|
|
740
|
+
headers: dict[str, str] | None = None,
|
|
741
|
+
**extra: Any,
|
|
742
|
+
) -> ClientResponse:
|
|
604
743
|
"""Send a resource to the server using PATCH."""
|
|
605
744
|
self.extra = extra
|
|
606
745
|
self.headers = headers
|
|
607
|
-
|
|
746
|
+
# Build the request using the factory
|
|
747
|
+
wsgi_request = self._request_factory.patch(
|
|
608
748
|
path,
|
|
609
749
|
data=data,
|
|
610
750
|
content_type=content_type,
|
|
@@ -612,6 +752,8 @@ class Client(RequestFactory):
|
|
|
612
752
|
headers=headers,
|
|
613
753
|
**extra,
|
|
614
754
|
)
|
|
755
|
+
# Execute and get response
|
|
756
|
+
response = self.request(**wsgi_request.environ)
|
|
615
757
|
if follow:
|
|
616
758
|
response = self._handle_redirects(
|
|
617
759
|
response, data=data, content_type=content_type, headers=headers, **extra
|
|
@@ -620,19 +762,20 @@ class Client(RequestFactory):
|
|
|
620
762
|
|
|
621
763
|
def delete(
|
|
622
764
|
self,
|
|
623
|
-
path,
|
|
624
|
-
data="",
|
|
625
|
-
content_type="application/octet-stream",
|
|
626
|
-
follow=False,
|
|
627
|
-
secure=True,
|
|
765
|
+
path: str,
|
|
766
|
+
data: Any = "",
|
|
767
|
+
content_type: str = "application/octet-stream",
|
|
768
|
+
follow: bool = False,
|
|
769
|
+
secure: bool = True,
|
|
628
770
|
*,
|
|
629
|
-
headers=None,
|
|
630
|
-
**extra,
|
|
631
|
-
):
|
|
771
|
+
headers: dict[str, str] | None = None,
|
|
772
|
+
**extra: Any,
|
|
773
|
+
) -> ClientResponse:
|
|
632
774
|
"""Send a DELETE request to the server."""
|
|
633
775
|
self.extra = extra
|
|
634
776
|
self.headers = headers
|
|
635
|
-
|
|
777
|
+
# Build the request using the factory
|
|
778
|
+
wsgi_request = self._request_factory.delete(
|
|
636
779
|
path,
|
|
637
780
|
data=data,
|
|
638
781
|
content_type=content_type,
|
|
@@ -640,6 +783,8 @@ class Client(RequestFactory):
|
|
|
640
783
|
headers=headers,
|
|
641
784
|
**extra,
|
|
642
785
|
)
|
|
786
|
+
# Execute and get response
|
|
787
|
+
response = self.request(**wsgi_request.environ)
|
|
643
788
|
if follow:
|
|
644
789
|
response = self._handle_redirects(
|
|
645
790
|
response, data=data, content_type=content_type, headers=headers, **extra
|
|
@@ -648,20 +793,23 @@ class Client(RequestFactory):
|
|
|
648
793
|
|
|
649
794
|
def trace(
|
|
650
795
|
self,
|
|
651
|
-
path,
|
|
652
|
-
data="",
|
|
653
|
-
follow=False,
|
|
654
|
-
secure=True,
|
|
796
|
+
path: str,
|
|
797
|
+
data: Any = "",
|
|
798
|
+
follow: bool = False,
|
|
799
|
+
secure: bool = True,
|
|
655
800
|
*,
|
|
656
|
-
headers=None,
|
|
657
|
-
**extra,
|
|
658
|
-
):
|
|
801
|
+
headers: dict[str, str] | None = None,
|
|
802
|
+
**extra: Any,
|
|
803
|
+
) -> ClientResponse:
|
|
659
804
|
"""Send a TRACE request to the server."""
|
|
660
805
|
self.extra = extra
|
|
661
806
|
self.headers = headers
|
|
662
|
-
|
|
807
|
+
# Build the request using the factory
|
|
808
|
+
wsgi_request = self._request_factory.trace(
|
|
663
809
|
path, data=data, secure=secure, headers=headers, **extra
|
|
664
810
|
)
|
|
811
|
+
# Execute and get response
|
|
812
|
+
response = self.request(**wsgi_request.environ)
|
|
665
813
|
if follow:
|
|
666
814
|
response = self._handle_redirects(
|
|
667
815
|
response, data=data, headers=headers, **extra
|
|
@@ -670,12 +818,12 @@ class Client(RequestFactory):
|
|
|
670
818
|
|
|
671
819
|
def _handle_redirects(
|
|
672
820
|
self,
|
|
673
|
-
response,
|
|
674
|
-
data="",
|
|
675
|
-
content_type="",
|
|
676
|
-
headers=None,
|
|
677
|
-
**extra,
|
|
678
|
-
):
|
|
821
|
+
response: ClientResponse,
|
|
822
|
+
data: Any = "",
|
|
823
|
+
content_type: str = "",
|
|
824
|
+
headers: dict[str, str] | None = None,
|
|
825
|
+
**extra: Any,
|
|
826
|
+
) -> ClientResponse:
|
|
679
827
|
"""
|
|
680
828
|
Follow any redirects by requesting responses from the server using GET.
|
|
681
829
|
"""
|
|
@@ -714,14 +862,14 @@ class Client(RequestFactory):
|
|
|
714
862
|
):
|
|
715
863
|
# Preserve request method and query string (if needed)
|
|
716
864
|
# post-redirect for 307/308 responses.
|
|
717
|
-
|
|
718
|
-
if
|
|
865
|
+
request_method_name = response.request["REQUEST_METHOD"].lower()
|
|
866
|
+
if request_method_name not in ("get", "head"):
|
|
719
867
|
extra["QUERY_STRING"] = url.query
|
|
720
|
-
request_method = getattr(self,
|
|
868
|
+
request_method = getattr(self, request_method_name)
|
|
721
869
|
else:
|
|
722
870
|
request_method = self.get
|
|
723
871
|
data = QueryDict(url.query)
|
|
724
|
-
content_type =
|
|
872
|
+
content_type = ""
|
|
725
873
|
|
|
726
874
|
response = request_method(
|
|
727
875
|
path,
|
|
@@ -747,50 +895,20 @@ class Client(RequestFactory):
|
|
|
747
895
|
|
|
748
896
|
return response
|
|
749
897
|
|
|
750
|
-
def store_exc_info(self, **kwargs):
|
|
751
|
-
"""Store exceptions when they are generated by a view."""
|
|
752
|
-
self.exc_info = sys.exc_info()
|
|
753
|
-
|
|
754
|
-
def check_exception(self, response):
|
|
755
|
-
"""
|
|
756
|
-
Look for a signaled exception, clear the current context exception
|
|
757
|
-
data, re-raise the signaled exception, and clear the signaled exception
|
|
758
|
-
from the local cache.
|
|
759
|
-
"""
|
|
760
|
-
response.exc_info = self.exc_info
|
|
761
|
-
if self.exc_info:
|
|
762
|
-
_, exc_value, _ = self.exc_info
|
|
763
|
-
self.exc_info = None
|
|
764
|
-
if self.raise_request_exception:
|
|
765
|
-
raise exc_value
|
|
766
|
-
|
|
767
898
|
@property
|
|
768
|
-
def session(self):
|
|
899
|
+
def session(self) -> Any:
|
|
769
900
|
"""Return the current session variables."""
|
|
770
901
|
from plain.sessions.test import get_client_session
|
|
771
902
|
|
|
772
903
|
return get_client_session(self)
|
|
773
904
|
|
|
774
|
-
def force_login(self, user):
|
|
905
|
+
def force_login(self, user: Any) -> None:
|
|
775
906
|
from plain.auth.test import login_client
|
|
776
907
|
|
|
777
908
|
login_client(self, user)
|
|
778
909
|
|
|
779
|
-
def logout(self):
|
|
910
|
+
def logout(self) -> None:
|
|
780
911
|
"""Log out the user by removing the cookies and session object."""
|
|
781
912
|
from plain.auth.test import logout_client
|
|
782
913
|
|
|
783
914
|
logout_client(self)
|
|
784
|
-
|
|
785
|
-
def _parse_json(self, response, **extra):
|
|
786
|
-
if not hasattr(response, "_json"):
|
|
787
|
-
if not _JSON_CONTENT_TYPE_RE.match(response.headers.get("Content-Type")):
|
|
788
|
-
raise ValueError(
|
|
789
|
-
'Content-Type header is "{}", not "application/json"'.format(
|
|
790
|
-
response.headers.get("Content-Type")
|
|
791
|
-
)
|
|
792
|
-
)
|
|
793
|
-
response._json = json.loads(
|
|
794
|
-
response.content.decode(response.charset), **extra
|
|
795
|
-
)
|
|
796
|
-
return response._json
|