dycw-utilities 0.108.2__py3-none-any.whl → 0.108.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.108.2
3
+ Version: 0.108.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -82,7 +82,7 @@ Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
82
82
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
83
83
  Requires-Dist: hypothesis<6.132,>=6.131.6; extra == 'zzz-test-hypothesis'
84
84
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
85
- Requires-Dist: numpy<2.3,>=2.2.4; extra == 'zzz-test-hypothesis'
85
+ Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-hypothesis'
86
86
  Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
87
87
  Requires-Dist: redis<5.3,>=5.2.1; extra == 'zzz-test-hypothesis'
88
88
  Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-hypothesis'
@@ -112,14 +112,14 @@ Provides-Extra: zzz-test-luigi
112
112
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-luigi'
113
113
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-luigi'
114
114
  Provides-Extra: zzz-test-math
115
- Requires-Dist: numpy<2.3,>=2.2.4; extra == 'zzz-test-math'
115
+ Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-math'
116
116
  Provides-Extra: zzz-test-memory-profiler
117
117
  Requires-Dist: memory-profiler<0.62,>=0.61.0; extra == 'zzz-test-memory-profiler'
118
118
  Provides-Extra: zzz-test-modules
119
119
  Provides-Extra: zzz-test-more-itertools
120
120
  Requires-Dist: more-itertools<10.7,>=10.6.0; extra == 'zzz-test-more-itertools'
121
121
  Provides-Extra: zzz-test-numpy
122
- Requires-Dist: numpy<2.3,>=2.2.4; extra == 'zzz-test-numpy'
122
+ Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-numpy'
123
123
  Provides-Extra: zzz-test-operator
124
124
  Requires-Dist: polars-lts-cpu<1.28,>=1.27.1; extra == 'zzz-test-operator'
125
125
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-operator'
@@ -177,7 +177,7 @@ Requires-Dist: scipy<1.16,>=1.15.2; extra == 'zzz-test-scipy'
177
177
  Provides-Extra: zzz-test-sentinel
178
178
  Provides-Extra: zzz-test-shelve
179
179
  Provides-Extra: zzz-test-slack-sdk
180
- Requires-Dist: aiohttp<3.12,>=3.11.16; extra == 'zzz-test-slack-sdk'
180
+ Requires-Dist: aiohttp<3.12,>=3.11.17; extra == 'zzz-test-slack-sdk'
181
181
  Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
182
182
  Provides-Extra: zzz-test-socket
183
183
  Provides-Extra: zzz-test-sqlalchemy
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=cA1_Gx_WFoPXFOqGWzIcljSCQWUSLDE4oIIYB9493-g,60
1
+ utilities/__init__.py,sha256=-2lmFv1UZnPQpN8IvJ-ViPH2DA03IC4wyaSgQYQI0zc,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,14 +11,14 @@ 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=SjDgGG6n9m5dn10KKTyC4In7mJ5pjoOqTKJ_xJi75Ss,14031
14
+ utilities/dataclasses.py,sha256=SOWaLVMK6p_HTMPhiXtgCk5F1KGiY8wNxQkYRBz1dVg,18683
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
18
18
  utilities/eventkit.py,sha256=bG2rjgQqPaaOEW879Pc8vOCX6zRAl1frIhB1Y6fqQXg,13149
19
19
  utilities/fastapi.py,sha256=uwqOGbGwzIbP-lfm-ApG1ZEN3BA_TDsaiuTghhLmxb8,2413
20
20
  utilities/fpdf2.py,sha256=zM3gwOYcAfv7P4qhbyvzPmRY4PPAiAQ-ZnPC6I9SZ1M,1832
21
- utilities/functions.py,sha256=1yeJqFc6OO0W8N5x6Wc0BaEyz094Ny_oYY3qYILOho4,26706
21
+ utilities/functions.py,sha256=BH4F_X34tqHuk-BzG9lzooYIP1OmVKm6GQw51qqYShM,27461
22
22
  utilities/functools.py,sha256=WrpHt7NLNWSUn9A1Q_ZIWlNaYZOEI4IFKyBG9HO3BC4,1643
23
23
  utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
24
24
  utilities/git.py,sha256=wpt5dZ5Oi5931pN24_VLZYaQOvmR0OcQuVtgHzFUN1k,2359
@@ -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=vcjTodgb3IAQEtUgPODsxGAYwPaXHPuV3G_iDcXm0zY,7886
56
+ utilities/python_dotenv.py,sha256=7N4ZbBxXpPNttOTfg-hpaLFFWA5iJwF7tREBzUnbPOM,3415
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.108.2.dist-info/METADATA,sha256=N4v4D-Slw7bPRUT5KdnuEIJnUoDQVx3pTRFXkQ8kNp4,13004
88
- dycw_utilities-0.108.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
- dycw_utilities-0.108.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
- dycw_utilities-0.108.2.dist-info/RECORD,,
87
+ dycw_utilities-0.108.4.dist-info/METADATA,sha256=rAF0wyx5NT-CYkx5Meatz3jzjaRSMeBHx6mSCdfBcM8,13004
88
+ dycw_utilities-0.108.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
+ dycw_utilities-0.108.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
+ dycw_utilities-0.108.4.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.108.2"
3
+ __version__ = "0.108.4"
utilities/dataclasses.py CHANGED
@@ -1,7 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Mapping
3
4
  from dataclasses import MISSING, dataclass, field, fields, replace
4
- from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload, override
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Generic,
9
+ Literal,
10
+ TypeVar,
11
+ assert_never,
12
+ overload,
13
+ override,
14
+ )
5
15
 
6
16
  from utilities.errors import ImpossibleCaseError
7
17
  from utilities.functions import (
@@ -11,14 +21,16 @@ from utilities.functions import (
11
21
  )
12
22
  from utilities.iterables import OneStrEmptyError, OneStrNonUniqueError, one_str
13
23
  from utilities.operator import is_equal
24
+ from utilities.parse import ParseTextError, parse_text
14
25
  from utilities.reprlib import get_repr
15
26
  from utilities.sentinel import Sentinel, sentinel
27
+ from utilities.types import TDataclass
16
28
  from utilities.typing import get_type_hints
17
29
 
18
30
  if TYPE_CHECKING:
19
- from collections.abc import Callable, Iterable, Iterator, Mapping
31
+ from collections.abc import Callable, Iterable, Iterator
20
32
 
21
- from utilities.types import Dataclass, StrMapping, TDataclass
33
+ from utilities.types import Dataclass, StrMapping
22
34
 
23
35
 
24
36
  _T = TypeVar("_T")
@@ -118,7 +130,7 @@ def dataclass_to_dict(
118
130
  recursive: bool = False,
119
131
  ) -> StrMapping:
120
132
  """Convert a dataclass to a dictionary."""
121
- out: dict[str, Any] = {}
133
+ out: StrMapping = {}
122
134
  for fld in yield_fields(obj, globalns=globalns, localns=localns):
123
135
  if fld.keep(
124
136
  include=include,
@@ -233,9 +245,7 @@ class _MappingToDataclassEmptyError(MappingToDataclassError):
233
245
  @override
234
246
  def __str__(self) -> str:
235
247
  desc = f"Mapping {get_repr(self.mapping)} does not contain {self.field!r}"
236
- if not self.case_sensitive:
237
- desc += " (modulo case)"
238
- return desc
248
+ return desc if self.case_sensitive else f"{desc} (modulo case)"
239
249
 
240
250
 
241
251
  @dataclass(kw_only=True, slots=True)
@@ -280,6 +290,142 @@ def replace_non_sentinel(
280
290
  ##
281
291
 
282
292
 
293
+ def text_to_dataclass(
294
+ text_or_mapping: str | Mapping[str, str],
295
+ cls: type[TDataclass],
296
+ /,
297
+ *,
298
+ globalns: StrMapping | None = None,
299
+ localns: StrMapping | None = None,
300
+ case_sensitive: bool = False,
301
+ ) -> TDataclass:
302
+ """Construct a dataclass from a string or a mapping or strings."""
303
+ fields = list(yield_fields(cls, globalns=globalns, localns=localns))
304
+ match text_or_mapping:
305
+ case str() as text:
306
+ text_mapping = _text_to_dataclass_split_text(text, cls)
307
+ case Mapping() as text_mapping:
308
+ ...
309
+ case _ as never:
310
+ assert_never(never)
311
+ value_mapping = dict(
312
+ _text_to_dataclass_get_and_parse(
313
+ fields, key, value, cls, case_sensitive=case_sensitive
314
+ )
315
+ for key, value in text_mapping.items()
316
+ )
317
+ return mapping_to_dataclass(
318
+ cls,
319
+ value_mapping,
320
+ globalns=globalns,
321
+ localns=localns,
322
+ case_sensitive=case_sensitive,
323
+ )
324
+
325
+
326
+ def _text_to_dataclass_split_text(
327
+ text: str, cls: type[TDataclass], /
328
+ ) -> Mapping[str, str]:
329
+ pairs = (t for t in text.split(",") if t != "")
330
+ return dict(_text_to_dataclass_split_key_value_pair(pair, cls) for pair in pairs)
331
+
332
+
333
+ def _text_to_dataclass_split_key_value_pair(
334
+ text: str, cls: type[Dataclass], /
335
+ ) -> tuple[str, str]:
336
+ try:
337
+ key, value = text.split("=")
338
+ except ValueError:
339
+ raise _TextToDataClassSplitKeyValuePairError(cls=cls, text=text) from None
340
+ return key, value
341
+
342
+
343
+ def _text_to_dataclass_get_and_parse(
344
+ fields: Iterable[_YieldFieldsClass[Any]],
345
+ key: str,
346
+ value: str,
347
+ cls: type[Dataclass],
348
+ /,
349
+ *,
350
+ case_sensitive: bool = False,
351
+ ) -> tuple[str, Any]:
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]
368
+ try:
369
+ parsed = parse_text(field.type_, value, case_sensitive=case_sensitive)
370
+ except ParseTextError:
371
+ raise _TextToDataClassParseValueError(
372
+ cls=cls, field=field, text=value
373
+ ) from None
374
+ return key, parsed
375
+
376
+
377
+ @dataclass(kw_only=True, slots=True)
378
+ class TextToDataClassError(Exception, Generic[TDataclass]):
379
+ cls: type[TDataclass]
380
+
381
+
382
+ @dataclass(kw_only=True, slots=True)
383
+ class _TextToDataClassSplitKeyValuePairError(TextToDataClassError):
384
+ text: str
385
+
386
+ @override
387
+ def __str__(self) -> str:
388
+ return f"Unable to construct {get_class_name(self.cls)!r}; failed to split key-value pair {self.text!r}"
389
+
390
+
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
+ @dataclass(kw_only=True, slots=True)
417
+ class _TextToDataClassParseValueError(TextToDataClassError[TDataclass]):
418
+ field: _YieldFieldsClass[Any]
419
+ text: str
420
+
421
+ @override
422
+ def __str__(self) -> str:
423
+ return f"Unable to construct {get_class_name(self.cls)!r}; unable to parse field {self.field.name!r} of type {self.field.type_!r}; got {self.text!r}"
424
+
425
+
426
+ ##
427
+
428
+
283
429
  @overload
284
430
  def yield_fields(
285
431
  obj: Dataclass,
@@ -346,18 +492,18 @@ def yield_fields(
346
492
  raise YieldFieldsError(obj=obj)
347
493
 
348
494
 
349
- @dataclass(kw_only=True, slots=True)
495
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
350
496
  class _YieldFieldsInstance(Generic[_T]):
351
497
  name: str
352
- value: _T
353
- type_: Any
354
- default: _T | Sentinel = sentinel
355
- default_factory: Callable[[], _T] | Sentinel = sentinel
498
+ value: _T = field(hash=False)
499
+ type_: Any = field(hash=False)
500
+ default: _T | Sentinel = field(default=sentinel, hash=False)
501
+ default_factory: Callable[[], _T] | Sentinel = field(default=sentinel, hash=False)
356
502
  repr: bool = True
357
503
  hash_: bool | None = None
358
504
  init: bool = True
359
505
  compare: bool = True
360
- metadata: StrMapping = field(default_factory=dict)
506
+ metadata: StrMapping = field(default_factory=dict, hash=False)
361
507
  kw_only: bool | Sentinel = sentinel
362
508
 
363
509
  def equals_default(
@@ -407,17 +553,17 @@ class _YieldFieldsInstance(Generic[_T]):
407
553
  return (defaults and equal) or not equal
408
554
 
409
555
 
410
- @dataclass(kw_only=True, slots=True)
556
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
411
557
  class _YieldFieldsClass(Generic[_T]):
412
558
  name: str
413
- type_: Any
414
- default: _T | Sentinel = sentinel
415
- default_factory: Callable[[], _T] | Sentinel = sentinel
559
+ type_: Any = field(hash=False)
560
+ default: _T | Sentinel = field(default=sentinel, hash=False)
561
+ default_factory: Callable[[], _T] | Sentinel = field(default=sentinel, hash=False)
416
562
  repr: bool = True
417
563
  hash_: bool | None = None
418
564
  init: bool = True
419
565
  compare: bool = True
420
- metadata: StrMapping = field(default_factory=dict)
566
+ metadata: StrMapping = field(default_factory=dict, hash=False)
421
567
  kw_only: bool | Sentinel = sentinel
422
568
 
423
569
 
@@ -434,10 +580,12 @@ class YieldFieldsError(Exception):
434
580
 
435
581
  __all__ = [
436
582
  "MappingToDataclassError",
583
+ "TextToDataClassError",
437
584
  "YieldFieldsError",
438
585
  "dataclass_repr",
439
586
  "dataclass_to_dict",
440
587
  "mapping_to_dataclass",
441
588
  "replace_non_sentinel",
589
+ "text_to_dataclass",
442
590
  "yield_fields",
443
591
  ]
utilities/functions.py CHANGED
@@ -5,6 +5,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence
5
5
  from dataclasses import asdict, dataclass, is_dataclass
6
6
  from functools import _lru_cache_wrapper, cached_property, partial, reduce, wraps
7
7
  from inspect import getattr_static
8
+ from pathlib import Path
8
9
  from re import findall
9
10
  from types import (
10
11
  BuiltinFunctionType,
@@ -405,6 +406,31 @@ class EnsureNumberError(Exception):
405
406
  ##
406
407
 
407
408
 
409
+ @overload
410
+ def ensure_path(obj: Any, /, *, nullable: bool) -> Path | None: ...
411
+ @overload
412
+ def ensure_path(obj: Any, /, *, nullable: Literal[False] = False) -> Path: ...
413
+ def ensure_path(obj: Any, /, *, nullable: bool = False) -> Path | None:
414
+ """Ensure an object is a Path."""
415
+ try:
416
+ return ensure_class(obj, Path, nullable=nullable)
417
+ except EnsureClassError as error:
418
+ raise EnsurePathError(obj=error.obj, nullable=nullable) from None
419
+
420
+
421
+ @dataclass(kw_only=True, slots=True)
422
+ class EnsurePathError(Exception):
423
+ obj: Any
424
+ nullable: bool
425
+
426
+ @override
427
+ def __str__(self) -> str:
428
+ return _make_error_msg(self.obj, "a Path", nullable=self.nullable)
429
+
430
+
431
+ ##
432
+
433
+
408
434
  def ensure_sized(obj: Any, /) -> Sized:
409
435
  """Ensure an object is sized."""
410
436
  if is_sized(obj):
@@ -985,6 +1011,7 @@ __all__ = [
985
1011
  "EnsureMemberError",
986
1012
  "EnsureNotNoneError",
987
1013
  "EnsureNumberError",
1014
+ "EnsurePathError",
988
1015
  "EnsureSizedError",
989
1016
  "EnsureSizedNotStrError",
990
1017
  "EnsureStrError",
@@ -1004,6 +1031,7 @@ __all__ = [
1004
1031
  "ensure_member",
1005
1032
  "ensure_not_none",
1006
1033
  "ensure_number",
1034
+ "ensure_path",
1007
1035
  "ensure_sized",
1008
1036
  "ensure_sized_not_str",
1009
1037
  "ensure_str",
@@ -1,12 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
3
  from dataclasses import dataclass
5
- from enum import Enum
6
4
  from functools import partial
7
5
  from os import environ
8
- from pathlib import Path
9
- from re import IGNORECASE, search
10
6
  from typing import TYPE_CHECKING, Any, override
11
7
 
12
8
  from dotenv import dotenv_values
@@ -16,16 +12,15 @@ from utilities.dataclasses import (
16
12
  _YieldFieldsClass,
17
13
  mapping_to_dataclass,
18
14
  )
19
- from utilities.enum import EnsureEnumError, ensure_enum
20
- from utilities.functions import get_class_name
21
15
  from utilities.git import get_repo_root
22
- from utilities.iterables import MergeStrMappingsError, merge_str_mappings, one, one_str
16
+ from utilities.iterables import MergeStrMappingsError, merge_str_mappings
17
+ from utilities.parse import ParseTextError, parse_text
23
18
  from utilities.pathlib import PWD
24
19
  from utilities.reprlib import get_repr
25
- from utilities.typing import get_args, is_literal_type, is_optional_type
26
20
 
27
21
  if TYPE_CHECKING:
28
22
  from collections.abc import Mapping
23
+ from pathlib import Path
29
24
 
30
25
  from utilities.types import PathLike, StrMapping, TDataclass
31
26
 
@@ -65,72 +60,14 @@ def load_settings(
65
60
 
66
61
 
67
62
  def _load_settings_post(
68
- field: _YieldFieldsClass[Any], value: Any, /, *, path: Path, values: StrMapping
63
+ field: _YieldFieldsClass[Any], text: str, /, *, path: Path, values: StrMapping
69
64
  ) -> Any:
70
- type_ = field.type_
71
- if type_ is str:
72
- return value
73
- if type_ is bool:
74
- if value == "0" or search("false", value, flags=IGNORECASE):
75
- return False
76
- if value == "1" or search("true", value, flags=IGNORECASE):
77
- return True
78
- raise _LoadSettingsInvalidBoolError(
79
- path=path, values=values, field=field.name, value=value
80
- )
81
- if type_ is float:
82
- try:
83
- return float(value)
84
- except ValueError:
85
- raise _LoadSettingsInvalidFloatError(
86
- path=path, values=values, field=field.name, value=value
87
- ) from None
88
- if type_ is int:
89
- try:
90
- return int(value)
91
- except ValueError:
92
- raise _LoadSettingsInvalidIntError(
93
- path=path, values=values, field=field.name, value=value
94
- ) from None
95
- if type_ is Path:
96
- return Path(value).expanduser()
97
- if type_ is dt.date:
98
- from utilities.whenever import ParseDateError, parse_date
99
-
100
- try:
101
- return parse_date(value)
102
- except ParseDateError:
103
- raise _LoadSettingsInvalidDateError(
104
- path=path, values=values, field=field.name, value=value
105
- ) from None
106
- if type_ is dt.timedelta:
107
- from utilities.whenever import ParseTimedeltaError, parse_timedelta
108
-
109
- try:
110
- return parse_timedelta(value)
111
- except ParseTimedeltaError:
112
- raise _LoadSettingsInvalidTimeDeltaError(
113
- path=path, values=values, field=field.name, value=value
114
- ) from None
115
- if isinstance(type_, type) and issubclass(type_, Enum):
116
- try:
117
- return ensure_enum(value, type_)
118
- except EnsureEnumError:
119
- raise _LoadSettingsInvalidEnumError(
120
- path=path, values=values, field=field.name, type_=type_, value=value
121
- ) from None
122
- if is_literal_type(type_):
123
- return one_str(get_args(type_), value)
124
- if is_optional_type(type_) and (one(get_args(type_)) is int):
125
- if (value is None) or (value == "") or search("none", value, flags=IGNORECASE):
126
- return None
127
- try:
128
- return int(value)
129
- except ValueError:
130
- raise _LoadSettingsInvalidNullableIntError(
131
- path=path, values=values, field=field.name, value=value
132
- ) from None
133
- raise _LoadSettingsTypeError(path=path, field=field.name, type=type_)
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
134
71
 
135
72
 
136
73
  @dataclass(kw_only=True, slots=True)
@@ -138,13 +75,6 @@ class LoadSettingsError(Exception):
138
75
  path: Path
139
76
 
140
77
 
141
- @dataclass(kw_only=True, slots=True)
142
- class _LoadSettingsFileNotFoundError(LoadSettingsError):
143
- @override
144
- def __str__(self) -> str:
145
- return f"Path {str(self.path)!r} must exist"
146
-
147
-
148
78
  @dataclass(kw_only=True, slots=True)
149
79
  class _LoadSettingsDuplicateKeysError(LoadSettingsError):
150
80
  values: StrMapping
@@ -166,94 +96,21 @@ class _LoadSettingsEmptyError(LoadSettingsError):
166
96
 
167
97
 
168
98
  @dataclass(kw_only=True, slots=True)
169
- class _LoadSettingsInvalidBoolError(LoadSettingsError):
170
- values: StrMapping
171
- field: str
172
- value: str
173
-
174
- @override
175
- def __str__(self) -> str:
176
- return f"Field {self.field!r} must contain a valid boolean; got {self.value!r}"
177
-
178
-
179
- @dataclass(kw_only=True, slots=True)
180
- class _LoadSettingsInvalidDateError(LoadSettingsError):
181
- values: StrMapping
182
- field: str
183
- value: str
184
-
185
- @override
186
- def __str__(self) -> str:
187
- return f"Field {self.field!r} must contain a valid date; got {self.value!r}"
188
-
189
-
190
- @dataclass(kw_only=True, slots=True)
191
- class _LoadSettingsInvalidEnumError(LoadSettingsError):
192
- values: StrMapping
193
- field: str
194
- type_: type[Enum]
195
- value: str
196
-
197
- @override
198
- def __str__(self) -> str:
199
- type_ = get_class_name(self.type_)
200
- return f"Field {self.field!r} must contain a valid member of {type_!r}; got {self.value!r}"
201
-
202
-
203
- @dataclass(kw_only=True, slots=True)
204
- class _LoadSettingsInvalidFloatError(LoadSettingsError):
205
- values: StrMapping
206
- field: str
207
- value: str
208
-
209
- @override
210
- def __str__(self) -> str:
211
- return f"Field {self.field!r} must contain a valid float; got {self.value!r}"
212
-
213
-
214
- @dataclass(kw_only=True, slots=True)
215
- class _LoadSettingsInvalidIntError(LoadSettingsError):
216
- values: StrMapping
217
- field: str
218
- value: str
219
-
220
- @override
221
- def __str__(self) -> str:
222
- return f"Field {self.field!r} must contain a valid integer; got {self.value!r}"
223
-
224
-
225
- @dataclass(kw_only=True, slots=True)
226
- class _LoadSettingsInvalidNullableIntError(LoadSettingsError):
227
- values: StrMapping
228
- field: str
229
- value: str
230
-
99
+ class _LoadSettingsFileNotFoundError(LoadSettingsError):
231
100
  @override
232
101
  def __str__(self) -> str:
233
- return f"Field {self.field!r} must contain a valid nullable integer; got {self.value!r}"
102
+ return f"Path {str(self.path)!r} must exist"
234
103
 
235
104
 
236
105
  @dataclass(kw_only=True, slots=True)
237
- class _LoadSettingsInvalidTimeDeltaError(LoadSettingsError):
106
+ class _LoadSettingsParseTextError(LoadSettingsError):
238
107
  values: StrMapping
239
- field: str
240
- value: str
241
-
242
- @override
243
- def __str__(self) -> str:
244
- return (
245
- f"Field {self.field!r} must contain a valid timedelta; got {self.value!r}"
246
- )
247
-
248
-
249
- @dataclass(kw_only=True, slots=True)
250
- class _LoadSettingsTypeError(LoadSettingsError):
251
- field: str
252
- type: Any
108
+ field: _YieldFieldsClass[Any]
109
+ text: str
253
110
 
254
111
  @override
255
112
  def __str__(self) -> str:
256
- return f"Field {self.field!r} has unsupported type {self.type!r}"
113
+ return f"Unable to parse field {self.field.name!r} of type {self.field.type_!r}; got {self.text!r}"
257
114
 
258
115
 
259
116
  __all__ = ["LoadSettingsError", "load_settings"]