plain 0.13.1__tar.gz → 0.14.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.14.0/.gitignore +13 -0
- {plain-0.13.1 → plain-0.14.0}/PKG-INFO +7 -12
- {plain-0.13.1 → plain-0.14.0}/plain/cli/README.md +2 -2
- {plain-0.13.1 → plain-0.14.0}/plain/cli/cli.py +7 -11
- {plain-0.13.1 → plain-0.14.0}/plain/cli/packages.py +7 -3
- {plain-0.13.1 → plain-0.14.0}/plain/cli/startup.py +2 -2
- {plain-0.13.1 → plain-0.14.0}/plain/csrf/middleware.py +1 -0
- {plain-0.13.1 → plain-0.14.0}/plain/exceptions.py +2 -1
- {plain-0.13.1 → plain-0.14.0}/plain/forms/forms.py +5 -5
- {plain-0.13.1 → plain-0.14.0}/plain/http/multipartparser.py +5 -5
- {plain-0.13.1 → plain-0.14.0}/plain/http/request.py +5 -5
- {plain-0.13.1 → plain-0.14.0}/plain/http/response.py +19 -14
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/locks.py +1 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/move.py +1 -2
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/uploadhandler.py +1 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/utils.py +3 -3
- {plain-0.13.1 → plain-0.14.0}/plain/internal/handlers/base.py +3 -7
- {plain-0.13.1 → plain-0.14.0}/plain/internal/handlers/exception.py +1 -3
- {plain-0.13.1 → plain-0.14.0}/plain/internal/handlers/wsgi.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/internal/middleware/slash.py +5 -8
- {plain-0.13.1 → plain-0.14.0}/plain/packages/config.py +9 -15
- {plain-0.13.1 → plain-0.14.0}/plain/packages/registry.py +12 -12
- {plain-0.13.1 → plain-0.14.0}/plain/paginator.py +1 -2
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/messages.py +3 -10
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/registry.py +2 -2
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/urls.py +4 -4
- {plain-0.13.1 → plain-0.14.0}/plain/runtime/global_settings.py +1 -0
- {plain-0.13.1 → plain-0.14.0}/plain/runtime/user_settings.py +6 -6
- {plain-0.13.1 → plain-0.14.0}/plain/signing.py +4 -4
- {plain-0.13.1 → plain-0.14.0}/plain/test/client.py +22 -21
- {plain-0.13.1 → plain-0.14.0}/plain/urls/base.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/urls/conf.py +2 -1
- {plain-0.13.1 → plain-0.14.0}/plain/urls/resolvers.py +20 -33
- {plain-0.13.1 → plain-0.14.0}/plain/utils/cache.py +1 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/crypto.py +2 -1
- {plain-0.13.1 → plain-0.14.0}/plain/utils/datastructures.py +2 -2
- {plain-0.13.1 → plain-0.14.0}/plain/utils/dateformat.py +13 -12
- {plain-0.13.1 → plain-0.14.0}/plain/utils/dateparse.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/utils/decorators.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/utils/html.py +7 -7
- {plain-0.13.1 → plain-0.14.0}/plain/utils/http.py +8 -8
- {plain-0.13.1 → plain-0.14.0}/plain/utils/ipv6.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/utils/module_loading.py +2 -2
- {plain-0.13.1 → plain-0.14.0}/plain/utils/regex_helper.py +7 -6
- {plain-0.13.1 → plain-0.14.0}/plain/utils/text.py +7 -7
- {plain-0.13.1 → plain-0.14.0}/plain/utils/timesince.py +1 -1
- {plain-0.13.1 → plain-0.14.0}/plain/utils/timezone.py +5 -5
- {plain-0.13.1 → plain-0.14.0}/plain/validators.py +1 -3
- {plain-0.13.1 → plain-0.14.0}/plain/views/forms.py +4 -4
- {plain-0.13.1 → plain-0.14.0}/plain/views/objects.py +2 -2
- plain-0.14.0/pyproject.toml +26 -0
- plain-0.14.0/tests/.bolt/assets_collected/assets.json +1 -0
- plain-0.14.0/tests/.gitignore +3 -0
- plain-0.14.0/tests/app/.gitignore +1 -0
- plain-0.14.0/tests/app/settings.py +9 -0
- plain-0.14.0/tests/app/test/__init__.py +0 -0
- plain-0.14.0/tests/app/test/default_settings.py +4 -0
- plain-0.14.0/tests/app/urls.py +12 -0
- plain-0.14.0/tests/conftest.py +7 -0
- plain-0.14.0/tests/test_cli.py +11 -0
- plain-0.14.0/tests/test_runtime.py +18 -0
- plain-0.14.0/tests/test_wsgi.py +23 -0
- plain-0.14.0/uv.lock +148 -0
- plain-0.13.1/plain/utils/termcolors.py +0 -221
- plain-0.13.1/pyproject.toml +0 -35
- {plain-0.13.1 → plain-0.14.0}/LICENSE +0 -0
- {plain-0.13.1 → plain-0.14.0}/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/__main__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/compile.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/finders.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/urls.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/assets/views.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/cli/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/cli/formatting.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/cli/print.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/csrf/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/csrf/views.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/debug.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/forms/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/forms/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/forms/boundfield.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/forms/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/forms/fields.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/http/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/http/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/http/cookie.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/base.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/temp.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/middleware/headers.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/json.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/logs/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/logs/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/logs/configure.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/logs/loggers.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/logs/utils.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/packages/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/packages/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/files.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/preflight/security.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/runtime/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/runtime/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/signals/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/signals/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/core.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/test/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/test/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/urls/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/urls/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/urls/converters.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/urls/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/_os.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/connection.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/dates.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/deprecation.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/duration.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/email.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/encoding.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/functional.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/hashable.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/inspect.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/itercompat.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/safestring.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/utils/tree.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/README.md +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/__init__.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/base.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/csrf.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/errors.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/exceptions.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/redirect.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/views/templates.py +0 -0
- {plain-0.13.1 → plain-0.14.0}/plain/wsgi.py +0 -0
plain-0.14.0/.gitignore
ADDED
@@ -1,16 +1,12 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: plain
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.0
|
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
|
@@ -422,21 +422,17 @@ class AppCLIGroup(click.Group):
|
|
422
422
|
MODULE_NAME = "app.cli"
|
423
423
|
|
424
424
|
def list_commands(self, ctx):
|
425
|
-
|
426
|
-
find_spec(self.MODULE_NAME)
|
425
|
+
if find_spec(self.MODULE_NAME):
|
427
426
|
return ["app"]
|
428
|
-
|
427
|
+
else:
|
429
428
|
return []
|
430
429
|
|
431
430
|
def get_command(self, ctx, name):
|
432
431
|
if name != "app":
|
433
432
|
return
|
434
433
|
|
435
|
-
|
436
|
-
|
437
|
-
return cli.cli
|
438
|
-
except ModuleNotFoundError:
|
439
|
-
return
|
434
|
+
cli = importlib.import_module(self.MODULE_NAME)
|
435
|
+
return cli.cli
|
440
436
|
|
441
437
|
|
442
438
|
class PlainCommandCollection(click.CommandCollection):
|
@@ -36,11 +36,15 @@ class InstalledPackagesGroup(click.Group):
|
|
36
36
|
def get_command(self, ctx, name):
|
37
37
|
# Try it as plain.x and just x (we don't know ahead of time which it is, but prefer plain.x)
|
38
38
|
for n in [self.PLAIN_APPS_PREFIX + name, name]:
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
if not find_spec(n):
|
40
|
+
# plain.<name> doesn't exist at all
|
41
|
+
continue
|
42
|
+
|
43
|
+
if not find_spec(f"{n}.{self.MODULE_NAME}"):
|
42
44
|
continue
|
43
45
|
|
46
|
+
cli = importlib.import_module(f"{n}.{self.MODULE_NAME}")
|
47
|
+
|
44
48
|
# Get the app's cli.py group
|
45
49
|
try:
|
46
50
|
return cli.cli
|
@@ -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
|
|
@@ -7,6 +7,7 @@ import re
|
|
7
7
|
import sys
|
8
8
|
import time
|
9
9
|
from email.header import Header
|
10
|
+
from functools import cached_property
|
10
11
|
from http.client import responses
|
11
12
|
from http.cookies import SimpleCookie
|
12
13
|
from urllib.parse import urlparse
|
@@ -72,7 +73,7 @@ class ResponseHeaders(CaseInsensitiveMapping):
|
|
72
73
|
if mime_encode:
|
73
74
|
value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
|
74
75
|
else:
|
75
|
-
e.reason += ", HTTP response headers must be in
|
76
|
+
e.reason += f", HTTP response headers must be in {charset} format"
|
76
77
|
raise
|
77
78
|
return value
|
78
79
|
|
@@ -181,7 +182,7 @@ class ResponseBase:
|
|
181
182
|
@property
|
182
183
|
def _content_type_for_repr(self):
|
183
184
|
return (
|
184
|
-
', "
|
185
|
+
', "{}"'.format(self.headers["Content-Type"])
|
185
186
|
if "Content-Type" in self.headers
|
186
187
|
else ""
|
187
188
|
)
|
@@ -236,8 +237,8 @@ class ResponseBase:
|
|
236
237
|
if expires is not None:
|
237
238
|
if isinstance(expires, datetime.datetime):
|
238
239
|
if timezone.is_naive(expires):
|
239
|
-
expires = timezone.make_aware(expires, datetime.
|
240
|
-
delta = expires - datetime.datetime.now(tz=datetime.
|
240
|
+
expires = timezone.make_aware(expires, datetime.UTC)
|
241
|
+
delta = expires - datetime.datetime.now(tz=datetime.UTC)
|
241
242
|
# Add one second so the date matches exactly (a fraction of
|
242
243
|
# time gets lost between converting to a timedelta and
|
243
244
|
# then the date string).
|
@@ -332,14 +333,14 @@ class ResponseBase:
|
|
332
333
|
signals.request_finished.send(sender=self._handler_class)
|
333
334
|
|
334
335
|
def write(self, content):
|
335
|
-
raise OSError("This
|
336
|
+
raise OSError(f"This {self.__class__.__name__} instance is not writable")
|
336
337
|
|
337
338
|
def flush(self):
|
338
339
|
pass
|
339
340
|
|
340
341
|
def tell(self):
|
341
342
|
raise OSError(
|
342
|
-
"This
|
343
|
+
f"This {self.__class__.__name__} instance cannot tell its position"
|
343
344
|
)
|
344
345
|
|
345
346
|
# These methods partially implement a stream-like object interface.
|
@@ -355,7 +356,7 @@ class ResponseBase:
|
|
355
356
|
return False
|
356
357
|
|
357
358
|
def writelines(self, lines):
|
358
|
-
raise OSError("This
|
359
|
+
raise OSError(f"This {self.__class__.__name__} instance is not writable")
|
359
360
|
|
360
361
|
|
361
362
|
class Response(ResponseBase):
|
@@ -390,7 +391,7 @@ class Response(ResponseBase):
|
|
390
391
|
return obj_dict
|
391
392
|
|
392
393
|
def __repr__(self):
|
393
|
-
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
|
394
|
+
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
394
395
|
"cls": self.__class__.__name__,
|
395
396
|
"status_code": self.status_code,
|
396
397
|
"content_type": self._content_type_for_repr,
|
@@ -423,6 +424,10 @@ class Response(ResponseBase):
|
|
423
424
|
# Create a list of properly encoded bytestrings to support write().
|
424
425
|
self._container = [content]
|
425
426
|
|
427
|
+
@cached_property
|
428
|
+
def text(self):
|
429
|
+
return self.content.decode(self.charset or "utf-8")
|
430
|
+
|
426
431
|
def __iter__(self):
|
427
432
|
return iter(self._container)
|
428
433
|
|
@@ -461,7 +466,7 @@ class StreamingResponse(ResponseBase):
|
|
461
466
|
self.streaming_content = streaming_content
|
462
467
|
|
463
468
|
def __repr__(self):
|
464
|
-
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
|
469
|
+
return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
465
470
|
"cls": self.__class__.__qualname__,
|
466
471
|
"status_code": self.status_code,
|
467
472
|
"content_type": self._content_type_for_repr,
|
@@ -470,8 +475,8 @@ class StreamingResponse(ResponseBase):
|
|
470
475
|
@property
|
471
476
|
def content(self):
|
472
477
|
raise AttributeError(
|
473
|
-
"This
|
474
|
-
"`streaming_content` instead."
|
478
|
+
f"This {self.__class__.__name__} instance has no `content` attribute. Use "
|
479
|
+
"`streaming_content` instead."
|
475
480
|
)
|
476
481
|
|
477
482
|
@property
|
@@ -586,14 +591,14 @@ class ResponseRedirectBase(Response):
|
|
586
591
|
parsed = urlparse(str(redirect_to))
|
587
592
|
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
|
588
593
|
raise DisallowedRedirect(
|
589
|
-
"Unsafe redirect to URL with protocol '
|
594
|
+
f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
|
590
595
|
)
|
591
596
|
|
592
597
|
url = property(lambda self: self["Location"])
|
593
598
|
|
594
599
|
def __repr__(self):
|
595
600
|
return (
|
596
|
-
'<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
|
601
|
+
'<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">' # noqa: UP031
|
597
602
|
% {
|
598
603
|
"cls": self.__class__.__name__,
|
599
604
|
"status_code": self.status_code,
|
@@ -661,7 +666,7 @@ class ResponseNotAllowed(Response):
|
|
661
666
|
self["Allow"] = ", ".join(permitted_methods)
|
662
667
|
|
663
668
|
def __repr__(self):
|
664
|
-
return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
|
669
|
+
return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
|
665
670
|
"cls": self.__class__.__name__,
|
666
671
|
"status_code": self.status_code,
|
667
672
|
"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.
|