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