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/runtime/user_settings.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import importlib
|
2
4
|
import json
|
3
5
|
import os
|
@@ -26,13 +28,13 @@ class Settings:
|
|
26
28
|
Lazy initialization is implemented to defer loading until settings are first accessed.
|
27
29
|
"""
|
28
30
|
|
29
|
-
def __init__(self, settings_module=None):
|
31
|
+
def __init__(self, settings_module: str | None = None):
|
30
32
|
self._settings_module = settings_module
|
31
33
|
self._settings = {}
|
32
34
|
self._errors = [] # Collect configuration errors
|
33
35
|
self.configured = False
|
34
36
|
|
35
|
-
def _setup(self):
|
37
|
+
def _setup(self) -> None:
|
36
38
|
if self.configured:
|
37
39
|
return
|
38
40
|
else:
|
@@ -71,7 +73,7 @@ class Settings:
|
|
71
73
|
# Check for any collected errors
|
72
74
|
self._raise_errors_if_any()
|
73
75
|
|
74
|
-
def _load_module_settings(self, module):
|
76
|
+
def _load_module_settings(self, module: types.ModuleType) -> None:
|
75
77
|
annotations = getattr(module, "__annotations__", {})
|
76
78
|
settings = dir(module)
|
77
79
|
|
@@ -100,7 +102,7 @@ class Settings:
|
|
100
102
|
required=True,
|
101
103
|
)
|
102
104
|
|
103
|
-
def _load_default_settings(self, settings_module):
|
105
|
+
def _load_default_settings(self, settings_module: types.ModuleType) -> None:
|
104
106
|
for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
|
105
107
|
if isinstance(entry, PackageConfig):
|
106
108
|
app_settings = entry.module.default_settings
|
@@ -111,7 +113,7 @@ class Settings:
|
|
111
113
|
|
112
114
|
self._load_module_settings(app_settings)
|
113
115
|
|
114
|
-
def _load_env_settings(self):
|
116
|
+
def _load_env_settings(self) -> None:
|
115
117
|
env_settings = {
|
116
118
|
k[len(ENV_SETTINGS_PREFIX) :]: v
|
117
119
|
for k, v in os.environ.items()
|
@@ -128,7 +130,7 @@ class Settings:
|
|
128
130
|
except ImproperlyConfigured as e:
|
129
131
|
self._errors.append(str(e))
|
130
132
|
|
131
|
-
def _load_explicit_settings(self, settings_module):
|
133
|
+
def _load_explicit_settings(self, settings_module: types.ModuleType) -> None:
|
132
134
|
for setting in dir(settings_module):
|
133
135
|
if setting.isupper():
|
134
136
|
setting_value = getattr(settings_module, setting)
|
@@ -172,19 +174,19 @@ class Settings:
|
|
172
174
|
os.environ["TZ"] = self.TIME_ZONE
|
173
175
|
time.tzset()
|
174
176
|
|
175
|
-
def _check_required_settings(self):
|
177
|
+
def _check_required_settings(self) -> None:
|
176
178
|
missing = [k for k, v in self._settings.items() if v.required and not v.is_set]
|
177
179
|
if missing:
|
178
180
|
self._errors.append(f"Missing required setting(s): {', '.join(missing)}.")
|
179
181
|
|
180
|
-
def _raise_errors_if_any(self):
|
182
|
+
def _raise_errors_if_any(self) -> None:
|
181
183
|
if self._errors:
|
182
184
|
errors = ["- " + e for e in self._errors]
|
183
185
|
raise ImproperlyConfigured(
|
184
186
|
"Settings configuration errors:\n" + "\n".join(errors)
|
185
187
|
)
|
186
188
|
|
187
|
-
def __getattr__(self, name):
|
189
|
+
def __getattr__(self, name: str) -> typing.Any:
|
188
190
|
# Avoid recursion by directly returning internal attributes
|
189
191
|
if not name.isupper():
|
190
192
|
return object.__getattribute__(self, name)
|
@@ -196,7 +198,7 @@ class Settings:
|
|
196
198
|
else:
|
197
199
|
raise AttributeError(f"'Settings' object has no attribute '{name}'")
|
198
200
|
|
199
|
-
def __setattr__(self, name, value):
|
201
|
+
def __setattr__(self, name: str, value: typing.Any) -> None:
|
200
202
|
# Handle internal attributes without recursion
|
201
203
|
if not name.isupper():
|
202
204
|
object.__setattr__(self, name, value)
|
@@ -207,13 +209,15 @@ class Settings:
|
|
207
209
|
else:
|
208
210
|
object.__setattr__(self, name, value)
|
209
211
|
|
210
|
-
def __repr__(self):
|
212
|
+
def __repr__(self) -> str:
|
211
213
|
if not self.configured:
|
212
214
|
return "<Settings [Unevaluated]>"
|
213
215
|
return f'<Settings "{self._settings_module}">'
|
214
216
|
|
215
217
|
|
216
|
-
def _parse_env_value(
|
218
|
+
def _parse_env_value(
|
219
|
+
value: str, annotation: type | None, setting_name: str
|
220
|
+
) -> typing.Any:
|
217
221
|
if not annotation:
|
218
222
|
raise ImproperlyConfigured(
|
219
223
|
f"{setting_name}: Type hint required to set from environment."
|
@@ -238,7 +242,12 @@ class SettingDefinition:
|
|
238
242
|
"""Store detailed information about settings."""
|
239
243
|
|
240
244
|
def __init__(
|
241
|
-
self,
|
245
|
+
self,
|
246
|
+
name: str,
|
247
|
+
default_value: typing.Any = None,
|
248
|
+
annotation: type | None = None,
|
249
|
+
module: types.ModuleType | None = None,
|
250
|
+
required: bool = False,
|
242
251
|
):
|
243
252
|
self.name = name
|
244
253
|
self.default_value = default_value
|
@@ -249,13 +258,13 @@ class SettingDefinition:
|
|
249
258
|
self.source = "default" # 'default', 'env', 'explicit', or 'runtime'
|
250
259
|
self.is_set = False # Indicates if the value was set explicitly
|
251
260
|
|
252
|
-
def set_value(self, value, source):
|
261
|
+
def set_value(self, value: typing.Any, source: str) -> None:
|
253
262
|
self.check_type(value)
|
254
263
|
self.value = value
|
255
264
|
self.source = source
|
256
265
|
self.is_set = True
|
257
266
|
|
258
|
-
def check_type(self, obj):
|
267
|
+
def check_type(self, obj: typing.Any) -> None:
|
259
268
|
if not self.annotation:
|
260
269
|
return
|
261
270
|
|
@@ -265,7 +274,7 @@ class SettingDefinition:
|
|
265
274
|
)
|
266
275
|
|
267
276
|
@staticmethod
|
268
|
-
def _is_instance_of_type(value, type_hint) -> bool:
|
277
|
+
def _is_instance_of_type(value: typing.Any, type_hint: typing.Any) -> bool:
|
269
278
|
# Simple types
|
270
279
|
if isinstance(type_hint, type):
|
271
280
|
return isinstance(value, type_hint)
|
@@ -300,5 +309,5 @@ class SettingDefinition:
|
|
300
309
|
|
301
310
|
raise ValueError(f"Unsupported type hint: {type_hint}")
|
302
311
|
|
303
|
-
def __str__(self):
|
312
|
+
def __str__(self) -> str:
|
304
313
|
return f"SettingDefinition(name={self.name}, value={self.value}, source={self.source})"
|
plain/runtime/utils.py
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
import threading
|
3
5
|
import weakref
|
6
|
+
from collections.abc import Callable
|
7
|
+
from typing import TYPE_CHECKING, Any
|
4
8
|
|
5
9
|
from plain.utils.inspect import func_accepts_kwargs
|
6
10
|
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from collections.abc import Sequence
|
13
|
+
|
7
14
|
logger = logging.getLogger("plain.signals.dispatch")
|
8
15
|
|
9
16
|
|
10
|
-
def _make_id(target):
|
17
|
+
def _make_id(target: Any) -> int | tuple[int, int]:
|
11
18
|
if hasattr(target, "__func__"):
|
12
19
|
return (id(target.__self__), id(target.__func__))
|
13
20
|
return id(target)
|
@@ -29,7 +36,7 @@ class Signal:
|
|
29
36
|
{ receiverkey (id) : weakref(receiver) }
|
30
37
|
"""
|
31
38
|
|
32
|
-
def __init__(self, use_caching=False):
|
39
|
+
def __init__(self, use_caching: bool = False):
|
33
40
|
"""
|
34
41
|
Create a new signal.
|
35
42
|
"""
|
@@ -44,7 +51,13 @@ class Signal:
|
|
44
51
|
self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
|
45
52
|
self._dead_receivers = False
|
46
53
|
|
47
|
-
def connect(
|
54
|
+
def connect(
|
55
|
+
self,
|
56
|
+
receiver: Callable[..., Any],
|
57
|
+
sender: Any = None,
|
58
|
+
weak: bool = True,
|
59
|
+
dispatch_uid: Any = None,
|
60
|
+
) -> None:
|
48
61
|
"""
|
49
62
|
Connect receiver to sender for signal.
|
50
63
|
|
@@ -111,7 +124,12 @@ class Signal:
|
|
111
124
|
self.receivers.append((lookup_key, receiver))
|
112
125
|
self.sender_receivers_cache.clear()
|
113
126
|
|
114
|
-
def disconnect(
|
127
|
+
def disconnect(
|
128
|
+
self,
|
129
|
+
receiver: Callable[..., Any] | None = None,
|
130
|
+
sender: Any = None,
|
131
|
+
dispatch_uid: Any = None,
|
132
|
+
) -> bool:
|
115
133
|
"""
|
116
134
|
Disconnect receiver from sender for signal.
|
117
135
|
|
@@ -147,11 +165,11 @@ class Signal:
|
|
147
165
|
self.sender_receivers_cache.clear()
|
148
166
|
return disconnected
|
149
167
|
|
150
|
-
def has_listeners(self, sender=None):
|
168
|
+
def has_listeners(self, sender: Any = None) -> bool:
|
151
169
|
sync_receivers = self._live_receivers(sender)
|
152
170
|
return bool(sync_receivers)
|
153
171
|
|
154
|
-
def send(self, sender, **named):
|
172
|
+
def send(self, sender: Any, **named: Any) -> list[tuple[Callable[..., Any], Any]]:
|
155
173
|
"""
|
156
174
|
Send signal from sender to all connected receivers.
|
157
175
|
|
@@ -185,15 +203,17 @@ class Signal:
|
|
185
203
|
responses.append((receiver, response))
|
186
204
|
return responses
|
187
205
|
|
188
|
-
def _log_robust_failure(self, receiver, err):
|
206
|
+
def _log_robust_failure(self, receiver: Callable[..., Any], err: Exception) -> None:
|
189
207
|
logger.error(
|
190
208
|
"Error calling %s in Signal.send_robust() (%s)",
|
191
|
-
receiver
|
209
|
+
getattr(receiver, "__qualname__", repr(receiver)),
|
192
210
|
err,
|
193
211
|
exc_info=err,
|
194
212
|
)
|
195
213
|
|
196
|
-
def send_robust(
|
214
|
+
def send_robust(
|
215
|
+
self, sender: Any, **named: Any
|
216
|
+
) -> list[tuple[Callable[..., Any], Any]]:
|
197
217
|
"""
|
198
218
|
Send signal from sender to all connected receivers catching errors.
|
199
219
|
|
@@ -236,7 +256,7 @@ class Signal:
|
|
236
256
|
responses.append((receiver, response))
|
237
257
|
return responses
|
238
258
|
|
239
|
-
def _clear_dead_receivers(self):
|
259
|
+
def _clear_dead_receivers(self) -> None:
|
240
260
|
# Note: caller is assumed to hold self.lock.
|
241
261
|
if self._dead_receivers:
|
242
262
|
self._dead_receivers = False
|
@@ -246,7 +266,7 @@ class Signal:
|
|
246
266
|
if not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None)
|
247
267
|
]
|
248
268
|
|
249
|
-
def _live_receivers(self, sender):
|
269
|
+
def _live_receivers(self, sender: Any) -> list[Callable[..., Any]]:
|
250
270
|
"""
|
251
271
|
Filter sequence of receivers to get resolved, live receivers.
|
252
272
|
|
@@ -285,7 +305,7 @@ class Signal:
|
|
285
305
|
non_weak_sync_receivers.append(receiver)
|
286
306
|
return non_weak_sync_receivers
|
287
307
|
|
288
|
-
def _remove_receiver(self, receiver=None):
|
308
|
+
def _remove_receiver(self, receiver: Any = None) -> None:
|
289
309
|
# Mark that the self.receivers list has dead weakrefs. If so, we will
|
290
310
|
# clean those up in connect, disconnect and _live_receivers while
|
291
311
|
# holding self.lock. Note that doing the cleanup here isn't a good
|
@@ -295,7 +315,9 @@ class Signal:
|
|
295
315
|
self._dead_receivers = True
|
296
316
|
|
297
317
|
|
298
|
-
def receiver(
|
318
|
+
def receiver(
|
319
|
+
signal: Signal | Sequence[Signal], **kwargs: Any
|
320
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
299
321
|
"""
|
300
322
|
A decorator for connecting receivers to signals. Used by passing in the
|
301
323
|
signal (or list of signals) and keyword arguments to connect::
|
@@ -309,12 +331,12 @@ def receiver(signal, **kwargs):
|
|
309
331
|
...
|
310
332
|
"""
|
311
333
|
|
312
|
-
def _decorator(func):
|
313
|
-
if isinstance(signal,
|
334
|
+
def _decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
335
|
+
if isinstance(signal, Signal):
|
336
|
+
signal.connect(func, **kwargs)
|
337
|
+
else:
|
314
338
|
for s in signal:
|
315
339
|
s.connect(func, **kwargs)
|
316
|
-
else:
|
317
|
-
signal.connect(func, **kwargs)
|
318
340
|
return func
|
319
341
|
|
320
342
|
return _decorator
|
plain/signing.py
CHANGED
@@ -33,12 +33,15 @@ There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
|
|
33
33
|
These functions make use of all of them.
|
34
34
|
"""
|
35
35
|
|
36
|
+
from __future__ import annotations
|
37
|
+
|
36
38
|
import base64
|
37
39
|
import datetime
|
38
40
|
import hmac
|
39
41
|
import json
|
40
42
|
import time
|
41
43
|
import zlib
|
44
|
+
from typing import Any
|
42
45
|
|
43
46
|
from plain.runtime import settings
|
44
47
|
from plain.utils.crypto import salted_hmac
|
@@ -61,7 +64,7 @@ class SignatureExpired(BadSignature):
|
|
61
64
|
pass
|
62
65
|
|
63
66
|
|
64
|
-
def b62_encode(s):
|
67
|
+
def b62_encode(s: int) -> str:
|
65
68
|
if s == 0:
|
66
69
|
return "0"
|
67
70
|
sign = "-" if s < 0 else ""
|
@@ -73,7 +76,7 @@ def b62_encode(s):
|
|
73
76
|
return sign + encoded
|
74
77
|
|
75
78
|
|
76
|
-
def b62_decode(s):
|
79
|
+
def b62_decode(s: str) -> int:
|
77
80
|
if s == "0":
|
78
81
|
return 0
|
79
82
|
sign = 1
|
@@ -86,16 +89,16 @@ def b62_decode(s):
|
|
86
89
|
return sign * decoded
|
87
90
|
|
88
91
|
|
89
|
-
def b64_encode(s):
|
92
|
+
def b64_encode(s: bytes) -> bytes:
|
90
93
|
return base64.urlsafe_b64encode(s).strip(b"=")
|
91
94
|
|
92
95
|
|
93
|
-
def b64_decode(s):
|
96
|
+
def b64_decode(s: bytes) -> bytes:
|
94
97
|
pad = b"=" * (-len(s) % 4)
|
95
98
|
return base64.urlsafe_b64decode(s + pad)
|
96
99
|
|
97
100
|
|
98
|
-
def base64_hmac(salt, value, key, algorithm="sha1"):
|
101
|
+
def base64_hmac(salt: str, value: str, key: str, algorithm: str = "sha1") -> str:
|
99
102
|
return b64_encode(
|
100
103
|
salted_hmac(salt, value, key, algorithm=algorithm).digest()
|
101
104
|
).decode()
|
@@ -107,16 +110,20 @@ class JSONSerializer:
|
|
107
110
|
signing.loads.
|
108
111
|
"""
|
109
112
|
|
110
|
-
def dumps(self, obj):
|
113
|
+
def dumps(self, obj: Any) -> bytes:
|
111
114
|
return json.dumps(obj, separators=(",", ":")).encode("latin-1")
|
112
115
|
|
113
|
-
def loads(self, data):
|
116
|
+
def loads(self, data: bytes) -> Any:
|
114
117
|
return json.loads(data.decode("latin-1"))
|
115
118
|
|
116
119
|
|
117
120
|
def dumps(
|
118
|
-
obj
|
119
|
-
|
121
|
+
obj: Any,
|
122
|
+
key: str | None = None,
|
123
|
+
salt: str = "plain.signing",
|
124
|
+
serializer: type[JSONSerializer] = JSONSerializer,
|
125
|
+
compress: bool = False,
|
126
|
+
) -> str:
|
120
127
|
"""
|
121
128
|
Return URL-safe, hmac signed base64 compressed JSON string. If key is
|
122
129
|
None, use settings.SECRET_KEY instead. The hmac algorithm is the default
|
@@ -139,13 +146,13 @@ def dumps(
|
|
139
146
|
|
140
147
|
|
141
148
|
def loads(
|
142
|
-
s,
|
143
|
-
key=None,
|
144
|
-
salt="plain.signing",
|
145
|
-
serializer=JSONSerializer,
|
146
|
-
max_age=None,
|
147
|
-
fallback_keys=None,
|
148
|
-
):
|
149
|
+
s: str,
|
150
|
+
key: str | None = None,
|
151
|
+
salt: str = "plain.signing",
|
152
|
+
serializer: type[JSONSerializer] = JSONSerializer,
|
153
|
+
max_age: int | float | datetime.timedelta | None = None,
|
154
|
+
fallback_keys: list[str] | None = None,
|
155
|
+
) -> Any:
|
149
156
|
"""
|
150
157
|
Reverse of dumps(), raise BadSignature if signature fails.
|
151
158
|
|
@@ -164,12 +171,12 @@ class Signer:
|
|
164
171
|
def __init__(
|
165
172
|
self,
|
166
173
|
*,
|
167
|
-
key=None,
|
168
|
-
sep=":",
|
169
|
-
salt=None,
|
170
|
-
algorithm="sha256",
|
171
|
-
fallback_keys=None,
|
172
|
-
):
|
174
|
+
key: str | None = None,
|
175
|
+
sep: str = ":",
|
176
|
+
salt: str | None = None,
|
177
|
+
algorithm: str = "sha256",
|
178
|
+
fallback_keys: list[str] | None = None,
|
179
|
+
) -> None:
|
173
180
|
self.key = key or settings.SECRET_KEY
|
174
181
|
self.fallback_keys = (
|
175
182
|
fallback_keys
|
@@ -186,14 +193,14 @@ class Signer:
|
|
186
193
|
"only A-z0-9-_=)",
|
187
194
|
)
|
188
195
|
|
189
|
-
def signature(self, value, key=None):
|
196
|
+
def signature(self, value: str, key: str | None = None) -> str:
|
190
197
|
key = key or self.key
|
191
198
|
return base64_hmac(self.salt + "signer", value, key, algorithm=self.algorithm)
|
192
199
|
|
193
|
-
def sign(self, value):
|
200
|
+
def sign(self, value: str) -> str:
|
194
201
|
return f"{value}{self.sep}{self.signature(value)}"
|
195
202
|
|
196
|
-
def unsign(self, signed_value):
|
203
|
+
def unsign(self, signed_value: str) -> str:
|
197
204
|
if self.sep not in signed_value:
|
198
205
|
raise BadSignature(f'No "{self.sep}" found in value')
|
199
206
|
value, sig = signed_value.rsplit(self.sep, 1)
|
@@ -204,7 +211,12 @@ class Signer:
|
|
204
211
|
return value
|
205
212
|
raise BadSignature(f'Signature "{sig}" does not match')
|
206
213
|
|
207
|
-
def sign_object(
|
214
|
+
def sign_object(
|
215
|
+
self,
|
216
|
+
obj: Any,
|
217
|
+
serializer: type[JSONSerializer] = JSONSerializer,
|
218
|
+
compress: bool = False,
|
219
|
+
) -> str:
|
208
220
|
"""
|
209
221
|
Return URL-safe, hmac signed base64 compressed JSON string.
|
210
222
|
|
@@ -229,7 +241,12 @@ class Signer:
|
|
229
241
|
base64d = "." + base64d
|
230
242
|
return self.sign(base64d)
|
231
243
|
|
232
|
-
def unsign_object(
|
244
|
+
def unsign_object(
|
245
|
+
self,
|
246
|
+
signed_obj: str,
|
247
|
+
serializer: type[JSONSerializer] = JSONSerializer,
|
248
|
+
**kwargs: Any,
|
249
|
+
) -> Any:
|
233
250
|
# Signer.unsign() returns str but base64 and zlib compression operate
|
234
251
|
# on bytes.
|
235
252
|
base64d = self.unsign(signed_obj, **kwargs).encode()
|
@@ -244,14 +261,16 @@ class Signer:
|
|
244
261
|
|
245
262
|
|
246
263
|
class TimestampSigner(Signer):
|
247
|
-
def timestamp(self):
|
264
|
+
def timestamp(self) -> str:
|
248
265
|
return b62_encode(int(time.time()))
|
249
266
|
|
250
|
-
def sign(self, value):
|
267
|
+
def sign(self, value: str) -> str:
|
251
268
|
value = f"{value}{self.sep}{self.timestamp()}"
|
252
269
|
return super().sign(value)
|
253
270
|
|
254
|
-
def unsign(
|
271
|
+
def unsign(
|
272
|
+
self, value: str, max_age: int | float | datetime.timedelta | None = None
|
273
|
+
) -> str:
|
255
274
|
"""
|
256
275
|
Retrieve original value and check it wasn't signed more
|
257
276
|
than max_age seconds ago.
|
@@ -1,3 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
5
|
+
|
1
6
|
from plain.packages import packages_registry
|
2
7
|
from plain.runtime import settings
|
3
8
|
from plain.utils.functional import LazyObject
|
@@ -7,7 +12,7 @@ from .environments import DefaultEnvironment, get_template_dirs
|
|
7
12
|
|
8
13
|
|
9
14
|
class JinjaEnvironment(LazyObject):
|
10
|
-
def _setup(self):
|
15
|
+
def _setup(self) -> None:
|
11
16
|
environment_setting = settings.TEMPLATES_JINJA_ENVIRONMENT
|
12
17
|
|
13
18
|
if isinstance(environment_setting, str):
|
@@ -25,12 +30,12 @@ class JinjaEnvironment(LazyObject):
|
|
25
30
|
environment = JinjaEnvironment()
|
26
31
|
|
27
32
|
|
28
|
-
def register_template_extension(extension_class):
|
33
|
+
def register_template_extension(extension_class: type) -> type:
|
29
34
|
environment.add_extension(extension_class)
|
30
35
|
return extension_class
|
31
36
|
|
32
37
|
|
33
|
-
def register_template_global(value, name=None):
|
38
|
+
def register_template_global(value: Any, name: str | None = None) -> Any:
|
34
39
|
"""
|
35
40
|
Adds a global to the Jinja environment.
|
36
41
|
|
@@ -54,9 +59,12 @@ def register_template_global(value, name=None):
|
|
54
59
|
return value
|
55
60
|
|
56
61
|
|
57
|
-
def register_template_filter(
|
62
|
+
def register_template_filter(
|
63
|
+
func: Callable[..., Any], name: str | None = None
|
64
|
+
) -> Callable[..., Any]:
|
58
65
|
"""Adds a filter to the Jinja environment."""
|
59
|
-
|
66
|
+
filter_name = name if name is not None else func.__name__ # type: ignore[attr-defined]
|
67
|
+
environment.filters[filter_name] = func
|
60
68
|
return func
|
61
69
|
|
62
70
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import functools
|
2
2
|
from pathlib import Path
|
3
|
+
from typing import Any
|
3
4
|
|
4
5
|
from jinja2 import Environment, StrictUndefined
|
5
6
|
from jinja2.loaders import FileSystemLoader
|
@@ -11,7 +12,7 @@ from .filters import default_filters
|
|
11
12
|
from .globals import default_globals
|
12
13
|
|
13
14
|
|
14
|
-
def finalize_callable_error(obj):
|
15
|
+
def finalize_callable_error(obj: Any) -> Any:
|
15
16
|
"""Prevent direct rendering of a callable (likely just forgotten ()) by raising a TypeError"""
|
16
17
|
if callable(obj):
|
17
18
|
raise TypeError(f"{obj} is callable, did you forget parentheses?")
|
@@ -23,14 +24,14 @@ def finalize_callable_error(obj):
|
|
23
24
|
return obj
|
24
25
|
|
25
26
|
|
26
|
-
def get_template_dirs():
|
27
|
+
def get_template_dirs() -> tuple[Path, ...]:
|
27
28
|
jinja_templates = Path(__file__).parent / "templates"
|
28
29
|
app_templates = settings.path.parent / "templates"
|
29
30
|
return (jinja_templates, app_templates) + _get_app_template_dirs()
|
30
31
|
|
31
32
|
|
32
33
|
@functools.lru_cache
|
33
|
-
def _get_app_template_dirs():
|
34
|
+
def _get_app_template_dirs() -> tuple[Path, ...]:
|
34
35
|
"""
|
35
36
|
Return an iterable of paths of directories to load app templates from.
|
36
37
|
|
@@ -1,3 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
1
5
|
from jinja2 import nodes
|
2
6
|
from jinja2.ext import Extension
|
3
7
|
|
@@ -9,7 +13,7 @@ class InclusionTagExtension(Extension):
|
|
9
13
|
tags: set[str]
|
10
14
|
template_name: str
|
11
15
|
|
12
|
-
def parse(self, parser):
|
16
|
+
def parse(self, parser: Any) -> nodes.Node:
|
13
17
|
lineno = next(parser.stream).lineno
|
14
18
|
args = [
|
15
19
|
nodes.DerivedContextReference(),
|
@@ -28,12 +32,14 @@ class InclusionTagExtension(Extension):
|
|
28
32
|
call = self.call_method("_render", args=args, kwargs=kwargs, lineno=lineno)
|
29
33
|
return nodes.CallBlock(call, [], [], []).set_lineno(lineno)
|
30
34
|
|
31
|
-
def _render(self, context, *args, **kwargs):
|
35
|
+
def _render(self, context: dict[str, Any], *args: Any, **kwargs: Any) -> str:
|
32
36
|
context = self.get_context(context, *args, **kwargs)
|
33
37
|
template = self.environment.get_template(self.template_name)
|
34
38
|
return template.render(context)
|
35
39
|
|
36
|
-
def get_context(
|
40
|
+
def get_context(
|
41
|
+
self, context: dict[str, Any], *args: Any, **kwargs: Any
|
42
|
+
) -> dict[str, Any]:
|
37
43
|
raise NotImplementedError(
|
38
44
|
"You need to implement the `get_context` method in your subclass."
|
39
45
|
)
|
plain/templates/jinja/filters.py
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import datetime
|
2
4
|
from itertools import islice
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.utils.html import json_script
|
5
8
|
from plain.utils.timesince import timesince, timeuntil
|
6
9
|
from plain.utils.timezone import localtime
|
7
10
|
|
8
11
|
|
9
|
-
def localtime_filter(
|
12
|
+
def localtime_filter(
|
13
|
+
value: datetime.datetime | None, timezone: Any = None
|
14
|
+
) -> datetime.datetime:
|
10
15
|
"""Converts a datetime to local time in a template."""
|
11
16
|
if not value:
|
12
17
|
# Without this, we get the current localtime
|
@@ -15,7 +20,7 @@ def localtime_filter(value, timezone=None):
|
|
15
20
|
return localtime(value, timezone)
|
16
21
|
|
17
22
|
|
18
|
-
def pluralize_filter(value, singular="", plural="s"):
|
23
|
+
def pluralize_filter(value: Any, singular: str = "", plural: str = "s") -> str:
|
19
24
|
"""Returns plural suffix based on the value count.
|
20
25
|
|
21
26
|
Usage:
|
plain/templates/jinja/globals.py
CHANGED
@@ -5,7 +5,7 @@ from plain.urls import reverse
|
|
5
5
|
from plain.utils import timezone
|
6
6
|
|
7
7
|
|
8
|
-
def asset(url_path):
|
8
|
+
def asset(url_path: str) -> str:
|
9
9
|
# An explicit callable we can control, but also delay the import of asset.urls->views->templates
|
10
10
|
# for circular import reasons
|
11
11
|
from plain.assets.urls import get_asset_url
|