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