plain 0.1.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 (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/forms/fields.py ADDED
@@ -0,0 +1,1030 @@
1
+ """
2
+ Field classes.
3
+ """
4
+
5
+ import copy
6
+ import datetime
7
+ import json
8
+ import math
9
+ import re
10
+ import uuid
11
+ from decimal import Decimal, DecimalException
12
+ from io import BytesIO
13
+ from urllib.parse import urlsplit, urlunsplit
14
+
15
+ from plain import validators
16
+ from plain.exceptions import ValidationError
17
+ from plain.runtime import settings
18
+ from plain.utils import timezone
19
+ from plain.utils.dateparse import parse_datetime, parse_duration
20
+ from plain.utils.duration import duration_string
21
+ from plain.utils.regex_helper import _lazy_re_compile
22
+ from plain.utils.text import pluralize_lazy
23
+
24
+ from .boundfield import BoundField
25
+ from .exceptions import FormFieldMissingError
26
+
27
+ __all__ = (
28
+ "Field",
29
+ "CharField",
30
+ "IntegerField",
31
+ "DateField",
32
+ "TimeField",
33
+ "DateTimeField",
34
+ "DurationField",
35
+ "RegexField",
36
+ "EmailField",
37
+ "FileField",
38
+ "ImageField",
39
+ "URLField",
40
+ "BooleanField",
41
+ "NullBooleanField",
42
+ "ChoiceField",
43
+ "MultipleChoiceField",
44
+ "FloatField",
45
+ "DecimalField",
46
+ "JSONField",
47
+ "SlugField",
48
+ "TypedChoiceField",
49
+ "UUIDField",
50
+ )
51
+
52
+
53
+ FILE_INPUT_CONTRADICTION = object()
54
+
55
+
56
+ class Field:
57
+ default_validators = [] # Default set of validators
58
+ # Add an 'invalid' entry to default_error_message if you want a specific
59
+ # field error message not raised by the field validators.
60
+ default_error_messages = {
61
+ "required": "This field is required.",
62
+ }
63
+ empty_values = list(validators.EMPTY_VALUES)
64
+
65
+ def __init__(
66
+ self,
67
+ *,
68
+ required=True,
69
+ initial=None,
70
+ error_messages=None,
71
+ validators=(),
72
+ disabled=False,
73
+ ):
74
+ # required -- Boolean that specifies whether the field is required.
75
+ # True by default.
76
+ # widget -- A Widget class, or instance of a Widget class, that should
77
+ # be used for this Field when displaying it. Each Field has a
78
+ # default Widget that it'll use if you don't specify this. In
79
+ # most cases, the default widget is TextInput.
80
+ # initial -- A value to use in this Field's initial display. This value
81
+ # is *not* used as a fallback if data isn't given.
82
+ # error_messages -- An optional dictionary to override the default
83
+ # messages that the field will raise.
84
+ # validators -- List of additional validators to use
85
+ # disabled -- Boolean that specifies whether the field is disabled, that
86
+ # is its widget is shown in the form but not editable.
87
+ self.required, self.initial = required, initial
88
+ self.disabled = disabled
89
+
90
+ messages = {}
91
+ for c in reversed(self.__class__.__mro__):
92
+ messages.update(getattr(c, "default_error_messages", {}))
93
+ messages.update(error_messages or {})
94
+ self.error_messages = messages
95
+
96
+ self.validators = [*self.default_validators, *validators]
97
+
98
+ def prepare_value(self, value):
99
+ return value
100
+
101
+ def to_python(self, value):
102
+ return value
103
+
104
+ def validate(self, value):
105
+ if value in self.empty_values and self.required:
106
+ raise ValidationError(self.error_messages["required"], code="required")
107
+
108
+ def run_validators(self, value):
109
+ if value in self.empty_values:
110
+ return
111
+ errors = []
112
+ for v in self.validators:
113
+ try:
114
+ v(value)
115
+ except ValidationError as e:
116
+ if hasattr(e, "code") and e.code in self.error_messages:
117
+ e.message = self.error_messages[e.code]
118
+ errors.extend(e.error_list)
119
+ if errors:
120
+ raise ValidationError(errors)
121
+
122
+ def clean(self, value):
123
+ """
124
+ Validate the given value and return its "cleaned" value as an
125
+ appropriate Python object. Raise ValidationError for any errors.
126
+ """
127
+ value = self.to_python(value)
128
+ self.validate(value)
129
+ self.run_validators(value)
130
+ return value
131
+
132
+ def bound_data(self, data, initial):
133
+ """
134
+ Return the value that should be shown for this field on render of a
135
+ bound form, given the submitted POST data for the field and the initial
136
+ data, if any.
137
+
138
+ For most fields, this will simply be data; FileFields need to handle it
139
+ a bit differently.
140
+ """
141
+ if self.disabled:
142
+ return initial
143
+ return data
144
+
145
+ def has_changed(self, initial, data):
146
+ """Return True if data differs from initial."""
147
+ # Always return False if the field is disabled since self.bound_data
148
+ # always uses the initial value in this case.
149
+ if self.disabled:
150
+ return False
151
+ try:
152
+ data = self.to_python(data)
153
+ if hasattr(self, "_coerce"):
154
+ return self._coerce(data) != self._coerce(initial)
155
+ except ValidationError:
156
+ return True
157
+ # For purposes of seeing whether something has changed, None is
158
+ # the same as an empty string, if the data or initial value we get
159
+ # is None, replace it with ''.
160
+ initial_value = initial if initial is not None else ""
161
+ data_value = data if data is not None else ""
162
+ return initial_value != data_value
163
+
164
+ def get_bound_field(self, form, field_name):
165
+ """
166
+ Return a BoundField instance that will be used when accessing the form
167
+ field in a template.
168
+ """
169
+ return BoundField(form, self, field_name)
170
+
171
+ def __deepcopy__(self, memo):
172
+ result = copy.copy(self)
173
+ memo[id(self)] = result
174
+ result.error_messages = self.error_messages.copy()
175
+ result.validators = self.validators[:]
176
+ return result
177
+
178
+ def value_from_form_data(self, data, files, html_name):
179
+ try:
180
+ return data[html_name]
181
+ except KeyError as e:
182
+ raise FormFieldMissingError(
183
+ f'The "{html_name}" field is missing from the form data.'
184
+ ) from e
185
+
186
+
187
+ class CharField(Field):
188
+ def __init__(
189
+ self, *, max_length=None, min_length=None, strip=True, empty_value="", **kwargs
190
+ ):
191
+ self.max_length = max_length
192
+ self.min_length = min_length
193
+ self.strip = strip
194
+ self.empty_value = empty_value
195
+ super().__init__(**kwargs)
196
+ if min_length is not None:
197
+ self.validators.append(validators.MinLengthValidator(int(min_length)))
198
+ if max_length is not None:
199
+ self.validators.append(validators.MaxLengthValidator(int(max_length)))
200
+ self.validators.append(validators.ProhibitNullCharactersValidator())
201
+
202
+ def to_python(self, value):
203
+ """Return a string."""
204
+ if value not in self.empty_values:
205
+ value = str(value)
206
+ if self.strip:
207
+ value = value.strip()
208
+ if value in self.empty_values:
209
+ return self.empty_value
210
+ return value
211
+
212
+
213
+ class IntegerField(Field):
214
+ default_error_messages = {
215
+ "invalid": "Enter a whole number.",
216
+ }
217
+ re_decimal = _lazy_re_compile(r"\.0*\s*$")
218
+
219
+ def __init__(self, *, max_value=None, min_value=None, step_size=None, **kwargs):
220
+ self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
221
+ super().__init__(**kwargs)
222
+
223
+ if max_value is not None:
224
+ self.validators.append(validators.MaxValueValidator(max_value))
225
+ if min_value is not None:
226
+ self.validators.append(validators.MinValueValidator(min_value))
227
+ if step_size is not None:
228
+ self.validators.append(validators.StepValueValidator(step_size))
229
+
230
+ def to_python(self, value):
231
+ """
232
+ Validate that int() can be called on the input. Return the result
233
+ of int() or None for empty values.
234
+ """
235
+ value = super().to_python(value)
236
+ if value in self.empty_values:
237
+ return None
238
+ # Strip trailing decimal and zeros.
239
+ try:
240
+ value = int(self.re_decimal.sub("", str(value)))
241
+ except (ValueError, TypeError):
242
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
243
+ return value
244
+
245
+
246
+ class FloatField(IntegerField):
247
+ default_error_messages = {
248
+ "invalid": "Enter a number.",
249
+ }
250
+
251
+ def to_python(self, value):
252
+ """
253
+ Validate that float() can be called on the input. Return the result
254
+ of float() or None for empty values.
255
+ """
256
+ value = super(IntegerField, self).to_python(value)
257
+ if value in self.empty_values:
258
+ return None
259
+ try:
260
+ value = float(value)
261
+ except (ValueError, TypeError):
262
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
263
+ return value
264
+
265
+ def validate(self, value):
266
+ super().validate(value)
267
+ if value in self.empty_values:
268
+ return
269
+ if not math.isfinite(value):
270
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
271
+
272
+
273
+ class DecimalField(IntegerField):
274
+ default_error_messages = {
275
+ "invalid": "Enter a number.",
276
+ }
277
+
278
+ def __init__(
279
+ self,
280
+ *,
281
+ max_value=None,
282
+ min_value=None,
283
+ max_digits=None,
284
+ decimal_places=None,
285
+ **kwargs,
286
+ ):
287
+ self.max_digits, self.decimal_places = max_digits, decimal_places
288
+ super().__init__(max_value=max_value, min_value=min_value, **kwargs)
289
+ self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
290
+
291
+ def to_python(self, value):
292
+ """
293
+ Validate that the input is a decimal number. Return a Decimal
294
+ instance or None for empty values. Ensure that there are no more
295
+ than max_digits in the number and no more than decimal_places digits
296
+ after the decimal point.
297
+ """
298
+ if value in self.empty_values:
299
+ return None
300
+ try:
301
+ value = Decimal(str(value))
302
+ except DecimalException:
303
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
304
+ return value
305
+
306
+ def validate(self, value):
307
+ super().validate(value)
308
+ if value in self.empty_values:
309
+ return
310
+ if not value.is_finite():
311
+ raise ValidationError(
312
+ self.error_messages["invalid"],
313
+ code="invalid",
314
+ params={"value": value},
315
+ )
316
+
317
+
318
+ class BaseTemporalField(Field):
319
+ # Default formats to be used when parsing dates from input boxes, in order
320
+ # See all available format string here:
321
+ # https://docs.python.org/library/datetime.html#strftime-behavior
322
+ # * Note that these format strings are different from the ones to display dates
323
+ DATE_INPUT_FORMATS = [
324
+ "%Y-%m-%d", # '2006-10-25'
325
+ "%m/%d/%Y", # '10/25/2006'
326
+ "%m/%d/%y", # '10/25/06'
327
+ "%b %d %Y", # 'Oct 25 2006'
328
+ "%b %d, %Y", # 'Oct 25, 2006'
329
+ "%d %b %Y", # '25 Oct 2006'
330
+ "%d %b, %Y", # '25 Oct, 2006'
331
+ "%B %d %Y", # 'October 25 2006'
332
+ "%B %d, %Y", # 'October 25, 2006'
333
+ "%d %B %Y", # '25 October 2006'
334
+ "%d %B, %Y", # '25 October, 2006'
335
+ ]
336
+
337
+ # Default formats to be used when parsing times from input boxes, in order
338
+ # See all available format string here:
339
+ # https://docs.python.org/library/datetime.html#strftime-behavior
340
+ # * Note that these format strings are different from the ones to display dates
341
+ TIME_INPUT_FORMATS = [
342
+ "%H:%M:%S", # '14:30:59'
343
+ "%H:%M:%S.%f", # '14:30:59.000200'
344
+ "%H:%M", # '14:30'
345
+ ]
346
+
347
+ # Default formats to be used when parsing dates and times from input boxes,
348
+ # in order
349
+ # See all available format string here:
350
+ # https://docs.python.org/library/datetime.html#strftime-behavior
351
+ # * Note that these format strings are different from the ones to display dates
352
+ DATETIME_INPUT_FORMATS = [
353
+ "%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
354
+ "%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200'
355
+ "%Y-%m-%d %H:%M", # '2006-10-25 14:30'
356
+ "%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
357
+ "%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200'
358
+ "%m/%d/%Y %H:%M", # '10/25/2006 14:30'
359
+ "%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
360
+ "%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200'
361
+ "%m/%d/%y %H:%M", # '10/25/06 14:30'
362
+ ]
363
+
364
+ def __init__(self, *, input_formats=None, **kwargs):
365
+ super().__init__(**kwargs)
366
+ if input_formats is not None:
367
+ self.input_formats = input_formats
368
+
369
+ def to_python(self, value):
370
+ value = value.strip()
371
+ # Try to strptime against each input format.
372
+ for format in self.input_formats:
373
+ try:
374
+ return self.strptime(value, format)
375
+ except (ValueError, TypeError):
376
+ continue
377
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
378
+
379
+ def strptime(self, value, format):
380
+ raise NotImplementedError("Subclasses must define this method.")
381
+
382
+
383
+ class DateField(BaseTemporalField):
384
+ input_formats = BaseTemporalField.DATE_INPUT_FORMATS
385
+ default_error_messages = {
386
+ "invalid": "Enter a valid date.",
387
+ }
388
+
389
+ def to_python(self, value):
390
+ """
391
+ Validate that the input can be converted to a date. Return a Python
392
+ datetime.date object.
393
+ """
394
+ if value in self.empty_values:
395
+ return None
396
+ if isinstance(value, datetime.datetime):
397
+ return value.date()
398
+ if isinstance(value, datetime.date):
399
+ return value
400
+ return super().to_python(value)
401
+
402
+ def strptime(self, value, format):
403
+ return datetime.datetime.strptime(value, format).date()
404
+
405
+
406
+ class TimeField(BaseTemporalField):
407
+ input_formats = BaseTemporalField.TIME_INPUT_FORMATS
408
+ default_error_messages = {"invalid": "Enter a valid time."}
409
+
410
+ def to_python(self, value):
411
+ """
412
+ Validate that the input can be converted to a time. Return a Python
413
+ datetime.time object.
414
+ """
415
+ if value in self.empty_values:
416
+ return None
417
+ if isinstance(value, datetime.time):
418
+ return value
419
+ return super().to_python(value)
420
+
421
+ def strptime(self, value, format):
422
+ return datetime.datetime.strptime(value, format).time()
423
+
424
+
425
+ class DateTimeFormatsIterator:
426
+ def __iter__(self):
427
+ yield from BaseTemporalField.DATETIME_INPUT_FORMATS
428
+ yield from BaseTemporalField.DATE_INPUT_FORMATS
429
+
430
+
431
+ class DateTimeField(BaseTemporalField):
432
+ input_formats = DateTimeFormatsIterator()
433
+ default_error_messages = {
434
+ "invalid": "Enter a valid date/time.",
435
+ }
436
+
437
+ def prepare_value(self, value):
438
+ if isinstance(value, datetime.datetime):
439
+ value = to_current_timezone(value)
440
+ return value
441
+
442
+ def to_python(self, value):
443
+ """
444
+ Validate that the input can be converted to a datetime. Return a
445
+ Python datetime.datetime object.
446
+ """
447
+ if value in self.empty_values:
448
+ return None
449
+ if isinstance(value, datetime.datetime):
450
+ return from_current_timezone(value)
451
+ if isinstance(value, datetime.date):
452
+ result = datetime.datetime(value.year, value.month, value.day)
453
+ return from_current_timezone(result)
454
+ try:
455
+ result = parse_datetime(value.strip())
456
+ except ValueError:
457
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
458
+ if not result:
459
+ result = super().to_python(value)
460
+ return from_current_timezone(result)
461
+
462
+ def strptime(self, value, format):
463
+ return datetime.datetime.strptime(value, format)
464
+
465
+
466
+ class DurationField(Field):
467
+ default_error_messages = {
468
+ "invalid": "Enter a valid duration.",
469
+ "overflow": "The number of days must be between {min_days} and {max_days}.",
470
+ }
471
+
472
+ def prepare_value(self, value):
473
+ if isinstance(value, datetime.timedelta):
474
+ return duration_string(value)
475
+ return value
476
+
477
+ def to_python(self, value):
478
+ if value in self.empty_values:
479
+ return None
480
+ if isinstance(value, datetime.timedelta):
481
+ return value
482
+ try:
483
+ value = parse_duration(str(value))
484
+ except OverflowError:
485
+ raise ValidationError(
486
+ self.error_messages["overflow"].format(
487
+ min_days=datetime.timedelta.min.days,
488
+ max_days=datetime.timedelta.max.days,
489
+ ),
490
+ code="overflow",
491
+ )
492
+ if value is None:
493
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
494
+ return value
495
+
496
+
497
+ class RegexField(CharField):
498
+ def __init__(self, regex, **kwargs):
499
+ """
500
+ regex can be either a string or a compiled regular expression object.
501
+ """
502
+ kwargs.setdefault("strip", False)
503
+ super().__init__(**kwargs)
504
+ self._set_regex(regex)
505
+
506
+ def _get_regex(self):
507
+ return self._regex
508
+
509
+ def _set_regex(self, regex):
510
+ if isinstance(regex, str):
511
+ regex = re.compile(regex)
512
+ self._regex = regex
513
+ if (
514
+ hasattr(self, "_regex_validator")
515
+ and self._regex_validator in self.validators
516
+ ):
517
+ self.validators.remove(self._regex_validator)
518
+ self._regex_validator = validators.RegexValidator(regex=regex)
519
+ self.validators.append(self._regex_validator)
520
+
521
+ regex = property(_get_regex, _set_regex)
522
+
523
+
524
+ class EmailField(CharField):
525
+ default_validators = [validators.validate_email]
526
+
527
+ def __init__(self, **kwargs):
528
+ super().__init__(strip=True, **kwargs)
529
+
530
+
531
+ class FileField(Field):
532
+ default_error_messages = {
533
+ "invalid": "No file was submitted. Check the encoding type on the form.",
534
+ "missing": "No file was submitted.",
535
+ "empty": "The submitted file is empty.",
536
+ "text": pluralize_lazy(
537
+ "Ensure this filename has at most %(max)d character (it has %(length)d).",
538
+ "Ensure this filename has at most %(max)d characters (it has %(length)d).",
539
+ "max",
540
+ ),
541
+ "contradiction": "Please either submit a file or check the clear checkbox, not both.",
542
+ }
543
+
544
+ def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs):
545
+ self.max_length = max_length
546
+ self.allow_empty_file = allow_empty_file
547
+ super().__init__(**kwargs)
548
+
549
+ def to_python(self, data):
550
+ if data in self.empty_values:
551
+ return None
552
+
553
+ # UploadedFile objects should have name and size attributes.
554
+ try:
555
+ file_name = data.name
556
+ file_size = data.size
557
+ except AttributeError:
558
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
559
+
560
+ if self.max_length is not None and len(file_name) > self.max_length:
561
+ params = {"max": self.max_length, "length": len(file_name)}
562
+ raise ValidationError(
563
+ self.error_messages["max_length"], code="max_length", params=params
564
+ )
565
+ if not file_name:
566
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
567
+ if not self.allow_empty_file and not file_size:
568
+ raise ValidationError(self.error_messages["empty"], code="empty")
569
+
570
+ return data
571
+
572
+ def clean(self, data, initial=None):
573
+ # If the widget got contradictory inputs, we raise a validation error
574
+ if data is FILE_INPUT_CONTRADICTION:
575
+ raise ValidationError(
576
+ self.error_messages["contradiction"], code="contradiction"
577
+ )
578
+ # False means the field value should be cleared; further validation is
579
+ # not needed.
580
+ if data is False:
581
+ if not self.required:
582
+ return False
583
+ # If the field is required, clearing is not possible (the widget
584
+ # shouldn't return False data in that case anyway). False is not
585
+ # in self.empty_value; if a False value makes it this far
586
+ # it should be validated from here on out as None (so it will be
587
+ # caught by the required check).
588
+ data = None
589
+ if not data and initial:
590
+ return initial
591
+ return super().clean(data)
592
+
593
+ def bound_data(self, _, initial):
594
+ return initial
595
+
596
+ def has_changed(self, initial, data):
597
+ return not self.disabled and data is not None
598
+
599
+ def value_from_form_data(self, data, files, html_name):
600
+ return files.get(html_name)
601
+
602
+
603
+ class ImageField(FileField):
604
+ default_validators = [validators.validate_image_file_extension]
605
+ default_error_messages = {
606
+ "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
607
+ }
608
+
609
+ def to_python(self, data):
610
+ """
611
+ Check that the file-upload field data contains a valid image (GIF, JPG,
612
+ PNG, etc. -- whatever Pillow supports).
613
+ """
614
+ f = super().to_python(data)
615
+ if f is None:
616
+ return None
617
+
618
+ from PIL import Image
619
+
620
+ # We need to get a file object for Pillow. We might have a path or we might
621
+ # have to read the data into memory.
622
+ if hasattr(data, "temporary_file_path"):
623
+ file = data.temporary_file_path()
624
+ else:
625
+ if hasattr(data, "read"):
626
+ file = BytesIO(data.read())
627
+ else:
628
+ file = BytesIO(data["content"])
629
+
630
+ try:
631
+ # load() could spot a truncated JPEG, but it loads the entire
632
+ # image in memory, which is a DoS vector. See #3848 and #18520.
633
+ image = Image.open(file)
634
+ # verify() must be called immediately after the constructor.
635
+ image.verify()
636
+
637
+ # Annotating so subclasses can reuse it for their own validation
638
+ f.image = image
639
+ # Pillow doesn't detect the MIME type of all formats. In those
640
+ # cases, content_type will be None.
641
+ f.content_type = Image.MIME.get(image.format)
642
+ except Exception as exc:
643
+ # Pillow doesn't recognize it as an image.
644
+ raise ValidationError(
645
+ self.error_messages["invalid_image"],
646
+ code="invalid_image",
647
+ ) from exc
648
+ if hasattr(f, "seek") and callable(f.seek):
649
+ f.seek(0)
650
+ return f
651
+
652
+
653
+ class URLField(CharField):
654
+ default_error_messages = {
655
+ "invalid": "Enter a valid URL.",
656
+ }
657
+ default_validators = [validators.URLValidator()]
658
+
659
+ def __init__(self, **kwargs):
660
+ super().__init__(strip=True, **kwargs)
661
+
662
+ def to_python(self, value):
663
+ def split_url(url):
664
+ """
665
+ Return a list of url parts via urlparse.urlsplit(), or raise
666
+ ValidationError for some malformed URLs.
667
+ """
668
+ try:
669
+ return list(urlsplit(url))
670
+ except ValueError:
671
+ # urlparse.urlsplit can raise a ValueError with some
672
+ # misformatted URLs.
673
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
674
+
675
+ value = super().to_python(value)
676
+ if value:
677
+ url_fields = split_url(value)
678
+ if not url_fields[0]:
679
+ # If no URL scheme given, assume http://
680
+ url_fields[0] = "http"
681
+ if not url_fields[1]:
682
+ # Assume that if no domain is provided, that the path segment
683
+ # contains the domain.
684
+ url_fields[1] = url_fields[2]
685
+ url_fields[2] = ""
686
+ # Rebuild the url_fields list, since the domain segment may now
687
+ # contain the path too.
688
+ url_fields = split_url(urlunsplit(url_fields))
689
+ value = urlunsplit(url_fields)
690
+ return value
691
+
692
+
693
+ class BooleanField(Field):
694
+ def to_python(self, value):
695
+ """Return a Python boolean object."""
696
+ # Explicitly check for the string 'False', which is what a hidden field
697
+ # will submit for False. Also check for '0', since this is what
698
+ # RadioSelect will provide. Because bool("True") == bool('1') == True,
699
+ # we don't need to handle that explicitly.
700
+ if isinstance(value, str) and value.lower() in ("false", "0"):
701
+ value = False
702
+ else:
703
+ value = bool(value)
704
+ return super().to_python(value)
705
+
706
+ def validate(self, value):
707
+ if not value and self.required:
708
+ raise ValidationError(self.error_messages["required"], code="required")
709
+
710
+ def has_changed(self, initial, data):
711
+ if self.disabled:
712
+ return False
713
+ # Sometimes data or initial may be a string equivalent of a boolean
714
+ # so we should run it through to_python first to get a boolean value
715
+ return self.to_python(initial) != self.to_python(data)
716
+
717
+ def value_from_form_data(self, data, files, html_name):
718
+ if html_name not in data:
719
+ # Unselected checkboxes aren't in HTML form data, so return False
720
+ return False
721
+
722
+ value = data.get(html_name)
723
+ # Translate true and false strings to boolean values.
724
+ return {
725
+ True: True,
726
+ "True": True,
727
+ "False": False,
728
+ False: False,
729
+ "true": True,
730
+ "false": False,
731
+ "on": True,
732
+ }.get(value)
733
+
734
+
735
+ class NullBooleanField(BooleanField):
736
+ """
737
+ A field whose valid values are None, True, and False. Clean invalid values
738
+ to None.
739
+ """
740
+
741
+ def to_python(self, value):
742
+ """
743
+ Explicitly check for the string 'True' and 'False', which is what a
744
+ hidden field will submit for True and False, for 'true' and 'false',
745
+ which are likely to be returned by JavaScript serializations of forms,
746
+ and for '1' and '0', which is what a RadioField will submit. Unlike
747
+ the Booleanfield, this field must check for True because it doesn't
748
+ use the bool() function.
749
+ """
750
+ if value in (True, "True", "true", "1"):
751
+ return True
752
+ elif value in (False, "False", "false", "0"):
753
+ return False
754
+ else:
755
+ return None
756
+
757
+ def validate(self, value):
758
+ pass
759
+
760
+
761
+ class CallableChoiceIterator:
762
+ def __init__(self, choices_func):
763
+ self.choices_func = choices_func
764
+
765
+ def __iter__(self):
766
+ yield from self.choices_func()
767
+
768
+
769
+ class ChoiceField(Field):
770
+ default_error_messages = {
771
+ "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
772
+ }
773
+
774
+ def __init__(self, *, choices=(), **kwargs):
775
+ super().__init__(**kwargs)
776
+ if hasattr(choices, "choices"):
777
+ choices = choices.choices
778
+ self.choices = choices
779
+
780
+ def __deepcopy__(self, memo):
781
+ result = super().__deepcopy__(memo)
782
+ result._choices = copy.deepcopy(self._choices, memo)
783
+ return result
784
+
785
+ def _get_choices(self):
786
+ return self._choices
787
+
788
+ def _set_choices(self, value):
789
+ # Setting choices also sets the choices on the widget.
790
+ # choices can be any iterable, but we call list() on it because
791
+ # it will be consumed more than once.
792
+ if callable(value):
793
+ value = CallableChoiceIterator(value)
794
+ else:
795
+ value = list(value)
796
+
797
+ self._choices = value
798
+
799
+ choices = property(_get_choices, _set_choices)
800
+
801
+ def to_python(self, value):
802
+ """Return a string."""
803
+ if value in self.empty_values:
804
+ return ""
805
+ return str(value)
806
+
807
+ def validate(self, value):
808
+ """Validate that the input is in self.choices."""
809
+ super().validate(value)
810
+ if value and not self.valid_value(value):
811
+ raise ValidationError(
812
+ self.error_messages["invalid_choice"],
813
+ code="invalid_choice",
814
+ params={"value": value},
815
+ )
816
+
817
+ def valid_value(self, value):
818
+ """Check to see if the provided value is a valid choice."""
819
+ text_value = str(value)
820
+ for k, v in self.choices:
821
+ if isinstance(v, list | tuple):
822
+ # This is an optgroup, so look inside the group for options
823
+ for k2, v2 in v:
824
+ if value == k2 or text_value == str(k2):
825
+ return True
826
+ else:
827
+ if value == k or text_value == str(k):
828
+ return True
829
+ return False
830
+
831
+
832
+ class TypedChoiceField(ChoiceField):
833
+ def __init__(self, *, coerce=lambda val: val, empty_value="", **kwargs):
834
+ self.coerce = coerce
835
+ self.empty_value = empty_value
836
+ super().__init__(**kwargs)
837
+
838
+ def _coerce(self, value):
839
+ """
840
+ Validate that the value can be coerced to the right type (if not empty).
841
+ """
842
+ if value == self.empty_value or value in self.empty_values:
843
+ return self.empty_value
844
+ try:
845
+ value = self.coerce(value)
846
+ except (ValueError, TypeError, ValidationError):
847
+ raise ValidationError(
848
+ self.error_messages["invalid_choice"],
849
+ code="invalid_choice",
850
+ params={"value": value},
851
+ )
852
+ return value
853
+
854
+ def clean(self, value):
855
+ value = super().clean(value)
856
+ return self._coerce(value)
857
+
858
+
859
+ class MultipleChoiceField(ChoiceField):
860
+ default_error_messages = {
861
+ "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
862
+ "invalid_list": "Enter a list of values.",
863
+ }
864
+
865
+ def to_python(self, value):
866
+ if not value:
867
+ return []
868
+ elif not isinstance(value, list | tuple):
869
+ raise ValidationError(
870
+ self.error_messages["invalid_list"], code="invalid_list"
871
+ )
872
+ return [str(val) for val in value]
873
+
874
+ def validate(self, value):
875
+ """Validate that the input is a list or tuple."""
876
+ if self.required and not value:
877
+ raise ValidationError(self.error_messages["required"], code="required")
878
+ # Validate that each value in the value list is in self.choices.
879
+ for val in value:
880
+ if not self.valid_value(val):
881
+ raise ValidationError(
882
+ self.error_messages["invalid_choice"],
883
+ code="invalid_choice",
884
+ params={"value": val},
885
+ )
886
+
887
+ def has_changed(self, initial, data):
888
+ if self.disabled:
889
+ return False
890
+ if initial is None:
891
+ initial = []
892
+ if data is None:
893
+ data = []
894
+ if len(initial) != len(data):
895
+ return True
896
+ initial_set = {str(value) for value in initial}
897
+ data_set = {str(value) for value in data}
898
+ return data_set != initial_set
899
+
900
+ def value_from_form_data(self, data, files, html_name):
901
+ return data.getlist(html_name)
902
+
903
+
904
+ class SlugField(CharField):
905
+ default_validators = [validators.validate_slug]
906
+
907
+ def __init__(self, *, allow_unicode=False, **kwargs):
908
+ self.allow_unicode = allow_unicode
909
+ if self.allow_unicode:
910
+ self.default_validators = [validators.validate_unicode_slug]
911
+ super().__init__(**kwargs)
912
+
913
+
914
+ class UUIDField(CharField):
915
+ default_error_messages = {
916
+ "invalid": "Enter a valid UUID.",
917
+ }
918
+
919
+ def prepare_value(self, value):
920
+ if isinstance(value, uuid.UUID):
921
+ return str(value)
922
+ return value
923
+
924
+ def to_python(self, value):
925
+ value = super().to_python(value)
926
+ if value in self.empty_values:
927
+ return None
928
+ if not isinstance(value, uuid.UUID):
929
+ try:
930
+ value = uuid.UUID(value)
931
+ except ValueError:
932
+ raise ValidationError(self.error_messages["invalid"], code="invalid")
933
+ return value
934
+
935
+
936
+ class InvalidJSONInput(str):
937
+ pass
938
+
939
+
940
+ class JSONString(str):
941
+ pass
942
+
943
+
944
+ class JSONField(CharField):
945
+ default_error_messages = {
946
+ "invalid": "Enter a valid JSON.",
947
+ }
948
+
949
+ def __init__(self, encoder=None, decoder=None, **kwargs):
950
+ self.encoder = encoder
951
+ self.decoder = decoder
952
+ super().__init__(**kwargs)
953
+
954
+ def to_python(self, value):
955
+ if self.disabled:
956
+ return value
957
+ if value in self.empty_values:
958
+ return None
959
+ elif isinstance(value, list | dict | int | float | JSONString):
960
+ return value
961
+ try:
962
+ converted = json.loads(value, cls=self.decoder)
963
+ except json.JSONDecodeError:
964
+ raise ValidationError(
965
+ self.error_messages["invalid"],
966
+ code="invalid",
967
+ params={"value": value},
968
+ )
969
+ if isinstance(converted, str):
970
+ return JSONString(converted)
971
+ else:
972
+ return converted
973
+
974
+ def bound_data(self, data, initial):
975
+ if self.disabled:
976
+ return initial
977
+ if data is None:
978
+ return None
979
+ try:
980
+ return json.loads(data, cls=self.decoder)
981
+ except json.JSONDecodeError:
982
+ return InvalidJSONInput(data)
983
+
984
+ def prepare_value(self, value):
985
+ if isinstance(value, InvalidJSONInput):
986
+ return value
987
+ return json.dumps(value, ensure_ascii=False, cls=self.encoder)
988
+
989
+ def has_changed(self, initial, data):
990
+ if super().has_changed(initial, data):
991
+ return True
992
+ # For purposes of seeing whether something has changed, True isn't the
993
+ # same as 1 and the order of keys doesn't matter.
994
+ return json.dumps(initial, sort_keys=True, cls=self.encoder) != json.dumps(
995
+ self.to_python(data), sort_keys=True, cls=self.encoder
996
+ )
997
+
998
+
999
+ def from_current_timezone(value):
1000
+ """
1001
+ When time zone support is enabled, convert naive datetimes
1002
+ entered in the current time zone to aware datetimes.
1003
+ """
1004
+ if settings.USE_TZ and value is not None and timezone.is_naive(value):
1005
+ current_timezone = timezone.get_current_timezone()
1006
+ try:
1007
+ if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
1008
+ raise ValueError("Ambiguous or non-existent time.")
1009
+ return timezone.make_aware(value, current_timezone)
1010
+ except Exception as exc:
1011
+ raise ValidationError(
1012
+ (
1013
+ "%(datetime)s couldn’t be interpreted "
1014
+ "in time zone %(current_timezone)s; it "
1015
+ "may be ambiguous or it may not exist."
1016
+ ),
1017
+ code="ambiguous_timezone",
1018
+ params={"datetime": value, "current_timezone": current_timezone},
1019
+ ) from exc
1020
+ return value
1021
+
1022
+
1023
+ def to_current_timezone(value):
1024
+ """
1025
+ When time zone support is enabled, convert aware datetimes
1026
+ to naive datetimes in the current time zone for display.
1027
+ """
1028
+ if settings.USE_TZ and value is not None and timezone.is_aware(value):
1029
+ return timezone.make_naive(value)
1030
+ return value