plain 0.69.0__py3-none-any.whl → 0.71.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 +28 -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 +19 -8
- 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/README.md +1 -1
- plain/http/__init__.py +4 -4
- plain/http/cookie.py +15 -7
- plain/http/multipartparser.py +50 -30
- plain/http/request.py +156 -108
- 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 +50 -29
- plain/internal/files/utils.py +13 -6
- plain/internal/handlers/base.py +21 -7
- plain/internal/handlers/exception.py +19 -5
- plain/internal/handlers/wsgi.py +33 -21
- plain/internal/middleware/headers.py +11 -2
- plain/internal/middleware/hosts.py +12 -4
- 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 +7 -6
- plain/runtime/global_settings.py +6 -9
- 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 +249 -177
- 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 -8
- plain/views/errors.py +11 -4
- plain/views/exceptions.py +4 -1
- plain/views/objects.py +15 -15
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/METADATA +1 -1
- plain-0.71.0.dist-info/RECORD +169 -0
- plain-0.69.0.dist-info/RECORD +0 -169
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/WHEEL +0 -0
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/entry_points.txt +0 -0
- {plain-0.69.0.dist-info → plain-0.71.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 (
|
@@ -43,7 +47,7 @@ class RawPostDataException(Exception):
|
|
43
47
|
pass
|
44
48
|
|
45
49
|
|
46
|
-
class
|
50
|
+
class Request:
|
47
51
|
"""A basic HTTP request."""
|
48
52
|
|
49
53
|
# The encoding used in GET/POST dicts. None means use default setting.
|
@@ -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]) -> Request:
|
89
93
|
obj = copy.copy(self)
|
90
94
|
for attr in self.non_picklable_attrs:
|
91
95
|
if hasattr(self, attr):
|
@@ -94,20 +98,43 @@ class HttpRequest:
|
|
94
98
|
return obj
|
95
99
|
|
96
100
|
@cached_property
|
97
|
-
def headers(self):
|
98
|
-
return
|
101
|
+
def headers(self) -> RequestHeaders:
|
102
|
+
return RequestHeaders(self.meta)
|
99
103
|
|
100
104
|
@cached_property
|
101
|
-
def accepted_types(self):
|
102
|
-
"""Return
|
103
|
-
return parse_accept_header(self.headers.get("Accept", "*/*"))
|
105
|
+
def accepted_types(self) -> list[MediaType]:
|
106
|
+
"""Return accepted media types sorted by quality value (highest first).
|
104
107
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
)
|
108
|
+
When quality values are equal, the original order from the Accept header
|
109
|
+
is preserved (as per HTTP spec).
|
110
|
+
"""
|
111
|
+
header = self.headers.get("Accept", "*/*")
|
112
|
+
types = [MediaType(token) for token in header.split(",") if token.strip()]
|
113
|
+
return sorted(types, key=lambda t: t.quality, reverse=True)
|
114
|
+
|
115
|
+
def get_preferred_type(self, *media_types: str) -> str | None:
|
116
|
+
"""Return the most preferred media type from the given options.
|
117
|
+
|
118
|
+
Checks the Accept header in priority order (by quality value) and returns
|
119
|
+
the first matching media type from the provided options.
|
120
|
+
|
121
|
+
Returns None if none of the options are accepted.
|
109
122
|
|
110
|
-
|
123
|
+
Example:
|
124
|
+
# Accept: text/html;q=1.0, application/json;q=0.5
|
125
|
+
request.get_preferred_type("application/json", "text/html") # Returns "text/html"
|
126
|
+
"""
|
127
|
+
for accepted in self.accepted_types:
|
128
|
+
for option in media_types:
|
129
|
+
if accepted.match(option):
|
130
|
+
return option
|
131
|
+
return None
|
132
|
+
|
133
|
+
def accepts(self, media_type: str) -> bool:
|
134
|
+
"""Check if the given media type is accepted."""
|
135
|
+
return self.get_preferred_type(media_type) is not None
|
136
|
+
|
137
|
+
def _set_content_type_params(self, meta: dict[str, Any]) -> None:
|
111
138
|
"""Set content_type, content_params, and encoding."""
|
112
139
|
self.content_type, self.content_params = parse_header_parameters(
|
113
140
|
meta.get("CONTENT_TYPE", "")
|
@@ -121,7 +148,7 @@ class HttpRequest:
|
|
121
148
|
self.encoding = self.content_params["charset"]
|
122
149
|
|
123
150
|
@cached_property
|
124
|
-
def host(self):
|
151
|
+
def host(self) -> str:
|
125
152
|
"""
|
126
153
|
Return the HTTP host using the environment or request headers.
|
127
154
|
|
@@ -142,7 +169,7 @@ class HttpRequest:
|
|
142
169
|
return host
|
143
170
|
|
144
171
|
@cached_property
|
145
|
-
def port(self):
|
172
|
+
def port(self) -> str:
|
146
173
|
"""Return the port number for the request as a string."""
|
147
174
|
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.meta:
|
148
175
|
port = self.meta["HTTP_X_FORWARDED_PORT"]
|
@@ -150,7 +177,7 @@ class HttpRequest:
|
|
150
177
|
port = self.meta["SERVER_PORT"]
|
151
178
|
return str(port)
|
152
179
|
|
153
|
-
def get_full_path(self, force_append_slash=False):
|
180
|
+
def get_full_path(self, force_append_slash: bool = False) -> str:
|
154
181
|
"""
|
155
182
|
Return the full path for the request, including query string.
|
156
183
|
|
@@ -160,7 +187,7 @@ class HttpRequest:
|
|
160
187
|
# RFC 3986 requires query string arguments to be in the ASCII range.
|
161
188
|
# Rather than crash if this doesn't happen, we encode defensively.
|
162
189
|
|
163
|
-
def escape_uri_path(path):
|
190
|
+
def escape_uri_path(path: str) -> str:
|
164
191
|
"""
|
165
192
|
Escape the unsafe characters from the path portion of a Uniform Resource
|
166
193
|
Identifier (URI).
|
@@ -176,15 +203,14 @@ class HttpRequest:
|
|
176
203
|
# the entire path, not a path segment.
|
177
204
|
return quote(path, safe="/:@&+$,-_.!~*'()")
|
178
205
|
|
206
|
+
query_string = self.meta.get("QUERY_STRING", "")
|
179
207
|
return "{}{}{}".format(
|
180
208
|
escape_uri_path(self.path),
|
181
209
|
"/" if force_append_slash and not self.path.endswith("/") else "",
|
182
|
-
("?" + iri_to_uri(
|
183
|
-
if self.meta.get("QUERY_STRING", "")
|
184
|
-
else "",
|
210
|
+
("?" + (iri_to_uri(query_string) or "")) if query_string else "",
|
185
211
|
)
|
186
212
|
|
187
|
-
def build_absolute_uri(self, location=None):
|
213
|
+
def build_absolute_uri(self, location: str | None = None) -> str:
|
188
214
|
"""
|
189
215
|
Build an absolute URI from the location and the variables available in
|
190
216
|
this request. If no ``location`` is specified, build the absolute URI
|
@@ -224,9 +250,9 @@ class HttpRequest:
|
|
224
250
|
# base path.
|
225
251
|
location = urljoin(current_scheme_host + self.path, location)
|
226
252
|
|
227
|
-
return iri_to_uri(location)
|
253
|
+
return iri_to_uri(location) or ""
|
228
254
|
|
229
|
-
def _get_scheme(self):
|
255
|
+
def _get_scheme(self) -> str:
|
230
256
|
"""
|
231
257
|
Hook for subclasses like WSGIRequest to implement. Return 'http' by
|
232
258
|
default.
|
@@ -234,7 +260,7 @@ class HttpRequest:
|
|
234
260
|
return "http"
|
235
261
|
|
236
262
|
@property
|
237
|
-
def scheme(self):
|
263
|
+
def scheme(self) -> str:
|
238
264
|
if settings.HTTPS_PROXY_HEADER:
|
239
265
|
try:
|
240
266
|
header, secure_value = settings.HTTPS_PROXY_HEADER
|
@@ -249,15 +275,15 @@ class HttpRequest:
|
|
249
275
|
return "https" if header_value.strip() == secure_value else "http"
|
250
276
|
return self._get_scheme()
|
251
277
|
|
252
|
-
def is_https(self):
|
278
|
+
def is_https(self) -> bool:
|
253
279
|
return self.scheme == "https"
|
254
280
|
|
255
281
|
@property
|
256
|
-
def encoding(self):
|
282
|
+
def encoding(self) -> str | None:
|
257
283
|
return self._encoding
|
258
284
|
|
259
285
|
@encoding.setter
|
260
|
-
def encoding(self, val):
|
286
|
+
def encoding(self, val: str) -> None:
|
261
287
|
"""
|
262
288
|
Set the encoding used for query_params/data accesses. If the query_params or data
|
263
289
|
dictionary has already been created, remove and recreate it on the
|
@@ -269,21 +295,21 @@ class HttpRequest:
|
|
269
295
|
if hasattr(self, "_data"):
|
270
296
|
del self._data
|
271
297
|
|
272
|
-
def _initialize_handlers(self):
|
298
|
+
def _initialize_handlers(self) -> None:
|
273
299
|
self._upload_handlers = [
|
274
300
|
uploadhandler.load_handler(handler, self)
|
275
301
|
for handler in settings.FILE_UPLOAD_HANDLERS
|
276
302
|
]
|
277
303
|
|
278
304
|
@property
|
279
|
-
def upload_handlers(self):
|
305
|
+
def upload_handlers(self) -> list[Any]:
|
280
306
|
if not self._upload_handlers:
|
281
307
|
# If there are no upload handlers defined, initialize them from settings.
|
282
308
|
self._initialize_handlers()
|
283
309
|
return self._upload_handlers
|
284
310
|
|
285
311
|
@upload_handlers.setter
|
286
|
-
def upload_handlers(self, upload_handlers):
|
312
|
+
def upload_handlers(self, upload_handlers: list[Any]) -> None:
|
287
313
|
if hasattr(self, "_files"):
|
288
314
|
raise AttributeError(
|
289
315
|
"You cannot set the upload handlers after the upload has been "
|
@@ -291,7 +317,9 @@ class HttpRequest:
|
|
291
317
|
)
|
292
318
|
self._upload_handlers = upload_handlers
|
293
319
|
|
294
|
-
def parse_file_upload(
|
320
|
+
def parse_file_upload(
|
321
|
+
self, meta: dict[str, Any], post_data: IO[bytes]
|
322
|
+
) -> tuple[Any, MultiValueDict]:
|
295
323
|
"""Return a tuple of (data QueryDict, files MultiValueDict)."""
|
296
324
|
self.upload_handlers = ImmutableList(
|
297
325
|
self.upload_handlers,
|
@@ -303,7 +331,7 @@ class HttpRequest:
|
|
303
331
|
return parser.parse()
|
304
332
|
|
305
333
|
@property
|
306
|
-
def body(self):
|
334
|
+
def body(self) -> bytes:
|
307
335
|
if not hasattr(self, "_body"):
|
308
336
|
if self._read_started:
|
309
337
|
raise RawPostDataException(
|
@@ -329,11 +357,11 @@ class HttpRequest:
|
|
329
357
|
self._stream = BytesIO(self._body)
|
330
358
|
return self._body
|
331
359
|
|
332
|
-
def _mark_post_parse_error(self):
|
360
|
+
def _mark_post_parse_error(self) -> None:
|
333
361
|
self._data = QueryDict()
|
334
362
|
self._files = MultiValueDict()
|
335
363
|
|
336
|
-
def _load_data_and_files(self):
|
364
|
+
def _load_data_and_files(self) -> None:
|
337
365
|
"""Populate self._data and self._files"""
|
338
366
|
|
339
367
|
if self._read_started and not hasattr(self, "_body"):
|
@@ -373,7 +401,7 @@ class HttpRequest:
|
|
373
401
|
MultiValueDict(),
|
374
402
|
)
|
375
403
|
|
376
|
-
def close(self):
|
404
|
+
def close(self) -> None:
|
377
405
|
if hasattr(self, "_files"):
|
378
406
|
for f in chain.from_iterable(list_[1] for list_ in self._files.lists()):
|
379
407
|
f.close()
|
@@ -386,27 +414,33 @@ class HttpRequest:
|
|
386
414
|
# request.body, self._stream points to a BytesIO instance
|
387
415
|
# containing that data.
|
388
416
|
|
389
|
-
def read(self, *args, **kwargs):
|
417
|
+
def read(self, *args: Any, **kwargs: Any) -> bytes:
|
390
418
|
self._read_started = True
|
391
419
|
try:
|
392
420
|
return self._stream.read(*args, **kwargs)
|
393
421
|
except OSError as e:
|
394
422
|
raise UnreadablePostError(*e.args) from e
|
395
423
|
|
396
|
-
def readline(self, *args, **kwargs):
|
424
|
+
def readline(self, *args: Any, **kwargs: Any) -> bytes:
|
397
425
|
self._read_started = True
|
398
426
|
try:
|
399
427
|
return self._stream.readline(*args, **kwargs)
|
400
428
|
except OSError as e:
|
401
429
|
raise UnreadablePostError(*e.args) from e
|
402
430
|
|
403
|
-
def __iter__(self):
|
431
|
+
def __iter__(self) -> Iterator[bytes]:
|
404
432
|
return iter(self.readline, b"")
|
405
433
|
|
406
|
-
def readlines(self):
|
434
|
+
def readlines(self) -> list[bytes]:
|
407
435
|
return list(self)
|
408
436
|
|
409
|
-
def get_signed_cookie(
|
437
|
+
def get_signed_cookie(
|
438
|
+
self,
|
439
|
+
key: str,
|
440
|
+
default: str | None = None,
|
441
|
+
salt: str = "",
|
442
|
+
max_age: int | None = None,
|
443
|
+
) -> str | None:
|
410
444
|
"""
|
411
445
|
Retrieve a cookie value signed with the SECRET_KEY.
|
412
446
|
|
@@ -421,12 +455,12 @@ class HttpRequest:
|
|
421
455
|
return unsign_cookie_value(key, cookie_value, salt, max_age, default)
|
422
456
|
|
423
457
|
|
424
|
-
class
|
458
|
+
class RequestHeaders(CaseInsensitiveMapping):
|
425
459
|
HTTP_PREFIX = "HTTP_"
|
426
460
|
# PEP 333 gives two headers which aren't prepended with HTTP_.
|
427
461
|
UNPREFIXED_HEADERS = {"CONTENT_TYPE", "CONTENT_LENGTH"}
|
428
462
|
|
429
|
-
def __init__(self, environ):
|
463
|
+
def __init__(self, environ: dict[str, Any]):
|
430
464
|
headers = {}
|
431
465
|
for header, value in environ.items():
|
432
466
|
name = self.parse_header_name(header)
|
@@ -434,12 +468,12 @@ class HttpHeaders(CaseInsensitiveMapping):
|
|
434
468
|
headers[name] = value
|
435
469
|
super().__init__(headers)
|
436
470
|
|
437
|
-
def __getitem__(self, key):
|
471
|
+
def __getitem__(self, key: str) -> str:
|
438
472
|
"""Allow header lookup using underscores in place of hyphens."""
|
439
473
|
return super().__getitem__(key.replace("_", "-"))
|
440
474
|
|
441
475
|
@classmethod
|
442
|
-
def parse_header_name(cls, header):
|
476
|
+
def parse_header_name(cls, header: str) -> str | None:
|
443
477
|
if header.startswith(cls.HTTP_PREFIX):
|
444
478
|
header = header.removeprefix(cls.HTTP_PREFIX)
|
445
479
|
elif header not in cls.UNPREFIXED_HEADERS:
|
@@ -447,14 +481,14 @@ class HttpHeaders(CaseInsensitiveMapping):
|
|
447
481
|
return header.replace("_", "-").title()
|
448
482
|
|
449
483
|
@classmethod
|
450
|
-
def to_wsgi_name(cls, header):
|
484
|
+
def to_wsgi_name(cls, header: str) -> str:
|
451
485
|
header = header.replace("-", "_").upper()
|
452
486
|
if header in cls.UNPREFIXED_HEADERS:
|
453
487
|
return header
|
454
488
|
return f"{cls.HTTP_PREFIX}{header}"
|
455
489
|
|
456
490
|
@classmethod
|
457
|
-
def to_wsgi_names(cls, headers):
|
491
|
+
def to_wsgi_names(cls, headers: dict[str, Any]) -> dict[str, Any]:
|
458
492
|
return {
|
459
493
|
cls.to_wsgi_name(header_name): value
|
460
494
|
for header_name, value in headers.items()
|
@@ -481,7 +515,12 @@ class QueryDict(MultiValueDict):
|
|
481
515
|
_mutable = True
|
482
516
|
_encoding = None
|
483
517
|
|
484
|
-
def __init__(
|
518
|
+
def __init__(
|
519
|
+
self,
|
520
|
+
query_string: str | bytes | None = None,
|
521
|
+
mutable: bool = False,
|
522
|
+
encoding: str | None = None,
|
523
|
+
):
|
485
524
|
super().__init__()
|
486
525
|
self.encoding = encoding or settings.DEFAULT_CHARSET
|
487
526
|
query_string = query_string or ""
|
@@ -492,11 +531,12 @@ class QueryDict(MultiValueDict):
|
|
492
531
|
}
|
493
532
|
if isinstance(query_string, bytes):
|
494
533
|
# query_string normally contains URL-encoded data, a subset of ASCII.
|
534
|
+
query_bytes = query_string
|
495
535
|
try:
|
496
|
-
query_string =
|
536
|
+
query_string = query_bytes.decode(self.encoding)
|
497
537
|
except UnicodeDecodeError:
|
498
538
|
# ... but some user agents are misbehaving :-(
|
499
|
-
query_string =
|
539
|
+
query_string = query_bytes.decode("iso-8859-1")
|
500
540
|
try:
|
501
541
|
for key, value in parse_qsl(query_string, **parse_qsl_kwargs):
|
502
542
|
self.appendlist(key, value)
|
@@ -512,7 +552,13 @@ class QueryDict(MultiValueDict):
|
|
512
552
|
self._mutable = mutable
|
513
553
|
|
514
554
|
@classmethod
|
515
|
-
def fromkeys(
|
555
|
+
def fromkeys(
|
556
|
+
cls,
|
557
|
+
iterable: Any,
|
558
|
+
value: str = "",
|
559
|
+
mutable: bool = False,
|
560
|
+
encoding: str | None = None,
|
561
|
+
) -> QueryDict:
|
516
562
|
"""
|
517
563
|
Return a new QueryDict with keys (may be repeated) from an iterable and
|
518
564
|
values from value.
|
@@ -525,81 +571,83 @@ class QueryDict(MultiValueDict):
|
|
525
571
|
return q
|
526
572
|
|
527
573
|
@property
|
528
|
-
def encoding(self):
|
574
|
+
def encoding(self) -> str:
|
529
575
|
if self._encoding is None:
|
530
576
|
self._encoding = settings.DEFAULT_CHARSET
|
531
577
|
return self._encoding
|
532
578
|
|
533
579
|
@encoding.setter
|
534
|
-
def encoding(self, value):
|
580
|
+
def encoding(self, value: str) -> None:
|
535
581
|
self._encoding = value
|
536
582
|
|
537
|
-
def _assert_mutable(self):
|
583
|
+
def _assert_mutable(self) -> None:
|
538
584
|
if not self._mutable:
|
539
585
|
raise AttributeError("This QueryDict instance is immutable")
|
540
586
|
|
541
|
-
def __setitem__(self, key, value):
|
587
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
542
588
|
self._assert_mutable()
|
543
|
-
key = bytes_to_text(key, self.encoding)
|
544
|
-
value = bytes_to_text(value, self.encoding)
|
589
|
+
key = self.bytes_to_text(key, self.encoding)
|
590
|
+
value = self.bytes_to_text(value, self.encoding)
|
545
591
|
super().__setitem__(key, value)
|
546
592
|
|
547
|
-
def __delitem__(self, key):
|
593
|
+
def __delitem__(self, key: str) -> None:
|
548
594
|
self._assert_mutable()
|
549
595
|
super().__delitem__(key)
|
550
596
|
|
551
|
-
def __copy__(self):
|
597
|
+
def __copy__(self) -> QueryDict:
|
552
598
|
result = self.__class__("", mutable=True, encoding=self.encoding)
|
553
599
|
for key, value in self.lists():
|
554
600
|
result.setlist(key, value)
|
555
601
|
return result
|
556
602
|
|
557
|
-
def __deepcopy__(self, memo):
|
603
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> QueryDict:
|
558
604
|
result = self.__class__("", mutable=True, encoding=self.encoding)
|
559
605
|
memo[id(self)] = result
|
560
606
|
for key, value in self.lists():
|
561
607
|
result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo))
|
562
608
|
return result
|
563
609
|
|
564
|
-
def setlist(self, key, list_):
|
610
|
+
def setlist(self, key: str, list_: list[Any]) -> None:
|
565
611
|
self._assert_mutable()
|
566
|
-
key = bytes_to_text(key, self.encoding)
|
567
|
-
list_ = [bytes_to_text(elt, self.encoding) for elt in list_]
|
612
|
+
key = self.bytes_to_text(key, self.encoding)
|
613
|
+
list_ = [self.bytes_to_text(elt, self.encoding) for elt in list_]
|
568
614
|
super().setlist(key, list_)
|
569
615
|
|
570
|
-
def setlistdefault(
|
616
|
+
def setlistdefault(
|
617
|
+
self, key: str, default_list: list[Any] | None = None
|
618
|
+
) -> list[Any]:
|
571
619
|
self._assert_mutable()
|
572
620
|
return super().setlistdefault(key, default_list)
|
573
621
|
|
574
|
-
def appendlist(self, key, value):
|
622
|
+
def appendlist(self, key: str, value: Any) -> None:
|
575
623
|
self._assert_mutable()
|
576
|
-
key = bytes_to_text(key, self.encoding)
|
577
|
-
value = bytes_to_text(value, self.encoding)
|
624
|
+
key = self.bytes_to_text(key, self.encoding)
|
625
|
+
value = self.bytes_to_text(value, self.encoding)
|
578
626
|
super().appendlist(key, value)
|
579
627
|
|
580
|
-
def pop(self, key, *args):
|
628
|
+
def pop(self, key: str, *args: Any) -> Any:
|
581
629
|
self._assert_mutable()
|
582
630
|
return super().pop(key, *args)
|
583
631
|
|
584
|
-
def popitem(self):
|
632
|
+
def popitem(self) -> tuple[str, Any]:
|
585
633
|
self._assert_mutable()
|
586
634
|
return super().popitem()
|
587
635
|
|
588
|
-
def clear(self):
|
636
|
+
def clear(self) -> None:
|
589
637
|
self._assert_mutable()
|
590
638
|
super().clear()
|
591
639
|
|
592
|
-
def setdefault(self, key, default=None):
|
640
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
593
641
|
self._assert_mutable()
|
594
|
-
key = bytes_to_text(key, self.encoding)
|
595
|
-
default = bytes_to_text(default, self.encoding)
|
642
|
+
key = self.bytes_to_text(key, self.encoding)
|
643
|
+
default = self.bytes_to_text(default, self.encoding)
|
596
644
|
return super().setdefault(key, default)
|
597
645
|
|
598
|
-
def copy(self):
|
646
|
+
def copy(self) -> QueryDict:
|
599
647
|
"""Return a mutable copy of this object."""
|
600
648
|
return self.__deepcopy__({})
|
601
649
|
|
602
|
-
def urlencode(self, safe=None):
|
650
|
+
def urlencode(self, safe: str | None = None) -> str:
|
603
651
|
"""
|
604
652
|
Return an encoded string of all query string arguments.
|
605
653
|
|
@@ -614,14 +662,14 @@ class QueryDict(MultiValueDict):
|
|
614
662
|
"""
|
615
663
|
output = []
|
616
664
|
if safe:
|
617
|
-
|
665
|
+
safe_bytes: bytes = safe.encode(self.encoding)
|
618
666
|
|
619
|
-
def encode(k, v):
|
620
|
-
return f"{quote(k,
|
667
|
+
def encode(k: bytes, v: bytes) -> str:
|
668
|
+
return f"{quote(k, safe_bytes)}={quote(v, safe_bytes)}"
|
621
669
|
|
622
670
|
else:
|
623
671
|
|
624
|
-
def encode(k, v):
|
672
|
+
def encode(k: bytes, v: bytes) -> str:
|
625
673
|
return urlencode({k: v})
|
626
674
|
|
627
675
|
for k, list_ in self.lists():
|
@@ -631,15 +679,31 @@ class QueryDict(MultiValueDict):
|
|
631
679
|
)
|
632
680
|
return "&".join(output)
|
633
681
|
|
682
|
+
# It's neither necessary nor appropriate to use
|
683
|
+
# plain.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
|
684
|
+
# this slightly more restricted function, used by QueryDict.
|
685
|
+
@staticmethod
|
686
|
+
def bytes_to_text(s: Any, encoding: str) -> str:
|
687
|
+
"""
|
688
|
+
Convert bytes objects to strings, using the given encoding. Illegally
|
689
|
+
encoded input characters are replaced with Unicode "unknown" codepoint
|
690
|
+
(\ufffd).
|
691
|
+
|
692
|
+
Return any non-bytes objects without change.
|
693
|
+
"""
|
694
|
+
if isinstance(s, bytes):
|
695
|
+
return str(s, encoding, "replace")
|
696
|
+
else:
|
697
|
+
return s
|
698
|
+
|
634
699
|
|
635
700
|
class MediaType:
|
636
|
-
def __init__(self, media_type_raw_line):
|
637
|
-
|
638
|
-
|
639
|
-
)
|
701
|
+
def __init__(self, media_type_raw_line: str | MediaType):
|
702
|
+
line = str(media_type_raw_line) if media_type_raw_line else ""
|
703
|
+
full_type, self.params = parse_header_parameters(line)
|
640
704
|
self.main_type, _, self.sub_type = full_type.partition("/")
|
641
705
|
|
642
|
-
def __str__(self):
|
706
|
+
def __str__(self) -> str:
|
643
707
|
params_str = "".join(f"; {k}={v}" for k, v in self.params.items())
|
644
708
|
return "{}{}{}".format(
|
645
709
|
self.main_type,
|
@@ -647,38 +711,22 @@ class MediaType:
|
|
647
711
|
params_str,
|
648
712
|
)
|
649
713
|
|
650
|
-
def __repr__(self):
|
714
|
+
def __repr__(self) -> str:
|
651
715
|
return f"<{self.__class__.__qualname__}: {self}>"
|
652
716
|
|
653
717
|
@property
|
654
|
-
def is_all_types(self):
|
718
|
+
def is_all_types(self) -> bool:
|
655
719
|
return self.main_type == "*" and self.sub_type == "*"
|
656
720
|
|
657
|
-
|
721
|
+
@property
|
722
|
+
def quality(self) -> float:
|
723
|
+
"""Return the quality value from the Accept header (default 1.0)."""
|
724
|
+
return float(self.params.get("q", 1.0))
|
725
|
+
|
726
|
+
def match(self, other: str | MediaType) -> bool:
|
658
727
|
if self.is_all_types:
|
659
728
|
return True
|
660
729
|
other = MediaType(other)
|
661
730
|
if self.main_type == other.main_type and self.sub_type in {"*", other.sub_type}:
|
662
731
|
return True
|
663
732
|
return False
|
664
|
-
|
665
|
-
|
666
|
-
# It's neither necessary nor appropriate to use
|
667
|
-
# plain.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
|
668
|
-
# this slightly more restricted function, used by QueryDict.
|
669
|
-
def bytes_to_text(s, encoding):
|
670
|
-
"""
|
671
|
-
Convert bytes objects to strings, using the given encoding. Illegally
|
672
|
-
encoded input characters are replaced with Unicode "unknown" codepoint
|
673
|
-
(\ufffd).
|
674
|
-
|
675
|
-
Return any non-bytes objects without change.
|
676
|
-
"""
|
677
|
-
if isinstance(s, bytes):
|
678
|
-
return str(s, encoding, "replace")
|
679
|
-
else:
|
680
|
-
return s
|
681
|
-
|
682
|
-
|
683
|
-
def parse_accept_header(header):
|
684
|
-
return [MediaType(token) for token in header.split(",") if token.strip()]
|