plain 0.13.1__tar.gz → 0.13.2__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.13.2/.gitignore +13 -0
- {plain-0.13.1 → plain-0.13.2}/PKG-INFO +7 -12
- {plain-0.13.1 → plain-0.13.2}/plain/cli/README.md +2 -2
- {plain-0.13.1 → plain-0.13.2}/plain/cli/cli.py +3 -3
- {plain-0.13.1 → plain-0.13.2}/plain/cli/startup.py +2 -2
- {plain-0.13.1 → plain-0.13.2}/plain/csrf/middleware.py +1 -0
- {plain-0.13.1 → plain-0.13.2}/plain/exceptions.py +2 -1
- {plain-0.13.1 → plain-0.13.2}/plain/forms/forms.py +5 -5
- {plain-0.13.1 → plain-0.13.2}/plain/http/multipartparser.py +5 -5
- {plain-0.13.1 → plain-0.13.2}/plain/http/request.py +5 -5
- {plain-0.13.1 → plain-0.13.2}/plain/http/response.py +14 -14
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/locks.py +1 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/move.py +1 -2
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/uploadhandler.py +1 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/utils.py +3 -3
- {plain-0.13.1 → plain-0.13.2}/plain/internal/handlers/base.py +3 -7
- {plain-0.13.1 → plain-0.13.2}/plain/internal/handlers/exception.py +1 -3
- {plain-0.13.1 → plain-0.13.2}/plain/internal/handlers/wsgi.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/internal/middleware/slash.py +5 -8
- {plain-0.13.1 → plain-0.13.2}/plain/packages/config.py +9 -15
- {plain-0.13.1 → plain-0.13.2}/plain/packages/registry.py +12 -12
- {plain-0.13.1 → plain-0.13.2}/plain/paginator.py +1 -2
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/messages.py +3 -10
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/registry.py +2 -2
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/urls.py +4 -4
- {plain-0.13.1 → plain-0.13.2}/plain/runtime/global_settings.py +1 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signing.py +4 -4
- {plain-0.13.1 → plain-0.13.2}/plain/test/client.py +14 -13
- {plain-0.13.1 → plain-0.13.2}/plain/urls/base.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/urls/conf.py +2 -1
- {plain-0.13.1 → plain-0.13.2}/plain/urls/resolvers.py +20 -33
- {plain-0.13.1 → plain-0.13.2}/plain/utils/cache.py +1 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/crypto.py +2 -1
- {plain-0.13.1 → plain-0.13.2}/plain/utils/datastructures.py +2 -2
- {plain-0.13.1 → plain-0.13.2}/plain/utils/dateformat.py +13 -12
- {plain-0.13.1 → plain-0.13.2}/plain/utils/dateparse.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/utils/decorators.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/utils/html.py +7 -7
- {plain-0.13.1 → plain-0.13.2}/plain/utils/http.py +8 -8
- {plain-0.13.1 → plain-0.13.2}/plain/utils/ipv6.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/utils/module_loading.py +2 -2
- {plain-0.13.1 → plain-0.13.2}/plain/utils/regex_helper.py +7 -6
- {plain-0.13.1 → plain-0.13.2}/plain/utils/termcolors.py +4 -4
- {plain-0.13.1 → plain-0.13.2}/plain/utils/text.py +7 -7
- {plain-0.13.1 → plain-0.13.2}/plain/utils/timesince.py +1 -1
- {plain-0.13.1 → plain-0.13.2}/plain/utils/timezone.py +5 -5
- {plain-0.13.1 → plain-0.13.2}/plain/validators.py +1 -3
- {plain-0.13.1 → plain-0.13.2}/plain/views/forms.py +2 -2
- plain-0.13.2/pyproject.toml +26 -0
- plain-0.13.2/tests/.bolt/assets_collected/assets.json +1 -0
- plain-0.13.2/tests/.gitignore +3 -0
- plain-0.13.2/tests/app/.gitignore +1 -0
- plain-0.13.2/tests/app/settings.py +9 -0
- plain-0.13.2/tests/app/test/__init__.py +0 -0
- plain-0.13.2/tests/app/test/default_settings.py +4 -0
- plain-0.13.2/tests/app/urls.py +12 -0
- plain-0.13.2/tests/conftest.py +7 -0
- plain-0.13.2/tests/test_cli.py +11 -0
- plain-0.13.2/tests/test_runtime.py +18 -0
- plain-0.13.2/tests/test_wsgi.py +23 -0
- plain-0.13.2/uv.lock +148 -0
- plain-0.13.1/pyproject.toml +0 -35
- {plain-0.13.1 → plain-0.13.2}/LICENSE +0 -0
- {plain-0.13.1 → plain-0.13.2}/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/__main__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/compile.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/finders.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/fingerprints.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/urls.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/assets/views.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/cli/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/cli/formatting.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/cli/packages.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/cli/print.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/csrf/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/csrf/views.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/debug.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/forms/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/forms/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/forms/boundfield.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/forms/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/forms/fields.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/http/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/http/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/http/cookie.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/base.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/temp.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/middleware/headers.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/internal/middleware/https.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/json.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/logs/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/logs/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/logs/configure.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/logs/loggers.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/logs/utils.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/packages/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/packages/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/files.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/preflight/security.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/runtime/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/runtime/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/runtime/user_settings.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signals/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signals/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/core.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/environments.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/filters.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/templates/jinja/globals.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/test/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/test/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/urls/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/urls/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/urls/converters.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/urls/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/_os.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/connection.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/dates.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/deconstruct.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/deprecation.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/duration.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/email.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/encoding.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/functional.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/hashable.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/inspect.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/itercompat.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/safestring.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/utils/tree.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/README.md +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/__init__.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/base.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/csrf.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/errors.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/objects.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/redirect.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/views/templates.py +0 -0
- {plain-0.13.1 → plain-0.13.2}/plain/wsgi.py +0 -0
plain-0.13.2/.gitignore
ADDED
@@ -1,16 +1,12 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: plain
|
3
|
-
Version: 0.13.
|
3
|
+
Version: 0.13.2
|
4
4
|
Summary: A web framework for building products with Python.
|
5
|
-
Author: Dave Gaeddert
|
6
|
-
|
7
|
-
Requires-Python: >=3.11
|
8
|
-
|
9
|
-
|
10
|
-
Classifier: Programming Language :: Python :: 3.12
|
11
|
-
Classifier: Programming Language :: Python :: 3.13
|
12
|
-
Requires-Dist: click (>=8.0.0)
|
13
|
-
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
|
+
License-File: LICENSE
|
7
|
+
Requires-Python: >=3.11
|
8
|
+
Requires-Dist: click>=8.0.0
|
9
|
+
Requires-Dist: jinja2>=3.1.2
|
14
10
|
Description-Content-Type: text/markdown
|
15
11
|
|
16
12
|
<!-- This file is compiled from plain/plain/README.md. Do not edit this file directly. -->
|
@@ -48,4 +44,3 @@ With the official Plain ecosystem packages you can:
|
|
48
44
|
- Build [staff tooling and admin dashboards](https://plainframework.com/docs/plain-staff/)
|
49
45
|
|
50
46
|
Learn more at [plainframework.com](https://plainframework.com).
|
51
|
-
|
@@ -108,11 +108,11 @@ Some packages, like [plain-dev](https://plainframework.com/docs/plain-dev/),
|
|
108
108
|
never show up in `INSTALLED_PACKAGES` but still have CLI commands.
|
109
109
|
These are detected via Python entry points.
|
110
110
|
|
111
|
-
An example with `pyproject.toml` and
|
111
|
+
An example with `pyproject.toml` and UV:
|
112
112
|
|
113
113
|
```toml
|
114
114
|
# pyproject.toml
|
115
|
-
[
|
115
|
+
[project.entry-points."plain.cli"]
|
116
116
|
"dev" = "plain.dev:cli"
|
117
117
|
"pre-commit" = "plain.dev.precommit:cli"
|
118
118
|
"contrib" = "plain.dev.contribute:cli"
|
@@ -232,10 +232,10 @@ def preflight_checks(package_label, deploy, fail_level, databases):
|
|
232
232
|
if visible_issue_count == 0
|
233
233
|
else "1 issue"
|
234
234
|
if visible_issue_count == 1
|
235
|
-
else "
|
235
|
+
else f"{visible_issue_count} issues",
|
236
236
|
len(all_issues) - visible_issue_count,
|
237
237
|
)
|
238
|
-
msg = click.style("SystemCheckError:
|
238
|
+
msg = click.style(f"SystemCheckError: {header}", fg="red") + body + footer
|
239
239
|
raise click.ClickException(msg)
|
240
240
|
else:
|
241
241
|
if visible_issue_count:
|
@@ -245,7 +245,7 @@ def preflight_checks(package_label, deploy, fail_level, databases):
|
|
245
245
|
if visible_issue_count == 0
|
246
246
|
else "1 issue"
|
247
247
|
if visible_issue_count == 1
|
248
|
-
else "
|
248
|
+
else f"{visible_issue_count} issues",
|
249
249
|
len(all_issues) - visible_issue_count,
|
250
250
|
)
|
251
251
|
msg = header + body + footer
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Global Plain exception and warning classes.
|
3
3
|
"""
|
4
|
+
|
4
5
|
import operator
|
5
6
|
|
6
7
|
from plain.utils.hashable import make_hashable
|
@@ -209,7 +210,7 @@ class ValidationError(Exception):
|
|
209
210
|
return repr(list(self))
|
210
211
|
|
211
212
|
def __repr__(self):
|
212
|
-
return "ValidationError(
|
213
|
+
return f"ValidationError({self})"
|
213
214
|
|
214
215
|
def __eq__(self, other):
|
215
216
|
if not isinstance(other, ValidationError):
|
@@ -172,7 +172,7 @@ class BaseForm:
|
|
172
172
|
if not isinstance(error, ValidationError):
|
173
173
|
raise TypeError(
|
174
174
|
"The argument `error` must be an instance of "
|
175
|
-
"`ValidationError`, not
|
175
|
+
f"`ValidationError`, not `{type(error).__name__}`."
|
176
176
|
)
|
177
177
|
|
178
178
|
if hasattr(error, "error_dict"):
|
@@ -221,9 +221,9 @@ class BaseForm:
|
|
221
221
|
self._post_clean()
|
222
222
|
|
223
223
|
def _field_data_value(self, field, html_name):
|
224
|
-
if hasattr(self, "parse_
|
224
|
+
if hasattr(self, f"parse_{html_name}"):
|
225
225
|
# Allow custom parsing from form data/files at the form level
|
226
|
-
return getattr(self, "parse_
|
226
|
+
return getattr(self, f"parse_{html_name}")()
|
227
227
|
|
228
228
|
return field.value_from_form_data(self.data, self.files, html_name)
|
229
229
|
|
@@ -242,8 +242,8 @@ class BaseForm:
|
|
242
242
|
else:
|
243
243
|
value = field.clean(value)
|
244
244
|
self.cleaned_data[name] = value
|
245
|
-
if hasattr(self, "clean_
|
246
|
-
value = getattr(self, "clean_
|
245
|
+
if hasattr(self, f"clean_{name}"):
|
246
|
+
value = getattr(self, f"clean_{name}")()
|
247
247
|
self.cleaned_data[name] = value
|
248
248
|
except ValidationError as e:
|
249
249
|
self.add_error(name, e)
|
@@ -4,6 +4,7 @@ Multi-part parsing for file uploads.
|
|
4
4
|
Exposes one class, ``MultiPartParser``, which feeds chunks of uploaded data to
|
5
5
|
file upload handlers for processing.
|
6
6
|
"""
|
7
|
+
|
7
8
|
import base64
|
8
9
|
import binascii
|
9
10
|
import collections
|
@@ -70,14 +71,13 @@ class MultiPartParser:
|
|
70
71
|
# Content-Type should contain multipart and the boundary information.
|
71
72
|
content_type = META.get("CONTENT_TYPE", "")
|
72
73
|
if not content_type.startswith("multipart/"):
|
73
|
-
raise MultiPartParserError("Invalid Content-Type:
|
74
|
+
raise MultiPartParserError(f"Invalid Content-Type: {content_type}")
|
74
75
|
|
75
76
|
try:
|
76
77
|
content_type.encode("ascii")
|
77
78
|
except UnicodeEncodeError:
|
78
79
|
raise MultiPartParserError(
|
79
|
-
"Invalid non-ASCII Content-Type in multipart:
|
80
|
-
% force_str(content_type)
|
80
|
+
f"Invalid non-ASCII Content-Type in multipart: {force_str(content_type)}"
|
81
81
|
)
|
82
82
|
|
83
83
|
# Parse the header to get the boundary to split the parts.
|
@@ -85,7 +85,7 @@ class MultiPartParser:
|
|
85
85
|
boundary = opts.get("boundary")
|
86
86
|
if not boundary or not self.boundary_re.fullmatch(boundary):
|
87
87
|
raise MultiPartParserError(
|
88
|
-
"Invalid boundary in multipart:
|
88
|
+
f"Invalid boundary in multipart: {force_str(boundary)}"
|
89
89
|
)
|
90
90
|
|
91
91
|
# Content-Length should contain the length of the body we are about
|
@@ -97,7 +97,7 @@ class MultiPartParser:
|
|
97
97
|
|
98
98
|
if content_length < 0:
|
99
99
|
# This means we shouldn't continue...raise an error.
|
100
|
-
raise MultiPartParserError("Invalid content length:
|
100
|
+
raise MultiPartParserError(f"Invalid content length: {content_length!r}")
|
101
101
|
|
102
102
|
self._boundary = boundary.encode("ascii")
|
103
103
|
self._input_data = input_data
|
@@ -81,7 +81,7 @@ class HttpRequest:
|
|
81
81
|
|
82
82
|
def __repr__(self):
|
83
83
|
if self.method is None or not self.get_full_path():
|
84
|
-
return "
|
84
|
+
return f"<{self.__class__.__name__}>"
|
85
85
|
return f"<{self.__class__.__name__}: {self.method} {self.get_full_path()!r}>"
|
86
86
|
|
87
87
|
def __getstate__(self):
|
@@ -157,9 +157,9 @@ class HttpRequest:
|
|
157
157
|
if domain and validate_host(domain, allowed_hosts):
|
158
158
|
return host
|
159
159
|
else:
|
160
|
-
msg = "Invalid HTTP_HOST header:
|
160
|
+
msg = f"Invalid HTTP_HOST header: {host!r}."
|
161
161
|
if domain:
|
162
|
-
msg += " You may need to add
|
162
|
+
msg += f" You may need to add {domain!r} to ALLOWED_HOSTS."
|
163
163
|
else:
|
164
164
|
msg += (
|
165
165
|
" The domain name provided is not valid according to RFC 1034/1035."
|
@@ -227,7 +227,7 @@ class HttpRequest:
|
|
227
227
|
if location is None:
|
228
228
|
# Make it an absolute url (but schemeless and domainless) for the
|
229
229
|
# edge case that the path starts with '//'.
|
230
|
-
location = "
|
230
|
+
location = f"//{self.get_full_path()}"
|
231
231
|
else:
|
232
232
|
# Coerce lazy locations.
|
233
233
|
location = str(location)
|
@@ -671,7 +671,7 @@ class MediaType:
|
|
671
671
|
params_str = "".join(f"; {k}={v}" for k, v in self.params.items())
|
672
672
|
return "{}{}{}".format(
|
673
673
|
self.main_type,
|
674
|
-
("
|
674
|
+
(f"/{self.sub_type}") if self.sub_type else "",
|
675
675
|
params_str,
|
676
676
|
)
|
677
677
|
|
@@ -72,7 +72,7 @@ class ResponseHeaders(CaseInsensitiveMapping):
|
|
72
72
|
if mime_encode:
|
73
73
|
value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
|
74
74
|
else:
|
75
|
-
e.reason += ", HTTP response headers must be in
|
75
|
+
e.reason += f", HTTP response headers must be in {charset} format"
|
76
76
|
raise
|
77
77
|
return value
|
78
78
|
|
@@ -181,7 +181,7 @@ class ResponseBase:
|
|
181
181
|
@property
|
182
182
|
def _content_type_for_repr(self):
|
183
183
|
return (
|
184
|
-
', "
|
184
|
+
', "{}"'.format(self.headers["Content-Type"])
|
185
185
|
if "Content-Type" in self.headers
|
186
186
|
else ""
|
187
187
|
)
|
@@ -236,8 +236,8 @@ class ResponseBase:
|
|
236
236
|
if expires is not None:
|
237
237
|
if isinstance(expires, datetime.datetime):
|
238
238
|
if timezone.is_naive(expires):
|
239
|
-
expires = timezone.make_aware(expires, datetime.
|
240
|
-
delta = expires - datetime.datetime.now(tz=datetime.
|
239
|
+
expires = timezone.make_aware(expires, datetime.UTC)
|
240
|
+
delta = expires - datetime.datetime.now(tz=datetime.UTC)
|
241
241
|
# Add one second so the date matches exactly (a fraction of
|
242
242
|
# time gets lost between converting to a timedelta and
|
243
243
|
# then the date string).
|
@@ -332,14 +332,14 @@ class ResponseBase:
|
|
332
332
|
signals.request_finished.send(sender=self._handler_class)
|
333
333
|
|
334
334
|
def write(self, content):
|
335
|
-
raise OSError("This
|
335
|
+
raise OSError(f"This {self.__class__.__name__} instance is not writable")
|
336
336
|
|
337
337
|
def flush(self):
|
338
338
|
pass
|
339
339
|
|
340
340
|
def tell(self):
|
341
341
|
raise OSError(
|
342
|
-
"This
|
342
|
+
f"This {self.__class__.__name__} instance cannot tell its position"
|
343
343
|
)
|
344
344
|
|
345
345
|
# These methods partially implement a stream-like object interface.
|
@@ -355,7 +355,7 @@ class ResponseBase:
|
|
355
355
|
return False
|
356
356
|
|
357
357
|
def writelines(self, lines):
|
358
|
-
raise OSError("This
|
358
|
+
raise OSError(f"This {self.__class__.__name__} instance is not writable")
|
359
359
|
|
360
360
|
|
361
361
|
class Response(ResponseBase):
|
@@ -390,7 +390,7 @@ class Response(ResponseBase):
|
|
390
390
|
return obj_dict
|
391
391
|
|
392
392
|
def __repr__(self):
|
393
|
-
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
|
393
|
+
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
394
394
|
"cls": self.__class__.__name__,
|
395
395
|
"status_code": self.status_code,
|
396
396
|
"content_type": self._content_type_for_repr,
|
@@ -461,7 +461,7 @@ class StreamingResponse(ResponseBase):
|
|
461
461
|
self.streaming_content = streaming_content
|
462
462
|
|
463
463
|
def __repr__(self):
|
464
|
-
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
|
464
|
+
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
465
465
|
"cls": self.__class__.__qualname__,
|
466
466
|
"status_code": self.status_code,
|
467
467
|
"content_type": self._content_type_for_repr,
|
@@ -470,8 +470,8 @@ class StreamingResponse(ResponseBase):
|
|
470
470
|
@property
|
471
471
|
def content(self):
|
472
472
|
raise AttributeError(
|
473
|
-
"This
|
474
|
-
"`streaming_content` instead."
|
473
|
+
f"This {self.__class__.__name__} instance has no `content` attribute. Use "
|
474
|
+
"`streaming_content` instead."
|
475
475
|
)
|
476
476
|
|
477
477
|
@property
|
@@ -586,14 +586,14 @@ class ResponseRedirectBase(Response):
|
|
586
586
|
parsed = urlparse(str(redirect_to))
|
587
587
|
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
|
588
588
|
raise DisallowedRedirect(
|
589
|
-
"Unsafe redirect to URL with protocol '
|
589
|
+
f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
|
590
590
|
)
|
591
591
|
|
592
592
|
url = property(lambda self: self["Location"])
|
593
593
|
|
594
594
|
def __repr__(self):
|
595
595
|
return (
|
596
|
-
'<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
|
596
|
+
'<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">' # noqa: UP031
|
597
597
|
% {
|
598
598
|
"cls": self.__class__.__name__,
|
599
599
|
"status_code": self.status_code,
|
@@ -661,7 +661,7 @@ class ResponseNotAllowed(Response):
|
|
661
661
|
self["Allow"] = ", ".join(permitted_methods)
|
662
662
|
|
663
663
|
def __repr__(self):
|
664
|
-
return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
|
664
|
+
return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
665
665
|
"cls": self.__class__.__name__,
|
666
666
|
"status_code": self.status_code,
|
667
667
|
"content_type": self._content_type_for_repr,
|
@@ -46,8 +46,7 @@ def file_move_safe(
|
|
46
46
|
try:
|
47
47
|
if not allow_overwrite and os.access(new_file_name, os.F_OK):
|
48
48
|
raise FileExistsError(
|
49
|
-
"Destination file
|
50
|
-
% new_file_name
|
49
|
+
f"Destination file {new_file_name} exists and allow_overwrite is False."
|
51
50
|
)
|
52
51
|
|
53
52
|
os.rename(old_file_name, new_file_name)
|
@@ -7,7 +7,7 @@ from plain.exceptions import SuspiciousFileOperation
|
|
7
7
|
def validate_file_name(name, allow_relative_path=False):
|
8
8
|
# Remove potentially dangerous names
|
9
9
|
if os.path.basename(name) in {"", ".", ".."}:
|
10
|
-
raise SuspiciousFileOperation("Could not derive file name from '
|
10
|
+
raise SuspiciousFileOperation(f"Could not derive file name from '{name}'")
|
11
11
|
|
12
12
|
if allow_relative_path:
|
13
13
|
# Use PurePosixPath() because this branch is checked only in
|
@@ -16,10 +16,10 @@ def validate_file_name(name, allow_relative_path=False):
|
|
16
16
|
path = pathlib.PurePosixPath(name)
|
17
17
|
if path.is_absolute() or ".." in path.parts:
|
18
18
|
raise SuspiciousFileOperation(
|
19
|
-
"Detected path traversal attempt in '
|
19
|
+
f"Detected path traversal attempt in '{name}'"
|
20
20
|
)
|
21
21
|
elif name != os.path.basename(name):
|
22
|
-
raise SuspiciousFileOperation("File name '
|
22
|
+
raise SuspiciousFileOperation(f"File name '{name}' includes path elements")
|
23
23
|
|
24
24
|
return name
|
25
25
|
|
@@ -45,7 +45,7 @@ class BaseHandler:
|
|
45
45
|
|
46
46
|
if mw_instance is None:
|
47
47
|
raise ImproperlyConfigured(
|
48
|
-
"Middleware factory
|
48
|
+
f"Middleware factory {middleware_path} returned None."
|
49
49
|
)
|
50
50
|
|
51
51
|
if hasattr(mw_instance, "process_view"):
|
@@ -126,14 +126,10 @@ class BaseHandler:
|
|
126
126
|
if isinstance(callback, types.FunctionType): # FBV
|
127
127
|
name = f"The view {callback.__module__}.{callback.__name__}"
|
128
128
|
else: # CBV
|
129
|
-
name = "The view {}.{}.__call__"
|
130
|
-
callback.__module__,
|
131
|
-
callback.__class__.__name__,
|
132
|
-
)
|
129
|
+
name = f"The view {callback.__module__}.{callback.__class__.__name__}.__call__"
|
133
130
|
if response is None:
|
134
131
|
raise ValueError(
|
135
|
-
"
|
136
|
-
"instead." % name
|
132
|
+
f"{name} didn't return a Response object. It returned None " "instead."
|
137
133
|
)
|
138
134
|
|
139
135
|
|
@@ -85,9 +85,7 @@ def response_for_exception(request, exc):
|
|
85
85
|
|
86
86
|
# The request logger receives events for any problematic request
|
87
87
|
# The security logger receives events for all SuspiciousOperations
|
88
|
-
security_logger = logging.getLogger(
|
89
|
-
"plain.security.%s" % exc.__class__.__name__
|
90
|
-
)
|
88
|
+
security_logger = logging.getLogger(f"plain.security.{exc.__class__.__name__}")
|
91
89
|
security_logger.error(
|
92
90
|
str(exc),
|
93
91
|
exc_info=exc,
|
@@ -138,7 +138,7 @@ class WSGIHandler(base.BaseHandler):
|
|
138
138
|
|
139
139
|
response._handler_class = self.__class__
|
140
140
|
|
141
|
-
status = "%d %s" % (response.status_code, response.reason_phrase)
|
141
|
+
status = "%d %s" % (response.status_code, response.reason_phrase) # noqa: UP031
|
142
142
|
response_headers = [
|
143
143
|
*response.items(),
|
144
144
|
*(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
|
@@ -34,7 +34,7 @@ class RedirectSlashMiddleware:
|
|
34
34
|
if settings.APPEND_SLASH and not request.path_info.endswith("/"):
|
35
35
|
urlconf = getattr(request, "urlconf", None)
|
36
36
|
if not is_valid_path(request.path_info, urlconf):
|
37
|
-
match = is_valid_path("
|
37
|
+
match = is_valid_path(f"{request.path_info}/", urlconf)
|
38
38
|
if match:
|
39
39
|
view = match.func
|
40
40
|
return getattr(view, "should_append_slash", True)
|
@@ -52,13 +52,10 @@ class RedirectSlashMiddleware:
|
|
52
52
|
new_path = escape_leading_slashes(new_path)
|
53
53
|
if settings.DEBUG and request.method in ("POST", "PUT", "PATCH"):
|
54
54
|
raise RuntimeError(
|
55
|
-
"You called this URL via {method}, but the URL doesn't end "
|
55
|
+
f"You called this URL via {request.method}, but the URL doesn't end "
|
56
56
|
"in a slash and you have APPEND_SLASH set. Plain can't "
|
57
|
-
"redirect to the slash URL while maintaining {method} data. "
|
58
|
-
"Change your form to point to {
|
59
|
-
"slash), or set APPEND_SLASH=False in your Plain settings."
|
60
|
-
method=request.method,
|
61
|
-
url=request.get_host() + new_path,
|
62
|
-
)
|
57
|
+
f"redirect to the slash URL while maintaining {request.method} data. "
|
58
|
+
f"Change your form to point to {request.get_host() + new_path} (note the trailing "
|
59
|
+
"slash), or set APPEND_SLASH=False in your Plain settings."
|
63
60
|
)
|
64
61
|
return new_path
|
@@ -35,7 +35,7 @@ class PackageConfig:
|
|
35
35
|
self.label = package_name.rpartition(".")[2]
|
36
36
|
if not self.label.isidentifier():
|
37
37
|
raise ImproperlyConfigured(
|
38
|
-
"The app label '
|
38
|
+
f"The app label '{self.label}' is not a valid Python identifier."
|
39
39
|
)
|
40
40
|
|
41
41
|
# Filesystem path to the application directory e.g.
|
@@ -71,15 +71,15 @@ class PackageConfig:
|
|
71
71
|
paths = list(set(paths))
|
72
72
|
if len(paths) > 1:
|
73
73
|
raise ImproperlyConfigured(
|
74
|
-
"The app module {!r} has multiple filesystem locations ({!r}); "
|
74
|
+
f"The app module {module!r} has multiple filesystem locations ({paths!r}); "
|
75
75
|
"you must configure this app with an PackageConfig subclass "
|
76
|
-
"with a 'path' class attribute."
|
76
|
+
"with a 'path' class attribute."
|
77
77
|
)
|
78
78
|
elif not paths:
|
79
79
|
raise ImproperlyConfigured(
|
80
|
-
"The app module
|
80
|
+
f"The app module {module!r} has no filesystem location, "
|
81
81
|
"you must configure this app with an PackageConfig subclass "
|
82
|
-
"with a 'path' class attribute."
|
82
|
+
"with a 'path' class attribute."
|
83
83
|
)
|
84
84
|
return paths[0]
|
85
85
|
|
@@ -170,7 +170,7 @@ class PackageConfig:
|
|
170
170
|
]
|
171
171
|
msg = f"Module '{mod_path}' does not contain a '{cls_name}' class."
|
172
172
|
if candidates:
|
173
|
-
msg += " Choices are:
|
173
|
+
msg += " Choices are: {}.".format(", ".join(candidates))
|
174
174
|
raise ImportError(msg)
|
175
175
|
else:
|
176
176
|
# Re-trigger the module import exception.
|
@@ -179,9 +179,7 @@ class PackageConfig:
|
|
179
179
|
# Check for obvious errors. (This check prevents duck typing, but
|
180
180
|
# it could be removed if it became a problem in practice.)
|
181
181
|
if not issubclass(package_config_class, PackageConfig):
|
182
|
-
raise ImproperlyConfigured(
|
183
|
-
"'%s' isn't a subclass of PackageConfig." % entry
|
184
|
-
)
|
182
|
+
raise ImproperlyConfigured(f"'{entry}' isn't a subclass of PackageConfig.")
|
185
183
|
|
186
184
|
# Obtain package name here rather than in PackageClass.__init__ to keep
|
187
185
|
# all error checking for entries in INSTALLED_PACKAGES in one place.
|
@@ -189,18 +187,14 @@ class PackageConfig:
|
|
189
187
|
try:
|
190
188
|
package_name = package_config_class.name
|
191
189
|
except AttributeError:
|
192
|
-
raise ImproperlyConfigured("'
|
190
|
+
raise ImproperlyConfigured(f"'{entry}' must supply a name attribute.")
|
193
191
|
|
194
192
|
# Ensure package_name points to a valid module.
|
195
193
|
try:
|
196
194
|
package_module = import_module(package_name)
|
197
195
|
except ImportError:
|
198
196
|
raise ImproperlyConfigured(
|
199
|
-
"Cannot import '{}'. Check that '{}.{}.name' is correct."
|
200
|
-
package_name,
|
201
|
-
package_config_class.__module__,
|
202
|
-
package_config_class.__qualname__,
|
203
|
-
)
|
197
|
+
f"Cannot import '{package_name}'. Check that '{package_config_class.__module__}.{package_config_class.__qualname__}.name' is correct."
|
204
198
|
)
|
205
199
|
|
206
200
|
# Entry is a path to an app config class.
|
@@ -90,7 +90,7 @@ class Packages:
|
|
90
90
|
if package_config.label in self.package_configs:
|
91
91
|
raise ImproperlyConfigured(
|
92
92
|
"Package labels aren't unique, "
|
93
|
-
"duplicates:
|
93
|
+
f"duplicates: {package_config.label}"
|
94
94
|
)
|
95
95
|
|
96
96
|
self.package_configs[package_config.label] = package_config
|
@@ -103,8 +103,9 @@ class Packages:
|
|
103
103
|
duplicates = [name for name, count in counts.most_common() if count > 1]
|
104
104
|
if duplicates:
|
105
105
|
raise ImproperlyConfigured(
|
106
|
-
"Package names aren't unique, "
|
107
|
-
|
106
|
+
"Package names aren't unique, " "duplicates: {}".format(
|
107
|
+
", ".join(duplicates)
|
108
|
+
)
|
108
109
|
)
|
109
110
|
|
110
111
|
self.packages_ready = True
|
@@ -154,10 +155,10 @@ class Packages:
|
|
154
155
|
try:
|
155
156
|
return self.package_configs[package_label]
|
156
157
|
except KeyError:
|
157
|
-
message = "No installed app with label '
|
158
|
+
message = f"No installed app with label '{package_label}'."
|
158
159
|
for package_config in self.get_package_configs():
|
159
160
|
if package_config.name == package_label:
|
160
|
-
message += " Did you mean '
|
161
|
+
message += f" Did you mean '{package_config.label}'?"
|
161
162
|
break
|
162
163
|
raise LookupError(message)
|
163
164
|
|
@@ -223,17 +224,15 @@ class Packages:
|
|
223
224
|
and model.__module__ == app_models[model_name].__module__
|
224
225
|
):
|
225
226
|
warnings.warn(
|
226
|
-
"Model '{}.{}' was already registered. Reloading models is not "
|
227
|
+
f"Model '{package_label}.{model_name}' was already registered. Reloading models is not "
|
227
228
|
"advised as it can lead to inconsistencies, most notably with "
|
228
|
-
"related models."
|
229
|
+
"related models.",
|
229
230
|
RuntimeWarning,
|
230
231
|
stacklevel=2,
|
231
232
|
)
|
232
233
|
else:
|
233
234
|
raise RuntimeError(
|
234
|
-
"Conflicting '{}' models in application '{}': {} and {}."
|
235
|
-
model_name, package_label, app_models[model_name], model
|
236
|
-
)
|
235
|
+
f"Conflicting '{model_name}' models in application '{package_label}': {app_models[model_name]} and {model}."
|
237
236
|
)
|
238
237
|
app_models[model_name] = model
|
239
238
|
self.do_pending_operations(model)
|
@@ -321,8 +320,9 @@ class Packages:
|
|
321
320
|
}
|
322
321
|
if not available.issubset(installed):
|
323
322
|
raise ValueError(
|
324
|
-
"Available packages isn't a subset of installed packages, extra packages:
|
325
|
-
|
323
|
+
"Available packages isn't a subset of installed packages, extra packages: {}".format(
|
324
|
+
", ".join(available - installed)
|
325
|
+
)
|
326
326
|
)
|
327
327
|
|
328
328
|
self.stored_package_configs.append(self.package_configs)
|
@@ -142,8 +142,7 @@ class Page(collections.abc.Sequence):
|
|
142
142
|
def __getitem__(self, index):
|
143
143
|
if not isinstance(index, int | slice):
|
144
144
|
raise TypeError(
|
145
|
-
"Page indices must be integers or slices, not
|
146
|
-
% type(index).__name__
|
145
|
+
f"Page indices must be integers or slices, not {type(index).__name__}."
|
147
146
|
)
|
148
147
|
# The object_list is converted to a list so that if it was a QuerySet
|
149
148
|
# it won't be a database hit per __getitem__.
|
@@ -40,19 +40,12 @@ class CheckMessage:
|
|
40
40
|
obj = self.obj._meta.label
|
41
41
|
else:
|
42
42
|
obj = str(self.obj)
|
43
|
-
id = "(
|
44
|
-
hint = "\n\tHINT:
|
43
|
+
id = f"({self.id}) " if self.id else ""
|
44
|
+
hint = f"\n\tHINT: {self.hint}" if self.hint else ""
|
45
45
|
return f"{obj}: {id}{self.msg}{hint}"
|
46
46
|
|
47
47
|
def __repr__(self):
|
48
|
-
return "<{}: level={!r}, msg={!r}, hint={!r}, obj={!r}, id={!r}>"
|
49
|
-
self.__class__.__name__,
|
50
|
-
self.level,
|
51
|
-
self.msg,
|
52
|
-
self.hint,
|
53
|
-
self.obj,
|
54
|
-
self.id,
|
55
|
-
)
|
48
|
+
return f"<{self.__class__.__name__}: level={self.level!r}, msg={self.msg!r}, hint={self.hint!r}, obj={self.obj!r}, id={self.id!r}>"
|
56
49
|
|
57
50
|
def is_serious(self, level=ERROR):
|
58
51
|
return self.level >= level
|