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.
Files changed (128) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +28 -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/README.md +1 -1
  41. plain/http/__init__.py +4 -4
  42. plain/http/cookie.py +15 -7
  43. plain/http/multipartparser.py +50 -30
  44. plain/http/request.py +156 -108
  45. plain/http/response.py +99 -80
  46. plain/internal/__init__.py +8 -1
  47. plain/internal/files/base.py +34 -18
  48. plain/internal/files/locks.py +19 -11
  49. plain/internal/files/move.py +8 -3
  50. plain/internal/files/temp.py +23 -5
  51. plain/internal/files/uploadedfile.py +42 -26
  52. plain/internal/files/uploadhandler.py +50 -29
  53. plain/internal/files/utils.py +13 -6
  54. plain/internal/handlers/base.py +21 -7
  55. plain/internal/handlers/exception.py +19 -5
  56. plain/internal/handlers/wsgi.py +33 -21
  57. plain/internal/middleware/headers.py +11 -2
  58. plain/internal/middleware/hosts.py +12 -4
  59. plain/internal/middleware/https.py +13 -3
  60. plain/internal/middleware/slash.py +15 -5
  61. plain/json.py +2 -1
  62. plain/logs/configure.py +3 -1
  63. plain/logs/debug.py +16 -5
  64. plain/logs/formatters.py +6 -3
  65. plain/logs/loggers.py +56 -52
  66. plain/logs/utils.py +19 -9
  67. plain/packages/config.py +14 -6
  68. plain/packages/registry.py +27 -12
  69. plain/paginator.py +31 -21
  70. plain/preflight/checks.py +3 -1
  71. plain/preflight/files.py +3 -1
  72. plain/preflight/registry.py +25 -10
  73. plain/preflight/results.py +10 -4
  74. plain/preflight/security.py +7 -5
  75. plain/preflight/urls.py +4 -1
  76. plain/runtime/__init__.py +7 -6
  77. plain/runtime/global_settings.py +6 -9
  78. plain/runtime/user_settings.py +26 -17
  79. plain/runtime/utils.py +1 -1
  80. plain/signals/dispatch/dispatcher.py +39 -17
  81. plain/signing.py +49 -30
  82. plain/templates/jinja/__init__.py +13 -5
  83. plain/templates/jinja/environments.py +4 -3
  84. plain/templates/jinja/extensions.py +9 -3
  85. plain/templates/jinja/filters.py +7 -2
  86. plain/templates/jinja/globals.py +1 -1
  87. plain/test/client.py +249 -177
  88. plain/test/encoding.py +9 -6
  89. plain/test/exceptions.py +10 -2
  90. plain/urls/converters.py +13 -10
  91. plain/urls/patterns.py +32 -20
  92. plain/urls/resolvers.py +32 -22
  93. plain/urls/utils.py +5 -1
  94. plain/utils/cache.py +14 -8
  95. plain/utils/crypto.py +21 -5
  96. plain/utils/datastructures.py +84 -54
  97. plain/utils/dateparse.py +10 -7
  98. plain/utils/deconstruct.py +12 -4
  99. plain/utils/decorators.py +5 -1
  100. plain/utils/duration.py +8 -4
  101. plain/utils/encoding.py +14 -7
  102. plain/utils/functional.py +62 -47
  103. plain/utils/hashable.py +5 -1
  104. plain/utils/html.py +21 -14
  105. plain/utils/http.py +16 -9
  106. plain/utils/inspect.py +14 -6
  107. plain/utils/ipv6.py +7 -3
  108. plain/utils/itercompat.py +6 -1
  109. plain/utils/module_loading.py +7 -3
  110. plain/utils/regex_helper.py +23 -13
  111. plain/utils/safestring.py +14 -6
  112. plain/utils/text.py +34 -18
  113. plain/utils/timezone.py +30 -19
  114. plain/utils/tree.py +31 -18
  115. plain/validators.py +71 -44
  116. plain/views/base.py +16 -8
  117. plain/views/errors.py +11 -4
  118. plain/views/exceptions.py +4 -1
  119. plain/views/objects.py +15 -15
  120. plain/views/redirect.py +14 -10
  121. plain/views/templates.py +1 -1
  122. plain/wsgi.py +3 -1
  123. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/METADATA +1 -1
  124. plain-0.71.0.dist-info/RECORD +169 -0
  125. plain-0.69.0.dist-info/RECORD +0 -169
  126. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/WHEEL +0 -0
  127. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/entry_points.txt +0 -0
  128. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/licenses/LICENSE +0 -0
plain/forms/fields.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Field classes.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import copy
6
8
  import datetime
7
9
  import enum
@@ -9,8 +11,10 @@ import json
9
11
  import math
10
12
  import re
11
13
  import uuid
14
+ from collections.abc import Callable
12
15
  from decimal import Decimal, DecimalException
13
16
  from io import BytesIO
17
+ from typing import TYPE_CHECKING, Any
14
18
  from urllib.parse import urlsplit, urlunsplit
15
19
 
16
20
  from plain import validators
@@ -24,6 +28,9 @@ from plain.utils.text import pluralize_lazy
24
28
  from .boundfield import BoundField
25
29
  from .exceptions import FormFieldMissingError
26
30
 
31
+ if TYPE_CHECKING:
32
+ from .forms import BaseForm
33
+
27
34
  __all__ = (
28
35
  "Field",
29
36
  "CharField",
@@ -53,7 +60,7 @@ FILE_INPUT_CONTRADICTION = object()
53
60
 
54
61
 
55
62
  class Field:
56
- default_validators = [] # Default set of validators
63
+ default_validators: list[Callable[[Any], None]] = [] # Default set of validators
57
64
  # Add an 'invalid' entry to default_error_message if you want a specific
58
65
  # field error message not raised by the field validators.
59
66
  default_error_messages = {
@@ -64,10 +71,10 @@ class Field:
64
71
  def __init__(
65
72
  self,
66
73
  *,
67
- required=True,
68
- initial=None,
69
- error_messages=None,
70
- validators=(),
74
+ required: bool = True,
75
+ initial: Any = None,
76
+ error_messages: dict[str, str] | None = None,
77
+ validators: tuple[Callable[[Any], None], ...] = (),
71
78
  ):
72
79
  # required -- Boolean that specifies whether the field is required.
73
80
  # True by default.
@@ -87,19 +94,19 @@ class Field:
87
94
 
88
95
  self.validators = [*self.default_validators, *validators]
89
96
 
90
- def prepare_value(self, value):
97
+ def prepare_value(self, value: Any) -> Any:
91
98
  return value
92
99
 
93
- def to_python(self, value):
100
+ def to_python(self, value: Any) -> Any:
94
101
  return value
95
102
 
96
- def validate(self, value):
103
+ def validate(self, value: Any) -> None:
97
104
  if value in self.empty_values and self.required:
98
105
  raise ValidationError(self.error_messages["required"], code="required")
99
106
 
100
- def run_validators(self, value):
107
+ def run_validators(self, value: Any) -> None:
101
108
  if value in self.empty_values:
102
- return
109
+ return None
103
110
  errors = []
104
111
  for v in self.validators:
105
112
  try:
@@ -111,7 +118,7 @@ class Field:
111
118
  if errors:
112
119
  raise ValidationError(errors)
113
120
 
114
- def clean(self, value):
121
+ def clean(self, value: Any) -> Any:
115
122
  """
116
123
  Validate the given value and return its "cleaned" value as an
117
124
  appropriate Python object. Raise ValidationError for any errors.
@@ -121,7 +128,7 @@ class Field:
121
128
  self.run_validators(value)
122
129
  return value
123
130
 
124
- def bound_data(self, data, initial):
131
+ def bound_data(self, data: Any, initial: Any) -> Any:
125
132
  """
126
133
  Return the value that should be shown for this field on render of a
127
134
  bound form, given the submitted POST data for the field and the initial
@@ -132,7 +139,7 @@ class Field:
132
139
  """
133
140
  return data
134
141
 
135
- def has_changed(self, initial, data):
142
+ def has_changed(self, initial: Any, data: Any) -> bool:
136
143
  """Return True if data differs from initial."""
137
144
  try:
138
145
  data = self.to_python(data)
@@ -147,28 +154,28 @@ class Field:
147
154
  data_value = data if data is not None else ""
148
155
  return initial_value != data_value
149
156
 
150
- def get_bound_field(self, form, field_name):
157
+ def get_bound_field(self, form: BaseForm, field_name: str) -> BoundField:
151
158
  """
152
159
  Return a BoundField instance that will be used when accessing the form
153
160
  field in a template.
154
161
  """
155
162
  return BoundField(form, self, field_name)
156
163
 
157
- def __deepcopy__(self, memo):
164
+ def __deepcopy__(self, memo: dict[int, Any]) -> Field:
158
165
  result = copy.copy(self)
159
166
  memo[id(self)] = result
160
167
  result.error_messages = self.error_messages.copy()
161
168
  result.validators = self.validators[:]
162
169
  return result
163
170
 
164
- def value_from_form_data(self, data, files, html_name):
171
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
165
172
  # By default, all fields are expected to be present in HTML form data.
166
173
  try:
167
174
  return data[html_name]
168
175
  except KeyError as e:
169
176
  raise FormFieldMissingError(html_name) from e
170
177
 
171
- def value_from_json_data(self, data, files, html_name):
178
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
172
179
  if self.required and html_name not in data:
173
180
  raise FormFieldMissingError(html_name)
174
181
 
@@ -177,7 +184,13 @@ class Field:
177
184
 
178
185
  class CharField(Field):
179
186
  def __init__(
180
- self, *, max_length=None, min_length=None, strip=True, empty_value="", **kwargs
187
+ self,
188
+ *,
189
+ max_length: int | None = None,
190
+ min_length: int | None = None,
191
+ strip: bool = True,
192
+ empty_value: str = "",
193
+ **kwargs: Any,
181
194
  ):
182
195
  self.max_length = max_length
183
196
  self.min_length = min_length
@@ -190,7 +203,7 @@ class CharField(Field):
190
203
  self.validators.append(validators.MaxLengthValidator(int(max_length)))
191
204
  self.validators.append(validators.ProhibitNullCharactersValidator())
192
205
 
193
- def to_python(self, value):
206
+ def to_python(self, value: Any) -> str:
194
207
  """Return a string."""
195
208
  if value not in self.empty_values:
196
209
  value = str(value)
@@ -207,7 +220,14 @@ class IntegerField(Field):
207
220
  }
208
221
  re_decimal = _lazy_re_compile(r"\.0*\s*$")
209
222
 
210
- def __init__(self, *, max_value=None, min_value=None, step_size=None, **kwargs):
223
+ def __init__(
224
+ self,
225
+ *,
226
+ max_value: int | None = None,
227
+ min_value: int | None = None,
228
+ step_size: int | None = None,
229
+ **kwargs: Any,
230
+ ):
211
231
  self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
212
232
  super().__init__(**kwargs)
213
233
 
@@ -218,7 +238,7 @@ class IntegerField(Field):
218
238
  if step_size is not None:
219
239
  self.validators.append(validators.StepValueValidator(step_size))
220
240
 
221
- def to_python(self, value):
241
+ def to_python(self, value: Any) -> int | None:
222
242
  """
223
243
  Validate that int() can be called on the input. Return the result
224
244
  of int() or None for empty values.
@@ -239,7 +259,7 @@ class FloatField(IntegerField):
239
259
  "invalid": "Enter a number.",
240
260
  }
241
261
 
242
- def to_python(self, value):
262
+ def to_python(self, value: Any) -> float | None:
243
263
  """
244
264
  Validate that float() can be called on the input. Return the result
245
265
  of float() or None for empty values.
@@ -253,10 +273,10 @@ class FloatField(IntegerField):
253
273
  raise ValidationError(self.error_messages["invalid"], code="invalid")
254
274
  return value
255
275
 
256
- def validate(self, value):
276
+ def validate(self, value: Any) -> None:
257
277
  super().validate(value)
258
278
  if value in self.empty_values:
259
- return
279
+ return None
260
280
  if not math.isfinite(value):
261
281
  raise ValidationError(self.error_messages["invalid"], code="invalid")
262
282
 
@@ -269,17 +289,17 @@ class DecimalField(IntegerField):
269
289
  def __init__(
270
290
  self,
271
291
  *,
272
- max_value=None,
273
- min_value=None,
274
- max_digits=None,
275
- decimal_places=None,
276
- **kwargs,
292
+ max_value: int | None = None,
293
+ min_value: int | None = None,
294
+ max_digits: int | None = None,
295
+ decimal_places: int | None = None,
296
+ **kwargs: Any,
277
297
  ):
278
298
  self.max_digits, self.decimal_places = max_digits, decimal_places
279
299
  super().__init__(max_value=max_value, min_value=min_value, **kwargs)
280
300
  self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
281
301
 
282
- def to_python(self, value):
302
+ def to_python(self, value: Any) -> Decimal | None:
283
303
  """
284
304
  Validate that the input is a decimal number. Return a Decimal
285
305
  instance or None for empty values. Ensure that there are no more
@@ -294,10 +314,10 @@ class DecimalField(IntegerField):
294
314
  raise ValidationError(self.error_messages["invalid"], code="invalid")
295
315
  return value
296
316
 
297
- def validate(self, value):
317
+ def validate(self, value: Any) -> None:
298
318
  super().validate(value)
299
319
  if value in self.empty_values:
300
- return
320
+ return None
301
321
  if not value.is_finite():
302
322
  raise ValidationError(
303
323
  self.error_messages["invalid"],
@@ -352,12 +372,12 @@ class BaseTemporalField(Field):
352
372
  "%m/%d/%y %H:%M", # '10/25/06 14:30'
353
373
  ]
354
374
 
355
- def __init__(self, *, input_formats=None, **kwargs):
375
+ def __init__(self, *, input_formats: list[str] | None = None, **kwargs: Any):
356
376
  super().__init__(**kwargs)
357
377
  if input_formats is not None:
358
378
  self.input_formats = input_formats
359
379
 
360
- def to_python(self, value):
380
+ def to_python(self, value: Any) -> Any:
361
381
  value = value.strip()
362
382
  # Try to strptime against each input format.
363
383
  for format in self.input_formats:
@@ -367,7 +387,7 @@ class BaseTemporalField(Field):
367
387
  continue
368
388
  raise ValidationError(self.error_messages["invalid"], code="invalid")
369
389
 
370
- def strptime(self, value, format):
390
+ def strptime(self, value: str, format: str) -> Any:
371
391
  raise NotImplementedError("Subclasses must define this method.")
372
392
 
373
393
 
@@ -377,7 +397,7 @@ class DateField(BaseTemporalField):
377
397
  "invalid": "Enter a valid date.",
378
398
  }
379
399
 
380
- def to_python(self, value):
400
+ def to_python(self, value: Any) -> datetime.date | None:
381
401
  """
382
402
  Validate that the input can be converted to a date. Return a Python
383
403
  datetime.date object.
@@ -390,7 +410,7 @@ class DateField(BaseTemporalField):
390
410
  return value
391
411
  return super().to_python(value)
392
412
 
393
- def strptime(self, value, format):
413
+ def strptime(self, value: str, format: str) -> datetime.date:
394
414
  return datetime.datetime.strptime(value, format).date()
395
415
 
396
416
 
@@ -398,7 +418,7 @@ class TimeField(BaseTemporalField):
398
418
  input_formats = BaseTemporalField.TIME_INPUT_FORMATS
399
419
  default_error_messages = {"invalid": "Enter a valid time."}
400
420
 
401
- def to_python(self, value):
421
+ def to_python(self, value: Any) -> datetime.time | None:
402
422
  """
403
423
  Validate that the input can be converted to a time. Return a Python
404
424
  datetime.time object.
@@ -409,12 +429,12 @@ class TimeField(BaseTemporalField):
409
429
  return value
410
430
  return super().to_python(value)
411
431
 
412
- def strptime(self, value, format):
432
+ def strptime(self, value: str, format: str) -> datetime.time:
413
433
  return datetime.datetime.strptime(value, format).time()
414
434
 
415
435
 
416
436
  class DateTimeFormatsIterator:
417
- def __iter__(self):
437
+ def __iter__(self) -> Any:
418
438
  yield from BaseTemporalField.DATETIME_INPUT_FORMATS
419
439
  yield from BaseTemporalField.DATE_INPUT_FORMATS
420
440
 
@@ -425,12 +445,12 @@ class DateTimeField(BaseTemporalField):
425
445
  "invalid": "Enter a valid date/time.",
426
446
  }
427
447
 
428
- def prepare_value(self, value):
448
+ def prepare_value(self, value: Any) -> Any:
429
449
  if isinstance(value, datetime.datetime):
430
450
  value = to_current_timezone(value)
431
451
  return value
432
452
 
433
- def to_python(self, value):
453
+ def to_python(self, value: Any) -> datetime.datetime | None:
434
454
  """
435
455
  Validate that the input can be converted to a datetime. Return a
436
456
  Python datetime.datetime object.
@@ -450,7 +470,7 @@ class DateTimeField(BaseTemporalField):
450
470
  result = super().to_python(value)
451
471
  return from_current_timezone(result)
452
472
 
453
- def strptime(self, value, format):
473
+ def strptime(self, value: str, format: str) -> datetime.datetime:
454
474
  return datetime.datetime.strptime(value, format)
455
475
 
456
476
 
@@ -460,12 +480,12 @@ class DurationField(Field):
460
480
  "overflow": "The number of days must be between {min_days} and {max_days}.",
461
481
  }
462
482
 
463
- def prepare_value(self, value):
483
+ def prepare_value(self, value: Any) -> Any:
464
484
  if isinstance(value, datetime.timedelta):
465
485
  return duration_string(value)
466
486
  return value
467
487
 
468
- def to_python(self, value):
488
+ def to_python(self, value: Any) -> datetime.timedelta | None:
469
489
  if value in self.empty_values:
470
490
  return None
471
491
  if isinstance(value, datetime.timedelta):
@@ -486,7 +506,7 @@ class DurationField(Field):
486
506
 
487
507
 
488
508
  class RegexField(CharField):
489
- def __init__(self, regex, **kwargs):
509
+ def __init__(self, regex: str | re.Pattern[str], **kwargs: Any) -> None:
490
510
  """
491
511
  regex can be either a string or a compiled regular expression object.
492
512
  """
@@ -494,10 +514,10 @@ class RegexField(CharField):
494
514
  super().__init__(**kwargs)
495
515
  self._set_regex(regex)
496
516
 
497
- def _get_regex(self):
517
+ def _get_regex(self) -> re.Pattern[str]:
498
518
  return self._regex
499
519
 
500
- def _set_regex(self, regex):
520
+ def _set_regex(self, regex: str | re.Pattern[str]) -> None:
501
521
  if isinstance(regex, str):
502
522
  regex = re.compile(regex)
503
523
  self._regex = regex
@@ -515,7 +535,7 @@ class RegexField(CharField):
515
535
  class EmailField(CharField):
516
536
  default_validators = [validators.validate_email]
517
537
 
518
- def __init__(self, **kwargs):
538
+ def __init__(self, **kwargs: Any) -> None:
519
539
  super().__init__(strip=True, **kwargs)
520
540
 
521
541
 
@@ -532,12 +552,18 @@ class FileField(Field):
532
552
  "contradiction": "Please either submit a file or check the clear checkbox, not both.",
533
553
  }
534
554
 
535
- def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs):
555
+ def __init__(
556
+ self,
557
+ *,
558
+ max_length: int | None = None,
559
+ allow_empty_file: bool = False,
560
+ **kwargs: Any,
561
+ ) -> None:
536
562
  self.max_length = max_length
537
563
  self.allow_empty_file = allow_empty_file
538
564
  super().__init__(**kwargs)
539
565
 
540
- def to_python(self, data):
566
+ def to_python(self, data: Any) -> Any:
541
567
  if data in self.empty_values:
542
568
  return None
543
569
 
@@ -560,7 +586,7 @@ class FileField(Field):
560
586
 
561
587
  return data
562
588
 
563
- def clean(self, data, initial=None):
589
+ def clean(self, data: Any, initial: Any = None) -> Any:
564
590
  # If the widget got contradictory inputs, we raise a validation error
565
591
  if data is FILE_INPUT_CONTRADICTION:
566
592
  raise ValidationError(
@@ -581,16 +607,16 @@ class FileField(Field):
581
607
  return initial
582
608
  return super().clean(data)
583
609
 
584
- def bound_data(self, _, initial):
610
+ def bound_data(self, _: Any, initial: Any) -> Any:
585
611
  return initial
586
612
 
587
- def has_changed(self, initial, data):
613
+ def has_changed(self, initial: Any, data: Any) -> bool:
588
614
  return data is not None
589
615
 
590
- def value_from_form_data(self, data, files, html_name):
616
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
591
617
  return files.get(html_name)
592
618
 
593
- def value_from_json_data(self, data, files, html_name):
619
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
594
620
  return files.get(html_name)
595
621
 
596
622
 
@@ -600,7 +626,7 @@ class ImageField(FileField):
600
626
  "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
601
627
  }
602
628
 
603
- def to_python(self, data):
629
+ def to_python(self, data: Any) -> Any:
604
630
  """
605
631
  Check that the file-upload field data contains a valid image (GIF, JPG,
606
632
  PNG, etc. -- whatever Pillow supports).
@@ -609,7 +635,7 @@ class ImageField(FileField):
609
635
  if f is None:
610
636
  return None
611
637
 
612
- from PIL import Image
638
+ from PIL import Image # type: ignore[import-not-found]
613
639
 
614
640
  # We need to get a file object for Pillow. We might have a path or we might
615
641
  # have to read the data into memory.
@@ -650,11 +676,11 @@ class URLField(CharField):
650
676
  }
651
677
  default_validators = [validators.URLValidator()]
652
678
 
653
- def __init__(self, **kwargs):
679
+ def __init__(self, **kwargs: Any) -> None:
654
680
  super().__init__(strip=True, **kwargs)
655
681
 
656
- def to_python(self, value):
657
- def split_url(url):
682
+ def to_python(self, value: Any) -> str:
683
+ def split_url(url: str | bytes) -> list[str]:
658
684
  """
659
685
  Return a list of url parts via urlparse.urlsplit(), or raise
660
686
  ValidationError for some malformed URLs.
@@ -679,13 +705,16 @@ class URLField(CharField):
679
705
  url_fields[2] = ""
680
706
  # Rebuild the url_fields list, since the domain segment may now
681
707
  # contain the path too.
682
- url_fields = split_url(urlunsplit(url_fields))
683
- value = urlunsplit(url_fields)
708
+ url_result = urlunsplit(url_fields)
709
+ url_fields = split_url(
710
+ str(url_result) if isinstance(url_result, bytes) else url_result
711
+ )
712
+ value = str(urlunsplit(url_fields))
684
713
  return value
685
714
 
686
715
 
687
716
  class BooleanField(Field):
688
- def to_python(self, value):
717
+ def to_python(self, value: Any) -> bool:
689
718
  """Return a Python boolean object."""
690
719
  # Explicitly check for the string 'False', which is what a hidden field
691
720
  # will submit for False. Also check for '0', since this is what
@@ -697,16 +726,18 @@ class BooleanField(Field):
697
726
  value = bool(value)
698
727
  return super().to_python(value)
699
728
 
700
- def validate(self, value):
729
+ def validate(self, value: Any) -> None:
701
730
  if not value and self.required:
702
731
  raise ValidationError(self.error_messages["required"], code="required")
703
732
 
704
- def has_changed(self, initial, data):
733
+ def has_changed(self, initial: Any, data: Any) -> bool:
705
734
  # Sometimes data or initial may be a string equivalent of a boolean
706
735
  # so we should run it through to_python first to get a boolean value
707
736
  return self.to_python(initial) != self.to_python(data)
708
737
 
709
- def value_from_form_data(self, data, files, html_name):
738
+ def value_from_form_data(
739
+ self, data: Any, files: Any, html_name: str
740
+ ) -> bool | None:
710
741
  if html_name not in data:
711
742
  # Unselected checkboxes aren't in HTML form data, so return False
712
743
  return False
@@ -723,7 +754,7 @@ class BooleanField(Field):
723
754
  "on": True,
724
755
  }.get(value)
725
756
 
726
- def value_from_json_data(self, data, files, html_name):
757
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
727
758
  # Boolean fields must be present in the JSON data
728
759
  try:
729
760
  return data[html_name]
@@ -737,7 +768,7 @@ class NullBooleanField(BooleanField):
737
768
  to None.
738
769
  """
739
770
 
740
- def to_python(self, value):
771
+ def to_python(self, value: Any) -> bool | None:
741
772
  """
742
773
  Explicitly check for the string 'True' and 'False', which is what a
743
774
  hidden field will submit for True and False, for 'true' and 'false',
@@ -753,15 +784,15 @@ class NullBooleanField(BooleanField):
753
784
  else:
754
785
  return None
755
786
 
756
- def validate(self, value):
787
+ def validate(self, value: Any) -> None:
757
788
  pass
758
789
 
759
790
 
760
791
  class CallableChoiceIterator:
761
- def __init__(self, choices_func):
792
+ def __init__(self, choices_func: Callable[[], Any]) -> None:
762
793
  self.choices_func = choices_func
763
794
 
764
- def __iter__(self):
795
+ def __iter__(self) -> Any:
765
796
  yield from self.choices_func()
766
797
 
767
798
 
@@ -770,7 +801,7 @@ class ChoiceField(Field):
770
801
  "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
771
802
  }
772
803
 
773
- def __init__(self, *, choices=(), **kwargs):
804
+ def __init__(self, *, choices: Any = (), **kwargs: Any) -> None:
774
805
  super().__init__(**kwargs)
775
806
  if hasattr(choices, "choices"):
776
807
  choices = choices.choices
@@ -778,15 +809,15 @@ class ChoiceField(Field):
778
809
  choices = [(member.value, member.name) for member in choices]
779
810
  self.choices = choices
780
811
 
781
- def __deepcopy__(self, memo):
812
+ def __deepcopy__(self, memo: dict[int, Any]) -> ChoiceField:
782
813
  result = super().__deepcopy__(memo)
783
814
  result._choices = copy.deepcopy(self._choices, memo)
784
815
  return result
785
816
 
786
- def _get_choices(self):
817
+ def _get_choices(self) -> Any:
787
818
  return self._choices
788
819
 
789
- def _set_choices(self, value):
820
+ def _set_choices(self, value: Any) -> None:
790
821
  # Setting choices also sets the choices on the widget.
791
822
  # choices can be any iterable, but we call list() on it because
792
823
  # it will be consumed more than once.
@@ -799,13 +830,13 @@ class ChoiceField(Field):
799
830
 
800
831
  choices = property(_get_choices, _set_choices)
801
832
 
802
- def to_python(self, value):
833
+ def to_python(self, value: Any) -> str:
803
834
  """Return a string."""
804
835
  if value in self.empty_values:
805
836
  return ""
806
837
  return str(value)
807
838
 
808
- def validate(self, value):
839
+ def validate(self, value: Any) -> None:
809
840
  """Validate that the input is in self.choices."""
810
841
  super().validate(value)
811
842
  if value and not self.valid_value(value):
@@ -815,7 +846,7 @@ class ChoiceField(Field):
815
846
  params={"value": value},
816
847
  )
817
848
 
818
- def valid_value(self, value):
849
+ def valid_value(self, value: Any) -> bool:
819
850
  """Check to see if the provided value is a valid choice."""
820
851
  text_value = str(value)
821
852
  for k, v in self.choices:
@@ -831,12 +862,18 @@ class ChoiceField(Field):
831
862
 
832
863
 
833
864
  class TypedChoiceField(ChoiceField):
834
- def __init__(self, *, coerce=lambda val: val, empty_value="", **kwargs):
865
+ def __init__(
866
+ self,
867
+ *,
868
+ coerce: Callable[[Any], Any] = lambda val: val,
869
+ empty_value: Any = "",
870
+ **kwargs: Any,
871
+ ) -> None:
835
872
  self.coerce = coerce
836
873
  self.empty_value = empty_value
837
874
  super().__init__(**kwargs)
838
875
 
839
- def _coerce(self, value):
876
+ def _coerce(self, value: Any) -> Any:
840
877
  """
841
878
  Validate that the value can be coerced to the right type (if not empty).
842
879
  """
@@ -852,7 +889,7 @@ class TypedChoiceField(ChoiceField):
852
889
  )
853
890
  return value
854
891
 
855
- def clean(self, value):
892
+ def clean(self, value: Any) -> Any:
856
893
  value = super().clean(value)
857
894
  return self._coerce(value)
858
895
 
@@ -863,7 +900,7 @@ class MultipleChoiceField(ChoiceField):
863
900
  "invalid_list": "Enter a list of values.",
864
901
  }
865
902
 
866
- def to_python(self, value):
903
+ def to_python(self, value: Any) -> list[str]:
867
904
  if not value:
868
905
  return []
869
906
  elif not isinstance(value, list | tuple):
@@ -872,7 +909,7 @@ class MultipleChoiceField(ChoiceField):
872
909
  )
873
910
  return [str(val) for val in value]
874
911
 
875
- def validate(self, value):
912
+ def validate(self, value: Any) -> None:
876
913
  """Validate that the input is a list or tuple."""
877
914
  if self.required and not value:
878
915
  raise ValidationError(self.error_messages["required"], code="required")
@@ -885,7 +922,7 @@ class MultipleChoiceField(ChoiceField):
885
922
  params={"value": val},
886
923
  )
887
924
 
888
- def has_changed(self, initial, data):
925
+ def has_changed(self, initial: Any, data: Any) -> bool:
889
926
  if initial is None:
890
927
  initial = []
891
928
  if data is None:
@@ -896,7 +933,7 @@ class MultipleChoiceField(ChoiceField):
896
933
  data_set = {str(value) for value in data}
897
934
  return data_set != initial_set
898
935
 
899
- def value_from_form_data(self, data, files, html_name):
936
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
900
937
  return data.getlist(html_name)
901
938
 
902
939
 
@@ -905,12 +942,12 @@ class UUIDField(CharField):
905
942
  "invalid": "Enter a valid UUID.",
906
943
  }
907
944
 
908
- def prepare_value(self, value):
945
+ def prepare_value(self, value: Any) -> Any:
909
946
  if isinstance(value, uuid.UUID):
910
947
  return str(value)
911
948
  return value
912
949
 
913
- def to_python(self, value):
950
+ def to_python(self, value: Any) -> uuid.UUID | None:
914
951
  value = super().to_python(value)
915
952
  if value in self.empty_values:
916
953
  return None
@@ -936,15 +973,20 @@ class JSONField(CharField):
936
973
  }
937
974
 
938
975
  def __init__(
939
- self, encoder=None, decoder=None, indent=None, sort_keys=False, **kwargs
940
- ):
976
+ self,
977
+ encoder: Any = None,
978
+ decoder: Any = None,
979
+ indent: int | None = None,
980
+ sort_keys: bool = False,
981
+ **kwargs: Any,
982
+ ) -> None:
941
983
  self.encoder = encoder
942
984
  self.decoder = decoder
943
985
  self.indent = indent
944
986
  self.sort_keys = sort_keys
945
987
  super().__init__(**kwargs)
946
988
 
947
- def to_python(self, value):
989
+ def to_python(self, value: Any) -> Any:
948
990
  if value in self.empty_values:
949
991
  return None
950
992
  elif isinstance(value, list | dict | int | float | JSONString):
@@ -962,7 +1004,7 @@ class JSONField(CharField):
962
1004
  else:
963
1005
  return converted
964
1006
 
965
- def bound_data(self, data, initial):
1007
+ def bound_data(self, data: Any, initial: Any) -> Any:
966
1008
  if data is None:
967
1009
  return None
968
1010
  try:
@@ -970,7 +1012,7 @@ class JSONField(CharField):
970
1012
  except json.JSONDecodeError:
971
1013
  return InvalidJSONInput(data)
972
1014
 
973
- def prepare_value(self, value):
1015
+ def prepare_value(self, value: Any) -> Any:
974
1016
  if isinstance(value, InvalidJSONInput):
975
1017
  return value
976
1018
  return json.dumps(
@@ -981,7 +1023,7 @@ class JSONField(CharField):
981
1023
  cls=self.encoder,
982
1024
  )
983
1025
 
984
- def has_changed(self, initial, data):
1026
+ def has_changed(self, initial: Any, data: Any) -> bool:
985
1027
  if super().has_changed(initial, data):
986
1028
  return True
987
1029
  # For purposes of seeing whether something has changed, True isn't the
@@ -991,7 +1033,7 @@ class JSONField(CharField):
991
1033
  )
992
1034
 
993
1035
 
994
- def from_current_timezone(value):
1036
+ def from_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
995
1037
  """
996
1038
  When time zone support is enabled, convert naive datetimes
997
1039
  entered in the current time zone to aware datetimes.
@@ -1005,7 +1047,7 @@ def from_current_timezone(value):
1005
1047
  except Exception as exc:
1006
1048
  raise ValidationError(
1007
1049
  (
1008
- "%(datetime)s couldnt be interpreted "
1050
+ "%(datetime)s couldn't be interpreted "
1009
1051
  "in time zone %(current_timezone)s; it "
1010
1052
  "may be ambiguous or it may not exist."
1011
1053
  ),
@@ -1015,7 +1057,7 @@ def from_current_timezone(value):
1015
1057
  return value
1016
1058
 
1017
1059
 
1018
- def to_current_timezone(value):
1060
+ def to_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1019
1061
  """
1020
1062
  When time zone support is enabled, convert aware datetimes
1021
1063
  to naive datetimes in the current time zone for display.