plain 0.68.1__py3-none-any.whl → 0.70.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/AGENTS.md +1 -1
- plain/CHANGELOG.md +23 -0
- plain/assets/compile.py +20 -7
- plain/assets/finders.py +15 -11
- plain/assets/fingerprints.py +6 -5
- plain/assets/urls.py +1 -1
- plain/assets/views.py +23 -17
- plain/chores/registry.py +14 -9
- plain/cli/agent/__init__.py +1 -1
- plain/cli/agent/docs.py +7 -6
- plain/cli/agent/llmdocs.py +18 -8
- plain/cli/agent/md.py +19 -14
- plain/cli/agent/prompt.py +1 -1
- plain/cli/agent/request.py +37 -17
- plain/cli/build.py +2 -2
- plain/cli/changelog.py +8 -4
- plain/cli/chores.py +4 -4
- plain/cli/core.py +8 -5
- plain/cli/docs.py +2 -2
- plain/cli/formatting.py +10 -7
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +3 -3
- plain/cli/print.py +1 -1
- plain/cli/registry.py +10 -6
- plain/cli/scaffold.py +1 -1
- plain/cli/settings.py +1 -1
- plain/cli/shell.py +10 -7
- plain/cli/startup.py +3 -3
- plain/cli/urls.py +10 -4
- plain/cli/utils.py +2 -2
- plain/csrf/middleware.py +15 -5
- plain/csrf/views.py +11 -8
- plain/debug.py +5 -2
- plain/exceptions.py +20 -51
- plain/forms/__init__.py +1 -1
- plain/forms/boundfield.py +14 -7
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +139 -97
- plain/forms/forms.py +55 -39
- plain/http/cookie.py +15 -7
- plain/http/multipartparser.py +50 -30
- plain/http/request.py +97 -73
- plain/http/response.py +99 -80
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +34 -18
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +23 -5
- plain/internal/files/uploadedfile.py +42 -26
- plain/internal/files/uploadhandler.py +48 -27
- plain/internal/files/utils.py +13 -6
- plain/internal/handlers/base.py +20 -6
- plain/internal/handlers/exception.py +19 -5
- plain/internal/handlers/wsgi.py +30 -18
- plain/internal/middleware/headers.py +11 -2
- plain/internal/middleware/hosts.py +10 -2
- plain/internal/middleware/https.py +13 -3
- plain/internal/middleware/slash.py +15 -5
- plain/json.py +2 -1
- plain/logs/configure.py +3 -1
- plain/logs/debug.py +16 -5
- plain/logs/formatters.py +6 -3
- plain/logs/loggers.py +56 -52
- plain/logs/utils.py +19 -9
- plain/packages/config.py +14 -6
- plain/packages/registry.py +27 -12
- plain/paginator.py +31 -21
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +25 -10
- plain/preflight/results.py +10 -4
- plain/preflight/security.py +7 -5
- plain/preflight/urls.py +4 -1
- plain/runtime/__init__.py +4 -3
- plain/runtime/global_settings.py +1 -1
- plain/runtime/user_settings.py +26 -17
- plain/runtime/utils.py +1 -1
- plain/signals/dispatch/dispatcher.py +39 -17
- plain/signing.py +49 -30
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +4 -3
- plain/templates/jinja/extensions.py +9 -3
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +1 -1
- plain/test/client.py +246 -174
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +10 -2
- plain/urls/converters.py +13 -10
- plain/urls/patterns.py +32 -20
- plain/urls/resolvers.py +32 -22
- plain/urls/utils.py +5 -1
- plain/utils/cache.py +14 -8
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +84 -54
- plain/utils/dateparse.py +10 -7
- plain/utils/deconstruct.py +12 -4
- plain/utils/decorators.py +5 -1
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +62 -47
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +21 -14
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +23 -13
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +34 -18
- plain/utils/timezone.py +30 -19
- plain/utils/tree.py +31 -18
- plain/validators.py +71 -44
- plain/views/base.py +16 -6
- plain/views/errors.py +11 -4
- plain/views/exceptions.py +4 -1
- plain/views/objects.py +27 -17
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
- plain-0.70.0.dist-info/RECORD +169 -0
- plain-0.68.1.dist-info/RECORD +0 -169
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
plain/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,
|
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__(
|
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__(
|
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
|
-
|
683
|
-
|
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(
|
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__(
|
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,
|
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 couldn
|
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.
|