plain 0.69.0__py3-none-any.whl → 0.70.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/AGENTS.md +1 -1
- plain/CHANGELOG.md +11 -0
- plain/assets/compile.py +20 -7
- plain/assets/finders.py +15 -11
- plain/assets/fingerprints.py +6 -5
- plain/assets/urls.py +1 -1
- plain/assets/views.py +23 -17
- plain/chores/registry.py +14 -9
- plain/cli/agent/__init__.py +1 -1
- plain/cli/agent/docs.py +7 -6
- plain/cli/agent/llmdocs.py +18 -8
- plain/cli/agent/md.py +19 -14
- plain/cli/agent/prompt.py +1 -1
- plain/cli/agent/request.py +37 -17
- plain/cli/build.py +2 -2
- plain/cli/changelog.py +8 -4
- plain/cli/chores.py +4 -4
- plain/cli/core.py +8 -5
- plain/cli/docs.py +2 -2
- plain/cli/formatting.py +10 -7
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +3 -3
- plain/cli/print.py +1 -1
- plain/cli/registry.py +10 -6
- plain/cli/scaffold.py +1 -1
- plain/cli/settings.py +1 -1
- plain/cli/shell.py +10 -7
- plain/cli/startup.py +3 -3
- plain/cli/urls.py +10 -4
- plain/cli/utils.py +2 -2
- plain/csrf/middleware.py +15 -5
- plain/csrf/views.py +11 -8
- plain/debug.py +5 -2
- plain/exceptions.py +19 -8
- plain/forms/__init__.py +1 -1
- plain/forms/boundfield.py +14 -7
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +139 -97
- plain/forms/forms.py +55 -39
- plain/http/cookie.py +15 -7
- plain/http/multipartparser.py +50 -30
- plain/http/request.py +97 -73
- plain/http/response.py +99 -80
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +34 -18
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +23 -5
- plain/internal/files/uploadedfile.py +42 -26
- plain/internal/files/uploadhandler.py +48 -27
- plain/internal/files/utils.py +13 -6
- plain/internal/handlers/base.py +20 -6
- plain/internal/handlers/exception.py +19 -5
- plain/internal/handlers/wsgi.py +30 -18
- plain/internal/middleware/headers.py +11 -2
- plain/internal/middleware/hosts.py +10 -2
- plain/internal/middleware/https.py +13 -3
- plain/internal/middleware/slash.py +15 -5
- plain/json.py +2 -1
- plain/logs/configure.py +3 -1
- plain/logs/debug.py +16 -5
- plain/logs/formatters.py +6 -3
- plain/logs/loggers.py +56 -52
- plain/logs/utils.py +19 -9
- plain/packages/config.py +14 -6
- plain/packages/registry.py +27 -12
- plain/paginator.py +31 -21
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +25 -10
- plain/preflight/results.py +10 -4
- plain/preflight/security.py +7 -5
- plain/preflight/urls.py +4 -1
- plain/runtime/__init__.py +4 -3
- plain/runtime/global_settings.py +1 -1
- plain/runtime/user_settings.py +26 -17
- plain/runtime/utils.py +1 -1
- plain/signals/dispatch/dispatcher.py +39 -17
- plain/signing.py +49 -30
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +4 -3
- plain/templates/jinja/extensions.py +9 -3
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +1 -1
- plain/test/client.py +246 -174
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +10 -2
- plain/urls/converters.py +13 -10
- plain/urls/patterns.py +32 -20
- plain/urls/resolvers.py +32 -22
- plain/urls/utils.py +5 -1
- plain/utils/cache.py +14 -8
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +84 -54
- plain/utils/dateparse.py +10 -7
- plain/utils/deconstruct.py +12 -4
- plain/utils/decorators.py +5 -1
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +62 -47
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +21 -14
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +23 -13
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +34 -18
- plain/utils/timezone.py +30 -19
- plain/utils/tree.py +31 -18
- plain/validators.py +71 -44
- plain/views/base.py +16 -6
- plain/views/errors.py +11 -4
- plain/views/exceptions.py +4 -1
- plain/views/objects.py +15 -15
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
- plain-0.70.0.dist-info/RECORD +169 -0
- plain-0.69.0.dist-info/RECORD +0 -169
- {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
- {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
- {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
plain/test/encoding.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import mimetypes
|
2
4
|
import os
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.runtime import settings
|
5
8
|
from plain.utils.encoding import force_bytes
|
6
9
|
from plain.utils.itercompat import is_iterable
|
7
10
|
|
8
11
|
|
9
|
-
def encode_multipart(boundary, data):
|
12
|
+
def encode_multipart(boundary: str, data: dict[str, Any]) -> bytes:
|
10
13
|
"""
|
11
14
|
Encode multipart POST data from a dictionary of form values.
|
12
15
|
|
@@ -14,13 +17,13 @@ def encode_multipart(boundary, data):
|
|
14
17
|
as content. If the value is a file, the contents of the file will be sent
|
15
18
|
as an application/octet-stream; otherwise, str(value) will be sent.
|
16
19
|
"""
|
17
|
-
lines = []
|
20
|
+
lines: list[bytes] = []
|
18
21
|
|
19
|
-
def to_bytes(s):
|
22
|
+
def to_bytes(s: str) -> bytes:
|
20
23
|
return force_bytes(s, settings.DEFAULT_CHARSET)
|
21
24
|
|
22
25
|
# Not by any means perfect, but good enough for our purposes.
|
23
|
-
def is_file(thing):
|
26
|
+
def is_file(thing: Any) -> bool:
|
24
27
|
return hasattr(thing, "read") and callable(thing.read)
|
25
28
|
|
26
29
|
# Each bit of the multipart form data could be either a form value or a
|
@@ -68,8 +71,8 @@ def encode_multipart(boundary, data):
|
|
68
71
|
return b"\r\n".join(lines)
|
69
72
|
|
70
73
|
|
71
|
-
def encode_file(boundary, key, file):
|
72
|
-
def to_bytes(s):
|
74
|
+
def encode_file(boundary: str, key: str, file: Any) -> list[bytes]:
|
75
|
+
def to_bytes(s: str) -> bytes:
|
73
76
|
return force_bytes(s, settings.DEFAULT_CHARSET)
|
74
77
|
|
75
78
|
# file.name might not be a string. For example, it's an int for
|
plain/test/exceptions.py
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from plain.http import Response
|
7
|
+
|
8
|
+
|
1
9
|
class RedirectCycleError(Exception):
|
2
10
|
"""The test client has been asked to follow a redirect loop."""
|
3
11
|
|
4
|
-
def __init__(self, message, last_response):
|
12
|
+
def __init__(self, message: str, last_response: Response) -> None:
|
5
13
|
super().__init__(message)
|
6
14
|
self.last_response = last_response
|
7
|
-
self.redirect_chain = last_response.redirect_chain
|
15
|
+
self.redirect_chain: list[tuple[str, int]] = last_response.redirect_chain # type: ignore[attr-defined]
|
plain/urls/converters.py
CHANGED
@@ -1,34 +1,37 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import functools
|
2
4
|
import uuid
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
|
5
8
|
class IntConverter:
|
6
9
|
regex = "[0-9]+"
|
7
10
|
|
8
|
-
def to_python(self, value):
|
11
|
+
def to_python(self, value: str) -> int:
|
9
12
|
return int(value)
|
10
13
|
|
11
|
-
def to_url(self, value):
|
14
|
+
def to_url(self, value: int) -> str:
|
12
15
|
return str(value)
|
13
16
|
|
14
17
|
|
15
18
|
class StringConverter:
|
16
19
|
regex = "[^/]+"
|
17
20
|
|
18
|
-
def to_python(self, value):
|
21
|
+
def to_python(self, value: str) -> str:
|
19
22
|
return value
|
20
23
|
|
21
|
-
def to_url(self, value):
|
24
|
+
def to_url(self, value: str) -> str:
|
22
25
|
return value
|
23
26
|
|
24
27
|
|
25
28
|
class UUIDConverter:
|
26
29
|
regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
27
30
|
|
28
|
-
def to_python(self, value):
|
31
|
+
def to_python(self, value: str) -> uuid.UUID:
|
29
32
|
return uuid.UUID(value)
|
30
33
|
|
31
|
-
def to_url(self, value):
|
34
|
+
def to_url(self, value: uuid.UUID) -> str:
|
32
35
|
return str(value)
|
33
36
|
|
34
37
|
|
@@ -49,18 +52,18 @@ DEFAULT_CONVERTERS = {
|
|
49
52
|
}
|
50
53
|
|
51
54
|
|
52
|
-
REGISTERED_CONVERTERS = {}
|
55
|
+
REGISTERED_CONVERTERS: dict[str, Any] = {}
|
53
56
|
|
54
57
|
|
55
|
-
def register_converter(converter, type_name):
|
58
|
+
def register_converter(converter: type, type_name: str) -> None:
|
56
59
|
REGISTERED_CONVERTERS[type_name] = converter()
|
57
60
|
get_converters.cache_clear()
|
58
61
|
|
59
62
|
|
60
63
|
@functools.cache
|
61
|
-
def get_converters():
|
64
|
+
def get_converters() -> dict[str, Any]:
|
62
65
|
return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
|
63
66
|
|
64
67
|
|
65
|
-
def get_converter(raw_converter):
|
68
|
+
def get_converter(raw_converter: str) -> Any:
|
66
69
|
return get_converters()[raw_converter]
|
plain/urls/patterns.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
2
4
|
import string
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.exceptions import ImproperlyConfigured
|
5
8
|
from plain.internal import internalcode
|
@@ -12,7 +15,7 @@ from .converters import get_converter
|
|
12
15
|
|
13
16
|
@internalcode
|
14
17
|
class CheckURLMixin:
|
15
|
-
def describe(self):
|
18
|
+
def describe(self) -> str:
|
16
19
|
"""
|
17
20
|
Format the URL pattern for display in warning messages.
|
18
21
|
"""
|
@@ -21,7 +24,7 @@ class CheckURLMixin:
|
|
21
24
|
description += f" [name='{self.name}']"
|
22
25
|
return description
|
23
26
|
|
24
|
-
def _check_pattern_startswith_slash(self):
|
27
|
+
def _check_pattern_startswith_slash(self) -> list[PreflightResult]:
|
25
28
|
"""
|
26
29
|
Check that the pattern does not begin with a forward slash.
|
27
30
|
"""
|
@@ -44,14 +47,14 @@ class CheckURLMixin:
|
|
44
47
|
|
45
48
|
|
46
49
|
class RegexPattern(CheckURLMixin):
|
47
|
-
def __init__(self, regex, name=None, is_endpoint=False):
|
50
|
+
def __init__(self, regex: str, name: str | None = None, is_endpoint: bool = False):
|
48
51
|
self._regex = regex
|
49
52
|
self._is_endpoint = is_endpoint
|
50
53
|
self.name = name
|
51
|
-
self.converters = {}
|
54
|
+
self.converters: dict[str, Any] = {}
|
52
55
|
self.regex = self._compile(str(regex))
|
53
56
|
|
54
|
-
def match(self, path):
|
57
|
+
def match(self, path: str) -> tuple[str, tuple[Any, ...], dict[str, Any]] | None:
|
55
58
|
match = (
|
56
59
|
self.regex.fullmatch(path)
|
57
60
|
if self._is_endpoint and self.regex.pattern.endswith("$")
|
@@ -67,14 +70,14 @@ class RegexPattern(CheckURLMixin):
|
|
67
70
|
return path[match.end() :], args, kwargs
|
68
71
|
return None
|
69
72
|
|
70
|
-
def preflight(self):
|
73
|
+
def preflight(self) -> list[PreflightResult]:
|
71
74
|
warnings = []
|
72
75
|
warnings.extend(self._check_pattern_startswith_slash())
|
73
76
|
if not self._is_endpoint:
|
74
77
|
warnings.extend(self._check_include_trailing_dollar())
|
75
78
|
return warnings
|
76
79
|
|
77
|
-
def _check_include_trailing_dollar(self):
|
80
|
+
def _check_include_trailing_dollar(self) -> list[PreflightResult]:
|
78
81
|
regex_pattern = self.regex.pattern
|
79
82
|
if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
|
80
83
|
return [
|
@@ -87,7 +90,7 @@ class RegexPattern(CheckURLMixin):
|
|
87
90
|
else:
|
88
91
|
return []
|
89
92
|
|
90
|
-
def _compile(self, regex):
|
93
|
+
def _compile(self, regex: str) -> re.Pattern[str]:
|
91
94
|
"""Compile and return the given regular expression."""
|
92
95
|
try:
|
93
96
|
return re.compile(regex)
|
@@ -96,7 +99,7 @@ class RegexPattern(CheckURLMixin):
|
|
96
99
|
f'"{regex}" is not a valid regular expression: {e}'
|
97
100
|
) from e
|
98
101
|
|
99
|
-
def __str__(self):
|
102
|
+
def __str__(self) -> str:
|
100
103
|
return str(self._regex)
|
101
104
|
|
102
105
|
|
@@ -105,7 +108,9 @@ _PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
|
|
105
108
|
)
|
106
109
|
|
107
110
|
|
108
|
-
def _route_to_regex(
|
111
|
+
def _route_to_regex(
|
112
|
+
route: str, is_endpoint: bool = False
|
113
|
+
) -> tuple[str, dict[str, Any]]:
|
109
114
|
"""
|
110
115
|
Convert a path pattern into a regular expression. Return the regular
|
111
116
|
expression and a dictionary mapping the capture names to the converters.
|
@@ -151,14 +156,14 @@ def _route_to_regex(route, is_endpoint=False):
|
|
151
156
|
|
152
157
|
|
153
158
|
class RoutePattern(CheckURLMixin):
|
154
|
-
def __init__(self, route, name=None, is_endpoint=False):
|
159
|
+
def __init__(self, route: str, name: str | None = None, is_endpoint: bool = False):
|
155
160
|
self._route = route
|
156
161
|
self._is_endpoint = is_endpoint
|
157
162
|
self.name = name
|
158
163
|
self.converters = _route_to_regex(str(route), is_endpoint)[1]
|
159
164
|
self.regex = self._compile(str(route))
|
160
165
|
|
161
|
-
def match(self, path):
|
166
|
+
def match(self, path: str) -> tuple[str, tuple[()], dict[str, Any]] | None:
|
162
167
|
match = self.regex.search(path)
|
163
168
|
if match:
|
164
169
|
# RoutePattern doesn't allow non-named groups so args are ignored.
|
@@ -172,7 +177,7 @@ class RoutePattern(CheckURLMixin):
|
|
172
177
|
return path[match.end() :], (), kwargs
|
173
178
|
return None
|
174
179
|
|
175
|
-
def preflight(self):
|
180
|
+
def preflight(self) -> list[PreflightResult]:
|
176
181
|
warnings = self._check_pattern_startswith_slash()
|
177
182
|
route = self._route
|
178
183
|
if "(?P<" in route or route.startswith("^") or route.endswith("$"):
|
@@ -187,28 +192,34 @@ class RoutePattern(CheckURLMixin):
|
|
187
192
|
)
|
188
193
|
return warnings
|
189
194
|
|
190
|
-
def _compile(self, route):
|
195
|
+
def _compile(self, route: str) -> re.Pattern[str]:
|
191
196
|
return re.compile(_route_to_regex(route, self._is_endpoint)[0])
|
192
197
|
|
193
|
-
def __str__(self):
|
198
|
+
def __str__(self) -> str:
|
194
199
|
return str(self._route)
|
195
200
|
|
196
201
|
|
197
202
|
class URLPattern:
|
198
|
-
def __init__(
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
*,
|
206
|
+
pattern: RegexPattern | RoutePattern,
|
207
|
+
view: Any,
|
208
|
+
name: str | None = None,
|
209
|
+
):
|
199
210
|
self.pattern = pattern
|
200
211
|
self.view = view
|
201
212
|
self.name = name
|
202
213
|
|
203
|
-
def __repr__(self):
|
214
|
+
def __repr__(self) -> str:
|
204
215
|
return f"<{self.__class__.__name__} {self.pattern.describe()}>"
|
205
216
|
|
206
|
-
def preflight(self):
|
217
|
+
def preflight(self) -> list[PreflightResult]:
|
207
218
|
warnings = self._check_pattern_name()
|
208
219
|
warnings.extend(self.pattern.preflight())
|
209
220
|
return warnings
|
210
221
|
|
211
|
-
def _check_pattern_name(self):
|
222
|
+
def _check_pattern_name(self) -> list[PreflightResult]:
|
212
223
|
"""
|
213
224
|
Check that the pattern name does not contain a colon.
|
214
225
|
"""
|
@@ -223,7 +234,7 @@ class URLPattern:
|
|
223
234
|
else:
|
224
235
|
return []
|
225
236
|
|
226
|
-
def resolve(self, path):
|
237
|
+
def resolve(self, path: str) -> Any:
|
227
238
|
match = self.pattern.match(path)
|
228
239
|
if match:
|
229
240
|
new_path, args, captured_kwargs = match
|
@@ -236,3 +247,4 @@ class URLPattern:
|
|
236
247
|
url_name=self.pattern.name,
|
237
248
|
route=str(self.pattern),
|
238
249
|
)
|
250
|
+
return None
|
plain/urls/resolvers.py
CHANGED
@@ -6,9 +6,12 @@ a string) and returns a ResolverMatch object which provides access to all
|
|
6
6
|
attributes of the resolved URL match.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import functools
|
10
12
|
import re
|
11
13
|
from threading import local
|
14
|
+
from typing import TYPE_CHECKING, Any
|
12
15
|
from urllib.parse import quote
|
13
16
|
|
14
17
|
from plain.runtime import settings
|
@@ -18,19 +21,24 @@ from plain.utils.module_loading import import_string
|
|
18
21
|
from plain.utils.regex_helper import normalize
|
19
22
|
|
20
23
|
from .exceptions import NoReverseMatch, Resolver404
|
21
|
-
from .patterns import RegexPattern, URLPattern
|
24
|
+
from .patterns import RegexPattern, RoutePattern, URLPattern
|
25
|
+
|
26
|
+
if TYPE_CHECKING:
|
27
|
+
from plain.preflight import PreflightResult
|
28
|
+
|
29
|
+
from .routers import Router
|
22
30
|
|
23
31
|
|
24
32
|
class ResolverMatch:
|
25
33
|
def __init__(
|
26
34
|
self,
|
27
35
|
*,
|
28
|
-
view,
|
29
|
-
args,
|
30
|
-
kwargs,
|
31
|
-
url_name=None,
|
32
|
-
namespaces=None,
|
33
|
-
route=None,
|
36
|
+
view: Any,
|
37
|
+
args: tuple[Any, ...],
|
38
|
+
kwargs: dict[str, Any],
|
39
|
+
url_name: str | None = None,
|
40
|
+
namespaces: list[str] | None = None,
|
41
|
+
route: str | None = None,
|
34
42
|
):
|
35
43
|
self.view = view
|
36
44
|
self.args = args
|
@@ -48,7 +56,7 @@ class ResolverMatch:
|
|
48
56
|
)
|
49
57
|
|
50
58
|
|
51
|
-
def get_resolver(router=None):
|
59
|
+
def get_resolver(router: str | Router | None = None) -> URLResolver:
|
52
60
|
if router is None:
|
53
61
|
router = settings.URLS_ROUTER
|
54
62
|
|
@@ -56,7 +64,7 @@ def get_resolver(router=None):
|
|
56
64
|
|
57
65
|
|
58
66
|
@functools.cache
|
59
|
-
def _get_cached_resolver(router):
|
67
|
+
def _get_cached_resolver(router: str | Router) -> URLResolver:
|
60
68
|
if isinstance(router, str):
|
61
69
|
# Do this inside the cached call, primarily for the URLS_ROUTER
|
62
70
|
router_class = import_string(router)
|
@@ -66,7 +74,9 @@ def _get_cached_resolver(router):
|
|
66
74
|
|
67
75
|
|
68
76
|
@functools.cache
|
69
|
-
def get_ns_resolver(
|
77
|
+
def get_ns_resolver(
|
78
|
+
ns_pattern: str, resolver: URLResolver, converters: tuple[tuple[str, Any], ...]
|
79
|
+
) -> URLResolver:
|
70
80
|
from .routers import Router
|
71
81
|
|
72
82
|
# Build a namespaced resolver for the given parent urls_module pattern.
|
@@ -95,13 +105,13 @@ class URLResolver:
|
|
95
105
|
def __init__(
|
96
106
|
self,
|
97
107
|
*,
|
98
|
-
pattern,
|
99
|
-
router,
|
108
|
+
pattern: RegexPattern | RoutePattern,
|
109
|
+
router: Router,
|
100
110
|
):
|
101
111
|
self.pattern = pattern
|
102
112
|
self.router = router
|
103
|
-
self._reverse_dict = {}
|
104
|
-
self._namespace_dict = {}
|
113
|
+
self._reverse_dict: dict[Any, Any] = {}
|
114
|
+
self._namespace_dict: dict[str, tuple[str, URLResolver]] = {}
|
105
115
|
self._populated = False
|
106
116
|
self._local = local()
|
107
117
|
|
@@ -110,17 +120,17 @@ class URLResolver:
|
|
110
120
|
self.namespace = self.router.namespace
|
111
121
|
self.url_patterns = self.router.urls
|
112
122
|
|
113
|
-
def __repr__(self):
|
123
|
+
def __repr__(self) -> str:
|
114
124
|
return f"<{self.__class__.__name__} {repr(self.router)} ({self.namespace}) {self.pattern.describe()}>"
|
115
125
|
|
116
|
-
def preflight(self):
|
126
|
+
def preflight(self) -> list[PreflightResult]:
|
117
127
|
messages = []
|
118
128
|
messages.extend(self.pattern.preflight())
|
119
129
|
for pattern in self.url_patterns:
|
120
130
|
messages.extend(pattern.preflight())
|
121
131
|
return messages
|
122
132
|
|
123
|
-
def _populate(self):
|
133
|
+
def _populate(self) -> None:
|
124
134
|
# Short-circuit if called recursively in this thread to prevent
|
125
135
|
# infinite recursion. Concurrent threads may call this at the same
|
126
136
|
# time and will need to continue, so set 'populating' on a
|
@@ -191,26 +201,26 @@ class URLResolver:
|
|
191
201
|
self._local.populating = False
|
192
202
|
|
193
203
|
@property
|
194
|
-
def reverse_dict(self):
|
204
|
+
def reverse_dict(self) -> MultiValueDict:
|
195
205
|
if not self._reverse_dict:
|
196
206
|
self._populate()
|
197
207
|
return self._reverse_dict
|
198
208
|
|
199
209
|
@property
|
200
|
-
def namespace_dict(self):
|
210
|
+
def namespace_dict(self) -> dict[str, tuple[str, URLResolver]]:
|
201
211
|
if not self._namespace_dict:
|
202
212
|
self._populate()
|
203
213
|
return self._namespace_dict
|
204
214
|
|
205
215
|
@staticmethod
|
206
|
-
def _join_route(route1, route2):
|
216
|
+
def _join_route(route1: str, route2: str) -> str:
|
207
217
|
"""Join two routes, without the starting ^ in the second route."""
|
208
218
|
if not route1:
|
209
219
|
return route2
|
210
220
|
route2 = route2.removeprefix("^")
|
211
221
|
return route1 + route2
|
212
222
|
|
213
|
-
def resolve(self, path):
|
223
|
+
def resolve(self, path: str) -> ResolverMatch:
|
214
224
|
path = str(path) # path may be a reverse_lazy object
|
215
225
|
match = self.pattern.match(path)
|
216
226
|
if match:
|
@@ -247,7 +257,7 @@ class URLResolver:
|
|
247
257
|
raise Resolver404({"path": new_path})
|
248
258
|
raise Resolver404({"path": path})
|
249
259
|
|
250
|
-
def reverse(self, lookup_view, *args, **kwargs):
|
260
|
+
def reverse(self, lookup_view: Any, *args: Any, **kwargs: Any) -> str:
|
251
261
|
if args and kwargs:
|
252
262
|
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
|
253
263
|
|
plain/urls/utils.py
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
1
5
|
from plain.utils.functional import lazy
|
2
6
|
|
3
7
|
from .exceptions import NoReverseMatch
|
4
8
|
from .resolvers import get_ns_resolver, get_resolver
|
5
9
|
|
6
10
|
|
7
|
-
def reverse(url_name: str, *args, **kwargs):
|
11
|
+
def reverse(url_name: str, *args: Any, **kwargs: Any) -> str:
|
8
12
|
resolver = get_resolver()
|
9
13
|
|
10
14
|
*path, view = url_name.split(":")
|
plain/utils/cache.py
CHANGED
@@ -15,16 +15,22 @@ An example: i18n middleware would need to distinguish caches by the
|
|
15
15
|
"Accept-language" header.
|
16
16
|
"""
|
17
17
|
|
18
|
+
from __future__ import annotations
|
19
|
+
|
18
20
|
import time
|
19
21
|
from collections import defaultdict
|
22
|
+
from typing import TYPE_CHECKING, Any
|
20
23
|
|
21
24
|
from .http import http_date
|
22
25
|
from .regex_helper import _lazy_re_compile
|
23
26
|
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from plain.http import Response
|
29
|
+
|
24
30
|
cc_delim_re = _lazy_re_compile(r"\s*,\s*")
|
25
31
|
|
26
32
|
|
27
|
-
def patch_response_headers(response, cache_timeout):
|
33
|
+
def patch_response_headers(response: Response, cache_timeout: int | float) -> None:
|
28
34
|
"""
|
29
35
|
Add HTTP caching headers to the given HttpResponse: Expires and
|
30
36
|
Cache-Control.
|
@@ -38,7 +44,7 @@ def patch_response_headers(response, cache_timeout):
|
|
38
44
|
patch_cache_control(response, max_age=cache_timeout)
|
39
45
|
|
40
46
|
|
41
|
-
def add_never_cache_headers(response):
|
47
|
+
def add_never_cache_headers(response: Response) -> None:
|
42
48
|
"""
|
43
49
|
Add headers to a response to indicate that a page should never be cached.
|
44
50
|
"""
|
@@ -48,7 +54,7 @@ def add_never_cache_headers(response):
|
|
48
54
|
)
|
49
55
|
|
50
56
|
|
51
|
-
def patch_cache_control(response, **kwargs):
|
57
|
+
def patch_cache_control(response: Response, **kwargs: Any) -> None:
|
52
58
|
"""
|
53
59
|
Patch the Cache-Control header by adding all keyword arguments to it.
|
54
60
|
The transformation is as follows:
|
@@ -61,16 +67,16 @@ def patch_cache_control(response, **kwargs):
|
|
61
67
|
str() to it.
|
62
68
|
"""
|
63
69
|
|
64
|
-
def dictitem(s):
|
70
|
+
def dictitem(s: str) -> tuple[str, str | bool]:
|
65
71
|
t = s.split("=", 1)
|
66
72
|
if len(t) > 1:
|
67
73
|
return (t[0].lower(), t[1])
|
68
74
|
else:
|
69
75
|
return (t[0].lower(), True)
|
70
76
|
|
71
|
-
def dictvalue(*t):
|
77
|
+
def dictvalue(*t: str | bool) -> str:
|
72
78
|
if t[1] is True:
|
73
|
-
return t[0]
|
79
|
+
return str(t[0])
|
74
80
|
else:
|
75
81
|
return f"{t[0]}={t[1]}"
|
76
82
|
|
@@ -117,7 +123,7 @@ def patch_cache_control(response, **kwargs):
|
|
117
123
|
response.headers["Cache-Control"] = cc
|
118
124
|
|
119
125
|
|
120
|
-
def patch_vary_headers(response, newheaders):
|
126
|
+
def patch_vary_headers(response: Response, newheaders: list[str]) -> None:
|
121
127
|
"""
|
122
128
|
Add (or update) the "Vary" header in the given Response object.
|
123
129
|
newheaders is a list of header names that should be in "Vary". If headers
|
@@ -145,7 +151,7 @@ def patch_vary_headers(response, newheaders):
|
|
145
151
|
response.headers["Vary"] = ", ".join(vary_headers)
|
146
152
|
|
147
153
|
|
148
|
-
def _to_tuple(s):
|
154
|
+
def _to_tuple(s: str) -> tuple[str, str | bool]:
|
149
155
|
t = s.split("=", 1)
|
150
156
|
if len(t) == 2:
|
151
157
|
return t[0].lower(), t[1]
|
plain/utils/crypto.py
CHANGED
@@ -2,9 +2,13 @@
|
|
2
2
|
Plain's standard crypto functions and utilities.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
|
5
7
|
import hashlib
|
6
8
|
import hmac
|
7
9
|
import secrets
|
10
|
+
from collections.abc import Callable
|
11
|
+
from typing import Any
|
8
12
|
|
9
13
|
from plain.runtime import settings
|
10
14
|
from plain.utils.encoding import force_bytes
|
@@ -16,7 +20,13 @@ class InvalidAlgorithm(ValueError):
|
|
16
20
|
pass
|
17
21
|
|
18
22
|
|
19
|
-
def salted_hmac(
|
23
|
+
def salted_hmac(
|
24
|
+
key_salt: str | bytes,
|
25
|
+
value: str | bytes,
|
26
|
+
secret: str | bytes | None = None,
|
27
|
+
*,
|
28
|
+
algorithm: str = "sha1",
|
29
|
+
) -> hmac.HMAC:
|
20
30
|
"""
|
21
31
|
Return the HMAC of 'value', using a key generated from key_salt and a
|
22
32
|
secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
|
@@ -48,7 +58,7 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
|
48
58
|
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
49
59
|
|
50
60
|
|
51
|
-
def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
61
|
+
def get_random_string(length: int, allowed_chars: str = RANDOM_STRING_CHARS) -> str:
|
52
62
|
"""
|
53
63
|
Return a securely generated random string.
|
54
64
|
|
@@ -62,11 +72,17 @@ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
|
62
72
|
return "".join(secrets.choice(allowed_chars) for i in range(length))
|
63
73
|
|
64
74
|
|
65
|
-
def pbkdf2(
|
75
|
+
def pbkdf2(
|
76
|
+
password: str | bytes,
|
77
|
+
salt: str | bytes,
|
78
|
+
iterations: int,
|
79
|
+
dklen: int = 0,
|
80
|
+
digest: Callable[[], Any] | None = None,
|
81
|
+
) -> bytes:
|
66
82
|
"""Return the hash of password using pbkdf2."""
|
67
83
|
if digest is None:
|
68
84
|
digest = hashlib.sha256
|
69
|
-
|
85
|
+
dklen_value: int | None = dklen if dklen else None
|
70
86
|
password = force_bytes(password)
|
71
87
|
salt = force_bytes(salt)
|
72
|
-
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations,
|
88
|
+
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen_value)
|