plain 0.66.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.
Files changed (197) hide show
  1. plain/CHANGELOG.md +684 -0
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -53
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +112 -28
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -13
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +14 -27
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +56 -40
  145. plain/urls/resolvers.py +38 -28
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/utils/tree.py CHANGED
@@ -3,7 +3,13 @@ A class for storing a tree graph. Primarily used for filter constructs in the
3
3
  ORM.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  import copy
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Self
7
13
 
8
14
  from plain.utils.hashable import make_hashable
9
15
 
@@ -17,16 +23,26 @@ class Node:
17
23
 
18
24
  # Standard connector type. Clients usually won't use this at all and
19
25
  # subclasses will usually override the value.
20
- default = "DEFAULT"
21
-
22
- def __init__(self, children=None, connector=None, negated=False):
26
+ default: str = "DEFAULT"
27
+
28
+ def __init__(
29
+ self,
30
+ children: list[Any] | None = None,
31
+ connector: str | None = None,
32
+ negated: bool = False,
33
+ ) -> None:
23
34
  """Construct a new Node. If no connector is given, use the default."""
24
- self.children = children[:] if children else []
25
- self.connector = connector or self.default
26
- self.negated = negated
35
+ self.children: list[Any] = children[:] if children else []
36
+ self.connector: str = connector or self.default
37
+ self.negated: bool = negated
27
38
 
28
39
  @classmethod
29
- def create(cls, children=None, connector=None, negated=False):
40
+ def create(
41
+ cls,
42
+ children: list[Any] | None = None,
43
+ connector: str | None = None,
44
+ negated: bool = False,
45
+ ) -> Self:
30
46
  """
31
47
  Create a new instance using Node() instead of __init__() as some
32
48
  subclasses, e.g. plain.models.query_utils.Q, may implement a custom
@@ -35,40 +51,40 @@ class Node:
35
51
  """
36
52
  obj = Node(children, connector or cls.default, negated)
37
53
  obj.__class__ = cls
38
- return obj
54
+ return obj # type: ignore[return-value]
39
55
 
40
- def __str__(self):
56
+ def __str__(self) -> str:
41
57
  template = "(NOT (%s: %s))" if self.negated else "(%s: %s)"
42
58
  return template % (self.connector, ", ".join(str(c) for c in self.children))
43
59
 
44
- def __repr__(self):
60
+ def __repr__(self) -> str:
45
61
  return f"<{self.__class__.__name__}: {self}>"
46
62
 
47
- def __copy__(self):
63
+ def __copy__(self) -> Self:
48
64
  obj = self.create(connector=self.connector, negated=self.negated)
49
65
  obj.children = self.children # Don't [:] as .__init__() via .create() does.
50
66
  return obj
51
67
 
52
68
  copy = __copy__
53
69
 
54
- def __deepcopy__(self, memodict):
70
+ def __deepcopy__(self, memodict: dict[int, Any]) -> Self:
55
71
  obj = self.create(connector=self.connector, negated=self.negated)
56
72
  obj.children = copy.deepcopy(self.children, memodict)
57
73
  return obj
58
74
 
59
- def __len__(self):
75
+ def __len__(self) -> int:
60
76
  """Return the number of children this node has."""
61
77
  return len(self.children)
62
78
 
63
- def __bool__(self):
79
+ def __bool__(self) -> bool:
64
80
  """Return whether or not this node has children."""
65
81
  return bool(self.children)
66
82
 
67
- def __contains__(self, other):
83
+ def __contains__(self, other: Any) -> bool:
68
84
  """Return True if 'other' is a direct child of this instance."""
69
85
  return other in self.children
70
86
 
71
- def __eq__(self, other):
87
+ def __eq__(self, other: Any) -> bool:
72
88
  return (
73
89
  self.__class__ == other.__class__
74
90
  and self.connector == other.connector
@@ -76,7 +92,7 @@ class Node:
76
92
  and self.children == other.children
77
93
  )
78
94
 
79
- def __hash__(self):
95
+ def __hash__(self) -> int:
80
96
  return hash(
81
97
  (
82
98
  self.__class__,
@@ -86,7 +102,7 @@ class Node:
86
102
  )
87
103
  )
88
104
 
89
- def add(self, data, conn_type):
105
+ def add(self, data: Any, conn_type: str) -> Any:
90
106
  """
91
107
  Combine this tree and the data represented by data using the
92
108
  connector conn_type. The combine is done by squashing the node other
@@ -121,6 +137,6 @@ class Node:
121
137
  self.children.append(data)
122
138
  return data
123
139
 
124
- def negate(self):
140
+ def negate(self) -> None:
125
141
  """Negate the sense of the root connector."""
126
142
  self.negated = not self.negated
plain/validators.py CHANGED
@@ -1,7 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  import ipaddress
2
4
  import math
3
5
  import re
6
+ from collections.abc import Callable
4
7
  from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, cast
5
9
  from urllib.parse import urlsplit, urlunsplit
6
10
 
7
11
  from plain.exceptions import ValidationError
@@ -11,23 +15,40 @@ from plain.utils.ipv6 import is_valid_ipv6_address
11
15
  from plain.utils.regex_helper import _lazy_re_compile
12
16
  from plain.utils.text import pluralize_lazy
13
17
 
18
+ if TYPE_CHECKING:
19
+ from plain.utils.functional import SimpleLazyObject
20
+
14
21
  # These values, if given to validate(), will trigger the self.required check.
15
22
  EMPTY_VALUES = (None, "", [], (), {})
16
23
 
17
24
 
18
25
  @deconstructible
19
26
  class RegexValidator:
20
- regex = ""
27
+ regex: str | re.Pattern[str] | SimpleLazyObject = ""
21
28
  message = "Enter a valid value."
22
29
  code = "invalid"
23
30
  inverse_match = False
24
31
  flags = 0
25
32
 
26
33
  def __init__(
27
- self, regex=None, message=None, code=None, inverse_match=None, flags=None
28
- ):
34
+ self,
35
+ regex: str | re.Pattern[str] | None = None,
36
+ message: str | None = None,
37
+ code: str | None = None,
38
+ inverse_match: bool | None = None,
39
+ flags: int | None = None,
40
+ ) -> None:
41
+ # Only compile regex if explicitly provided or if class default needs compilation
29
42
  if regex is not None:
30
- self.regex = regex
43
+ regex_to_compile: str | re.Pattern[str] = regex
44
+ elif isinstance(self.regex, str | re.Pattern):
45
+ # Class-level regex is a string or pattern that needs compilation
46
+ regex_to_compile = self.regex
47
+ else:
48
+ # Class-level regex is already compiled (e.g., in URL Validator subclass)
49
+ # Don't recompile it
50
+ regex_to_compile = None
51
+
31
52
  if message is not None:
32
53
  self.message = message
33
54
  if code is not None:
@@ -36,28 +57,31 @@ class RegexValidator:
36
57
  self.inverse_match = inverse_match
37
58
  if flags is not None:
38
59
  self.flags = flags
39
- if self.flags and not isinstance(self.regex, str):
40
- raise TypeError(
41
- "If the flags are set, regex must be a regular expression string."
42
- )
43
60
 
44
- self.regex = _lazy_re_compile(self.regex, self.flags)
61
+ # Only compile if we have a regex to compile
62
+ if regex_to_compile is not None:
63
+ if self.flags and not isinstance(regex_to_compile, str):
64
+ raise TypeError(
65
+ "If the flags are set, regex must be a regular expression string."
66
+ )
67
+ self.regex = _lazy_re_compile(regex_to_compile, self.flags)
45
68
 
46
- def __call__(self, value):
69
+ def __call__(self, value: Any) -> None:
47
70
  """
48
71
  Validate that the input contains (or does *not* contain, if
49
72
  inverse_match is True) a match for the regular expression.
50
73
  """
51
- regex_matches = self.regex.search(str(value))
74
+ # self.regex is always a SimpleLazyObject with search() after __init__
75
+ regex_matches = cast(re.Pattern[str], self.regex).search(str(value))
52
76
  invalid_input = regex_matches if self.inverse_match else not regex_matches
53
77
  if invalid_input:
54
78
  raise ValidationError(self.message, code=self.code, params={"value": value})
55
79
 
56
- def __eq__(self, other):
80
+ def __eq__(self, other: object) -> bool:
57
81
  return (
58
82
  isinstance(other, RegexValidator)
59
- and self.regex.pattern == other.regex.pattern
60
- and self.regex.flags == other.regex.flags
83
+ and self.regex.pattern == other.regex.pattern # type: ignore[union-attr]
84
+ and self.regex.flags == other.regex.flags # type: ignore[union-attr]
61
85
  and (self.message == other.message)
62
86
  and (self.code == other.code)
63
87
  and (self.inverse_match == other.inverse_match)
@@ -104,12 +128,12 @@ class URLValidator(RegexValidator):
104
128
  schemes = ["http", "https", "ftp", "ftps"]
105
129
  unsafe_chars = frozenset("\t\r\n")
106
130
 
107
- def __init__(self, schemes=None, **kwargs):
131
+ def __init__(self, schemes: list[str] | None = None, **kwargs: Any) -> None:
108
132
  super().__init__(**kwargs)
109
133
  if schemes is not None:
110
134
  self.schemes = schemes
111
135
 
112
- def __call__(self, value):
136
+ def __call__(self, value: Any) -> None:
113
137
  if not isinstance(value, str):
114
138
  raise ValidationError(self.message, code=self.code, params={"value": value})
115
139
  if self.unsafe_chars.intersection(value):
@@ -182,7 +206,12 @@ class EmailValidator:
182
206
  )
183
207
  domain_allowlist = ["localhost"]
184
208
 
185
- def __init__(self, message=None, code=None, allowlist=None):
209
+ def __init__(
210
+ self,
211
+ message: str | None = None,
212
+ code: str | None = None,
213
+ allowlist: list[str] | None = None,
214
+ ) -> None:
186
215
  if message is not None:
187
216
  self.message = message
188
217
  if code is not None:
@@ -190,7 +219,7 @@ class EmailValidator:
190
219
  if allowlist is not None:
191
220
  self.domain_allowlist = allowlist
192
221
 
193
- def __call__(self, value):
222
+ def __call__(self, value: Any) -> None:
194
223
  if not value or "@" not in value:
195
224
  raise ValidationError(self.message, code=self.code, params={"value": value})
196
225
 
@@ -209,10 +238,11 @@ class EmailValidator:
209
238
  pass
210
239
  else:
211
240
  if self.validate_domain_part(domain_part):
212
- return
241
+ return None
213
242
  raise ValidationError(self.message, code=self.code, params={"value": value})
243
+ return None
214
244
 
215
- def validate_domain_part(self, domain_part):
245
+ def validate_domain_part(self, domain_part: str) -> bool:
216
246
  if self.domain_regex.match(domain_part):
217
247
  return True
218
248
 
@@ -226,7 +256,7 @@ class EmailValidator:
226
256
  pass
227
257
  return False
228
258
 
229
- def __eq__(self, other):
259
+ def __eq__(self, other: object) -> bool:
230
260
  return (
231
261
  isinstance(other, EmailValidator)
232
262
  and (self.domain_allowlist == other.domain_allowlist)
@@ -238,7 +268,7 @@ class EmailValidator:
238
268
  validate_email = EmailValidator()
239
269
 
240
270
 
241
- def validate_ipv4_address(value):
271
+ def validate_ipv4_address(value: str) -> None:
242
272
  try:
243
273
  ipaddress.IPv4Address(value)
244
274
  except ValueError:
@@ -247,14 +277,14 @@ def validate_ipv4_address(value):
247
277
  )
248
278
 
249
279
 
250
- def validate_ipv6_address(value):
280
+ def validate_ipv6_address(value: str) -> None:
251
281
  if not is_valid_ipv6_address(value):
252
282
  raise ValidationError(
253
283
  "Enter a valid IPv6 address.", code="invalid", params={"value": value}
254
284
  )
255
285
 
256
286
 
257
- def validate_ipv46_address(value):
287
+ def validate_ipv46_address(value: str) -> None:
258
288
  try:
259
289
  validate_ipv4_address(value)
260
290
  except ValidationError:
@@ -275,7 +305,9 @@ ip_address_validator_map = {
275
305
  }
276
306
 
277
307
 
278
- def ip_address_validators(protocol, unpack_ipv4):
308
+ def ip_address_validators(
309
+ protocol: str, unpack_ipv4: bool
310
+ ) -> tuple[list[Callable[[str], None]], str]:
279
311
  """
280
312
  Depending on the given parameters, return the appropriate validators for
281
313
  the GenericIPAddressField.
@@ -292,14 +324,19 @@ def ip_address_validators(protocol, unpack_ipv4):
292
324
  )
293
325
 
294
326
 
295
- def int_list_validator(sep=",", message=None, code="invalid", allow_negative=False):
327
+ def int_list_validator(
328
+ sep: str = ",",
329
+ message: str | None = None,
330
+ code: str = "invalid",
331
+ allow_negative: bool = False,
332
+ ) -> RegexValidator:
296
333
  regexp = _lazy_re_compile(
297
334
  r"^{neg}\d+(?:{sep}{neg}\d+)*\Z".format(
298
335
  neg="(-)?" if allow_negative else "",
299
336
  sep=re.escape(sep),
300
337
  )
301
338
  )
302
- return RegexValidator(regexp, message=message, code=code)
339
+ return RegexValidator(regexp, message=message, code=code) # type: ignore[arg-type]
303
340
 
304
341
 
305
342
  validate_comma_separated_integer_list = int_list_validator(
@@ -312,12 +349,12 @@ class BaseValidator:
312
349
  message = "Ensure this value is %(limit_value)s (it is %(show_value)s)."
313
350
  code = "limit_value"
314
351
 
315
- def __init__(self, limit_value, message=None):
352
+ def __init__(self, limit_value: Any, message: str | None = None) -> None:
316
353
  self.limit_value = limit_value
317
354
  if message:
318
355
  self.message = message
319
356
 
320
- def __call__(self, value):
357
+ def __call__(self, value: Any) -> None:
321
358
  cleaned = self.clean(value)
322
359
  limit_value = (
323
360
  self.limit_value() if callable(self.limit_value) else self.limit_value
@@ -326,7 +363,7 @@ class BaseValidator:
326
363
  if self.compare(cleaned, limit_value):
327
364
  raise ValidationError(self.message, code=self.code, params=params)
328
365
 
329
- def __eq__(self, other):
366
+ def __eq__(self, other: object) -> bool:
330
367
  if not isinstance(other, self.__class__):
331
368
  return NotImplemented
332
369
  return (
@@ -335,10 +372,10 @@ class BaseValidator:
335
372
  and self.code == other.code
336
373
  )
337
374
 
338
- def compare(self, a, b):
375
+ def compare(self, a: Any, b: Any) -> bool:
339
376
  return a is not b
340
377
 
341
- def clean(self, x):
378
+ def clean(self, x: Any) -> Any:
342
379
  return x
343
380
 
344
381
 
@@ -347,7 +384,7 @@ class MaxValueValidator(BaseValidator):
347
384
  message = "Ensure this value is less than or equal to %(limit_value)s."
348
385
  code = "max_value"
349
386
 
350
- def compare(self, a, b):
387
+ def compare(self, a: Any, b: Any) -> bool:
351
388
  return a > b
352
389
 
353
390
 
@@ -356,7 +393,7 @@ class MinValueValidator(BaseValidator):
356
393
  message = "Ensure this value is greater than or equal to %(limit_value)s."
357
394
  code = "min_value"
358
395
 
359
- def compare(self, a, b):
396
+ def compare(self, a: Any, b: Any) -> bool:
360
397
  return a < b
361
398
 
362
399
 
@@ -365,7 +402,7 @@ class StepValueValidator(BaseValidator):
365
402
  message = "Ensure this value is a multiple of step size %(limit_value)s."
366
403
  code = "step_size"
367
404
 
368
- def compare(self, a, b):
405
+ def compare(self, a: Any, b: Any) -> bool:
369
406
  return not math.isclose(math.remainder(a, b), 0, abs_tol=1e-9)
370
407
 
371
408
 
@@ -380,10 +417,10 @@ class MinLengthValidator(BaseValidator):
380
417
  )
381
418
  code = "min_length"
382
419
 
383
- def compare(self, a, b):
420
+ def compare(self, a: Any, b: Any) -> bool:
384
421
  return a < b
385
422
 
386
- def clean(self, x):
423
+ def clean(self, x: Any) -> int:
387
424
  return len(x)
388
425
 
389
426
 
@@ -398,10 +435,10 @@ class MaxLengthValidator(BaseValidator):
398
435
  )
399
436
  code = "max_length"
400
437
 
401
- def compare(self, a, b):
438
+ def compare(self, a: Any, b: Any) -> bool:
402
439
  return a > b
403
440
 
404
- def clean(self, x):
441
+ def clean(self, x: Any) -> int:
405
442
  return len(x)
406
443
 
407
444
 
@@ -433,11 +470,11 @@ class DecimalValidator:
433
470
  ),
434
471
  }
435
472
 
436
- def __init__(self, max_digits, decimal_places):
473
+ def __init__(self, max_digits: int | None, decimal_places: int | None) -> None:
437
474
  self.max_digits = max_digits
438
475
  self.decimal_places = decimal_places
439
476
 
440
- def __call__(self, value):
477
+ def __call__(self, value: Any) -> None:
441
478
  digit_tuple, exponent = value.as_tuple()[1:]
442
479
  if exponent in {"F", "n", "N"}:
443
480
  raise ValidationError(
@@ -485,7 +522,7 @@ class DecimalValidator:
485
522
  params={"max": (self.max_digits - self.decimal_places), "value": value},
486
523
  )
487
524
 
488
- def __eq__(self, other):
525
+ def __eq__(self, other: object) -> bool:
489
526
  return (
490
527
  isinstance(other, self.__class__)
491
528
  and self.max_digits == other.max_digits
@@ -495,10 +532,15 @@ class DecimalValidator:
495
532
 
496
533
  @deconstructible
497
534
  class FileExtensionValidator:
498
- message = "File extension “%(extension)s is not allowed. Allowed extensions are: %(allowed_extensions)s."
535
+ message = 'File extension "%(extension)s" is not allowed. Allowed extensions are: %(allowed_extensions)s.'
499
536
  code = "invalid_extension"
500
537
 
501
- def __init__(self, allowed_extensions=None, message=None, code=None):
538
+ def __init__(
539
+ self,
540
+ allowed_extensions: list[str] | None = None,
541
+ message: str | None = None,
542
+ code: str | None = None,
543
+ ) -> None:
502
544
  if allowed_extensions is not None:
503
545
  allowed_extensions = [
504
546
  allowed_extension.lower() for allowed_extension in allowed_extensions
@@ -509,7 +551,7 @@ class FileExtensionValidator:
509
551
  if code is not None:
510
552
  self.code = code
511
553
 
512
- def __call__(self, value):
554
+ def __call__(self, value: Any) -> None:
513
555
  extension = Path(value.name).suffix[1:].lower()
514
556
  if (
515
557
  self.allowed_extensions is not None
@@ -525,7 +567,7 @@ class FileExtensionValidator:
525
567
  },
526
568
  )
527
569
 
528
- def __eq__(self, other):
570
+ def __eq__(self, other: object) -> bool:
529
571
  return (
530
572
  isinstance(other, self.__class__)
531
573
  and self.allowed_extensions == other.allowed_extensions
@@ -534,9 +576,9 @@ class FileExtensionValidator:
534
576
  )
535
577
 
536
578
 
537
- def get_available_image_extensions():
579
+ def get_available_image_extensions() -> list[str]:
538
580
  try:
539
- from PIL import Image
581
+ from PIL import Image # type: ignore[import-not-found]
540
582
  except ImportError:
541
583
  return []
542
584
  else:
@@ -544,7 +586,7 @@ def get_available_image_extensions():
544
586
  return [ext.lower()[1:] for ext in Image.EXTENSION]
545
587
 
546
588
 
547
- def validate_image_file_extension(value):
589
+ def validate_image_file_extension(value: Any) -> None:
548
590
  return FileExtensionValidator(allowed_extensions=get_available_image_extensions())(
549
591
  value
550
592
  )
@@ -557,17 +599,17 @@ class ProhibitNullCharactersValidator:
557
599
  message = "Null characters are not allowed."
558
600
  code = "null_characters_not_allowed"
559
601
 
560
- def __init__(self, message=None, code=None):
602
+ def __init__(self, message: str | None = None, code: str | None = None) -> None:
561
603
  if message is not None:
562
604
  self.message = message
563
605
  if code is not None:
564
606
  self.code = code
565
607
 
566
- def __call__(self, value):
608
+ def __call__(self, value: Any) -> None:
567
609
  if "\x00" in str(value):
568
610
  raise ValidationError(self.message, code=self.code, params={"value": value})
569
611
 
570
- def __eq__(self, other):
612
+ def __eq__(self, other: object) -> bool:
571
613
  return (
572
614
  isinstance(other, self.__class__)
573
615
  and self.message == other.message