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/datastructures.py
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import copy
|
2
|
-
from collections.abc import Mapping
|
4
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
5
|
+
from typing import Any, TypeVar
|
6
|
+
|
7
|
+
_KT = TypeVar("_KT")
|
8
|
+
_VT = TypeVar("_VT")
|
3
9
|
|
4
10
|
|
5
11
|
class OrderedSet:
|
@@ -7,37 +13,37 @@ class OrderedSet:
|
|
7
13
|
A set which keeps the ordering of the inserted items.
|
8
14
|
"""
|
9
15
|
|
10
|
-
def __init__(self, iterable=None):
|
11
|
-
self.dict = dict.fromkeys(iterable or ())
|
16
|
+
def __init__(self, iterable: Iterable[Any] | None = None) -> None:
|
17
|
+
self.dict: dict[Any, None] = dict.fromkeys(iterable or ())
|
12
18
|
|
13
|
-
def add(self, item):
|
19
|
+
def add(self, item: Any) -> None:
|
14
20
|
self.dict[item] = None
|
15
21
|
|
16
|
-
def remove(self, item):
|
22
|
+
def remove(self, item: Any) -> None:
|
17
23
|
del self.dict[item]
|
18
24
|
|
19
|
-
def discard(self, item):
|
25
|
+
def discard(self, item: Any) -> None:
|
20
26
|
try:
|
21
27
|
self.remove(item)
|
22
28
|
except KeyError:
|
23
29
|
pass
|
24
30
|
|
25
|
-
def __iter__(self):
|
31
|
+
def __iter__(self) -> Iterator[Any]:
|
26
32
|
return iter(self.dict)
|
27
33
|
|
28
|
-
def __reversed__(self):
|
34
|
+
def __reversed__(self) -> Iterator[Any]:
|
29
35
|
return reversed(self.dict)
|
30
36
|
|
31
|
-
def __contains__(self, item):
|
37
|
+
def __contains__(self, item: Any) -> bool:
|
32
38
|
return item in self.dict
|
33
39
|
|
34
|
-
def __bool__(self):
|
40
|
+
def __bool__(self) -> bool:
|
35
41
|
return bool(self.dict)
|
36
42
|
|
37
|
-
def __len__(self):
|
43
|
+
def __len__(self) -> int:
|
38
44
|
return len(self.dict)
|
39
45
|
|
40
|
-
def __repr__(self):
|
46
|
+
def __repr__(self) -> str:
|
41
47
|
data = repr(list(self.dict)) if self.dict else ""
|
42
48
|
return f"{self.__class__.__qualname__}({data})"
|
43
49
|
|
@@ -46,7 +52,7 @@ class MultiValueDictKeyError(KeyError):
|
|
46
52
|
pass
|
47
53
|
|
48
54
|
|
49
|
-
class MultiValueDict(dict):
|
55
|
+
class MultiValueDict(dict[str, list[Any]]):
|
50
56
|
"""
|
51
57
|
A subclass of dictionary customized to handle multiple values for the
|
52
58
|
same key.
|
@@ -69,13 +75,17 @@ class MultiValueDict(dict):
|
|
69
75
|
single name-value pairs.
|
70
76
|
"""
|
71
77
|
|
72
|
-
def __init__(
|
78
|
+
def __init__(
|
79
|
+
self,
|
80
|
+
key_to_list_mapping: Mapping[str, list[Any]]
|
81
|
+
| Iterable[tuple[str, list[Any]]] = (),
|
82
|
+
) -> None:
|
73
83
|
super().__init__(key_to_list_mapping)
|
74
84
|
|
75
|
-
def __repr__(self):
|
85
|
+
def __repr__(self) -> str:
|
76
86
|
return f"<{self.__class__.__name__}: {super().__repr__()}>"
|
77
87
|
|
78
|
-
def __getitem__(self, key):
|
88
|
+
def __getitem__(self, key: str) -> Any:
|
79
89
|
"""
|
80
90
|
Return the last data value for this key, or [] if it's an empty list;
|
81
91
|
raise KeyError if not found.
|
@@ -89,13 +99,13 @@ class MultiValueDict(dict):
|
|
89
99
|
except IndexError:
|
90
100
|
return []
|
91
101
|
|
92
|
-
def __setitem__(self, key, value):
|
102
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
93
103
|
super().__setitem__(key, [value])
|
94
104
|
|
95
|
-
def __copy__(self):
|
105
|
+
def __copy__(self) -> MultiValueDict:
|
96
106
|
return self.__class__([(k, v[:]) for k, v in self.lists()])
|
97
107
|
|
98
|
-
def __deepcopy__(self, memo):
|
108
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> MultiValueDict:
|
99
109
|
result = self.__class__()
|
100
110
|
memo[id(self)] = result
|
101
111
|
for key, value in dict.items(self):
|
@@ -104,16 +114,16 @@ class MultiValueDict(dict):
|
|
104
114
|
)
|
105
115
|
return result
|
106
116
|
|
107
|
-
def __getstate__(self):
|
117
|
+
def __getstate__(self) -> dict[str, Any]:
|
108
118
|
return {**self.__dict__, "_data": {k: self._getlist(k) for k in self}}
|
109
119
|
|
110
|
-
def __setstate__(self, obj_dict):
|
120
|
+
def __setstate__(self, obj_dict: dict[str, Any]) -> None:
|
111
121
|
data = obj_dict.pop("_data", {})
|
112
122
|
for k, v in data.items():
|
113
123
|
self.setlist(k, v)
|
114
124
|
self.__dict__.update(obj_dict)
|
115
125
|
|
116
|
-
def get(self, key, default=None):
|
126
|
+
def get(self, key: str, default: Any = None) -> Any:
|
117
127
|
"""
|
118
128
|
Return the last data value for the passed key. If key doesn't exist
|
119
129
|
or value is an empty list, return `default`.
|
@@ -126,7 +136,9 @@ class MultiValueDict(dict):
|
|
126
136
|
return default
|
127
137
|
return val
|
128
138
|
|
129
|
-
def _getlist(
|
139
|
+
def _getlist(
|
140
|
+
self, key: str, default: list[Any] | None = None, force_list: bool = False
|
141
|
+
) -> list[Any] | None:
|
130
142
|
"""
|
131
143
|
Return a list of values for the key.
|
132
144
|
|
@@ -144,24 +156,26 @@ class MultiValueDict(dict):
|
|
144
156
|
values = list(values) if values is not None else None
|
145
157
|
return values
|
146
158
|
|
147
|
-
def getlist(self, key, default=None):
|
159
|
+
def getlist(self, key: str, default: list[Any] | None = None) -> list[Any]:
|
148
160
|
"""
|
149
161
|
Return the list of values for the key. If key doesn't exist, return a
|
150
162
|
default value.
|
151
163
|
"""
|
152
164
|
return self._getlist(key, default, force_list=True)
|
153
165
|
|
154
|
-
def setlist(self, key, list_):
|
166
|
+
def setlist(self, key: str, list_: list[Any]) -> None:
|
155
167
|
super().__setitem__(key, list_)
|
156
168
|
|
157
|
-
def setdefault(self, key, default=None):
|
169
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
158
170
|
if key not in self:
|
159
171
|
self[key] = default
|
160
172
|
# Do not return default here because __setitem__() may store
|
161
173
|
# another value -- QueryDict.__setitem__() does. Look it up.
|
162
174
|
return self[key]
|
163
175
|
|
164
|
-
def setlistdefault(
|
176
|
+
def setlistdefault(
|
177
|
+
self, key: str, default_list: list[Any] | None = None
|
178
|
+
) -> list[Any]:
|
165
179
|
if key not in self:
|
166
180
|
if default_list is None:
|
167
181
|
default_list = []
|
@@ -170,11 +184,11 @@ class MultiValueDict(dict):
|
|
170
184
|
# another value -- QueryDict.setlist() does. Look it up.
|
171
185
|
return self._getlist(key)
|
172
186
|
|
173
|
-
def appendlist(self, key, value):
|
187
|
+
def appendlist(self, key: str, value: Any) -> None:
|
174
188
|
"""Append an item to the internal list associated with key."""
|
175
189
|
self.setlistdefault(key).append(value)
|
176
190
|
|
177
|
-
def items(self):
|
191
|
+
def items(self) -> Iterator[tuple[str, Any]]:
|
178
192
|
"""
|
179
193
|
Yield (key, value) pairs, where value is the last item in the list
|
180
194
|
associated with the key.
|
@@ -182,23 +196,23 @@ class MultiValueDict(dict):
|
|
182
196
|
for key in self:
|
183
197
|
yield key, self[key]
|
184
198
|
|
185
|
-
def lists(self):
|
199
|
+
def lists(self) -> Iterator[tuple[str, list[Any]]]:
|
186
200
|
"""Yield (key, list) pairs."""
|
187
201
|
return iter(super().items())
|
188
202
|
|
189
|
-
def values(self):
|
203
|
+
def values(self) -> Iterator[Any]:
|
190
204
|
"""Yield the last value on every key list."""
|
191
205
|
for key in self:
|
192
206
|
yield self[key]
|
193
207
|
|
194
|
-
def copy(self):
|
208
|
+
def copy(self) -> MultiValueDict:
|
195
209
|
"""Return a shallow copy of this object."""
|
196
210
|
return copy.copy(self)
|
197
211
|
|
198
|
-
def update(self, *args, **kwargs):
|
212
|
+
def update(self, *args: Any, **kwargs: Any) -> None:
|
199
213
|
"""Extend rather than replace existing key lists."""
|
200
214
|
if len(args) > 1:
|
201
|
-
raise TypeError("update expected at most 1 argument, got
|
215
|
+
raise TypeError(f"update expected at most 1 argument, got {len(args)}")
|
202
216
|
if args:
|
203
217
|
arg = args[0]
|
204
218
|
if isinstance(arg, MultiValueDict):
|
@@ -212,7 +226,7 @@ class MultiValueDict(dict):
|
|
212
226
|
for key, value in kwargs.items():
|
213
227
|
self.setlistdefault(key).append(value)
|
214
228
|
|
215
|
-
def dict(self):
|
229
|
+
def dict(self) -> dict[str, Any]:
|
216
230
|
"""Return current object as a dict with singular values."""
|
217
231
|
return {key: self[key] for key in self}
|
218
232
|
|
@@ -230,12 +244,17 @@ class ImmutableList(tuple):
|
|
230
244
|
AttributeError: You cannot mutate this.
|
231
245
|
"""
|
232
246
|
|
233
|
-
def __new__(
|
247
|
+
def __new__(
|
248
|
+
cls,
|
249
|
+
*args: Any,
|
250
|
+
warning: str = "ImmutableList object is immutable.",
|
251
|
+
**kwargs: Any,
|
252
|
+
) -> ImmutableList:
|
234
253
|
self = tuple.__new__(cls, *args, **kwargs)
|
235
254
|
self.warning = warning
|
236
255
|
return self
|
237
256
|
|
238
|
-
def complain(self, *args, **kwargs):
|
257
|
+
def complain(self, *args: Any, **kwargs: Any) -> None:
|
239
258
|
raise AttributeError(self.warning)
|
240
259
|
|
241
260
|
# All list mutation functions complain.
|
@@ -254,7 +273,7 @@ class ImmutableList(tuple):
|
|
254
273
|
reverse = complain
|
255
274
|
|
256
275
|
|
257
|
-
class DictWrapper(dict):
|
276
|
+
class DictWrapper(dict[str, Any]):
|
258
277
|
"""
|
259
278
|
Wrap accesses to a dictionary so that certain values (those starting with
|
260
279
|
the specified prefix) are passed through a function before being returned.
|
@@ -264,12 +283,14 @@ class DictWrapper(dict):
|
|
264
283
|
quoted before being used.
|
265
284
|
"""
|
266
285
|
|
267
|
-
def __init__(
|
286
|
+
def __init__(
|
287
|
+
self, data: dict[str, Any], func: Callable[[Any], Any], prefix: str
|
288
|
+
) -> None:
|
268
289
|
super().__init__(data)
|
269
290
|
self.func = func
|
270
291
|
self.prefix = prefix
|
271
292
|
|
272
|
-
def __getitem__(self, key):
|
293
|
+
def __getitem__(self, key: str) -> Any:
|
273
294
|
"""
|
274
295
|
Retrieve the real value after stripping the prefix string (if
|
275
296
|
present). If the prefix is present, pass the value through self.func
|
@@ -283,7 +304,7 @@ class DictWrapper(dict):
|
|
283
304
|
return value
|
284
305
|
|
285
306
|
|
286
|
-
class CaseInsensitiveMapping(Mapping):
|
307
|
+
class CaseInsensitiveMapping(Mapping[str, Any]):
|
287
308
|
"""
|
288
309
|
Mapping allowing case-insensitive key lookups. Original case of keys is
|
289
310
|
preserved for iteration and string representation.
|
@@ -301,35 +322,44 @@ class CaseInsensitiveMapping(Mapping):
|
|
301
322
|
{'name': 'Jane'}
|
302
323
|
"""
|
303
324
|
|
304
|
-
def __init__(self, data):
|
305
|
-
self._store
|
325
|
+
def __init__(self, data: Mapping[str, Any] | Iterable[tuple[str, Any]]) -> None:
|
326
|
+
self._store: dict[str, tuple[str, Any]] = {
|
327
|
+
k.lower(): (k, v) for k, v in self._unpack_items(data)
|
328
|
+
}
|
306
329
|
|
307
|
-
def __getitem__(self, key):
|
330
|
+
def __getitem__(self, key: str) -> Any:
|
308
331
|
return self._store[key.lower()][1]
|
309
332
|
|
310
|
-
def __len__(self):
|
333
|
+
def __len__(self) -> int:
|
311
334
|
return len(self._store)
|
312
335
|
|
313
|
-
def __eq__(self, other):
|
314
|
-
|
315
|
-
|
316
|
-
|
336
|
+
def __eq__(self, other: object) -> bool:
|
337
|
+
if not isinstance(other, Mapping):
|
338
|
+
return False
|
339
|
+
return {k.lower(): v for k, v in self.items()} == {
|
340
|
+
k.lower(): v for k, v in other.items() if isinstance(k, str)
|
341
|
+
}
|
317
342
|
|
318
|
-
def __iter__(self):
|
343
|
+
def __iter__(self) -> Iterator[str]:
|
319
344
|
return (original_key for original_key, value in self._store.values())
|
320
345
|
|
321
|
-
def __repr__(self):
|
346
|
+
def __repr__(self) -> str:
|
322
347
|
return repr(dict(self._store.values()))
|
323
348
|
|
324
|
-
def copy(self):
|
349
|
+
def copy(self) -> CaseInsensitiveMapping:
|
325
350
|
return self
|
326
351
|
|
327
352
|
@staticmethod
|
328
|
-
def _unpack_items(
|
353
|
+
def _unpack_items(
|
354
|
+
data: Mapping[str, Any] | Iterable[tuple[str, Any]],
|
355
|
+
) -> Iterator[tuple[str, Any]]:
|
329
356
|
# Explicitly test for dict first as the common case for performance,
|
330
357
|
# avoiding abc's __instancecheck__ and _abc_instancecheck for the
|
331
358
|
# general Mapping case.
|
332
|
-
if isinstance(data, dict
|
359
|
+
if isinstance(data, dict):
|
360
|
+
yield from data.items()
|
361
|
+
return
|
362
|
+
if isinstance(data, Mapping):
|
333
363
|
yield from data.items()
|
334
364
|
return
|
335
365
|
for i, elem in enumerate(data):
|
plain/utils/dateparse.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
"""Functions to parse datetime objects."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
# We're using regular expressions rather than time.strptime because:
|
4
6
|
# - They provide both validation and parsing.
|
5
7
|
# - They're more flexible for datetimes.
|
6
8
|
# - The date/datetime/time constructors produce friendlier error messages.
|
7
|
-
|
8
9
|
import datetime
|
9
10
|
|
10
11
|
from plain.utils.regex_helper import _lazy_re_compile
|
@@ -64,7 +65,7 @@ postgres_interval_re = _lazy_re_compile(
|
|
64
65
|
)
|
65
66
|
|
66
67
|
|
67
|
-
def parse_date(value):
|
68
|
+
def parse_date(value: str) -> datetime.date | None:
|
68
69
|
"""Parse a string and return a datetime.date.
|
69
70
|
|
70
71
|
Raise ValueError if the input is well formatted but not a valid date.
|
@@ -75,10 +76,11 @@ def parse_date(value):
|
|
75
76
|
except ValueError:
|
76
77
|
if match := date_re.match(value):
|
77
78
|
kw = {k: int(v) for k, v in match.groupdict().items()}
|
78
|
-
return datetime.date(**kw)
|
79
|
+
return datetime.date(**kw) # type: ignore[arg-type]
|
80
|
+
return None
|
79
81
|
|
80
82
|
|
81
|
-
def parse_time(value):
|
83
|
+
def parse_time(value: str) -> datetime.time | None:
|
82
84
|
"""Parse a string and return a datetime.time.
|
83
85
|
|
84
86
|
This function doesn't support time zone offsets.
|
@@ -101,7 +103,7 @@ def parse_time(value):
|
|
101
103
|
return datetime.time(**kw)
|
102
104
|
|
103
105
|
|
104
|
-
def parse_datetime(value):
|
106
|
+
def parse_datetime(value: str) -> datetime.datetime | None:
|
105
107
|
"""Parse a string and return a datetime.datetime.
|
106
108
|
|
107
109
|
This function supports time zone offsets. When the input contains one,
|
@@ -126,10 +128,11 @@ def parse_datetime(value):
|
|
126
128
|
offset = -offset
|
127
129
|
tzinfo = get_fixed_timezone(offset)
|
128
130
|
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
129
|
-
return datetime.datetime(**kw, tzinfo=tzinfo)
|
131
|
+
return datetime.datetime(**kw, tzinfo=tzinfo) # type: ignore[arg-type]
|
132
|
+
return None
|
130
133
|
|
131
134
|
|
132
|
-
def parse_duration(value):
|
135
|
+
def parse_duration(value: str) -> datetime.timedelta | None:
|
133
136
|
"""Parse a duration string and return a datetime.timedelta.
|
134
137
|
|
135
138
|
The preferred format for durations in Plain is '%d %H:%M:%S.%f'.
|
plain/utils/deconstruct.py
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
1
4
|
from importlib import import_module
|
5
|
+
from typing import Any, TypeVar
|
6
|
+
|
7
|
+
T = TypeVar("T")
|
2
8
|
|
3
9
|
|
4
|
-
def deconstructible(
|
10
|
+
def deconstructible(
|
11
|
+
*args: type[T], path: str | None = None
|
12
|
+
) -> Callable[[type[T]], type[T]] | type[T]:
|
5
13
|
"""
|
6
14
|
Class decorator that allows the decorated class to be serialized
|
7
15
|
by the migrations subsystem.
|
@@ -9,14 +17,14 @@ def deconstructible(*args, path=None):
|
|
9
17
|
The `path` kwarg specifies the import path.
|
10
18
|
"""
|
11
19
|
|
12
|
-
def decorator(klass):
|
13
|
-
def __new__(cls, *args, **kwargs):
|
20
|
+
def decorator(klass: type[T]) -> type[T]:
|
21
|
+
def __new__(cls: type[T], *args: Any, **kwargs: Any) -> T:
|
14
22
|
# We capture the arguments to make returning them trivial
|
15
23
|
obj = super(klass, cls).__new__(cls)
|
16
24
|
obj._constructor_args = (args, kwargs)
|
17
25
|
return obj
|
18
26
|
|
19
|
-
def deconstruct(obj):
|
27
|
+
def deconstruct(obj: Any) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
20
28
|
"""
|
21
29
|
Return a 3-tuple of class import path, positional arguments,
|
22
30
|
and keyword arguments.
|
plain/utils/decorators.py
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
"Functions that help with dynamically creating decorators for views."
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
3
7
|
|
4
8
|
class classonlymethod(classmethod):
|
5
|
-
def __get__(self, instance, cls=None):
|
9
|
+
def __get__(self, instance: object | None, cls: type | None = None) -> Any:
|
6
10
|
if instance is not None:
|
7
11
|
raise AttributeError(
|
8
12
|
"This method is available only on the class, not on instances."
|
plain/utils/duration.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import datetime
|
2
4
|
|
3
5
|
|
4
|
-
def _get_duration_components(
|
6
|
+
def _get_duration_components(
|
7
|
+
duration: datetime.timedelta,
|
8
|
+
) -> tuple[int, int, int, int, int]:
|
5
9
|
days = duration.days
|
6
10
|
seconds = duration.seconds
|
7
11
|
microseconds = duration.microseconds
|
@@ -15,7 +19,7 @@ def _get_duration_components(duration):
|
|
15
19
|
return days, hours, minutes, seconds, microseconds
|
16
20
|
|
17
21
|
|
18
|
-
def duration_string(duration):
|
22
|
+
def duration_string(duration: datetime.timedelta) -> str:
|
19
23
|
"""Version of str(timedelta) which is not English specific."""
|
20
24
|
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
|
21
25
|
|
@@ -28,7 +32,7 @@ def duration_string(duration):
|
|
28
32
|
return string
|
29
33
|
|
30
34
|
|
31
|
-
def duration_iso_string(duration):
|
35
|
+
def duration_iso_string(duration: datetime.timedelta) -> str:
|
32
36
|
if duration < datetime.timedelta(0):
|
33
37
|
sign = "-"
|
34
38
|
duration *= -1
|
@@ -40,5 +44,5 @@ def duration_iso_string(duration):
|
|
40
44
|
return f"{sign}P{days}DT{hours:02d}H{minutes:02d}M{seconds:02d}{ms}S"
|
41
45
|
|
42
46
|
|
43
|
-
def duration_microseconds(delta):
|
47
|
+
def duration_microseconds(delta: datetime.timedelta) -> int:
|
44
48
|
return (24 * 60 * 60 * delta.days + delta.seconds) * 1000000 + delta.microseconds
|
plain/utils/encoding.py
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import datetime
|
2
4
|
from decimal import Decimal
|
3
5
|
from types import NoneType
|
6
|
+
from typing import Any
|
4
7
|
from urllib.parse import quote
|
5
8
|
|
6
9
|
from plain.utils.functional import Promise
|
7
10
|
|
8
11
|
|
9
12
|
class PlainUnicodeDecodeError(UnicodeDecodeError):
|
10
|
-
def __init__(self, obj, *args):
|
13
|
+
def __init__(self, obj: Any, *args: Any):
|
11
14
|
self.obj = obj
|
12
15
|
super().__init__(*args)
|
13
16
|
|
14
|
-
def __str__(self):
|
17
|
+
def __str__(self) -> str:
|
15
18
|
return f"{super().__str__()}. You passed in {self.obj!r} ({type(self.obj)})"
|
16
19
|
|
17
20
|
|
@@ -26,7 +29,7 @@ _PROTECTED_TYPES = (
|
|
26
29
|
)
|
27
30
|
|
28
31
|
|
29
|
-
def is_protected_type(obj):
|
32
|
+
def is_protected_type(obj: Any) -> bool:
|
30
33
|
"""Determine if the object instance is of a protected type.
|
31
34
|
|
32
35
|
Objects of protected types are preserved as-is when passed to
|
@@ -35,7 +38,9 @@ def is_protected_type(obj):
|
|
35
38
|
return isinstance(obj, _PROTECTED_TYPES)
|
36
39
|
|
37
40
|
|
38
|
-
def force_str(
|
41
|
+
def force_str(
|
42
|
+
s: Any, encoding: str = "utf-8", strings_only: bool = False, errors: str = "strict"
|
43
|
+
) -> str | Any:
|
39
44
|
"""
|
40
45
|
Similar to smart_str(), except that lazy instances are resolved to
|
41
46
|
strings, rather than kept as lazy objects.
|
@@ -57,7 +62,9 @@ def force_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
|
57
62
|
return s
|
58
63
|
|
59
64
|
|
60
|
-
def force_bytes(
|
65
|
+
def force_bytes(
|
66
|
+
s: Any, encoding: str = "utf-8", strings_only: bool = False, errors: str = "strict"
|
67
|
+
) -> bytes | Any:
|
61
68
|
"""
|
62
69
|
Similar to smart_bytes, except that lazy instances are resolved to
|
63
70
|
strings, rather than kept as lazy objects.
|
@@ -77,7 +84,7 @@ def force_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
|
77
84
|
return str(s).encode(encoding, errors)
|
78
85
|
|
79
86
|
|
80
|
-
def iri_to_uri(iri):
|
87
|
+
def iri_to_uri(iri: str | Promise | None) -> str | None:
|
81
88
|
"""
|
82
89
|
Convert an Internationalized Resource Identifier (IRI) portion to a URI
|
83
90
|
portion that is suitable for inclusion in a URL.
|
@@ -125,6 +132,6 @@ _hextobyte.update(
|
|
125
132
|
)
|
126
133
|
|
127
134
|
|
128
|
-
def punycode(domain):
|
135
|
+
def punycode(domain: str) -> str:
|
129
136
|
"""Return the Punycode of the given domain if it's non-ASCII."""
|
130
137
|
return domain.encode("idna").decode("ascii")
|