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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +23 -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 +20 -51
  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 +27 -17
  118. plain/views/redirect.py +14 -10
  119. plain/views/templates.py +1 -1
  120. plain/wsgi.py +3 -1
  121. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
  122. plain-0.70.0.dist-info/RECORD +169 -0
  123. plain-0.68.1.dist-info/RECORD +0 -169
  124. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
  125. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
  126. {plain-0.68.1.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,11 +2,15 @@
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
 
9
- # MARK: Configuration and Registry
13
+ # MARK: Configuration and Package Registry
10
14
 
11
15
 
12
16
  class PackageRegistryNotReady(Exception):
@@ -21,33 +25,6 @@ class ImproperlyConfigured(Exception):
21
25
  pass
22
26
 
23
27
 
24
- # MARK: Model and Field Errors
25
-
26
-
27
- class FieldDoesNotExist(Exception):
28
- """The requested model field does not exist"""
29
-
30
- pass
31
-
32
-
33
- class FieldError(Exception):
34
- """Some kind of problem with a model field."""
35
-
36
- pass
37
-
38
-
39
- class ObjectDoesNotExist(Exception):
40
- """The requested object does not exist"""
41
-
42
- pass
43
-
44
-
45
- class MultipleObjectsReturned(Exception):
46
- """The query returned multiple objects when only one was expected."""
47
-
48
- pass
49
-
50
-
51
28
  # MARK: Security and Suspicious Operations
52
29
 
53
30
 
@@ -117,7 +94,12 @@ NON_FIELD_ERRORS = "__all__"
117
94
  class ValidationError(Exception):
118
95
  """An error while validating data."""
119
96
 
120
- 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
+ ):
121
103
  """
122
104
  The `message` argument can be a single error, a list of errors, or a
123
105
  dictionary that maps field names to lists of errors. What we define as
@@ -161,12 +143,14 @@ class ValidationError(Exception):
161
143
  self.error_list = [self]
162
144
 
163
145
  @property
164
- def messages(self):
146
+ def messages(self) -> list[str]:
165
147
  if hasattr(self, "error_dict"):
166
148
  return sum(dict(self).values(), [])
167
149
  return list(self)
168
150
 
169
- 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]]:
170
154
  if hasattr(self, "error_dict"):
171
155
  for field, error_list in self.error_dict.items():
172
156
  error_dict.setdefault(field, []).extend(error_list)
@@ -174,7 +158,7 @@ class ValidationError(Exception):
174
158
  error_dict.setdefault(NON_FIELD_ERRORS, []).extend(self.error_list)
175
159
  return error_dict
176
160
 
177
- def __iter__(self):
161
+ def __iter__(self) -> Iterator[tuple[str, list[str]] | str]:
178
162
  if hasattr(self, "error_dict"):
179
163
  for field, errors in self.error_dict.items():
180
164
  yield field, list(ValidationError(errors))
@@ -185,20 +169,20 @@ class ValidationError(Exception):
185
169
  message %= error.params
186
170
  yield str(message)
187
171
 
188
- def __str__(self):
172
+ def __str__(self) -> str:
189
173
  if hasattr(self, "error_dict"):
190
174
  return repr(dict(self))
191
175
  return repr(list(self))
192
176
 
193
- def __repr__(self):
177
+ def __repr__(self) -> str:
194
178
  return f"ValidationError({self})"
195
179
 
196
- def __eq__(self, other):
180
+ def __eq__(self, other: object) -> bool:
197
181
  if not isinstance(other, ValidationError):
198
182
  return NotImplemented
199
183
  return hash(self) == hash(other)
200
184
 
201
- def __hash__(self):
185
+ def __hash__(self) -> int:
202
186
  if hasattr(self, "message"):
203
187
  return hash(
204
188
  (
@@ -210,18 +194,3 @@ class ValidationError(Exception):
210
194
  if hasattr(self, "error_dict"):
211
195
  return hash(make_hashable(self.error_dict))
212
196
  return hash(tuple(sorted(self.error_list, key=operator.attrgetter("message"))))
213
-
214
-
215
- # MARK: Database
216
-
217
-
218
- class EmptyResultSet(Exception):
219
- """A database query predicate is impossible."""
220
-
221
- pass
222
-
223
-
224
- class FullResultSet(Exception):
225
- """A database query predicate is matches everything."""
226
-
227
- pass
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