dycw-utilities 0.109.0__py3-none-any.whl → 0.109.1__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.1.dist-info}/METADATA +1 -1
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.1.dist-info}/RECORD +7 -7
- utilities/__init__.py +1 -1
- utilities/dataclasses.py +202 -104
- utilities/python_dotenv.py +24 -43
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.1.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.109.0.dist-info → dycw_utilities-0.109.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=hXFp40lgEbzHJocIbjY5_v_hKGMzhE6Os9g06SFuZME,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=12Cxq0ZXFuc4lPD6Y92Ny-b25C0EnkAZ3Hz59tetcbQ,21709
|
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
|
@@ -53,7 +53,7 @@ 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
55
|
utilities/pytest_regressions.py,sha256=Kp1NS_cyXvBFqyiF_oSzYmSJzIOdAZ0SFcSGmbL_UtI,5001
|
56
|
-
utilities/python_dotenv.py,sha256
|
56
|
+
utilities/python_dotenv.py,sha256=-d2bQ3Ayyv9JUK59k6F3-mTzQmb2SV0HzqG9fpsD8C8,2976
|
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
|
@@ -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.1.dist-info/METADATA,sha256=dCcv9ZcxodWQ1Ze4u30ZUglTEC7NipHnFMOMN9zv6Oc,13004
|
88
|
+
dycw_utilities-0.109.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
89
|
+
dycw_utilities-0.109.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
90
|
+
dycw_utilities-0.109.1.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
|
@@ -186,76 +186,140 @@ def mapping_to_dataclass(
|
|
186
186
|
mapping: StrMapping,
|
187
187
|
/,
|
188
188
|
*,
|
189
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
189
190
|
globalns: StrMapping | None = None,
|
190
191
|
localns: StrMapping | None = None,
|
192
|
+
head: bool = False,
|
191
193
|
case_sensitive: bool = False,
|
192
|
-
|
194
|
+
allow_extra: bool = False,
|
193
195
|
) -> TDataclass:
|
194
196
|
"""Construct a dataclass from a mapping."""
|
195
|
-
fields
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
197
|
+
if fields is None:
|
198
|
+
fields_use = list(yield_fields(cls, globalns=globalns, localns=localns))
|
199
|
+
else:
|
200
|
+
fields_use = fields
|
201
|
+
fields_to_values = str_mapping_to_field_mapping(
|
202
|
+
cls,
|
203
|
+
mapping,
|
204
|
+
fields=fields_use,
|
205
|
+
globalns=globalns,
|
206
|
+
localns=localns,
|
207
|
+
head=head,
|
208
|
+
case_sensitive=case_sensitive,
|
209
|
+
allow_extra=allow_extra,
|
210
|
+
)
|
211
|
+
field_names_to_values = {f.name: v for f, v in fields_to_values.items()}
|
212
|
+
default = {
|
213
|
+
f.name
|
214
|
+
for f in fields_use
|
215
|
+
if (not isinstance(f.default, Sentinel))
|
216
|
+
or (not isinstance(f.default_factory, Sentinel))
|
201
217
|
}
|
202
|
-
|
218
|
+
have = set(field_names_to_values) | default
|
219
|
+
missing = {f.name for f in fields_use} - have
|
220
|
+
if len(missing) >= 1:
|
221
|
+
raise MappingToDataclassError(cls=cls, fields=missing)
|
222
|
+
return cls(**field_names_to_values)
|
203
223
|
|
204
224
|
|
205
|
-
|
206
|
-
|
207
|
-
|
225
|
+
@dataclass(kw_only=True, slots=True)
|
226
|
+
class MappingToDataclassError(Exception, Generic[TDataclass]):
|
227
|
+
cls: type[TDataclass]
|
228
|
+
fields: AbstractSet[str]
|
229
|
+
|
230
|
+
@override
|
231
|
+
def __str__(self) -> str:
|
232
|
+
desc = ", ".join(map(repr, sorted(self.fields)))
|
233
|
+
return f"Unable to construct {get_class_name(self.cls)!r}; missing values for {desc}"
|
234
|
+
|
235
|
+
|
236
|
+
##
|
237
|
+
|
238
|
+
|
239
|
+
def one_field(
|
240
|
+
cls: type[Dataclass],
|
241
|
+
key: str,
|
208
242
|
/,
|
209
243
|
*,
|
244
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
245
|
+
globalns: StrMapping | None = None,
|
246
|
+
localns: StrMapping | None = None,
|
247
|
+
head: bool = False,
|
210
248
|
case_sensitive: bool = False,
|
211
|
-
|
212
|
-
|
249
|
+
) -> _YieldFieldsClass[Any]:
|
250
|
+
"""Get the unique field a key matches to."""
|
251
|
+
if fields is None:
|
252
|
+
fields_use = list(yield_fields(cls, globalns=globalns, localns=localns))
|
253
|
+
else:
|
254
|
+
fields_use = fields
|
255
|
+
mapping = {f.name: f for f in fields_use}
|
213
256
|
try:
|
214
|
-
|
257
|
+
name = one_str(mapping, key, head=head, case_sensitive=case_sensitive)
|
215
258
|
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
|
259
|
+
raise OneFieldEmptyError(
|
260
|
+
cls=cls, key=key, head=head, case_sensitive=case_sensitive
|
261
|
+
) from None
|
224
262
|
except OneStrNonUniqueError as error:
|
225
|
-
raise
|
226
|
-
|
263
|
+
raise OneFieldNonUniqueError(
|
264
|
+
cls=cls,
|
265
|
+
key=key,
|
266
|
+
head=head,
|
267
|
+
case_sensitive=case_sensitive,
|
268
|
+
first=error.first,
|
269
|
+
second=error.second,
|
227
270
|
) from None
|
228
|
-
|
229
|
-
value = mapping[key]
|
230
|
-
if post is not None:
|
231
|
-
value = post(field, value)
|
232
|
-
return value
|
271
|
+
return mapping[name]
|
233
272
|
|
234
273
|
|
235
274
|
@dataclass(kw_only=True, slots=True)
|
236
|
-
class
|
237
|
-
|
238
|
-
|
275
|
+
class OneFieldError(Exception, Generic[TDataclass]):
|
276
|
+
cls: type[TDataclass]
|
277
|
+
key: str
|
278
|
+
head: bool = False
|
279
|
+
case_sensitive: bool = False
|
239
280
|
|
240
281
|
|
241
282
|
@dataclass(kw_only=True, slots=True)
|
242
|
-
class
|
243
|
-
case_sensitive: bool = False
|
244
|
-
|
283
|
+
class OneFieldEmptyError(OneFieldError[TDataclass]):
|
245
284
|
@override
|
246
285
|
def __str__(self) -> str:
|
247
|
-
|
248
|
-
|
286
|
+
head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
|
287
|
+
match self.head, self.case_sensitive:
|
288
|
+
case False, True:
|
289
|
+
tail = f"a field {self.key!r}"
|
290
|
+
case False, False:
|
291
|
+
tail = f"a field {self.key!r} (modulo case)"
|
292
|
+
case True, True:
|
293
|
+
tail = f"any field starting with {self.key!r}"
|
294
|
+
case True, False:
|
295
|
+
tail = f"any field starting with {self.key!r} (modulo case)"
|
296
|
+
case _ as never:
|
297
|
+
assert_never(never)
|
298
|
+
return f"{head} {tail}"
|
249
299
|
|
250
300
|
|
251
301
|
@dataclass(kw_only=True, slots=True)
|
252
|
-
class
|
302
|
+
class OneFieldNonUniqueError(OneFieldError[TDataclass]):
|
253
303
|
first: str
|
254
304
|
second: str
|
255
305
|
|
256
306
|
@override
|
257
307
|
def __str__(self) -> str:
|
258
|
-
|
308
|
+
head = f"Dataclass {get_class_name(self.cls)!r} must contain"
|
309
|
+
match self.head, self.case_sensitive:
|
310
|
+
case False, True:
|
311
|
+
raise ImpossibleCaseError( # pragma: no cover
|
312
|
+
case=[f"{self.head=}", f"{self.case_sensitive=}"]
|
313
|
+
)
|
314
|
+
case False, False:
|
315
|
+
mid = f"field {self.key!r} exactly once (modulo case)"
|
316
|
+
case True, True:
|
317
|
+
mid = f"exactly one field starting with {self.key!r}"
|
318
|
+
case True, False:
|
319
|
+
mid = f"exactly one field starting with {self.key!r} (modulo case)"
|
320
|
+
case _ as never:
|
321
|
+
assert_never(never)
|
322
|
+
return f"{head} {mid}; got {self.first!r}, {self.second!r} and perhaps more"
|
259
323
|
|
260
324
|
|
261
325
|
##
|
@@ -290,6 +354,66 @@ def replace_non_sentinel(
|
|
290
354
|
##
|
291
355
|
|
292
356
|
|
357
|
+
def str_mapping_to_field_mapping(
|
358
|
+
cls: type[TDataclass],
|
359
|
+
mapping: Mapping[str, _T],
|
360
|
+
/,
|
361
|
+
*,
|
362
|
+
fields: Iterable[_YieldFieldsClass[Any]] | None = None,
|
363
|
+
globalns: StrMapping | None = None,
|
364
|
+
localns: StrMapping | None = None,
|
365
|
+
head: bool = False,
|
366
|
+
case_sensitive: bool = False,
|
367
|
+
allow_extra: bool = False,
|
368
|
+
) -> Mapping[_YieldFieldsClass[Any], _T]:
|
369
|
+
"""Convert a string-mapping into a field-mapping."""
|
370
|
+
keys_to_fields: Mapping[str, _YieldFieldsClass[Any]] = {}
|
371
|
+
for key in mapping:
|
372
|
+
try:
|
373
|
+
keys_to_fields[key] = one_field(
|
374
|
+
cls,
|
375
|
+
key,
|
376
|
+
fields=fields,
|
377
|
+
globalns=globalns,
|
378
|
+
localns=localns,
|
379
|
+
head=head,
|
380
|
+
case_sensitive=case_sensitive,
|
381
|
+
)
|
382
|
+
except OneFieldEmptyError:
|
383
|
+
if not allow_extra:
|
384
|
+
raise StrMappingToFieldMappingError(
|
385
|
+
cls=cls, key=key, head=head, case_sensitive=case_sensitive
|
386
|
+
) from None
|
387
|
+
return {field: mapping[key] for key, field in keys_to_fields.items()}
|
388
|
+
|
389
|
+
|
390
|
+
@dataclass(kw_only=True, slots=True)
|
391
|
+
class StrMappingToFieldMappingError(Exception):
|
392
|
+
cls: type[Dataclass]
|
393
|
+
key: str
|
394
|
+
head: bool = False
|
395
|
+
case_sensitive: bool = False
|
396
|
+
|
397
|
+
@override
|
398
|
+
def __str__(self) -> str:
|
399
|
+
head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
|
400
|
+
match self.head, self.case_sensitive:
|
401
|
+
case False, True:
|
402
|
+
tail = f"a field {self.key!r}"
|
403
|
+
case False, False:
|
404
|
+
tail = f"a field {self.key!r} (modulo case)"
|
405
|
+
case True, True:
|
406
|
+
tail = f"any field starting with {self.key!r}"
|
407
|
+
case True, False:
|
408
|
+
tail = f"any field starting with {self.key!r} (modulo case)"
|
409
|
+
case _ as never:
|
410
|
+
assert_never(never)
|
411
|
+
return f"{head} {tail}"
|
412
|
+
|
413
|
+
|
414
|
+
##
|
415
|
+
|
416
|
+
|
293
417
|
def text_to_dataclass(
|
294
418
|
text_or_mapping: str | Mapping[str, str],
|
295
419
|
cls: type[TDataclass],
|
@@ -297,29 +421,42 @@ def text_to_dataclass(
|
|
297
421
|
*,
|
298
422
|
globalns: StrMapping | None = None,
|
299
423
|
localns: StrMapping | None = None,
|
424
|
+
head: bool = False,
|
300
425
|
case_sensitive: bool = False,
|
426
|
+
allow_extra: bool = False,
|
301
427
|
) -> TDataclass:
|
302
428
|
"""Construct a dataclass from a string or a mapping or strings."""
|
303
|
-
fields = list(yield_fields(cls, globalns=globalns, localns=localns))
|
304
429
|
match text_or_mapping:
|
305
430
|
case str() as text:
|
306
|
-
|
307
|
-
case Mapping() as
|
431
|
+
keys_to_serializes = _text_to_dataclass_split_text(text, cls)
|
432
|
+
case Mapping() as keys_to_serializes:
|
308
433
|
...
|
309
434
|
case _ as never:
|
310
435
|
assert_never(never)
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
436
|
+
fields = list(yield_fields(cls, globalns=globalns, localns=localns))
|
437
|
+
fields_to_serializes = str_mapping_to_field_mapping(
|
438
|
+
cls,
|
439
|
+
keys_to_serializes,
|
440
|
+
fields=fields,
|
441
|
+
globalns=globalns,
|
442
|
+
localns=localns,
|
443
|
+
head=head,
|
444
|
+
case_sensitive=case_sensitive,
|
445
|
+
allow_extra=allow_extra,
|
316
446
|
)
|
447
|
+
field_names_to_values = {
|
448
|
+
f.name: _text_to_dataclass_parse(f, t, cls, case_sensitive=case_sensitive)
|
449
|
+
for f, t in fields_to_serializes.items()
|
450
|
+
}
|
317
451
|
return mapping_to_dataclass(
|
318
452
|
cls,
|
319
|
-
|
453
|
+
field_names_to_values,
|
454
|
+
fields=fields,
|
320
455
|
globalns=globalns,
|
321
456
|
localns=localns,
|
457
|
+
head=head,
|
322
458
|
case_sensitive=case_sensitive,
|
459
|
+
allow_extra=allow_extra,
|
323
460
|
)
|
324
461
|
|
325
462
|
|
@@ -340,38 +477,18 @@ def _text_to_dataclass_split_key_value_pair(
|
|
340
477
|
return key, value
|
341
478
|
|
342
479
|
|
343
|
-
def
|
344
|
-
|
345
|
-
|
346
|
-
value: str,
|
480
|
+
def _text_to_dataclass_parse(
|
481
|
+
field: _YieldFieldsClass[Any],
|
482
|
+
text: str,
|
347
483
|
cls: type[Dataclass],
|
348
484
|
/,
|
349
485
|
*,
|
350
486
|
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]
|
487
|
+
) -> Any:
|
368
488
|
try:
|
369
|
-
|
489
|
+
return parse_text(field.type_, text, case_sensitive=case_sensitive)
|
370
490
|
except ParseTextError:
|
371
|
-
raise _TextToDataClassParseValueError(
|
372
|
-
cls=cls, field=field, text=value
|
373
|
-
) from None
|
374
|
-
return key, parsed
|
491
|
+
raise _TextToDataClassParseValueError(cls=cls, field=field, text=text) from None
|
375
492
|
|
376
493
|
|
377
494
|
@dataclass(kw_only=True, slots=True)
|
@@ -388,31 +505,6 @@ class _TextToDataClassSplitKeyValuePairError(TextToDataClassError):
|
|
388
505
|
return f"Unable to construct {get_class_name(self.cls)!r}; failed to split key-value pair {self.text!r}"
|
389
506
|
|
390
507
|
|
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
508
|
@dataclass(kw_only=True, slots=True)
|
417
509
|
class _TextToDataClassParseValueError(TextToDataClassError[TDataclass]):
|
418
510
|
field: _YieldFieldsClass[Any]
|
@@ -431,16 +523,16 @@ def yield_fields(
|
|
431
523
|
obj: Dataclass,
|
432
524
|
/,
|
433
525
|
*,
|
434
|
-
globalns: StrMapping | None =
|
435
|
-
localns: StrMapping | None =
|
526
|
+
globalns: StrMapping | None = None,
|
527
|
+
localns: StrMapping | None = None,
|
436
528
|
) -> Iterator[_YieldFieldsInstance[Any]]: ...
|
437
529
|
@overload
|
438
530
|
def yield_fields(
|
439
531
|
obj: type[Dataclass],
|
440
532
|
/,
|
441
533
|
*,
|
442
|
-
globalns: StrMapping | None =
|
443
|
-
localns: StrMapping | None =
|
534
|
+
globalns: StrMapping | None = None,
|
535
|
+
localns: StrMapping | None = None,
|
444
536
|
) -> Iterator[_YieldFieldsClass[Any]]: ...
|
445
537
|
def yield_fields(
|
446
538
|
obj: Dataclass | type[Dataclass],
|
@@ -580,12 +672,18 @@ class YieldFieldsError(Exception):
|
|
580
672
|
|
581
673
|
__all__ = [
|
582
674
|
"MappingToDataclassError",
|
675
|
+
"OneFieldEmptyError",
|
676
|
+
"OneFieldError",
|
677
|
+
"OneFieldNonUniqueError",
|
678
|
+
"StrMappingToFieldMappingError",
|
583
679
|
"TextToDataClassError",
|
584
680
|
"YieldFieldsError",
|
585
681
|
"dataclass_repr",
|
586
682
|
"dataclass_to_dict",
|
587
683
|
"mapping_to_dataclass",
|
684
|
+
"one_field",
|
588
685
|
"replace_non_sentinel",
|
686
|
+
"str_mapping_to_field_mapping",
|
589
687
|
"text_to_dataclass",
|
590
688
|
"yield_fields",
|
591
689
|
]
|
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,8 @@ def load_settings(
|
|
32
27
|
cwd: PathLike = PWD,
|
33
28
|
globalns: StrMapping | None = None,
|
34
29
|
localns: StrMapping | None = None,
|
30
|
+
head: bool = False,
|
31
|
+
case_sensitive: bool = False,
|
35
32
|
) -> TDataclass:
|
36
33
|
"""Load a set of settings from the `.env` file."""
|
37
34
|
path = get_repo_root(cwd=cwd).joinpath(".env")
|
@@ -39,35 +36,29 @@ def load_settings(
|
|
39
36
|
raise _LoadSettingsFileNotFoundError(path=path) from None
|
40
37
|
maybe_values_dotenv = dotenv_values(path)
|
41
38
|
try:
|
42
|
-
maybe_values = merge_str_mappings(
|
39
|
+
maybe_values: Mapping[str, str | None] = merge_str_mappings(
|
40
|
+
maybe_values_dotenv, environ, case_sensitive=case_sensitive
|
41
|
+
)
|
43
42
|
except MergeStrMappingsError as error:
|
44
43
|
raise _LoadSettingsDuplicateKeysError(
|
45
|
-
path=path,
|
44
|
+
path=path,
|
45
|
+
values=error.mapping,
|
46
|
+
counts=error.counts,
|
47
|
+
case_sensitive=case_sensitive,
|
46
48
|
) from None
|
47
49
|
values = {k: v for k, v in maybe_values.items() if v is not None}
|
48
50
|
try:
|
49
|
-
return
|
50
|
-
cls,
|
51
|
+
return text_to_dataclass(
|
51
52
|
values,
|
53
|
+
cls,
|
52
54
|
globalns=globalns,
|
53
55
|
localns=localns,
|
54
|
-
|
56
|
+
head=head,
|
57
|
+
case_sensitive=case_sensitive,
|
58
|
+
allow_extra=True,
|
55
59
|
)
|
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
|
60
|
+
except MappingToDataclassError as error:
|
61
|
+
raise _LoadSettingsMissingKeysError(path=path, fields=error.fields) from None
|
71
62
|
|
72
63
|
|
73
64
|
@dataclass(kw_only=True, slots=True)
|
@@ -79,22 +70,13 @@ class LoadSettingsError(Exception):
|
|
79
70
|
class _LoadSettingsDuplicateKeysError(LoadSettingsError):
|
80
71
|
values: StrMapping
|
81
72
|
counts: Mapping[str, int]
|
73
|
+
case_sensitive: bool = False
|
82
74
|
|
83
75
|
@override
|
84
76
|
def __str__(self) -> str:
|
85
77
|
return f"Mapping {get_repr(dict(self.values))} keys must not contain duplicates (modulo case); got {get_repr(self.counts)}"
|
86
78
|
|
87
79
|
|
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
80
|
@dataclass(kw_only=True, slots=True)
|
99
81
|
class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
100
82
|
@override
|
@@ -103,14 +85,13 @@ class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
|
103
85
|
|
104
86
|
|
105
87
|
@dataclass(kw_only=True, slots=True)
|
106
|
-
class
|
107
|
-
|
108
|
-
field: _YieldFieldsClass[Any]
|
109
|
-
text: str
|
88
|
+
class _LoadSettingsMissingKeysError(LoadSettingsError):
|
89
|
+
fields: AbstractSet[str]
|
110
90
|
|
111
91
|
@override
|
112
92
|
def __str__(self) -> str:
|
113
|
-
|
93
|
+
desc = ", ".join(map(repr, sorted(self.fields)))
|
94
|
+
return f"Unable to load {str(self.path)!r}; missing value(s) for {desc}"
|
114
95
|
|
115
96
|
|
116
97
|
__all__ = ["LoadSettingsError", "load_settings"]
|
File without changes
|
File without changes
|