dycw-utilities 0.108.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.108.4
3
+ Version: 0.109.1
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=-2lmFv1UZnPQpN8IvJ-ViPH2DA03IC4wyaSgQYQI0zc,60
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=SOWaLVMK6p_HTMPhiXtgCk5F1KGiY8wNxQkYRBz1dVg,18683
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
@@ -40,12 +40,12 @@ 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
+ 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=ENkJunh7goUT0IOQ9LFbh-AFj7plC5ePKHBsnXk6slo,52122
48
+ utilities/polars.py,sha256=ZXiHLkn6CbRh0_e0db5KRjHPU0LAedwzGno7k9fsiIo,48917
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
@@ -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=7N4ZbBxXpPNttOTfg-hpaLFFWA5iJwF7tREBzUnbPOM,3415
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.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,,
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
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.108.4"
3
+ __version__ = "0.109.1"
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
- post: Callable[[_YieldFieldsClass[Any], Any], Any] | None = None,
194
+ allow_extra: bool = False,
193
195
  ) -> TDataclass:
194
196
  """Construct a dataclass from a mapping."""
195
- fields = yield_fields(cls, globalns=globalns, localns=localns)
196
- mapping_use = {
197
- f.name: _mapping_to_dataclass_one(
198
- f, mapping, case_sensitive=case_sensitive, post=post
199
- )
200
- for f in fields
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
- return cls(**mapping_use)
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
- def _mapping_to_dataclass_one(
206
- field: _YieldFieldsClass[Any],
207
- mapping: StrMapping,
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
- post: Callable[[_YieldFieldsClass[Any], Any], Any] | None = None,
212
- ) -> Any:
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
- key = one_str(mapping, field.name, case_sensitive=case_sensitive)
257
+ name = one_str(mapping, key, head=head, case_sensitive=case_sensitive)
215
258
  except OneStrEmptyError:
216
- if not isinstance(field.default, Sentinel):
217
- value = field.default
218
- elif not isinstance(field.default_factory, Sentinel):
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 _MappingToDataclassCaseInsensitiveNonUniqueError(
226
- mapping=mapping, field=field.name, first=error.first, second=error.second
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
- else:
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 MappingToDataclassError(Exception):
237
- mapping: StrMapping
238
- field: str
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 _MappingToDataclassEmptyError(MappingToDataclassError):
243
- case_sensitive: bool = False
244
-
283
+ class OneFieldEmptyError(OneFieldError[TDataclass]):
245
284
  @override
246
285
  def __str__(self) -> str:
247
- desc = f"Mapping {get_repr(self.mapping)} does not contain {self.field!r}"
248
- return desc if self.case_sensitive else f"{desc} (modulo case)"
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 _MappingToDataclassCaseInsensitiveNonUniqueError(MappingToDataclassError):
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
- return f"Mapping {get_repr(self.mapping)} must contain {self.field!r} exactly once (modulo case); got {self.first!r}, {self.second!r} and perhaps more"
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
- text_mapping = _text_to_dataclass_split_text(text, cls)
307
- case Mapping() as text_mapping:
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
- 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()
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
- value_mapping,
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 _text_to_dataclass_get_and_parse(
344
- fields: Iterable[_YieldFieldsClass[Any]],
345
- key: str,
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
- ) -> 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]
487
+ ) -> Any:
368
488
  try:
369
- parsed = parse_text(field.type_, value, case_sensitive=case_sensitive)
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/parse.py CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
8
  from types import NoneType
9
- from typing import Any, get_args, override
9
+ from typing import Any, override
10
10
 
11
11
  from utilities.datetime import is_subclass_date_not_datetime
12
12
  from utilities.enum import ParseEnumError, parse_enum
@@ -14,7 +14,7 @@ from utilities.functions import is_subclass_int_not_bool
14
14
  from utilities.iterables import one, one_str
15
15
  from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
16
16
  from utilities.text import ParseBoolError, ParseNoneError, parse_bool, parse_none
17
- from utilities.typing import is_literal_type, is_optional_type
17
+ from utilities.typing import get_args, is_literal_type, is_optional_type
18
18
  from utilities.version import ParseVersionError, Version, parse_version
19
19
 
20
20
 
utilities/polars.py CHANGED
@@ -71,7 +71,6 @@ from utilities.iterables import (
71
71
  CheckMappingsEqualError,
72
72
  CheckSubSetError,
73
73
  CheckSuperMappingError,
74
- CheckSuperSetError,
75
74
  OneEmptyError,
76
75
  OneNonUniqueError,
77
76
  always_iterable,
@@ -79,7 +78,6 @@ from utilities.iterables import (
79
78
  check_mappings_equal,
80
79
  check_subset,
81
80
  check_supermapping,
82
- check_superset,
83
81
  is_iterable_not_str,
84
82
  one,
85
83
  )
@@ -92,7 +90,6 @@ from utilities.math import (
92
90
  number_of_decimals,
93
91
  )
94
92
  from utilities.reprlib import get_repr
95
- from utilities.sentinel import Sentinel
96
93
  from utilities.types import MaybeStr, Number, WeekDay
97
94
  from utilities.typing import (
98
95
  get_args,
@@ -1484,100 +1481,6 @@ def week_num(column: IntoExprColumn, /, *, start: WeekDay = "mon") -> Expr | Ser
1484
1481
  ##
1485
1482
 
1486
1483
 
1487
- def yield_rows_as_dataclasses(
1488
- df: DataFrame,
1489
- cls: type[TDataclass],
1490
- /,
1491
- *,
1492
- globalns: StrMapping | None = None,
1493
- localns: StrMapping | None = None,
1494
- check_types: Literal["none", "first", "all"] = "first",
1495
- ) -> Iterator[TDataclass]:
1496
- """Yield the rows of a DataFrame as dataclasses."""
1497
- from dacite import from_dict
1498
- from dacite.exceptions import WrongTypeError
1499
-
1500
- columns = df.columns
1501
- required: set[str] = set()
1502
- for field in yield_fields(cls, globalns=globalns, localns=localns):
1503
- if isinstance(field.default, Sentinel) and isinstance(
1504
- field.default_factory, Sentinel
1505
- ):
1506
- required.add(field.name)
1507
- try:
1508
- check_superset(columns, required)
1509
- except CheckSuperSetError as error:
1510
- raise _YieldRowsAsDataClassesColumnsSuperSetError(
1511
- df=df, cls=cls, left=error.left, right=error.right, extra=error.extra
1512
- ) from None
1513
- rows = df.iter_rows(named=True)
1514
- match check_types:
1515
- case "none":
1516
- yield from _yield_rows_as_dataclasses_no_check_types(rows, cls)
1517
- case "first":
1518
- try:
1519
- first = next(rows)
1520
- except StopIteration:
1521
- return
1522
- try:
1523
- yield from_dict(cls, cast("Data", first))
1524
- except WrongTypeError as error:
1525
- raise _YieldRowsAsDataClassesWrongTypeError(
1526
- df=df, cls=cls, msg=str(error)
1527
- ) from None
1528
- yield from _yield_rows_as_dataclasses_no_check_types(rows, cls)
1529
- case "all":
1530
- try:
1531
- for row in rows:
1532
- yield from_dict(cls, cast("Data", row))
1533
- except WrongTypeError as error:
1534
- raise _YieldRowsAsDataClassesWrongTypeError(
1535
- df=df, cls=cls, msg=str(error)
1536
- ) from None
1537
- case _ as never:
1538
- assert_never(never)
1539
-
1540
-
1541
- def _yield_rows_as_dataclasses_no_check_types(
1542
- rows: Iterator[dict[str, Any]], cls: type[TDataclass], /
1543
- ) -> Iterator[TDataclass]:
1544
- """Yield the rows of a DataFrame as dataclasses without type checking."""
1545
- from dacite import Config, from_dict
1546
-
1547
- config = Config(check_types=False)
1548
- for row in rows:
1549
- yield from_dict(cls, cast("Data", row), config=config)
1550
-
1551
-
1552
- @dataclass(kw_only=True, slots=True)
1553
- class YieldRowsAsDataClassesError(Exception):
1554
- df: DataFrame
1555
- cls: type[Dataclass]
1556
-
1557
-
1558
- @dataclass(kw_only=True, slots=True)
1559
- class _YieldRowsAsDataClassesColumnsSuperSetError(YieldRowsAsDataClassesError):
1560
- left: AbstractSet[str]
1561
- right: AbstractSet[str]
1562
- extra: AbstractSet[str]
1563
-
1564
- @override
1565
- def __str__(self) -> str:
1566
- return f"DataFrame columns {get_repr(self.left)} must be a superset of dataclass fields {get_repr(self.right)}; dataclass had extra fields {get_repr(self.extra)}."
1567
-
1568
-
1569
- @dataclass(kw_only=True, slots=True)
1570
- class _YieldRowsAsDataClassesWrongTypeError(YieldRowsAsDataClassesError):
1571
- msg: str
1572
-
1573
- @override
1574
- def __str__(self) -> str:
1575
- return self.msg
1576
-
1577
-
1578
- ##
1579
-
1580
-
1581
1484
  @overload
1582
1485
  def yield_struct_series_elements(
1583
1486
  series: Series, /, *, strict: Literal[True]
@@ -1714,7 +1617,6 @@ __all__ = [
1714
1617
  "IsNullStructSeriesError",
1715
1618
  "SetFirstRowAsColumnsError",
1716
1619
  "StructFromDataClassError",
1717
- "YieldRowsAsDataClassesError",
1718
1620
  "YieldStructSeriesElementsError",
1719
1621
  "append_dataclass",
1720
1622
  "are_frames_equal",
@@ -1749,7 +1651,6 @@ __all__ = [
1749
1651
  "struct_from_dataclass",
1750
1652
  "touch",
1751
1653
  "unique_element",
1752
- "yield_rows_as_dataclasses",
1753
1654
  "yield_struct_series_dataclasses",
1754
1655
  "yield_struct_series_elements",
1755
1656
  "zoned_datetime",
@@ -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, Any, override
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(maybe_values_dotenv, environ)
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, values=error.mapping, counts=error.counts
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 mapping_to_dataclass(
50
- cls,
51
+ return text_to_dataclass(
51
52
  values,
53
+ cls,
52
54
  globalns=globalns,
53
55
  localns=localns,
54
- post=partial(_load_settings_post, path=path, values=values),
56
+ head=head,
57
+ case_sensitive=case_sensitive,
58
+ allow_extra=True,
55
59
  )
56
- except _MappingToDataclassEmptyError as error:
57
- raise _LoadSettingsEmptyError(
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 _LoadSettingsParseTextError(LoadSettingsError):
107
- values: StrMapping
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
- return f"Unable to parse field {self.field.name!r} of type {self.field.type_!r}; got {self.text!r}"
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"]