plain 0.68.0__py3-none-any.whl → 0.103.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/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +234 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- 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 +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.103.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,15 @@ 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:
|
|
23
|
+
assert cls is not None, "cls must be provided for classproperty"
|
|
24
|
+
assert self.fget is not None, "fget must be set before accessing classproperty"
|
|
17
25
|
return self.fget(cls)
|
|
18
26
|
|
|
19
|
-
def getter(self, method):
|
|
27
|
+
def getter(self, method: Callable[[type], Any]) -> classproperty:
|
|
20
28
|
self.fget = method
|
|
21
29
|
return self
|
|
22
30
|
|
|
@@ -30,7 +38,7 @@ class Promise:
|
|
|
30
38
|
pass
|
|
31
39
|
|
|
32
40
|
|
|
33
|
-
def lazy(func, *resultclasses):
|
|
41
|
+
def lazy(func: Callable[..., Any], *resultclasses: type) -> Callable[..., Any]:
|
|
34
42
|
"""
|
|
35
43
|
Turn any callable into a lazy evaluated callable. result classes or types
|
|
36
44
|
is required -- at least one is needed so that the automatic forcing of
|
|
@@ -48,24 +56,24 @@ def lazy(func, *resultclasses):
|
|
|
48
56
|
|
|
49
57
|
__prepared = False
|
|
50
58
|
|
|
51
|
-
def __init__(self, args, kw):
|
|
59
|
+
def __init__(self, args: tuple[Any, ...], kw: dict[str, Any]) -> None:
|
|
52
60
|
self.__args = args
|
|
53
61
|
self.__kw = kw
|
|
54
62
|
if not self.__prepared:
|
|
55
63
|
self.__prepare_class__()
|
|
56
64
|
self.__class__.__prepared = True
|
|
57
65
|
|
|
58
|
-
def __reduce__(self):
|
|
66
|
+
def __reduce__(self) -> tuple[Callable[..., Any], tuple[Any, ...]]:
|
|
59
67
|
return (
|
|
60
68
|
_lazy_proxy_unpickle,
|
|
61
69
|
(func, self.__args, self.__kw) + resultclasses,
|
|
62
70
|
)
|
|
63
71
|
|
|
64
|
-
def __repr__(self):
|
|
72
|
+
def __repr__(self) -> str:
|
|
65
73
|
return repr(self.__cast())
|
|
66
74
|
|
|
67
75
|
@classmethod
|
|
68
|
-
def __prepare_class__(cls):
|
|
76
|
+
def __prepare_class__(cls) -> None:
|
|
69
77
|
for resultclass in resultclasses:
|
|
70
78
|
for type_ in resultclass.mro():
|
|
71
79
|
for method_name in type_.__dict__:
|
|
@@ -82,14 +90,14 @@ def lazy(func, *resultclasses):
|
|
|
82
90
|
"Cannot call lazy() with both bytes and text return types."
|
|
83
91
|
)
|
|
84
92
|
if cls._delegate_text:
|
|
85
|
-
cls
|
|
93
|
+
setattr(cls, "__str__", cls.__text_cast)
|
|
86
94
|
elif cls._delegate_bytes:
|
|
87
|
-
cls
|
|
95
|
+
setattr(cls, "__bytes__", cls.__bytes_cast)
|
|
88
96
|
|
|
89
97
|
@classmethod
|
|
90
|
-
def __promise__(cls, method_name):
|
|
98
|
+
def __promise__(cls, method_name: str) -> Callable[..., Any]:
|
|
91
99
|
# Builds a wrapper around some magic method
|
|
92
|
-
def __wrapper__(self, *args, **kw):
|
|
100
|
+
def __wrapper__(self: Any, *args: Any, **kw: Any) -> Any:
|
|
93
101
|
# Automatically triggers the evaluation of a lazy value and
|
|
94
102
|
# applies the given magic method of the result type.
|
|
95
103
|
res = func(*self.__args, **self.__kw)
|
|
@@ -97,16 +105,16 @@ def lazy(func, *resultclasses):
|
|
|
97
105
|
|
|
98
106
|
return __wrapper__
|
|
99
107
|
|
|
100
|
-
def __text_cast(self):
|
|
108
|
+
def __text_cast(self) -> str:
|
|
101
109
|
return func(*self.__args, **self.__kw)
|
|
102
110
|
|
|
103
|
-
def __bytes_cast(self):
|
|
111
|
+
def __bytes_cast(self) -> bytes:
|
|
104
112
|
return bytes(func(*self.__args, **self.__kw))
|
|
105
113
|
|
|
106
|
-
def __bytes_cast_encoded(self):
|
|
114
|
+
def __bytes_cast_encoded(self) -> bytes:
|
|
107
115
|
return func(*self.__args, **self.__kw).encode()
|
|
108
116
|
|
|
109
|
-
def __cast(self):
|
|
117
|
+
def __cast(self) -> Any:
|
|
110
118
|
if self._delegate_bytes:
|
|
111
119
|
return self.__bytes_cast()
|
|
112
120
|
elif self._delegate_text:
|
|
@@ -114,36 +122,36 @@ def lazy(func, *resultclasses):
|
|
|
114
122
|
else:
|
|
115
123
|
return func(*self.__args, **self.__kw)
|
|
116
124
|
|
|
117
|
-
def __str__(self):
|
|
125
|
+
def __str__(self) -> str:
|
|
118
126
|
# object defines __str__(), so __prepare_class__() won't overload
|
|
119
127
|
# a __str__() method from the proxied class.
|
|
120
128
|
return str(self.__cast())
|
|
121
129
|
|
|
122
|
-
def __eq__(self, other):
|
|
130
|
+
def __eq__(self, other: Any) -> bool:
|
|
123
131
|
if isinstance(other, Promise):
|
|
124
132
|
other = other.__cast()
|
|
125
133
|
return self.__cast() == other
|
|
126
134
|
|
|
127
|
-
def __lt__(self, other):
|
|
135
|
+
def __lt__(self, other: Any) -> bool:
|
|
128
136
|
if isinstance(other, Promise):
|
|
129
137
|
other = other.__cast()
|
|
130
138
|
return self.__cast() < other
|
|
131
139
|
|
|
132
|
-
def __hash__(self):
|
|
140
|
+
def __hash__(self) -> int:
|
|
133
141
|
return hash(self.__cast())
|
|
134
142
|
|
|
135
|
-
def __mod__(self, rhs):
|
|
143
|
+
def __mod__(self, rhs: Any) -> Any:
|
|
136
144
|
if self._delegate_text:
|
|
137
145
|
return str(self) % rhs
|
|
138
146
|
return self.__cast() % rhs
|
|
139
147
|
|
|
140
|
-
def __add__(self, other):
|
|
148
|
+
def __add__(self, other: Any) -> Any:
|
|
141
149
|
return self.__cast() + other
|
|
142
150
|
|
|
143
|
-
def __radd__(self, other):
|
|
151
|
+
def __radd__(self, other: Any) -> Any:
|
|
144
152
|
return other + self.__cast()
|
|
145
153
|
|
|
146
|
-
def __deepcopy__(self, memo):
|
|
154
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> __proxy__:
|
|
147
155
|
# Instances of this class are effectively immutable. It's just a
|
|
148
156
|
# collection of functions. So we don't need to do anything
|
|
149
157
|
# complicated for copying.
|
|
@@ -151,18 +159,25 @@ def lazy(func, *resultclasses):
|
|
|
151
159
|
return self
|
|
152
160
|
|
|
153
161
|
@wraps(func)
|
|
154
|
-
def __wrapper__(*args, **kw):
|
|
162
|
+
def __wrapper__(*args: Any, **kw: Any) -> __proxy__:
|
|
155
163
|
# Creates the proxy object, instead of the actual value.
|
|
156
164
|
return __proxy__(args, kw)
|
|
157
165
|
|
|
158
166
|
return __wrapper__
|
|
159
167
|
|
|
160
168
|
|
|
161
|
-
def _lazy_proxy_unpickle(
|
|
169
|
+
def _lazy_proxy_unpickle(
|
|
170
|
+
func: Callable[..., Any],
|
|
171
|
+
args: tuple[Any, ...],
|
|
172
|
+
kwargs: dict[str, Any],
|
|
173
|
+
*resultclasses: type,
|
|
174
|
+
) -> Any:
|
|
162
175
|
return lazy(func, *resultclasses)(*args, **kwargs)
|
|
163
176
|
|
|
164
177
|
|
|
165
|
-
def keep_lazy(
|
|
178
|
+
def keep_lazy(
|
|
179
|
+
*resultclasses: type,
|
|
180
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
166
181
|
"""
|
|
167
182
|
A decorator that allows a function to be called with one or more lazy
|
|
168
183
|
arguments. If none of the args are lazy, the function is evaluated
|
|
@@ -172,11 +187,11 @@ def keep_lazy(*resultclasses):
|
|
|
172
187
|
if not resultclasses:
|
|
173
188
|
raise TypeError("You must pass at least one argument to keep_lazy().")
|
|
174
189
|
|
|
175
|
-
def decorator(func):
|
|
190
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
176
191
|
lazy_func = lazy(func, *resultclasses)
|
|
177
192
|
|
|
178
193
|
@wraps(func)
|
|
179
|
-
def wrapper(*args, **kwargs):
|
|
194
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
180
195
|
if any(
|
|
181
196
|
isinstance(arg, Promise)
|
|
182
197
|
for arg in itertools.chain(args, kwargs.values())
|
|
@@ -189,7 +204,7 @@ def keep_lazy(*resultclasses):
|
|
|
189
204
|
return decorator
|
|
190
205
|
|
|
191
206
|
|
|
192
|
-
def keep_lazy_text(func):
|
|
207
|
+
def keep_lazy_text(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
193
208
|
"""
|
|
194
209
|
A decorator for functions that accept lazy arguments and return text.
|
|
195
210
|
"""
|
|
@@ -199,14 +214,14 @@ def keep_lazy_text(func):
|
|
|
199
214
|
empty = object()
|
|
200
215
|
|
|
201
216
|
|
|
202
|
-
def new_method_proxy(func):
|
|
203
|
-
def inner(self, *args):
|
|
217
|
+
def new_method_proxy(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
218
|
+
def inner(self: Any, *args: Any) -> Any:
|
|
204
219
|
if (_wrapped := self._wrapped) is empty:
|
|
205
220
|
self._setup()
|
|
206
221
|
_wrapped = self._wrapped
|
|
207
222
|
return func(_wrapped, *args)
|
|
208
223
|
|
|
209
|
-
inner._mask_wrapped = False
|
|
224
|
+
inner._mask_wrapped = False # type: ignore[attr-defined]
|
|
210
225
|
return inner
|
|
211
226
|
|
|
212
227
|
|
|
@@ -227,7 +242,7 @@ class LazyObject:
|
|
|
227
242
|
# override __copy__() and __deepcopy__() as well.
|
|
228
243
|
self._wrapped = empty
|
|
229
244
|
|
|
230
|
-
def __getattribute__(self, name):
|
|
245
|
+
def __getattribute__(self, name: str) -> Any:
|
|
231
246
|
if name == "_wrapped":
|
|
232
247
|
# Avoid recursion when getting wrapped object.
|
|
233
248
|
return super().__getattribute__(name)
|
|
@@ -240,7 +255,7 @@ class LazyObject:
|
|
|
240
255
|
|
|
241
256
|
__getattr__ = new_method_proxy(getattr)
|
|
242
257
|
|
|
243
|
-
def __setattr__(self, name, value):
|
|
258
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
244
259
|
if name == "_wrapped":
|
|
245
260
|
# Assign to __dict__ to avoid infinite __setattr__ loops.
|
|
246
261
|
self.__dict__["_wrapped"] = value
|
|
@@ -249,14 +264,14 @@ class LazyObject:
|
|
|
249
264
|
self._setup()
|
|
250
265
|
setattr(self._wrapped, name, value)
|
|
251
266
|
|
|
252
|
-
def __delattr__(self, name):
|
|
267
|
+
def __delattr__(self, name: str) -> None:
|
|
253
268
|
if name == "_wrapped":
|
|
254
269
|
raise TypeError("can't delete _wrapped.")
|
|
255
270
|
if self._wrapped is empty:
|
|
256
271
|
self._setup()
|
|
257
272
|
delattr(self._wrapped, name)
|
|
258
273
|
|
|
259
|
-
def _setup(self):
|
|
274
|
+
def _setup(self) -> None:
|
|
260
275
|
"""
|
|
261
276
|
Must be implemented by subclasses to initialize the wrapped object.
|
|
262
277
|
"""
|
|
@@ -278,12 +293,12 @@ class LazyObject:
|
|
|
278
293
|
# pickle the wrapped object as the unpickler's argument, so that pickle
|
|
279
294
|
# will pickle it normally, and then the unpickler simply returns its
|
|
280
295
|
# argument.
|
|
281
|
-
def __reduce__(self):
|
|
296
|
+
def __reduce__(self) -> tuple[Callable[[Any], Any], tuple[Any, ...]]:
|
|
282
297
|
if self._wrapped is empty:
|
|
283
298
|
self._setup()
|
|
284
299
|
return (unpickle_lazyobject, (self._wrapped,))
|
|
285
300
|
|
|
286
|
-
def __copy__(self):
|
|
301
|
+
def __copy__(self) -> LazyObject | Any:
|
|
287
302
|
if self._wrapped is empty:
|
|
288
303
|
# If uninitialized, copy the wrapper. Use type(self), not
|
|
289
304
|
# self.__class__, because the latter is proxied.
|
|
@@ -292,7 +307,7 @@ class LazyObject:
|
|
|
292
307
|
# If initialized, return a copy of the wrapped object.
|
|
293
308
|
return copy.copy(self._wrapped)
|
|
294
309
|
|
|
295
|
-
def __deepcopy__(self, memo):
|
|
310
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> LazyObject | Any:
|
|
296
311
|
if self._wrapped is empty:
|
|
297
312
|
# We have to use type(self), not self.__class__, because the
|
|
298
313
|
# latter is proxied.
|
|
@@ -326,7 +341,7 @@ class LazyObject:
|
|
|
326
341
|
__contains__ = new_method_proxy(operator.contains)
|
|
327
342
|
|
|
328
343
|
|
|
329
|
-
def unpickle_lazyobject(wrapped):
|
|
344
|
+
def unpickle_lazyobject(wrapped: Any) -> Any:
|
|
330
345
|
"""
|
|
331
346
|
Used to unpickle lazy objects. Just return its argument, which will be the
|
|
332
347
|
wrapped object.
|
|
@@ -342,7 +357,7 @@ class SimpleLazyObject(LazyObject):
|
|
|
342
357
|
known type, use plain.utils.functional.lazy.
|
|
343
358
|
"""
|
|
344
359
|
|
|
345
|
-
def __init__(self, func):
|
|
360
|
+
def __init__(self, func: Callable[[], Any]) -> None:
|
|
346
361
|
"""
|
|
347
362
|
Pass in a callable that returns the object to be wrapped.
|
|
348
363
|
|
|
@@ -354,19 +369,19 @@ class SimpleLazyObject(LazyObject):
|
|
|
354
369
|
self.__dict__["_setupfunc"] = func
|
|
355
370
|
super().__init__()
|
|
356
371
|
|
|
357
|
-
def _setup(self):
|
|
372
|
+
def _setup(self) -> None:
|
|
358
373
|
self._wrapped = self._setupfunc()
|
|
359
374
|
|
|
360
375
|
# Return a meaningful representation of the lazy object for debugging
|
|
361
376
|
# without evaluating the wrapped object.
|
|
362
|
-
def __repr__(self):
|
|
377
|
+
def __repr__(self) -> str:
|
|
363
378
|
if self._wrapped is empty:
|
|
364
379
|
repr_attr = self._setupfunc
|
|
365
380
|
else:
|
|
366
381
|
repr_attr = self._wrapped
|
|
367
382
|
return f"<{type(self).__name__}: {repr_attr!r}>"
|
|
368
383
|
|
|
369
|
-
def __copy__(self):
|
|
384
|
+
def __copy__(self) -> SimpleLazyObject | Any:
|
|
370
385
|
if self._wrapped is empty:
|
|
371
386
|
# If uninitialized, copy the wrapper. Use SimpleLazyObject, not
|
|
372
387
|
# self.__class__, because the latter is proxied.
|
|
@@ -375,7 +390,7 @@ class SimpleLazyObject(LazyObject):
|
|
|
375
390
|
# If initialized, return a copy of the wrapped object.
|
|
376
391
|
return copy.copy(self._wrapped)
|
|
377
392
|
|
|
378
|
-
def __deepcopy__(self, memo):
|
|
393
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> SimpleLazyObject | Any:
|
|
379
394
|
if self._wrapped is empty:
|
|
380
395
|
# We have to use SimpleLazyObject, not self.__class__, because the
|
|
381
396
|
# latter is proxied.
|
|
@@ -387,11 +402,13 @@ class SimpleLazyObject(LazyObject):
|
|
|
387
402
|
__add__ = new_method_proxy(operator.add)
|
|
388
403
|
|
|
389
404
|
@new_method_proxy
|
|
390
|
-
def __radd__(self, other):
|
|
405
|
+
def __radd__(self: Any, other: Any) -> Any:
|
|
391
406
|
return other + self
|
|
392
407
|
|
|
393
408
|
|
|
394
|
-
def partition(
|
|
409
|
+
def partition(
|
|
410
|
+
predicate: Callable[[Any], bool], values: Any
|
|
411
|
+
) -> tuple[list[Any], list[Any]]:
|
|
395
412
|
"""
|
|
396
413
|
Split the values into two sets, based on the return value of the function
|
|
397
414
|
(True/False). e.g.:
|
|
@@ -399,7 +416,7 @@ def partition(predicate, values):
|
|
|
399
416
|
>>> partition(lambda x: x > 3, range(5))
|
|
400
417
|
[0, 1, 2, 3], [4]
|
|
401
418
|
"""
|
|
402
|
-
results = ([], [])
|
|
419
|
+
results: tuple[list[Any], list[Any]] = ([], [])
|
|
403
420
|
for item in values:
|
|
404
421
|
results[predicate(item)].append(item)
|
|
405
422
|
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,19 @@
|
|
|
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
|
|
|
10
|
+
from plain.internal import internalcode
|
|
7
11
|
from plain.utils.functional import Promise, keep_lazy, keep_lazy_text
|
|
8
12
|
from plain.utils.safestring import SafeString, mark_safe
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
@keep_lazy(SafeString)
|
|
12
|
-
def escape(text):
|
|
16
|
+
def escape(text: Any) -> SafeString:
|
|
13
17
|
"""
|
|
14
18
|
Return the given text with ampersands, quotes and angle brackets encoded
|
|
15
19
|
for use in HTML.
|
|
@@ -28,27 +32,36 @@ _json_script_escapes = {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
|
|
31
|
-
def json_script(
|
|
35
|
+
def json_script(
|
|
36
|
+
value: Any,
|
|
37
|
+
element_id: str | None = None,
|
|
38
|
+
nonce: str = "",
|
|
39
|
+
encoder: type[json.JSONEncoder] | None = None,
|
|
40
|
+
) -> SafeString:
|
|
32
41
|
"""
|
|
33
42
|
Escape all the HTML/XML special characters with their unicode escapes, so
|
|
34
43
|
value is safe to be output anywhere except for inside a tag attribute. Wrap
|
|
35
44
|
the escaped JSON in a script tag.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
value: The data to encode as JSON
|
|
48
|
+
element_id: Optional ID attribute for the script tag
|
|
49
|
+
nonce: Optional CSP nonce for inline script tags
|
|
50
|
+
encoder: Optional custom JSON encoder class
|
|
36
51
|
"""
|
|
37
52
|
from plain.json import PlainJSONEncoder
|
|
38
53
|
|
|
39
54
|
json_str = json.dumps(value, cls=encoder or PlainJSONEncoder).translate(
|
|
40
55
|
_json_script_escapes
|
|
41
56
|
)
|
|
42
|
-
if element_id
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
args = (mark_safe(json_str),)
|
|
48
|
-
return format_html(template, *args)
|
|
57
|
+
id_attr = f' id="{element_id}"' if element_id else ""
|
|
58
|
+
nonce_attr = f' nonce="{nonce}"' if nonce else ""
|
|
59
|
+
return mark_safe(
|
|
60
|
+
f'<script{id_attr}{nonce_attr} type="application/json">{json_str}</script>'
|
|
61
|
+
)
|
|
49
62
|
|
|
50
63
|
|
|
51
|
-
def conditional_escape(text):
|
|
64
|
+
def conditional_escape(text: Any) -> SafeString | str:
|
|
52
65
|
"""
|
|
53
66
|
Similar to escape(), except that it doesn't operate on pre-escaped strings.
|
|
54
67
|
|
|
@@ -58,12 +71,12 @@ def conditional_escape(text):
|
|
|
58
71
|
if isinstance(text, Promise):
|
|
59
72
|
text = str(text)
|
|
60
73
|
if hasattr(text, "__html__"):
|
|
61
|
-
return text.__html__()
|
|
74
|
+
return text.__html__() # type: ignore[union-attr]
|
|
62
75
|
else:
|
|
63
76
|
return escape(text)
|
|
64
77
|
|
|
65
78
|
|
|
66
|
-
def format_html(format_string, *args, **kwargs):
|
|
79
|
+
def format_html(format_string: str, *args: Any, **kwargs: Any) -> SafeString:
|
|
67
80
|
"""
|
|
68
81
|
Similar to str.format, but pass all arguments through conditional_escape(),
|
|
69
82
|
and call mark_safe() on the result. This function should be used instead
|
|
@@ -74,26 +87,27 @@ def format_html(format_string, *args, **kwargs):
|
|
|
74
87
|
return mark_safe(format_string.format(*args_safe, **kwargs_safe))
|
|
75
88
|
|
|
76
89
|
|
|
90
|
+
@internalcode
|
|
77
91
|
class MLStripper(HTMLParser):
|
|
78
|
-
def __init__(self):
|
|
92
|
+
def __init__(self) -> None:
|
|
79
93
|
super().__init__(convert_charrefs=False)
|
|
80
94
|
self.reset()
|
|
81
|
-
self.fed = []
|
|
95
|
+
self.fed: list[str] = []
|
|
82
96
|
|
|
83
|
-
def handle_data(self,
|
|
84
|
-
self.fed.append(
|
|
97
|
+
def handle_data(self, data: str) -> None:
|
|
98
|
+
self.fed.append(data)
|
|
85
99
|
|
|
86
|
-
def handle_entityref(self, name):
|
|
100
|
+
def handle_entityref(self, name: str) -> None:
|
|
87
101
|
self.fed.append(f"&{name};")
|
|
88
102
|
|
|
89
|
-
def handle_charref(self, name):
|
|
103
|
+
def handle_charref(self, name: str) -> None:
|
|
90
104
|
self.fed.append(f"&#{name};")
|
|
91
105
|
|
|
92
|
-
def get_data(self):
|
|
106
|
+
def get_data(self) -> str:
|
|
93
107
|
return "".join(self.fed)
|
|
94
108
|
|
|
95
109
|
|
|
96
|
-
def _strip_once(value):
|
|
110
|
+
def _strip_once(value: str) -> str:
|
|
97
111
|
"""
|
|
98
112
|
Internal tag stripping utility used by strip_tags.
|
|
99
113
|
"""
|
|
@@ -104,7 +118,7 @@ def _strip_once(value):
|
|
|
104
118
|
|
|
105
119
|
|
|
106
120
|
@keep_lazy_text
|
|
107
|
-
def strip_tags(value):
|
|
121
|
+
def strip_tags(value: Any) -> str:
|
|
108
122
|
"""Return the given HTML with all tags stripped."""
|
|
109
123
|
# Note: in typical case this loop executes _strip_once once. Loop condition
|
|
110
124
|
# is redundant, but helps to reduce number of executions of _strip_once.
|
|
@@ -118,7 +132,7 @@ def strip_tags(value):
|
|
|
118
132
|
return value
|
|
119
133
|
|
|
120
134
|
|
|
121
|
-
def avoid_wrapping(value):
|
|
135
|
+
def avoid_wrapping(value: str) -> str:
|
|
122
136
|
"""
|
|
123
137
|
Avoid text wrapping in the middle of a phrase by adding non-breaking
|
|
124
138
|
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[union-attr]
|
|
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[union-attr]
|
|
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
|
"""
|