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.
Files changed (126) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +23 -0
  3. plain/assets/compile.py +20 -7
  4. plain/assets/finders.py +15 -11
  5. plain/assets/fingerprints.py +6 -5
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +23 -17
  8. plain/chores/registry.py +14 -9
  9. plain/cli/agent/__init__.py +1 -1
  10. plain/cli/agent/docs.py +7 -6
  11. plain/cli/agent/llmdocs.py +18 -8
  12. plain/cli/agent/md.py +19 -14
  13. plain/cli/agent/prompt.py +1 -1
  14. plain/cli/agent/request.py +37 -17
  15. plain/cli/build.py +2 -2
  16. plain/cli/changelog.py +8 -4
  17. plain/cli/chores.py +4 -4
  18. plain/cli/core.py +8 -5
  19. plain/cli/docs.py +2 -2
  20. plain/cli/formatting.py +10 -7
  21. plain/cli/output.py +6 -2
  22. plain/cli/preflight.py +3 -3
  23. plain/cli/print.py +1 -1
  24. plain/cli/registry.py +10 -6
  25. plain/cli/scaffold.py +1 -1
  26. plain/cli/settings.py +1 -1
  27. plain/cli/shell.py +10 -7
  28. plain/cli/startup.py +3 -3
  29. plain/cli/urls.py +10 -4
  30. plain/cli/utils.py +2 -2
  31. plain/csrf/middleware.py +15 -5
  32. plain/csrf/views.py +11 -8
  33. plain/debug.py +5 -2
  34. plain/exceptions.py +20 -51
  35. plain/forms/__init__.py +1 -1
  36. plain/forms/boundfield.py +14 -7
  37. plain/forms/exceptions.py +1 -1
  38. plain/forms/fields.py +139 -97
  39. plain/forms/forms.py +55 -39
  40. plain/http/cookie.py +15 -7
  41. plain/http/multipartparser.py +50 -30
  42. plain/http/request.py +97 -73
  43. plain/http/response.py +99 -80
  44. plain/internal/__init__.py +8 -1
  45. plain/internal/files/base.py +34 -18
  46. plain/internal/files/locks.py +19 -11
  47. plain/internal/files/move.py +8 -3
  48. plain/internal/files/temp.py +23 -5
  49. plain/internal/files/uploadedfile.py +42 -26
  50. plain/internal/files/uploadhandler.py +48 -27
  51. plain/internal/files/utils.py +13 -6
  52. plain/internal/handlers/base.py +20 -6
  53. plain/internal/handlers/exception.py +19 -5
  54. plain/internal/handlers/wsgi.py +30 -18
  55. plain/internal/middleware/headers.py +11 -2
  56. plain/internal/middleware/hosts.py +10 -2
  57. plain/internal/middleware/https.py +13 -3
  58. plain/internal/middleware/slash.py +15 -5
  59. plain/json.py +2 -1
  60. plain/logs/configure.py +3 -1
  61. plain/logs/debug.py +16 -5
  62. plain/logs/formatters.py +6 -3
  63. plain/logs/loggers.py +56 -52
  64. plain/logs/utils.py +19 -9
  65. plain/packages/config.py +14 -6
  66. plain/packages/registry.py +27 -12
  67. plain/paginator.py +31 -21
  68. plain/preflight/checks.py +3 -1
  69. plain/preflight/files.py +3 -1
  70. plain/preflight/registry.py +25 -10
  71. plain/preflight/results.py +10 -4
  72. plain/preflight/security.py +7 -5
  73. plain/preflight/urls.py +4 -1
  74. plain/runtime/__init__.py +4 -3
  75. plain/runtime/global_settings.py +1 -1
  76. plain/runtime/user_settings.py +26 -17
  77. plain/runtime/utils.py +1 -1
  78. plain/signals/dispatch/dispatcher.py +39 -17
  79. plain/signing.py +49 -30
  80. plain/templates/jinja/__init__.py +13 -5
  81. plain/templates/jinja/environments.py +4 -3
  82. plain/templates/jinja/extensions.py +9 -3
  83. plain/templates/jinja/filters.py +7 -2
  84. plain/templates/jinja/globals.py +1 -1
  85. plain/test/client.py +246 -174
  86. plain/test/encoding.py +9 -6
  87. plain/test/exceptions.py +10 -2
  88. plain/urls/converters.py +13 -10
  89. plain/urls/patterns.py +32 -20
  90. plain/urls/resolvers.py +32 -22
  91. plain/urls/utils.py +5 -1
  92. plain/utils/cache.py +14 -8
  93. plain/utils/crypto.py +21 -5
  94. plain/utils/datastructures.py +84 -54
  95. plain/utils/dateparse.py +10 -7
  96. plain/utils/deconstruct.py +12 -4
  97. plain/utils/decorators.py +5 -1
  98. plain/utils/duration.py +8 -4
  99. plain/utils/encoding.py +14 -7
  100. plain/utils/functional.py +62 -47
  101. plain/utils/hashable.py +5 -1
  102. plain/utils/html.py +21 -14
  103. plain/utils/http.py +16 -9
  104. plain/utils/inspect.py +14 -6
  105. plain/utils/ipv6.py +7 -3
  106. plain/utils/itercompat.py +6 -1
  107. plain/utils/module_loading.py +7 -3
  108. plain/utils/regex_helper.py +23 -13
  109. plain/utils/safestring.py +14 -6
  110. plain/utils/text.py +34 -18
  111. plain/utils/timezone.py +30 -19
  112. plain/utils/tree.py +31 -18
  113. plain/validators.py +71 -44
  114. plain/views/base.py +16 -6
  115. plain/views/errors.py +11 -4
  116. plain/views/exceptions.py +4 -1
  117. plain/views/objects.py +27 -17
  118. plain/views/redirect.py +14 -10
  119. plain/views/templates.py +1 -1
  120. plain/wsgi.py +3 -1
  121. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
  122. plain-0.70.0.dist-info/RECORD +169 -0
  123. plain-0.68.1.dist-info/RECORD +0 -169
  124. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
  125. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
  126. {plain-0.68.1.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(ch, input_iter):
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
- ch = None
270
- if ch == "?":
271
- ch = None
272
- return int(values[0]), ch
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(regex, flags=0):
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(safety_marker, func):
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(self, length, truncate, text, truncate_len):
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(self, length, truncate, text, truncate_len, words):
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(func, resultclass, number, kwargs):
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(func, resultclass, number=None, **kwargs):
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__(self, exc_type, exc_value, traceback):
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(value=None, timezone=None):
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__(self, children=None, connector=None, negated=False):
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(cls, children=None, connector=None, negated=False):
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