dycw-utilities 0.109.0__py3-none-any.whl → 0.109.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.
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.2.dist-info}/METADATA +1 -1
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.2.dist-info}/RECORD +11 -11
- utilities/__init__.py +1 -1
- utilities/dataclasses.py +250 -106
- utilities/orjson.py +14 -0
- utilities/polars.py +8 -2
- utilities/pytest_regressions.py +2 -0
- utilities/python_dotenv.py +26 -43
- utilities/typing.py +8 -5
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.2.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=BFspQsIuZJOmuE31YyhwkR4P62-Ad5dFurosyyABzLA,60
|
2
2
|
utilities/altair.py,sha256=NSyDsm8QlkAGmsGdxVwCkHnPxt_35yJBa9Lg7bz9Ays,9054
|
3
3
|
utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
|
4
4
|
utilities/asyncio.py,sha256=41oQUurWMvadFK5gFnaG21hMM0Vmfn2WS6OpC0R9mas,14757
|
@@ -11,7 +11,7 @@ utilities/contextlib.py,sha256=OOIIEa5lXKGzFAnauaul40nlQnQko6Na4ryiMJcHkIg,478
|
|
11
11
|
utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
|
12
12
|
utilities/cryptography.py,sha256=HyOewI20cl3uRXsKivhIaeLVDInQdzgXZGaly7hS5dE,771
|
13
13
|
utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
|
14
|
-
utilities/dataclasses.py,sha256=
|
14
|
+
utilities/dataclasses.py,sha256=4siQUalzjYckhJ7R9Cn4Pmerxzx18_mjPIwUmeuwwKs,23049
|
15
15
|
utilities/datetime.py,sha256=GOs-MIEW_A49kzqa1yhIoeNeSqqPVgGO-h2AThtgTDk,37326
|
16
16
|
utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
|
17
17
|
utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
|
@@ -38,22 +38,22 @@ utilities/more_itertools.py,sha256=CPUxrMAcTwRxbzbhiqPKi3Xx9hxqI0t6gkWjutaibGk,5
|
|
38
38
|
utilities/numpy.py,sha256=rA1b0_GkBUSMjnv77tinRM70KRnkcmZxI9xbrsXFDRg,21819
|
39
39
|
utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
|
40
40
|
utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
|
41
|
-
utilities/orjson.py,sha256=
|
41
|
+
utilities/orjson.py,sha256=Wj5pzG_VdgoAy14a7Luhem-BgYrRtRFvvl_POiszRd0,36930
|
42
42
|
utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
|
43
43
|
utilities/parse.py,sha256=yLLH51VNwmcWbEvwqh6M-weWt7NIayd7No67Oe80S3k,4585
|
44
44
|
utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
|
45
45
|
utilities/period.py,sha256=ikHXsWtDLr553cfH6p9mMaiCnIAP69B7q84ckWV3HaA,10884
|
46
46
|
utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
|
47
47
|
utilities/platform.py,sha256=NU7ycTvAXAG-fdYmDXaM1m4EOml2cGiaYwaUzfzSqyU,1767
|
48
|
-
utilities/polars.py,sha256=
|
48
|
+
utilities/polars.py,sha256=USK_Rck8nmFYg2Rs-akqN9jV4w52lpz4rgkWUMQdLMk,49087
|
49
49
|
utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
|
50
50
|
utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
51
|
utilities/pydantic.py,sha256=f6qtR5mO2YMuyvNmbaEj5YeD9eGA4YYfb7Bjzh9jUs0,1845
|
52
52
|
utilities/pyinstrument.py,sha256=ROq2txPwbe2ZUuYJ2IDNbfT97lu2ca0v5_C_yn6sSlM,800
|
53
53
|
utilities/pyrsistent.py,sha256=TLJfiiKO4cKNU_pCoM3zDqmSM421qpuoaeaBNnyC_Ac,2489
|
54
54
|
utilities/pytest.py,sha256=85QUax4g2VBBAqAHtM9wekcSLB7_9O8AKFTaCshztL8,7989
|
55
|
-
utilities/pytest_regressions.py,sha256
|
56
|
-
utilities/python_dotenv.py,sha256=
|
55
|
+
utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq3c,5088
|
56
|
+
utilities/python_dotenv.py,sha256=10DHEB7AVeZqH7I4wr6nACdJQYQJanlEj6EsyLvCN9w,3059
|
57
57
|
utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
|
58
58
|
utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
|
59
59
|
utilities/redis.py,sha256=CsDQqc9V6ASLzLQwtbQXZQEndyG9pJiCOhPlPeszt7Y,21203
|
@@ -75,7 +75,7 @@ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
|
|
75
75
|
utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
|
76
76
|
utilities/traceback.py,sha256=KwHPLdEbdj0fFhXo8MBfxcvem8A-VXYDwFMNJ6f0cTM,27328
|
77
77
|
utilities/types.py,sha256=QK8kgH80TJdh_vktaZHrCEk7f1f8kHiDr8dJlK8aSac,17814
|
78
|
-
utilities/typing.py,sha256=
|
78
|
+
utilities/typing.py,sha256=h1vt82GUs-3ww7yFbZ3BWjdUM4NBPHaptcJHSKuRa5E,5341
|
79
79
|
utilities/tzdata.py,sha256=2ZsPmhTVM9Ptrxb4QrWKtKOB9RiH8IOO-A1u7ULdVbg,176
|
80
80
|
utilities/tzlocal.py,sha256=42BCquGF54oIqIKe5RGziP4K8Nbm3Ey7uqcNn6m5ge8,534
|
81
81
|
utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
|
@@ -84,7 +84,7 @@ utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
|
|
84
84
|
utilities/whenever.py,sha256=5x2t47VJmJRWcd_NLFy54NkB3uom-XQYxEbLtEfL1bs,17775
|
85
85
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
86
86
|
utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
|
87
|
-
dycw_utilities-0.109.
|
88
|
-
dycw_utilities-0.109.
|
89
|
-
dycw_utilities-0.109.
|
90
|
-
dycw_utilities-0.109.
|
87
|
+
dycw_utilities-0.109.2.dist-info/METADATA,sha256=9cmTxKEFxwqlG4VNbAnM88UGivDzayFQlB0ntOvtBno,13004
|
88
|
+
dycw_utilities-0.109.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
89
|
+
dycw_utilities-0.109.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
90
|
+
dycw_utilities-0.109.2.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/dataclasses.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from collections.abc import Mapping
|
4
|
+
from collections.abc import Set as AbstractSet
|
4
5
|
from dataclasses import MISSING, dataclass, field, fields, replace
|
5
6
|
from typing import (
|
6
7
|
TYPE_CHECKING,
|
@@ -22,7 +23,6 @@ from utilities.functions import (
|
|
22
23
|
from utilities.iterables import OneStrEmptyError, OneStrNonUniqueError, one_str
|
23
24
|
from utilities.operator import is_equal
|
24
25
|
from utilities.parse import ParseTextError, parse_text
|
25
|
-
from utilities.reprlib import get_repr
|
26
26
|
from utilities.sentinel import Sentinel, sentinel
|
27
27
|
from utilities.types import TDataclass
|
28
28
|
from utilities.typing import get_type_hints
|
@@ -48,6 +48,7 @@ def dataclass_repr(
|
|
48
48
|
exclude: Iterable[str] | None = None,
|
49
49
|
globalns: StrMapping | None = None,
|
50
50
|
localns: StrMapping | None = None,
|
51
|
+
warn_name_errors: bool = False,
|
51
52
|
rel_tol: float | None = None,
|
52
53
|
abs_tol: float | None = None,
|
53
54
|
extra: Mapping[type[_T], Callable[[_T, _T], bool]] | None = None,
|
@@ -56,7 +57,9 @@ def dataclass_repr(
|
|
56
57
|
) -> str:
|
57
58
|
"""Repr a dataclass, without its defaults."""
|
58
59
|
out: dict[str, str] = {}
|
59
|
-
for fld in yield_fields(
|
60
|
+
for fld in yield_fields(
|
61
|
+
obj, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
62
|
+
):
|
60
63
|
if (
|
61
64
|
fld.keep(
|
62
65
|
include=include,
|
@@ -76,6 +79,7 @@ def dataclass_repr(
|
|
76
79
|
exclude=exclude,
|
77
80
|
globalns=globalns,
|
78
81
|
localns=localns,
|
82
|
+
warn_name_errors=warn_name_errors,
|
79
83
|
rel_tol=rel_tol,
|
80
84
|
abs_tol=abs_tol,
|
81
85
|
extra=extra,
|
@@ -90,6 +94,7 @@ def dataclass_repr(
|
|
90
94
|
exclude=exclude,
|
91
95
|
globalns=globalns,
|
92
96
|
localns=localns,
|
97
|
+
warn_name_errors=warn_name_errors,
|
93
98
|
rel_tol=rel_tol,
|
94
99
|
abs_tol=abs_tol,
|
95
100
|
extra=extra,
|
@@ -122,6 +127,7 @@ def dataclass_to_dict(
|
|
122
127
|
exclude: Iterable[str] | None = None,
|
123
128
|
globalns: StrMapping | None = None,
|
124
129
|
localns: StrMapping | None = None,
|
130
|
+
warn_name_errors: bool = False,
|
125
131
|
rel_tol: float | None = None,
|
126
132
|
abs_tol: float | None = None,
|
127
133
|
extra: Mapping[type[_T], Callable[[_T, _T], bool]] | None = None,
|
@@ -131,7 +137,9 @@ def dataclass_to_dict(
|
|
131
137
|
) -> StrMapping:
|
132
138
|
"""Convert a dataclass to a dictionary."""
|
133
139
|
out: StrMapping = {}
|
134
|
-
for fld in yield_fields(
|
140
|
+
for fld in yield_fields(
|
141
|
+
obj, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
142
|
+
):
|
135
143
|
if fld.keep(
|
136
144
|
include=include,
|
137
145
|
exclude=exclude,
|
@@ -146,6 +154,7 @@ def dataclass_to_dict(
|
|
146
154
|
fld.value,
|
147
155
|
globalns=globalns,
|
148
156
|
localns=localns,
|
157
|
+
warn_name_errors=warn_name_errors,
|
149
158
|
rel_tol=rel_tol,
|
150
159
|
abs_tol=abs_tol,
|
151
160
|
extra=extra,
|
@@ -159,6 +168,7 @@ def dataclass_to_dict(
|
|
159
168
|
v,
|
160
169
|
globalns=globalns,
|
161
170
|
localns=localns,
|
171
|
+
warn_name_errors=warn_name_errors,
|
162
172
|
rel_tol=rel_tol,
|
163
173
|
abs_tol=abs_tol,
|
164
174
|
extra=extra,
|
@@ -186,76 +196,157 @@ def mapping_to_dataclass(
|
|
186
196
|
mapping: StrMapping,
|
187
197
|
/,
|
188
198
|
*,
|
199
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
189
200
|
globalns: StrMapping | None = None,
|
190
201
|
localns: StrMapping | None = None,
|
202
|
+
warn_name_errors: bool = False,
|
203
|
+
head: bool = False,
|
191
204
|
case_sensitive: bool = False,
|
192
|
-
|
205
|
+
allow_extra: bool = False,
|
193
206
|
) -> TDataclass:
|
194
207
|
"""Construct a dataclass from a mapping."""
|
195
|
-
fields
|
196
|
-
|
197
|
-
|
198
|
-
|
208
|
+
if fields is None:
|
209
|
+
fields_use = list(
|
210
|
+
yield_fields(
|
211
|
+
cls,
|
212
|
+
globalns=globalns,
|
213
|
+
localns=localns,
|
214
|
+
warn_name_errors=warn_name_errors,
|
215
|
+
)
|
199
216
|
)
|
200
|
-
|
217
|
+
else:
|
218
|
+
fields_use = fields
|
219
|
+
fields_to_values = str_mapping_to_field_mapping(
|
220
|
+
cls,
|
221
|
+
mapping,
|
222
|
+
fields=fields_use,
|
223
|
+
globalns=globalns,
|
224
|
+
localns=localns,
|
225
|
+
warn_name_errors=warn_name_errors,
|
226
|
+
head=head,
|
227
|
+
case_sensitive=case_sensitive,
|
228
|
+
allow_extra=allow_extra,
|
229
|
+
)
|
230
|
+
field_names_to_values = {f.name: v for f, v in fields_to_values.items()}
|
231
|
+
default = {
|
232
|
+
f.name
|
233
|
+
for f in fields_use
|
234
|
+
if (not isinstance(f.default, Sentinel))
|
235
|
+
or (not isinstance(f.default_factory, Sentinel))
|
201
236
|
}
|
202
|
-
|
237
|
+
have = set(field_names_to_values) | default
|
238
|
+
missing = {f.name for f in fields_use} - have
|
239
|
+
if len(missing) >= 1:
|
240
|
+
raise MappingToDataclassError(cls=cls, fields=missing)
|
241
|
+
return cls(**field_names_to_values)
|
203
242
|
|
204
243
|
|
205
|
-
|
206
|
-
|
207
|
-
|
244
|
+
@dataclass(kw_only=True, slots=True)
|
245
|
+
class MappingToDataclassError(Exception, Generic[TDataclass]):
|
246
|
+
cls: type[TDataclass]
|
247
|
+
fields: AbstractSet[str]
|
248
|
+
|
249
|
+
@override
|
250
|
+
def __str__(self) -> str:
|
251
|
+
desc = ", ".join(map(repr, sorted(self.fields)))
|
252
|
+
return f"Unable to construct {get_class_name(self.cls)!r}; missing values for {desc}"
|
253
|
+
|
254
|
+
|
255
|
+
##
|
256
|
+
|
257
|
+
|
258
|
+
def one_field(
|
259
|
+
cls: type[Dataclass],
|
260
|
+
key: str,
|
208
261
|
/,
|
209
262
|
*,
|
263
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
264
|
+
globalns: StrMapping | None = None,
|
265
|
+
localns: StrMapping | None = None,
|
266
|
+
warn_name_errors: bool = False,
|
267
|
+
head: bool = False,
|
210
268
|
case_sensitive: bool = False,
|
211
|
-
|
212
|
-
|
269
|
+
) -> _YieldFieldsClass[Any]:
|
270
|
+
"""Get the unique field a key matches to."""
|
271
|
+
if fields is None:
|
272
|
+
fields_use = list(
|
273
|
+
yield_fields(
|
274
|
+
cls,
|
275
|
+
globalns=globalns,
|
276
|
+
localns=localns,
|
277
|
+
warn_name_errors=warn_name_errors,
|
278
|
+
)
|
279
|
+
)
|
280
|
+
else:
|
281
|
+
fields_use = fields
|
282
|
+
mapping = {f.name: f for f in fields_use}
|
213
283
|
try:
|
214
|
-
|
284
|
+
name = one_str(mapping, key, head=head, case_sensitive=case_sensitive)
|
215
285
|
except OneStrEmptyError:
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
value = field.default_factory()
|
220
|
-
else:
|
221
|
-
raise _MappingToDataclassEmptyError(
|
222
|
-
mapping=mapping, field=field.name, case_sensitive=case_sensitive
|
223
|
-
) from None
|
286
|
+
raise OneFieldEmptyError(
|
287
|
+
cls=cls, key=key, head=head, case_sensitive=case_sensitive
|
288
|
+
) from None
|
224
289
|
except OneStrNonUniqueError as error:
|
225
|
-
raise
|
226
|
-
|
290
|
+
raise OneFieldNonUniqueError(
|
291
|
+
cls=cls,
|
292
|
+
key=key,
|
293
|
+
head=head,
|
294
|
+
case_sensitive=case_sensitive,
|
295
|
+
first=error.first,
|
296
|
+
second=error.second,
|
227
297
|
) from None
|
228
|
-
|
229
|
-
value = mapping[key]
|
230
|
-
if post is not None:
|
231
|
-
value = post(field, value)
|
232
|
-
return value
|
298
|
+
return mapping[name]
|
233
299
|
|
234
300
|
|
235
301
|
@dataclass(kw_only=True, slots=True)
|
236
|
-
class
|
237
|
-
|
238
|
-
|
302
|
+
class OneFieldError(Exception, Generic[TDataclass]):
|
303
|
+
cls: type[TDataclass]
|
304
|
+
key: str
|
305
|
+
head: bool = False
|
306
|
+
case_sensitive: bool = False
|
239
307
|
|
240
308
|
|
241
309
|
@dataclass(kw_only=True, slots=True)
|
242
|
-
class
|
243
|
-
case_sensitive: bool = False
|
244
|
-
|
310
|
+
class OneFieldEmptyError(OneFieldError[TDataclass]):
|
245
311
|
@override
|
246
312
|
def __str__(self) -> str:
|
247
|
-
|
248
|
-
|
313
|
+
head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
|
314
|
+
match self.head, self.case_sensitive:
|
315
|
+
case False, True:
|
316
|
+
tail = f"a field {self.key!r}"
|
317
|
+
case False, False:
|
318
|
+
tail = f"a field {self.key!r} (modulo case)"
|
319
|
+
case True, True:
|
320
|
+
tail = f"any field starting with {self.key!r}"
|
321
|
+
case True, False:
|
322
|
+
tail = f"any field starting with {self.key!r} (modulo case)"
|
323
|
+
case _ as never:
|
324
|
+
assert_never(never)
|
325
|
+
return f"{head} {tail}"
|
249
326
|
|
250
327
|
|
251
328
|
@dataclass(kw_only=True, slots=True)
|
252
|
-
class
|
329
|
+
class OneFieldNonUniqueError(OneFieldError[TDataclass]):
|
253
330
|
first: str
|
254
331
|
second: str
|
255
332
|
|
256
333
|
@override
|
257
334
|
def __str__(self) -> str:
|
258
|
-
|
335
|
+
head = f"Dataclass {get_class_name(self.cls)!r} must contain"
|
336
|
+
match self.head, self.case_sensitive:
|
337
|
+
case False, True:
|
338
|
+
raise ImpossibleCaseError( # pragma: no cover
|
339
|
+
case=[f"{self.head=}", f"{self.case_sensitive=}"]
|
340
|
+
)
|
341
|
+
case False, False:
|
342
|
+
mid = f"field {self.key!r} exactly once (modulo case)"
|
343
|
+
case True, True:
|
344
|
+
mid = f"exactly one field starting with {self.key!r}"
|
345
|
+
case True, False:
|
346
|
+
mid = f"exactly one field starting with {self.key!r} (modulo case)"
|
347
|
+
case _ as never:
|
348
|
+
assert_never(never)
|
349
|
+
return f"{head} {mid}; got {self.first!r}, {self.second!r} and perhaps more"
|
259
350
|
|
260
351
|
|
261
352
|
##
|
@@ -290,6 +381,68 @@ def replace_non_sentinel(
|
|
290
381
|
##
|
291
382
|
|
292
383
|
|
384
|
+
def str_mapping_to_field_mapping(
|
385
|
+
cls: type[TDataclass],
|
386
|
+
mapping: Mapping[str, _T],
|
387
|
+
/,
|
388
|
+
*,
|
389
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
390
|
+
globalns: StrMapping | None = None,
|
391
|
+
localns: StrMapping | None = None,
|
392
|
+
warn_name_errors: bool = False,
|
393
|
+
head: bool = False,
|
394
|
+
case_sensitive: bool = False,
|
395
|
+
allow_extra: bool = False,
|
396
|
+
) -> Mapping[_YieldFieldsClass[Any], _T]:
|
397
|
+
"""Convert a string-mapping into a field-mapping."""
|
398
|
+
keys_to_fields: Mapping[str, _YieldFieldsClass[Any]] = {}
|
399
|
+
for key in mapping:
|
400
|
+
try:
|
401
|
+
keys_to_fields[key] = one_field(
|
402
|
+
cls,
|
403
|
+
key,
|
404
|
+
fields=fields,
|
405
|
+
globalns=globalns,
|
406
|
+
localns=localns,
|
407
|
+
warn_name_errors=warn_name_errors,
|
408
|
+
head=head,
|
409
|
+
case_sensitive=case_sensitive,
|
410
|
+
)
|
411
|
+
except OneFieldEmptyError:
|
412
|
+
if not allow_extra:
|
413
|
+
raise StrMappingToFieldMappingError(
|
414
|
+
cls=cls, key=key, head=head, case_sensitive=case_sensitive
|
415
|
+
) from None
|
416
|
+
return {field: mapping[key] for key, field in keys_to_fields.items()}
|
417
|
+
|
418
|
+
|
419
|
+
@dataclass(kw_only=True, slots=True)
|
420
|
+
class StrMappingToFieldMappingError(Exception):
|
421
|
+
cls: type[Dataclass]
|
422
|
+
key: str
|
423
|
+
head: bool = False
|
424
|
+
case_sensitive: bool = False
|
425
|
+
|
426
|
+
@override
|
427
|
+
def __str__(self) -> str:
|
428
|
+
head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
|
429
|
+
match self.head, self.case_sensitive:
|
430
|
+
case False, True:
|
431
|
+
tail = f"a field {self.key!r}"
|
432
|
+
case False, False:
|
433
|
+
tail = f"a field {self.key!r} (modulo case)"
|
434
|
+
case True, True:
|
435
|
+
tail = f"any field starting with {self.key!r}"
|
436
|
+
case True, False:
|
437
|
+
tail = f"any field starting with {self.key!r} (modulo case)"
|
438
|
+
case _ as never:
|
439
|
+
assert_never(never)
|
440
|
+
return f"{head} {tail}"
|
441
|
+
|
442
|
+
|
443
|
+
##
|
444
|
+
|
445
|
+
|
293
446
|
def text_to_dataclass(
|
294
447
|
text_or_mapping: str | Mapping[str, str],
|
295
448
|
cls: type[TDataclass],
|
@@ -297,29 +450,49 @@ def text_to_dataclass(
|
|
297
450
|
*,
|
298
451
|
globalns: StrMapping | None = None,
|
299
452
|
localns: StrMapping | None = None,
|
453
|
+
warn_name_errors: bool = False,
|
454
|
+
head: bool = False,
|
300
455
|
case_sensitive: bool = False,
|
456
|
+
allow_extra: bool = False,
|
301
457
|
) -> TDataclass:
|
302
458
|
"""Construct a dataclass from a string or a mapping or strings."""
|
303
|
-
fields = list(yield_fields(cls, globalns=globalns, localns=localns))
|
304
459
|
match text_or_mapping:
|
305
460
|
case str() as text:
|
306
|
-
|
307
|
-
case Mapping() as
|
461
|
+
keys_to_serializes = _text_to_dataclass_split_text(text, cls)
|
462
|
+
case Mapping() as keys_to_serializes:
|
308
463
|
...
|
309
464
|
case _ as never:
|
310
465
|
assert_never(never)
|
311
|
-
|
312
|
-
|
313
|
-
|
466
|
+
fields = list(
|
467
|
+
yield_fields(
|
468
|
+
cls, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
314
469
|
)
|
315
|
-
for key, value in text_mapping.items()
|
316
470
|
)
|
471
|
+
fields_to_serializes = str_mapping_to_field_mapping(
|
472
|
+
cls,
|
473
|
+
keys_to_serializes,
|
474
|
+
fields=fields,
|
475
|
+
globalns=globalns,
|
476
|
+
localns=localns,
|
477
|
+
warn_name_errors=warn_name_errors,
|
478
|
+
head=head,
|
479
|
+
case_sensitive=case_sensitive,
|
480
|
+
allow_extra=allow_extra,
|
481
|
+
)
|
482
|
+
field_names_to_values = {
|
483
|
+
f.name: _text_to_dataclass_parse(f, t, cls, case_sensitive=case_sensitive)
|
484
|
+
for f, t in fields_to_serializes.items()
|
485
|
+
}
|
317
486
|
return mapping_to_dataclass(
|
318
487
|
cls,
|
319
|
-
|
488
|
+
field_names_to_values,
|
489
|
+
fields=fields,
|
320
490
|
globalns=globalns,
|
321
491
|
localns=localns,
|
492
|
+
warn_name_errors=warn_name_errors,
|
493
|
+
head=head,
|
322
494
|
case_sensitive=case_sensitive,
|
495
|
+
allow_extra=allow_extra,
|
323
496
|
)
|
324
497
|
|
325
498
|
|
@@ -340,38 +513,18 @@ def _text_to_dataclass_split_key_value_pair(
|
|
340
513
|
return key, value
|
341
514
|
|
342
515
|
|
343
|
-
def
|
344
|
-
|
345
|
-
|
346
|
-
value: str,
|
516
|
+
def _text_to_dataclass_parse(
|
517
|
+
field: _YieldFieldsClass[Any],
|
518
|
+
text: str,
|
347
519
|
cls: type[Dataclass],
|
348
520
|
/,
|
349
521
|
*,
|
350
522
|
case_sensitive: bool = False,
|
351
|
-
) ->
|
352
|
-
mapping = {f.name: f for f in fields}
|
353
|
-
try:
|
354
|
-
name = one_str(mapping, key, head=True, case_sensitive=case_sensitive)
|
355
|
-
except OneStrEmptyError:
|
356
|
-
raise _TextToDataClassGetFieldEmptyError(
|
357
|
-
cls=cls, key=key, case_sensitive=case_sensitive
|
358
|
-
) from None
|
359
|
-
except OneStrNonUniqueError as error:
|
360
|
-
raise _TextToDataClassGetFieldNonUniqueError(
|
361
|
-
cls=cls,
|
362
|
-
key=key,
|
363
|
-
case_sensitive=case_sensitive,
|
364
|
-
first=error.first,
|
365
|
-
second=error.second,
|
366
|
-
) from None
|
367
|
-
field = mapping[name]
|
523
|
+
) -> Any:
|
368
524
|
try:
|
369
|
-
|
525
|
+
return parse_text(field.type_, text, case_sensitive=case_sensitive)
|
370
526
|
except ParseTextError:
|
371
|
-
raise _TextToDataClassParseValueError(
|
372
|
-
cls=cls, field=field, text=value
|
373
|
-
) from None
|
374
|
-
return key, parsed
|
527
|
+
raise _TextToDataClassParseValueError(cls=cls, field=field, text=text) from None
|
375
528
|
|
376
529
|
|
377
530
|
@dataclass(kw_only=True, slots=True)
|
@@ -388,31 +541,6 @@ class _TextToDataClassSplitKeyValuePairError(TextToDataClassError):
|
|
388
541
|
return f"Unable to construct {get_class_name(self.cls)!r}; failed to split key-value pair {self.text!r}"
|
389
542
|
|
390
543
|
|
391
|
-
@dataclass(kw_only=True, slots=True)
|
392
|
-
class _TextToDataClassGetFieldEmptyError(TextToDataClassError[TDataclass]):
|
393
|
-
key: str
|
394
|
-
case_sensitive: bool = False
|
395
|
-
|
396
|
-
@override
|
397
|
-
def __str__(self) -> str:
|
398
|
-
desc = f"Dataclass {get_class_name(self.cls)!r} does not contain any field starting with {self.key!r}"
|
399
|
-
return desc if self.case_sensitive else f"{desc} (modulo case)"
|
400
|
-
|
401
|
-
|
402
|
-
@dataclass(kw_only=True, slots=True)
|
403
|
-
class _TextToDataClassGetFieldNonUniqueError(TextToDataClassError[TDataclass]):
|
404
|
-
key: str
|
405
|
-
case_sensitive: bool = False
|
406
|
-
first: str
|
407
|
-
second: str
|
408
|
-
|
409
|
-
@override
|
410
|
-
def __str__(self) -> str:
|
411
|
-
head = f"Dataclass {get_class_name(self.cls)!r} must contain exactly one field starting with {self.key!r}"
|
412
|
-
mid = "" if self.case_sensitive else " (modulo case)"
|
413
|
-
return f"{head}{mid}; got {self.first!r}, {self.second!r} and perhaps more"
|
414
|
-
|
415
|
-
|
416
544
|
@dataclass(kw_only=True, slots=True)
|
417
545
|
class _TextToDataClassParseValueError(TextToDataClassError[TDataclass]):
|
418
546
|
field: _YieldFieldsClass[Any]
|
@@ -431,16 +559,18 @@ def yield_fields(
|
|
431
559
|
obj: Dataclass,
|
432
560
|
/,
|
433
561
|
*,
|
434
|
-
globalns: StrMapping | None =
|
435
|
-
localns: StrMapping | None =
|
562
|
+
globalns: StrMapping | None = None,
|
563
|
+
localns: StrMapping | None = None,
|
564
|
+
warn_name_errors: bool = False,
|
436
565
|
) -> Iterator[_YieldFieldsInstance[Any]]: ...
|
437
566
|
@overload
|
438
567
|
def yield_fields(
|
439
568
|
obj: type[Dataclass],
|
440
569
|
/,
|
441
570
|
*,
|
442
|
-
globalns: StrMapping | None =
|
443
|
-
localns: StrMapping | None =
|
571
|
+
globalns: StrMapping | None = None,
|
572
|
+
localns: StrMapping | None = None,
|
573
|
+
warn_name_errors: bool = False,
|
444
574
|
) -> Iterator[_YieldFieldsClass[Any]]: ...
|
445
575
|
def yield_fields(
|
446
576
|
obj: Dataclass | type[Dataclass],
|
@@ -448,10 +578,16 @@ def yield_fields(
|
|
448
578
|
*,
|
449
579
|
globalns: StrMapping | None = None,
|
450
580
|
localns: StrMapping | None = None,
|
581
|
+
warn_name_errors: bool = False,
|
451
582
|
) -> Iterator[_YieldFieldsInstance[Any]] | Iterator[_YieldFieldsClass[Any]]:
|
452
583
|
"""Yield the fields of a dataclass."""
|
453
584
|
if is_dataclass_instance(obj):
|
454
|
-
for field in yield_fields(
|
585
|
+
for field in yield_fields(
|
586
|
+
type(obj),
|
587
|
+
globalns=globalns,
|
588
|
+
localns=localns,
|
589
|
+
warn_name_errors=warn_name_errors,
|
590
|
+
):
|
455
591
|
yield _YieldFieldsInstance(
|
456
592
|
name=field.name,
|
457
593
|
value=getattr(obj, field.name),
|
@@ -466,7 +602,9 @@ def yield_fields(
|
|
466
602
|
kw_only=field.kw_only,
|
467
603
|
)
|
468
604
|
elif is_dataclass_class(obj):
|
469
|
-
hints = get_type_hints(
|
605
|
+
hints = get_type_hints(
|
606
|
+
obj, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
607
|
+
)
|
470
608
|
for field in fields(obj):
|
471
609
|
if isinstance(field.type, type):
|
472
610
|
type_ = field.type
|
@@ -580,12 +718,18 @@ class YieldFieldsError(Exception):
|
|
580
718
|
|
581
719
|
__all__ = [
|
582
720
|
"MappingToDataclassError",
|
721
|
+
"OneFieldEmptyError",
|
722
|
+
"OneFieldError",
|
723
|
+
"OneFieldNonUniqueError",
|
724
|
+
"StrMappingToFieldMappingError",
|
583
725
|
"TextToDataClassError",
|
584
726
|
"YieldFieldsError",
|
585
727
|
"dataclass_repr",
|
586
728
|
"dataclass_to_dict",
|
587
729
|
"mapping_to_dataclass",
|
730
|
+
"one_field",
|
588
731
|
"replace_non_sentinel",
|
732
|
+
"str_mapping_to_field_mapping",
|
589
733
|
"text_to_dataclass",
|
590
734
|
"yield_fields",
|
591
735
|
]
|
utilities/orjson.py
CHANGED
@@ -114,6 +114,7 @@ def serialize(
|
|
114
114
|
before: Callable[[Any], Any] | None = None,
|
115
115
|
globalns: StrMapping | None = None,
|
116
116
|
localns: StrMapping | None = None,
|
117
|
+
warn_name_errors: bool = False,
|
117
118
|
dataclass_hook: _DataclassHook | None = None,
|
118
119
|
dataclass_defaults: bool = False,
|
119
120
|
) -> bytes:
|
@@ -123,6 +124,7 @@ def serialize(
|
|
123
124
|
before=before,
|
124
125
|
globalns=globalns,
|
125
126
|
localns=localns,
|
127
|
+
warn_name_errors=warn_name_errors,
|
126
128
|
dataclass_hook=dataclass_hook,
|
127
129
|
dataclass_defaults=dataclass_defaults,
|
128
130
|
)
|
@@ -139,6 +141,7 @@ def _pre_process(
|
|
139
141
|
before: Callable[[Any], Any] | None = None,
|
140
142
|
globalns: StrMapping | None = None,
|
141
143
|
localns: StrMapping | None = None,
|
144
|
+
warn_name_errors: bool = False,
|
142
145
|
dataclass_hook: _DataclassHook | None = None,
|
143
146
|
dataclass_defaults: bool = False,
|
144
147
|
error: _ErrorMode = "raise",
|
@@ -150,6 +153,7 @@ def _pre_process(
|
|
150
153
|
before=before,
|
151
154
|
globalns=globalns,
|
152
155
|
localns=localns,
|
156
|
+
warn_name_errors=warn_name_errors,
|
153
157
|
dataclass_hook=dataclass_hook,
|
154
158
|
dataclass_defaults=dataclass_defaults,
|
155
159
|
error=error,
|
@@ -196,6 +200,7 @@ def _pre_process(
|
|
196
200
|
dataclass,
|
197
201
|
globalns=globalns,
|
198
202
|
localns=localns,
|
203
|
+
warn_name_errors=warn_name_errors,
|
199
204
|
final=partial(_dataclass_final, hook=dataclass_hook),
|
200
205
|
defaults=dataclass_defaults,
|
201
206
|
)
|
@@ -212,6 +217,7 @@ def _pre_process(
|
|
212
217
|
before=before,
|
213
218
|
globalns=globalns,
|
214
219
|
localns=localns,
|
220
|
+
warn_name_errors=warn_name_errors,
|
215
221
|
dataclass_hook=dataclass_hook,
|
216
222
|
)
|
217
223
|
case list() as list_:
|
@@ -222,6 +228,7 @@ def _pre_process(
|
|
222
228
|
before=before,
|
223
229
|
globalns=globalns,
|
224
230
|
localns=localns,
|
231
|
+
warn_name_errors=warn_name_errors,
|
225
232
|
dataclass_hook=dataclass_hook,
|
226
233
|
)
|
227
234
|
case Mapping() as mapping:
|
@@ -234,6 +241,7 @@ def _pre_process(
|
|
234
241
|
before=before,
|
235
242
|
globalns=globalns,
|
236
243
|
localns=localns,
|
244
|
+
warn_name_errors=warn_name_errors,
|
237
245
|
dataclass_hook=dataclass_hook,
|
238
246
|
)
|
239
247
|
case tuple() as tuple_:
|
@@ -244,6 +252,7 @@ def _pre_process(
|
|
244
252
|
before=before,
|
245
253
|
globalns=globalns,
|
246
254
|
localns=localns,
|
255
|
+
warn_name_errors=warn_name_errors,
|
247
256
|
dataclass_hook=dataclass_hook,
|
248
257
|
)
|
249
258
|
# other
|
@@ -263,6 +272,7 @@ def _pre_process_container(
|
|
263
272
|
before: Callable[[Any], Any] | None = None,
|
264
273
|
globalns: StrMapping | None = None,
|
265
274
|
localns: StrMapping | None = None,
|
275
|
+
warn_name_errors: bool = False,
|
266
276
|
dataclass_hook: _DataclassHook | None = None,
|
267
277
|
dataclass_include_defaults: bool = False,
|
268
278
|
) -> Any:
|
@@ -272,6 +282,7 @@ def _pre_process_container(
|
|
272
282
|
before=before,
|
273
283
|
globalns=globalns,
|
274
284
|
localns=localns,
|
285
|
+
warn_name_errors=warn_name_errors,
|
275
286
|
dataclass_hook=dataclass_hook,
|
276
287
|
dataclass_defaults=dataclass_include_defaults,
|
277
288
|
)
|
@@ -716,6 +727,7 @@ class OrjsonFormatter(Formatter):
|
|
716
727
|
before: Callable[[Any], Any] | None = None,
|
717
728
|
globalns: StrMapping | None = None,
|
718
729
|
localns: StrMapping | None = None,
|
730
|
+
warn_name_errors: bool = False,
|
719
731
|
dataclass_hook: _DataclassHook | None = None,
|
720
732
|
dataclass_defaults: bool = False,
|
721
733
|
) -> None:
|
@@ -723,6 +735,7 @@ class OrjsonFormatter(Formatter):
|
|
723
735
|
self._before = before
|
724
736
|
self._globalns = globalns
|
725
737
|
self._localns = localns
|
738
|
+
self._warn_name_errors = warn_name_errors
|
726
739
|
self._dataclass_hook = dataclass_hook
|
727
740
|
self._dataclass_defaults = dataclass_defaults
|
728
741
|
|
@@ -752,6 +765,7 @@ class OrjsonFormatter(Formatter):
|
|
752
765
|
before=self._before,
|
753
766
|
globalns=self._globalns,
|
754
767
|
localns=self._localns,
|
768
|
+
warn_name_errors=self._warn_name_errors,
|
755
769
|
dataclass_hook=self._dataclass_hook,
|
756
770
|
dataclass_defaults=self._dataclass_defaults,
|
757
771
|
).decode()
|
utilities/polars.py
CHANGED
@@ -726,10 +726,13 @@ def dataclass_to_schema(
|
|
726
726
|
*,
|
727
727
|
globalns: StrMapping | None = None,
|
728
728
|
localns: StrMapping | None = None,
|
729
|
+
warn_name_errors: bool = False,
|
729
730
|
) -> SchemaDict:
|
730
731
|
"""Cast a dataclass as a schema dict."""
|
731
732
|
out: dict[str, Any] = {}
|
732
|
-
for field in yield_fields(
|
733
|
+
for field in yield_fields(
|
734
|
+
obj, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
735
|
+
):
|
733
736
|
if is_dataclass_instance(field.value):
|
734
737
|
dtypes = dataclass_to_schema(
|
735
738
|
field.value, globalns=globalns, localns=localns
|
@@ -1388,12 +1391,15 @@ def struct_from_dataclass(
|
|
1388
1391
|
*,
|
1389
1392
|
globalns: StrMapping | None = None,
|
1390
1393
|
localns: StrMapping | None = None,
|
1394
|
+
warn_name_errors: bool = False,
|
1391
1395
|
time_zone: TimeZoneLike | None = None,
|
1392
1396
|
) -> Struct:
|
1393
1397
|
"""Construct the Struct data type for a dataclass."""
|
1394
1398
|
if not is_dataclass_class(cls):
|
1395
1399
|
raise _StructFromDataClassNotADataclassError(cls=cls)
|
1396
|
-
anns = get_type_hints(
|
1400
|
+
anns = get_type_hints(
|
1401
|
+
cls, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
|
1402
|
+
)
|
1397
1403
|
data_types = {
|
1398
1404
|
k: _struct_from_dataclass_one(v, time_zone=time_zone) for k, v in anns.items()
|
1399
1405
|
}
|
utilities/pytest_regressions.py
CHANGED
@@ -51,6 +51,7 @@ class OrjsonRegressionFixture:
|
|
51
51
|
*,
|
52
52
|
globalns: StrMapping | None = None,
|
53
53
|
localns: StrMapping | None = None,
|
54
|
+
warn_name_errors: bool = False,
|
54
55
|
dataclass_defaults: bool = False,
|
55
56
|
suffix: str | None = None,
|
56
57
|
) -> None:
|
@@ -61,6 +62,7 @@ class OrjsonRegressionFixture:
|
|
61
62
|
obj,
|
62
63
|
globalns=globalns,
|
63
64
|
localns=localns,
|
65
|
+
warn_name_errors=warn_name_errors,
|
64
66
|
dataclass_defaults=dataclass_defaults,
|
65
67
|
)
|
66
68
|
basename = self._basename
|
utilities/python_dotenv.py
CHANGED
@@ -1,25 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
|
-
from functools import partial
|
5
4
|
from os import environ
|
6
|
-
from typing import TYPE_CHECKING,
|
5
|
+
from typing import TYPE_CHECKING, override
|
7
6
|
|
8
7
|
from dotenv import dotenv_values
|
9
8
|
|
10
|
-
from utilities.dataclasses import
|
11
|
-
_MappingToDataclassEmptyError,
|
12
|
-
_YieldFieldsClass,
|
13
|
-
mapping_to_dataclass,
|
14
|
-
)
|
9
|
+
from utilities.dataclasses import MappingToDataclassError, text_to_dataclass
|
15
10
|
from utilities.git import get_repo_root
|
16
11
|
from utilities.iterables import MergeStrMappingsError, merge_str_mappings
|
17
|
-
from utilities.parse import ParseTextError, parse_text
|
18
12
|
from utilities.pathlib import PWD
|
19
13
|
from utilities.reprlib import get_repr
|
20
14
|
|
21
15
|
if TYPE_CHECKING:
|
22
16
|
from collections.abc import Mapping
|
17
|
+
from collections.abc import Set as AbstractSet
|
23
18
|
from pathlib import Path
|
24
19
|
|
25
20
|
from utilities.types import PathLike, StrMapping, TDataclass
|
@@ -32,6 +27,9 @@ def load_settings(
|
|
32
27
|
cwd: PathLike = PWD,
|
33
28
|
globalns: StrMapping | None = None,
|
34
29
|
localns: StrMapping | None = None,
|
30
|
+
warn_name_errors: bool = False,
|
31
|
+
head: bool = False,
|
32
|
+
case_sensitive: bool = False,
|
35
33
|
) -> TDataclass:
|
36
34
|
"""Load a set of settings from the `.env` file."""
|
37
35
|
path = get_repo_root(cwd=cwd).joinpath(".env")
|
@@ -39,35 +37,30 @@ def load_settings(
|
|
39
37
|
raise _LoadSettingsFileNotFoundError(path=path) from None
|
40
38
|
maybe_values_dotenv = dotenv_values(path)
|
41
39
|
try:
|
42
|
-
maybe_values = merge_str_mappings(
|
40
|
+
maybe_values: Mapping[str, str | None] = merge_str_mappings(
|
41
|
+
maybe_values_dotenv, environ, case_sensitive=case_sensitive
|
42
|
+
)
|
43
43
|
except MergeStrMappingsError as error:
|
44
44
|
raise _LoadSettingsDuplicateKeysError(
|
45
|
-
path=path,
|
45
|
+
path=path,
|
46
|
+
values=error.mapping,
|
47
|
+
counts=error.counts,
|
48
|
+
case_sensitive=case_sensitive,
|
46
49
|
) from None
|
47
50
|
values = {k: v for k, v in maybe_values.items() if v is not None}
|
48
51
|
try:
|
49
|
-
return
|
50
|
-
cls,
|
52
|
+
return text_to_dataclass(
|
51
53
|
values,
|
54
|
+
cls,
|
52
55
|
globalns=globalns,
|
53
56
|
localns=localns,
|
54
|
-
|
57
|
+
warn_name_errors=warn_name_errors,
|
58
|
+
head=head,
|
59
|
+
case_sensitive=case_sensitive,
|
60
|
+
allow_extra=True,
|
55
61
|
)
|
56
|
-
except
|
57
|
-
raise
|
58
|
-
path=path, values=error.mapping, field=error.field
|
59
|
-
) from None
|
60
|
-
|
61
|
-
|
62
|
-
def _load_settings_post(
|
63
|
-
field: _YieldFieldsClass[Any], text: str, /, *, path: Path, values: StrMapping
|
64
|
-
) -> Any:
|
65
|
-
try:
|
66
|
-
return parse_text(field.type_, text)
|
67
|
-
except ParseTextError:
|
68
|
-
raise _LoadSettingsParseTextError(
|
69
|
-
path=path, values=values, field=field, text=text
|
70
|
-
) from None
|
62
|
+
except MappingToDataclassError as error:
|
63
|
+
raise _LoadSettingsMissingKeysError(path=path, fields=error.fields) from None
|
71
64
|
|
72
65
|
|
73
66
|
@dataclass(kw_only=True, slots=True)
|
@@ -79,22 +72,13 @@ class LoadSettingsError(Exception):
|
|
79
72
|
class _LoadSettingsDuplicateKeysError(LoadSettingsError):
|
80
73
|
values: StrMapping
|
81
74
|
counts: Mapping[str, int]
|
75
|
+
case_sensitive: bool = False
|
82
76
|
|
83
77
|
@override
|
84
78
|
def __str__(self) -> str:
|
85
79
|
return f"Mapping {get_repr(dict(self.values))} keys must not contain duplicates (modulo case); got {get_repr(self.counts)}"
|
86
80
|
|
87
81
|
|
88
|
-
@dataclass(kw_only=True, slots=True)
|
89
|
-
class _LoadSettingsEmptyError(LoadSettingsError):
|
90
|
-
values: StrMapping
|
91
|
-
field: str
|
92
|
-
|
93
|
-
@override
|
94
|
-
def __str__(self) -> str:
|
95
|
-
return f"Field {self.field!r} must exist (modulo case)"
|
96
|
-
|
97
|
-
|
98
82
|
@dataclass(kw_only=True, slots=True)
|
99
83
|
class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
100
84
|
@override
|
@@ -103,14 +87,13 @@ class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
|
103
87
|
|
104
88
|
|
105
89
|
@dataclass(kw_only=True, slots=True)
|
106
|
-
class
|
107
|
-
|
108
|
-
field: _YieldFieldsClass[Any]
|
109
|
-
text: str
|
90
|
+
class _LoadSettingsMissingKeysError(LoadSettingsError):
|
91
|
+
fields: AbstractSet[str]
|
110
92
|
|
111
93
|
@override
|
112
94
|
def __str__(self) -> str:
|
113
|
-
|
95
|
+
desc = ", ".join(map(repr, sorted(self.fields)))
|
96
|
+
return f"Unable to load {str(self.path)!r}; missing value(s) for {desc}"
|
114
97
|
|
115
98
|
|
116
99
|
__all__ = ["LoadSettingsError", "load_settings"]
|
utilities/typing.py
CHANGED
@@ -19,6 +19,7 @@ from typing import (
|
|
19
19
|
from typing import get_args as _get_args
|
20
20
|
from typing import get_type_hints as _get_type_hints
|
21
21
|
from uuid import UUID
|
22
|
+
from warnings import warn
|
22
23
|
|
23
24
|
from utilities.iterables import unique_everseen
|
24
25
|
from utilities.sentinel import Sentinel
|
@@ -68,16 +69,18 @@ def get_type_hints(
|
|
68
69
|
*,
|
69
70
|
globalns: StrMapping | None = None,
|
70
71
|
localns: StrMapping | None = None,
|
72
|
+
warn_name_errors: bool = False,
|
71
73
|
) -> dict[str, Any]:
|
72
74
|
"""Get the type hints of an object."""
|
73
75
|
result: dict[str, Any] = cls.__annotations__
|
74
76
|
_ = {Literal, Path, Sentinel, StrMapping, UUID, dt}
|
75
|
-
|
76
|
-
|
77
|
+
globalns_use = globals() | ({} if globalns is None else dict(globalns))
|
78
|
+
localns_use = {} if localns is None else dict(localns)
|
77
79
|
try:
|
78
|
-
hints = _get_type_hints(cls, globalns=
|
79
|
-
except NameError:
|
80
|
-
|
80
|
+
hints = _get_type_hints(cls, globalns=globalns_use, localns=localns_use)
|
81
|
+
except NameError as error:
|
82
|
+
if warn_name_errors:
|
83
|
+
warn(f"Error getting type hints for {cls!r}; {error}", stacklevel=2)
|
81
84
|
else:
|
82
85
|
result.update({
|
83
86
|
key: value
|
File without changes
|
File without changes
|