plain 0.68.1__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 +23 -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 +20 -51
- 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 +27 -17
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
- plain-0.70.0.dist-info/RECORD +169 -0
- plain-0.68.1.dist-info/RECORD +0 -169
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
- {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
plain/utils/regex_helper.py
CHANGED
@@ -6,7 +6,11 @@ This is not, and is not intended to be, a complete reg-exp decompiler. It
|
|
6
6
|
should be good enough for a large class of URLS, however.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import re
|
12
|
+
from collections.abc import Iterator
|
13
|
+
from typing import Any, cast
|
10
14
|
|
11
15
|
from plain.utils.functional import SimpleLazyObject
|
12
16
|
|
@@ -39,7 +43,7 @@ class NonCapture(list):
|
|
39
43
|
"""Represent a non-capturing group in the pattern string."""
|
40
44
|
|
41
45
|
|
42
|
-
def normalize(pattern):
|
46
|
+
def normalize(pattern: str) -> list[tuple[str, list[str | None]]]:
|
43
47
|
r"""
|
44
48
|
Given a reg-exp pattern, normalize it to an iterable of forms that
|
45
49
|
suffice for reverse matching. This does the following:
|
@@ -193,7 +197,7 @@ def normalize(pattern):
|
|
193
197
|
return list(zip(*flatten_result(result)))
|
194
198
|
|
195
199
|
|
196
|
-
def next_char(input_iter):
|
200
|
+
def next_char(input_iter: Iterator[str]) -> Iterator[tuple[str, bool]]:
|
197
201
|
r"""
|
198
202
|
An iterator that yields the next character from "pattern_iter", respecting
|
199
203
|
escape sequences. An escaped character is replaced by a representative of
|
@@ -214,7 +218,7 @@ def next_char(input_iter):
|
|
214
218
|
yield representative, True
|
215
219
|
|
216
220
|
|
217
|
-
def walk_to_end(ch, input_iter):
|
221
|
+
def walk_to_end(ch: str, input_iter: Iterator[tuple[str, bool]]) -> None:
|
218
222
|
"""
|
219
223
|
The iterator is currently inside a capturing group. Walk to the close of
|
220
224
|
this group, skipping over any nested groups and handling escaped
|
@@ -235,7 +239,9 @@ def walk_to_end(ch, input_iter):
|
|
235
239
|
nesting -= 1
|
236
240
|
|
237
241
|
|
238
|
-
def get_quantifier(
|
242
|
+
def get_quantifier(
|
243
|
+
ch: str, input_iter: Iterator[tuple[str, bool]]
|
244
|
+
) -> tuple[int, str | None]:
|
239
245
|
"""
|
240
246
|
Parse a quantifier from the input, where "ch" is the first character in the
|
241
247
|
quantifier.
|
@@ -263,16 +269,18 @@ def get_quantifier(ch, input_iter):
|
|
263
269
|
values = "".join(quant).split(",")
|
264
270
|
|
265
271
|
# Consume the trailing '?', if necessary.
|
272
|
+
ch2: str | None
|
266
273
|
try:
|
267
274
|
ch, escaped = next(input_iter)
|
275
|
+
ch2 = ch
|
268
276
|
except StopIteration:
|
269
|
-
|
270
|
-
if
|
271
|
-
|
272
|
-
return int(values[0]),
|
277
|
+
ch2 = None
|
278
|
+
if ch2 == "?":
|
279
|
+
ch2 = None
|
280
|
+
return int(values[0]), ch2
|
273
281
|
|
274
282
|
|
275
|
-
def contains(source, inst):
|
283
|
+
def contains(source: Any, inst: type) -> bool:
|
276
284
|
"""
|
277
285
|
Return True if the "source" contains an instance of "inst". False,
|
278
286
|
otherwise.
|
@@ -286,7 +294,7 @@ def contains(source, inst):
|
|
286
294
|
return False
|
287
295
|
|
288
296
|
|
289
|
-
def flatten_result(source):
|
297
|
+
def flatten_result(source: Any) -> tuple[list[str], list[list[str | None]]]:
|
290
298
|
"""
|
291
299
|
Turn the given source sequence into a list of reg-exp possibilities and
|
292
300
|
their arguments. Return a list of strings and a list of argument lists.
|
@@ -340,15 +348,17 @@ def flatten_result(source):
|
|
340
348
|
return result, result_args
|
341
349
|
|
342
350
|
|
343
|
-
def _lazy_re_compile(
|
351
|
+
def _lazy_re_compile(
|
352
|
+
regex: str | bytes | re.Pattern[str] | re.Pattern[bytes], flags: int = 0
|
353
|
+
) -> SimpleLazyObject:
|
344
354
|
"""Lazily compile a regex with flags."""
|
345
355
|
|
346
|
-
def _compile():
|
356
|
+
def _compile() -> re.Pattern[str] | re.Pattern[bytes]:
|
347
357
|
# Compile the regex if it was not passed pre-compiled.
|
348
358
|
if isinstance(regex, str | bytes):
|
349
359
|
return re.compile(regex, flags)
|
350
360
|
else:
|
351
361
|
assert not flags, "flags must be empty if regex is passed pre-compiled"
|
352
|
-
return regex
|
362
|
+
return cast(re.Pattern[str] | re.Pattern[bytes], regex)
|
353
363
|
|
354
364
|
return SimpleLazyObject(_compile)
|
plain/utils/safestring.py
CHANGED
@@ -5,15 +5,21 @@ that the producer of the string has already turned characters that should not
|
|
5
5
|
be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
|
6
6
|
"""
|
7
7
|
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from collections.abc import Callable
|
8
11
|
from functools import wraps
|
12
|
+
from typing import Any, TypeVar
|
9
13
|
|
10
14
|
from plain.utils.functional import keep_lazy
|
11
15
|
|
16
|
+
_T = TypeVar("_T")
|
17
|
+
|
12
18
|
|
13
19
|
class SafeData:
|
14
20
|
__slots__ = ()
|
15
21
|
|
16
|
-
def __html__(self):
|
22
|
+
def __html__(self) -> SafeData:
|
17
23
|
"""
|
18
24
|
Return the html representation of a string for interoperability.
|
19
25
|
|
@@ -30,7 +36,7 @@ class SafeString(str, SafeData):
|
|
30
36
|
|
31
37
|
__slots__ = ()
|
32
38
|
|
33
|
-
def __add__(self, rhs):
|
39
|
+
def __add__(self, rhs: str) -> SafeString | str:
|
34
40
|
"""
|
35
41
|
Concatenating a safe string with another safe bytestring or
|
36
42
|
safe string is safe. Otherwise, the result is no longer safe.
|
@@ -40,20 +46,22 @@ class SafeString(str, SafeData):
|
|
40
46
|
return SafeString(t)
|
41
47
|
return t
|
42
48
|
|
43
|
-
def __str__(self):
|
49
|
+
def __str__(self) -> str:
|
44
50
|
return self
|
45
51
|
|
46
52
|
|
47
|
-
def _safety_decorator(
|
53
|
+
def _safety_decorator(
|
54
|
+
safety_marker: Callable[[Any], _T], func: Callable[..., Any]
|
55
|
+
) -> Callable[..., _T]:
|
48
56
|
@wraps(func)
|
49
|
-
def wrapper(*args, **kwargs):
|
57
|
+
def wrapper(*args: Any, **kwargs: Any) -> _T:
|
50
58
|
return safety_marker(func(*args, **kwargs))
|
51
59
|
|
52
60
|
return wrapper
|
53
61
|
|
54
62
|
|
55
63
|
@keep_lazy(SafeString)
|
56
|
-
def mark_safe(s):
|
64
|
+
def mark_safe(s: Any) -> SafeString | SafeData | Callable[..., Any]:
|
57
65
|
"""
|
58
66
|
Explicitly mark a string as safe for (HTML) output purposes. The returned
|
59
67
|
object can be used everywhere a string is appropriate.
|
plain/utils/text.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
2
4
|
import unicodedata
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
|
5
8
|
from plain.utils.regex_helper import _lazy_re_compile
|
@@ -15,10 +18,10 @@ class Truncator(SimpleLazyObject):
|
|
15
18
|
An object used to truncate text, either by characters or words.
|
16
19
|
"""
|
17
20
|
|
18
|
-
def __init__(self, text):
|
21
|
+
def __init__(self, text: Any):
|
19
22
|
super().__init__(lambda: str(text))
|
20
23
|
|
21
|
-
def add_truncation_text(self, text, truncate=None):
|
24
|
+
def add_truncation_text(self, text: str, truncate: str | None = None) -> str:
|
22
25
|
if truncate is None:
|
23
26
|
truncate = "%(truncated_text)s…"
|
24
27
|
if "%(truncated_text)s" in truncate:
|
@@ -31,7 +34,7 @@ class Truncator(SimpleLazyObject):
|
|
31
34
|
return text
|
32
35
|
return f"{text}{truncate}"
|
33
36
|
|
34
|
-
def chars(self, num, truncate=None, html=False):
|
37
|
+
def chars(self, num: int, truncate: str | None = None, html: bool = False) -> str:
|
35
38
|
"""
|
36
39
|
Return the text truncated to be no longer than the specified number
|
37
40
|
of characters.
|
@@ -54,7 +57,9 @@ class Truncator(SimpleLazyObject):
|
|
54
57
|
return self._truncate_html(length, truncate, text, truncate_len, False)
|
55
58
|
return self._text_chars(length, truncate, text, truncate_len)
|
56
59
|
|
57
|
-
def _text_chars(
|
60
|
+
def _text_chars(
|
61
|
+
self, length: int, truncate: str | None, text: str, truncate_len: int
|
62
|
+
) -> str:
|
58
63
|
"""Truncate a string after a certain number of chars."""
|
59
64
|
s_len = 0
|
60
65
|
end_index = None
|
@@ -73,7 +78,7 @@ class Truncator(SimpleLazyObject):
|
|
73
78
|
# Return the original string since no truncation was necessary
|
74
79
|
return text
|
75
80
|
|
76
|
-
def words(self, num, truncate=None, html=False):
|
81
|
+
def words(self, num: int, truncate: str | None = None, html: bool = False) -> str:
|
77
82
|
"""
|
78
83
|
Truncate a string after a certain number of words. `truncate` specifies
|
79
84
|
what should be used to notify that the string has been truncated,
|
@@ -85,7 +90,7 @@ class Truncator(SimpleLazyObject):
|
|
85
90
|
return self._truncate_html(length, truncate, self._wrapped, length, True)
|
86
91
|
return self._text_words(length, truncate)
|
87
92
|
|
88
|
-
def _text_words(self, length, truncate):
|
93
|
+
def _text_words(self, length: int, truncate: str | None) -> str:
|
89
94
|
"""
|
90
95
|
Truncate a string after a certain number of words.
|
91
96
|
|
@@ -97,7 +102,14 @@ class Truncator(SimpleLazyObject):
|
|
97
102
|
return self.add_truncation_text(" ".join(words), truncate)
|
98
103
|
return " ".join(words)
|
99
104
|
|
100
|
-
def _truncate_html(
|
105
|
+
def _truncate_html(
|
106
|
+
self,
|
107
|
+
length: int,
|
108
|
+
truncate: str | None,
|
109
|
+
text: str,
|
110
|
+
truncate_len: int,
|
111
|
+
words: bool,
|
112
|
+
) -> str:
|
101
113
|
"""
|
102
114
|
Truncate HTML to a certain number of chars (not counting tags and
|
103
115
|
comments), or, if words is True, then to a certain number of words.
|
@@ -178,7 +190,7 @@ class Truncator(SimpleLazyObject):
|
|
178
190
|
|
179
191
|
|
180
192
|
@keep_lazy_text
|
181
|
-
def slugify(value, allow_unicode=False):
|
193
|
+
def slugify(value: Any, allow_unicode: bool = False) -> str:
|
182
194
|
"""
|
183
195
|
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
|
184
196
|
dashes to single dashes. Remove characters that aren't alphanumerics,
|
@@ -198,18 +210,22 @@ def slugify(value, allow_unicode=False):
|
|
198
210
|
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
199
211
|
|
200
212
|
|
201
|
-
def pluralize(singular, plural, number):
|
213
|
+
def pluralize(singular: str, plural: str, number: int) -> str:
|
202
214
|
if number == 1:
|
203
215
|
return singular
|
204
216
|
else:
|
205
217
|
return plural
|
206
218
|
|
207
219
|
|
208
|
-
def pluralize_lazy(singular, plural, number):
|
209
|
-
def _lazy_number_unpickle(
|
220
|
+
def pluralize_lazy(singular: str, plural: str, number: int | str) -> Any:
|
221
|
+
def _lazy_number_unpickle(
|
222
|
+
func: Any, resultclass: Any, number: Any, kwargs: dict[str, Any]
|
223
|
+
) -> Any:
|
210
224
|
return lazy_number(func, resultclass, number=number, **kwargs)
|
211
225
|
|
212
|
-
def lazy_number(
|
226
|
+
def lazy_number(
|
227
|
+
func: Any, resultclass: Any, number: int | str | None = None, **kwargs: Any
|
228
|
+
) -> Any:
|
213
229
|
if isinstance(number, int):
|
214
230
|
kwargs["number"] = number
|
215
231
|
proxy = lazy(func, resultclass)(**kwargs)
|
@@ -217,12 +233,12 @@ def pluralize_lazy(singular, plural, number):
|
|
217
233
|
original_kwargs = kwargs.copy()
|
218
234
|
|
219
235
|
class NumberAwareString(resultclass):
|
220
|
-
def __bool__(self):
|
236
|
+
def __bool__(self) -> bool:
|
221
237
|
return bool(kwargs["singular"])
|
222
238
|
|
223
|
-
def _get_number_value(self, values):
|
239
|
+
def _get_number_value(self, values: dict[str, Any]) -> Any:
|
224
240
|
try:
|
225
|
-
return values[number]
|
241
|
+
return values[number] # type: ignore[index]
|
226
242
|
except KeyError:
|
227
243
|
raise KeyError(
|
228
244
|
f"Your dictionary lacks key '{number}'. Please provide "
|
@@ -230,17 +246,17 @@ def pluralize_lazy(singular, plural, number):
|
|
230
246
|
"string is singular or plural."
|
231
247
|
)
|
232
248
|
|
233
|
-
def _translate(self, number_value):
|
249
|
+
def _translate(self, number_value: int) -> str:
|
234
250
|
kwargs["number"] = number_value
|
235
251
|
return func(**kwargs)
|
236
252
|
|
237
|
-
def format(self, *args, **kwargs):
|
253
|
+
def format(self, *args: Any, **kwargs: Any) -> str:
|
238
254
|
number_value = (
|
239
255
|
self._get_number_value(kwargs) if kwargs and number else args[0]
|
240
256
|
)
|
241
257
|
return self._translate(number_value).format(*args, **kwargs)
|
242
258
|
|
243
|
-
def __mod__(self, rhs):
|
259
|
+
def __mod__(self, rhs: Any) -> str:
|
244
260
|
if isinstance(rhs, dict) and number:
|
245
261
|
number_value = self._get_number_value(rhs)
|
246
262
|
else:
|
plain/utils/timezone.py
CHANGED
@@ -2,11 +2,14 @@
|
|
2
2
|
Timezone-related classes and functions.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
|
5
7
|
import functools
|
6
8
|
import zoneinfo
|
7
9
|
from contextlib import ContextDecorator
|
8
10
|
from datetime import UTC, datetime, timedelta, timezone, tzinfo
|
9
11
|
from threading import local
|
12
|
+
from types import TracebackType
|
10
13
|
|
11
14
|
from plain.runtime import settings
|
12
15
|
|
@@ -28,10 +31,10 @@ __all__ = [
|
|
28
31
|
]
|
29
32
|
|
30
33
|
|
31
|
-
def get_fixed_timezone(offset):
|
34
|
+
def get_fixed_timezone(offset: int | timedelta) -> timezone:
|
32
35
|
"""Return a tzinfo instance with a fixed offset from UTC."""
|
33
36
|
if isinstance(offset, timedelta):
|
34
|
-
offset = offset.total_seconds() // 60
|
37
|
+
offset = int(offset.total_seconds() // 60)
|
35
38
|
sign = "-" if offset < 0 else "+"
|
36
39
|
hhmm = "%02d%02d" % divmod(abs(offset), 60) # noqa: UP031
|
37
40
|
name = sign + hhmm
|
@@ -41,7 +44,7 @@ def get_fixed_timezone(offset):
|
|
41
44
|
# In order to avoid accessing settings at compile time,
|
42
45
|
# wrap the logic in a function and cache the result.
|
43
46
|
@functools.lru_cache
|
44
|
-
def get_default_timezone():
|
47
|
+
def get_default_timezone() -> zoneinfo.ZoneInfo:
|
45
48
|
"""
|
46
49
|
Return the default time zone as a tzinfo instance.
|
47
50
|
|
@@ -51,7 +54,7 @@ def get_default_timezone():
|
|
51
54
|
|
52
55
|
|
53
56
|
# This function exists for consistency with get_current_timezone_name
|
54
|
-
def get_default_timezone_name():
|
57
|
+
def get_default_timezone_name() -> str:
|
55
58
|
"""Return the name of the default time zone."""
|
56
59
|
return _get_timezone_name(get_default_timezone())
|
57
60
|
|
@@ -59,17 +62,17 @@ def get_default_timezone_name():
|
|
59
62
|
_active = local()
|
60
63
|
|
61
64
|
|
62
|
-
def get_current_timezone():
|
65
|
+
def get_current_timezone() -> tzinfo:
|
63
66
|
"""Return the currently active time zone as a tzinfo instance."""
|
64
67
|
return getattr(_active, "value", get_default_timezone())
|
65
68
|
|
66
69
|
|
67
|
-
def get_current_timezone_name():
|
70
|
+
def get_current_timezone_name() -> str:
|
68
71
|
"""Return the name of the currently active time zone."""
|
69
72
|
return _get_timezone_name(get_current_timezone())
|
70
73
|
|
71
74
|
|
72
|
-
def _get_timezone_name(timezone):
|
75
|
+
def _get_timezone_name(timezone: tzinfo) -> str:
|
73
76
|
"""
|
74
77
|
Return the offset for fixed offset timezones, or the name of timezone if
|
75
78
|
not set.
|
@@ -83,7 +86,7 @@ def _get_timezone_name(timezone):
|
|
83
86
|
# because it isn't thread safe.
|
84
87
|
|
85
88
|
|
86
|
-
def activate(timezone):
|
89
|
+
def activate(timezone: tzinfo | str) -> None:
|
87
90
|
"""
|
88
91
|
Set the time zone for the current thread.
|
89
92
|
|
@@ -98,7 +101,7 @@ def activate(timezone):
|
|
98
101
|
raise ValueError(f"Invalid timezone: {timezone!r}")
|
99
102
|
|
100
103
|
|
101
|
-
def deactivate():
|
104
|
+
def deactivate() -> None:
|
102
105
|
"""
|
103
106
|
Unset the time zone for the current thread.
|
104
107
|
|
@@ -121,17 +124,23 @@ class override(ContextDecorator):
|
|
121
124
|
time zone.
|
122
125
|
"""
|
123
126
|
|
124
|
-
def __init__(self, timezone):
|
127
|
+
def __init__(self, timezone: tzinfo | str | None) -> None:
|
125
128
|
self.timezone = timezone
|
129
|
+
self.old_timezone: tzinfo | None = None
|
126
130
|
|
127
|
-
def __enter__(self):
|
131
|
+
def __enter__(self) -> None:
|
128
132
|
self.old_timezone = getattr(_active, "value", None)
|
129
133
|
if self.timezone is None:
|
130
134
|
deactivate()
|
131
135
|
else:
|
132
136
|
activate(self.timezone)
|
133
137
|
|
134
|
-
def __exit__(
|
138
|
+
def __exit__(
|
139
|
+
self,
|
140
|
+
exc_type: type[BaseException] | None,
|
141
|
+
exc_value: BaseException | None,
|
142
|
+
traceback: TracebackType | None,
|
143
|
+
) -> None:
|
135
144
|
if self.old_timezone is None:
|
136
145
|
deactivate()
|
137
146
|
else:
|
@@ -141,7 +150,9 @@ class override(ContextDecorator):
|
|
141
150
|
# Utilities
|
142
151
|
|
143
152
|
|
144
|
-
def localtime(
|
153
|
+
def localtime(
|
154
|
+
value: datetime | None = None, timezone: tzinfo | None = None
|
155
|
+
) -> datetime:
|
145
156
|
"""
|
146
157
|
Convert an aware datetime.datetime to local time.
|
147
158
|
|
@@ -161,7 +172,7 @@ def localtime(value=None, timezone=None):
|
|
161
172
|
return value.astimezone(timezone)
|
162
173
|
|
163
174
|
|
164
|
-
def now():
|
175
|
+
def now() -> datetime:
|
165
176
|
"""
|
166
177
|
Return a timezone aware datetime.
|
167
178
|
"""
|
@@ -172,7 +183,7 @@ def now():
|
|
172
183
|
# The caller should ensure that they don't receive an invalid value like None.
|
173
184
|
|
174
185
|
|
175
|
-
def is_aware(value):
|
186
|
+
def is_aware(value: datetime) -> bool:
|
176
187
|
"""
|
177
188
|
Determine if a given datetime.datetime is aware.
|
178
189
|
|
@@ -185,7 +196,7 @@ def is_aware(value):
|
|
185
196
|
return value.utcoffset() is not None
|
186
197
|
|
187
198
|
|
188
|
-
def is_naive(value):
|
199
|
+
def is_naive(value: datetime) -> bool:
|
189
200
|
"""
|
190
201
|
Determine if a given datetime.datetime is naive.
|
191
202
|
|
@@ -198,7 +209,7 @@ def is_naive(value):
|
|
198
209
|
return value.utcoffset() is None
|
199
210
|
|
200
211
|
|
201
|
-
def make_aware(value, timezone=None):
|
212
|
+
def make_aware(value: datetime, timezone: tzinfo | None = None) -> datetime:
|
202
213
|
"""Make a naive datetime.datetime in a given time zone aware."""
|
203
214
|
if timezone is None:
|
204
215
|
timezone = get_current_timezone()
|
@@ -209,7 +220,7 @@ def make_aware(value, timezone=None):
|
|
209
220
|
return value.replace(tzinfo=timezone)
|
210
221
|
|
211
222
|
|
212
|
-
def make_naive(value, timezone=None):
|
223
|
+
def make_naive(value: datetime, timezone: tzinfo | None = None) -> datetime:
|
213
224
|
"""Make an aware datetime.datetime naive in a given time zone."""
|
214
225
|
if timezone is None:
|
215
226
|
timezone = get_current_timezone()
|
@@ -219,5 +230,5 @@ def make_naive(value, timezone=None):
|
|
219
230
|
return value.astimezone(timezone).replace(tzinfo=None)
|
220
231
|
|
221
232
|
|
222
|
-
def _datetime_ambiguous_or_imaginary(dt, tz):
|
233
|
+
def _datetime_ambiguous_or_imaginary(dt: datetime, tz: tzinfo) -> bool:
|
223
234
|
return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)
|
plain/utils/tree.py
CHANGED
@@ -3,7 +3,10 @@ A class for storing a tree graph. Primarily used for filter constructs in the
|
|
3
3
|
ORM.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from __future__ import annotations
|
7
|
+
|
6
8
|
import copy
|
9
|
+
from typing import Any
|
7
10
|
|
8
11
|
from plain.utils.hashable import make_hashable
|
9
12
|
|
@@ -17,16 +20,26 @@ class Node:
|
|
17
20
|
|
18
21
|
# Standard connector type. Clients usually won't use this at all and
|
19
22
|
# subclasses will usually override the value.
|
20
|
-
default = "DEFAULT"
|
21
|
-
|
22
|
-
def __init__(
|
23
|
+
default: str = "DEFAULT"
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
children: list[Any] | None = None,
|
28
|
+
connector: str | None = None,
|
29
|
+
negated: bool = False,
|
30
|
+
) -> None:
|
23
31
|
"""Construct a new Node. If no connector is given, use the default."""
|
24
|
-
self.children = children[:] if children else []
|
25
|
-
self.connector = connector or self.default
|
26
|
-
self.negated = negated
|
32
|
+
self.children: list[Any] = children[:] if children else []
|
33
|
+
self.connector: str = connector or self.default
|
34
|
+
self.negated: bool = negated
|
27
35
|
|
28
36
|
@classmethod
|
29
|
-
def create(
|
37
|
+
def create(
|
38
|
+
cls,
|
39
|
+
children: list[Any] | None = None,
|
40
|
+
connector: str | None = None,
|
41
|
+
negated: bool = False,
|
42
|
+
) -> Node:
|
30
43
|
"""
|
31
44
|
Create a new instance using Node() instead of __init__() as some
|
32
45
|
subclasses, e.g. plain.models.query_utils.Q, may implement a custom
|
@@ -37,38 +50,38 @@ class Node:
|
|
37
50
|
obj.__class__ = cls
|
38
51
|
return obj
|
39
52
|
|
40
|
-
def __str__(self):
|
53
|
+
def __str__(self) -> str:
|
41
54
|
template = "(NOT (%s: %s))" if self.negated else "(%s: %s)"
|
42
55
|
return template % (self.connector, ", ".join(str(c) for c in self.children))
|
43
56
|
|
44
|
-
def __repr__(self):
|
57
|
+
def __repr__(self) -> str:
|
45
58
|
return f"<{self.__class__.__name__}: {self}>"
|
46
59
|
|
47
|
-
def __copy__(self):
|
60
|
+
def __copy__(self) -> Node:
|
48
61
|
obj = self.create(connector=self.connector, negated=self.negated)
|
49
62
|
obj.children = self.children # Don't [:] as .__init__() via .create() does.
|
50
63
|
return obj
|
51
64
|
|
52
65
|
copy = __copy__
|
53
66
|
|
54
|
-
def __deepcopy__(self, memodict):
|
67
|
+
def __deepcopy__(self, memodict: dict[int, Any]) -> Node:
|
55
68
|
obj = self.create(connector=self.connector, negated=self.negated)
|
56
69
|
obj.children = copy.deepcopy(self.children, memodict)
|
57
70
|
return obj
|
58
71
|
|
59
|
-
def __len__(self):
|
72
|
+
def __len__(self) -> int:
|
60
73
|
"""Return the number of children this node has."""
|
61
74
|
return len(self.children)
|
62
75
|
|
63
|
-
def __bool__(self):
|
76
|
+
def __bool__(self) -> bool:
|
64
77
|
"""Return whether or not this node has children."""
|
65
78
|
return bool(self.children)
|
66
79
|
|
67
|
-
def __contains__(self, other):
|
80
|
+
def __contains__(self, other: Any) -> bool:
|
68
81
|
"""Return True if 'other' is a direct child of this instance."""
|
69
82
|
return other in self.children
|
70
83
|
|
71
|
-
def __eq__(self, other):
|
84
|
+
def __eq__(self, other: Any) -> bool:
|
72
85
|
return (
|
73
86
|
self.__class__ == other.__class__
|
74
87
|
and self.connector == other.connector
|
@@ -76,7 +89,7 @@ class Node:
|
|
76
89
|
and self.children == other.children
|
77
90
|
)
|
78
91
|
|
79
|
-
def __hash__(self):
|
92
|
+
def __hash__(self) -> int:
|
80
93
|
return hash(
|
81
94
|
(
|
82
95
|
self.__class__,
|
@@ -86,7 +99,7 @@ class Node:
|
|
86
99
|
)
|
87
100
|
)
|
88
101
|
|
89
|
-
def add(self, data, conn_type):
|
102
|
+
def add(self, data: Any, conn_type: str) -> Any:
|
90
103
|
"""
|
91
104
|
Combine this tree and the data represented by data using the
|
92
105
|
connector conn_type. The combine is done by squashing the node other
|
@@ -121,6 +134,6 @@ class Node:
|
|
121
134
|
self.children.append(data)
|
122
135
|
return data
|
123
136
|
|
124
|
-
def negate(self):
|
137
|
+
def negate(self) -> None:
|
125
138
|
"""Negate the sense of the root connector."""
|
126
139
|
self.negated = not self.negated
|