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.
- {dycw_utilities-0.108.3.dist-info → dycw_utilities-0.108.4.dist-info}/METADATA +5 -5
- {dycw_utilities-0.108.3.dist-info → dycw_utilities-0.108.4.dist-info}/RECORD +6 -6
- utilities/__init__.py +1 -1
- utilities/dataclasses.py +166 -18
- {dycw_utilities-0.108.3.dist-info → dycw_utilities-0.108.4.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.108.3.dist-info → dycw_utilities-0.108.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.108.
|
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.
|
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.
|
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.
|
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.
|
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
|
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=
|
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.
|
88
|
-
dycw_utilities-0.108.
|
89
|
-
dycw_utilities-0.108.
|
90
|
-
dycw_utilities-0.108.
|
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
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
|
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
|
31
|
+
from collections.abc import Callable, Iterable, Iterator
|
20
32
|
|
21
|
-
from utilities.types import Dataclass, StrMapping
|
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:
|
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
|
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
|
]
|
File without changes
|
File without changes
|