plain 0.68.0__py3-none-any.whl → 0.103.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 (192) hide show
  1. plain/CHANGELOG.md +684 -1
  2. plain/README.md +1 -1
  3. plain/agents/.claude/rules/plain.md +88 -0
  4. plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
  5. plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
  6. plain/assets/compile.py +25 -12
  7. plain/assets/finders.py +24 -17
  8. plain/assets/fingerprints.py +10 -7
  9. plain/assets/urls.py +1 -1
  10. plain/assets/views.py +47 -33
  11. plain/chores/README.md +25 -23
  12. plain/chores/__init__.py +2 -1
  13. plain/chores/core.py +27 -0
  14. plain/chores/registry.py +23 -36
  15. plain/cli/README.md +185 -16
  16. plain/cli/__init__.py +2 -1
  17. plain/cli/agent.py +234 -0
  18. plain/cli/build.py +7 -8
  19. plain/cli/changelog.py +11 -5
  20. plain/cli/chores.py +32 -34
  21. plain/cli/core.py +110 -26
  22. plain/cli/docs.py +98 -21
  23. plain/cli/formatting.py +40 -17
  24. plain/cli/install.py +10 -54
  25. plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
  26. plain/cli/output.py +6 -2
  27. plain/cli/preflight.py +27 -75
  28. plain/cli/print.py +4 -4
  29. plain/cli/registry.py +96 -10
  30. plain/cli/{agent/request.py → request.py} +67 -33
  31. plain/cli/runtime.py +45 -0
  32. plain/cli/scaffold.py +2 -7
  33. plain/cli/server.py +153 -0
  34. plain/cli/settings.py +53 -49
  35. plain/cli/shell.py +15 -12
  36. plain/cli/startup.py +9 -8
  37. plain/cli/upgrade.py +17 -104
  38. plain/cli/urls.py +12 -7
  39. plain/cli/utils.py +3 -3
  40. plain/csrf/README.md +65 -40
  41. plain/csrf/middleware.py +53 -43
  42. plain/debug.py +5 -2
  43. plain/exceptions.py +22 -114
  44. plain/forms/README.md +453 -24
  45. plain/forms/__init__.py +55 -4
  46. plain/forms/boundfield.py +15 -8
  47. plain/forms/exceptions.py +1 -1
  48. plain/forms/fields.py +346 -143
  49. plain/forms/forms.py +75 -45
  50. plain/http/README.md +356 -9
  51. plain/http/__init__.py +41 -26
  52. plain/http/cookie.py +15 -7
  53. plain/http/exceptions.py +65 -0
  54. plain/http/middleware.py +32 -0
  55. plain/http/multipartparser.py +99 -88
  56. plain/http/request.py +362 -250
  57. plain/http/response.py +99 -197
  58. plain/internal/__init__.py +8 -1
  59. plain/internal/files/base.py +35 -19
  60. plain/internal/files/locks.py +19 -11
  61. plain/internal/files/move.py +8 -3
  62. plain/internal/files/temp.py +25 -6
  63. plain/internal/files/uploadedfile.py +47 -28
  64. plain/internal/files/uploadhandler.py +64 -58
  65. plain/internal/files/utils.py +24 -10
  66. plain/internal/handlers/base.py +34 -23
  67. plain/internal/handlers/exception.py +68 -65
  68. plain/internal/handlers/wsgi.py +65 -54
  69. plain/internal/middleware/headers.py +37 -11
  70. plain/internal/middleware/hosts.py +11 -8
  71. plain/internal/middleware/https.py +17 -7
  72. plain/internal/middleware/slash.py +14 -9
  73. plain/internal/reloader.py +77 -0
  74. plain/json.py +2 -1
  75. plain/logs/README.md +161 -62
  76. plain/logs/__init__.py +1 -1
  77. plain/logs/{loggers.py → app.py} +71 -67
  78. plain/logs/configure.py +63 -14
  79. plain/logs/debug.py +17 -6
  80. plain/logs/filters.py +15 -0
  81. plain/logs/formatters.py +7 -4
  82. plain/packages/README.md +105 -23
  83. plain/packages/config.py +15 -7
  84. plain/packages/registry.py +27 -16
  85. plain/paginator.py +31 -21
  86. plain/preflight/README.md +209 -24
  87. plain/preflight/__init__.py +1 -0
  88. plain/preflight/checks.py +3 -1
  89. plain/preflight/files.py +3 -1
  90. plain/preflight/registry.py +26 -11
  91. plain/preflight/results.py +15 -7
  92. plain/preflight/security.py +15 -13
  93. plain/preflight/settings.py +54 -0
  94. plain/preflight/urls.py +4 -1
  95. plain/runtime/README.md +115 -47
  96. plain/runtime/__init__.py +10 -6
  97. plain/runtime/global_settings.py +34 -25
  98. plain/runtime/secret.py +20 -0
  99. plain/runtime/user_settings.py +110 -38
  100. plain/runtime/utils.py +1 -1
  101. plain/server/LICENSE +35 -0
  102. plain/server/README.md +155 -0
  103. plain/server/__init__.py +9 -0
  104. plain/server/app.py +52 -0
  105. plain/server/arbiter.py +555 -0
  106. plain/server/config.py +118 -0
  107. plain/server/errors.py +31 -0
  108. plain/server/glogging.py +292 -0
  109. plain/server/http/__init__.py +12 -0
  110. plain/server/http/body.py +283 -0
  111. plain/server/http/errors.py +155 -0
  112. plain/server/http/message.py +400 -0
  113. plain/server/http/parser.py +70 -0
  114. plain/server/http/unreader.py +88 -0
  115. plain/server/http/wsgi.py +421 -0
  116. plain/server/pidfile.py +92 -0
  117. plain/server/sock.py +240 -0
  118. plain/server/util.py +317 -0
  119. plain/server/workers/__init__.py +6 -0
  120. plain/server/workers/base.py +304 -0
  121. plain/server/workers/sync.py +212 -0
  122. plain/server/workers/thread.py +399 -0
  123. plain/server/workers/workertmp.py +50 -0
  124. plain/signals/README.md +170 -1
  125. plain/signals/__init__.py +0 -1
  126. plain/signals/dispatch/dispatcher.py +49 -27
  127. plain/signing.py +131 -35
  128. plain/templates/README.md +211 -20
  129. plain/templates/jinja/__init__.py +13 -5
  130. plain/templates/jinja/environments.py +5 -4
  131. plain/templates/jinja/extensions.py +12 -5
  132. plain/templates/jinja/filters.py +7 -2
  133. plain/templates/jinja/globals.py +2 -2
  134. plain/test/README.md +184 -22
  135. plain/test/client.py +340 -222
  136. plain/test/encoding.py +9 -6
  137. plain/test/exceptions.py +7 -2
  138. plain/urls/README.md +157 -73
  139. plain/urls/converters.py +18 -15
  140. plain/urls/exceptions.py +2 -2
  141. plain/urls/patterns.py +38 -22
  142. plain/urls/resolvers.py +35 -25
  143. plain/urls/utils.py +5 -1
  144. plain/utils/README.md +250 -3
  145. plain/utils/cache.py +17 -11
  146. plain/utils/crypto.py +21 -5
  147. plain/utils/datastructures.py +89 -56
  148. plain/utils/dateparse.py +9 -6
  149. plain/utils/deconstruct.py +15 -7
  150. plain/utils/decorators.py +5 -1
  151. plain/utils/dotenv.py +373 -0
  152. plain/utils/duration.py +8 -4
  153. plain/utils/encoding.py +14 -7
  154. plain/utils/functional.py +66 -49
  155. plain/utils/hashable.py +5 -1
  156. plain/utils/html.py +36 -22
  157. plain/utils/http.py +16 -9
  158. plain/utils/inspect.py +14 -6
  159. plain/utils/ipv6.py +7 -3
  160. plain/utils/itercompat.py +6 -1
  161. plain/utils/module_loading.py +7 -3
  162. plain/utils/regex_helper.py +37 -23
  163. plain/utils/safestring.py +14 -6
  164. plain/utils/text.py +41 -23
  165. plain/utils/timezone.py +33 -22
  166. plain/utils/tree.py +35 -19
  167. plain/validators.py +94 -52
  168. plain/views/README.md +156 -79
  169. plain/views/__init__.py +0 -1
  170. plain/views/base.py +25 -18
  171. plain/views/errors.py +13 -5
  172. plain/views/exceptions.py +4 -1
  173. plain/views/forms.py +6 -6
  174. plain/views/objects.py +52 -49
  175. plain/views/redirect.py +18 -15
  176. plain/views/templates.py +5 -3
  177. plain/wsgi.py +3 -1
  178. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
  179. plain-0.103.0.dist-info/RECORD +198 -0
  180. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
  181. plain-0.103.0.dist-info/entry_points.txt +2 -0
  182. plain/AGENTS.md +0 -18
  183. plain/cli/agent/__init__.py +0 -20
  184. plain/cli/agent/docs.py +0 -80
  185. plain/cli/agent/md.py +0 -87
  186. plain/cli/agent/prompt.py +0 -45
  187. plain/csrf/views.py +0 -31
  188. plain/logs/utils.py +0 -46
  189. plain/templates/AGENTS.md +0 -3
  190. plain-0.68.0.dist-info/RECORD +0 -169
  191. plain-0.68.0.dist-info/entry_points.txt +0 -5
  192. {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
1
4
  import copy
2
- from collections.abc import Mapping
5
+ from collections.abc import Callable, Iterable, Iterator, Mapping
6
+ from typing import Any, TypeVar
7
+
8
+ _KT = TypeVar("_KT")
9
+ _VT = TypeVar("_VT")
3
10
 
4
11
 
5
12
  class OrderedSet:
@@ -7,37 +14,37 @@ class OrderedSet:
7
14
  A set which keeps the ordering of the inserted items.
8
15
  """
9
16
 
10
- def __init__(self, iterable=None):
11
- self.dict = dict.fromkeys(iterable or ())
17
+ def __init__(self, iterable: Iterable[Any] | None = None) -> None:
18
+ self.dict: dict[Any, None] = dict.fromkeys(iterable or ())
12
19
 
13
- def add(self, item):
20
+ def add(self, item: Any) -> None:
14
21
  self.dict[item] = None
15
22
 
16
- def remove(self, item):
23
+ def remove(self, item: Any) -> None:
17
24
  del self.dict[item]
18
25
 
19
- def discard(self, item):
26
+ def discard(self, item: Any) -> None:
20
27
  try:
21
28
  self.remove(item)
22
29
  except KeyError:
23
30
  pass
24
31
 
25
- def __iter__(self):
32
+ def __iter__(self) -> Iterator[Any]:
26
33
  return iter(self.dict)
27
34
 
28
- def __reversed__(self):
35
+ def __reversed__(self) -> Iterator[Any]:
29
36
  return reversed(self.dict)
30
37
 
31
- def __contains__(self, item):
38
+ def __contains__(self, item: Any) -> bool:
32
39
  return item in self.dict
33
40
 
34
- def __bool__(self):
41
+ def __bool__(self) -> bool:
35
42
  return bool(self.dict)
36
43
 
37
- def __len__(self):
44
+ def __len__(self) -> int:
38
45
  return len(self.dict)
39
46
 
40
- def __repr__(self):
47
+ def __repr__(self) -> str:
41
48
  data = repr(list(self.dict)) if self.dict else ""
42
49
  return f"{self.__class__.__qualname__}({data})"
43
50
 
@@ -46,7 +53,7 @@ class MultiValueDictKeyError(KeyError):
46
53
  pass
47
54
 
48
55
 
49
- class MultiValueDict(dict):
56
+ class MultiValueDict(dict[str, list[Any]]):
50
57
  """
51
58
  A subclass of dictionary customized to handle multiple values for the
52
59
  same key.
@@ -69,13 +76,17 @@ class MultiValueDict(dict):
69
76
  single name-value pairs.
70
77
  """
71
78
 
72
- def __init__(self, key_to_list_mapping=()):
79
+ def __init__(
80
+ self,
81
+ key_to_list_mapping: Mapping[str, list[Any]]
82
+ | Iterable[tuple[str, list[Any]]] = (),
83
+ ) -> None:
73
84
  super().__init__(key_to_list_mapping)
74
85
 
75
- def __repr__(self):
86
+ def __repr__(self) -> str:
76
87
  return f"<{self.__class__.__name__}: {super().__repr__()}>"
77
88
 
78
- def __getitem__(self, key):
89
+ def __getitem__(self, key: str) -> Any:
79
90
  """
80
91
  Return the last data value for this key, or [] if it's an empty list;
81
92
  raise KeyError if not found.
@@ -89,13 +100,13 @@ class MultiValueDict(dict):
89
100
  except IndexError:
90
101
  return []
91
102
 
92
- def __setitem__(self, key, value):
103
+ def __setitem__(self, key: str, value: Any) -> None:
93
104
  super().__setitem__(key, [value])
94
105
 
95
- def __copy__(self):
106
+ def __copy__(self) -> MultiValueDict:
96
107
  return self.__class__([(k, v[:]) for k, v in self.lists()])
97
108
 
98
- def __deepcopy__(self, memo):
109
+ def __deepcopy__(self, memo: builtins.dict[int, Any]) -> MultiValueDict:
99
110
  result = self.__class__()
100
111
  memo[id(self)] = result
101
112
  for key, value in dict.items(self):
@@ -104,16 +115,16 @@ class MultiValueDict(dict):
104
115
  )
105
116
  return result
106
117
 
107
- def __getstate__(self):
118
+ def __getstate__(self) -> builtins.dict[str, Any]:
108
119
  return {**self.__dict__, "_data": {k: self._getlist(k) for k in self}}
109
120
 
110
- def __setstate__(self, obj_dict):
121
+ def __setstate__(self, obj_dict: builtins.dict[str, Any]) -> None:
111
122
  data = obj_dict.pop("_data", {})
112
123
  for k, v in data.items():
113
124
  self.setlist(k, v)
114
125
  self.__dict__.update(obj_dict)
115
126
 
116
- def get(self, key, default=None):
127
+ def get(self, key: str, default: Any = None) -> Any:
117
128
  """
118
129
  Return the last data value for the passed key. If key doesn't exist
119
130
  or value is an empty list, return `default`.
@@ -126,7 +137,9 @@ class MultiValueDict(dict):
126
137
  return default
127
138
  return val
128
139
 
129
- def _getlist(self, key, default=None, force_list=False):
140
+ def _getlist(
141
+ self, key: str, default: list[Any] | None = None, force_list: bool = False
142
+ ) -> list[Any] | None:
130
143
  """
131
144
  Return a list of values for the key.
132
145
 
@@ -144,37 +157,39 @@ class MultiValueDict(dict):
144
157
  values = list(values) if values is not None else None
145
158
  return values
146
159
 
147
- def getlist(self, key, default=None):
160
+ def getlist(self, key: str, default: list[Any] | None = None) -> list[Any]:
148
161
  """
149
162
  Return the list of values for the key. If key doesn't exist, return a
150
163
  default value.
151
164
  """
152
- return self._getlist(key, default, force_list=True)
165
+ return self._getlist(key, default, force_list=True) # type: ignore
153
166
 
154
- def setlist(self, key, list_):
167
+ def setlist(self, key: str, list_: list[Any]) -> None:
155
168
  super().__setitem__(key, list_)
156
169
 
157
- def setdefault(self, key, default=None):
170
+ def setdefault(self, key: str, default: Any = None) -> Any:
158
171
  if key not in self:
159
172
  self[key] = default
160
173
  # Do not return default here because __setitem__() may store
161
174
  # another value -- QueryDict.__setitem__() does. Look it up.
162
175
  return self[key]
163
176
 
164
- def setlistdefault(self, key, default_list=None):
177
+ def setlistdefault(
178
+ self, key: str, default_list: list[Any] | None = None
179
+ ) -> list[Any]:
165
180
  if key not in self:
166
181
  if default_list is None:
167
182
  default_list = []
168
183
  self.setlist(key, default_list)
169
184
  # Do not return default_list here because setlist() may store
170
185
  # another value -- QueryDict.setlist() does. Look it up.
171
- return self._getlist(key)
186
+ return self._getlist(key) # type: ignore[return-value]
172
187
 
173
- def appendlist(self, key, value):
188
+ def appendlist(self, key: str, value: Any) -> None:
174
189
  """Append an item to the internal list associated with key."""
175
190
  self.setlistdefault(key).append(value)
176
191
 
177
- def items(self):
192
+ def items(self) -> Iterator[tuple[str, Any]]: # type: ignore[override]
178
193
  """
179
194
  Yield (key, value) pairs, where value is the last item in the list
180
195
  associated with the key.
@@ -182,23 +197,23 @@ class MultiValueDict(dict):
182
197
  for key in self:
183
198
  yield key, self[key]
184
199
 
185
- def lists(self):
200
+ def lists(self) -> Iterator[tuple[str, list[Any]]]:
186
201
  """Yield (key, list) pairs."""
187
202
  return iter(super().items())
188
203
 
189
- def values(self):
204
+ def values(self) -> Iterator[Any]: # type: ignore[override]
190
205
  """Yield the last value on every key list."""
191
206
  for key in self:
192
207
  yield self[key]
193
208
 
194
- def copy(self):
209
+ def copy(self) -> MultiValueDict:
195
210
  """Return a shallow copy of this object."""
196
211
  return copy.copy(self)
197
212
 
198
- def update(self, *args, **kwargs):
213
+ def update(self, *args: Any, **kwargs: Any) -> None:
199
214
  """Extend rather than replace existing key lists."""
200
215
  if len(args) > 1:
201
- raise TypeError("update expected at most 1 argument, got %d" % len(args)) # noqa: UP031
216
+ raise TypeError(f"update expected at most 1 argument, got {len(args)}")
202
217
  if args:
203
218
  arg = args[0]
204
219
  if isinstance(arg, MultiValueDict):
@@ -212,7 +227,7 @@ class MultiValueDict(dict):
212
227
  for key, value in kwargs.items():
213
228
  self.setlistdefault(key).append(value)
214
229
 
215
- def dict(self):
230
+ def dict(self) -> builtins.dict[str, Any]:
216
231
  """Return current object as a dict with singular values."""
217
232
  return {key: self[key] for key in self}
218
233
 
@@ -230,12 +245,19 @@ class ImmutableList(tuple):
230
245
  AttributeError: You cannot mutate this.
231
246
  """
232
247
 
233
- def __new__(cls, *args, warning="ImmutableList object is immutable.", **kwargs):
248
+ warning: str # Set in __new__
249
+
250
+ def __new__(
251
+ cls,
252
+ *args: Any,
253
+ warning: str = "ImmutableList object is immutable.",
254
+ **kwargs: Any,
255
+ ) -> ImmutableList:
234
256
  self = tuple.__new__(cls, *args, **kwargs)
235
257
  self.warning = warning
236
258
  return self
237
259
 
238
- def complain(self, *args, **kwargs):
260
+ def complain(self, *args: Any, **kwargs: Any) -> None:
239
261
  raise AttributeError(self.warning)
240
262
 
241
263
  # All list mutation functions complain.
@@ -254,7 +276,7 @@ class ImmutableList(tuple):
254
276
  reverse = complain
255
277
 
256
278
 
257
- class DictWrapper(dict):
279
+ class DictWrapper(dict[str, Any]):
258
280
  """
259
281
  Wrap accesses to a dictionary so that certain values (those starting with
260
282
  the specified prefix) are passed through a function before being returned.
@@ -264,12 +286,14 @@ class DictWrapper(dict):
264
286
  quoted before being used.
265
287
  """
266
288
 
267
- def __init__(self, data, func, prefix):
289
+ def __init__(
290
+ self, data: dict[str, Any], func: Callable[[Any], Any], prefix: str
291
+ ) -> None:
268
292
  super().__init__(data)
269
293
  self.func = func
270
294
  self.prefix = prefix
271
295
 
272
- def __getitem__(self, key):
296
+ def __getitem__(self, key: str) -> Any:
273
297
  """
274
298
  Retrieve the real value after stripping the prefix string (if
275
299
  present). If the prefix is present, pass the value through self.func
@@ -283,7 +307,7 @@ class DictWrapper(dict):
283
307
  return value
284
308
 
285
309
 
286
- class CaseInsensitiveMapping(Mapping):
310
+ class CaseInsensitiveMapping(Mapping[str, Any]):
287
311
  """
288
312
  Mapping allowing case-insensitive key lookups. Original case of keys is
289
313
  preserved for iteration and string representation.
@@ -301,35 +325,44 @@ class CaseInsensitiveMapping(Mapping):
301
325
  {'name': 'Jane'}
302
326
  """
303
327
 
304
- def __init__(self, data):
305
- self._store = {k.lower(): (k, v) for k, v in self._unpack_items(data)}
328
+ def __init__(self, data: Mapping[str, Any] | Iterable[tuple[str, Any]]) -> None:
329
+ self._store: dict[str, tuple[str, Any]] = {
330
+ k.lower(): (k, v) for k, v in self._unpack_items(data)
331
+ }
306
332
 
307
- def __getitem__(self, key):
333
+ def __getitem__(self, key: str) -> Any:
308
334
  return self._store[key.lower()][1]
309
335
 
310
- def __len__(self):
336
+ def __len__(self) -> int:
311
337
  return len(self._store)
312
338
 
313
- def __eq__(self, other):
314
- return isinstance(other, Mapping) and {
315
- k.lower(): v for k, v in self.items()
316
- } == {k.lower(): v for k, v in other.items()}
339
+ def __eq__(self, other: object) -> bool:
340
+ if not isinstance(other, Mapping):
341
+ return False
342
+ return {k.lower(): v for k, v in self.items()} == {
343
+ k.lower(): v for k, v in other.items() if isinstance(k, str)
344
+ }
317
345
 
318
- def __iter__(self):
346
+ def __iter__(self) -> Iterator[str]:
319
347
  return (original_key for original_key, value in self._store.values())
320
348
 
321
- def __repr__(self):
349
+ def __repr__(self) -> str:
322
350
  return repr(dict(self._store.values()))
323
351
 
324
- def copy(self):
352
+ def copy(self) -> CaseInsensitiveMapping:
325
353
  return self
326
354
 
327
355
  @staticmethod
328
- def _unpack_items(data):
356
+ def _unpack_items(
357
+ data: Mapping[str, Any] | Iterable[tuple[str, Any]],
358
+ ) -> Iterator[tuple[str, Any]]:
329
359
  # Explicitly test for dict first as the common case for performance,
330
360
  # avoiding abc's __instancecheck__ and _abc_instancecheck for the
331
361
  # general Mapping case.
332
- if isinstance(data, dict | Mapping):
362
+ if isinstance(data, dict):
363
+ yield from data.items()
364
+ return
365
+ if isinstance(data, Mapping):
333
366
  yield from data.items()
334
367
  return
335
368
  for i, elem in enumerate(data):
plain/utils/dateparse.py CHANGED
@@ -1,10 +1,11 @@
1
1
  """Functions to parse datetime objects."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  # We're using regular expressions rather than time.strptime because:
4
6
  # - They provide both validation and parsing.
5
7
  # - They're more flexible for datetimes.
6
8
  # - The date/datetime/time constructors produce friendlier error messages.
7
-
8
9
  import datetime
9
10
 
10
11
  from plain.utils.regex_helper import _lazy_re_compile
@@ -64,7 +65,7 @@ postgres_interval_re = _lazy_re_compile(
64
65
  )
65
66
 
66
67
 
67
- def parse_date(value):
68
+ def parse_date(value: str) -> datetime.date | None:
68
69
  """Parse a string and return a datetime.date.
69
70
 
70
71
  Raise ValueError if the input is well formatted but not a valid date.
@@ -76,9 +77,10 @@ def parse_date(value):
76
77
  if match := date_re.match(value):
77
78
  kw = {k: int(v) for k, v in match.groupdict().items()}
78
79
  return datetime.date(**kw)
80
+ return None
79
81
 
80
82
 
81
- def parse_time(value):
83
+ def parse_time(value: str) -> datetime.time | None:
82
84
  """Parse a string and return a datetime.time.
83
85
 
84
86
  This function doesn't support time zone offsets.
@@ -98,10 +100,10 @@ def parse_time(value):
98
100
  kw = match.groupdict()
99
101
  kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
100
102
  kw = {k: int(v) for k, v in kw.items() if v is not None}
101
- return datetime.time(**kw)
103
+ return datetime.time(**kw) # type: ignore[arg-type]
102
104
 
103
105
 
104
- def parse_datetime(value):
106
+ def parse_datetime(value: str) -> datetime.datetime | None:
105
107
  """Parse a string and return a datetime.datetime.
106
108
 
107
109
  This function supports time zone offsets. When the input contains one,
@@ -127,9 +129,10 @@ def parse_datetime(value):
127
129
  tzinfo = get_fixed_timezone(offset)
128
130
  kw = {k: int(v) for k, v in kw.items() if v is not None}
129
131
  return datetime.datetime(**kw, tzinfo=tzinfo)
132
+ return None
130
133
 
131
134
 
132
- def parse_duration(value):
135
+ def parse_duration(value: str) -> datetime.timedelta | None:
133
136
  """Parse a duration string and return a datetime.timedelta.
134
137
 
135
138
  The preferred format for durations in Plain is '%d %H:%M:%S.%f'.
@@ -1,7 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
1
4
  from importlib import import_module
5
+ from typing import Any, TypeVar
6
+
7
+ T = TypeVar("T")
2
8
 
3
9
 
4
- def deconstructible(*args, path=None):
10
+ def deconstructible(
11
+ *args: type[T], path: str | None = None
12
+ ) -> Callable[[type[T]], type[T]] | type[T]:
5
13
  """
6
14
  Class decorator that allows the decorated class to be serialized
7
15
  by the migrations subsystem.
@@ -9,14 +17,14 @@ def deconstructible(*args, path=None):
9
17
  The `path` kwarg specifies the import path.
10
18
  """
11
19
 
12
- def decorator(klass):
13
- def __new__(cls, *args, **kwargs):
20
+ def decorator(klass: type[T]) -> type[T]:
21
+ def __new__(cls: type[T], *args: Any, **kwargs: Any) -> T:
14
22
  # We capture the arguments to make returning them trivial
15
- obj = super(klass, cls).__new__(cls)
23
+ obj = super(klass, cls).__new__(cls) # type: ignore[misc]
16
24
  obj._constructor_args = (args, kwargs)
17
25
  return obj
18
26
 
19
- def deconstruct(obj):
27
+ def deconstruct(obj: Any) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
20
28
  """
21
29
  Return a 3-tuple of class import path, positional arguments,
22
30
  and keyword arguments.
@@ -44,8 +52,8 @@ def deconstructible(*args, path=None):
44
52
  obj._constructor_args[1],
45
53
  )
46
54
 
47
- klass.__new__ = staticmethod(__new__)
48
- klass.deconstruct = deconstruct
55
+ setattr(klass, "__new__", staticmethod(__new__))
56
+ setattr(klass, "deconstruct", deconstruct)
49
57
 
50
58
  return klass
51
59
 
plain/utils/decorators.py CHANGED
@@ -1,8 +1,12 @@
1
1
  "Functions that help with dynamically creating decorators for views."
2
2
 
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
3
7
 
4
8
  class classonlymethod(classmethod):
5
- def __get__(self, instance, cls=None):
9
+ def __get__(self, instance: object | None, cls: type | None = None) -> Any:
6
10
  if instance is not None:
7
11
  raise AttributeError(
8
12
  "This method is available only on the class, not on instances."