plain 0.68.0__py3-none-any.whl → 0.103.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 (192) hide show
  1. plain/CHANGELOG.md +684 -1
  2. plain/README.md +1 -1
  3. plain/agents/.claude/rules/plain.md +88 -0
  4. plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
  5. plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
  6. plain/assets/compile.py +25 -12
  7. plain/assets/finders.py +24 -17
  8. plain/assets/fingerprints.py +10 -7
  9. plain/assets/urls.py +1 -1
  10. plain/assets/views.py +47 -33
  11. plain/chores/README.md +25 -23
  12. plain/chores/__init__.py +2 -1
  13. plain/chores/core.py +27 -0
  14. plain/chores/registry.py +23 -36
  15. plain/cli/README.md +185 -16
  16. plain/cli/__init__.py +2 -1
  17. plain/cli/agent.py +234 -0
  18. plain/cli/build.py +7 -8
  19. plain/cli/changelog.py +11 -5
  20. plain/cli/chores.py +32 -34
  21. plain/cli/core.py +110 -26
  22. plain/cli/docs.py +98 -21
  23. plain/cli/formatting.py +40 -17
  24. plain/cli/install.py +10 -54
  25. plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
  26. plain/cli/output.py +6 -2
  27. plain/cli/preflight.py +27 -75
  28. plain/cli/print.py +4 -4
  29. plain/cli/registry.py +96 -10
  30. plain/cli/{agent/request.py → request.py} +67 -33
  31. plain/cli/runtime.py +45 -0
  32. plain/cli/scaffold.py +2 -7
  33. plain/cli/server.py +153 -0
  34. plain/cli/settings.py +53 -49
  35. plain/cli/shell.py +15 -12
  36. plain/cli/startup.py +9 -8
  37. plain/cli/upgrade.py +17 -104
  38. plain/cli/urls.py +12 -7
  39. plain/cli/utils.py +3 -3
  40. plain/csrf/README.md +65 -40
  41. plain/csrf/middleware.py +53 -43
  42. plain/debug.py +5 -2
  43. plain/exceptions.py +22 -114
  44. plain/forms/README.md +453 -24
  45. plain/forms/__init__.py +55 -4
  46. plain/forms/boundfield.py +15 -8
  47. plain/forms/exceptions.py +1 -1
  48. plain/forms/fields.py +346 -143
  49. plain/forms/forms.py +75 -45
  50. plain/http/README.md +356 -9
  51. plain/http/__init__.py +41 -26
  52. plain/http/cookie.py +15 -7
  53. plain/http/exceptions.py +65 -0
  54. plain/http/middleware.py +32 -0
  55. plain/http/multipartparser.py +99 -88
  56. plain/http/request.py +362 -250
  57. plain/http/response.py +99 -197
  58. plain/internal/__init__.py +8 -1
  59. plain/internal/files/base.py +35 -19
  60. plain/internal/files/locks.py +19 -11
  61. plain/internal/files/move.py +8 -3
  62. plain/internal/files/temp.py +25 -6
  63. plain/internal/files/uploadedfile.py +47 -28
  64. plain/internal/files/uploadhandler.py +64 -58
  65. plain/internal/files/utils.py +24 -10
  66. plain/internal/handlers/base.py +34 -23
  67. plain/internal/handlers/exception.py +68 -65
  68. plain/internal/handlers/wsgi.py +65 -54
  69. plain/internal/middleware/headers.py +37 -11
  70. plain/internal/middleware/hosts.py +11 -8
  71. plain/internal/middleware/https.py +17 -7
  72. plain/internal/middleware/slash.py +14 -9
  73. plain/internal/reloader.py +77 -0
  74. plain/json.py +2 -1
  75. plain/logs/README.md +161 -62
  76. plain/logs/__init__.py +1 -1
  77. plain/logs/{loggers.py → app.py} +71 -67
  78. plain/logs/configure.py +63 -14
  79. plain/logs/debug.py +17 -6
  80. plain/logs/filters.py +15 -0
  81. plain/logs/formatters.py +7 -4
  82. plain/packages/README.md +105 -23
  83. plain/packages/config.py +15 -7
  84. plain/packages/registry.py +27 -16
  85. plain/paginator.py +31 -21
  86. plain/preflight/README.md +209 -24
  87. plain/preflight/__init__.py +1 -0
  88. plain/preflight/checks.py +3 -1
  89. plain/preflight/files.py +3 -1
  90. plain/preflight/registry.py +26 -11
  91. plain/preflight/results.py +15 -7
  92. plain/preflight/security.py +15 -13
  93. plain/preflight/settings.py +54 -0
  94. plain/preflight/urls.py +4 -1
  95. plain/runtime/README.md +115 -47
  96. plain/runtime/__init__.py +10 -6
  97. plain/runtime/global_settings.py +34 -25
  98. plain/runtime/secret.py +20 -0
  99. plain/runtime/user_settings.py +110 -38
  100. plain/runtime/utils.py +1 -1
  101. plain/server/LICENSE +35 -0
  102. plain/server/README.md +155 -0
  103. plain/server/__init__.py +9 -0
  104. plain/server/app.py +52 -0
  105. plain/server/arbiter.py +555 -0
  106. plain/server/config.py +118 -0
  107. plain/server/errors.py +31 -0
  108. plain/server/glogging.py +292 -0
  109. plain/server/http/__init__.py +12 -0
  110. plain/server/http/body.py +283 -0
  111. plain/server/http/errors.py +155 -0
  112. plain/server/http/message.py +400 -0
  113. plain/server/http/parser.py +70 -0
  114. plain/server/http/unreader.py +88 -0
  115. plain/server/http/wsgi.py +421 -0
  116. plain/server/pidfile.py +92 -0
  117. plain/server/sock.py +240 -0
  118. plain/server/util.py +317 -0
  119. plain/server/workers/__init__.py +6 -0
  120. plain/server/workers/base.py +304 -0
  121. plain/server/workers/sync.py +212 -0
  122. plain/server/workers/thread.py +399 -0
  123. plain/server/workers/workertmp.py +50 -0
  124. plain/signals/README.md +170 -1
  125. plain/signals/__init__.py +0 -1
  126. plain/signals/dispatch/dispatcher.py +49 -27
  127. plain/signing.py +131 -35
  128. plain/templates/README.md +211 -20
  129. plain/templates/jinja/__init__.py +13 -5
  130. plain/templates/jinja/environments.py +5 -4
  131. plain/templates/jinja/extensions.py +12 -5
  132. plain/templates/jinja/filters.py +7 -2
  133. plain/templates/jinja/globals.py +2 -2
  134. plain/test/README.md +184 -22
  135. plain/test/client.py +340 -222
  136. plain/test/encoding.py +9 -6
  137. plain/test/exceptions.py +7 -2
  138. plain/urls/README.md +157 -73
  139. plain/urls/converters.py +18 -15
  140. plain/urls/exceptions.py +2 -2
  141. plain/urls/patterns.py +38 -22
  142. plain/urls/resolvers.py +35 -25
  143. plain/urls/utils.py +5 -1
  144. plain/utils/README.md +250 -3
  145. plain/utils/cache.py +17 -11
  146. plain/utils/crypto.py +21 -5
  147. plain/utils/datastructures.py +89 -56
  148. plain/utils/dateparse.py +9 -6
  149. plain/utils/deconstruct.py +15 -7
  150. plain/utils/decorators.py +5 -1
  151. plain/utils/dotenv.py +373 -0
  152. plain/utils/duration.py +8 -4
  153. plain/utils/encoding.py +14 -7
  154. plain/utils/functional.py +66 -49
  155. plain/utils/hashable.py +5 -1
  156. plain/utils/html.py +36 -22
  157. plain/utils/http.py +16 -9
  158. plain/utils/inspect.py +14 -6
  159. plain/utils/ipv6.py +7 -3
  160. plain/utils/itercompat.py +6 -1
  161. plain/utils/module_loading.py +7 -3
  162. plain/utils/regex_helper.py +37 -23
  163. plain/utils/safestring.py +14 -6
  164. plain/utils/text.py +41 -23
  165. plain/utils/timezone.py +33 -22
  166. plain/utils/tree.py +35 -19
  167. plain/validators.py +94 -52
  168. plain/views/README.md +156 -79
  169. plain/views/__init__.py +0 -1
  170. plain/views/base.py +25 -18
  171. plain/views/errors.py +13 -5
  172. plain/views/exceptions.py +4 -1
  173. plain/views/forms.py +6 -6
  174. plain/views/objects.py +52 -49
  175. plain/views/redirect.py +18 -15
  176. plain/views/templates.py +5 -3
  177. plain/wsgi.py +3 -1
  178. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
  179. plain-0.103.0.dist-info/RECORD +198 -0
  180. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
  181. plain-0.103.0.dist-info/entry_points.txt +2 -0
  182. plain/AGENTS.md +0 -18
  183. plain/cli/agent/__init__.py +0 -20
  184. plain/cli/agent/docs.py +0 -80
  185. plain/cli/agent/md.py +0 -87
  186. plain/cli/agent/prompt.py +0 -45
  187. plain/csrf/views.py +0 -31
  188. plain/logs/utils.py +0 -46
  189. plain/templates/AGENTS.md +0 -3
  190. plain-0.68.0.dist-info/RECORD +0 -169
  191. plain-0.68.0.dist-info/entry_points.txt +0 -5
  192. {plain-0.68.0.dist-info → plain-0.103.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,12 +11,15 @@ import json
9
11
  import math
10
12
  import re
11
13
  import uuid
14
+ from collections.abc import Callable, Iterable, Iterator, Sequence
12
15
  from decimal import Decimal, DecimalException
13
16
  from io import BytesIO
17
+ from typing import TYPE_CHECKING, Any, Self
14
18
  from urllib.parse import urlsplit, urlunsplit
15
19
 
16
- from plain import validators
20
+ from plain import validators as validators_
17
21
  from plain.exceptions import ValidationError
22
+ from plain.internal import internalcode
18
23
  from plain.utils import timezone
19
24
  from plain.utils.dateparse import parse_datetime, parse_duration
20
25
  from plain.utils.duration import duration_string
@@ -24,6 +29,9 @@ from plain.utils.text import pluralize_lazy
24
29
  from .boundfield import BoundField
25
30
  from .exceptions import FormFieldMissingError
26
31
 
32
+ if TYPE_CHECKING:
33
+ from .forms import BaseForm
34
+
27
35
  __all__ = (
28
36
  "Field",
29
37
  "CharField",
@@ -49,25 +57,25 @@ __all__ = (
49
57
  )
50
58
 
51
59
 
52
- FILE_INPUT_CONTRADICTION = object()
60
+ _FILE_INPUT_CONTRADICTION = object()
53
61
 
54
62
 
55
63
  class Field:
56
- default_validators = [] # Default set of validators
64
+ default_validators: list[Callable[[Any], None]] = [] # Default set of validators
57
65
  # Add an 'invalid' entry to default_error_message if you want a specific
58
66
  # field error message not raised by the field validators.
59
67
  default_error_messages = {
60
68
  "required": "This field is required.",
61
69
  }
62
- empty_values = list(validators.EMPTY_VALUES)
70
+ empty_values = list(validators_.EMPTY_VALUES)
63
71
 
64
72
  def __init__(
65
73
  self,
66
74
  *,
67
- required=True,
68
- initial=None,
69
- error_messages=None,
70
- validators=(),
75
+ required: bool = True,
76
+ initial: Any = None,
77
+ error_messages: dict[str, str] | None = None,
78
+ validators: Sequence[Callable[[Any], None]] = (),
71
79
  ):
72
80
  # required -- Boolean that specifies whether the field is required.
73
81
  # True by default.
@@ -87,19 +95,19 @@ class Field:
87
95
 
88
96
  self.validators = [*self.default_validators, *validators]
89
97
 
90
- def prepare_value(self, value):
98
+ def prepare_value(self, value: Any) -> Any:
91
99
  return value
92
100
 
93
- def to_python(self, value):
101
+ def to_python(self, value: Any) -> Any:
94
102
  return value
95
103
 
96
- def validate(self, value):
104
+ def validate(self, value: Any) -> None:
97
105
  if value in self.empty_values and self.required:
98
106
  raise ValidationError(self.error_messages["required"], code="required")
99
107
 
100
- def run_validators(self, value):
108
+ def run_validators(self, value: Any) -> None:
101
109
  if value in self.empty_values:
102
- return
110
+ return None
103
111
  errors = []
104
112
  for v in self.validators:
105
113
  try:
@@ -111,7 +119,7 @@ class Field:
111
119
  if errors:
112
120
  raise ValidationError(errors)
113
121
 
114
- def clean(self, value):
122
+ def clean(self, value: Any) -> Any:
115
123
  """
116
124
  Validate the given value and return its "cleaned" value as an
117
125
  appropriate Python object. Raise ValidationError for any errors.
@@ -121,7 +129,7 @@ class Field:
121
129
  self.run_validators(value)
122
130
  return value
123
131
 
124
- def bound_data(self, data, initial):
132
+ def bound_data(self, data: Any, initial: Any) -> Any:
125
133
  """
126
134
  Return the value that should be shown for this field on render of a
127
135
  bound form, given the submitted POST data for the field and the initial
@@ -132,12 +140,12 @@ class Field:
132
140
  """
133
141
  return data
134
142
 
135
- def has_changed(self, initial, data):
143
+ def has_changed(self, initial: Any, data: Any) -> bool:
136
144
  """Return True if data differs from initial."""
137
145
  try:
138
146
  data = self.to_python(data)
139
147
  if hasattr(self, "_coerce"):
140
- return self._coerce(data) != self._coerce(initial)
148
+ return self._coerce(data) != self._coerce(initial) # type: ignore[misc]
141
149
  except ValidationError:
142
150
  return True
143
151
  # For purposes of seeing whether something has changed, None is
@@ -147,28 +155,28 @@ class Field:
147
155
  data_value = data if data is not None else ""
148
156
  return initial_value != data_value
149
157
 
150
- def get_bound_field(self, form, field_name):
158
+ def get_bound_field(self, form: BaseForm, field_name: str) -> BoundField:
151
159
  """
152
160
  Return a BoundField instance that will be used when accessing the form
153
161
  field in a template.
154
162
  """
155
163
  return BoundField(form, self, field_name)
156
164
 
157
- def __deepcopy__(self, memo):
165
+ def __deepcopy__(self: Self, memo: dict[int, Any]) -> Self:
158
166
  result = copy.copy(self)
159
167
  memo[id(self)] = result
160
168
  result.error_messages = self.error_messages.copy()
161
169
  result.validators = self.validators[:]
162
170
  return result
163
171
 
164
- def value_from_form_data(self, data, files, html_name):
172
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
165
173
  # By default, all fields are expected to be present in HTML form data.
166
174
  try:
167
175
  return data[html_name]
168
176
  except KeyError as e:
169
177
  raise FormFieldMissingError(html_name) from e
170
178
 
171
- def value_from_json_data(self, data, files, html_name):
179
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
172
180
  if self.required and html_name not in data:
173
181
  raise FormFieldMissingError(html_name)
174
182
 
@@ -177,20 +185,34 @@ class Field:
177
185
 
178
186
  class CharField(Field):
179
187
  def __init__(
180
- self, *, max_length=None, min_length=None, strip=True, empty_value="", **kwargs
188
+ self,
189
+ *,
190
+ max_length: int | None = None,
191
+ min_length: int | None = None,
192
+ strip: bool = True,
193
+ empty_value: str = "",
194
+ required: bool = True,
195
+ initial: Any = None,
196
+ error_messages: dict[str, str] | None = None,
197
+ validators: Sequence[Callable[[Any], None]] = (),
181
198
  ):
182
199
  self.max_length = max_length
183
200
  self.min_length = min_length
184
201
  self.strip = strip
185
202
  self.empty_value = empty_value
186
- super().__init__(**kwargs)
203
+ super().__init__(
204
+ required=required,
205
+ initial=initial,
206
+ error_messages=error_messages,
207
+ validators=validators,
208
+ )
187
209
  if min_length is not None:
188
- self.validators.append(validators.MinLengthValidator(int(min_length)))
210
+ self.validators.append(validators_.MinLengthValidator(int(min_length)))
189
211
  if max_length is not None:
190
- self.validators.append(validators.MaxLengthValidator(int(max_length)))
191
- self.validators.append(validators.ProhibitNullCharactersValidator())
212
+ self.validators.append(validators_.MaxLengthValidator(int(max_length)))
213
+ self.validators.append(validators_.ProhibitNullCharactersValidator())
192
214
 
193
- def to_python(self, value):
215
+ def to_python(self, value: Any) -> str:
194
216
  """Return a string."""
195
217
  if value not in self.empty_values:
196
218
  value = str(value)
@@ -201,24 +223,43 @@ class CharField(Field):
201
223
  return value
202
224
 
203
225
 
204
- class IntegerField(Field):
205
- default_error_messages = {
206
- "invalid": "Enter a whole number.",
207
- }
208
- re_decimal = _lazy_re_compile(r"\.0*\s*$")
226
+ class NumericField(Field):
227
+ """Base class for numeric fields with min/max/step validation."""
209
228
 
210
- def __init__(self, *, max_value=None, min_value=None, step_size=None, **kwargs):
229
+ def __init__(
230
+ self,
231
+ *,
232
+ max_value: int | float | Decimal | None = None,
233
+ min_value: int | float | Decimal | None = None,
234
+ step_size: int | float | Decimal | None = None,
235
+ required: bool = True,
236
+ initial: Any = None,
237
+ error_messages: dict[str, str] | None = None,
238
+ validators: Sequence[Callable[[Any], None]] = (),
239
+ ):
211
240
  self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
212
- super().__init__(**kwargs)
241
+ super().__init__(
242
+ required=required,
243
+ initial=initial,
244
+ error_messages=error_messages,
245
+ validators=validators,
246
+ )
213
247
 
214
248
  if max_value is not None:
215
- self.validators.append(validators.MaxValueValidator(max_value))
249
+ self.validators.append(validators_.MaxValueValidator(max_value))
216
250
  if min_value is not None:
217
- self.validators.append(validators.MinValueValidator(min_value))
251
+ self.validators.append(validators_.MinValueValidator(min_value))
218
252
  if step_size is not None:
219
- self.validators.append(validators.StepValueValidator(step_size))
253
+ self.validators.append(validators_.StepValueValidator(step_size))
254
+
220
255
 
221
- def to_python(self, value):
256
+ class IntegerField(NumericField):
257
+ default_error_messages = {
258
+ "invalid": "Enter a whole number.",
259
+ }
260
+ re_decimal = _lazy_re_compile(r"\.0*\s*$")
261
+
262
+ def to_python(self, value: Any) -> int | None:
222
263
  """
223
264
  Validate that int() can be called on the input. Return the result
224
265
  of int() or None for empty values.
@@ -234,17 +275,17 @@ class IntegerField(Field):
234
275
  return value
235
276
 
236
277
 
237
- class FloatField(IntegerField):
278
+ class FloatField(NumericField):
238
279
  default_error_messages = {
239
280
  "invalid": "Enter a number.",
240
281
  }
241
282
 
242
- def to_python(self, value):
283
+ def to_python(self, value: Any) -> float | None:
243
284
  """
244
285
  Validate that float() can be called on the input. Return the result
245
286
  of float() or None for empty values.
246
287
  """
247
- value = super(IntegerField, self).to_python(value)
288
+ value = super().to_python(value)
248
289
  if value in self.empty_values:
249
290
  return None
250
291
  try:
@@ -253,15 +294,15 @@ class FloatField(IntegerField):
253
294
  raise ValidationError(self.error_messages["invalid"], code="invalid")
254
295
  return value
255
296
 
256
- def validate(self, value):
297
+ def validate(self, value: Any) -> None:
257
298
  super().validate(value)
258
299
  if value in self.empty_values:
259
- return
300
+ return None
260
301
  if not math.isfinite(value):
261
302
  raise ValidationError(self.error_messages["invalid"], code="invalid")
262
303
 
263
304
 
264
- class DecimalField(IntegerField):
305
+ class DecimalField(NumericField):
265
306
  default_error_messages = {
266
307
  "invalid": "Enter a number.",
267
308
  }
@@ -269,17 +310,27 @@ class DecimalField(IntegerField):
269
310
  def __init__(
270
311
  self,
271
312
  *,
272
- max_value=None,
273
- min_value=None,
274
- max_digits=None,
275
- decimal_places=None,
276
- **kwargs,
313
+ max_value: Decimal | int | None = None,
314
+ min_value: Decimal | int | None = None,
315
+ max_digits: int | None = None,
316
+ decimal_places: int | None = None,
317
+ required: bool = True,
318
+ initial: Any = None,
319
+ error_messages: dict[str, str] | None = None,
320
+ validators: Sequence[Callable[[Any], None]] = (),
277
321
  ):
278
322
  self.max_digits, self.decimal_places = max_digits, decimal_places
279
- super().__init__(max_value=max_value, min_value=min_value, **kwargs)
280
- self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
323
+ super().__init__(
324
+ max_value=max_value,
325
+ min_value=min_value,
326
+ required=required,
327
+ initial=initial,
328
+ error_messages=error_messages,
329
+ validators=validators,
330
+ )
331
+ self.validators.append(validators_.DecimalValidator(max_digits, decimal_places))
281
332
 
282
- def to_python(self, value):
333
+ def to_python(self, value: Any) -> Decimal | None:
283
334
  """
284
335
  Validate that the input is a decimal number. Return a Decimal
285
336
  instance or None for empty values. Ensure that there are no more
@@ -294,10 +345,10 @@ class DecimalField(IntegerField):
294
345
  raise ValidationError(self.error_messages["invalid"], code="invalid")
295
346
  return value
296
347
 
297
- def validate(self, value):
348
+ def validate(self, value: Any) -> None:
298
349
  super().validate(value)
299
350
  if value in self.empty_values:
300
- return
351
+ return None
301
352
  if not value.is_finite():
302
353
  raise ValidationError(
303
354
  self.error_messages["invalid"],
@@ -352,12 +403,25 @@ class BaseTemporalField(Field):
352
403
  "%m/%d/%y %H:%M", # '10/25/06 14:30'
353
404
  ]
354
405
 
355
- def __init__(self, *, input_formats=None, **kwargs):
356
- super().__init__(**kwargs)
406
+ def __init__(
407
+ self,
408
+ *,
409
+ input_formats: list[str] | None = None,
410
+ required: bool = True,
411
+ initial: Any = None,
412
+ error_messages: dict[str, str] | None = None,
413
+ validators: Sequence[Callable[[Any], None]] = (),
414
+ ):
415
+ super().__init__(
416
+ required=required,
417
+ initial=initial,
418
+ error_messages=error_messages,
419
+ validators=validators,
420
+ )
357
421
  if input_formats is not None:
358
422
  self.input_formats = input_formats
359
423
 
360
- def to_python(self, value):
424
+ def to_python(self, value: Any) -> Any:
361
425
  value = value.strip()
362
426
  # Try to strptime against each input format.
363
427
  for format in self.input_formats:
@@ -367,7 +431,7 @@ class BaseTemporalField(Field):
367
431
  continue
368
432
  raise ValidationError(self.error_messages["invalid"], code="invalid")
369
433
 
370
- def strptime(self, value, format):
434
+ def strptime(self, value: str, format: str) -> Any:
371
435
  raise NotImplementedError("Subclasses must define this method.")
372
436
 
373
437
 
@@ -377,7 +441,7 @@ class DateField(BaseTemporalField):
377
441
  "invalid": "Enter a valid date.",
378
442
  }
379
443
 
380
- def to_python(self, value):
444
+ def to_python(self, value: Any) -> datetime.date | None:
381
445
  """
382
446
  Validate that the input can be converted to a date. Return a Python
383
447
  datetime.date object.
@@ -390,7 +454,7 @@ class DateField(BaseTemporalField):
390
454
  return value
391
455
  return super().to_python(value)
392
456
 
393
- def strptime(self, value, format):
457
+ def strptime(self, value: str, format: str) -> datetime.date:
394
458
  return datetime.datetime.strptime(value, format).date()
395
459
 
396
460
 
@@ -398,7 +462,7 @@ class TimeField(BaseTemporalField):
398
462
  input_formats = BaseTemporalField.TIME_INPUT_FORMATS
399
463
  default_error_messages = {"invalid": "Enter a valid time."}
400
464
 
401
- def to_python(self, value):
465
+ def to_python(self, value: Any) -> datetime.time | None:
402
466
  """
403
467
  Validate that the input can be converted to a time. Return a Python
404
468
  datetime.time object.
@@ -409,12 +473,13 @@ class TimeField(BaseTemporalField):
409
473
  return value
410
474
  return super().to_python(value)
411
475
 
412
- def strptime(self, value, format):
476
+ def strptime(self, value: str, format: str) -> datetime.time:
413
477
  return datetime.datetime.strptime(value, format).time()
414
478
 
415
479
 
480
+ @internalcode
416
481
  class DateTimeFormatsIterator:
417
- def __iter__(self):
482
+ def __iter__(self) -> Any:
418
483
  yield from BaseTemporalField.DATETIME_INPUT_FORMATS
419
484
  yield from BaseTemporalField.DATE_INPUT_FORMATS
420
485
 
@@ -425,12 +490,12 @@ class DateTimeField(BaseTemporalField):
425
490
  "invalid": "Enter a valid date/time.",
426
491
  }
427
492
 
428
- def prepare_value(self, value):
493
+ def prepare_value(self, value: Any) -> Any:
429
494
  if isinstance(value, datetime.datetime):
430
495
  value = to_current_timezone(value)
431
496
  return value
432
497
 
433
- def to_python(self, value):
498
+ def to_python(self, value: Any) -> datetime.datetime | None:
434
499
  """
435
500
  Validate that the input can be converted to a datetime. Return a
436
501
  Python datetime.datetime object.
@@ -450,7 +515,7 @@ class DateTimeField(BaseTemporalField):
450
515
  result = super().to_python(value)
451
516
  return from_current_timezone(result)
452
517
 
453
- def strptime(self, value, format):
518
+ def strptime(self, value: str, format: str) -> datetime.datetime:
454
519
  return datetime.datetime.strptime(value, format)
455
520
 
456
521
 
@@ -460,12 +525,12 @@ class DurationField(Field):
460
525
  "overflow": "The number of days must be between {min_days} and {max_days}.",
461
526
  }
462
527
 
463
- def prepare_value(self, value):
528
+ def prepare_value(self, value: Any) -> Any:
464
529
  if isinstance(value, datetime.timedelta):
465
530
  return duration_string(value)
466
531
  return value
467
532
 
468
- def to_python(self, value):
533
+ def to_python(self, value: Any) -> datetime.timedelta | None:
469
534
  if value in self.empty_values:
470
535
  return None
471
536
  if isinstance(value, datetime.timedelta):
@@ -486,18 +551,38 @@ class DurationField(Field):
486
551
 
487
552
 
488
553
  class RegexField(CharField):
489
- def __init__(self, regex, **kwargs):
554
+ def __init__(
555
+ self,
556
+ regex: str | re.Pattern[str],
557
+ *,
558
+ max_length: int | None = None,
559
+ min_length: int | None = None,
560
+ strip: bool = False,
561
+ empty_value: str = "",
562
+ required: bool = True,
563
+ initial: Any = None,
564
+ error_messages: dict[str, str] | None = None,
565
+ validators: Sequence[Callable[[Any], None]] = (),
566
+ ) -> None:
490
567
  """
491
568
  regex can be either a string or a compiled regular expression object.
492
569
  """
493
- kwargs.setdefault("strip", False)
494
- super().__init__(**kwargs)
570
+ super().__init__(
571
+ max_length=max_length,
572
+ min_length=min_length,
573
+ strip=strip,
574
+ empty_value=empty_value,
575
+ required=required,
576
+ initial=initial,
577
+ error_messages=error_messages,
578
+ validators=validators,
579
+ )
495
580
  self._set_regex(regex)
496
581
 
497
- def _get_regex(self):
582
+ def _get_regex(self) -> re.Pattern[str]:
498
583
  return self._regex
499
584
 
500
- def _set_regex(self, regex):
585
+ def _set_regex(self, regex: str | re.Pattern[str]) -> None:
501
586
  if isinstance(regex, str):
502
587
  regex = re.compile(regex)
503
588
  self._regex = regex
@@ -506,17 +591,37 @@ class RegexField(CharField):
506
591
  and self._regex_validator in self.validators
507
592
  ):
508
593
  self.validators.remove(self._regex_validator)
509
- self._regex_validator = validators.RegexValidator(regex=regex)
594
+ self._regex_validator = validators_.RegexValidator(regex=regex)
510
595
  self.validators.append(self._regex_validator)
511
596
 
512
597
  regex = property(_get_regex, _set_regex)
513
598
 
514
599
 
515
600
  class EmailField(CharField):
516
- default_validators = [validators.validate_email]
601
+ default_validators = [validators_.validate_email]
517
602
 
518
- def __init__(self, **kwargs):
519
- super().__init__(strip=True, **kwargs)
603
+ def __init__(
604
+ self,
605
+ *,
606
+ max_length: int | None = None,
607
+ min_length: int | None = None,
608
+ strip: bool = True,
609
+ empty_value: str = "",
610
+ required: bool = True,
611
+ initial: Any = None,
612
+ error_messages: dict[str, str] | None = None,
613
+ validators: Sequence[Callable[[Any], None]] = (),
614
+ ) -> None:
615
+ super().__init__(
616
+ max_length=max_length,
617
+ min_length=min_length,
618
+ strip=strip,
619
+ empty_value=empty_value,
620
+ required=required,
621
+ initial=initial,
622
+ error_messages=error_messages,
623
+ validators=validators,
624
+ )
520
625
 
521
626
 
522
627
  class FileField(Field):
@@ -532,19 +637,33 @@ class FileField(Field):
532
637
  "contradiction": "Please either submit a file or check the clear checkbox, not both.",
533
638
  }
534
639
 
535
- def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs):
640
+ def __init__(
641
+ self,
642
+ *,
643
+ max_length: int | None = None,
644
+ allow_empty_file: bool = False,
645
+ required: bool = True,
646
+ initial: Any = None,
647
+ error_messages: dict[str, str] | None = None,
648
+ validators: Sequence[Callable[[Any], None]] = (),
649
+ ) -> None:
536
650
  self.max_length = max_length
537
651
  self.allow_empty_file = allow_empty_file
538
- super().__init__(**kwargs)
652
+ super().__init__(
653
+ required=required,
654
+ initial=initial,
655
+ error_messages=error_messages,
656
+ validators=validators,
657
+ )
539
658
 
540
- def to_python(self, data):
541
- if data in self.empty_values:
659
+ def to_python(self, value: Any) -> Any:
660
+ if value in self.empty_values:
542
661
  return None
543
662
 
544
663
  # UploadedFile objects should have name and size attributes.
545
664
  try:
546
- file_name = data.name
547
- file_size = data.size
665
+ file_name = value.name
666
+ file_size = value.size
548
667
  except AttributeError:
549
668
  raise ValidationError(self.error_messages["invalid"], code="invalid")
550
669
 
@@ -558,11 +677,11 @@ class FileField(Field):
558
677
  if not self.allow_empty_file and not file_size:
559
678
  raise ValidationError(self.error_messages["empty"], code="empty")
560
679
 
561
- return data
680
+ return value
562
681
 
563
- def clean(self, data, initial=None):
682
+ def clean(self, data: Any, initial: Any = None) -> Any: # type: ignore[override]
564
683
  # If the widget got contradictory inputs, we raise a validation error
565
- if data is FILE_INPUT_CONTRADICTION:
684
+ if data is _FILE_INPUT_CONTRADICTION:
566
685
  raise ValidationError(
567
686
  self.error_messages["contradiction"], code="contradiction"
568
687
  )
@@ -581,45 +700,45 @@ class FileField(Field):
581
700
  return initial
582
701
  return super().clean(data)
583
702
 
584
- def bound_data(self, _, initial):
703
+ def bound_data(self, data: Any, initial: Any) -> Any:
585
704
  return initial
586
705
 
587
- def has_changed(self, initial, data):
706
+ def has_changed(self, initial: Any, data: Any) -> bool:
588
707
  return data is not None
589
708
 
590
- def value_from_form_data(self, data, files, html_name):
709
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
591
710
  return files.get(html_name)
592
711
 
593
- def value_from_json_data(self, data, files, html_name):
712
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
594
713
  return files.get(html_name)
595
714
 
596
715
 
597
716
  class ImageField(FileField):
598
- default_validators = [validators.validate_image_file_extension]
717
+ default_validators = [validators_.validate_image_file_extension]
599
718
  default_error_messages = {
600
719
  "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
601
720
  }
602
721
 
603
- def to_python(self, data):
722
+ def to_python(self, value: Any) -> Any:
604
723
  """
605
724
  Check that the file-upload field data contains a valid image (GIF, JPG,
606
725
  PNG, etc. -- whatever Pillow supports).
607
726
  """
608
- f = super().to_python(data)
727
+ f = super().to_python(value)
609
728
  if f is None:
610
729
  return None
611
730
 
612
- from PIL import Image
731
+ from PIL import Image # type: ignore[import-not-found]
613
732
 
614
733
  # We need to get a file object for Pillow. We might have a path or we might
615
734
  # have to read the data into memory.
616
- if hasattr(data, "temporary_file_path"):
617
- file = data.temporary_file_path()
735
+ if hasattr(value, "temporary_file_path"):
736
+ file = value.temporary_file_path()
618
737
  else:
619
- if hasattr(data, "read"):
620
- file = BytesIO(data.read())
738
+ if hasattr(value, "read"):
739
+ file = BytesIO(value.read())
621
740
  else:
622
- file = BytesIO(data["content"])
741
+ file = BytesIO(value["content"])
623
742
 
624
743
  try:
625
744
  # load() could spot a truncated JPEG, but it loads the entire
@@ -648,18 +767,41 @@ class URLField(CharField):
648
767
  default_error_messages = {
649
768
  "invalid": "Enter a valid URL.",
650
769
  }
651
- default_validators = [validators.URLValidator()]
770
+ default_validators = [validators_.URLValidator()]
652
771
 
653
- def __init__(self, **kwargs):
654
- super().__init__(strip=True, **kwargs)
772
+ def __init__(
773
+ self,
774
+ *,
775
+ max_length: int | None = None,
776
+ min_length: int | None = None,
777
+ strip: bool = True,
778
+ empty_value: str = "",
779
+ required: bool = True,
780
+ initial: Any = None,
781
+ error_messages: dict[str, str] | None = None,
782
+ validators: Sequence[Callable[[Any], None]] = (),
783
+ ) -> None:
784
+ super().__init__(
785
+ max_length=max_length,
786
+ min_length=min_length,
787
+ strip=strip,
788
+ empty_value=empty_value,
789
+ required=required,
790
+ initial=initial,
791
+ error_messages=error_messages,
792
+ validators=validators,
793
+ )
655
794
 
656
- def to_python(self, value):
657
- def split_url(url):
795
+ def to_python(self, value: Any) -> str:
796
+ def split_url(url: str | bytes) -> list[str]:
658
797
  """
659
798
  Return a list of url parts via urlparse.urlsplit(), or raise
660
799
  ValidationError for some malformed URLs.
661
800
  """
662
801
  try:
802
+ # Ensure url is a string for consistent typing
803
+ if isinstance(url, bytes):
804
+ url = url.decode("utf-8")
663
805
  return list(urlsplit(url))
664
806
  except ValueError:
665
807
  # urlparse.urlsplit can raise a ValueError with some
@@ -679,13 +821,16 @@ class URLField(CharField):
679
821
  url_fields[2] = ""
680
822
  # Rebuild the url_fields list, since the domain segment may now
681
823
  # contain the path too.
682
- url_fields = split_url(urlunsplit(url_fields))
683
- value = urlunsplit(url_fields)
824
+ url_result = urlunsplit(url_fields)
825
+ url_fields = split_url(
826
+ str(url_result) if isinstance(url_result, bytes) else url_result
827
+ )
828
+ value = str(urlunsplit(url_fields))
684
829
  return value
685
830
 
686
831
 
687
832
  class BooleanField(Field):
688
- def to_python(self, value):
833
+ def to_python(self, value: Any) -> bool:
689
834
  """Return a Python boolean object."""
690
835
  # Explicitly check for the string 'False', which is what a hidden field
691
836
  # will submit for False. Also check for '0', since this is what
@@ -697,16 +842,18 @@ class BooleanField(Field):
697
842
  value = bool(value)
698
843
  return super().to_python(value)
699
844
 
700
- def validate(self, value):
845
+ def validate(self, value: Any) -> None:
701
846
  if not value and self.required:
702
847
  raise ValidationError(self.error_messages["required"], code="required")
703
848
 
704
- def has_changed(self, initial, data):
849
+ def has_changed(self, initial: Any, data: Any) -> bool:
705
850
  # Sometimes data or initial may be a string equivalent of a boolean
706
851
  # so we should run it through to_python first to get a boolean value
707
852
  return self.to_python(initial) != self.to_python(data)
708
853
 
709
- def value_from_form_data(self, data, files, html_name):
854
+ def value_from_form_data(
855
+ self, data: Any, files: Any, html_name: str
856
+ ) -> bool | None:
710
857
  if html_name not in data:
711
858
  # Unselected checkboxes aren't in HTML form data, so return False
712
859
  return False
@@ -723,7 +870,7 @@ class BooleanField(Field):
723
870
  "on": True,
724
871
  }.get(value)
725
872
 
726
- def value_from_json_data(self, data, files, html_name):
873
+ def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
727
874
  # Boolean fields must be present in the JSON data
728
875
  try:
729
876
  return data[html_name]
@@ -737,7 +884,7 @@ class NullBooleanField(BooleanField):
737
884
  to None.
738
885
  """
739
886
 
740
- def to_python(self, value):
887
+ def to_python(self, value: Any) -> bool | None: # type: ignore[override]
741
888
  """
742
889
  Explicitly check for the string 'True' and 'False', which is what a
743
890
  hidden field will submit for True and False, for 'true' and 'false',
@@ -753,15 +900,16 @@ class NullBooleanField(BooleanField):
753
900
  else:
754
901
  return None
755
902
 
756
- def validate(self, value):
903
+ def validate(self, value: Any) -> None:
757
904
  pass
758
905
 
759
906
 
907
+ @internalcode
760
908
  class CallableChoiceIterator:
761
- def __init__(self, choices_func):
909
+ def __init__(self, choices_func: Callable[[], Any]) -> None:
762
910
  self.choices_func = choices_func
763
911
 
764
- def __iter__(self):
912
+ def __iter__(self) -> Iterator[Any]:
765
913
  yield from self.choices_func()
766
914
 
767
915
 
@@ -770,23 +918,38 @@ class ChoiceField(Field):
770
918
  "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
771
919
  }
772
920
 
773
- def __init__(self, *, choices=(), **kwargs):
774
- super().__init__(**kwargs)
921
+ _choices: CallableChoiceIterator | list[Any] # Set by choices property setter
922
+
923
+ def __init__(
924
+ self,
925
+ *,
926
+ choices: Any = (),
927
+ required: bool = True,
928
+ initial: Any = None,
929
+ error_messages: dict[str, str] | None = None,
930
+ validators: Sequence[Callable[[Any], None]] = (),
931
+ ) -> None:
932
+ super().__init__(
933
+ required=required,
934
+ initial=initial,
935
+ error_messages=error_messages,
936
+ validators=validators,
937
+ )
775
938
  if hasattr(choices, "choices"):
776
939
  choices = choices.choices
777
940
  elif isinstance(choices, enum.EnumMeta):
778
941
  choices = [(member.value, member.name) for member in choices]
779
942
  self.choices = choices
780
943
 
781
- def __deepcopy__(self, memo):
944
+ def __deepcopy__(self, memo: dict[int, Any]) -> ChoiceField:
782
945
  result = super().__deepcopy__(memo)
783
946
  result._choices = copy.deepcopy(self._choices, memo)
784
947
  return result
785
948
 
786
- def _get_choices(self):
949
+ def _get_choices(self) -> Iterable[Any]:
787
950
  return self._choices
788
951
 
789
- def _set_choices(self, value):
952
+ def _set_choices(self, value: Any) -> None:
790
953
  # Setting choices also sets the choices on the widget.
791
954
  # choices can be any iterable, but we call list() on it because
792
955
  # it will be consumed more than once.
@@ -799,13 +962,13 @@ class ChoiceField(Field):
799
962
 
800
963
  choices = property(_get_choices, _set_choices)
801
964
 
802
- def to_python(self, value):
965
+ def to_python(self, value: Any) -> str:
803
966
  """Return a string."""
804
967
  if value in self.empty_values:
805
968
  return ""
806
969
  return str(value)
807
970
 
808
- def validate(self, value):
971
+ def validate(self, value: Any) -> None:
809
972
  """Validate that the input is in self.choices."""
810
973
  super().validate(value)
811
974
  if value and not self.valid_value(value):
@@ -815,7 +978,7 @@ class ChoiceField(Field):
815
978
  params={"value": value},
816
979
  )
817
980
 
818
- def valid_value(self, value):
981
+ def valid_value(self, value: Any) -> bool:
819
982
  """Check to see if the provided value is a valid choice."""
820
983
  text_value = str(value)
821
984
  for k, v in self.choices:
@@ -831,12 +994,28 @@ class ChoiceField(Field):
831
994
 
832
995
 
833
996
  class TypedChoiceField(ChoiceField):
834
- def __init__(self, *, coerce=lambda val: val, empty_value="", **kwargs):
997
+ def __init__(
998
+ self,
999
+ *,
1000
+ coerce: Callable[[Any], Any] = lambda val: val,
1001
+ empty_value: Any = "",
1002
+ choices: Any = (),
1003
+ required: bool = True,
1004
+ initial: Any = None,
1005
+ error_messages: dict[str, str] | None = None,
1006
+ validators: Sequence[Callable[[Any], None]] = (),
1007
+ ) -> None:
835
1008
  self.coerce = coerce
836
1009
  self.empty_value = empty_value
837
- super().__init__(**kwargs)
1010
+ super().__init__(
1011
+ choices=choices,
1012
+ required=required,
1013
+ initial=initial,
1014
+ error_messages=error_messages,
1015
+ validators=validators,
1016
+ )
838
1017
 
839
- def _coerce(self, value):
1018
+ def _coerce(self, value: Any) -> Any:
840
1019
  """
841
1020
  Validate that the value can be coerced to the right type (if not empty).
842
1021
  """
@@ -852,7 +1031,7 @@ class TypedChoiceField(ChoiceField):
852
1031
  )
853
1032
  return value
854
1033
 
855
- def clean(self, value):
1034
+ def clean(self, value: Any) -> Any:
856
1035
  value = super().clean(value)
857
1036
  return self._coerce(value)
858
1037
 
@@ -863,7 +1042,7 @@ class MultipleChoiceField(ChoiceField):
863
1042
  "invalid_list": "Enter a list of values.",
864
1043
  }
865
1044
 
866
- def to_python(self, value):
1045
+ def to_python(self, value: Any) -> list[str]: # type: ignore[override]
867
1046
  if not value:
868
1047
  return []
869
1048
  elif not isinstance(value, list | tuple):
@@ -872,7 +1051,7 @@ class MultipleChoiceField(ChoiceField):
872
1051
  )
873
1052
  return [str(val) for val in value]
874
1053
 
875
- def validate(self, value):
1054
+ def validate(self, value: Any) -> None:
876
1055
  """Validate that the input is a list or tuple."""
877
1056
  if self.required and not value:
878
1057
  raise ValidationError(self.error_messages["required"], code="required")
@@ -885,7 +1064,7 @@ class MultipleChoiceField(ChoiceField):
885
1064
  params={"value": val},
886
1065
  )
887
1066
 
888
- def has_changed(self, initial, data):
1067
+ def has_changed(self, initial: Any, data: Any) -> bool:
889
1068
  if initial is None:
890
1069
  initial = []
891
1070
  if data is None:
@@ -896,7 +1075,7 @@ class MultipleChoiceField(ChoiceField):
896
1075
  data_set = {str(value) for value in data}
897
1076
  return data_set != initial_set
898
1077
 
899
- def value_from_form_data(self, data, files, html_name):
1078
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
900
1079
  return data.getlist(html_name)
901
1080
 
902
1081
 
@@ -905,12 +1084,12 @@ class UUIDField(CharField):
905
1084
  "invalid": "Enter a valid UUID.",
906
1085
  }
907
1086
 
908
- def prepare_value(self, value):
1087
+ def prepare_value(self, value: Any) -> Any:
909
1088
  if isinstance(value, uuid.UUID):
910
1089
  return str(value)
911
1090
  return value
912
1091
 
913
- def to_python(self, value):
1092
+ def to_python(self, value: Any) -> uuid.UUID | None: # type: ignore[override]
914
1093
  value = super().to_python(value)
915
1094
  if value in self.empty_values:
916
1095
  return None
@@ -922,10 +1101,12 @@ class UUIDField(CharField):
922
1101
  return value
923
1102
 
924
1103
 
1104
+ @internalcode
925
1105
  class InvalidJSONInput(str):
926
1106
  pass
927
1107
 
928
1108
 
1109
+ @internalcode
929
1110
  class JSONString(str):
930
1111
  pass
931
1112
 
@@ -936,15 +1117,37 @@ class JSONField(CharField):
936
1117
  }
937
1118
 
938
1119
  def __init__(
939
- self, encoder=None, decoder=None, indent=None, sort_keys=False, **kwargs
940
- ):
1120
+ self,
1121
+ encoder: Any = None,
1122
+ decoder: Any = None,
1123
+ indent: int | None = None,
1124
+ sort_keys: bool = False,
1125
+ *,
1126
+ max_length: int | None = None,
1127
+ min_length: int | None = None,
1128
+ strip: bool = True,
1129
+ empty_value: str = "",
1130
+ required: bool = True,
1131
+ initial: Any = None,
1132
+ error_messages: dict[str, str] | None = None,
1133
+ validators: Sequence[Callable[[Any], None]] = (),
1134
+ ) -> None:
941
1135
  self.encoder = encoder
942
1136
  self.decoder = decoder
943
1137
  self.indent = indent
944
1138
  self.sort_keys = sort_keys
945
- super().__init__(**kwargs)
1139
+ super().__init__(
1140
+ max_length=max_length,
1141
+ min_length=min_length,
1142
+ strip=strip,
1143
+ empty_value=empty_value,
1144
+ required=required,
1145
+ initial=initial,
1146
+ error_messages=error_messages,
1147
+ validators=validators,
1148
+ )
946
1149
 
947
- def to_python(self, value):
1150
+ def to_python(self, value: Any) -> Any:
948
1151
  if value in self.empty_values:
949
1152
  return None
950
1153
  elif isinstance(value, list | dict | int | float | JSONString):
@@ -962,7 +1165,7 @@ class JSONField(CharField):
962
1165
  else:
963
1166
  return converted
964
1167
 
965
- def bound_data(self, data, initial):
1168
+ def bound_data(self, data: Any, initial: Any) -> Any:
966
1169
  if data is None:
967
1170
  return None
968
1171
  try:
@@ -970,7 +1173,7 @@ class JSONField(CharField):
970
1173
  except json.JSONDecodeError:
971
1174
  return InvalidJSONInput(data)
972
1175
 
973
- def prepare_value(self, value):
1176
+ def prepare_value(self, value: Any) -> Any:
974
1177
  if isinstance(value, InvalidJSONInput):
975
1178
  return value
976
1179
  return json.dumps(
@@ -981,7 +1184,7 @@ class JSONField(CharField):
981
1184
  cls=self.encoder,
982
1185
  )
983
1186
 
984
- def has_changed(self, initial, data):
1187
+ def has_changed(self, initial: Any, data: Any) -> bool:
985
1188
  if super().has_changed(initial, data):
986
1189
  return True
987
1190
  # For purposes of seeing whether something has changed, True isn't the
@@ -991,7 +1194,7 @@ class JSONField(CharField):
991
1194
  )
992
1195
 
993
1196
 
994
- def from_current_timezone(value):
1197
+ def from_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
995
1198
  """
996
1199
  When time zone support is enabled, convert naive datetimes
997
1200
  entered in the current time zone to aware datetimes.
@@ -1005,7 +1208,7 @@ def from_current_timezone(value):
1005
1208
  except Exception as exc:
1006
1209
  raise ValidationError(
1007
1210
  (
1008
- "%(datetime)s couldnt be interpreted "
1211
+ "%(datetime)s couldn't be interpreted "
1009
1212
  "in time zone %(current_timezone)s; it "
1010
1213
  "may be ambiguous or it may not exist."
1011
1214
  ),
@@ -1015,7 +1218,7 @@ def from_current_timezone(value):
1015
1218
  return value
1016
1219
 
1017
1220
 
1018
- def to_current_timezone(value):
1221
+ def to_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1019
1222
  """
1020
1223
  When time zone support is enabled, convert aware datetimes
1021
1224
  to naive datetimes in the current time zone for display.