dycw-utilities 0.108.1__py3-none-any.whl → 0.108.3__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.1
3
+ Version: 0.108.3
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=IeBN4DoVYNosR_DmJpLKbjCsXl79LuQiIIKs3QarfVo,60
1
+ utilities/__init__.py,sha256=932kugeJpmqtWXsJGSf5NBQ-6ig0_Xf-TGDJSK9b28w,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
@@ -18,7 +18,7 @@ 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
@@ -40,6 +40,7 @@ utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
40
40
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
41
41
  utilities/orjson.py,sha256=DW5pOpMyrR5Q8caQYly9AqRPazDBqrWv5GRWfULqka4,36291
42
42
  utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
43
+ utilities/parse.py,sha256=vKVWIqR5JykQzPSnMHQr7_h43M6TwfYEnPmjmbgSA-o,4585
43
44
  utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
44
45
  utilities/period.py,sha256=ikHXsWtDLr553cfH6p9mMaiCnIAP69B7q84ckWV3HaA,10884
45
46
  utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
@@ -52,14 +53,14 @@ utilities/pyinstrument.py,sha256=ROq2txPwbe2ZUuYJ2IDNbfT97lu2ca0v5_C_yn6sSlM,800
52
53
  utilities/pyrsistent.py,sha256=TLJfiiKO4cKNU_pCoM3zDqmSM421qpuoaeaBNnyC_Ac,2489
53
54
  utilities/pytest.py,sha256=85QUax4g2VBBAqAHtM9wekcSLB7_9O8AKFTaCshztL8,7989
54
55
  utilities/pytest_regressions.py,sha256=Kp1NS_cyXvBFqyiF_oSzYmSJzIOdAZ0SFcSGmbL_UtI,5001
55
- utilities/python_dotenv.py,sha256=vcjTodgb3IAQEtUgPODsxGAYwPaXHPuV3G_iDcXm0zY,7886
56
+ utilities/python_dotenv.py,sha256=7N4ZbBxXpPNttOTfg-hpaLFFWA5iJwF7tREBzUnbPOM,3415
56
57
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
57
58
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
58
59
  utilities/redis.py,sha256=CsDQqc9V6ASLzLQwtbQXZQEndyG9pJiCOhPlPeszt7Y,21203
59
60
  utilities/reprlib.py,sha256=Re9bk3n-kC__9DxQmRlevqFA86pE6TtVfWjUgpbVOv0,1849
60
61
  utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
61
62
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
62
- utilities/sentinel.py,sha256=Y_UnVkIMlbN6pJquUz2ghY1rTmCo93k_os3PjyoqE9Q,684
63
+ utilities/sentinel.py,sha256=0X1GWWcnPxGCo7wDgVTOfhU5iKlXdvVZudlMM0iezDw,1246
63
64
  utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
64
65
  utilities/slack_sdk.py,sha256=SeDNMh24IPiEBWoGMdgvrflUaFa9TGlTS03H9-NKaQw,4132
65
66
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
@@ -69,7 +70,7 @@ utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
69
70
  utilities/sys.py,sha256=h0Xr7Vj86wNalvwJVP1wj5Y0kD_VWm1vzuXZ_jw94mE,2743
70
71
  utilities/tempfile.py,sha256=VqmZJAhTJ1OaVywFzk5eqROV8iJbW9XQ_QYAV0bpdRo,1384
71
72
  utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
72
- utilities/text.py,sha256=4DSi_m0E1Bxqp5LhMiPH_m38ag6wQAGrWP_iPkfBo9Q,2688
73
+ utilities/text.py,sha256=X_EjRQeV_PsG3oP7OiGYIyXGKWqciTnSwoKhM2tsy6M,3120
73
74
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
74
75
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
75
76
  utilities/traceback.py,sha256=KwHPLdEbdj0fFhXo8MBfxcvem8A-VXYDwFMNJ6f0cTM,27328
@@ -83,7 +84,7 @@ utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
83
84
  utilities/whenever.py,sha256=5x2t47VJmJRWcd_NLFy54NkB3uom-XQYxEbLtEfL1bs,17775
84
85
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
85
86
  utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
86
- dycw_utilities-0.108.1.dist-info/METADATA,sha256=VOAnUEUC7wcoSrSFoy7zijMLaJYHCt-euTVZdVZThEU,13004
87
- dycw_utilities-0.108.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
88
- dycw_utilities-0.108.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
89
- dycw_utilities-0.108.1.dist-info/RECORD,,
87
+ dycw_utilities-0.108.3.dist-info/METADATA,sha256=A5j6OCjBswwlY0SpdfckG4fb3_nZCjlgIDluLCVAdVY,13004
88
+ dycw_utilities-0.108.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
+ dycw_utilities-0.108.3.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
+ dycw_utilities-0.108.3.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.108.1"
3
+ __version__ = "0.108.3"
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",
utilities/parse.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ from contextlib import suppress
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from types import NoneType
9
+ from typing import Any, get_args, override
10
+
11
+ from utilities.datetime import is_subclass_date_not_datetime
12
+ from utilities.enum import ParseEnumError, parse_enum
13
+ from utilities.functions import is_subclass_int_not_bool
14
+ from utilities.iterables import one, one_str
15
+ from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
16
+ from utilities.text import ParseBoolError, ParseNoneError, parse_bool, parse_none
17
+ from utilities.typing import is_literal_type, is_optional_type
18
+ from utilities.version import ParseVersionError, Version, parse_version
19
+
20
+
21
+ def parse_text(
22
+ obj: Any, text: str, /, *, case_sensitive: bool = False, head: bool = False
23
+ ) -> Any:
24
+ """Parse text."""
25
+ if obj is None:
26
+ try:
27
+ return parse_none(text)
28
+ except ParseNoneError:
29
+ raise ParseTextError(obj=obj, text=text) from None
30
+ if isinstance(obj, type):
31
+ return _parse_text_type(obj, text, case_sensitive=case_sensitive)
32
+ if is_literal_type(obj):
33
+ return one_str(get_args(obj), text, head=head, case_sensitive=case_sensitive)
34
+ if is_optional_type(obj):
35
+ with suppress(ParseNoneError):
36
+ return parse_none(text)
37
+ inner = one(arg for arg in get_args(obj) if arg is not NoneType)
38
+ if isinstance(
39
+ inner := one(arg for arg in get_args(obj) if arg is not NoneType), type
40
+ ):
41
+ try:
42
+ return _parse_text_type(inner, text, case_sensitive=case_sensitive)
43
+ except ParseTextError:
44
+ raise ParseTextError(obj=obj, text=text) from None
45
+ raise ParseTextError(obj=obj, text=text) from None
46
+
47
+
48
+ def _parse_text_type(
49
+ cls: type[Any], text: str, /, *, case_sensitive: bool = False
50
+ ) -> Any:
51
+ """Parse text."""
52
+ if issubclass(cls, NoneType):
53
+ try:
54
+ return parse_none(text)
55
+ except ParseNoneError:
56
+ raise ParseTextError(obj=cls, text=text) from None
57
+ if issubclass(cls, str):
58
+ return text
59
+ if issubclass(cls, bool):
60
+ try:
61
+ return parse_bool(text)
62
+ except ParseBoolError:
63
+ raise ParseTextError(obj=cls, text=text) from None
64
+ if is_subclass_int_not_bool(cls):
65
+ try:
66
+ return int(text)
67
+ except ValueError:
68
+ raise ParseTextError(obj=cls, text=text) from None
69
+ if issubclass(cls, float):
70
+ try:
71
+ return float(text)
72
+ except ValueError:
73
+ raise ParseTextError(obj=cls, text=text) from None
74
+ if issubclass(cls, Enum):
75
+ try:
76
+ return parse_enum(text, cls, case_sensitive=case_sensitive)
77
+ except ParseEnumError:
78
+ raise ParseTextError(obj=cls, text=text) from None
79
+ if issubclass(cls, Path):
80
+ return Path(text).expanduser()
81
+ if issubclass(cls, Sentinel):
82
+ try:
83
+ return parse_sentinel(text)
84
+ except ParseSentinelError:
85
+ raise ParseTextError(obj=cls, text=text) from None
86
+ if issubclass(cls, Version):
87
+ try:
88
+ return parse_version(text)
89
+ except ParseVersionError:
90
+ raise ParseTextError(obj=cls, text=text) from None
91
+ if is_subclass_date_not_datetime(cls):
92
+ from utilities.whenever import ParseDateError, parse_date
93
+
94
+ try:
95
+ return parse_date(text)
96
+ except ParseDateError:
97
+ raise ParseTextError(obj=cls, text=text) from None
98
+ if issubclass(cls, dt.datetime):
99
+ from utilities.whenever import ParseDateTimeError, parse_datetime
100
+
101
+ try:
102
+ return parse_datetime(text)
103
+ except ParseDateTimeError:
104
+ raise ParseTextError(obj=cls, text=text) from None
105
+ if issubclass(cls, dt.time):
106
+ from utilities.whenever import ParseTimeError, parse_time
107
+
108
+ try:
109
+ return parse_time(text)
110
+ except ParseTimeError:
111
+ raise ParseTextError(obj=cls, text=text) from None
112
+ if issubclass(cls, dt.timedelta):
113
+ from utilities.whenever import ParseTimedeltaError, parse_timedelta
114
+
115
+ try:
116
+ return parse_timedelta(text)
117
+ except ParseTimedeltaError:
118
+ raise ParseTextError(obj=cls, text=text) from None
119
+ raise ParseTextError(obj=cls, text=text) from None
120
+
121
+
122
+ @dataclass
123
+ class ParseTextError(Exception):
124
+ obj: Any
125
+ text: str
126
+
127
+ @override
128
+ def __str__(self) -> str:
129
+ return f"Unable to parse {self.obj!r}; got {self.text!r}"
@@ -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"]
utilities/sentinel.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
4
+ from re import IGNORECASE, search
3
5
  from typing import Any, override
4
6
 
5
7
 
@@ -33,4 +35,29 @@ class Sentinel(metaclass=_Meta):
33
35
  sentinel = Sentinel()
34
36
 
35
37
 
36
- __all__ = ["SENTINEL_REPR", "Sentinel", "sentinel"]
38
+ ##
39
+
40
+
41
+ def parse_sentinel(text: str, /) -> Sentinel:
42
+ """Parse text into the Sentinel value."""
43
+ if text == "" or search("Sentinel", text, flags=IGNORECASE):
44
+ return sentinel
45
+ raise ParseSentinelError(text=text)
46
+
47
+
48
+ @dataclass(kw_only=True, slots=True)
49
+ class ParseSentinelError(Exception):
50
+ text: str
51
+
52
+ @override
53
+ def __str__(self) -> str:
54
+ return f"Unable to parse sentinel value; got {self.text!r}"
55
+
56
+
57
+ __all__ = [
58
+ "SENTINEL_REPR",
59
+ "ParseSentinelError",
60
+ "Sentinel",
61
+ "parse_sentinel",
62
+ "sentinel",
63
+ ]
utilities/text.py CHANGED
@@ -45,7 +45,26 @@ class ParseBoolError(Exception):
45
45
 
46
46
  @override
47
47
  def __str__(self) -> str:
48
- return f"Unable to parse {self.text!r} into a boolean value"
48
+ return f"Unable to parse boolean value; got {self.text!r}"
49
+
50
+
51
+ ##
52
+
53
+
54
+ def parse_none(text: str, /) -> None:
55
+ """Parse text into the None value."""
56
+ if text == "" or search("None", text, flags=IGNORECASE):
57
+ return
58
+ raise ParseNoneError(text=text)
59
+
60
+
61
+ @dataclass(kw_only=True, slots=True)
62
+ class ParseNoneError(Exception):
63
+ text: str
64
+
65
+ @override
66
+ def __str__(self) -> str:
67
+ return f"Unable to parse null value; got {self.text!r}"
49
68
 
50
69
 
51
70
  ##
@@ -108,8 +127,10 @@ def strip_and_dedent(text: str, /, *, trailing: bool = False) -> str:
108
127
 
109
128
  __all__ = [
110
129
  "ParseBoolError",
130
+ "ParseNoneError",
111
131
  "join_strs",
112
132
  "parse_bool",
133
+ "parse_none",
113
134
  "repr_encode",
114
135
  "snake_case",
115
136
  "split_str",