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.
- plain/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +234 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/utils/datastructures.py
CHANGED
|
@@ -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__(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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__(
|
|
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
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
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
|
|
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'.
|
plain/utils/deconstruct.py
CHANGED
|
@@ -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(
|
|
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
|
|
48
|
-
klass
|
|
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."
|