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/utils/functional.py
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import copy
|
2
4
|
import itertools
|
3
5
|
import operator
|
6
|
+
from collections.abc import Callable
|
4
7
|
from functools import total_ordering, wraps
|
8
|
+
from typing import Any, TypeVar
|
9
|
+
|
10
|
+
T = TypeVar("T")
|
5
11
|
|
6
12
|
|
7
13
|
class classproperty:
|
@@ -10,13 +16,13 @@ class classproperty:
|
|
10
16
|
that can be accessed directly from the class.
|
11
17
|
"""
|
12
18
|
|
13
|
-
def __init__(self, method=None):
|
19
|
+
def __init__(self, method: Callable[[type], Any] | None = None) -> None:
|
14
20
|
self.fget = method
|
15
21
|
|
16
|
-
def __get__(self, instance, cls=None):
|
22
|
+
def __get__(self, instance: Any, cls: type | None = None) -> Any:
|
17
23
|
return self.fget(cls)
|
18
24
|
|
19
|
-
def getter(self, method):
|
25
|
+
def getter(self, method: Callable[[type], Any]) -> classproperty:
|
20
26
|
self.fget = method
|
21
27
|
return self
|
22
28
|
|
@@ -30,7 +36,7 @@ class Promise:
|
|
30
36
|
pass
|
31
37
|
|
32
38
|
|
33
|
-
def lazy(func, *resultclasses):
|
39
|
+
def lazy(func: Callable[..., Any], *resultclasses: type) -> Callable[..., Any]:
|
34
40
|
"""
|
35
41
|
Turn any callable into a lazy evaluated callable. result classes or types
|
36
42
|
is required -- at least one is needed so that the automatic forcing of
|
@@ -48,24 +54,24 @@ def lazy(func, *resultclasses):
|
|
48
54
|
|
49
55
|
__prepared = False
|
50
56
|
|
51
|
-
def __init__(self, args, kw):
|
57
|
+
def __init__(self, args: tuple[Any, ...], kw: dict[str, Any]) -> None:
|
52
58
|
self.__args = args
|
53
59
|
self.__kw = kw
|
54
60
|
if not self.__prepared:
|
55
61
|
self.__prepare_class__()
|
56
62
|
self.__class__.__prepared = True
|
57
63
|
|
58
|
-
def __reduce__(self):
|
64
|
+
def __reduce__(self) -> tuple[Callable[..., Any], tuple[Any, ...]]:
|
59
65
|
return (
|
60
66
|
_lazy_proxy_unpickle,
|
61
67
|
(func, self.__args, self.__kw) + resultclasses,
|
62
68
|
)
|
63
69
|
|
64
|
-
def __repr__(self):
|
70
|
+
def __repr__(self) -> str:
|
65
71
|
return repr(self.__cast())
|
66
72
|
|
67
73
|
@classmethod
|
68
|
-
def __prepare_class__(cls):
|
74
|
+
def __prepare_class__(cls) -> None:
|
69
75
|
for resultclass in resultclasses:
|
70
76
|
for type_ in resultclass.mro():
|
71
77
|
for method_name in type_.__dict__:
|
@@ -87,9 +93,9 @@ def lazy(func, *resultclasses):
|
|
87
93
|
cls.__bytes__ = cls.__bytes_cast
|
88
94
|
|
89
95
|
@classmethod
|
90
|
-
def __promise__(cls, method_name):
|
96
|
+
def __promise__(cls, method_name: str) -> Callable[..., Any]:
|
91
97
|
# Builds a wrapper around some magic method
|
92
|
-
def __wrapper__(self, *args, **kw):
|
98
|
+
def __wrapper__(self: Any, *args: Any, **kw: Any) -> Any:
|
93
99
|
# Automatically triggers the evaluation of a lazy value and
|
94
100
|
# applies the given magic method of the result type.
|
95
101
|
res = func(*self.__args, **self.__kw)
|
@@ -97,16 +103,16 @@ def lazy(func, *resultclasses):
|
|
97
103
|
|
98
104
|
return __wrapper__
|
99
105
|
|
100
|
-
def __text_cast(self):
|
106
|
+
def __text_cast(self) -> str:
|
101
107
|
return func(*self.__args, **self.__kw)
|
102
108
|
|
103
|
-
def __bytes_cast(self):
|
109
|
+
def __bytes_cast(self) -> bytes:
|
104
110
|
return bytes(func(*self.__args, **self.__kw))
|
105
111
|
|
106
|
-
def __bytes_cast_encoded(self):
|
112
|
+
def __bytes_cast_encoded(self) -> bytes:
|
107
113
|
return func(*self.__args, **self.__kw).encode()
|
108
114
|
|
109
|
-
def __cast(self):
|
115
|
+
def __cast(self) -> Any:
|
110
116
|
if self._delegate_bytes:
|
111
117
|
return self.__bytes_cast()
|
112
118
|
elif self._delegate_text:
|
@@ -114,36 +120,36 @@ def lazy(func, *resultclasses):
|
|
114
120
|
else:
|
115
121
|
return func(*self.__args, **self.__kw)
|
116
122
|
|
117
|
-
def __str__(self):
|
123
|
+
def __str__(self) -> str:
|
118
124
|
# object defines __str__(), so __prepare_class__() won't overload
|
119
125
|
# a __str__() method from the proxied class.
|
120
126
|
return str(self.__cast())
|
121
127
|
|
122
|
-
def __eq__(self, other):
|
128
|
+
def __eq__(self, other: Any) -> bool:
|
123
129
|
if isinstance(other, Promise):
|
124
130
|
other = other.__cast()
|
125
131
|
return self.__cast() == other
|
126
132
|
|
127
|
-
def __lt__(self, other):
|
133
|
+
def __lt__(self, other: Any) -> bool:
|
128
134
|
if isinstance(other, Promise):
|
129
135
|
other = other.__cast()
|
130
136
|
return self.__cast() < other
|
131
137
|
|
132
|
-
def __hash__(self):
|
138
|
+
def __hash__(self) -> int:
|
133
139
|
return hash(self.__cast())
|
134
140
|
|
135
|
-
def __mod__(self, rhs):
|
141
|
+
def __mod__(self, rhs: Any) -> Any:
|
136
142
|
if self._delegate_text:
|
137
143
|
return str(self) % rhs
|
138
144
|
return self.__cast() % rhs
|
139
145
|
|
140
|
-
def __add__(self, other):
|
146
|
+
def __add__(self, other: Any) -> Any:
|
141
147
|
return self.__cast() + other
|
142
148
|
|
143
|
-
def __radd__(self, other):
|
149
|
+
def __radd__(self, other: Any) -> Any:
|
144
150
|
return other + self.__cast()
|
145
151
|
|
146
|
-
def __deepcopy__(self, memo):
|
152
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> __proxy__:
|
147
153
|
# Instances of this class are effectively immutable. It's just a
|
148
154
|
# collection of functions. So we don't need to do anything
|
149
155
|
# complicated for copying.
|
@@ -151,18 +157,25 @@ def lazy(func, *resultclasses):
|
|
151
157
|
return self
|
152
158
|
|
153
159
|
@wraps(func)
|
154
|
-
def __wrapper__(*args, **kw):
|
160
|
+
def __wrapper__(*args: Any, **kw: Any) -> __proxy__:
|
155
161
|
# Creates the proxy object, instead of the actual value.
|
156
162
|
return __proxy__(args, kw)
|
157
163
|
|
158
164
|
return __wrapper__
|
159
165
|
|
160
166
|
|
161
|
-
def _lazy_proxy_unpickle(
|
167
|
+
def _lazy_proxy_unpickle(
|
168
|
+
func: Callable[..., Any],
|
169
|
+
args: tuple[Any, ...],
|
170
|
+
kwargs: dict[str, Any],
|
171
|
+
*resultclasses: type,
|
172
|
+
) -> Any:
|
162
173
|
return lazy(func, *resultclasses)(*args, **kwargs)
|
163
174
|
|
164
175
|
|
165
|
-
def keep_lazy(
|
176
|
+
def keep_lazy(
|
177
|
+
*resultclasses: type,
|
178
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
166
179
|
"""
|
167
180
|
A decorator that allows a function to be called with one or more lazy
|
168
181
|
arguments. If none of the args are lazy, the function is evaluated
|
@@ -172,11 +185,11 @@ def keep_lazy(*resultclasses):
|
|
172
185
|
if not resultclasses:
|
173
186
|
raise TypeError("You must pass at least one argument to keep_lazy().")
|
174
187
|
|
175
|
-
def decorator(func):
|
188
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
176
189
|
lazy_func = lazy(func, *resultclasses)
|
177
190
|
|
178
191
|
@wraps(func)
|
179
|
-
def wrapper(*args, **kwargs):
|
192
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
180
193
|
if any(
|
181
194
|
isinstance(arg, Promise)
|
182
195
|
for arg in itertools.chain(args, kwargs.values())
|
@@ -189,7 +202,7 @@ def keep_lazy(*resultclasses):
|
|
189
202
|
return decorator
|
190
203
|
|
191
204
|
|
192
|
-
def keep_lazy_text(func):
|
205
|
+
def keep_lazy_text(func: Callable[..., Any]) -> Callable[..., Any]:
|
193
206
|
"""
|
194
207
|
A decorator for functions that accept lazy arguments and return text.
|
195
208
|
"""
|
@@ -199,14 +212,14 @@ def keep_lazy_text(func):
|
|
199
212
|
empty = object()
|
200
213
|
|
201
214
|
|
202
|
-
def new_method_proxy(func):
|
203
|
-
def inner(self, *args):
|
215
|
+
def new_method_proxy(func: Callable[..., Any]) -> Callable[..., Any]:
|
216
|
+
def inner(self: Any, *args: Any) -> Any:
|
204
217
|
if (_wrapped := self._wrapped) is empty:
|
205
218
|
self._setup()
|
206
219
|
_wrapped = self._wrapped
|
207
220
|
return func(_wrapped, *args)
|
208
221
|
|
209
|
-
inner._mask_wrapped = False
|
222
|
+
inner._mask_wrapped = False # type: ignore[attr-defined]
|
210
223
|
return inner
|
211
224
|
|
212
225
|
|
@@ -227,7 +240,7 @@ class LazyObject:
|
|
227
240
|
# override __copy__() and __deepcopy__() as well.
|
228
241
|
self._wrapped = empty
|
229
242
|
|
230
|
-
def __getattribute__(self, name):
|
243
|
+
def __getattribute__(self, name: str) -> Any:
|
231
244
|
if name == "_wrapped":
|
232
245
|
# Avoid recursion when getting wrapped object.
|
233
246
|
return super().__getattribute__(name)
|
@@ -240,7 +253,7 @@ class LazyObject:
|
|
240
253
|
|
241
254
|
__getattr__ = new_method_proxy(getattr)
|
242
255
|
|
243
|
-
def __setattr__(self, name, value):
|
256
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
244
257
|
if name == "_wrapped":
|
245
258
|
# Assign to __dict__ to avoid infinite __setattr__ loops.
|
246
259
|
self.__dict__["_wrapped"] = value
|
@@ -249,14 +262,14 @@ class LazyObject:
|
|
249
262
|
self._setup()
|
250
263
|
setattr(self._wrapped, name, value)
|
251
264
|
|
252
|
-
def __delattr__(self, name):
|
265
|
+
def __delattr__(self, name: str) -> None:
|
253
266
|
if name == "_wrapped":
|
254
267
|
raise TypeError("can't delete _wrapped.")
|
255
268
|
if self._wrapped is empty:
|
256
269
|
self._setup()
|
257
270
|
delattr(self._wrapped, name)
|
258
271
|
|
259
|
-
def _setup(self):
|
272
|
+
def _setup(self) -> None:
|
260
273
|
"""
|
261
274
|
Must be implemented by subclasses to initialize the wrapped object.
|
262
275
|
"""
|
@@ -278,12 +291,12 @@ class LazyObject:
|
|
278
291
|
# pickle the wrapped object as the unpickler's argument, so that pickle
|
279
292
|
# will pickle it normally, and then the unpickler simply returns its
|
280
293
|
# argument.
|
281
|
-
def __reduce__(self):
|
294
|
+
def __reduce__(self) -> tuple[Callable[[Any], Any], tuple[Any, ...]]:
|
282
295
|
if self._wrapped is empty:
|
283
296
|
self._setup()
|
284
297
|
return (unpickle_lazyobject, (self._wrapped,))
|
285
298
|
|
286
|
-
def __copy__(self):
|
299
|
+
def __copy__(self) -> LazyObject | Any:
|
287
300
|
if self._wrapped is empty:
|
288
301
|
# If uninitialized, copy the wrapper. Use type(self), not
|
289
302
|
# self.__class__, because the latter is proxied.
|
@@ -292,7 +305,7 @@ class LazyObject:
|
|
292
305
|
# If initialized, return a copy of the wrapped object.
|
293
306
|
return copy.copy(self._wrapped)
|
294
307
|
|
295
|
-
def __deepcopy__(self, memo):
|
308
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> LazyObject | Any:
|
296
309
|
if self._wrapped is empty:
|
297
310
|
# We have to use type(self), not self.__class__, because the
|
298
311
|
# latter is proxied.
|
@@ -326,7 +339,7 @@ class LazyObject:
|
|
326
339
|
__contains__ = new_method_proxy(operator.contains)
|
327
340
|
|
328
341
|
|
329
|
-
def unpickle_lazyobject(wrapped):
|
342
|
+
def unpickle_lazyobject(wrapped: Any) -> Any:
|
330
343
|
"""
|
331
344
|
Used to unpickle lazy objects. Just return its argument, which will be the
|
332
345
|
wrapped object.
|
@@ -342,7 +355,7 @@ class SimpleLazyObject(LazyObject):
|
|
342
355
|
known type, use plain.utils.functional.lazy.
|
343
356
|
"""
|
344
357
|
|
345
|
-
def __init__(self, func):
|
358
|
+
def __init__(self, func: Callable[[], Any]) -> None:
|
346
359
|
"""
|
347
360
|
Pass in a callable that returns the object to be wrapped.
|
348
361
|
|
@@ -354,19 +367,19 @@ class SimpleLazyObject(LazyObject):
|
|
354
367
|
self.__dict__["_setupfunc"] = func
|
355
368
|
super().__init__()
|
356
369
|
|
357
|
-
def _setup(self):
|
370
|
+
def _setup(self) -> None:
|
358
371
|
self._wrapped = self._setupfunc()
|
359
372
|
|
360
373
|
# Return a meaningful representation of the lazy object for debugging
|
361
374
|
# without evaluating the wrapped object.
|
362
|
-
def __repr__(self):
|
375
|
+
def __repr__(self) -> str:
|
363
376
|
if self._wrapped is empty:
|
364
377
|
repr_attr = self._setupfunc
|
365
378
|
else:
|
366
379
|
repr_attr = self._wrapped
|
367
380
|
return f"<{type(self).__name__}: {repr_attr!r}>"
|
368
381
|
|
369
|
-
def __copy__(self):
|
382
|
+
def __copy__(self) -> SimpleLazyObject | Any:
|
370
383
|
if self._wrapped is empty:
|
371
384
|
# If uninitialized, copy the wrapper. Use SimpleLazyObject, not
|
372
385
|
# self.__class__, because the latter is proxied.
|
@@ -375,7 +388,7 @@ class SimpleLazyObject(LazyObject):
|
|
375
388
|
# If initialized, return a copy of the wrapped object.
|
376
389
|
return copy.copy(self._wrapped)
|
377
390
|
|
378
|
-
def __deepcopy__(self, memo):
|
391
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> SimpleLazyObject | Any:
|
379
392
|
if self._wrapped is empty:
|
380
393
|
# We have to use SimpleLazyObject, not self.__class__, because the
|
381
394
|
# latter is proxied.
|
@@ -387,11 +400,13 @@ class SimpleLazyObject(LazyObject):
|
|
387
400
|
__add__ = new_method_proxy(operator.add)
|
388
401
|
|
389
402
|
@new_method_proxy
|
390
|
-
def __radd__(self, other):
|
403
|
+
def __radd__(self: Any, other: Any) -> Any:
|
391
404
|
return other + self
|
392
405
|
|
393
406
|
|
394
|
-
def partition(
|
407
|
+
def partition(
|
408
|
+
predicate: Callable[[Any], bool], values: Any
|
409
|
+
) -> tuple[list[Any], list[Any]]:
|
395
410
|
"""
|
396
411
|
Split the values into two sets, based on the return value of the function
|
397
412
|
(True/False). e.g.:
|
@@ -399,7 +414,7 @@ def partition(predicate, values):
|
|
399
414
|
>>> partition(lambda x: x > 3, range(5))
|
400
415
|
[0, 1, 2, 3], [4]
|
401
416
|
"""
|
402
|
-
results = ([], [])
|
417
|
+
results: tuple[list[Any], list[Any]] = ([], [])
|
403
418
|
for item in values:
|
404
419
|
results[predicate(item)].append(item)
|
405
420
|
return results
|
plain/utils/hashable.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
1
5
|
from plain.utils.itercompat import is_iterable
|
2
6
|
|
3
7
|
|
4
|
-
def make_hashable(value):
|
8
|
+
def make_hashable(value: Any) -> Any:
|
5
9
|
"""
|
6
10
|
Attempt to make value hashable or raise a TypeError if it fails.
|
7
11
|
|
plain/utils/html.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
"""HTML utilities suitable for global use."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
import html
|
4
6
|
import json
|
5
7
|
from html.parser import HTMLParser
|
8
|
+
from typing import Any
|
6
9
|
|
7
10
|
from plain.utils.functional import Promise, keep_lazy, keep_lazy_text
|
8
11
|
from plain.utils.safestring import SafeString, mark_safe
|
9
12
|
|
10
13
|
|
11
14
|
@keep_lazy(SafeString)
|
12
|
-
def escape(text):
|
15
|
+
def escape(text: Any) -> SafeString:
|
13
16
|
"""
|
14
17
|
Return the given text with ampersands, quotes and angle brackets encoded
|
15
18
|
for use in HTML.
|
@@ -28,7 +31,11 @@ _json_script_escapes = {
|
|
28
31
|
}
|
29
32
|
|
30
33
|
|
31
|
-
def json_script(
|
34
|
+
def json_script(
|
35
|
+
value: Any,
|
36
|
+
element_id: str | None = None,
|
37
|
+
encoder: type[json.JSONEncoder] | None = None,
|
38
|
+
) -> SafeString:
|
32
39
|
"""
|
33
40
|
Escape all the HTML/XML special characters with their unicode escapes, so
|
34
41
|
value is safe to be output anywhere except for inside a tag attribute. Wrap
|
@@ -48,7 +55,7 @@ def json_script(value, element_id=None, encoder=None):
|
|
48
55
|
return format_html(template, *args)
|
49
56
|
|
50
57
|
|
51
|
-
def conditional_escape(text):
|
58
|
+
def conditional_escape(text: Any) -> SafeString | str:
|
52
59
|
"""
|
53
60
|
Similar to escape(), except that it doesn't operate on pre-escaped strings.
|
54
61
|
|
@@ -58,12 +65,12 @@ def conditional_escape(text):
|
|
58
65
|
if isinstance(text, Promise):
|
59
66
|
text = str(text)
|
60
67
|
if hasattr(text, "__html__"):
|
61
|
-
return text.__html__()
|
68
|
+
return text.__html__() # type: ignore[call-non-callable]
|
62
69
|
else:
|
63
70
|
return escape(text)
|
64
71
|
|
65
72
|
|
66
|
-
def format_html(format_string, *args, **kwargs):
|
73
|
+
def format_html(format_string: str, *args: Any, **kwargs: Any) -> SafeString:
|
67
74
|
"""
|
68
75
|
Similar to str.format, but pass all arguments through conditional_escape(),
|
69
76
|
and call mark_safe() on the result. This function should be used instead
|
@@ -75,25 +82,25 @@ def format_html(format_string, *args, **kwargs):
|
|
75
82
|
|
76
83
|
|
77
84
|
class MLStripper(HTMLParser):
|
78
|
-
def __init__(self):
|
85
|
+
def __init__(self) -> None:
|
79
86
|
super().__init__(convert_charrefs=False)
|
80
87
|
self.reset()
|
81
|
-
self.fed = []
|
88
|
+
self.fed: list[str] = []
|
82
89
|
|
83
|
-
def handle_data(self, d):
|
90
|
+
def handle_data(self, d: str) -> None:
|
84
91
|
self.fed.append(d)
|
85
92
|
|
86
|
-
def handle_entityref(self, name):
|
93
|
+
def handle_entityref(self, name: str) -> None:
|
87
94
|
self.fed.append(f"&{name};")
|
88
95
|
|
89
|
-
def handle_charref(self, name):
|
96
|
+
def handle_charref(self, name: str) -> None:
|
90
97
|
self.fed.append(f"&#{name};")
|
91
98
|
|
92
|
-
def get_data(self):
|
99
|
+
def get_data(self) -> str:
|
93
100
|
return "".join(self.fed)
|
94
101
|
|
95
102
|
|
96
|
-
def _strip_once(value):
|
103
|
+
def _strip_once(value: str) -> str:
|
97
104
|
"""
|
98
105
|
Internal tag stripping utility used by strip_tags.
|
99
106
|
"""
|
@@ -104,7 +111,7 @@ def _strip_once(value):
|
|
104
111
|
|
105
112
|
|
106
113
|
@keep_lazy_text
|
107
|
-
def strip_tags(value):
|
114
|
+
def strip_tags(value: Any) -> str:
|
108
115
|
"""Return the given HTML with all tags stripped."""
|
109
116
|
# Note: in typical case this loop executes _strip_once once. Loop condition
|
110
117
|
# is redundant, but helps to reduce number of executions of _strip_once.
|
@@ -118,7 +125,7 @@ def strip_tags(value):
|
|
118
125
|
return value
|
119
126
|
|
120
127
|
|
121
|
-
def avoid_wrapping(value):
|
128
|
+
def avoid_wrapping(value: str) -> str:
|
122
129
|
"""
|
123
130
|
Avoid text wrapping in the middle of a phrase by adding non-breaking
|
124
131
|
spaces where there previously were normal spaces.
|
plain/utils/http.py
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Generator, Iterable
|
1
4
|
from email.utils import formatdate
|
5
|
+
from typing import Any
|
2
6
|
from urllib.parse import quote, unquote
|
3
7
|
from urllib.parse import urlencode as original_urlencode
|
4
8
|
|
@@ -7,7 +11,10 @@ from plain.utils.datastructures import MultiValueDict
|
|
7
11
|
RFC3986_SUBDELIMS = "!$&'()*+,;="
|
8
12
|
|
9
13
|
|
10
|
-
def urlencode(
|
14
|
+
def urlencode(
|
15
|
+
query: MultiValueDict | dict[str, Any] | Iterable[tuple[str, Any]],
|
16
|
+
doseq: bool = False,
|
17
|
+
) -> str:
|
11
18
|
"""
|
12
19
|
A version of Python's urllib.parse.urlencode() function that can operate on
|
13
20
|
MultiValueDict and non-string values.
|
@@ -15,7 +22,7 @@ def urlencode(query, doseq=False):
|
|
15
22
|
if isinstance(query, MultiValueDict):
|
16
23
|
query = query.lists()
|
17
24
|
elif hasattr(query, "items"):
|
18
|
-
query = query.items()
|
25
|
+
query = query.items() # type: ignore[assignment]
|
19
26
|
query_params = []
|
20
27
|
for key, value in query:
|
21
28
|
if value is None:
|
@@ -48,7 +55,7 @@ def urlencode(query, doseq=False):
|
|
48
55
|
return original_urlencode(query_params, doseq)
|
49
56
|
|
50
57
|
|
51
|
-
def http_date(epoch_seconds=None):
|
58
|
+
def http_date(epoch_seconds: float | None = None) -> str:
|
52
59
|
"""
|
53
60
|
Format the time to match the RFC 5322 date format as specified by RFC 9110
|
54
61
|
Section 5.6.7.
|
@@ -65,7 +72,7 @@ def http_date(epoch_seconds=None):
|
|
65
72
|
# Base 36 functions: useful for generating compact URLs
|
66
73
|
|
67
74
|
|
68
|
-
def base36_to_int(s):
|
75
|
+
def base36_to_int(s: str) -> int:
|
69
76
|
"""
|
70
77
|
Convert a base 36 string to an int. Raise ValueError if the input won't fit
|
71
78
|
into an int.
|
@@ -78,7 +85,7 @@ def base36_to_int(s):
|
|
78
85
|
return int(s, 36)
|
79
86
|
|
80
87
|
|
81
|
-
def int_to_base36(i):
|
88
|
+
def int_to_base36(i: int) -> str:
|
82
89
|
"""Convert an integer to a base36 string."""
|
83
90
|
char_set = "0123456789abcdefghijklmnopqrstuvwxyz"
|
84
91
|
if i < 0:
|
@@ -92,7 +99,7 @@ def int_to_base36(i):
|
|
92
99
|
return b36
|
93
100
|
|
94
101
|
|
95
|
-
def escape_leading_slashes(url):
|
102
|
+
def escape_leading_slashes(url: str) -> str:
|
96
103
|
"""
|
97
104
|
If redirecting to an absolute path (two leading slashes), a slash must be
|
98
105
|
escaped to prevent browsers from handling the path as schemaless and
|
@@ -103,7 +110,7 @@ def escape_leading_slashes(url):
|
|
103
110
|
return url
|
104
111
|
|
105
112
|
|
106
|
-
def _parseparam(s):
|
113
|
+
def _parseparam(s: str) -> Generator[str, None, None]:
|
107
114
|
while s[:1] == ";":
|
108
115
|
s = s[1:]
|
109
116
|
end = s.find(";")
|
@@ -116,7 +123,7 @@ def _parseparam(s):
|
|
116
123
|
s = s[end:]
|
117
124
|
|
118
125
|
|
119
|
-
def parse_header_parameters(line):
|
126
|
+
def parse_header_parameters(line: str) -> tuple[str, dict[str, str]]:
|
120
127
|
"""
|
121
128
|
Parse a Content-type like header.
|
122
129
|
Return the main content-type and a dictionary of options.
|
@@ -146,7 +153,7 @@ def parse_header_parameters(line):
|
|
146
153
|
return key, pdict
|
147
154
|
|
148
155
|
|
149
|
-
def content_disposition_header(as_attachment, filename):
|
156
|
+
def content_disposition_header(as_attachment: bool, filename: str) -> str | None:
|
150
157
|
"""
|
151
158
|
Construct a Content-Disposition HTTP header value from the given filename
|
152
159
|
as specified by RFC 6266.
|
plain/utils/inspect.py
CHANGED
@@ -1,22 +1,30 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import functools
|
2
4
|
import inspect
|
5
|
+
from collections.abc import Callable
|
6
|
+
from typing import Any
|
3
7
|
|
4
8
|
|
5
9
|
@functools.lru_cache(maxsize=512)
|
6
|
-
def _get_func_parameters(
|
10
|
+
def _get_func_parameters(
|
11
|
+
func: Callable[..., Any], remove_first: bool
|
12
|
+
) -> tuple[inspect.Parameter, ...]:
|
7
13
|
parameters = tuple(inspect.signature(func).parameters.values())
|
8
14
|
if remove_first:
|
9
15
|
parameters = parameters[1:]
|
10
16
|
return parameters
|
11
17
|
|
12
18
|
|
13
|
-
def _get_callable_parameters(
|
19
|
+
def _get_callable_parameters(
|
20
|
+
meth_or_func: Callable[..., Any],
|
21
|
+
) -> tuple[inspect.Parameter, ...]:
|
14
22
|
is_method = inspect.ismethod(meth_or_func)
|
15
|
-
func = meth_or_func.__func__ if is_method else meth_or_func
|
23
|
+
func = meth_or_func.__func__ if is_method else meth_or_func # type: ignore[attr-defined]
|
16
24
|
return _get_func_parameters(func, remove_first=is_method)
|
17
25
|
|
18
26
|
|
19
|
-
def get_func_args(func):
|
27
|
+
def get_func_args(func: Callable[..., Any]) -> list[str]:
|
20
28
|
params = _get_callable_parameters(func)
|
21
29
|
return [
|
22
30
|
param.name
|
@@ -25,12 +33,12 @@ def get_func_args(func):
|
|
25
33
|
]
|
26
34
|
|
27
35
|
|
28
|
-
def func_accepts_kwargs(func):
|
36
|
+
def func_accepts_kwargs(func: Callable[..., Any]) -> bool:
|
29
37
|
"""Return True if function 'func' accepts keyword arguments **kwargs."""
|
30
38
|
return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_KEYWORD)
|
31
39
|
|
32
40
|
|
33
|
-
def method_has_no_args(meth):
|
41
|
+
def method_has_no_args(meth: Callable[..., Any]) -> bool:
|
34
42
|
"""Return True if a method only accepts 'self'."""
|
35
43
|
count = len(
|
36
44
|
[p for p in _get_callable_parameters(meth) if p.kind == p.POSITIONAL_OR_KEYWORD]
|
plain/utils/ipv6.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import ipaddress
|
2
4
|
|
3
5
|
from plain.exceptions import ValidationError
|
4
6
|
|
5
7
|
|
6
8
|
def clean_ipv6_address(
|
7
|
-
ip_str
|
8
|
-
|
9
|
+
ip_str: str,
|
10
|
+
unpack_ipv4: bool = False,
|
11
|
+
error_message: str = "This is not a valid IPv6 address.",
|
12
|
+
) -> str:
|
9
13
|
"""
|
10
14
|
Clean an IPv6 address string.
|
11
15
|
|
@@ -35,7 +39,7 @@ def clean_ipv6_address(
|
|
35
39
|
return str(addr)
|
36
40
|
|
37
41
|
|
38
|
-
def is_valid_ipv6_address(ip_str):
|
42
|
+
def is_valid_ipv6_address(ip_str: str) -> bool:
|
39
43
|
"""
|
40
44
|
Return whether or not the `ip_str` string is a valid IPv6 address.
|
41
45
|
"""
|
plain/utils/itercompat.py
CHANGED
plain/utils/module_loading.py
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
import sys
|
3
5
|
from importlib import import_module
|
6
|
+
from types import ModuleType
|
7
|
+
from typing import Any
|
4
8
|
|
5
9
|
|
6
|
-
def cached_import(module_path, class_name):
|
10
|
+
def cached_import(module_path: str, class_name: str) -> Any:
|
7
11
|
# Check whether module is loaded and fully initialized.
|
8
12
|
if not (
|
9
13
|
(module := sys.modules.get(module_path))
|
@@ -14,7 +18,7 @@ def cached_import(module_path, class_name):
|
|
14
18
|
return getattr(module, class_name)
|
15
19
|
|
16
20
|
|
17
|
-
def import_string(dotted_path):
|
21
|
+
def import_string(dotted_path: str) -> Any:
|
18
22
|
"""
|
19
23
|
Import a dotted module path and return the attribute/class designated by the
|
20
24
|
last name in the path. Raise ImportError if the import failed.
|
@@ -32,7 +36,7 @@ def import_string(dotted_path):
|
|
32
36
|
) from err
|
33
37
|
|
34
38
|
|
35
|
-
def module_dir(module):
|
39
|
+
def module_dir(module: ModuleType) -> str:
|
36
40
|
"""
|
37
41
|
Find the name of the directory that contains a module, if possible.
|
38
42
|
|