plain 0.86.2__tar.gz → 0.88.0__tar.gz

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 (210) hide show
  1. {plain-0.86.2 → plain-0.88.0}/PKG-INFO +1 -1
  2. {plain-0.86.2 → plain-0.88.0}/plain/CHANGELOG.md +21 -0
  3. {plain-0.86.2 → plain-0.88.0}/plain/assets/views.py +7 -2
  4. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/request.py +1 -1
  5. {plain-0.86.2 → plain-0.88.0}/plain/cli/preflight.py +3 -2
  6. {plain-0.86.2 → plain-0.88.0}/plain/exceptions.py +2 -2
  7. plain-0.88.0/plain/forms/__init__.py +59 -0
  8. {plain-0.86.2 → plain-0.88.0}/plain/forms/boundfield.py +1 -1
  9. {plain-0.86.2 → plain-0.88.0}/plain/forms/fields.py +192 -43
  10. {plain-0.86.2 → plain-0.88.0}/plain/forms/forms.py +8 -1
  11. {plain-0.86.2 → plain-0.88.0}/plain/http/multipartparser.py +2 -2
  12. {plain-0.86.2 → plain-0.88.0}/plain/http/request.py +9 -6
  13. {plain-0.86.2 → plain-0.88.0}/plain/http/response.py +6 -6
  14. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/base.py +1 -1
  15. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/temp.py +2 -1
  16. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/uploadedfile.py +10 -7
  17. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/uploadhandler.py +13 -8
  18. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/utils.py +4 -1
  19. {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/base.py +4 -1
  20. {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/exception.py +4 -4
  21. {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/wsgi.py +5 -3
  22. {plain-0.86.2 → plain-0.88.0}/plain/logs/formatters.py +1 -1
  23. {plain-0.86.2 → plain-0.88.0}/plain/preflight/results.py +1 -1
  24. {plain-0.86.2 → plain-0.88.0}/plain/server/arbiter.py +9 -9
  25. {plain-0.86.2 → plain-0.88.0}/plain/server/http/body.py +2 -2
  26. {plain-0.86.2 → plain-0.88.0}/plain/server/http/errors.py +7 -2
  27. {plain-0.86.2 → plain-0.88.0}/plain/server/http/message.py +3 -2
  28. {plain-0.86.2 → plain-0.88.0}/plain/server/http/parser.py +3 -2
  29. {plain-0.86.2 → plain-0.88.0}/plain/server/sock.py +19 -0
  30. {plain-0.86.2 → plain-0.88.0}/plain/server/workers/base.py +11 -9
  31. {plain-0.86.2 → plain-0.88.0}/plain/server/workers/sync.py +5 -3
  32. {plain-0.86.2 → plain-0.88.0}/plain/server/workers/thread.py +8 -7
  33. {plain-0.86.2 → plain-0.88.0}/plain/test/client.py +8 -6
  34. {plain-0.86.2 → plain-0.88.0}/plain/test/exceptions.py +2 -5
  35. {plain-0.86.2 → plain-0.88.0}/plain/urls/patterns.py +4 -0
  36. {plain-0.86.2 → plain-0.88.0}/plain/urls/resolvers.py +1 -1
  37. {plain-0.86.2 → plain-0.88.0}/plain/utils/cache.py +5 -5
  38. {plain-0.86.2 → plain-0.88.0}/plain/utils/datastructures.py +4 -2
  39. {plain-0.86.2 → plain-0.88.0}/plain/utils/functional.py +2 -0
  40. {plain-0.86.2 → plain-0.88.0}/plain/utils/regex_helper.py +2 -2
  41. {plain-0.86.2 → plain-0.88.0}/plain/utils/text.py +2 -0
  42. {plain-0.86.2 → plain-0.88.0}/plain/utils/tree.py +8 -5
  43. {plain-0.86.2 → plain-0.88.0}/plain/validators.py +24 -9
  44. {plain-0.86.2 → plain-0.88.0}/plain/views/base.py +2 -2
  45. {plain-0.86.2 → plain-0.88.0}/plain/views/errors.py +1 -1
  46. {plain-0.86.2 → plain-0.88.0}/plain/views/forms.py +1 -1
  47. {plain-0.86.2 → plain-0.88.0}/plain/views/objects.py +17 -20
  48. {plain-0.86.2 → plain-0.88.0}/pyproject.toml +1 -1
  49. {plain-0.86.2 → plain-0.88.0}/tests/test_csrf.py +2 -1
  50. plain-0.86.2/plain/forms/__init__.py +0 -8
  51. {plain-0.86.2 → plain-0.88.0}/.gitignore +0 -0
  52. {plain-0.86.2 → plain-0.88.0}/LICENSE +0 -0
  53. {plain-0.86.2 → plain-0.88.0}/README.md +0 -0
  54. {plain-0.86.2 → plain-0.88.0}/plain/AGENTS.md +0 -0
  55. {plain-0.86.2 → plain-0.88.0}/plain/README.md +0 -0
  56. {plain-0.86.2 → plain-0.88.0}/plain/__main__.py +0 -0
  57. {plain-0.86.2 → plain-0.88.0}/plain/assets/README.md +0 -0
  58. {plain-0.86.2 → plain-0.88.0}/plain/assets/__init__.py +0 -0
  59. {plain-0.86.2 → plain-0.88.0}/plain/assets/compile.py +0 -0
  60. {plain-0.86.2 → plain-0.88.0}/plain/assets/finders.py +0 -0
  61. {plain-0.86.2 → plain-0.88.0}/plain/assets/fingerprints.py +0 -0
  62. {plain-0.86.2 → plain-0.88.0}/plain/assets/urls.py +0 -0
  63. {plain-0.86.2 → plain-0.88.0}/plain/chores/README.md +0 -0
  64. {plain-0.86.2 → plain-0.88.0}/plain/chores/__init__.py +0 -0
  65. {plain-0.86.2 → plain-0.88.0}/plain/chores/core.py +0 -0
  66. {plain-0.86.2 → plain-0.88.0}/plain/chores/registry.py +0 -0
  67. {plain-0.86.2 → plain-0.88.0}/plain/cli/README.md +0 -0
  68. {plain-0.86.2 → plain-0.88.0}/plain/cli/__init__.py +0 -0
  69. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/__init__.py +0 -0
  70. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/docs.py +0 -0
  71. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/llmdocs.py +0 -0
  72. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/md.py +0 -0
  73. {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/prompt.py +0 -0
  74. {plain-0.86.2 → plain-0.88.0}/plain/cli/build.py +0 -0
  75. {plain-0.86.2 → plain-0.88.0}/plain/cli/changelog.py +0 -0
  76. {plain-0.86.2 → plain-0.88.0}/plain/cli/chores.py +0 -0
  77. {plain-0.86.2 → plain-0.88.0}/plain/cli/core.py +0 -0
  78. {plain-0.86.2 → plain-0.88.0}/plain/cli/docs.py +0 -0
  79. {plain-0.86.2 → plain-0.88.0}/plain/cli/formatting.py +0 -0
  80. {plain-0.86.2 → plain-0.88.0}/plain/cli/install.py +0 -0
  81. {plain-0.86.2 → plain-0.88.0}/plain/cli/output.py +0 -0
  82. {plain-0.86.2 → plain-0.88.0}/plain/cli/print.py +0 -0
  83. {plain-0.86.2 → plain-0.88.0}/plain/cli/registry.py +0 -0
  84. {plain-0.86.2 → plain-0.88.0}/plain/cli/runtime.py +0 -0
  85. {plain-0.86.2 → plain-0.88.0}/plain/cli/scaffold.py +0 -0
  86. {plain-0.86.2 → plain-0.88.0}/plain/cli/server.py +0 -0
  87. {plain-0.86.2 → plain-0.88.0}/plain/cli/settings.py +0 -0
  88. {plain-0.86.2 → plain-0.88.0}/plain/cli/shell.py +0 -0
  89. {plain-0.86.2 → plain-0.88.0}/plain/cli/startup.py +0 -0
  90. {plain-0.86.2 → plain-0.88.0}/plain/cli/upgrade.py +0 -0
  91. {plain-0.86.2 → plain-0.88.0}/plain/cli/urls.py +0 -0
  92. {plain-0.86.2 → plain-0.88.0}/plain/cli/utils.py +0 -0
  93. {plain-0.86.2 → plain-0.88.0}/plain/csrf/README.md +0 -0
  94. {plain-0.86.2 → plain-0.88.0}/plain/csrf/middleware.py +0 -0
  95. {plain-0.86.2 → plain-0.88.0}/plain/debug.py +0 -0
  96. {plain-0.86.2 → plain-0.88.0}/plain/forms/README.md +0 -0
  97. {plain-0.86.2 → plain-0.88.0}/plain/forms/exceptions.py +0 -0
  98. {plain-0.86.2 → plain-0.88.0}/plain/http/README.md +0 -0
  99. {plain-0.86.2 → plain-0.88.0}/plain/http/__init__.py +0 -0
  100. {plain-0.86.2 → plain-0.88.0}/plain/http/cookie.py +0 -0
  101. {plain-0.86.2 → plain-0.88.0}/plain/http/middleware.py +0 -0
  102. {plain-0.86.2 → plain-0.88.0}/plain/internal/__init__.py +0 -0
  103. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/__init__.py +0 -0
  104. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/locks.py +0 -0
  105. {plain-0.86.2 → plain-0.88.0}/plain/internal/files/move.py +0 -0
  106. {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/__init__.py +0 -0
  107. {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/__init__.py +0 -0
  108. {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/headers.py +0 -0
  109. {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/hosts.py +0 -0
  110. {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/https.py +0 -0
  111. {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/slash.py +0 -0
  112. {plain-0.86.2 → plain-0.88.0}/plain/internal/reloader.py +0 -0
  113. {plain-0.86.2 → plain-0.88.0}/plain/json.py +0 -0
  114. {plain-0.86.2 → plain-0.88.0}/plain/logs/README.md +0 -0
  115. {plain-0.86.2 → plain-0.88.0}/plain/logs/__init__.py +0 -0
  116. {plain-0.86.2 → plain-0.88.0}/plain/logs/app.py +0 -0
  117. {plain-0.86.2 → plain-0.88.0}/plain/logs/configure.py +0 -0
  118. {plain-0.86.2 → plain-0.88.0}/plain/logs/debug.py +0 -0
  119. {plain-0.86.2 → plain-0.88.0}/plain/logs/filters.py +0 -0
  120. {plain-0.86.2 → plain-0.88.0}/plain/packages/README.md +0 -0
  121. {plain-0.86.2 → plain-0.88.0}/plain/packages/__init__.py +0 -0
  122. {plain-0.86.2 → plain-0.88.0}/plain/packages/config.py +0 -0
  123. {plain-0.86.2 → plain-0.88.0}/plain/packages/registry.py +0 -0
  124. {plain-0.86.2 → plain-0.88.0}/plain/paginator.py +0 -0
  125. {plain-0.86.2 → plain-0.88.0}/plain/preflight/README.md +0 -0
  126. {plain-0.86.2 → plain-0.88.0}/plain/preflight/__init__.py +0 -0
  127. {plain-0.86.2 → plain-0.88.0}/plain/preflight/checks.py +0 -0
  128. {plain-0.86.2 → plain-0.88.0}/plain/preflight/files.py +0 -0
  129. {plain-0.86.2 → plain-0.88.0}/plain/preflight/registry.py +0 -0
  130. {plain-0.86.2 → plain-0.88.0}/plain/preflight/security.py +0 -0
  131. {plain-0.86.2 → plain-0.88.0}/plain/preflight/urls.py +0 -0
  132. {plain-0.86.2 → plain-0.88.0}/plain/runtime/README.md +0 -0
  133. {plain-0.86.2 → plain-0.88.0}/plain/runtime/__init__.py +0 -0
  134. {plain-0.86.2 → plain-0.88.0}/plain/runtime/global_settings.py +0 -0
  135. {plain-0.86.2 → plain-0.88.0}/plain/runtime/user_settings.py +0 -0
  136. {plain-0.86.2 → plain-0.88.0}/plain/runtime/utils.py +0 -0
  137. {plain-0.86.2 → plain-0.88.0}/plain/server/LICENSE +0 -0
  138. {plain-0.86.2 → plain-0.88.0}/plain/server/README.md +0 -0
  139. {plain-0.86.2 → plain-0.88.0}/plain/server/__init__.py +0 -0
  140. {plain-0.86.2 → plain-0.88.0}/plain/server/app.py +0 -0
  141. {plain-0.86.2 → plain-0.88.0}/plain/server/config.py +0 -0
  142. {plain-0.86.2 → plain-0.88.0}/plain/server/errors.py +0 -0
  143. {plain-0.86.2 → plain-0.88.0}/plain/server/glogging.py +0 -0
  144. {plain-0.86.2 → plain-0.88.0}/plain/server/http/__init__.py +0 -0
  145. {plain-0.86.2 → plain-0.88.0}/plain/server/http/unreader.py +0 -0
  146. {plain-0.86.2 → plain-0.88.0}/plain/server/http/wsgi.py +0 -0
  147. {plain-0.86.2 → plain-0.88.0}/plain/server/pidfile.py +0 -0
  148. {plain-0.86.2 → plain-0.88.0}/plain/server/util.py +0 -0
  149. {plain-0.86.2 → plain-0.88.0}/plain/server/workers/__init__.py +0 -0
  150. {plain-0.86.2 → plain-0.88.0}/plain/server/workers/workertmp.py +0 -0
  151. {plain-0.86.2 → plain-0.88.0}/plain/signals/README.md +0 -0
  152. {plain-0.86.2 → plain-0.88.0}/plain/signals/__init__.py +0 -0
  153. {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/__init__.py +0 -0
  154. {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/dispatcher.py +0 -0
  155. {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/license.txt +0 -0
  156. {plain-0.86.2 → plain-0.88.0}/plain/signing.py +0 -0
  157. {plain-0.86.2 → plain-0.88.0}/plain/templates/AGENTS.md +0 -0
  158. {plain-0.86.2 → plain-0.88.0}/plain/templates/README.md +0 -0
  159. {plain-0.86.2 → plain-0.88.0}/plain/templates/__init__.py +0 -0
  160. {plain-0.86.2 → plain-0.88.0}/plain/templates/core.py +0 -0
  161. {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/__init__.py +0 -0
  162. {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/environments.py +0 -0
  163. {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/extensions.py +0 -0
  164. {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/filters.py +0 -0
  165. {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/globals.py +0 -0
  166. {plain-0.86.2 → plain-0.88.0}/plain/test/README.md +0 -0
  167. {plain-0.86.2 → plain-0.88.0}/plain/test/__init__.py +0 -0
  168. {plain-0.86.2 → plain-0.88.0}/plain/test/encoding.py +0 -0
  169. {plain-0.86.2 → plain-0.88.0}/plain/urls/README.md +0 -0
  170. {plain-0.86.2 → plain-0.88.0}/plain/urls/__init__.py +0 -0
  171. {plain-0.86.2 → plain-0.88.0}/plain/urls/converters.py +0 -0
  172. {plain-0.86.2 → plain-0.88.0}/plain/urls/exceptions.py +0 -0
  173. {plain-0.86.2 → plain-0.88.0}/plain/urls/routers.py +0 -0
  174. {plain-0.86.2 → plain-0.88.0}/plain/urls/utils.py +0 -0
  175. {plain-0.86.2 → plain-0.88.0}/plain/utils/README.md +0 -0
  176. {plain-0.86.2 → plain-0.88.0}/plain/utils/__init__.py +0 -0
  177. {plain-0.86.2 → plain-0.88.0}/plain/utils/crypto.py +0 -0
  178. {plain-0.86.2 → plain-0.88.0}/plain/utils/dateparse.py +0 -0
  179. {plain-0.86.2 → plain-0.88.0}/plain/utils/deconstruct.py +0 -0
  180. {plain-0.86.2 → plain-0.88.0}/plain/utils/decorators.py +0 -0
  181. {plain-0.86.2 → plain-0.88.0}/plain/utils/duration.py +0 -0
  182. {plain-0.86.2 → plain-0.88.0}/plain/utils/encoding.py +0 -0
  183. {plain-0.86.2 → plain-0.88.0}/plain/utils/hashable.py +0 -0
  184. {plain-0.86.2 → plain-0.88.0}/plain/utils/html.py +0 -0
  185. {plain-0.86.2 → plain-0.88.0}/plain/utils/http.py +0 -0
  186. {plain-0.86.2 → plain-0.88.0}/plain/utils/inspect.py +0 -0
  187. {plain-0.86.2 → plain-0.88.0}/plain/utils/ipv6.py +0 -0
  188. {plain-0.86.2 → plain-0.88.0}/plain/utils/itercompat.py +0 -0
  189. {plain-0.86.2 → plain-0.88.0}/plain/utils/module_loading.py +0 -0
  190. {plain-0.86.2 → plain-0.88.0}/plain/utils/safestring.py +0 -0
  191. {plain-0.86.2 → plain-0.88.0}/plain/utils/timesince.py +0 -0
  192. {plain-0.86.2 → plain-0.88.0}/plain/utils/timezone.py +0 -0
  193. {plain-0.86.2 → plain-0.88.0}/plain/views/README.md +0 -0
  194. {plain-0.86.2 → plain-0.88.0}/plain/views/__init__.py +0 -0
  195. {plain-0.86.2 → plain-0.88.0}/plain/views/exceptions.py +0 -0
  196. {plain-0.86.2 → plain-0.88.0}/plain/views/redirect.py +0 -0
  197. {plain-0.86.2 → plain-0.88.0}/plain/views/templates.py +0 -0
  198. {plain-0.86.2 → plain-0.88.0}/plain/wsgi.py +0 -0
  199. {plain-0.86.2 → plain-0.88.0}/tests/.gitignore +0 -0
  200. {plain-0.86.2 → plain-0.88.0}/tests/app/.gitignore +0 -0
  201. {plain-0.86.2 → plain-0.88.0}/tests/app/settings.py +0 -0
  202. {plain-0.86.2 → plain-0.88.0}/tests/app/test/__init__.py +0 -0
  203. {plain-0.86.2 → plain-0.88.0}/tests/app/test/default_settings.py +0 -0
  204. {plain-0.86.2 → plain-0.88.0}/tests/app/urls.py +0 -0
  205. {plain-0.86.2 → plain-0.88.0}/tests/conftest.py +0 -0
  206. {plain-0.86.2 → plain-0.88.0}/tests/test_cli.py +0 -0
  207. {plain-0.86.2 → plain-0.88.0}/tests/test_http_hosts.py +0 -0
  208. {plain-0.86.2 → plain-0.88.0}/tests/test_logs.py +0 -0
  209. {plain-0.86.2 → plain-0.88.0}/tests/test_runtime.py +0 -0
  210. {plain-0.86.2 → plain-0.88.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.86.2
3
+ Version: 0.88.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -1,5 +1,26 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.88.0](https://github.com/dropseed/plain/releases/plain@0.88.0) (2025-11-13)
4
+
5
+ ### What's changed
6
+
7
+ - The `plain.forms` module now uses explicit imports instead of wildcard imports, improving IDE autocomplete and type checking support ([eff36f3](https://github.com/dropseed/plain/commit/eff36f31e8e15f84e11164a44c833aeab096ffbd))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
13
+ ## [0.87.0](https://github.com/dropseed/plain/releases/plain@0.87.0) (2025-11-12)
14
+
15
+ ### What's changed
16
+
17
+ - Internal classes now use abstract base classes with `@abstractmethod` decorators instead of raising `NotImplementedError`, improving type checking and IDE support ([91b329a](https://github.com/dropseed/plain/commit/91b329a8adb477031c4358e638b12f35f19bb85d), [81b5f88](https://github.com/dropseed/plain/commit/81b5f88a4bd39785f6b19c3c00c0ed23a36fb72f), [d2e2423](https://github.com/dropseed/plain/commit/d2e24235f497a92f45d5a21fc83d802897c2dec0), [61e7b5a](https://github.com/dropseed/plain/commit/61e7b5a0c8675aaaf65f0a626ff7959a786dca7f))
18
+ - Updated to latest version of `ty` type checker and fixed type errors and warnings throughout the codebase ([f4dbcef](https://github.com/dropseed/plain/commit/f4dbcefa929058be517cb1d4ab35bd73a89f26b8))
19
+
20
+ ### Upgrade instructions
21
+
22
+ - No changes required
23
+
3
24
  ## [0.86.2](https://github.com/dropseed/plain/releases/plain@0.86.2) (2025-11-11)
4
25
 
5
26
  ### What's changed
@@ -14,6 +14,7 @@ from plain.http import (
14
14
  ResponseRedirect,
15
15
  StreamingResponse,
16
16
  )
17
+ from plain.http.response import ResponseHeaders
17
18
  from plain.runtime import settings
18
19
  from plain.urls import reverse
19
20
  from plain.views import View
@@ -37,7 +38,7 @@ class AssetView(View):
37
38
  def get_url_path(self) -> str:
38
39
  return self.asset_path or self.url_kwargs["path"]
39
40
 
40
- def get(self) -> Response | FileResponse:
41
+ def get(self) -> Response | FileResponse | StreamingResponse:
41
42
  url_path = self.get_url_path()
42
43
 
43
44
  # Make a trailing slash work, but we don't expect it
@@ -52,7 +53,11 @@ class AssetView(View):
52
53
  if redirect_response := self.get_redirect_response(url_path):
53
54
  return redirect_response
54
55
 
56
+ # check_asset_path validates and raises if path is invalid
57
+ # After this point, absolute_path is guaranteed to be a valid str
55
58
  self.check_asset_path(absolute_path)
59
+ # Type guard: absolute_path is now str (check_asset_path raises if None/invalid)
60
+ assert absolute_path is not None
56
61
 
57
62
  if encoded_path := self.get_encoded_path(absolute_path):
58
63
  absolute_path = encoded_path
@@ -127,7 +132,7 @@ class AssetView(View):
127
132
  def get_size(self, path: str) -> int:
128
133
  return os.path.getsize(path)
129
134
 
130
- def update_headers(self, headers: dict, path: str) -> dict:
135
+ def update_headers(self, headers: ResponseHeaders, path: str) -> ResponseHeaders:
131
136
  headers.setdefault("Access-Control-Allow-Origin", "*")
132
137
 
133
138
  # Always vary on Accept-Encoding
@@ -98,7 +98,7 @@ def request(
98
98
 
99
99
  # Make the request
100
100
  method = method.upper()
101
- kwargs: dict[str, object] = {
101
+ kwargs: dict[str, Any] = {
102
102
  "follow": follow,
103
103
  }
104
104
  if header_dict:
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import sys
3
+ from typing import Any
3
4
 
4
5
  import click
5
6
 
@@ -125,12 +126,12 @@ def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
125
126
 
126
127
  if format == "json":
127
128
  # Build JSON output
128
- results = {"passed": not has_errors, "checks": []}
129
+ results: dict[str, Any] = {"passed": not has_errors, "checks": []}
129
130
 
130
131
  for check_class, check_name, issues in check_results:
131
132
  visible_issues = [issue for issue in issues if not issue.is_silenced()]
132
133
 
133
- check_result = {
134
+ check_result: dict[str, Any] = {
134
135
  "name": check_name,
135
136
  "passed": len(visible_issues) == 0,
136
137
  "issues": [],
@@ -145,7 +145,7 @@ class ValidationError(Exception):
145
145
  @property
146
146
  def messages(self) -> list[str]:
147
147
  if hasattr(self, "error_dict"):
148
- return sum(dict(self).values(), [])
148
+ return sum(dict(self).values(), []) # type: ignore[call-overload]
149
149
  return list(self)
150
150
 
151
151
  def update_error_dict(
@@ -171,7 +171,7 @@ class ValidationError(Exception):
171
171
 
172
172
  def __str__(self) -> str:
173
173
  if hasattr(self, "error_dict"):
174
- return repr(dict(self))
174
+ return repr(dict(self)) # type: ignore[call-overload]
175
175
  return repr(list(self))
176
176
 
177
177
  def __repr__(self) -> str:
@@ -0,0 +1,59 @@
1
+ """
2
+ Plain validation and HTML form handling.
3
+ """
4
+
5
+ from .boundfield import BoundField
6
+ from .exceptions import FormFieldMissingError, ValidationError
7
+ from .fields import (
8
+ BooleanField,
9
+ CharField,
10
+ ChoiceField,
11
+ DateField,
12
+ DateTimeField,
13
+ DecimalField,
14
+ DurationField,
15
+ EmailField,
16
+ Field,
17
+ FileField,
18
+ FloatField,
19
+ ImageField,
20
+ IntegerField,
21
+ JSONField,
22
+ MultipleChoiceField,
23
+ NullBooleanField,
24
+ RegexField,
25
+ TimeField,
26
+ TypedChoiceField,
27
+ URLField,
28
+ UUIDField,
29
+ )
30
+ from .forms import BaseForm, Form
31
+
32
+ __all__ = [
33
+ "BoundField",
34
+ "FormFieldMissingError",
35
+ "ValidationError",
36
+ "BooleanField",
37
+ "CharField",
38
+ "ChoiceField",
39
+ "DateField",
40
+ "DateTimeField",
41
+ "DecimalField",
42
+ "DurationField",
43
+ "EmailField",
44
+ "Field",
45
+ "FileField",
46
+ "FloatField",
47
+ "ImageField",
48
+ "IntegerField",
49
+ "JSONField",
50
+ "MultipleChoiceField",
51
+ "NullBooleanField",
52
+ "RegexField",
53
+ "TimeField",
54
+ "TypedChoiceField",
55
+ "URLField",
56
+ "UUIDField",
57
+ "BaseForm",
58
+ "Form",
59
+ ]
@@ -58,7 +58,7 @@ class BoundField:
58
58
  associated Form has specified auto_id. Return an empty string otherwise.
59
59
  """
60
60
  auto_id = self._form._auto_id # Boolean or string
61
- if auto_id and "%s" in str(auto_id):
61
+ if auto_id and isinstance(auto_id, str) and "%s" in auto_id:
62
62
  return auto_id % self.html_name
63
63
  elif auto_id:
64
64
  return self.html_name
@@ -11,13 +11,13 @@ import json
11
11
  import math
12
12
  import re
13
13
  import uuid
14
- from collections.abc import Callable
14
+ from collections.abc import Callable, Iterator, Sequence
15
15
  from decimal import Decimal, DecimalException
16
16
  from io import BytesIO
17
- from typing import TYPE_CHECKING, Any
17
+ from typing import TYPE_CHECKING, Any, Self
18
18
  from urllib.parse import urlsplit, urlunsplit
19
19
 
20
- from plain import validators
20
+ from plain import validators as validators_
21
21
  from plain.exceptions import ValidationError
22
22
  from plain.utils import timezone
23
23
  from plain.utils.dateparse import parse_datetime, parse_duration
@@ -66,7 +66,7 @@ class Field:
66
66
  default_error_messages = {
67
67
  "required": "This field is required.",
68
68
  }
69
- empty_values = list(validators.EMPTY_VALUES)
69
+ empty_values = list(validators_.EMPTY_VALUES)
70
70
 
71
71
  def __init__(
72
72
  self,
@@ -74,7 +74,7 @@ class Field:
74
74
  required: bool = True,
75
75
  initial: Any = None,
76
76
  error_messages: dict[str, str] | None = None,
77
- validators: tuple[Callable[[Any], None], ...] = (),
77
+ validators: Sequence[Callable[[Any], None]] = (),
78
78
  ):
79
79
  # required -- Boolean that specifies whether the field is required.
80
80
  # True by default.
@@ -144,7 +144,7 @@ class Field:
144
144
  try:
145
145
  data = self.to_python(data)
146
146
  if hasattr(self, "_coerce"):
147
- return self._coerce(data) != self._coerce(initial)
147
+ return self._coerce(data) != self._coerce(initial) # type: ignore[attr-defined]
148
148
  except ValidationError:
149
149
  return True
150
150
  # For purposes of seeing whether something has changed, None is
@@ -161,7 +161,7 @@ class Field:
161
161
  """
162
162
  return BoundField(form, self, field_name)
163
163
 
164
- def __deepcopy__(self, memo: dict[int, Any]) -> Field:
164
+ def __deepcopy__(self: Self, memo: dict[int, Any]) -> Self:
165
165
  result = copy.copy(self)
166
166
  memo[id(self)] = result
167
167
  result.error_messages = self.error_messages.copy()
@@ -190,18 +190,26 @@ class CharField(Field):
190
190
  min_length: int | None = None,
191
191
  strip: bool = True,
192
192
  empty_value: str = "",
193
- **kwargs: Any,
193
+ required: bool = True,
194
+ initial: Any = None,
195
+ error_messages: dict[str, str] | None = None,
196
+ validators: Sequence[Callable[[Any], None]] = (),
194
197
  ):
195
198
  self.max_length = max_length
196
199
  self.min_length = min_length
197
200
  self.strip = strip
198
201
  self.empty_value = empty_value
199
- super().__init__(**kwargs)
202
+ super().__init__(
203
+ required=required,
204
+ initial=initial,
205
+ error_messages=error_messages,
206
+ validators=validators,
207
+ )
200
208
  if min_length is not None:
201
- self.validators.append(validators.MinLengthValidator(int(min_length)))
209
+ self.validators.append(validators_.MinLengthValidator(int(min_length)))
202
210
  if max_length is not None:
203
- self.validators.append(validators.MaxLengthValidator(int(max_length)))
204
- self.validators.append(validators.ProhibitNullCharactersValidator())
211
+ self.validators.append(validators_.MaxLengthValidator(int(max_length)))
212
+ self.validators.append(validators_.ProhibitNullCharactersValidator())
205
213
 
206
214
  def to_python(self, value: Any) -> str:
207
215
  """Return a string."""
@@ -226,17 +234,25 @@ class IntegerField(Field):
226
234
  max_value: int | None = None,
227
235
  min_value: int | None = None,
228
236
  step_size: int | None = None,
229
- **kwargs: Any,
237
+ required: bool = True,
238
+ initial: Any = None,
239
+ error_messages: dict[str, str] | None = None,
240
+ validators: Sequence[Callable[[Any], None]] = (),
230
241
  ):
231
242
  self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
232
- super().__init__(**kwargs)
243
+ super().__init__(
244
+ required=required,
245
+ initial=initial,
246
+ error_messages=error_messages,
247
+ validators=validators,
248
+ )
233
249
 
234
250
  if max_value is not None:
235
- self.validators.append(validators.MaxValueValidator(max_value))
251
+ self.validators.append(validators_.MaxValueValidator(max_value))
236
252
  if min_value is not None:
237
- self.validators.append(validators.MinValueValidator(min_value))
253
+ self.validators.append(validators_.MinValueValidator(min_value))
238
254
  if step_size is not None:
239
- self.validators.append(validators.StepValueValidator(step_size))
255
+ self.validators.append(validators_.StepValueValidator(step_size))
240
256
 
241
257
  def to_python(self, value: Any) -> int | None:
242
258
  """
@@ -293,11 +309,21 @@ class DecimalField(IntegerField):
293
309
  min_value: int | None = None,
294
310
  max_digits: int | None = None,
295
311
  decimal_places: int | None = None,
296
- **kwargs: Any,
312
+ required: bool = True,
313
+ initial: Any = None,
314
+ error_messages: dict[str, str] | None = None,
315
+ validators: Sequence[Callable[[Any], None]] = (),
297
316
  ):
298
317
  self.max_digits, self.decimal_places = max_digits, decimal_places
299
- super().__init__(max_value=max_value, min_value=min_value, **kwargs)
300
- self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
318
+ super().__init__(
319
+ max_value=max_value,
320
+ min_value=min_value,
321
+ required=required,
322
+ initial=initial,
323
+ error_messages=error_messages,
324
+ validators=validators,
325
+ )
326
+ self.validators.append(validators_.DecimalValidator(max_digits, decimal_places))
301
327
 
302
328
  def to_python(self, value: Any) -> Decimal | None:
303
329
  """
@@ -372,8 +398,21 @@ class BaseTemporalField(Field):
372
398
  "%m/%d/%y %H:%M", # '10/25/06 14:30'
373
399
  ]
374
400
 
375
- def __init__(self, *, input_formats: list[str] | None = None, **kwargs: Any):
376
- super().__init__(**kwargs)
401
+ def __init__(
402
+ self,
403
+ *,
404
+ input_formats: list[str] | None = None,
405
+ required: bool = True,
406
+ initial: Any = None,
407
+ error_messages: dict[str, str] | None = None,
408
+ validators: Sequence[Callable[[Any], None]] = (),
409
+ ):
410
+ super().__init__(
411
+ required=required,
412
+ initial=initial,
413
+ error_messages=error_messages,
414
+ validators=validators,
415
+ )
377
416
  if input_formats is not None:
378
417
  self.input_formats = input_formats
379
418
 
@@ -506,12 +545,32 @@ class DurationField(Field):
506
545
 
507
546
 
508
547
  class RegexField(CharField):
509
- def __init__(self, regex: str | re.Pattern[str], **kwargs: Any) -> None:
548
+ def __init__(
549
+ self,
550
+ regex: str | re.Pattern[str],
551
+ *,
552
+ max_length: int | None = None,
553
+ min_length: int | None = None,
554
+ strip: bool = False,
555
+ empty_value: str = "",
556
+ required: bool = True,
557
+ initial: Any = None,
558
+ error_messages: dict[str, str] | None = None,
559
+ validators: Sequence[Callable[[Any], None]] = (),
560
+ ) -> None:
510
561
  """
511
562
  regex can be either a string or a compiled regular expression object.
512
563
  """
513
- kwargs.setdefault("strip", False)
514
- super().__init__(**kwargs)
564
+ super().__init__(
565
+ max_length=max_length,
566
+ min_length=min_length,
567
+ strip=strip,
568
+ empty_value=empty_value,
569
+ required=required,
570
+ initial=initial,
571
+ error_messages=error_messages,
572
+ validators=validators,
573
+ )
515
574
  self._set_regex(regex)
516
575
 
517
576
  def _get_regex(self) -> re.Pattern[str]:
@@ -526,17 +585,37 @@ class RegexField(CharField):
526
585
  and self._regex_validator in self.validators
527
586
  ):
528
587
  self.validators.remove(self._regex_validator)
529
- self._regex_validator = validators.RegexValidator(regex=regex)
588
+ self._regex_validator = validators_.RegexValidator(regex=regex)
530
589
  self.validators.append(self._regex_validator)
531
590
 
532
591
  regex = property(_get_regex, _set_regex)
533
592
 
534
593
 
535
594
  class EmailField(CharField):
536
- default_validators = [validators.validate_email]
595
+ default_validators = [validators_.validate_email]
537
596
 
538
- def __init__(self, **kwargs: Any) -> None:
539
- super().__init__(strip=True, **kwargs)
597
+ def __init__(
598
+ self,
599
+ *,
600
+ max_length: int | None = None,
601
+ min_length: int | None = None,
602
+ strip: bool = True,
603
+ empty_value: str = "",
604
+ required: bool = True,
605
+ initial: Any = None,
606
+ error_messages: dict[str, str] | None = None,
607
+ validators: Sequence[Callable[[Any], None]] = (),
608
+ ) -> None:
609
+ super().__init__(
610
+ max_length=max_length,
611
+ min_length=min_length,
612
+ strip=strip,
613
+ empty_value=empty_value,
614
+ required=required,
615
+ initial=initial,
616
+ error_messages=error_messages,
617
+ validators=validators,
618
+ )
540
619
 
541
620
 
542
621
  class FileField(Field):
@@ -557,11 +636,19 @@ class FileField(Field):
557
636
  *,
558
637
  max_length: int | None = None,
559
638
  allow_empty_file: bool = False,
560
- **kwargs: Any,
639
+ required: bool = True,
640
+ initial: Any = None,
641
+ error_messages: dict[str, str] | None = None,
642
+ validators: Sequence[Callable[[Any], None]] = (),
561
643
  ) -> None:
562
644
  self.max_length = max_length
563
645
  self.allow_empty_file = allow_empty_file
564
- super().__init__(**kwargs)
646
+ super().__init__(
647
+ required=required,
648
+ initial=initial,
649
+ error_messages=error_messages,
650
+ validators=validators,
651
+ )
565
652
 
566
653
  def to_python(self, data: Any) -> Any:
567
654
  if data in self.empty_values:
@@ -621,7 +708,7 @@ class FileField(Field):
621
708
 
622
709
 
623
710
  class ImageField(FileField):
624
- default_validators = [validators.validate_image_file_extension]
711
+ default_validators = [validators_.validate_image_file_extension]
625
712
  default_error_messages = {
626
713
  "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
627
714
  }
@@ -674,10 +761,30 @@ class URLField(CharField):
674
761
  default_error_messages = {
675
762
  "invalid": "Enter a valid URL.",
676
763
  }
677
- default_validators = [validators.URLValidator()]
764
+ default_validators = [validators_.URLValidator()]
678
765
 
679
- def __init__(self, **kwargs: Any) -> None:
680
- super().__init__(strip=True, **kwargs)
766
+ def __init__(
767
+ self,
768
+ *,
769
+ max_length: int | None = None,
770
+ min_length: int | None = None,
771
+ strip: bool = True,
772
+ empty_value: str = "",
773
+ required: bool = True,
774
+ initial: Any = None,
775
+ error_messages: dict[str, str] | None = None,
776
+ validators: Sequence[Callable[[Any], None]] = (),
777
+ ) -> None:
778
+ super().__init__(
779
+ max_length=max_length,
780
+ min_length=min_length,
781
+ strip=strip,
782
+ empty_value=empty_value,
783
+ required=required,
784
+ initial=initial,
785
+ error_messages=error_messages,
786
+ validators=validators,
787
+ )
681
788
 
682
789
  def to_python(self, value: Any) -> str:
683
790
  def split_url(url: str | bytes) -> list[str]:
@@ -792,7 +899,7 @@ class CallableChoiceIterator:
792
899
  def __init__(self, choices_func: Callable[[], Any]) -> None:
793
900
  self.choices_func = choices_func
794
901
 
795
- def __iter__(self) -> Any:
902
+ def __iter__(self) -> Iterator[Any]:
796
903
  yield from self.choices_func()
797
904
 
798
905
 
@@ -801,8 +908,23 @@ class ChoiceField(Field):
801
908
  "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
802
909
  }
803
910
 
804
- def __init__(self, *, choices: Any = (), **kwargs: Any) -> None:
805
- super().__init__(**kwargs)
911
+ _choices: CallableChoiceIterator | list[Any] # Set by choices property setter
912
+
913
+ def __init__(
914
+ self,
915
+ *,
916
+ choices: Any = (),
917
+ required: bool = True,
918
+ initial: Any = None,
919
+ error_messages: dict[str, str] | None = None,
920
+ validators: Sequence[Callable[[Any], None]] = (),
921
+ ) -> None:
922
+ super().__init__(
923
+ required=required,
924
+ initial=initial,
925
+ error_messages=error_messages,
926
+ validators=validators,
927
+ )
806
928
  if hasattr(choices, "choices"):
807
929
  choices = choices.choices
808
930
  elif isinstance(choices, enum.EnumMeta):
@@ -814,7 +936,7 @@ class ChoiceField(Field):
814
936
  result._choices = copy.deepcopy(self._choices, memo)
815
937
  return result
816
938
 
817
- def _get_choices(self) -> Any:
939
+ def _get_choices(self) -> CallableChoiceIterator | list[Any]:
818
940
  return self._choices
819
941
 
820
942
  def _set_choices(self, value: Any) -> None:
@@ -867,11 +989,21 @@ class TypedChoiceField(ChoiceField):
867
989
  *,
868
990
  coerce: Callable[[Any], Any] = lambda val: val,
869
991
  empty_value: Any = "",
870
- **kwargs: Any,
992
+ choices: Any = (),
993
+ required: bool = True,
994
+ initial: Any = None,
995
+ error_messages: dict[str, str] | None = None,
996
+ validators: Sequence[Callable[[Any], None]] = (),
871
997
  ) -> None:
872
998
  self.coerce = coerce
873
999
  self.empty_value = empty_value
874
- super().__init__(**kwargs)
1000
+ super().__init__(
1001
+ choices=choices,
1002
+ required=required,
1003
+ initial=initial,
1004
+ error_messages=error_messages,
1005
+ validators=validators,
1006
+ )
875
1007
 
876
1008
  def _coerce(self, value: Any) -> Any:
877
1009
  """
@@ -978,13 +1110,30 @@ class JSONField(CharField):
978
1110
  decoder: Any = None,
979
1111
  indent: int | None = None,
980
1112
  sort_keys: bool = False,
981
- **kwargs: Any,
1113
+ *,
1114
+ max_length: int | None = None,
1115
+ min_length: int | None = None,
1116
+ strip: bool = True,
1117
+ empty_value: str = "",
1118
+ required: bool = True,
1119
+ initial: Any = None,
1120
+ error_messages: dict[str, str] | None = None,
1121
+ validators: Sequence[Callable[[Any], None]] = (),
982
1122
  ) -> None:
983
1123
  self.encoder = encoder
984
1124
  self.decoder = decoder
985
1125
  self.indent = indent
986
1126
  self.sort_keys = sort_keys
987
- super().__init__(**kwargs)
1127
+ super().__init__(
1128
+ max_length=max_length,
1129
+ min_length=min_length,
1130
+ strip=strip,
1131
+ empty_value=empty_value,
1132
+ required=required,
1133
+ initial=initial,
1134
+ error_messages=error_messages,
1135
+ validators=validators,
1136
+ )
988
1137
 
989
1138
  def to_python(self, value: Any) -> Any:
990
1139
  if value in self.empty_values:
@@ -65,6 +65,9 @@ class BaseForm:
65
65
  class.
66
66
  """
67
67
 
68
+ # Set by DeclarativeFieldsMetaclass
69
+ base_fields: dict[str, Field]
70
+
68
71
  prefix: str | None = None
69
72
 
70
73
  def __init__(
@@ -139,10 +142,11 @@ class BaseForm:
139
142
  return self._bound_fields_cache[name]
140
143
 
141
144
  @property
142
- def errors(self) -> dict[str, list[str]] | None:
145
+ def errors(self) -> dict[str, list[str]]:
143
146
  """Return an error dict for the data provided for the form."""
144
147
  if self._errors is None:
145
148
  self.full_clean()
149
+ assert self._errors is not None, "full_clean should initialize _errors"
146
150
  return self._errors
147
151
 
148
152
  def is_valid(self) -> bool:
@@ -213,13 +217,16 @@ class BaseForm:
213
217
  yield next(iter(err))
214
218
 
215
219
  for field_key, error_list in error_dict.items():
220
+ # Accessing self.errors ensures _errors is initialized
216
221
  if field_key not in self.errors:
217
222
  if field_key != NON_FIELD_ERRORS and field_key not in self.fields:
218
223
  raise ValueError(
219
224
  f"'{self.__class__.__name__}' has no field named '{field_key}'."
220
225
  )
226
+ assert self._errors is not None, "errors property initializes _errors"
221
227
  self._errors[field_key] = ValidationErrors()
222
228
 
229
+ assert self._errors is not None, "errors property initializes _errors"
223
230
  self._errors[field_key].extend(error_list)
224
231
 
225
232
  # The field had an error, so removed it from the final data
@@ -424,7 +424,7 @@ class MultiPartParser:
424
424
  # (Maybe add handler.free_file to complement new_file)
425
425
  for handler in self._upload_handlers:
426
426
  if hasattr(handler, "file"):
427
- handler.file.close()
427
+ handler.file.close() # type: ignore[union-attr]
428
428
 
429
429
 
430
430
  class LazyStream:
@@ -503,7 +503,7 @@ class LazyStream:
503
503
  Replace the producer with an empty list. Any leftover bytes that have
504
504
  already been read will still be reported upon read() and/or next().
505
505
  """
506
- self._producer = []
506
+ self._producer = iter([])
507
507
 
508
508
  def __iter__(self) -> LazyStream:
509
509
  return self