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.
- {plain-0.86.2 → plain-0.88.0}/PKG-INFO +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/CHANGELOG.md +21 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/views.py +7 -2
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/request.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/cli/preflight.py +3 -2
- {plain-0.86.2 → plain-0.88.0}/plain/exceptions.py +2 -2
- plain-0.88.0/plain/forms/__init__.py +59 -0
- {plain-0.86.2 → plain-0.88.0}/plain/forms/boundfield.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/forms/fields.py +192 -43
- {plain-0.86.2 → plain-0.88.0}/plain/forms/forms.py +8 -1
- {plain-0.86.2 → plain-0.88.0}/plain/http/multipartparser.py +2 -2
- {plain-0.86.2 → plain-0.88.0}/plain/http/request.py +9 -6
- {plain-0.86.2 → plain-0.88.0}/plain/http/response.py +6 -6
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/base.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/temp.py +2 -1
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/uploadedfile.py +10 -7
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/uploadhandler.py +13 -8
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/utils.py +4 -1
- {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/base.py +4 -1
- {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/exception.py +4 -4
- {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/wsgi.py +5 -3
- {plain-0.86.2 → plain-0.88.0}/plain/logs/formatters.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/results.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/server/arbiter.py +9 -9
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/body.py +2 -2
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/errors.py +7 -2
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/message.py +3 -2
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/parser.py +3 -2
- {plain-0.86.2 → plain-0.88.0}/plain/server/sock.py +19 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/workers/base.py +11 -9
- {plain-0.86.2 → plain-0.88.0}/plain/server/workers/sync.py +5 -3
- {plain-0.86.2 → plain-0.88.0}/plain/server/workers/thread.py +8 -7
- {plain-0.86.2 → plain-0.88.0}/plain/test/client.py +8 -6
- {plain-0.86.2 → plain-0.88.0}/plain/test/exceptions.py +2 -5
- {plain-0.86.2 → plain-0.88.0}/plain/urls/patterns.py +4 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/resolvers.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/utils/cache.py +5 -5
- {plain-0.86.2 → plain-0.88.0}/plain/utils/datastructures.py +4 -2
- {plain-0.86.2 → plain-0.88.0}/plain/utils/functional.py +2 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/regex_helper.py +2 -2
- {plain-0.86.2 → plain-0.88.0}/plain/utils/text.py +2 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/tree.py +8 -5
- {plain-0.86.2 → plain-0.88.0}/plain/validators.py +24 -9
- {plain-0.86.2 → plain-0.88.0}/plain/views/base.py +2 -2
- {plain-0.86.2 → plain-0.88.0}/plain/views/errors.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/views/forms.py +1 -1
- {plain-0.86.2 → plain-0.88.0}/plain/views/objects.py +17 -20
- {plain-0.86.2 → plain-0.88.0}/pyproject.toml +1 -1
- {plain-0.86.2 → plain-0.88.0}/tests/test_csrf.py +2 -1
- plain-0.86.2/plain/forms/__init__.py +0 -8
- {plain-0.86.2 → plain-0.88.0}/.gitignore +0 -0
- {plain-0.86.2 → plain-0.88.0}/LICENSE +0 -0
- {plain-0.86.2 → plain-0.88.0}/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/AGENTS.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/__main__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/compile.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/finders.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/assets/urls.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/chores/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/chores/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/chores/core.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/chores/registry.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/docs.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/md.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/build.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/changelog.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/chores.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/core.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/docs.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/formatting.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/install.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/output.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/print.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/registry.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/runtime.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/scaffold.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/server.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/settings.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/shell.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/startup.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/upgrade.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/urls.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/cli/utils.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/csrf/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/csrf/middleware.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/debug.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/forms/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/forms/exceptions.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/http/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/http/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/http/cookie.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/http/middleware.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/locks.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/files/move.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/headers.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/hosts.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/internal/reloader.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/json.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/app.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/configure.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/debug.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/logs/filters.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/packages/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/packages/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/packages/config.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/packages/registry.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/paginator.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/checks.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/files.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/registry.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/security.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/preflight/urls.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/runtime/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/runtime/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/runtime/global_settings.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/runtime/utils.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/LICENSE +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/app.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/config.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/errors.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/glogging.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/unreader.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/http/wsgi.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/pidfile.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/util.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signals/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signals/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/signing.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/core.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/test/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/test/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/test/encoding.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/converters.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/exceptions.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/routers.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/urls/utils.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/crypto.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/dateparse.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/decorators.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/duration.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/encoding.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/hashable.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/html.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/http.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/inspect.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/ipv6.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/itercompat.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/module_loading.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/safestring.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/timesince.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/utils/timezone.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/views/README.md +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/views/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/views/exceptions.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/views/redirect.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/views/templates.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/plain/wsgi.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/.gitignore +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/app/.gitignore +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/app/settings.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/app/test/__init__.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/app/urls.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/conftest.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/test_cli.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/test_http_hosts.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/test_logs.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/test_runtime.py +0 -0
- {plain-0.86.2 → plain-0.88.0}/tests/test_wsgi.py +0 -0
|
@@ -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:
|
|
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
|
|
@@ -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
|
|
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(
|
|
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:
|
|
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]) ->
|
|
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
|
-
|
|
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__(
|
|
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(
|
|
209
|
+
self.validators.append(validators_.MinLengthValidator(int(min_length)))
|
|
202
210
|
if max_length is not None:
|
|
203
|
-
self.validators.append(
|
|
204
|
-
self.validators.append(
|
|
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
|
-
|
|
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__(
|
|
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(
|
|
251
|
+
self.validators.append(validators_.MaxValueValidator(max_value))
|
|
236
252
|
if min_value is not None:
|
|
237
|
-
self.validators.append(
|
|
253
|
+
self.validators.append(validators_.MinValueValidator(min_value))
|
|
238
254
|
if step_size is not None:
|
|
239
|
-
self.validators.append(
|
|
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
|
-
|
|
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__(
|
|
300
|
-
|
|
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__(
|
|
376
|
-
|
|
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__(
|
|
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
|
-
|
|
514
|
-
|
|
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 =
|
|
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 = [
|
|
595
|
+
default_validators = [validators_.validate_email]
|
|
537
596
|
|
|
538
|
-
def __init__(
|
|
539
|
-
|
|
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
|
-
|
|
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__(
|
|
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 = [
|
|
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 = [
|
|
764
|
+
default_validators = [validators_.URLValidator()]
|
|
678
765
|
|
|
679
|
-
def __init__(
|
|
680
|
-
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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__(
|
|
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]]
|
|
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
|