plain 0.69.0__py3-none-any.whl → 0.70.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +11 -0
  3. plain/assets/compile.py +20 -7
  4. plain/assets/finders.py +15 -11
  5. plain/assets/fingerprints.py +6 -5
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +23 -17
  8. plain/chores/registry.py +14 -9
  9. plain/cli/agent/__init__.py +1 -1
  10. plain/cli/agent/docs.py +7 -6
  11. plain/cli/agent/llmdocs.py +18 -8
  12. plain/cli/agent/md.py +19 -14
  13. plain/cli/agent/prompt.py +1 -1
  14. plain/cli/agent/request.py +37 -17
  15. plain/cli/build.py +2 -2
  16. plain/cli/changelog.py +8 -4
  17. plain/cli/chores.py +4 -4
  18. plain/cli/core.py +8 -5
  19. plain/cli/docs.py +2 -2
  20. plain/cli/formatting.py +10 -7
  21. plain/cli/output.py +6 -2
  22. plain/cli/preflight.py +3 -3
  23. plain/cli/print.py +1 -1
  24. plain/cli/registry.py +10 -6
  25. plain/cli/scaffold.py +1 -1
  26. plain/cli/settings.py +1 -1
  27. plain/cli/shell.py +10 -7
  28. plain/cli/startup.py +3 -3
  29. plain/cli/urls.py +10 -4
  30. plain/cli/utils.py +2 -2
  31. plain/csrf/middleware.py +15 -5
  32. plain/csrf/views.py +11 -8
  33. plain/debug.py +5 -2
  34. plain/exceptions.py +19 -8
  35. plain/forms/__init__.py +1 -1
  36. plain/forms/boundfield.py +14 -7
  37. plain/forms/exceptions.py +1 -1
  38. plain/forms/fields.py +139 -97
  39. plain/forms/forms.py +55 -39
  40. plain/http/cookie.py +15 -7
  41. plain/http/multipartparser.py +50 -30
  42. plain/http/request.py +97 -73
  43. plain/http/response.py +99 -80
  44. plain/internal/__init__.py +8 -1
  45. plain/internal/files/base.py +34 -18
  46. plain/internal/files/locks.py +19 -11
  47. plain/internal/files/move.py +8 -3
  48. plain/internal/files/temp.py +23 -5
  49. plain/internal/files/uploadedfile.py +42 -26
  50. plain/internal/files/uploadhandler.py +48 -27
  51. plain/internal/files/utils.py +13 -6
  52. plain/internal/handlers/base.py +20 -6
  53. plain/internal/handlers/exception.py +19 -5
  54. plain/internal/handlers/wsgi.py +30 -18
  55. plain/internal/middleware/headers.py +11 -2
  56. plain/internal/middleware/hosts.py +10 -2
  57. plain/internal/middleware/https.py +13 -3
  58. plain/internal/middleware/slash.py +15 -5
  59. plain/json.py +2 -1
  60. plain/logs/configure.py +3 -1
  61. plain/logs/debug.py +16 -5
  62. plain/logs/formatters.py +6 -3
  63. plain/logs/loggers.py +56 -52
  64. plain/logs/utils.py +19 -9
  65. plain/packages/config.py +14 -6
  66. plain/packages/registry.py +27 -12
  67. plain/paginator.py +31 -21
  68. plain/preflight/checks.py +3 -1
  69. plain/preflight/files.py +3 -1
  70. plain/preflight/registry.py +25 -10
  71. plain/preflight/results.py +10 -4
  72. plain/preflight/security.py +7 -5
  73. plain/preflight/urls.py +4 -1
  74. plain/runtime/__init__.py +4 -3
  75. plain/runtime/global_settings.py +1 -1
  76. plain/runtime/user_settings.py +26 -17
  77. plain/runtime/utils.py +1 -1
  78. plain/signals/dispatch/dispatcher.py +39 -17
  79. plain/signing.py +49 -30
  80. plain/templates/jinja/__init__.py +13 -5
  81. plain/templates/jinja/environments.py +4 -3
  82. plain/templates/jinja/extensions.py +9 -3
  83. plain/templates/jinja/filters.py +7 -2
  84. plain/templates/jinja/globals.py +1 -1
  85. plain/test/client.py +246 -174
  86. plain/test/encoding.py +9 -6
  87. plain/test/exceptions.py +10 -2
  88. plain/urls/converters.py +13 -10
  89. plain/urls/patterns.py +32 -20
  90. plain/urls/resolvers.py +32 -22
  91. plain/urls/utils.py +5 -1
  92. plain/utils/cache.py +14 -8
  93. plain/utils/crypto.py +21 -5
  94. plain/utils/datastructures.py +84 -54
  95. plain/utils/dateparse.py +10 -7
  96. plain/utils/deconstruct.py +12 -4
  97. plain/utils/decorators.py +5 -1
  98. plain/utils/duration.py +8 -4
  99. plain/utils/encoding.py +14 -7
  100. plain/utils/functional.py +62 -47
  101. plain/utils/hashable.py +5 -1
  102. plain/utils/html.py +21 -14
  103. plain/utils/http.py +16 -9
  104. plain/utils/inspect.py +14 -6
  105. plain/utils/ipv6.py +7 -3
  106. plain/utils/itercompat.py +6 -1
  107. plain/utils/module_loading.py +7 -3
  108. plain/utils/regex_helper.py +23 -13
  109. plain/utils/safestring.py +14 -6
  110. plain/utils/text.py +34 -18
  111. plain/utils/timezone.py +30 -19
  112. plain/utils/tree.py +31 -18
  113. plain/validators.py +71 -44
  114. plain/views/base.py +16 -6
  115. plain/views/errors.py +11 -4
  116. plain/views/exceptions.py +4 -1
  117. plain/views/objects.py +15 -15
  118. plain/views/redirect.py +14 -10
  119. plain/views/templates.py +1 -1
  120. plain/wsgi.py +3 -1
  121. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
  122. plain-0.70.0.dist-info/RECORD +169 -0
  123. plain-0.69.0.dist-info/RECORD +0 -169
  124. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
  125. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
  126. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
plain/debug.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from pprint import pformat
4
+ from typing import Any, NoReturn
2
5
 
3
6
  from markupsafe import Markup, escape
4
7
 
@@ -6,7 +9,7 @@ from plain.http import Response
6
9
  from plain.views.exceptions import ResponseException
7
10
 
8
11
 
9
- def dd(*objs):
12
+ def dd(*objs: Any) -> NoReturn:
10
13
  """
11
14
  Dump and die.
12
15
 
@@ -25,5 +28,5 @@ def dd(*objs):
25
28
  response = Response()
26
29
  response.status_code = 500
27
30
  response.content = combined_dump_str
28
- response.content_type = "text/html"
31
+ response.headers["Content-Type"] = "text/html"
29
32
  raise ResponseException(response)
plain/exceptions.py CHANGED
@@ -2,7 +2,11 @@
2
2
  Global Plain exception and warning classes.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import operator
8
+ from collections.abc import Iterator
9
+ from typing import Any
6
10
 
7
11
  from plain.utils.hashable import make_hashable
8
12
 
@@ -90,7 +94,12 @@ NON_FIELD_ERRORS = "__all__"
90
94
  class ValidationError(Exception):
91
95
  """An error while validating data."""
92
96
 
93
- def __init__(self, message, code=None, params=None):
97
+ def __init__(
98
+ self,
99
+ message: str | list[Any] | dict[str, Any] | ValidationError,
100
+ code: str | None = None,
101
+ params: dict[str, Any] | None = None,
102
+ ):
94
103
  """
95
104
  The `message` argument can be a single error, a list of errors, or a
96
105
  dictionary that maps field names to lists of errors. What we define as
@@ -134,12 +143,14 @@ class ValidationError(Exception):
134
143
  self.error_list = [self]
135
144
 
136
145
  @property
137
- def messages(self):
146
+ def messages(self) -> list[str]:
138
147
  if hasattr(self, "error_dict"):
139
148
  return sum(dict(self).values(), [])
140
149
  return list(self)
141
150
 
142
- def update_error_dict(self, error_dict):
151
+ def update_error_dict(
152
+ self, error_dict: dict[str, list[ValidationError]]
153
+ ) -> dict[str, list[ValidationError]]:
143
154
  if hasattr(self, "error_dict"):
144
155
  for field, error_list in self.error_dict.items():
145
156
  error_dict.setdefault(field, []).extend(error_list)
@@ -147,7 +158,7 @@ class ValidationError(Exception):
147
158
  error_dict.setdefault(NON_FIELD_ERRORS, []).extend(self.error_list)
148
159
  return error_dict
149
160
 
150
- def __iter__(self):
161
+ def __iter__(self) -> Iterator[tuple[str, list[str]] | str]:
151
162
  if hasattr(self, "error_dict"):
152
163
  for field, errors in self.error_dict.items():
153
164
  yield field, list(ValidationError(errors))
@@ -158,20 +169,20 @@ class ValidationError(Exception):
158
169
  message %= error.params
159
170
  yield str(message)
160
171
 
161
- def __str__(self):
172
+ def __str__(self) -> str:
162
173
  if hasattr(self, "error_dict"):
163
174
  return repr(dict(self))
164
175
  return repr(list(self))
165
176
 
166
- def __repr__(self):
177
+ def __repr__(self) -> str:
167
178
  return f"ValidationError({self})"
168
179
 
169
- def __eq__(self, other):
180
+ def __eq__(self, other: object) -> bool:
170
181
  if not isinstance(other, ValidationError):
171
182
  return NotImplemented
172
183
  return hash(self) == hash(other)
173
184
 
174
- def __hash__(self):
185
+ def __hash__(self) -> int:
175
186
  if hasattr(self, "message"):
176
187
  return hash(
177
188
  (
plain/forms/__init__.py CHANGED
@@ -5,4 +5,4 @@ Plain validation and HTML form handling.
5
5
  from .boundfield import BoundField # NOQA
6
6
  from .exceptions import FormFieldMissingError, ValidationError # NOQA
7
7
  from .fields import * # NOQA
8
- from .forms import Form # NOQA
8
+ from .forms import BaseForm, Form # NOQA
plain/forms/boundfield.py CHANGED
@@ -1,4 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import cached_property
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from .fields import Field
8
+ from .forms import BaseForm
2
9
 
3
10
  __all__ = ("BoundField",)
4
11
 
@@ -6,24 +13,24 @@ __all__ = ("BoundField",)
6
13
  class BoundField:
7
14
  "A Field plus data"
8
15
 
9
- def __init__(self, form, field, name):
16
+ def __init__(self, form: BaseForm, field: Field, name: str):
10
17
  self._form = form
11
18
  self.field = field
12
19
  self.name = name
13
20
  self.html_name = form.add_prefix(name)
14
21
  self.html_id = form.add_prefix(self._auto_id)
15
22
 
16
- def __repr__(self):
23
+ def __repr__(self) -> str:
17
24
  return f'<{self.__class__.__name__} "{self.html_name}">'
18
25
 
19
26
  @property
20
- def errors(self):
27
+ def errors(self) -> list[str]:
21
28
  """
22
29
  Return an error list (empty if there are no errors) for this field.
23
30
  """
24
31
  return self._form.errors.get(self.name, [])
25
32
 
26
- def value(self):
33
+ def value(self) -> Any:
27
34
  """
28
35
  Return the value for this BoundField, using the initial value if
29
36
  the form is not bound or the data otherwise.
@@ -36,16 +43,16 @@ class BoundField:
36
43
  return self.field.prepare_value(data)
37
44
 
38
45
  @cached_property
39
- def initial(self):
46
+ def initial(self) -> Any:
40
47
  return self._form.get_initial_for_field(self.field, self.name)
41
48
 
42
- def _has_changed(self):
49
+ def _has_changed(self) -> bool:
43
50
  return self.field.has_changed(
44
51
  self.initial, self._form._field_data_value(self.field, self.html_name)
45
52
  )
46
53
 
47
54
  @property
48
- def _auto_id(self):
55
+ def _auto_id(self) -> str:
49
56
  """
50
57
  Calculate and return the ID attribute for this BoundField, if the
51
58
  associated Form has specified auto_id. Return an empty string otherwise.
plain/forms/exceptions.py CHANGED
@@ -2,7 +2,7 @@ from plain.exceptions import ValidationError
2
2
 
3
3
 
4
4
  class FormFieldMissingError(Exception):
5
- def __init__(self, field_name):
5
+ def __init__(self, field_name: str):
6
6
  self.field_name = field_name
7
7
  self.message = f'The "{self.field_name}" field is missing from the form data.'
8
8