dycw-utilities 0.112.6__py3-none-any.whl → 0.112.8__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.112.6
3
+ Version: 0.112.8
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=d0j_69nmkoIKyb6X7hdJ8nRV6_SJJNH1dwhdMB_yAA8,60
1
+ utilities/__init__.py,sha256=XeC2tMugkErM_YQbJ1Iwis-CVp-V14bjMaBeziiWF1s,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
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=EG2lZRPU_rmkP3KWR7J5J43r8hr68s9uchSvevxpoZI,27433
14
+ utilities/dataclasses.py,sha256=2BloGVlH85tNZjFQiKXNk00Qwe9aovoV7cwxqFRG2l8,32598
15
15
  utilities/datetime.py,sha256=OF7jZE702UecnwAbq9D3N-GINpp9gSGoidki1RhimCE,35752
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
@@ -41,7 +41,7 @@ utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
41
41
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
42
42
  utilities/orjson.py,sha256=DBm2zPP04kcHpY3l1etL24ksNynu-R3duFyx3U-RjqQ,36948
43
43
  utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
44
- utilities/parse.py,sha256=Qr8hw14v0Bfxnc0TjFfjZtmYk6FrdVoqLnszb7R27BA,18982
44
+ utilities/parse.py,sha256=vsZ2jf_ceSI_Kta9titixufysJaVXh0Whjz1T4awJZw,18938
45
45
  utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
46
46
  utilities/period.py,sha256=RWfcNVoNlW07RNdU47g_zuLZMKbtgfK4bE6G-9tVjY8,11024
47
47
  utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
@@ -55,7 +55,7 @@ utilities/pyinstrument.py,sha256=OJFDh4o1CWIa4aYPYURdQjgap_nvP45KUsCEe94rQHY,829
55
55
  utilities/pyrsistent.py,sha256=MoDcAqQGlSNkmlS32DCJLw-cZFAfHB6K9kpox_iyI4k,2512
56
56
  utilities/pytest.py,sha256=85QUax4g2VBBAqAHtM9wekcSLB7_9O8AKFTaCshztL8,7989
57
57
  utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq3c,5088
58
- utilities/python_dotenv.py,sha256=6viKAI7zx9YQU2ewITaOcad7wMwkrf6FbYpBmCl4vCA,3170
58
+ utilities/python_dotenv.py,sha256=iWcnpXbH7S6RoXHiLlGgyuH6udCupAcPd_gQ0eAenQ0,3190
59
59
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
60
60
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
61
61
  utilities/redis.py,sha256=fAUbfOlCmxcxhh47PXQX63w0CU5iOFKfdUJ7jDn9ntM,22096
@@ -73,7 +73,7 @@ utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
73
73
  utilities/sys.py,sha256=h0Xr7Vj86wNalvwJVP1wj5Y0kD_VWm1vzuXZ_jw94mE,2743
74
74
  utilities/tempfile.py,sha256=VqmZJAhTJ1OaVywFzk5eqROV8iJbW9XQ_QYAV0bpdRo,1384
75
75
  utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
76
- utilities/text.py,sha256=c65sonE-vMJtBR8-LIntXUqku7wDQC6p4z69DOSur7o,10947
76
+ utilities/text.py,sha256=Fo12N4aA7k2rnb4W4vH9iiDh88Q5_nvRssTkfNsvVM8,10965
77
77
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
78
78
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
79
79
  utilities/traceback.py,sha256=secexUnBsecfWV4ZuqP1W4pGF3prOeO1CRyJK-8zQDU,27402
@@ -87,7 +87,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
87
  utilities/whenever.py,sha256=iLRP_-8CZtBpHKbGZGu-kjSMg1ZubJ-VSmgSy7Eudxw,17787
88
88
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
89
  utilities/zoneinfo.py,sha256=-Xm57PMMwDTYpxJdkiJG13wnbwK--I7XItBh5WVhD-o,1874
90
- dycw_utilities-0.112.6.dist-info/METADATA,sha256=nVSss8zVsLAitA6xDEcurQtsusPov5dkkpgrD9Q-SBI,13004
91
- dycw_utilities-0.112.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.112.6.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.112.6.dist-info/RECORD,,
90
+ dycw_utilities-0.112.8.dist-info/METADATA,sha256=i2cGlKTXS9uPmvL_dC8xIdqS9wlxq5NbsOgmOn6J23o,13004
91
+ dycw_utilities-0.112.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.112.8.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.112.8.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.112.6"
3
+ __version__ = "0.112.8"
utilities/dataclasses.py CHANGED
@@ -240,17 +240,31 @@ def mapping_to_dataclass(
240
240
  )
241
241
  else:
242
242
  fields_use = fields
243
- fields_to_values = str_mapping_to_field_mapping(
244
- cls,
245
- mapping,
246
- fields=fields_use,
247
- globalns=globalns,
248
- localns=localns,
249
- warn_name_errors=warn_name_errors,
250
- head=head,
251
- case_sensitive=case_sensitive,
252
- allow_extra=allow_extra,
253
- )
243
+ try:
244
+ fields_to_values = str_mapping_to_field_mapping(
245
+ cls,
246
+ mapping,
247
+ fields=fields_use,
248
+ globalns=globalns,
249
+ localns=localns,
250
+ warn_name_errors=warn_name_errors,
251
+ head=head,
252
+ case_sensitive=case_sensitive,
253
+ allow_extra=allow_extra,
254
+ )
255
+ except _StrMappingToFieldMappingEmptyError as error:
256
+ raise _MappingToDataClassEmptyError(
257
+ cls=cls, key=error.key, head=head, case_sensitive=case_sensitive
258
+ ) from None
259
+ except _StrMappingToFieldMappingNonUniqueError as error:
260
+ raise _MappingToDataClassNonUniqueError(
261
+ cls=cls,
262
+ key=error.key,
263
+ head=head,
264
+ case_sensitive=case_sensitive,
265
+ first=error.first,
266
+ second=error.second,
267
+ ) from None
254
268
  field_names_to_values = {f.name: v for f, v in fields_to_values.items()}
255
269
  default = {
256
270
  f.name
@@ -261,13 +275,50 @@ def mapping_to_dataclass(
261
275
  have = set(field_names_to_values) | default
262
276
  missing = {f.name for f in fields_use} - have
263
277
  if len(missing) >= 1:
264
- raise MappingToDataclassError(cls=cls, fields=missing)
278
+ raise _MappingToDataClassMissingValuesError(cls=cls, fields=missing)
265
279
  return cls(**field_names_to_values)
266
280
 
267
281
 
268
282
  @dataclass(kw_only=True, slots=True)
269
283
  class MappingToDataclassError(Exception, Generic[TDataclass]):
270
284
  cls: type[TDataclass]
285
+
286
+
287
+ @dataclass(kw_only=True, slots=True)
288
+ class _MappingToDataClassEmptyError(MappingToDataclassError[TDataclass]):
289
+ key: str
290
+ head: bool = False
291
+ case_sensitive: bool = False
292
+
293
+ @override
294
+ def __str__(self) -> str:
295
+ return _empty_error_str(
296
+ self.cls, self.key, head=self.head, case_sensitive=self.case_sensitive
297
+ )
298
+
299
+
300
+ @dataclass(kw_only=True, slots=True)
301
+ class _MappingToDataClassNonUniqueError(MappingToDataclassError[TDataclass]):
302
+ key: str
303
+ head: bool = False
304
+ case_sensitive: bool = False
305
+ first: str
306
+ second: str
307
+
308
+ @override
309
+ def __str__(self) -> str:
310
+ return _non_unique_error_str(
311
+ self.cls,
312
+ self.key,
313
+ self.first,
314
+ self.second,
315
+ head=self.head,
316
+ case_sensitive=self.case_sensitive,
317
+ )
318
+
319
+
320
+ @dataclass(kw_only=True, slots=True)
321
+ class _MappingToDataClassMissingValuesError(MappingToDataclassError[TDataclass]):
271
322
  fields: AbstractSet[str]
272
323
 
273
324
  @override
@@ -307,11 +358,11 @@ def one_field(
307
358
  try:
308
359
  name = one_str(mapping, key, head=head, case_sensitive=case_sensitive)
309
360
  except OneStrEmptyError:
310
- raise OneFieldEmptyError(
361
+ raise _OneFieldEmptyError(
311
362
  cls=cls, key=key, head=head, case_sensitive=case_sensitive
312
363
  ) from None
313
364
  except OneStrNonUniqueError as error:
314
- raise OneFieldNonUniqueError(
365
+ raise _OneFieldNonUniqueError(
315
366
  cls=cls,
316
367
  key=key,
317
368
  head=head,
@@ -331,46 +382,29 @@ class OneFieldError(Exception, Generic[TDataclass]):
331
382
 
332
383
 
333
384
  @dataclass(kw_only=True, slots=True)
334
- class OneFieldEmptyError(OneFieldError[TDataclass]):
385
+ class _OneFieldEmptyError(OneFieldError[TDataclass]):
335
386
  @override
336
387
  def __str__(self) -> str:
337
- head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
338
- match self.head, self.case_sensitive:
339
- case False, True:
340
- tail = f"a field {self.key!r}"
341
- case False, False:
342
- tail = f"a field {self.key!r} (modulo case)"
343
- case True, True:
344
- tail = f"any field starting with {self.key!r}"
345
- case True, False:
346
- tail = f"any field starting with {self.key!r} (modulo case)"
347
- case _ as never:
348
- assert_never(never)
349
- return f"{head} {tail}"
388
+ return _empty_error_str(
389
+ self.cls, self.key, head=self.head, case_sensitive=self.case_sensitive
390
+ )
350
391
 
351
392
 
352
393
  @dataclass(kw_only=True, slots=True)
353
- class OneFieldNonUniqueError(OneFieldError[TDataclass]):
394
+ class _OneFieldNonUniqueError(OneFieldError[TDataclass]):
354
395
  first: str
355
396
  second: str
356
397
 
357
398
  @override
358
399
  def __str__(self) -> str:
359
- head = f"Dataclass {get_class_name(self.cls)!r} must contain"
360
- match self.head, self.case_sensitive:
361
- case False, True:
362
- raise ImpossibleCaseError( # pragma: no cover
363
- case=[f"{self.head=}", f"{self.case_sensitive=}"]
364
- )
365
- case False, False:
366
- mid = f"field {self.key!r} exactly once (modulo case)"
367
- case True, True:
368
- mid = f"exactly one field starting with {self.key!r}"
369
- case True, False:
370
- mid = f"exactly one field starting with {self.key!r} (modulo case)"
371
- case _ as never:
372
- assert_never(never)
373
- return f"{head} {mid}; got {self.first!r}, {self.second!r} and perhaps more"
400
+ return _non_unique_error_str(
401
+ self.cls,
402
+ self.key,
403
+ self.first,
404
+ self.second,
405
+ head=self.head,
406
+ case_sensitive=self.case_sensitive,
407
+ )
374
408
 
375
409
 
376
410
  ##
@@ -487,17 +521,31 @@ def parse_dataclass(
487
521
  cls, globalns=globalns, localns=localns, warn_name_errors=warn_name_errors
488
522
  )
489
523
  )
490
- fields_to_serializes = str_mapping_to_field_mapping(
491
- cls,
492
- keys_to_serializes,
493
- fields=fields,
494
- globalns=globalns,
495
- localns=localns,
496
- warn_name_errors=warn_name_errors,
497
- head=head,
498
- case_sensitive=case_sensitive,
499
- allow_extra=allow_extra_keys,
500
- )
524
+ try:
525
+ fields_to_serializes = str_mapping_to_field_mapping(
526
+ cls,
527
+ keys_to_serializes,
528
+ fields=fields,
529
+ globalns=globalns,
530
+ localns=localns,
531
+ warn_name_errors=warn_name_errors,
532
+ head=head,
533
+ case_sensitive=case_sensitive,
534
+ allow_extra=allow_extra_keys,
535
+ )
536
+ except _StrMappingToFieldMappingEmptyError as error:
537
+ raise _ParseDataClassStrMappingToFieldMappingEmptyError(
538
+ cls=cls, key=error.key, head=head, case_sensitive=case_sensitive
539
+ ) from None
540
+ except _StrMappingToFieldMappingNonUniqueError as error:
541
+ raise _ParseDataClassStrMappingToFieldMappingNonUniqueError(
542
+ cls=cls,
543
+ key=error.key,
544
+ head=head,
545
+ case_sensitive=case_sensitive,
546
+ first=error.first,
547
+ second=error.second,
548
+ ) from None
501
549
  field_names_to_values = {
502
550
  f.name: _parse_dataclass_parse_text(
503
551
  f,
@@ -511,17 +559,20 @@ def parse_dataclass(
511
559
  )
512
560
  for f, t in fields_to_serializes.items()
513
561
  }
514
- return mapping_to_dataclass(
515
- cls,
516
- field_names_to_values,
517
- fields=fields,
518
- globalns=globalns,
519
- localns=localns,
520
- warn_name_errors=warn_name_errors,
521
- head=head,
522
- case_sensitive=case_sensitive,
523
- allow_extra=allow_extra_keys,
524
- )
562
+ try:
563
+ return mapping_to_dataclass(
564
+ cls,
565
+ field_names_to_values,
566
+ fields=fields,
567
+ globalns=globalns,
568
+ localns=localns,
569
+ warn_name_errors=warn_name_errors,
570
+ head=head,
571
+ case_sensitive=case_sensitive,
572
+ allow_extra=allow_extra_keys,
573
+ )
574
+ except _MappingToDataClassMissingValuesError as error:
575
+ raise _ParseDataClassMissingValuesError(cls=cls, fields=error.fields) from None
525
576
 
526
577
 
527
578
  def _parse_dataclass_split_key_value_pairs(
@@ -549,7 +600,7 @@ def _parse_dataclass_split_key_value_pairs(
549
600
  ) from None
550
601
  except _SplitKeyValuePairsDuplicateKeysError as error:
551
602
  raise _ParseDataClassSplitKeyValuePairsDuplicateKeysError(
552
- text=error.text, cls=cls, counts=error.counts
603
+ cls=cls, counts=error.counts
553
604
  ) from None
554
605
 
555
606
 
@@ -579,25 +630,28 @@ def _parse_dataclass_parse_text(
579
630
  raise _ParseDataClassTextParseError(cls=cls, field=field, text=text) from None
580
631
  except _ParseObjectExtraNonUniqueError as error:
581
632
  raise _ParseDataClassTextExtraNonUniqueError(
582
- cls=cls, field=field, text=text, first=error.first, second=error.second
633
+ cls=cls, field=field, first=error.first, second=error.second
583
634
  ) from None
584
635
 
585
636
 
586
637
  @dataclass(kw_only=True, slots=True)
587
638
  class ParseDataClassError(Exception, Generic[TDataclass]):
588
- text: str
589
639
  cls: type[TDataclass]
590
640
 
591
641
 
592
642
  @dataclass(kw_only=True, slots=True)
593
- class _ParseDataClassSplitKeyValuePairsSplitError(ParseDataClassError):
643
+ class _ParseDataClassSplitKeyValuePairsSplitError(ParseDataClassError[TDataclass]):
644
+ text: str
645
+
594
646
  @override
595
647
  def __str__(self) -> str:
596
648
  return f"Unable to construct {get_class_name(self.cls)!r}; failed to split key-value pair {self.text!r}"
597
649
 
598
650
 
599
651
  @dataclass(kw_only=True, slots=True)
600
- class _ParseDataClassSplitKeyValuePairsDuplicateKeysError(ParseDataClassError):
652
+ class _ParseDataClassSplitKeyValuePairsDuplicateKeysError(
653
+ ParseDataClassError[TDataclass]
654
+ ):
601
655
  counts: Mapping[str, int]
602
656
 
603
657
  @override
@@ -608,6 +662,7 @@ class _ParseDataClassSplitKeyValuePairsDuplicateKeysError(ParseDataClassError):
608
662
  @dataclass(kw_only=True, slots=True)
609
663
  class _ParseDataClassTextParseError(ParseDataClassError[TDataclass]):
610
664
  field: _YieldFieldsClass[Any]
665
+ text: str
611
666
 
612
667
  @override
613
668
  def __str__(self) -> str:
@@ -625,6 +680,56 @@ class _ParseDataClassTextExtraNonUniqueError(ParseDataClassError[TDataclass]):
625
680
  return f"Unable to construct {get_class_name(self.cls)!r} since the field {self.field.name!r} of type {self.field.type_!r} must contain exactly one parent class in `extra`; got {self.first!r}, {self.second!r} and perhaps more"
626
681
 
627
682
 
683
+ @dataclass(kw_only=True, slots=True)
684
+ class _ParseDataClassStrMappingToFieldMappingEmptyError(
685
+ ParseDataClassError[TDataclass]
686
+ ):
687
+ key: str
688
+ head: bool = False
689
+ case_sensitive: bool = False
690
+
691
+ @override
692
+ def __str__(self) -> str:
693
+ head = f"Unable to construct {get_class_name(self.cls)!r} since it does not contain"
694
+ tail = _empty_error_str_core(
695
+ self.key, head=self.head, case_sensitive=self.case_sensitive
696
+ )
697
+ return f"{head} {tail}"
698
+
699
+
700
+ @dataclass(kw_only=True, slots=True)
701
+ class _ParseDataClassStrMappingToFieldMappingNonUniqueError(
702
+ ParseDataClassError[TDataclass]
703
+ ):
704
+ key: str
705
+ head: bool = False
706
+ case_sensitive: bool = False
707
+ first: str
708
+ second: str
709
+
710
+ @override
711
+ def __str__(self) -> str:
712
+ head = f"Unable to construct {get_class_name(self.cls)!r} since it must contain"
713
+ tail = _non_unique_error_str_core(
714
+ self.key,
715
+ self.first,
716
+ self.second,
717
+ head=self.head,
718
+ case_sensitive=self.case_sensitive,
719
+ )
720
+ return f"{head} {tail}"
721
+
722
+
723
+ @dataclass(kw_only=True, slots=True)
724
+ class _ParseDataClassMissingValuesError(ParseDataClassError[TDataclass]):
725
+ fields: AbstractSet[str]
726
+
727
+ @override
728
+ def __str__(self) -> str:
729
+ desc = ", ".join(map(repr, sorted(self.fields)))
730
+ return f"Unable to construct {get_class_name(self.cls)!r}; missing values for {desc}"
731
+
732
+
628
733
  ##
629
734
 
630
735
 
@@ -655,36 +760,55 @@ def str_mapping_to_field_mapping(
655
760
  head=head,
656
761
  case_sensitive=case_sensitive,
657
762
  )
658
- except OneFieldEmptyError:
763
+ except _OneFieldEmptyError:
659
764
  if not allow_extra:
660
- raise StrMappingToFieldMappingError(
765
+ raise _StrMappingToFieldMappingEmptyError(
661
766
  cls=cls, key=key, head=head, case_sensitive=case_sensitive
662
767
  ) from None
768
+ except _OneFieldNonUniqueError as error:
769
+ raise _StrMappingToFieldMappingNonUniqueError(
770
+ cls=cls,
771
+ key=key,
772
+ head=head,
773
+ case_sensitive=case_sensitive,
774
+ first=error.first,
775
+ second=error.second,
776
+ ) from None
663
777
  return {field: mapping[key] for key, field in keys_to_fields.items()}
664
778
 
665
779
 
666
780
  @dataclass(kw_only=True, slots=True)
667
- class StrMappingToFieldMappingError(Exception):
668
- cls: type[Dataclass]
781
+ class StrMappingToFieldMappingError(Exception, Generic[TDataclass]):
782
+ cls: type[TDataclass]
669
783
  key: str
670
784
  head: bool = False
671
785
  case_sensitive: bool = False
672
786
 
787
+
788
+ @dataclass(kw_only=True, slots=True)
789
+ class _StrMappingToFieldMappingEmptyError(StrMappingToFieldMappingError):
673
790
  @override
674
791
  def __str__(self) -> str:
675
- head = f"Dataclass {get_class_name(self.cls)!r} does not contain"
676
- match self.head, self.case_sensitive:
677
- case False, True:
678
- tail = f"a field {self.key!r}"
679
- case False, False:
680
- tail = f"a field {self.key!r} (modulo case)"
681
- case True, True:
682
- tail = f"any field starting with {self.key!r}"
683
- case True, False:
684
- tail = f"any field starting with {self.key!r} (modulo case)"
685
- case _ as never:
686
- assert_never(never)
687
- return f"{head} {tail}"
792
+ return _empty_error_str(
793
+ self.cls, self.key, head=self.head, case_sensitive=self.case_sensitive
794
+ )
795
+
796
+
797
+ @dataclass(kw_only=True, slots=True)
798
+ class _StrMappingToFieldMappingNonUniqueError(StrMappingToFieldMappingError):
799
+ first: str
800
+ second: str
801
+
802
+ @override
803
+ def __str__(self) -> str:
804
+ return _non_unique_error_str(
805
+ self.cls,
806
+ self.key,
807
+ self.first,
808
+ self.second,
809
+ head=self.head,
810
+ case_sensitive=self.case_sensitive,
811
+ )
688
812
 
689
813
 
690
814
  ##
@@ -852,11 +976,81 @@ class YieldFieldsError(Exception):
852
976
 
853
977
  ##
854
978
 
979
+
980
+ def _empty_error_str(
981
+ cls: type[Dataclass],
982
+ key: str,
983
+ /,
984
+ *,
985
+ head: bool = False,
986
+ case_sensitive: bool = False,
987
+ ) -> str:
988
+ head_msg = f"Dataclass {get_class_name(cls)!r} does not contain"
989
+ tail_msg = _empty_error_str_core(key, head=head, case_sensitive=case_sensitive)
990
+ return f"{head_msg} {tail_msg}"
991
+
992
+
993
+ def _empty_error_str_core(
994
+ key: str, /, *, head: bool = False, case_sensitive: bool = False
995
+ ) -> str:
996
+ match head, case_sensitive:
997
+ case False, True:
998
+ return f"a field {key!r}"
999
+ case False, False:
1000
+ return f"a field {key!r} (modulo case)"
1001
+ case True, True:
1002
+ return f"any field starting with {key!r}"
1003
+ case True, False:
1004
+ return f"any field starting with {key!r} (modulo case)"
1005
+ case _ as never:
1006
+ assert_never(never)
1007
+
1008
+
1009
+ def _non_unique_error_str(
1010
+ cls: type[Dataclass],
1011
+ key: str,
1012
+ first: str,
1013
+ second: str,
1014
+ /,
1015
+ *,
1016
+ head: bool = False,
1017
+ case_sensitive: bool = False,
1018
+ ) -> str:
1019
+ head_msg = f"Dataclass {get_class_name(cls)!r} must contain"
1020
+ tail_msg = _non_unique_error_str_core(
1021
+ key, first, second, head=head, case_sensitive=case_sensitive
1022
+ )
1023
+ return f"{head_msg} {tail_msg}"
1024
+
1025
+
1026
+ def _non_unique_error_str_core(
1027
+ key: str,
1028
+ first: str,
1029
+ second: str,
1030
+ /,
1031
+ *,
1032
+ head: bool = False,
1033
+ case_sensitive: bool = False,
1034
+ ) -> str:
1035
+ match head, case_sensitive:
1036
+ case False, True:
1037
+ raise ImpossibleCaseError( # pragma: no cover
1038
+ case=[f"{head=}", f"{case_sensitive=}"]
1039
+ )
1040
+ case False, False:
1041
+ head_msg = f"field {key!r} exactly once (modulo case)"
1042
+ case True, True:
1043
+ head_msg = f"exactly one field starting with {key!r}"
1044
+ case True, False:
1045
+ head_msg = f"exactly one field starting with {key!r} (modulo case)"
1046
+ case _ as never:
1047
+ assert_never(never)
1048
+ return f"{head_msg}; got {first!r}, {second!r} and perhaps more"
1049
+
1050
+
855
1051
  __all__ = [
856
1052
  "MappingToDataclassError",
857
- "OneFieldEmptyError",
858
1053
  "OneFieldError",
859
- "OneFieldNonUniqueError",
860
1054
  "ParseDataClassError",
861
1055
  "StrMappingToFieldMappingError",
862
1056
  "YieldFieldsError",
utilities/parse.py CHANGED
@@ -286,9 +286,7 @@ def _parse_object_extra(cls: Any, text: str, extra: ParseObjectExtra, /) -> Any:
286
286
  parser = extra[cls]
287
287
  except KeyError:
288
288
  try:
289
- parser = one(
290
- p for c, p in extra.items() if (cls is c) or is_subclass_gen(cls, c)
291
- )
289
+ parser = one(p for c, p in extra.items() if is_subclass_gen(cls, c))
292
290
  except (OneEmptyError, TypeError):
293
291
  raise _ParseObjectParseError(type_=cls, text=text) from None
294
292
  except OneNonUniqueError as error:
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, override
6
6
 
7
7
  from dotenv import dotenv_values
8
8
 
9
- from utilities.dataclasses import MappingToDataclassError, parse_dataclass
9
+ from utilities.dataclasses import _ParseDataClassMissingValuesError, parse_dataclass
10
10
  from utilities.git import get_repo_root
11
11
  from utilities.iterables import MergeStrMappingsError, merge_str_mappings
12
12
  from utilities.pathlib import PWD
@@ -61,7 +61,7 @@ def load_settings(
61
61
  allow_extra_keys=True,
62
62
  extra_parsers=extra_parsers,
63
63
  )
64
- except MappingToDataclassError as error:
64
+ except _ParseDataClassMissingValuesError as error:
65
65
  raise _LoadSettingsMissingKeysError(path=path, fields=error.fields) from None
66
66
 
67
67
 
utilities/text.py CHANGED
@@ -25,9 +25,9 @@ DEFAULT_SEPARATOR = ","
25
25
 
26
26
  def parse_bool(text: str, /) -> bool:
27
27
  """Parse text into a boolean value."""
28
- if search(r"^(0|False)$", text, flags=IGNORECASE):
28
+ if search(r"^(0|False|N|No|Off)$", text, flags=IGNORECASE):
29
29
  return False
30
- if search(r"^(1|True)$", text, flags=IGNORECASE):
30
+ if search(r"^(1|True|Y|Yes|On)$", text, flags=IGNORECASE):
31
31
  return True
32
32
  raise ParseBoolError(text=text)
33
33