plain 0.68.0__py3-none-any.whl → 0.101.2__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/CHANGELOG.md +656 -1
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- 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 +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.101.2.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
|
-
|
|
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(
|
|
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,
|
|
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__(
|
|
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(
|
|
210
|
+
self.validators.append(validators_.MinLengthValidator(int(min_length)))
|
|
189
211
|
if max_length is not None:
|
|
190
|
-
self.validators.append(
|
|
191
|
-
self.validators.append(
|
|
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
|
|
205
|
-
|
|
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__(
|
|
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__(
|
|
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(
|
|
249
|
+
self.validators.append(validators_.MaxValueValidator(max_value))
|
|
216
250
|
if min_value is not None:
|
|
217
|
-
self.validators.append(
|
|
251
|
+
self.validators.append(validators_.MinValueValidator(min_value))
|
|
218
252
|
if step_size is not None:
|
|
219
|
-
self.validators.append(
|
|
253
|
+
self.validators.append(validators_.StepValueValidator(step_size))
|
|
254
|
+
|
|
220
255
|
|
|
221
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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__(
|
|
280
|
-
|
|
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__(
|
|
356
|
-
|
|
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__(
|
|
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
|
-
|
|
494
|
-
|
|
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 =
|
|
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 = [
|
|
601
|
+
default_validators = [validators_.validate_email]
|
|
517
602
|
|
|
518
|
-
def __init__(
|
|
519
|
-
|
|
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__(
|
|
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__(
|
|
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,
|
|
541
|
-
if
|
|
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 =
|
|
547
|
-
file_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
|
|
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
|
|
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,
|
|
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 = [
|
|
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,
|
|
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(
|
|
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(
|
|
617
|
-
file =
|
|
735
|
+
if hasattr(value, "temporary_file_path"):
|
|
736
|
+
file = value.temporary_file_path()
|
|
618
737
|
else:
|
|
619
|
-
if hasattr(
|
|
620
|
-
file = BytesIO(
|
|
738
|
+
if hasattr(value, "read"):
|
|
739
|
+
file = BytesIO(value.read())
|
|
621
740
|
else:
|
|
622
|
-
file = BytesIO(
|
|
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 = [
|
|
770
|
+
default_validators = [validators_.URLValidator()]
|
|
652
771
|
|
|
653
|
-
def __init__(
|
|
654
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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(
|
|
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
|
-
|
|
774
|
-
|
|
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__(
|
|
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__(
|
|
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,
|
|
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__(
|
|
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 couldn
|
|
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.
|