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/http/request.py
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import codecs
|
2
4
|
import copy
|
3
5
|
import json
|
4
6
|
import uuid
|
7
|
+
from collections.abc import Iterator
|
5
8
|
from functools import cached_property
|
6
9
|
from io import BytesIO
|
7
10
|
from itertools import chain
|
11
|
+
from typing import IO, Any
|
8
12
|
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
|
9
13
|
|
10
14
|
from plain.exceptions import (
|
@@ -73,19 +77,19 @@ class HttpRequest:
|
|
73
77
|
self.content_type = None
|
74
78
|
self.content_params = None
|
75
79
|
|
76
|
-
def __repr__(self):
|
80
|
+
def __repr__(self) -> str:
|
77
81
|
if self.method is None or not self.get_full_path():
|
78
82
|
return f"<{self.__class__.__name__}>"
|
79
83
|
return f"<{self.__class__.__name__}: {self.method} {self.get_full_path()!r}>"
|
80
84
|
|
81
|
-
def __getstate__(self):
|
85
|
+
def __getstate__(self) -> dict[str, Any]:
|
82
86
|
obj_dict = self.__dict__.copy()
|
83
87
|
for attr in self.non_picklable_attrs:
|
84
88
|
if attr in obj_dict:
|
85
89
|
del obj_dict[attr]
|
86
90
|
return obj_dict
|
87
91
|
|
88
|
-
def __deepcopy__(self, memo):
|
92
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> HttpRequest:
|
89
93
|
obj = copy.copy(self)
|
90
94
|
for attr in self.non_picklable_attrs:
|
91
95
|
if hasattr(self, attr):
|
@@ -94,20 +98,20 @@ class HttpRequest:
|
|
94
98
|
return obj
|
95
99
|
|
96
100
|
@cached_property
|
97
|
-
def headers(self):
|
101
|
+
def headers(self) -> HttpHeaders:
|
98
102
|
return HttpHeaders(self.meta)
|
99
103
|
|
100
104
|
@cached_property
|
101
|
-
def accepted_types(self):
|
105
|
+
def accepted_types(self) -> list[MediaType]:
|
102
106
|
"""Return a list of MediaType instances."""
|
103
107
|
return parse_accept_header(self.headers.get("Accept", "*/*"))
|
104
108
|
|
105
|
-
def accepts(self, media_type):
|
109
|
+
def accepts(self, media_type: str) -> bool:
|
106
110
|
return any(
|
107
111
|
accepted_type.match(media_type) for accepted_type in self.accepted_types
|
108
112
|
)
|
109
113
|
|
110
|
-
def _set_content_type_params(self, meta):
|
114
|
+
def _set_content_type_params(self, meta: dict[str, Any]) -> None:
|
111
115
|
"""Set content_type, content_params, and encoding."""
|
112
116
|
self.content_type, self.content_params = parse_header_parameters(
|
113
117
|
meta.get("CONTENT_TYPE", "")
|
@@ -121,7 +125,7 @@ class HttpRequest:
|
|
121
125
|
self.encoding = self.content_params["charset"]
|
122
126
|
|
123
127
|
@cached_property
|
124
|
-
def host(self):
|
128
|
+
def host(self) -> str:
|
125
129
|
"""
|
126
130
|
Return the HTTP host using the environment or request headers.
|
127
131
|
|
@@ -142,7 +146,7 @@ class HttpRequest:
|
|
142
146
|
return host
|
143
147
|
|
144
148
|
@cached_property
|
145
|
-
def port(self):
|
149
|
+
def port(self) -> str:
|
146
150
|
"""Return the port number for the request as a string."""
|
147
151
|
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.meta:
|
148
152
|
port = self.meta["HTTP_X_FORWARDED_PORT"]
|
@@ -150,7 +154,7 @@ class HttpRequest:
|
|
150
154
|
port = self.meta["SERVER_PORT"]
|
151
155
|
return str(port)
|
152
156
|
|
153
|
-
def get_full_path(self, force_append_slash=False):
|
157
|
+
def get_full_path(self, force_append_slash: bool = False) -> str:
|
154
158
|
"""
|
155
159
|
Return the full path for the request, including query string.
|
156
160
|
|
@@ -160,7 +164,7 @@ class HttpRequest:
|
|
160
164
|
# RFC 3986 requires query string arguments to be in the ASCII range.
|
161
165
|
# Rather than crash if this doesn't happen, we encode defensively.
|
162
166
|
|
163
|
-
def escape_uri_path(path):
|
167
|
+
def escape_uri_path(path: str) -> str:
|
164
168
|
"""
|
165
169
|
Escape the unsafe characters from the path portion of a Uniform Resource
|
166
170
|
Identifier (URI).
|
@@ -176,15 +180,14 @@ class HttpRequest:
|
|
176
180
|
# the entire path, not a path segment.
|
177
181
|
return quote(path, safe="/:@&+$,-_.!~*'()")
|
178
182
|
|
183
|
+
query_string = self.meta.get("QUERY_STRING", "")
|
179
184
|
return "{}{}{}".format(
|
180
185
|
escape_uri_path(self.path),
|
181
186
|
"/" if force_append_slash and not self.path.endswith("/") else "",
|
182
|
-
("?" + iri_to_uri(
|
183
|
-
if self.meta.get("QUERY_STRING", "")
|
184
|
-
else "",
|
187
|
+
("?" + (iri_to_uri(query_string) or "")) if query_string else "",
|
185
188
|
)
|
186
189
|
|
187
|
-
def build_absolute_uri(self, location=None):
|
190
|
+
def build_absolute_uri(self, location: str | None = None) -> str:
|
188
191
|
"""
|
189
192
|
Build an absolute URI from the location and the variables available in
|
190
193
|
this request. If no ``location`` is specified, build the absolute URI
|
@@ -224,9 +227,9 @@ class HttpRequest:
|
|
224
227
|
# base path.
|
225
228
|
location = urljoin(current_scheme_host + self.path, location)
|
226
229
|
|
227
|
-
return iri_to_uri(location)
|
230
|
+
return iri_to_uri(location) or ""
|
228
231
|
|
229
|
-
def _get_scheme(self):
|
232
|
+
def _get_scheme(self) -> str:
|
230
233
|
"""
|
231
234
|
Hook for subclasses like WSGIRequest to implement. Return 'http' by
|
232
235
|
default.
|
@@ -234,7 +237,7 @@ class HttpRequest:
|
|
234
237
|
return "http"
|
235
238
|
|
236
239
|
@property
|
237
|
-
def scheme(self):
|
240
|
+
def scheme(self) -> str:
|
238
241
|
if settings.HTTPS_PROXY_HEADER:
|
239
242
|
try:
|
240
243
|
header, secure_value = settings.HTTPS_PROXY_HEADER
|
@@ -249,15 +252,15 @@ class HttpRequest:
|
|
249
252
|
return "https" if header_value.strip() == secure_value else "http"
|
250
253
|
return self._get_scheme()
|
251
254
|
|
252
|
-
def is_https(self):
|
255
|
+
def is_https(self) -> bool:
|
253
256
|
return self.scheme == "https"
|
254
257
|
|
255
258
|
@property
|
256
|
-
def encoding(self):
|
259
|
+
def encoding(self) -> str | None:
|
257
260
|
return self._encoding
|
258
261
|
|
259
262
|
@encoding.setter
|
260
|
-
def encoding(self, val):
|
263
|
+
def encoding(self, val: str) -> None:
|
261
264
|
"""
|
262
265
|
Set the encoding used for query_params/data accesses. If the query_params or data
|
263
266
|
dictionary has already been created, remove and recreate it on the
|
@@ -269,21 +272,21 @@ class HttpRequest:
|
|
269
272
|
if hasattr(self, "_data"):
|
270
273
|
del self._data
|
271
274
|
|
272
|
-
def _initialize_handlers(self):
|
275
|
+
def _initialize_handlers(self) -> None:
|
273
276
|
self._upload_handlers = [
|
274
277
|
uploadhandler.load_handler(handler, self)
|
275
278
|
for handler in settings.FILE_UPLOAD_HANDLERS
|
276
279
|
]
|
277
280
|
|
278
281
|
@property
|
279
|
-
def upload_handlers(self):
|
282
|
+
def upload_handlers(self) -> list[Any]:
|
280
283
|
if not self._upload_handlers:
|
281
284
|
# If there are no upload handlers defined, initialize them from settings.
|
282
285
|
self._initialize_handlers()
|
283
286
|
return self._upload_handlers
|
284
287
|
|
285
288
|
@upload_handlers.setter
|
286
|
-
def upload_handlers(self, upload_handlers):
|
289
|
+
def upload_handlers(self, upload_handlers: list[Any]) -> None:
|
287
290
|
if hasattr(self, "_files"):
|
288
291
|
raise AttributeError(
|
289
292
|
"You cannot set the upload handlers after the upload has been "
|
@@ -291,7 +294,9 @@ class HttpRequest:
|
|
291
294
|
)
|
292
295
|
self._upload_handlers = upload_handlers
|
293
296
|
|
294
|
-
def parse_file_upload(
|
297
|
+
def parse_file_upload(
|
298
|
+
self, meta: dict[str, Any], post_data: IO[bytes]
|
299
|
+
) -> tuple[Any, MultiValueDict]:
|
295
300
|
"""Return a tuple of (data QueryDict, files MultiValueDict)."""
|
296
301
|
self.upload_handlers = ImmutableList(
|
297
302
|
self.upload_handlers,
|
@@ -303,7 +308,7 @@ class HttpRequest:
|
|
303
308
|
return parser.parse()
|
304
309
|
|
305
310
|
@property
|
306
|
-
def body(self):
|
311
|
+
def body(self) -> bytes:
|
307
312
|
if not hasattr(self, "_body"):
|
308
313
|
if self._read_started:
|
309
314
|
raise RawPostDataException(
|
@@ -329,11 +334,11 @@ class HttpRequest:
|
|
329
334
|
self._stream = BytesIO(self._body)
|
330
335
|
return self._body
|
331
336
|
|
332
|
-
def _mark_post_parse_error(self):
|
337
|
+
def _mark_post_parse_error(self) -> None:
|
333
338
|
self._data = QueryDict()
|
334
339
|
self._files = MultiValueDict()
|
335
340
|
|
336
|
-
def _load_data_and_files(self):
|
341
|
+
def _load_data_and_files(self) -> None:
|
337
342
|
"""Populate self._data and self._files"""
|
338
343
|
|
339
344
|
if self._read_started and not hasattr(self, "_body"):
|
@@ -373,7 +378,7 @@ class HttpRequest:
|
|
373
378
|
MultiValueDict(),
|
374
379
|
)
|
375
380
|
|
376
|
-
def close(self):
|
381
|
+
def close(self) -> None:
|
377
382
|
if hasattr(self, "_files"):
|
378
383
|
for f in chain.from_iterable(list_[1] for list_ in self._files.lists()):
|
379
384
|
f.close()
|
@@ -386,27 +391,33 @@ class HttpRequest:
|
|
386
391
|
# request.body, self._stream points to a BytesIO instance
|
387
392
|
# containing that data.
|
388
393
|
|
389
|
-
def read(self, *args, **kwargs):
|
394
|
+
def read(self, *args: Any, **kwargs: Any) -> bytes:
|
390
395
|
self._read_started = True
|
391
396
|
try:
|
392
397
|
return self._stream.read(*args, **kwargs)
|
393
398
|
except OSError as e:
|
394
399
|
raise UnreadablePostError(*e.args) from e
|
395
400
|
|
396
|
-
def readline(self, *args, **kwargs):
|
401
|
+
def readline(self, *args: Any, **kwargs: Any) -> bytes:
|
397
402
|
self._read_started = True
|
398
403
|
try:
|
399
404
|
return self._stream.readline(*args, **kwargs)
|
400
405
|
except OSError as e:
|
401
406
|
raise UnreadablePostError(*e.args) from e
|
402
407
|
|
403
|
-
def __iter__(self):
|
408
|
+
def __iter__(self) -> Iterator[bytes]:
|
404
409
|
return iter(self.readline, b"")
|
405
410
|
|
406
|
-
def readlines(self):
|
411
|
+
def readlines(self) -> list[bytes]:
|
407
412
|
return list(self)
|
408
413
|
|
409
|
-
def get_signed_cookie(
|
414
|
+
def get_signed_cookie(
|
415
|
+
self,
|
416
|
+
key: str,
|
417
|
+
default: str | None = None,
|
418
|
+
salt: str = "",
|
419
|
+
max_age: int | None = None,
|
420
|
+
) -> str | None:
|
410
421
|
"""
|
411
422
|
Retrieve a cookie value signed with the SECRET_KEY.
|
412
423
|
|
@@ -426,7 +437,7 @@ class HttpHeaders(CaseInsensitiveMapping):
|
|
426
437
|
# PEP 333 gives two headers which aren't prepended with HTTP_.
|
427
438
|
UNPREFIXED_HEADERS = {"CONTENT_TYPE", "CONTENT_LENGTH"}
|
428
439
|
|
429
|
-
def __init__(self, environ):
|
440
|
+
def __init__(self, environ: dict[str, Any]):
|
430
441
|
headers = {}
|
431
442
|
for header, value in environ.items():
|
432
443
|
name = self.parse_header_name(header)
|
@@ -434,12 +445,12 @@ class HttpHeaders(CaseInsensitiveMapping):
|
|
434
445
|
headers[name] = value
|
435
446
|
super().__init__(headers)
|
436
447
|
|
437
|
-
def __getitem__(self, key):
|
448
|
+
def __getitem__(self, key: str) -> str:
|
438
449
|
"""Allow header lookup using underscores in place of hyphens."""
|
439
450
|
return super().__getitem__(key.replace("_", "-"))
|
440
451
|
|
441
452
|
@classmethod
|
442
|
-
def parse_header_name(cls, header):
|
453
|
+
def parse_header_name(cls, header: str) -> str | None:
|
443
454
|
if header.startswith(cls.HTTP_PREFIX):
|
444
455
|
header = header.removeprefix(cls.HTTP_PREFIX)
|
445
456
|
elif header not in cls.UNPREFIXED_HEADERS:
|
@@ -447,14 +458,14 @@ class HttpHeaders(CaseInsensitiveMapping):
|
|
447
458
|
return header.replace("_", "-").title()
|
448
459
|
|
449
460
|
@classmethod
|
450
|
-
def to_wsgi_name(cls, header):
|
461
|
+
def to_wsgi_name(cls, header: str) -> str:
|
451
462
|
header = header.replace("-", "_").upper()
|
452
463
|
if header in cls.UNPREFIXED_HEADERS:
|
453
464
|
return header
|
454
465
|
return f"{cls.HTTP_PREFIX}{header}"
|
455
466
|
|
456
467
|
@classmethod
|
457
|
-
def to_wsgi_names(cls, headers):
|
468
|
+
def to_wsgi_names(cls, headers: dict[str, Any]) -> dict[str, Any]:
|
458
469
|
return {
|
459
470
|
cls.to_wsgi_name(header_name): value
|
460
471
|
for header_name, value in headers.items()
|
@@ -481,7 +492,12 @@ class QueryDict(MultiValueDict):
|
|
481
492
|
_mutable = True
|
482
493
|
_encoding = None
|
483
494
|
|
484
|
-
def __init__(
|
495
|
+
def __init__(
|
496
|
+
self,
|
497
|
+
query_string: str | bytes | None = None,
|
498
|
+
mutable: bool = False,
|
499
|
+
encoding: str | None = None,
|
500
|
+
):
|
485
501
|
super().__init__()
|
486
502
|
self.encoding = encoding or settings.DEFAULT_CHARSET
|
487
503
|
query_string = query_string or ""
|
@@ -492,11 +508,12 @@ class QueryDict(MultiValueDict):
|
|
492
508
|
}
|
493
509
|
if isinstance(query_string, bytes):
|
494
510
|
# query_string normally contains URL-encoded data, a subset of ASCII.
|
511
|
+
query_bytes = query_string
|
495
512
|
try:
|
496
|
-
query_string =
|
513
|
+
query_string = query_bytes.decode(self.encoding)
|
497
514
|
except UnicodeDecodeError:
|
498
515
|
# ... but some user agents are misbehaving :-(
|
499
|
-
query_string =
|
516
|
+
query_string = query_bytes.decode("iso-8859-1")
|
500
517
|
try:
|
501
518
|
for key, value in parse_qsl(query_string, **parse_qsl_kwargs):
|
502
519
|
self.appendlist(key, value)
|
@@ -512,7 +529,13 @@ class QueryDict(MultiValueDict):
|
|
512
529
|
self._mutable = mutable
|
513
530
|
|
514
531
|
@classmethod
|
515
|
-
def fromkeys(
|
532
|
+
def fromkeys(
|
533
|
+
cls,
|
534
|
+
iterable: Any,
|
535
|
+
value: str = "",
|
536
|
+
mutable: bool = False,
|
537
|
+
encoding: str | None = None,
|
538
|
+
) -> QueryDict:
|
516
539
|
"""
|
517
540
|
Return a new QueryDict with keys (may be repeated) from an iterable and
|
518
541
|
values from value.
|
@@ -525,81 +548,83 @@ class QueryDict(MultiValueDict):
|
|
525
548
|
return q
|
526
549
|
|
527
550
|
@property
|
528
|
-
def encoding(self):
|
551
|
+
def encoding(self) -> str:
|
529
552
|
if self._encoding is None:
|
530
553
|
self._encoding = settings.DEFAULT_CHARSET
|
531
554
|
return self._encoding
|
532
555
|
|
533
556
|
@encoding.setter
|
534
|
-
def encoding(self, value):
|
557
|
+
def encoding(self, value: str) -> None:
|
535
558
|
self._encoding = value
|
536
559
|
|
537
|
-
def _assert_mutable(self):
|
560
|
+
def _assert_mutable(self) -> None:
|
538
561
|
if not self._mutable:
|
539
562
|
raise AttributeError("This QueryDict instance is immutable")
|
540
563
|
|
541
|
-
def __setitem__(self, key, value):
|
564
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
542
565
|
self._assert_mutable()
|
543
566
|
key = bytes_to_text(key, self.encoding)
|
544
567
|
value = bytes_to_text(value, self.encoding)
|
545
568
|
super().__setitem__(key, value)
|
546
569
|
|
547
|
-
def __delitem__(self, key):
|
570
|
+
def __delitem__(self, key: str) -> None:
|
548
571
|
self._assert_mutable()
|
549
572
|
super().__delitem__(key)
|
550
573
|
|
551
|
-
def __copy__(self):
|
574
|
+
def __copy__(self) -> QueryDict:
|
552
575
|
result = self.__class__("", mutable=True, encoding=self.encoding)
|
553
576
|
for key, value in self.lists():
|
554
577
|
result.setlist(key, value)
|
555
578
|
return result
|
556
579
|
|
557
|
-
def __deepcopy__(self, memo):
|
580
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> QueryDict:
|
558
581
|
result = self.__class__("", mutable=True, encoding=self.encoding)
|
559
582
|
memo[id(self)] = result
|
560
583
|
for key, value in self.lists():
|
561
584
|
result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo))
|
562
585
|
return result
|
563
586
|
|
564
|
-
def setlist(self, key, list_):
|
587
|
+
def setlist(self, key: str, list_: list[Any]) -> None:
|
565
588
|
self._assert_mutable()
|
566
589
|
key = bytes_to_text(key, self.encoding)
|
567
590
|
list_ = [bytes_to_text(elt, self.encoding) for elt in list_]
|
568
591
|
super().setlist(key, list_)
|
569
592
|
|
570
|
-
def setlistdefault(
|
593
|
+
def setlistdefault(
|
594
|
+
self, key: str, default_list: list[Any] | None = None
|
595
|
+
) -> list[Any]:
|
571
596
|
self._assert_mutable()
|
572
597
|
return super().setlistdefault(key, default_list)
|
573
598
|
|
574
|
-
def appendlist(self, key, value):
|
599
|
+
def appendlist(self, key: str, value: Any) -> None:
|
575
600
|
self._assert_mutable()
|
576
601
|
key = bytes_to_text(key, self.encoding)
|
577
602
|
value = bytes_to_text(value, self.encoding)
|
578
603
|
super().appendlist(key, value)
|
579
604
|
|
580
|
-
def pop(self, key, *args):
|
605
|
+
def pop(self, key: str, *args: Any) -> Any:
|
581
606
|
self._assert_mutable()
|
582
607
|
return super().pop(key, *args)
|
583
608
|
|
584
|
-
def popitem(self):
|
609
|
+
def popitem(self) -> tuple[str, Any]:
|
585
610
|
self._assert_mutable()
|
586
611
|
return super().popitem()
|
587
612
|
|
588
|
-
def clear(self):
|
613
|
+
def clear(self) -> None:
|
589
614
|
self._assert_mutable()
|
590
615
|
super().clear()
|
591
616
|
|
592
|
-
def setdefault(self, key, default=None):
|
617
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
593
618
|
self._assert_mutable()
|
594
619
|
key = bytes_to_text(key, self.encoding)
|
595
620
|
default = bytes_to_text(default, self.encoding)
|
596
621
|
return super().setdefault(key, default)
|
597
622
|
|
598
|
-
def copy(self):
|
623
|
+
def copy(self) -> QueryDict:
|
599
624
|
"""Return a mutable copy of this object."""
|
600
625
|
return self.__deepcopy__({})
|
601
626
|
|
602
|
-
def urlencode(self, safe=None):
|
627
|
+
def urlencode(self, safe: str | None = None) -> str:
|
603
628
|
"""
|
604
629
|
Return an encoded string of all query string arguments.
|
605
630
|
|
@@ -614,14 +639,14 @@ class QueryDict(MultiValueDict):
|
|
614
639
|
"""
|
615
640
|
output = []
|
616
641
|
if safe:
|
617
|
-
|
642
|
+
safe_bytes: bytes = safe.encode(self.encoding)
|
618
643
|
|
619
|
-
def encode(k, v):
|
620
|
-
return f"{quote(k,
|
644
|
+
def encode(k: bytes, v: bytes) -> str:
|
645
|
+
return f"{quote(k, safe_bytes)}={quote(v, safe_bytes)}"
|
621
646
|
|
622
647
|
else:
|
623
648
|
|
624
|
-
def encode(k, v):
|
649
|
+
def encode(k: bytes, v: bytes) -> str:
|
625
650
|
return urlencode({k: v})
|
626
651
|
|
627
652
|
for k, list_ in self.lists():
|
@@ -633,13 +658,12 @@ class QueryDict(MultiValueDict):
|
|
633
658
|
|
634
659
|
|
635
660
|
class MediaType:
|
636
|
-
def __init__(self, media_type_raw_line):
|
637
|
-
|
638
|
-
|
639
|
-
)
|
661
|
+
def __init__(self, media_type_raw_line: str | MediaType):
|
662
|
+
line = str(media_type_raw_line) if media_type_raw_line else ""
|
663
|
+
full_type, self.params = parse_header_parameters(line)
|
640
664
|
self.main_type, _, self.sub_type = full_type.partition("/")
|
641
665
|
|
642
|
-
def __str__(self):
|
666
|
+
def __str__(self) -> str:
|
643
667
|
params_str = "".join(f"; {k}={v}" for k, v in self.params.items())
|
644
668
|
return "{}{}{}".format(
|
645
669
|
self.main_type,
|
@@ -647,14 +671,14 @@ class MediaType:
|
|
647
671
|
params_str,
|
648
672
|
)
|
649
673
|
|
650
|
-
def __repr__(self):
|
674
|
+
def __repr__(self) -> str:
|
651
675
|
return f"<{self.__class__.__qualname__}: {self}>"
|
652
676
|
|
653
677
|
@property
|
654
|
-
def is_all_types(self):
|
678
|
+
def is_all_types(self) -> bool:
|
655
679
|
return self.main_type == "*" and self.sub_type == "*"
|
656
680
|
|
657
|
-
def match(self, other):
|
681
|
+
def match(self, other: str | MediaType) -> bool:
|
658
682
|
if self.is_all_types:
|
659
683
|
return True
|
660
684
|
other = MediaType(other)
|
@@ -666,7 +690,7 @@ class MediaType:
|
|
666
690
|
# It's neither necessary nor appropriate to use
|
667
691
|
# plain.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
|
668
692
|
# this slightly more restricted function, used by QueryDict.
|
669
|
-
def bytes_to_text(s, encoding):
|
693
|
+
def bytes_to_text(s: Any, encoding: str) -> str:
|
670
694
|
"""
|
671
695
|
Convert bytes objects to strings, using the given encoding. Illegally
|
672
696
|
encoded input characters are replaced with Unicode "unknown" codepoint
|
@@ -680,5 +704,5 @@ def bytes_to_text(s, encoding):
|
|
680
704
|
return s
|
681
705
|
|
682
706
|
|
683
|
-
def parse_accept_header(header):
|
707
|
+
def parse_accept_header(header: str) -> list[MediaType]:
|
684
708
|
return [MediaType(token) for token in header.split(",") if token.strip()]
|