plain 0.68.1__py3-none-any.whl → 0.70.0__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/AGENTS.md +1 -1
- plain/CHANGELOG.md +23 -0
- plain/assets/compile.py +20 -7
- plain/assets/finders.py +15 -11
- plain/assets/fingerprints.py +6 -5
- plain/assets/urls.py +1 -1
- plain/assets/views.py +23 -17
- plain/chores/registry.py +14 -9
- plain/cli/agent/__init__.py +1 -1
- plain/cli/agent/docs.py +7 -6
- plain/cli/agent/llmdocs.py +18 -8
- plain/cli/agent/md.py +19 -14
- plain/cli/agent/prompt.py +1 -1
- plain/cli/agent/request.py +37 -17
- plain/cli/build.py +2 -2
- plain/cli/changelog.py +8 -4
- plain/cli/chores.py +4 -4
- plain/cli/core.py +8 -5
- plain/cli/docs.py +2 -2
- plain/cli/formatting.py +10 -7
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +3 -3
- plain/cli/print.py +1 -1
- plain/cli/registry.py +10 -6
- plain/cli/scaffold.py +1 -1
- plain/cli/settings.py +1 -1
- plain/cli/shell.py +10 -7
- plain/cli/startup.py +3 -3
- plain/cli/urls.py +10 -4
- plain/cli/utils.py +2 -2
- plain/csrf/middleware.py +15 -5
- plain/csrf/views.py +11 -8
- plain/debug.py +5 -2
- plain/exceptions.py +20 -51
- plain/forms/__init__.py +1 -1
- plain/forms/boundfield.py +14 -7
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +139 -97
- plain/forms/forms.py +55 -39
- plain/http/cookie.py +15 -7
- plain/http/multipartparser.py +50 -30
- plain/http/request.py +97 -73
- plain/http/response.py +99 -80
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +34 -18
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +23 -5
- plain/internal/files/uploadedfile.py +42 -26
- plain/internal/files/uploadhandler.py +48 -27
- plain/internal/files/utils.py +13 -6
- plain/internal/handlers/base.py +20 -6
- plain/internal/handlers/exception.py +19 -5
- plain/internal/handlers/wsgi.py +30 -18
- plain/internal/middleware/headers.py +11 -2
- plain/internal/middleware/hosts.py +10 -2
- plain/internal/middleware/https.py +13 -3
- plain/internal/middleware/slash.py +15 -5
- plain/json.py +2 -1
- plain/logs/configure.py +3 -1
- plain/logs/debug.py +16 -5
- plain/logs/formatters.py +6 -3
- plain/logs/loggers.py +56 -52
- plain/logs/utils.py +19 -9
- plain/packages/config.py +14 -6
- plain/packages/registry.py +27 -12
- plain/paginator.py +31 -21
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +25 -10
- plain/preflight/results.py +10 -4
- plain/preflight/security.py +7 -5
- plain/preflight/urls.py +4 -1
- plain/runtime/__init__.py +4 -3
- plain/runtime/global_settings.py +1 -1
- plain/runtime/user_settings.py +26 -17
- plain/runtime/utils.py +1 -1
- plain/signals/dispatch/dispatcher.py +39 -17
- plain/signing.py +49 -30
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +4 -3
- plain/templates/jinja/extensions.py +9 -3
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +1 -1
- plain/test/client.py +246 -174
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +10 -2
- plain/urls/converters.py +13 -10
- plain/urls/patterns.py +32 -20
- plain/urls/resolvers.py +32 -22
- plain/urls/utils.py +5 -1
- plain/utils/cache.py +14 -8
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +84 -54
- plain/utils/dateparse.py +10 -7
- plain/utils/deconstruct.py +12 -4
- plain/utils/decorators.py +5 -1
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +62 -47
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +21 -14
- 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 +23 -13
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +34 -18
- plain/utils/timezone.py +30 -19
- plain/utils/tree.py +31 -18
- plain/validators.py +71 -44
- plain/views/base.py +16 -6
- plain/views/errors.py +11 -4
- plain/views/exceptions.py +4 -1
- plain/views/objects.py +27 -17
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
- plain-0.70.0.dist-info/RECORD +169 -0
- plain-0.68.1.dist-info/RECORD +0 -169
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
plain/test/client.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import json
|
2
4
|
import sys
|
3
5
|
from functools import partial
|
4
6
|
from http import HTTPStatus
|
5
7
|
from http.cookies import SimpleCookie
|
6
8
|
from io import BytesIO, IOBase
|
9
|
+
from typing import TYPE_CHECKING, Any
|
7
10
|
from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
8
11
|
|
9
12
|
from plain.http import HttpHeaders, QueryDict
|
@@ -22,6 +25,9 @@ from plain.utils.regex_helper import _lazy_re_compile
|
|
22
25
|
from .encoding import encode_multipart
|
23
26
|
from .exceptions import RedirectCycleError
|
24
27
|
|
28
|
+
if TYPE_CHECKING:
|
29
|
+
from plain.http import Response
|
30
|
+
|
25
31
|
__all__ = (
|
26
32
|
"Client",
|
27
33
|
"RequestFactory",
|
@@ -44,17 +50,17 @@ class FakePayload(IOBase):
|
|
44
50
|
that wouldn't work in real life.
|
45
51
|
"""
|
46
52
|
|
47
|
-
def __init__(self, initial_bytes=None):
|
53
|
+
def __init__(self, initial_bytes: bytes | None = None) -> None:
|
48
54
|
self.__content = BytesIO()
|
49
55
|
self.__len = 0
|
50
56
|
self.read_started = False
|
51
57
|
if initial_bytes is not None:
|
52
58
|
self.write(initial_bytes)
|
53
59
|
|
54
|
-
def __len__(self):
|
60
|
+
def __len__(self) -> int:
|
55
61
|
return self.__len
|
56
62
|
|
57
|
-
def read(self, size
|
63
|
+
def read(self, size: int = -1, /) -> bytes:
|
58
64
|
if not self.read_started:
|
59
65
|
self.__content.seek(0)
|
60
66
|
self.read_started = True
|
@@ -67,7 +73,7 @@ class FakePayload(IOBase):
|
|
67
73
|
self.__len -= len(content)
|
68
74
|
return content
|
69
75
|
|
70
|
-
def readline(self, size
|
76
|
+
def readline(self, size: int = -1, /) -> bytes:
|
71
77
|
if not self.read_started:
|
72
78
|
self.__content.seek(0)
|
73
79
|
self.read_started = True
|
@@ -80,7 +86,7 @@ class FakePayload(IOBase):
|
|
80
86
|
self.__len -= len(content)
|
81
87
|
return content
|
82
88
|
|
83
|
-
def write(self, b, /):
|
89
|
+
def write(self, b: bytes | str, /) -> None:
|
84
90
|
if self.read_started:
|
85
91
|
raise ValueError("Unable to write a payload after it's been read")
|
86
92
|
content = force_bytes(b)
|
@@ -88,20 +94,20 @@ class FakePayload(IOBase):
|
|
88
94
|
self.__len += len(content)
|
89
95
|
|
90
96
|
|
91
|
-
def _conditional_content_removal(request, response):
|
97
|
+
def _conditional_content_removal(request: WSGIRequest, response: Response) -> Response:
|
92
98
|
"""
|
93
99
|
Simulate the behavior of most web servers by removing the content of
|
94
100
|
responses for HEAD requests, 1xx, 204, and 304 responses. Ensure
|
95
101
|
compliance with RFC 9112 Section 6.3.
|
96
102
|
"""
|
97
103
|
if 100 <= response.status_code < 200 or response.status_code in (204, 304):
|
98
|
-
if response.streaming:
|
99
|
-
response.streaming_content = []
|
104
|
+
if response.streaming: # type: ignore[attr-defined]
|
105
|
+
response.streaming_content = [] # type: ignore[attr-defined]
|
100
106
|
else:
|
101
107
|
response.content = b""
|
102
108
|
if request.method == "HEAD":
|
103
|
-
if response.streaming:
|
104
|
-
response.streaming_content = []
|
109
|
+
if response.streaming: # type: ignore[attr-defined]
|
110
|
+
response.streaming_content = [] # type: ignore[attr-defined]
|
105
111
|
else:
|
106
112
|
response.content = b""
|
107
113
|
return response
|
@@ -114,10 +120,10 @@ class ClientHandler(BaseHandler):
|
|
114
120
|
the originating WSGIRequest attached to its ``wsgi_request`` attribute.
|
115
121
|
"""
|
116
122
|
|
117
|
-
def __init__(self, *args, **kwargs):
|
123
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
118
124
|
super().__init__(*args, **kwargs)
|
119
125
|
|
120
|
-
def __call__(self, environ):
|
126
|
+
def __call__(self, environ: dict[str, Any]) -> Response:
|
121
127
|
# Set up middleware if needed. We couldn't do this earlier, because
|
122
128
|
# settings weren't available.
|
123
129
|
if self._middleware_chain is None:
|
@@ -156,15 +162,21 @@ class RequestFactory:
|
|
156
162
|
just as if that view had been hooked up using a urlrouter.
|
157
163
|
"""
|
158
164
|
|
159
|
-
def __init__(
|
165
|
+
def __init__(
|
166
|
+
self,
|
167
|
+
*,
|
168
|
+
json_encoder: type[json.JSONEncoder] = PlainJSONEncoder,
|
169
|
+
headers: dict[str, str] | None = None,
|
170
|
+
**defaults: Any,
|
171
|
+
) -> None:
|
160
172
|
self.json_encoder = json_encoder
|
161
|
-
self.defaults = defaults
|
162
|
-
self.cookies = SimpleCookie()
|
173
|
+
self.defaults: dict[str, Any] = defaults
|
174
|
+
self.cookies: SimpleCookie[str] = SimpleCookie()
|
163
175
|
self.errors = BytesIO()
|
164
176
|
if headers:
|
165
177
|
self.defaults.update(HttpHeaders.to_wsgi_names(headers))
|
166
178
|
|
167
|
-
def _base_environ(self, **request):
|
179
|
+
def _base_environ(self, **request: Any) -> dict[str, Any]:
|
168
180
|
"""
|
169
181
|
The base environment for a request.
|
170
182
|
"""
|
@@ -197,13 +209,13 @@ class RequestFactory:
|
|
197
209
|
**request,
|
198
210
|
}
|
199
211
|
|
200
|
-
def request(self, **request):
|
212
|
+
def request(self, **request: Any) -> WSGIRequest:
|
201
213
|
"Construct a generic request object."
|
202
214
|
return WSGIRequest(self._base_environ(**request))
|
203
215
|
|
204
|
-
def _encode_data(self, data, content_type):
|
216
|
+
def _encode_data(self, data: dict[str, Any] | str, content_type: str) -> bytes:
|
205
217
|
if content_type is _MULTIPART_CONTENT:
|
206
|
-
return encode_multipart(_BOUNDARY, data)
|
218
|
+
return encode_multipart(_BOUNDARY, data) # type: ignore[arg-type]
|
207
219
|
else:
|
208
220
|
# Encode the content so that the byte representation is correct.
|
209
221
|
match = _CONTENT_TYPE_RE.match(content_type)
|
@@ -213,7 +225,7 @@ class RequestFactory:
|
|
213
225
|
charset = settings.DEFAULT_CHARSET
|
214
226
|
return force_bytes(data, encoding=charset)
|
215
227
|
|
216
|
-
def _encode_json(self, data, content_type):
|
228
|
+
def _encode_json(self, data: Any, content_type: str) -> Any:
|
217
229
|
"""
|
218
230
|
Return encoded JSON if data is a dict, list, or tuple and content_type
|
219
231
|
is application/json.
|
@@ -223,7 +235,7 @@ class RequestFactory:
|
|
223
235
|
)
|
224
236
|
return json.dumps(data, cls=self.json_encoder) if should_encode else data
|
225
237
|
|
226
|
-
def _get_path(self, parsed):
|
238
|
+
def _get_path(self, parsed: Any) -> str:
|
227
239
|
path = parsed.path
|
228
240
|
# If there are parameters, add them
|
229
241
|
if parsed.params:
|
@@ -234,7 +246,15 @@ class RequestFactory:
|
|
234
246
|
# Refs comment in `get_bytes_from_wsgi()`.
|
235
247
|
return path.decode("iso-8859-1")
|
236
248
|
|
237
|
-
def get(
|
249
|
+
def get(
|
250
|
+
self,
|
251
|
+
path: str,
|
252
|
+
data: dict[str, Any] | None = None,
|
253
|
+
secure: bool = True,
|
254
|
+
*,
|
255
|
+
headers: dict[str, str] | None = None,
|
256
|
+
**extra: Any,
|
257
|
+
) -> WSGIRequest:
|
238
258
|
"""Construct a GET request."""
|
239
259
|
data = {} if data is None else data
|
240
260
|
return self.generic(
|
@@ -250,14 +270,14 @@ class RequestFactory:
|
|
250
270
|
|
251
271
|
def post(
|
252
272
|
self,
|
253
|
-
path,
|
254
|
-
data=None,
|
255
|
-
content_type=_MULTIPART_CONTENT,
|
256
|
-
secure=True,
|
273
|
+
path: str,
|
274
|
+
data: dict[str, Any] | None = None,
|
275
|
+
content_type: str = _MULTIPART_CONTENT,
|
276
|
+
secure: bool = True,
|
257
277
|
*,
|
258
|
-
headers=None,
|
259
|
-
**extra,
|
260
|
-
):
|
278
|
+
headers: dict[str, str] | None = None,
|
279
|
+
**extra: Any,
|
280
|
+
) -> WSGIRequest:
|
261
281
|
"""Construct a POST request."""
|
262
282
|
data = self._encode_json({} if data is None else data, content_type)
|
263
283
|
post_data = self._encode_data(data, content_type)
|
@@ -272,7 +292,15 @@ class RequestFactory:
|
|
272
292
|
**extra,
|
273
293
|
)
|
274
294
|
|
275
|
-
def head(
|
295
|
+
def head(
|
296
|
+
self,
|
297
|
+
path: str,
|
298
|
+
data: dict[str, Any] | None = None,
|
299
|
+
secure: bool = True,
|
300
|
+
*,
|
301
|
+
headers: dict[str, str] | None = None,
|
302
|
+
**extra: Any,
|
303
|
+
) -> WSGIRequest:
|
276
304
|
"""Construct a HEAD request."""
|
277
305
|
data = {} if data is None else data
|
278
306
|
return self.generic(
|
@@ -286,20 +314,27 @@ class RequestFactory:
|
|
286
314
|
},
|
287
315
|
)
|
288
316
|
|
289
|
-
def trace(
|
317
|
+
def trace(
|
318
|
+
self,
|
319
|
+
path: str,
|
320
|
+
secure: bool = True,
|
321
|
+
*,
|
322
|
+
headers: dict[str, str] | None = None,
|
323
|
+
**extra: Any,
|
324
|
+
) -> WSGIRequest:
|
290
325
|
"""Construct a TRACE request."""
|
291
326
|
return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
|
292
327
|
|
293
328
|
def options(
|
294
329
|
self,
|
295
|
-
path,
|
296
|
-
data="",
|
297
|
-
content_type="application/octet-stream",
|
298
|
-
secure=True,
|
330
|
+
path: str,
|
331
|
+
data: Any = "",
|
332
|
+
content_type: str = "application/octet-stream",
|
333
|
+
secure: bool = True,
|
299
334
|
*,
|
300
|
-
headers=None,
|
301
|
-
**extra,
|
302
|
-
):
|
335
|
+
headers: dict[str, str] | None = None,
|
336
|
+
**extra: Any,
|
337
|
+
) -> WSGIRequest:
|
303
338
|
"Construct an OPTIONS request."
|
304
339
|
return self.generic(
|
305
340
|
"OPTIONS", path, data, content_type, secure=secure, headers=headers, **extra
|
@@ -307,14 +342,14 @@ class RequestFactory:
|
|
307
342
|
|
308
343
|
def put(
|
309
344
|
self,
|
310
|
-
path,
|
311
|
-
data="",
|
312
|
-
content_type="application/octet-stream",
|
313
|
-
secure=True,
|
345
|
+
path: str,
|
346
|
+
data: Any = "",
|
347
|
+
content_type: str = "application/octet-stream",
|
348
|
+
secure: bool = True,
|
314
349
|
*,
|
315
|
-
headers=None,
|
316
|
-
**extra,
|
317
|
-
):
|
350
|
+
headers: dict[str, str] | None = None,
|
351
|
+
**extra: Any,
|
352
|
+
) -> WSGIRequest:
|
318
353
|
"""Construct a PUT request."""
|
319
354
|
data = self._encode_json(data, content_type)
|
320
355
|
return self.generic(
|
@@ -323,14 +358,14 @@ class RequestFactory:
|
|
323
358
|
|
324
359
|
def patch(
|
325
360
|
self,
|
326
|
-
path,
|
327
|
-
data="",
|
328
|
-
content_type="application/octet-stream",
|
329
|
-
secure=True,
|
361
|
+
path: str,
|
362
|
+
data: Any = "",
|
363
|
+
content_type: str = "application/octet-stream",
|
364
|
+
secure: bool = True,
|
330
365
|
*,
|
331
|
-
headers=None,
|
332
|
-
**extra,
|
333
|
-
):
|
366
|
+
headers: dict[str, str] | None = None,
|
367
|
+
**extra: Any,
|
368
|
+
) -> WSGIRequest:
|
334
369
|
"""Construct a PATCH request."""
|
335
370
|
data = self._encode_json(data, content_type)
|
336
371
|
return self.generic(
|
@@ -339,14 +374,14 @@ class RequestFactory:
|
|
339
374
|
|
340
375
|
def delete(
|
341
376
|
self,
|
342
|
-
path,
|
343
|
-
data="",
|
344
|
-
content_type="application/octet-stream",
|
345
|
-
secure=True,
|
377
|
+
path: str,
|
378
|
+
data: Any = "",
|
379
|
+
content_type: str = "application/octet-stream",
|
380
|
+
secure: bool = True,
|
346
381
|
*,
|
347
|
-
headers=None,
|
348
|
-
**extra,
|
349
|
-
):
|
382
|
+
headers: dict[str, str] | None = None,
|
383
|
+
**extra: Any,
|
384
|
+
) -> WSGIRequest:
|
350
385
|
"""Construct a DELETE request."""
|
351
386
|
data = self._encode_json(data, content_type)
|
352
387
|
return self.generic(
|
@@ -355,19 +390,19 @@ class RequestFactory:
|
|
355
390
|
|
356
391
|
def generic(
|
357
392
|
self,
|
358
|
-
method,
|
359
|
-
path,
|
360
|
-
data="",
|
361
|
-
content_type="application/octet-stream",
|
362
|
-
secure=True,
|
393
|
+
method: str,
|
394
|
+
path: str,
|
395
|
+
data: Any = "",
|
396
|
+
content_type: str = "application/octet-stream",
|
397
|
+
secure: bool = True,
|
363
398
|
*,
|
364
|
-
headers=None,
|
365
|
-
**extra,
|
366
|
-
):
|
399
|
+
headers: dict[str, str] | None = None,
|
400
|
+
**extra: Any,
|
401
|
+
) -> WSGIRequest:
|
367
402
|
"""Construct an arbitrary HTTP request."""
|
368
403
|
parsed = urlparse(str(path)) # path can be lazy
|
369
404
|
data = force_bytes(data, settings.DEFAULT_CHARSET)
|
370
|
-
r = {
|
405
|
+
r: dict[str, Any] = {
|
371
406
|
"PATH_INFO": self._get_path(parsed),
|
372
407
|
"REQUEST_METHOD": method,
|
373
408
|
"SERVER_PORT": "443" if secure else "80",
|
@@ -392,7 +427,7 @@ class RequestFactory:
|
|
392
427
|
return self.request(**r)
|
393
428
|
|
394
429
|
|
395
|
-
class Client
|
430
|
+
class Client:
|
396
431
|
"""
|
397
432
|
A class that can act as a client for testing purposes.
|
398
433
|
|
@@ -413,26 +448,36 @@ class Client(RequestFactory):
|
|
413
448
|
|
414
449
|
def __init__(
|
415
450
|
self,
|
416
|
-
raise_request_exception=True,
|
451
|
+
raise_request_exception: bool = True,
|
417
452
|
*,
|
418
|
-
headers=None,
|
419
|
-
**defaults,
|
420
|
-
):
|
421
|
-
|
453
|
+
headers: dict[str, str] | None = None,
|
454
|
+
**defaults: Any,
|
455
|
+
) -> None:
|
456
|
+
self._request_factory = RequestFactory(headers=headers, **defaults)
|
422
457
|
self.handler = ClientHandler()
|
423
458
|
self.raise_request_exception = raise_request_exception
|
424
|
-
self.exc_info = None
|
425
|
-
self.extra = None
|
426
|
-
self.headers = None
|
459
|
+
self.exc_info: tuple[Any, Any, Any] | None = None
|
460
|
+
self.extra: dict[str, Any] | None = None
|
461
|
+
self.headers: dict[str, str] | None = None
|
427
462
|
|
428
|
-
|
463
|
+
@property
|
464
|
+
def cookies(self) -> SimpleCookie[str]:
|
465
|
+
"""Access the cookies from the request factory."""
|
466
|
+
return self._request_factory.cookies
|
467
|
+
|
468
|
+
@cookies.setter
|
469
|
+
def cookies(self, value: SimpleCookie[str]) -> None:
|
470
|
+
"""Set the cookies on the request factory."""
|
471
|
+
self._request_factory.cookies = value
|
472
|
+
|
473
|
+
def request(self, **request: Any) -> Response:
|
429
474
|
"""
|
430
475
|
Make a generic request. Compose the environment dictionary and pass
|
431
476
|
to the handler, return the result of the handler. Assume defaults for
|
432
477
|
the query environment, which can be overridden using the arguments to
|
433
478
|
the request.
|
434
479
|
"""
|
435
|
-
environ = self._base_environ(**request)
|
480
|
+
environ = self._request_factory._base_environ(**request)
|
436
481
|
|
437
482
|
# Capture exceptions created by the handler.
|
438
483
|
exception_uid = f"request-exception-{id(request)}"
|
@@ -466,18 +511,23 @@ class Client(RequestFactory):
|
|
466
511
|
|
467
512
|
def get(
|
468
513
|
self,
|
469
|
-
path,
|
470
|
-
data=None,
|
471
|
-
follow=False,
|
472
|
-
secure=True,
|
514
|
+
path: str,
|
515
|
+
data: dict[str, Any] | None = None,
|
516
|
+
follow: bool = False,
|
517
|
+
secure: bool = True,
|
473
518
|
*,
|
474
|
-
headers=None,
|
475
|
-
**extra,
|
476
|
-
):
|
519
|
+
headers: dict[str, str] | None = None,
|
520
|
+
**extra: Any,
|
521
|
+
) -> Response:
|
477
522
|
"""Request a response from the server using GET."""
|
478
523
|
self.extra = extra
|
479
524
|
self.headers = headers
|
480
|
-
|
525
|
+
# Build the request using the factory
|
526
|
+
wsgi_request = self._request_factory.get(
|
527
|
+
path, data=data, secure=secure, headers=headers, **extra
|
528
|
+
)
|
529
|
+
# Execute and get response
|
530
|
+
response = self.request(**wsgi_request.environ)
|
481
531
|
if follow:
|
482
532
|
response = self._handle_redirects(
|
483
533
|
response, data=data, headers=headers, **extra
|
@@ -486,19 +536,20 @@ class Client(RequestFactory):
|
|
486
536
|
|
487
537
|
def post(
|
488
538
|
self,
|
489
|
-
path,
|
490
|
-
data=None,
|
491
|
-
content_type=_MULTIPART_CONTENT,
|
492
|
-
follow=False,
|
493
|
-
secure=True,
|
539
|
+
path: str,
|
540
|
+
data: dict[str, Any] | None = None,
|
541
|
+
content_type: str = _MULTIPART_CONTENT,
|
542
|
+
follow: bool = False,
|
543
|
+
secure: bool = True,
|
494
544
|
*,
|
495
|
-
headers=None,
|
496
|
-
**extra,
|
497
|
-
):
|
545
|
+
headers: dict[str, str] | None = None,
|
546
|
+
**extra: Any,
|
547
|
+
) -> Response:
|
498
548
|
"""Request a response from the server using POST."""
|
499
549
|
self.extra = extra
|
500
550
|
self.headers = headers
|
501
|
-
|
551
|
+
# Build the request using the factory
|
552
|
+
wsgi_request = self._request_factory.post(
|
502
553
|
path,
|
503
554
|
data=data,
|
504
555
|
content_type=content_type,
|
@@ -506,6 +557,8 @@ class Client(RequestFactory):
|
|
506
557
|
headers=headers,
|
507
558
|
**extra,
|
508
559
|
)
|
560
|
+
# Execute and get response
|
561
|
+
response = self.request(**wsgi_request.environ)
|
509
562
|
if follow:
|
510
563
|
response = self._handle_redirects(
|
511
564
|
response, data=data, content_type=content_type, headers=headers, **extra
|
@@ -514,20 +567,23 @@ class Client(RequestFactory):
|
|
514
567
|
|
515
568
|
def head(
|
516
569
|
self,
|
517
|
-
path,
|
518
|
-
data=None,
|
519
|
-
follow=False,
|
520
|
-
secure=True,
|
570
|
+
path: str,
|
571
|
+
data: dict[str, Any] | None = None,
|
572
|
+
follow: bool = False,
|
573
|
+
secure: bool = True,
|
521
574
|
*,
|
522
|
-
headers=None,
|
523
|
-
**extra,
|
524
|
-
):
|
575
|
+
headers: dict[str, str] | None = None,
|
576
|
+
**extra: Any,
|
577
|
+
) -> Response:
|
525
578
|
"""Request a response from the server using HEAD."""
|
526
579
|
self.extra = extra
|
527
580
|
self.headers = headers
|
528
|
-
|
581
|
+
# Build the request using the factory
|
582
|
+
wsgi_request = self._request_factory.head(
|
529
583
|
path, data=data, secure=secure, headers=headers, **extra
|
530
584
|
)
|
585
|
+
# Execute and get response
|
586
|
+
response = self.request(**wsgi_request.environ)
|
531
587
|
if follow:
|
532
588
|
response = self._handle_redirects(
|
533
589
|
response, data=data, headers=headers, **extra
|
@@ -536,19 +592,20 @@ class Client(RequestFactory):
|
|
536
592
|
|
537
593
|
def options(
|
538
594
|
self,
|
539
|
-
path,
|
540
|
-
data="",
|
541
|
-
content_type="application/octet-stream",
|
542
|
-
follow=False,
|
543
|
-
secure=True,
|
595
|
+
path: str,
|
596
|
+
data: Any = "",
|
597
|
+
content_type: str = "application/octet-stream",
|
598
|
+
follow: bool = False,
|
599
|
+
secure: bool = True,
|
544
600
|
*,
|
545
|
-
headers=None,
|
546
|
-
**extra,
|
547
|
-
):
|
601
|
+
headers: dict[str, str] | None = None,
|
602
|
+
**extra: Any,
|
603
|
+
) -> Response:
|
548
604
|
"""Request a response from the server using OPTIONS."""
|
549
605
|
self.extra = extra
|
550
606
|
self.headers = headers
|
551
|
-
|
607
|
+
# Build the request using the factory
|
608
|
+
wsgi_request = self._request_factory.options(
|
552
609
|
path,
|
553
610
|
data=data,
|
554
611
|
content_type=content_type,
|
@@ -556,6 +613,8 @@ class Client(RequestFactory):
|
|
556
613
|
headers=headers,
|
557
614
|
**extra,
|
558
615
|
)
|
616
|
+
# Execute and get response
|
617
|
+
response = self.request(**wsgi_request.environ)
|
559
618
|
if follow:
|
560
619
|
response = self._handle_redirects(
|
561
620
|
response, data=data, content_type=content_type, headers=headers, **extra
|
@@ -564,19 +623,20 @@ class Client(RequestFactory):
|
|
564
623
|
|
565
624
|
def put(
|
566
625
|
self,
|
567
|
-
path,
|
568
|
-
data="",
|
569
|
-
content_type="application/octet-stream",
|
570
|
-
follow=False,
|
571
|
-
secure=True,
|
626
|
+
path: str,
|
627
|
+
data: Any = "",
|
628
|
+
content_type: str = "application/octet-stream",
|
629
|
+
follow: bool = False,
|
630
|
+
secure: bool = True,
|
572
631
|
*,
|
573
|
-
headers=None,
|
574
|
-
**extra,
|
575
|
-
):
|
632
|
+
headers: dict[str, str] | None = None,
|
633
|
+
**extra: Any,
|
634
|
+
) -> Response:
|
576
635
|
"""Send a resource to the server using PUT."""
|
577
636
|
self.extra = extra
|
578
637
|
self.headers = headers
|
579
|
-
|
638
|
+
# Build the request using the factory
|
639
|
+
wsgi_request = self._request_factory.put(
|
580
640
|
path,
|
581
641
|
data=data,
|
582
642
|
content_type=content_type,
|
@@ -584,6 +644,8 @@ class Client(RequestFactory):
|
|
584
644
|
headers=headers,
|
585
645
|
**extra,
|
586
646
|
)
|
647
|
+
# Execute and get response
|
648
|
+
response = self.request(**wsgi_request.environ)
|
587
649
|
if follow:
|
588
650
|
response = self._handle_redirects(
|
589
651
|
response, data=data, content_type=content_type, headers=headers, **extra
|
@@ -592,19 +654,20 @@ class Client(RequestFactory):
|
|
592
654
|
|
593
655
|
def patch(
|
594
656
|
self,
|
595
|
-
path,
|
596
|
-
data="",
|
597
|
-
content_type="application/octet-stream",
|
598
|
-
follow=False,
|
599
|
-
secure=True,
|
657
|
+
path: str,
|
658
|
+
data: Any = "",
|
659
|
+
content_type: str = "application/octet-stream",
|
660
|
+
follow: bool = False,
|
661
|
+
secure: bool = True,
|
600
662
|
*,
|
601
|
-
headers=None,
|
602
|
-
**extra,
|
603
|
-
):
|
663
|
+
headers: dict[str, str] | None = None,
|
664
|
+
**extra: Any,
|
665
|
+
) -> Response:
|
604
666
|
"""Send a resource to the server using PATCH."""
|
605
667
|
self.extra = extra
|
606
668
|
self.headers = headers
|
607
|
-
|
669
|
+
# Build the request using the factory
|
670
|
+
wsgi_request = self._request_factory.patch(
|
608
671
|
path,
|
609
672
|
data=data,
|
610
673
|
content_type=content_type,
|
@@ -612,6 +675,8 @@ class Client(RequestFactory):
|
|
612
675
|
headers=headers,
|
613
676
|
**extra,
|
614
677
|
)
|
678
|
+
# Execute and get response
|
679
|
+
response = self.request(**wsgi_request.environ)
|
615
680
|
if follow:
|
616
681
|
response = self._handle_redirects(
|
617
682
|
response, data=data, content_type=content_type, headers=headers, **extra
|
@@ -620,19 +685,20 @@ class Client(RequestFactory):
|
|
620
685
|
|
621
686
|
def delete(
|
622
687
|
self,
|
623
|
-
path,
|
624
|
-
data="",
|
625
|
-
content_type="application/octet-stream",
|
626
|
-
follow=False,
|
627
|
-
secure=True,
|
688
|
+
path: str,
|
689
|
+
data: Any = "",
|
690
|
+
content_type: str = "application/octet-stream",
|
691
|
+
follow: bool = False,
|
692
|
+
secure: bool = True,
|
628
693
|
*,
|
629
|
-
headers=None,
|
630
|
-
**extra,
|
631
|
-
):
|
694
|
+
headers: dict[str, str] | None = None,
|
695
|
+
**extra: Any,
|
696
|
+
) -> Response:
|
632
697
|
"""Send a DELETE request to the server."""
|
633
698
|
self.extra = extra
|
634
699
|
self.headers = headers
|
635
|
-
|
700
|
+
# Build the request using the factory
|
701
|
+
wsgi_request = self._request_factory.delete(
|
636
702
|
path,
|
637
703
|
data=data,
|
638
704
|
content_type=content_type,
|
@@ -640,6 +706,8 @@ class Client(RequestFactory):
|
|
640
706
|
headers=headers,
|
641
707
|
**extra,
|
642
708
|
)
|
709
|
+
# Execute and get response
|
710
|
+
response = self.request(**wsgi_request.environ)
|
643
711
|
if follow:
|
644
712
|
response = self._handle_redirects(
|
645
713
|
response, data=data, content_type=content_type, headers=headers, **extra
|
@@ -648,20 +716,23 @@ class Client(RequestFactory):
|
|
648
716
|
|
649
717
|
def trace(
|
650
718
|
self,
|
651
|
-
path,
|
652
|
-
data="",
|
653
|
-
follow=False,
|
654
|
-
secure=True,
|
719
|
+
path: str,
|
720
|
+
data: Any = "",
|
721
|
+
follow: bool = False,
|
722
|
+
secure: bool = True,
|
655
723
|
*,
|
656
|
-
headers=None,
|
657
|
-
**extra,
|
658
|
-
):
|
724
|
+
headers: dict[str, str] | None = None,
|
725
|
+
**extra: Any,
|
726
|
+
) -> Response:
|
659
727
|
"""Send a TRACE request to the server."""
|
660
728
|
self.extra = extra
|
661
729
|
self.headers = headers
|
662
|
-
|
730
|
+
# Build the request using the factory
|
731
|
+
wsgi_request = self._request_factory.trace(
|
663
732
|
path, data=data, secure=secure, headers=headers, **extra
|
664
733
|
)
|
734
|
+
# Execute and get response
|
735
|
+
response = self.request(**wsgi_request.environ)
|
665
736
|
if follow:
|
666
737
|
response = self._handle_redirects(
|
667
738
|
response, data=data, headers=headers, **extra
|
@@ -670,16 +741,16 @@ class Client(RequestFactory):
|
|
670
741
|
|
671
742
|
def _handle_redirects(
|
672
743
|
self,
|
673
|
-
response,
|
674
|
-
data="",
|
675
|
-
content_type="",
|
676
|
-
headers=None,
|
677
|
-
**extra,
|
678
|
-
):
|
744
|
+
response: Response,
|
745
|
+
data: Any = "",
|
746
|
+
content_type: str = "",
|
747
|
+
headers: dict[str, str] | None = None,
|
748
|
+
**extra: Any,
|
749
|
+
) -> Response:
|
679
750
|
"""
|
680
751
|
Follow any redirects by requesting responses from the server using GET.
|
681
752
|
"""
|
682
|
-
response.redirect_chain = []
|
753
|
+
response.redirect_chain = [] # type: ignore[attr-defined]
|
683
754
|
redirect_status_codes = (
|
684
755
|
HTTPStatus.MOVED_PERMANENTLY,
|
685
756
|
HTTPStatus.FOUND,
|
@@ -688,8 +759,8 @@ class Client(RequestFactory):
|
|
688
759
|
HTTPStatus.PERMANENT_REDIRECT,
|
689
760
|
)
|
690
761
|
while response.status_code in redirect_status_codes:
|
691
|
-
response_url = response.url
|
692
|
-
redirect_chain = response.redirect_chain
|
762
|
+
response_url = response.url # type: ignore[attr-defined]
|
763
|
+
redirect_chain = response.redirect_chain # type: ignore[attr-defined]
|
693
764
|
redirect_chain.append((response_url, response.status_code))
|
694
765
|
|
695
766
|
url = urlsplit(response_url)
|
@@ -706,7 +777,7 @@ class Client(RequestFactory):
|
|
706
777
|
path = "/"
|
707
778
|
# Prepend the request path to handle relative path redirects
|
708
779
|
if not path.startswith("/"):
|
709
|
-
path = urljoin(response.request["PATH_INFO"], path)
|
780
|
+
path = urljoin(response.request["PATH_INFO"], path) # type: ignore[attr-defined]
|
710
781
|
|
711
782
|
if response.status_code in (
|
712
783
|
HTTPStatus.TEMPORARY_REDIRECT,
|
@@ -714,24 +785,24 @@ class Client(RequestFactory):
|
|
714
785
|
):
|
715
786
|
# Preserve request method and query string (if needed)
|
716
787
|
# post-redirect for 307/308 responses.
|
717
|
-
|
718
|
-
if
|
788
|
+
request_method_name = response.request["REQUEST_METHOD"].lower() # type: ignore[attr-defined]
|
789
|
+
if request_method_name not in ("get", "head"):
|
719
790
|
extra["QUERY_STRING"] = url.query
|
720
|
-
request_method = getattr(self,
|
791
|
+
request_method = getattr(self, request_method_name)
|
721
792
|
else:
|
722
793
|
request_method = self.get
|
723
794
|
data = QueryDict(url.query)
|
724
|
-
content_type =
|
795
|
+
content_type = "" # type: ignore[assignment]
|
725
796
|
|
726
797
|
response = request_method(
|
727
798
|
path,
|
728
799
|
data=data,
|
729
|
-
content_type=content_type,
|
800
|
+
content_type=content_type, # type: ignore[arg-type]
|
730
801
|
follow=False,
|
731
802
|
headers=headers,
|
732
803
|
**extra,
|
733
804
|
)
|
734
|
-
response.redirect_chain = redirect_chain
|
805
|
+
response.redirect_chain = redirect_chain # type: ignore[attr-defined]
|
735
806
|
|
736
807
|
if redirect_chain[-1] in redirect_chain[:-1]:
|
737
808
|
# Check that we're not redirecting to somewhere we've already
|
@@ -747,17 +818,17 @@ class Client(RequestFactory):
|
|
747
818
|
|
748
819
|
return response
|
749
820
|
|
750
|
-
def store_exc_info(self, **kwargs):
|
821
|
+
def store_exc_info(self, **kwargs: Any) -> None:
|
751
822
|
"""Store exceptions when they are generated by a view."""
|
752
823
|
self.exc_info = sys.exc_info()
|
753
824
|
|
754
|
-
def check_exception(self, response):
|
825
|
+
def check_exception(self, response: Response) -> None:
|
755
826
|
"""
|
756
827
|
Look for a signaled exception, clear the current context exception
|
757
828
|
data, re-raise the signaled exception, and clear the signaled exception
|
758
829
|
from the local cache.
|
759
830
|
"""
|
760
|
-
response.exc_info = self.exc_info
|
831
|
+
response.exc_info = self.exc_info # type: ignore[attr-defined]
|
761
832
|
if self.exc_info:
|
762
833
|
_, exc_value, _ = self.exc_info
|
763
834
|
self.exc_info = None
|
@@ -765,24 +836,24 @@ class Client(RequestFactory):
|
|
765
836
|
raise exc_value
|
766
837
|
|
767
838
|
@property
|
768
|
-
def session(self):
|
839
|
+
def session(self) -> Any:
|
769
840
|
"""Return the current session variables."""
|
770
841
|
from plain.sessions.test import get_client_session
|
771
842
|
|
772
843
|
return get_client_session(self)
|
773
844
|
|
774
|
-
def force_login(self, user):
|
845
|
+
def force_login(self, user: Any) -> None:
|
775
846
|
from plain.auth.test import login_client
|
776
847
|
|
777
848
|
login_client(self, user)
|
778
849
|
|
779
|
-
def logout(self):
|
850
|
+
def logout(self) -> None:
|
780
851
|
"""Log out the user by removing the cookies and session object."""
|
781
852
|
from plain.auth.test import logout_client
|
782
853
|
|
783
854
|
logout_client(self)
|
784
855
|
|
785
|
-
def _parse_json(self, response, **extra):
|
856
|
+
def _parse_json(self, response: Response, **extra: Any) -> Any:
|
786
857
|
if not hasattr(response, "_json"):
|
787
858
|
if not _JSON_CONTENT_TYPE_RE.match(response.headers.get("Content-Type")):
|
788
859
|
raise ValueError(
|
@@ -790,7 +861,8 @@ class Client(RequestFactory):
|
|
790
861
|
response.headers.get("Content-Type")
|
791
862
|
)
|
792
863
|
)
|
793
|
-
response._json = json.loads(
|
794
|
-
response.content.decode(response.charset),
|
864
|
+
response._json = json.loads( # type: ignore[attr-defined]
|
865
|
+
response.content.decode(response.charset),
|
866
|
+
**extra, # type: ignore[arg-type]
|
795
867
|
)
|
796
|
-
return response._json
|
868
|
+
return response._json # type: ignore[attr-defined]
|