plain 0.69.0__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.
Files changed (126) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +11 -0
  3. plain/assets/compile.py +20 -7
  4. plain/assets/finders.py +15 -11
  5. plain/assets/fingerprints.py +6 -5
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +23 -17
  8. plain/chores/registry.py +14 -9
  9. plain/cli/agent/__init__.py +1 -1
  10. plain/cli/agent/docs.py +7 -6
  11. plain/cli/agent/llmdocs.py +18 -8
  12. plain/cli/agent/md.py +19 -14
  13. plain/cli/agent/prompt.py +1 -1
  14. plain/cli/agent/request.py +37 -17
  15. plain/cli/build.py +2 -2
  16. plain/cli/changelog.py +8 -4
  17. plain/cli/chores.py +4 -4
  18. plain/cli/core.py +8 -5
  19. plain/cli/docs.py +2 -2
  20. plain/cli/formatting.py +10 -7
  21. plain/cli/output.py +6 -2
  22. plain/cli/preflight.py +3 -3
  23. plain/cli/print.py +1 -1
  24. plain/cli/registry.py +10 -6
  25. plain/cli/scaffold.py +1 -1
  26. plain/cli/settings.py +1 -1
  27. plain/cli/shell.py +10 -7
  28. plain/cli/startup.py +3 -3
  29. plain/cli/urls.py +10 -4
  30. plain/cli/utils.py +2 -2
  31. plain/csrf/middleware.py +15 -5
  32. plain/csrf/views.py +11 -8
  33. plain/debug.py +5 -2
  34. plain/exceptions.py +19 -8
  35. plain/forms/__init__.py +1 -1
  36. plain/forms/boundfield.py +14 -7
  37. plain/forms/exceptions.py +1 -1
  38. plain/forms/fields.py +139 -97
  39. plain/forms/forms.py +55 -39
  40. plain/http/cookie.py +15 -7
  41. plain/http/multipartparser.py +50 -30
  42. plain/http/request.py +97 -73
  43. plain/http/response.py +99 -80
  44. plain/internal/__init__.py +8 -1
  45. plain/internal/files/base.py +34 -18
  46. plain/internal/files/locks.py +19 -11
  47. plain/internal/files/move.py +8 -3
  48. plain/internal/files/temp.py +23 -5
  49. plain/internal/files/uploadedfile.py +42 -26
  50. plain/internal/files/uploadhandler.py +48 -27
  51. plain/internal/files/utils.py +13 -6
  52. plain/internal/handlers/base.py +20 -6
  53. plain/internal/handlers/exception.py +19 -5
  54. plain/internal/handlers/wsgi.py +30 -18
  55. plain/internal/middleware/headers.py +11 -2
  56. plain/internal/middleware/hosts.py +10 -2
  57. plain/internal/middleware/https.py +13 -3
  58. plain/internal/middleware/slash.py +15 -5
  59. plain/json.py +2 -1
  60. plain/logs/configure.py +3 -1
  61. plain/logs/debug.py +16 -5
  62. plain/logs/formatters.py +6 -3
  63. plain/logs/loggers.py +56 -52
  64. plain/logs/utils.py +19 -9
  65. plain/packages/config.py +14 -6
  66. plain/packages/registry.py +27 -12
  67. plain/paginator.py +31 -21
  68. plain/preflight/checks.py +3 -1
  69. plain/preflight/files.py +3 -1
  70. plain/preflight/registry.py +25 -10
  71. plain/preflight/results.py +10 -4
  72. plain/preflight/security.py +7 -5
  73. plain/preflight/urls.py +4 -1
  74. plain/runtime/__init__.py +4 -3
  75. plain/runtime/global_settings.py +1 -1
  76. plain/runtime/user_settings.py +26 -17
  77. plain/runtime/utils.py +1 -1
  78. plain/signals/dispatch/dispatcher.py +39 -17
  79. plain/signing.py +49 -30
  80. plain/templates/jinja/__init__.py +13 -5
  81. plain/templates/jinja/environments.py +4 -3
  82. plain/templates/jinja/extensions.py +9 -3
  83. plain/templates/jinja/filters.py +7 -2
  84. plain/templates/jinja/globals.py +1 -1
  85. plain/test/client.py +246 -174
  86. plain/test/encoding.py +9 -6
  87. plain/test/exceptions.py +10 -2
  88. plain/urls/converters.py +13 -10
  89. plain/urls/patterns.py +32 -20
  90. plain/urls/resolvers.py +32 -22
  91. plain/urls/utils.py +5 -1
  92. plain/utils/cache.py +14 -8
  93. plain/utils/crypto.py +21 -5
  94. plain/utils/datastructures.py +84 -54
  95. plain/utils/dateparse.py +10 -7
  96. plain/utils/deconstruct.py +12 -4
  97. plain/utils/decorators.py +5 -1
  98. plain/utils/duration.py +8 -4
  99. plain/utils/encoding.py +14 -7
  100. plain/utils/functional.py +62 -47
  101. plain/utils/hashable.py +5 -1
  102. plain/utils/html.py +21 -14
  103. plain/utils/http.py +16 -9
  104. plain/utils/inspect.py +14 -6
  105. plain/utils/ipv6.py +7 -3
  106. plain/utils/itercompat.py +6 -1
  107. plain/utils/module_loading.py +7 -3
  108. plain/utils/regex_helper.py +23 -13
  109. plain/utils/safestring.py +14 -6
  110. plain/utils/text.py +34 -18
  111. plain/utils/timezone.py +30 -19
  112. plain/utils/tree.py +31 -18
  113. plain/validators.py +71 -44
  114. plain/views/base.py +16 -6
  115. plain/views/errors.py +11 -4
  116. plain/views/exceptions.py +4 -1
  117. plain/views/objects.py +15 -15
  118. plain/views/redirect.py +14 -10
  119. plain/views/templates.py +1 -1
  120. plain/wsgi.py +3 -1
  121. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
  122. plain-0.70.0.dist-info/RECORD +169 -0
  123. plain-0.69.0.dist-info/RECORD +0 -169
  124. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
  125. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
  126. {plain-0.69.0.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(self.meta.get("QUERY_STRING", "")))
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(self, meta, post_data):
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(self, key, default=None, salt="", max_age=None):
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__(self, query_string=None, mutable=False, encoding=None):
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 = query_string.decode(self.encoding)
513
+ query_string = query_bytes.decode(self.encoding)
497
514
  except UnicodeDecodeError:
498
515
  # ... but some user agents are misbehaving :-(
499
- query_string = query_string.decode("iso-8859-1")
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(cls, iterable, value="", mutable=False, encoding=None):
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(self, key, default_list=None):
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
- safe = safe.encode(self.encoding)
642
+ safe_bytes: bytes = safe.encode(self.encoding)
618
643
 
619
- def encode(k, v):
620
- return f"{quote(k, safe)}={quote(v, safe)}"
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
- full_type, self.params = parse_header_parameters(
638
- media_type_raw_line if media_type_raw_line else ""
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()]