dycw-utilities 0.108.3__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.3
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=932kugeJpmqtWXsJGSf5NBQ-6ig0_Xf-TGDJSK9b28w,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,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=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
@@ -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.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,,
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.3"
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
  ]