pythonwrench 0.5.0__tar.gz → 0.5.2__tar.gz
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.
- {pythonwrench-0.5.0/src/pythonwrench.egg-info → pythonwrench-0.5.2}/PKG-INFO +6 -2
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/README.md +3 -0
- pythonwrench-0.5.2/docs/requirements.txt +4 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/pyproject.toml +3 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/__init__.py +13 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/_core.py +1 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/argparse.py +0 -2
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/cast.py +4 -2
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/checksum.py +98 -35
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/collections.py +57 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/disk_cache.py +5 -9
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/functools.py +21 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/inspect.py +1 -20
- pythonwrench-0.5.2/src/pythonwrench/time.py +31 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/__init__.py +8 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/checks.py +97 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/typing/classes.py +2 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2/src/pythonwrench.egg-info}/PKG-INFO +6 -2
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/SOURCES.txt +2 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/requires.txt +3 -1
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_cast.py +12 -4
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_checksum.py +20 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_collections.py +14 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_typing.py +23 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/LICENSE +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/setup.cfg +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/setup.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/__main__.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/abc.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/__init__.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/prop.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/collections/reducers.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/concurrent.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/csv.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/dataclasses.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/datetime.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/difflib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/entries.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/enum.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/hashlib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/importlib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/json.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/jsonl.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/logging.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/math.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/os.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/pickle.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/random.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/re.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/semver.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench/warnings.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/entry_points.txt +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_abc.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_argparse.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_csv.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_dataclasses.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_difflib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_disk_cache.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_entries.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_enum.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_functools.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_hashlib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_importlib.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_inspect.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_json.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_jsonl.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_logging.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_math.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_os.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_random.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_readme.py +0 -0
- {pythonwrench-0.5.0 → pythonwrench-0.5.2}/tests/test_semver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonwrench
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python library with tools for typing, manipulating collections, and more!
|
|
5
5
|
Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
|
|
6
6
|
Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
|
|
@@ -50,9 +50,10 @@ Classifier: Operating System :: POSIX
|
|
|
50
50
|
Requires-Python: <3.15,>=3.8
|
|
51
51
|
Description-Content-Type: text/markdown
|
|
52
52
|
License-File: LICENSE
|
|
53
|
-
Requires-Dist: lazy-loader>=0.4
|
|
54
53
|
Requires-Dist: typing-extensions>=4.10.0
|
|
55
54
|
Provides-Extra: dev
|
|
55
|
+
Provides-Extra: lazy
|
|
56
|
+
Requires-Dist: lazy-loader>=0.4; extra == "lazy"
|
|
56
57
|
Dynamic: license-file
|
|
57
58
|
|
|
58
59
|
# pythonwrench
|
|
@@ -218,6 +219,9 @@ Easely converts common python structures like list of dicts to dict of lists :
|
|
|
218
219
|
... True
|
|
219
220
|
```
|
|
220
221
|
|
|
222
|
+
## Lazy loading
|
|
223
|
+
To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
|
|
224
|
+
|
|
221
225
|
## Contact
|
|
222
226
|
Maintainer:
|
|
223
227
|
- [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
|
|
@@ -161,6 +161,9 @@ Easely converts common python structures like list of dicts to dict of lists :
|
|
|
161
161
|
... True
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
## Lazy loading
|
|
165
|
+
To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
|
|
166
|
+
|
|
164
167
|
## Contact
|
|
165
168
|
Maintainer:
|
|
166
169
|
- [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
|
|
@@ -31,7 +31,6 @@ maintainers = [
|
|
|
31
31
|
]
|
|
32
32
|
dynamic = ["version"]
|
|
33
33
|
dependencies = [
|
|
34
|
-
"lazy-loader>=0.4",
|
|
35
34
|
"typing-extensions>=4.10.0",
|
|
36
35
|
]
|
|
37
36
|
|
|
@@ -52,6 +51,9 @@ pyw-safe-rmdir = "pythonwrench.entries:main_safe_rmdir"
|
|
|
52
51
|
|
|
53
52
|
[project.optional-dependencies]
|
|
54
53
|
dev = []
|
|
54
|
+
lazy = [
|
|
55
|
+
"lazy-loader>=0.4",
|
|
56
|
+
]
|
|
55
57
|
|
|
56
58
|
[build-system]
|
|
57
59
|
requires = ["setuptools >= 61.0"]
|
|
@@ -9,7 +9,7 @@ __author_email__ = "labbeti.pub@gmail.com"
|
|
|
9
9
|
__license__ = "MIT"
|
|
10
10
|
__maintainer__ = "Étienne Labbé (Labbeti)"
|
|
11
11
|
__status__ = "Development"
|
|
12
|
-
__version__ = "0.5.
|
|
12
|
+
__version__ = "0.5.2"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
@@ -114,10 +114,12 @@ if TYPE_CHECKING or lazy is None:
|
|
|
114
114
|
)
|
|
115
115
|
from .hashlib import hash_file
|
|
116
116
|
from .importlib import (
|
|
117
|
+
Placeholder,
|
|
117
118
|
is_available_package,
|
|
118
119
|
is_editable_package,
|
|
119
120
|
reload_editable_packages,
|
|
120
121
|
reload_submodules,
|
|
122
|
+
requires_packages,
|
|
121
123
|
search_submodules,
|
|
122
124
|
)
|
|
123
125
|
from .inspect import get_argnames, get_current_fn_name, get_fullname
|
|
@@ -187,6 +189,7 @@ if TYPE_CHECKING or lazy is None:
|
|
|
187
189
|
SupportsGetitemIterLen2,
|
|
188
190
|
SupportsGetitemLen,
|
|
189
191
|
SupportsGetitemLen2,
|
|
192
|
+
SupportsIter,
|
|
190
193
|
SupportsIterLen,
|
|
191
194
|
SupportsLen,
|
|
192
195
|
SupportsMatmul,
|
|
@@ -199,6 +202,7 @@ if TYPE_CHECKING or lazy is None:
|
|
|
199
202
|
is_builtin_number,
|
|
200
203
|
is_builtin_obj,
|
|
201
204
|
is_builtin_scalar,
|
|
205
|
+
is_collection_alias,
|
|
202
206
|
is_dataclass_instance,
|
|
203
207
|
is_iterable_bool,
|
|
204
208
|
is_iterable_bytes_or_list,
|
|
@@ -207,7 +211,9 @@ if TYPE_CHECKING or lazy is None:
|
|
|
207
211
|
is_iterable_integral,
|
|
208
212
|
is_iterable_str,
|
|
209
213
|
is_namedtuple_instance,
|
|
214
|
+
is_parameterized,
|
|
210
215
|
is_sequence_str,
|
|
216
|
+
is_special_form,
|
|
211
217
|
is_typed_dict,
|
|
212
218
|
isinstance_generic,
|
|
213
219
|
)
|
|
@@ -321,7 +327,9 @@ else:
|
|
|
321
327
|
"is_editable_package",
|
|
322
328
|
"reload_editable_packages",
|
|
323
329
|
"reload_submodules",
|
|
330
|
+
"requires_packages",
|
|
324
331
|
"search_submodules",
|
|
332
|
+
"Placeholder",
|
|
325
333
|
],
|
|
326
334
|
"inspect": ["get_argnames", "get_current_fn_name", "get_fullname"],
|
|
327
335
|
"json": [
|
|
@@ -397,6 +405,7 @@ else:
|
|
|
397
405
|
"SupportsGetitemIterLen2",
|
|
398
406
|
"SupportsGetitemLen",
|
|
399
407
|
"SupportsGetitemLen2",
|
|
408
|
+
"SupportsIter",
|
|
400
409
|
"SupportsIterLen",
|
|
401
410
|
"SupportsLen",
|
|
402
411
|
"SupportsMatmul",
|
|
@@ -409,6 +418,7 @@ else:
|
|
|
409
418
|
"is_builtin_number",
|
|
410
419
|
"is_builtin_obj",
|
|
411
420
|
"is_builtin_scalar",
|
|
421
|
+
"is_collection_alias",
|
|
412
422
|
"is_dataclass_instance",
|
|
413
423
|
"is_iterable_bool",
|
|
414
424
|
"is_iterable_bytes_or_list",
|
|
@@ -417,7 +427,9 @@ else:
|
|
|
417
427
|
"is_iterable_integral",
|
|
418
428
|
"is_iterable_str",
|
|
419
429
|
"is_namedtuple_instance",
|
|
430
|
+
"is_parameterized",
|
|
420
431
|
"is_sequence_str",
|
|
432
|
+
"is_special_form",
|
|
421
433
|
"is_typed_dict",
|
|
422
434
|
"isinstance_generic",
|
|
423
435
|
],
|
|
@@ -119,8 +119,6 @@ def str_to_optional_bool(
|
|
|
119
119
|
x: str,
|
|
120
120
|
*,
|
|
121
121
|
case_sensitive: bool = False,
|
|
122
|
-
true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
|
|
123
|
-
false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
|
|
124
122
|
none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
|
|
125
123
|
) -> Optional[bool]:
|
|
126
124
|
"""Convert string values to optional bool safely. Intended for argparse arguments.
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
from argparse import Namespace
|
|
5
5
|
from collections import Counter
|
|
6
|
-
from dataclasses import asdict
|
|
7
6
|
from datetime import date
|
|
8
7
|
from enum import Enum
|
|
9
8
|
from functools import partial
|
|
@@ -119,7 +118,10 @@ def _namespace_to_builtin(x: Namespace, **kwargs) -> Any:
|
|
|
119
118
|
|
|
120
119
|
@register_as_builtin_fn(DataclassInstance)
|
|
121
120
|
def _dataclass_to_builtin(x: DataclassInstance, **kwargs) -> Any:
|
|
122
|
-
|
|
121
|
+
# IMPORTANT note : we do not use dataclasses.asdict() because it also converts attributes like Counter, but not do dicts
|
|
122
|
+
field_names = x.__dataclass_fields__.keys()
|
|
123
|
+
xdict = {name: as_builtin(getattr(x, name), **kwargs) for name in field_names}
|
|
124
|
+
return xdict
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
@register_as_builtin_fn(NamedTupleInstance)
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
import struct
|
|
7
7
|
import zlib
|
|
8
8
|
from dataclasses import asdict
|
|
9
|
+
from datetime import date, datetime
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from functools import lru_cache
|
|
11
12
|
from pathlib import Path
|
|
@@ -13,12 +14,15 @@ from types import FunctionType, MethodType
|
|
|
13
14
|
from typing import (
|
|
14
15
|
Any,
|
|
15
16
|
Callable,
|
|
17
|
+
Dict,
|
|
16
18
|
Generator,
|
|
17
19
|
Iterable,
|
|
18
20
|
Mapping,
|
|
19
21
|
Optional,
|
|
20
22
|
TypeVar,
|
|
21
23
|
Union,
|
|
24
|
+
get_args,
|
|
25
|
+
get_origin,
|
|
22
26
|
overload,
|
|
23
27
|
)
|
|
24
28
|
|
|
@@ -27,14 +31,19 @@ from pythonwrench.functools import function_alias
|
|
|
27
31
|
from pythonwrench.inspect import get_fullname
|
|
28
32
|
from pythonwrench.typing import (
|
|
29
33
|
DataclassInstance,
|
|
34
|
+
EllipsisType,
|
|
30
35
|
NamedTupleInstance,
|
|
31
36
|
NoneType,
|
|
37
|
+
is_collection_alias,
|
|
38
|
+
is_parameterized,
|
|
39
|
+
is_special_form,
|
|
32
40
|
)
|
|
33
41
|
|
|
34
42
|
T = TypeVar("T")
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
_CHECKSUM_REGISTRY = _FunctionRegistry[int]()
|
|
46
|
+
_CHECKSUM_PROTOCOLS = False
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
@overload
|
|
@@ -130,9 +139,7 @@ def checksum_int(x: int, **kwargs) -> int:
|
|
|
130
139
|
# Intermediate functions
|
|
131
140
|
@register_checksum_fn(bytearray)
|
|
132
141
|
def checksum_bytearray(x: bytearray, **kwargs) -> int:
|
|
133
|
-
kwargs
|
|
134
|
-
get_fullname(x)
|
|
135
|
-
)
|
|
142
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
136
143
|
return _checksum_bytes_bytearray(x, **kwargs)
|
|
137
144
|
|
|
138
145
|
|
|
@@ -151,25 +158,25 @@ def checksum_complex(x: complex, **kwargs) -> int:
|
|
|
151
158
|
|
|
152
159
|
@register_checksum_fn(FunctionType)
|
|
153
160
|
def checksum_function(x: FunctionType, **kwargs) -> int:
|
|
154
|
-
kwargs
|
|
155
|
-
get_fullname(x)
|
|
156
|
-
)
|
|
161
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
157
162
|
return checksum_str(x.__qualname__, **kwargs)
|
|
158
163
|
|
|
159
164
|
|
|
160
165
|
@register_checksum_fn(NoneType)
|
|
161
166
|
def checksum_none(x: None, **kwargs) -> int:
|
|
162
|
-
kwargs
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
168
|
+
return checksum_type(x.__class__, **kwargs)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@register_checksum_fn(EllipsisType)
|
|
172
|
+
def checksum_ellipsis(x: None, **kwargs) -> int:
|
|
173
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
174
|
+
return checksum_type(x.__class__, **kwargs)
|
|
166
175
|
|
|
167
176
|
|
|
168
177
|
@register_checksum_fn(str)
|
|
169
178
|
def checksum_str(x: str, **kwargs) -> int:
|
|
170
|
-
kwargs
|
|
171
|
-
get_fullname(x)
|
|
172
|
-
)
|
|
179
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
173
180
|
return checksum_bytes(x.encode(), **kwargs)
|
|
174
181
|
|
|
175
182
|
|
|
@@ -181,12 +188,34 @@ def checksum_type(x: type, **kwargs) -> int:
|
|
|
181
188
|
# Recursive functions
|
|
182
189
|
@register_checksum_fn(DataclassInstance)
|
|
183
190
|
def checksum_dataclass(x: DataclassInstance, **kwargs) -> int:
|
|
184
|
-
kwargs
|
|
185
|
-
get_fullname(x)
|
|
186
|
-
)
|
|
191
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
187
192
|
return checksum_dict(asdict(x), **kwargs)
|
|
188
193
|
|
|
189
194
|
|
|
195
|
+
@register_checksum_fn(datetime)
|
|
196
|
+
def checksum_datetime(x: datetime, **kwargs) -> int:
|
|
197
|
+
return _checksum_iterable(
|
|
198
|
+
[
|
|
199
|
+
x.__class__,
|
|
200
|
+
x.year,
|
|
201
|
+
x.month,
|
|
202
|
+
x.day,
|
|
203
|
+
x.hour,
|
|
204
|
+
x.minute,
|
|
205
|
+
x.second,
|
|
206
|
+
x.microsecond,
|
|
207
|
+
x.tzinfo,
|
|
208
|
+
x.fold,
|
|
209
|
+
],
|
|
210
|
+
**kwargs,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@register_checksum_fn(date)
|
|
215
|
+
def checksum_date(x: date, **kwargs) -> int:
|
|
216
|
+
return _checksum_iterable([x.__class__, x.year, x.month, x.day], **kwargs)
|
|
217
|
+
|
|
218
|
+
|
|
190
219
|
@register_checksum_fn(dict)
|
|
191
220
|
def checksum_dict(x: dict, **kwargs) -> int:
|
|
192
221
|
return _checksum_mapping(x, **kwargs)
|
|
@@ -214,9 +243,7 @@ def checksum_set(x: Union[set, frozenset], **kwargs) -> int:
|
|
|
214
243
|
|
|
215
244
|
@register_checksum_fn(range)
|
|
216
245
|
def checksum_range(x: range, **kwargs) -> int:
|
|
217
|
-
kwargs
|
|
218
|
-
get_fullname(x)
|
|
219
|
-
)
|
|
246
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
220
247
|
return _checksum_iterable([x.start, x.stop, x.step], **kwargs)
|
|
221
248
|
|
|
222
249
|
|
|
@@ -238,48 +265,76 @@ def checksum_method(x: MethodType, **kwargs) -> int:
|
|
|
238
265
|
|
|
239
266
|
@register_checksum_fn(NamedTupleInstance)
|
|
240
267
|
def checksum_namedtuple(x: NamedTupleInstance, **kwargs) -> int:
|
|
241
|
-
kwargs
|
|
242
|
-
get_fullname(x)
|
|
243
|
-
)
|
|
268
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
244
269
|
return checksum_dict(x._asdict(), **kwargs)
|
|
245
270
|
|
|
246
271
|
|
|
247
272
|
@register_checksum_fn(functools.partial)
|
|
248
273
|
def checksum_partial(x: functools.partial, **kwargs) -> int:
|
|
249
|
-
kwargs
|
|
250
|
-
get_fullname(x)
|
|
251
|
-
)
|
|
274
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
252
275
|
return checksum_list_tuple((x.func, x.args, x.keywords), **kwargs)
|
|
253
276
|
|
|
254
277
|
|
|
255
278
|
@register_checksum_fn(re.Pattern)
|
|
256
279
|
def checksum_pattern(x: re.Pattern, **kwargs) -> int:
|
|
257
|
-
kwargs
|
|
258
|
-
get_fullname(x)
|
|
259
|
-
)
|
|
280
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
260
281
|
return checksum_str(str(x), **kwargs)
|
|
261
282
|
|
|
262
283
|
|
|
263
284
|
@register_checksum_fn(Path)
|
|
264
285
|
def checksum_path(x: Path, **kwargs) -> int:
|
|
265
|
-
kwargs
|
|
266
|
-
|
|
267
|
-
)
|
|
286
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
287
|
+
|
|
268
288
|
resolve_path = kwargs.get("resolve_path", False)
|
|
269
289
|
if isinstance(resolve_path, bool) and resolve_path:
|
|
270
290
|
x = x.expanduser().resolve()
|
|
271
|
-
|
|
272
291
|
return checksum_str(str(x), **kwargs)
|
|
273
292
|
|
|
274
293
|
|
|
275
294
|
@register_checksum_fn(slice)
|
|
276
295
|
def checksum_slice(x: slice, **kwargs) -> int:
|
|
277
|
-
kwargs
|
|
278
|
-
get_fullname(x)
|
|
279
|
-
)
|
|
296
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
280
297
|
return checksum_list_tuple((x.start, x.stop, x.step), **kwargs)
|
|
281
298
|
|
|
282
299
|
|
|
300
|
+
@register_checksum_fn(custom_predicate=is_parameterized)
|
|
301
|
+
def checksum_parametrized(x: Any, **kwargs) -> int:
|
|
302
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
303
|
+
return checksum_list_tuple((get_origin(x),) + get_args(x), **kwargs)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@register_checksum_fn(custom_predicate=is_collection_alias)
|
|
307
|
+
def checksum_collection_alias(x: Any, **kwargs) -> int:
|
|
308
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
309
|
+
return checksum_str(x._name, **kwargs)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@register_checksum_fn(custom_predicate=is_special_form)
|
|
313
|
+
def checksum_special_form(x: Any, **kwargs) -> int:
|
|
314
|
+
kwargs = _add_type_checksum_to_accumulator(x, kwargs)
|
|
315
|
+
|
|
316
|
+
if hasattr(x, "_name"):
|
|
317
|
+
name = x._name
|
|
318
|
+
elif hasattr(x, "__name__"):
|
|
319
|
+
name = x.__name__
|
|
320
|
+
else:
|
|
321
|
+
msg = f"Unsupported argument {x=} in checksum_special_form."
|
|
322
|
+
raise ValueError(msg)
|
|
323
|
+
|
|
324
|
+
return checksum_str(name, **kwargs)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if _CHECKSUM_PROTOCOLS:
|
|
328
|
+
|
|
329
|
+
@register_checksum_fn(Mapping, priority=-100)
|
|
330
|
+
def checksum_mapping(x: Mapping, **kwargs) -> int:
|
|
331
|
+
return _checksum_mapping(x, **kwargs)
|
|
332
|
+
|
|
333
|
+
@register_checksum_fn(Iterable, priority=-200)
|
|
334
|
+
def checksum_iterable(x: Iterable, **kwargs) -> int:
|
|
335
|
+
return _checksum_iterable(x, **kwargs)
|
|
336
|
+
|
|
337
|
+
|
|
283
338
|
# Private functions
|
|
284
339
|
def _checksum_bytes_bytearray(x: Union[bytes, bytearray], **kwargs) -> int:
|
|
285
340
|
xint = zlib.crc32(x) % (1 << 32)
|
|
@@ -307,9 +362,17 @@ def _checksum_mapping(x: Mapping, **kwargs) -> int:
|
|
|
307
362
|
|
|
308
363
|
|
|
309
364
|
def _terminate_checksum(x: int, fullname: str, **kwargs) -> int:
|
|
365
|
+
"""Returns checksum for final value + name + accumulator."""
|
|
310
366
|
return x + _cached_checksum_str(fullname) + kwargs.get("accumulator", 0)
|
|
311
367
|
|
|
312
368
|
|
|
369
|
+
def _add_type_checksum_to_accumulator(x: Any, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
370
|
+
kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
|
|
371
|
+
get_fullname(x)
|
|
372
|
+
)
|
|
373
|
+
return kwargs
|
|
374
|
+
|
|
375
|
+
|
|
313
376
|
@lru_cache(maxsize=None)
|
|
314
377
|
def _cached_checksum_str(x: str) -> int:
|
|
315
378
|
return zlib.crc32(x.encode()) % (1 << 32)
|
|
@@ -597,14 +597,70 @@ def recursive_generator(x: Any) -> Generator[Tuple[Any, int, int], None, None]:
|
|
|
597
597
|
return recursive_generator_impl(x, 0, 0)
|
|
598
598
|
|
|
599
599
|
|
|
600
|
+
@overload
|
|
600
601
|
def sorted_dict(
|
|
601
602
|
x: Mapping[K, V],
|
|
602
603
|
/,
|
|
603
604
|
*,
|
|
605
|
+
by: Literal["key"] = "key",
|
|
604
606
|
key: Optional[Callable[[K], Any]] = None,
|
|
605
607
|
reverse: bool = False,
|
|
608
|
+
) -> Dict[K, V]: ...
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@overload
|
|
612
|
+
def sorted_dict(
|
|
613
|
+
x: Mapping[K, V],
|
|
614
|
+
/,
|
|
615
|
+
*,
|
|
616
|
+
by: Literal["value"],
|
|
617
|
+
key: Optional[Callable[[V], Any]] = None,
|
|
618
|
+
reverse: bool = False,
|
|
619
|
+
) -> Dict[K, V]: ...
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@overload
|
|
623
|
+
def sorted_dict(
|
|
624
|
+
x: Mapping[K, V],
|
|
625
|
+
/,
|
|
626
|
+
*,
|
|
627
|
+
by: Literal["item"],
|
|
628
|
+
key: Optional[Callable[[Tuple[K, V]], Any]] = None,
|
|
629
|
+
reverse: bool = False,
|
|
630
|
+
) -> Dict[K, V]: ...
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def sorted_dict(
|
|
634
|
+
x: Mapping[K, V],
|
|
635
|
+
/,
|
|
636
|
+
*,
|
|
637
|
+
by: Literal["key", "value", "item"] = "key",
|
|
638
|
+
key: Optional[Callable[[Any], Any]] = None,
|
|
639
|
+
reverse: bool = False,
|
|
606
640
|
) -> Dict[K, V]:
|
|
607
|
-
|
|
641
|
+
"""Sort a dictionnary by key, value or item."""
|
|
642
|
+
if key is None or by == "item":
|
|
643
|
+
impl_key = key
|
|
644
|
+
|
|
645
|
+
elif by == "key":
|
|
646
|
+
|
|
647
|
+
def by_key_fn(x: Tuple[K, V]) -> Any:
|
|
648
|
+
return key(x[0])
|
|
649
|
+
|
|
650
|
+
impl_key = by_key_fn
|
|
651
|
+
|
|
652
|
+
elif by == "value":
|
|
653
|
+
|
|
654
|
+
def by_value_fn(x: Tuple[K, V]) -> Any:
|
|
655
|
+
return key(x[1])
|
|
656
|
+
|
|
657
|
+
impl_key = by_value_fn
|
|
658
|
+
|
|
659
|
+
else:
|
|
660
|
+
msg = f"Invalid argument {by=}. (expected one of {('key', 'value', 'item')})"
|
|
661
|
+
raise ValueError(msg)
|
|
662
|
+
|
|
663
|
+
return {k: v for k, v in sorted(x.items(), key=impl_key, reverse=reverse)} # type: ignore
|
|
608
664
|
|
|
609
665
|
|
|
610
666
|
def shuffled(
|
|
@@ -362,19 +362,15 @@ def _disk_cache_impl(
|
|
|
362
362
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
363
363
|
# for backward compatibility
|
|
364
364
|
if cache_fname_fmt is None:
|
|
365
|
-
expected = "{fn_name}_{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
DeprecationWarning,
|
|
369
|
-
)
|
|
365
|
+
expected = "{fn_name}_{csum}{suffix}"
|
|
366
|
+
msg = f"Deprecated argument value {cache_fname_fmt=}. (use {expected} instead)"
|
|
367
|
+
warnings.warn(msg, DeprecationWarning)
|
|
370
368
|
cache_fname_fmt = expected
|
|
371
369
|
|
|
372
370
|
if cache_saving_backend is None:
|
|
373
371
|
expected = "auto"
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
DeprecationWarning,
|
|
377
|
-
)
|
|
372
|
+
msg = f"Deprecated argument value {cache_saving_backend=}. (use {expected} instead)"
|
|
373
|
+
warnings.warn(msg, DeprecationWarning)
|
|
378
374
|
cache_saving_backend = expected
|
|
379
375
|
|
|
380
376
|
if cache_saving_backend == "auto":
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
+
import inspect
|
|
5
|
+
from types import CodeType
|
|
4
6
|
from typing import (
|
|
5
7
|
Any,
|
|
6
8
|
Callable,
|
|
@@ -14,7 +16,7 @@ from typing import (
|
|
|
14
16
|
from typing_extensions import ParamSpec
|
|
15
17
|
|
|
16
18
|
from pythonwrench._core import _decorator_factory, return_none # noqa: F401
|
|
17
|
-
from pythonwrench.inspect import
|
|
19
|
+
from pythonwrench.inspect import get_argnames
|
|
18
20
|
from pythonwrench.typing import isinstance_generic
|
|
19
21
|
|
|
20
22
|
T = TypeVar("T")
|
|
@@ -162,3 +164,21 @@ def identity(x: T, **kwargs) -> T:
|
|
|
162
164
|
def repeat_fn(f: Callable[[T], T], n: int) -> Callable[[T], T]:
|
|
163
165
|
"""Creates wrapper which call a function n items."""
|
|
164
166
|
return Compose([f] * n)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]:
|
|
170
|
+
if inspect.isfunction(fn):
|
|
171
|
+
code = fn.__code__
|
|
172
|
+
start = 0
|
|
173
|
+
elif inspect.ismethod(fn):
|
|
174
|
+
code = fn.__code__
|
|
175
|
+
start = 1 # If method, remove 'self' arg
|
|
176
|
+
elif inspect.isclass(fn):
|
|
177
|
+
# If init, remove 'self' arg
|
|
178
|
+
code = fn.__init__.__code__
|
|
179
|
+
start = 1 # If init, remove 'self' arg
|
|
180
|
+
else:
|
|
181
|
+
code = fn.__call__.__code__
|
|
182
|
+
start = 0
|
|
183
|
+
|
|
184
|
+
return code, start
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
4
|
import inspect
|
|
5
|
-
from
|
|
6
|
-
from typing import Any, Callable, List, Tuple, TypeVar, Union, get_args
|
|
5
|
+
from typing import Any, Callable, List, TypeVar, Union, get_args
|
|
7
6
|
|
|
8
7
|
T = TypeVar("T")
|
|
9
8
|
|
|
@@ -68,21 +67,3 @@ def get_fullname(x: Any, *, inst_suffix: str = "(...)") -> str:
|
|
|
68
67
|
name = f"{name}[{argsnames_str}]"
|
|
69
68
|
|
|
70
69
|
return name
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]:
|
|
74
|
-
if inspect.isfunction(fn):
|
|
75
|
-
code = fn.__code__
|
|
76
|
-
start = 0
|
|
77
|
-
elif inspect.ismethod(fn):
|
|
78
|
-
code = fn.__code__
|
|
79
|
-
start = 1 # If method, remove 'self' arg
|
|
80
|
-
elif inspect.isclass(fn):
|
|
81
|
-
# If init, remove 'self' arg
|
|
82
|
-
code = fn.__init__.__code__
|
|
83
|
-
start = 1 # If init, remove 'self' arg
|
|
84
|
-
else:
|
|
85
|
-
code = fn.__call__.__code__
|
|
86
|
-
start = 0
|
|
87
|
-
|
|
88
|
-
return code, start
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Ticker:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
*,
|
|
12
|
+
get_time_fn: Callable[[], float] = time.perf_counter,
|
|
13
|
+
prev_tick: Optional[float] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
if prev_tick is None:
|
|
16
|
+
prev_tick = get_time_fn()
|
|
17
|
+
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._get_time_fn = get_time_fn
|
|
20
|
+
self._prev_tick = prev_tick
|
|
21
|
+
|
|
22
|
+
def tick(self) -> float:
|
|
23
|
+
now = self._get_time_fn()
|
|
24
|
+
duration = now - self._prev_tick
|
|
25
|
+
self._prev_tick = now
|
|
26
|
+
return duration
|
|
27
|
+
|
|
28
|
+
def set_prev_tick(self, prev_tick: Optional[float] = None) -> None:
|
|
29
|
+
if prev_tick is None:
|
|
30
|
+
prev_tick = self._get_time_fn()
|
|
31
|
+
self._prev_tick = prev_tick
|
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING or lazy is None:
|
|
|
16
16
|
is_builtin_number,
|
|
17
17
|
is_builtin_obj,
|
|
18
18
|
is_builtin_scalar,
|
|
19
|
+
is_collection_alias,
|
|
19
20
|
is_dataclass_instance,
|
|
20
21
|
is_iterable_bool,
|
|
21
22
|
is_iterable_bytes_or_list,
|
|
@@ -24,7 +25,9 @@ if TYPE_CHECKING or lazy is None:
|
|
|
24
25
|
is_iterable_integral,
|
|
25
26
|
is_iterable_str,
|
|
26
27
|
is_namedtuple_instance,
|
|
28
|
+
is_parameterized,
|
|
27
29
|
is_sequence_str,
|
|
30
|
+
is_special_form,
|
|
28
31
|
is_typed_dict,
|
|
29
32
|
isinstance_generic,
|
|
30
33
|
)
|
|
@@ -47,6 +50,7 @@ if TYPE_CHECKING or lazy is None:
|
|
|
47
50
|
SupportsGetitemIterLen2,
|
|
48
51
|
SupportsGetitemLen,
|
|
49
52
|
SupportsGetitemLen2,
|
|
53
|
+
SupportsIter,
|
|
50
54
|
SupportsIterLen,
|
|
51
55
|
SupportsLen,
|
|
52
56
|
SupportsMatmul,
|
|
@@ -70,6 +74,7 @@ else:
|
|
|
70
74
|
"is_builtin_number",
|
|
71
75
|
"is_builtin_obj",
|
|
72
76
|
"is_builtin_scalar",
|
|
77
|
+
"is_collection_alias",
|
|
73
78
|
"is_dataclass_instance",
|
|
74
79
|
"is_iterable_bool",
|
|
75
80
|
"is_iterable_bytes_or_list",
|
|
@@ -78,7 +83,9 @@ else:
|
|
|
78
83
|
"is_iterable_integral",
|
|
79
84
|
"is_iterable_str",
|
|
80
85
|
"is_namedtuple_instance",
|
|
86
|
+
"is_parameterized",
|
|
81
87
|
"is_sequence_str",
|
|
88
|
+
"is_special_form",
|
|
82
89
|
"is_typed_dict",
|
|
83
90
|
"isinstance_generic",
|
|
84
91
|
],
|
|
@@ -101,6 +108,7 @@ else:
|
|
|
101
108
|
"SupportsGetitemIterLen2",
|
|
102
109
|
"SupportsGetitemLen",
|
|
103
110
|
"SupportsGetitemLen2",
|
|
111
|
+
"SupportsIter",
|
|
104
112
|
"SupportsIterLen",
|
|
105
113
|
"SupportsLen",
|
|
106
114
|
"SupportsMatmul",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import inspect
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
|
+
import typing
|
|
7
8
|
from numbers import Integral
|
|
8
9
|
from types import FunctionType, MethodType
|
|
9
10
|
from typing import (
|
|
@@ -395,3 +396,99 @@ def is_typed_dict(x: Any) -> TypeGuard[type]:
|
|
|
395
396
|
return x.__class__.__name__ == "_TypedDictMeta"
|
|
396
397
|
else:
|
|
397
398
|
return hasattr(x, "__orig_bases__") and TypedDict in x.__orig_bases__
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def is_parameterized(x: Any) -> bool:
|
|
402
|
+
"""Returns True if object is a parametrized object like `Iterable[int]`, `Mapping[str, float]`, etc."""
|
|
403
|
+
return get_origin(x) is not None and len(get_args(x)) > 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
_COLLECTION_ALIASES = [
|
|
407
|
+
typing.Iterable,
|
|
408
|
+
typing.Iterator,
|
|
409
|
+
typing.Reversible,
|
|
410
|
+
typing.Generator,
|
|
411
|
+
typing.AsyncIterable,
|
|
412
|
+
typing.AsyncIterator,
|
|
413
|
+
typing.AsyncGenerator,
|
|
414
|
+
typing.Collection,
|
|
415
|
+
typing.Container,
|
|
416
|
+
typing.Sized,
|
|
417
|
+
typing.Sequence,
|
|
418
|
+
typing.MutableSequence,
|
|
419
|
+
typing.Set,
|
|
420
|
+
typing.MutableSet,
|
|
421
|
+
typing.Mapping,
|
|
422
|
+
typing.MutableMapping,
|
|
423
|
+
typing.MappingView,
|
|
424
|
+
typing.KeysView,
|
|
425
|
+
typing.ItemsView,
|
|
426
|
+
typing.ValuesView,
|
|
427
|
+
typing.Awaitable,
|
|
428
|
+
typing.Coroutine,
|
|
429
|
+
typing.Callable,
|
|
430
|
+
typing.Hashable,
|
|
431
|
+
typing_extensions.Iterable,
|
|
432
|
+
typing_extensions.Iterator,
|
|
433
|
+
typing_extensions.Reversible,
|
|
434
|
+
typing_extensions.Generator,
|
|
435
|
+
typing_extensions.AsyncIterable,
|
|
436
|
+
typing_extensions.AsyncIterator,
|
|
437
|
+
typing_extensions.AsyncGenerator,
|
|
438
|
+
typing_extensions.Collection,
|
|
439
|
+
typing_extensions.Container,
|
|
440
|
+
typing_extensions.Sized,
|
|
441
|
+
typing_extensions.Sequence,
|
|
442
|
+
typing_extensions.MutableSequence,
|
|
443
|
+
typing_extensions.Set,
|
|
444
|
+
typing_extensions.MutableSet,
|
|
445
|
+
typing_extensions.Mapping,
|
|
446
|
+
typing_extensions.MutableMapping,
|
|
447
|
+
typing_extensions.MappingView,
|
|
448
|
+
typing_extensions.KeysView,
|
|
449
|
+
typing_extensions.ItemsView,
|
|
450
|
+
typing_extensions.ValuesView,
|
|
451
|
+
typing_extensions.Awaitable,
|
|
452
|
+
typing_extensions.Coroutine,
|
|
453
|
+
typing_extensions.Callable,
|
|
454
|
+
typing_extensions.Hashable,
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
_SPECIAL_FORMS = [] + [
|
|
458
|
+
getattr(module, candidate_name, None)
|
|
459
|
+
for module in (typing, typing_extensions)
|
|
460
|
+
for candidate_name in [
|
|
461
|
+
"Any",
|
|
462
|
+
"NoReturn",
|
|
463
|
+
"Union",
|
|
464
|
+
"Optional",
|
|
465
|
+
"Literal",
|
|
466
|
+
"Final",
|
|
467
|
+
"ClassVar",
|
|
468
|
+
"TypeVar",
|
|
469
|
+
"Annotated",
|
|
470
|
+
"Never",
|
|
471
|
+
"Self",
|
|
472
|
+
"Required",
|
|
473
|
+
"NotRequired",
|
|
474
|
+
"TypeGuard",
|
|
475
|
+
"TypeIs",
|
|
476
|
+
"Concatenate",
|
|
477
|
+
"ParamSpec",
|
|
478
|
+
"ParamSpecArgs",
|
|
479
|
+
"ParamSpecKwargs",
|
|
480
|
+
"TypeVarTuple",
|
|
481
|
+
"Unpack",
|
|
482
|
+
]
|
|
483
|
+
if getattr(module, candidate_name, None) is not None
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def is_collection_alias(x: Any) -> bool:
|
|
488
|
+
"""Returns True if object is a non-parameterized collection alias type."""
|
|
489
|
+
return x in _COLLECTION_ALIASES
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def is_special_form(x: Any) -> bool:
|
|
493
|
+
"""Returns True if object is a typing special form like `Any`."""
|
|
494
|
+
return x in _SPECIAL_FORMS
|
|
@@ -5,6 +5,7 @@ from typing import (
|
|
|
5
5
|
Any,
|
|
6
6
|
ClassVar,
|
|
7
7
|
Dict,
|
|
8
|
+
Iterable,
|
|
8
9
|
Iterator,
|
|
9
10
|
List,
|
|
10
11
|
Protocol,
|
|
@@ -41,6 +42,7 @@ T_BuiltinScalar = TypeVar(
|
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
ListOrTuple = Union[List[_T_Item], Tuple[_T_Item, ...]]
|
|
45
|
+
SupportsIter = Iterable
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
@runtime_checkable
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonwrench
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Python library with tools for typing, manipulating collections, and more!
|
|
5
5
|
Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
|
|
6
6
|
Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
|
|
@@ -50,9 +50,10 @@ Classifier: Operating System :: POSIX
|
|
|
50
50
|
Requires-Python: <3.15,>=3.8
|
|
51
51
|
Description-Content-Type: text/markdown
|
|
52
52
|
License-File: LICENSE
|
|
53
|
-
Requires-Dist: lazy-loader>=0.4
|
|
54
53
|
Requires-Dist: typing-extensions>=4.10.0
|
|
55
54
|
Provides-Extra: dev
|
|
55
|
+
Provides-Extra: lazy
|
|
56
|
+
Requires-Dist: lazy-loader>=0.4; extra == "lazy"
|
|
56
57
|
Dynamic: license-file
|
|
57
58
|
|
|
58
59
|
# pythonwrench
|
|
@@ -218,6 +219,9 @@ Easely converts common python structures like list of dicts to dict of lists :
|
|
|
218
219
|
... True
|
|
219
220
|
```
|
|
220
221
|
|
|
222
|
+
## Lazy loading
|
|
223
|
+
To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster.
|
|
224
|
+
|
|
221
225
|
## Contact
|
|
222
226
|
Maintainer:
|
|
223
227
|
- [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
setup.py
|
|
5
|
+
docs/requirements.txt
|
|
5
6
|
src/pythonwrench/__init__.py
|
|
6
7
|
src/pythonwrench/__main__.py
|
|
7
8
|
src/pythonwrench/_core.py
|
|
@@ -30,6 +31,7 @@ src/pythonwrench/pickle.py
|
|
|
30
31
|
src/pythonwrench/random.py
|
|
31
32
|
src/pythonwrench/re.py
|
|
32
33
|
src/pythonwrench/semver.py
|
|
34
|
+
src/pythonwrench/time.py
|
|
33
35
|
src/pythonwrench/warnings.py
|
|
34
36
|
src/pythonwrench.egg-info/PKG-INFO
|
|
35
37
|
src/pythonwrench.egg-info/SOURCES.txt
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import unittest
|
|
5
5
|
from collections import Counter
|
|
6
|
+
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from unittest import TestCase
|
|
8
9
|
|
|
@@ -14,8 +15,17 @@ class CustomClass:
|
|
|
14
15
|
b: str = ""
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@dataclass
|
|
19
|
+
class CustomDataclass:
|
|
20
|
+
counts: Counter = field(default_factory=Counter)
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
class TestCast(TestCase):
|
|
18
24
|
def test_example_1(self) -> None:
|
|
25
|
+
@register_as_builtin_fn(CustomClass)
|
|
26
|
+
def customclass_to_builtin(x: CustomClass) -> dict:
|
|
27
|
+
return {"a": x.a, "b": x.b, "added_prop": None}
|
|
28
|
+
|
|
19
29
|
examples = [
|
|
20
30
|
("a", "a"),
|
|
21
31
|
((), []),
|
|
@@ -25,12 +35,10 @@ class TestCast(TestCase):
|
|
|
25
35
|
(CustomClass(), {"a": 0, "b": "", "added_prop": None}),
|
|
26
36
|
({"a": (1, 2)}, {"a": [1, 2]}),
|
|
27
37
|
(Counter(a=2, b=1, c=3), {"a": 2, "b": 1, "c": 3}),
|
|
38
|
+
(Counter(["a", "b", "a"]), {"a": 2, "b": 1}),
|
|
39
|
+
(CustomDataclass(Counter(["a", "b", "a"])), {"counts": {"a": 2, "b": 1}}),
|
|
28
40
|
]
|
|
29
41
|
|
|
30
|
-
@register_as_builtin_fn(CustomClass)
|
|
31
|
-
def customclass_to_builtin(x: CustomClass) -> dict:
|
|
32
|
-
return {"a": x.a, "b": x.b, "added_prop": None}
|
|
33
|
-
|
|
34
42
|
for x, expected in examples:
|
|
35
43
|
result = as_builtin(x)
|
|
36
44
|
assert result == expected
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
import math
|
|
5
5
|
import random
|
|
6
6
|
import unittest
|
|
7
|
+
from typing import Any, Iterable, Mapping, Tuple
|
|
7
8
|
from unittest import TestCase
|
|
8
9
|
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
9
12
|
from pythonwrench.checksum import checksum_any
|
|
10
13
|
from pythonwrench.collections import all_ne
|
|
11
14
|
from pythonwrench.math import nextafter
|
|
@@ -74,6 +77,23 @@ class TestChecksum(TestCase):
|
|
|
74
77
|
assert Color.RED == Color.ROUGE
|
|
75
78
|
assert checksum_any(Color.RED) == checksum_any(Color.ROUGE)
|
|
76
79
|
|
|
80
|
+
def test_typing_classes(self) -> None:
|
|
81
|
+
x = [
|
|
82
|
+
Any,
|
|
83
|
+
Iterable,
|
|
84
|
+
Iterable[int],
|
|
85
|
+
Annotated,
|
|
86
|
+
Annotated[int, "annotation"],
|
|
87
|
+
Tuple[int, ...],
|
|
88
|
+
Tuple[int],
|
|
89
|
+
Tuple[int, int],
|
|
90
|
+
tuple,
|
|
91
|
+
Mapping,
|
|
92
|
+
Mapping[int, int],
|
|
93
|
+
]
|
|
94
|
+
csums = [checksum_any(xi) for xi in x]
|
|
95
|
+
assert all_ne(csums), f"{csums=}"
|
|
96
|
+
|
|
77
97
|
|
|
78
98
|
if __name__ == "__main__":
|
|
79
99
|
unittest.main()
|
|
@@ -25,6 +25,7 @@ from pythonwrench.collections import (
|
|
|
25
25
|
recursive_generator,
|
|
26
26
|
reduce_add,
|
|
27
27
|
shuffled,
|
|
28
|
+
sorted_dict,
|
|
28
29
|
unflat_dict_of_dict,
|
|
29
30
|
unflat_list_of_list,
|
|
30
31
|
union_dicts,
|
|
@@ -325,5 +326,18 @@ class TestReduce(TestCase):
|
|
|
325
326
|
assert reduce_add(args, args, start=1)
|
|
326
327
|
|
|
327
328
|
|
|
329
|
+
class TestSortedDict(TestCase):
|
|
330
|
+
def test_example_1(self) -> None:
|
|
331
|
+
x = {"b": 2, "a": 3, "c": 1}
|
|
332
|
+
result = sorted_dict(x)
|
|
333
|
+
assert result == {"a": 3, "b": 2, "c": 1}
|
|
334
|
+
|
|
335
|
+
result = sorted_dict(x, by="value")
|
|
336
|
+
assert result == {"c": 1, "b": 2, "a": 3}
|
|
337
|
+
|
|
338
|
+
result = sorted_dict(x, reverse=True, key=lambda x: x % 3, by="value")
|
|
339
|
+
assert result == {"a": 3, "c": 1, "b": 2}
|
|
340
|
+
|
|
341
|
+
|
|
328
342
|
if __name__ == "__main__":
|
|
329
343
|
unittest.main()
|
|
@@ -22,14 +22,18 @@ from typing import (
|
|
|
22
22
|
)
|
|
23
23
|
from unittest import TestCase
|
|
24
24
|
|
|
25
|
+
import typing_extensions
|
|
25
26
|
from typing_extensions import NotRequired
|
|
26
27
|
|
|
27
28
|
from pythonwrench.typing import (
|
|
28
29
|
NoneType,
|
|
29
30
|
check_args_types,
|
|
31
|
+
is_collection_alias,
|
|
30
32
|
is_dataclass_instance,
|
|
31
33
|
is_iterable_str,
|
|
32
34
|
is_namedtuple_instance,
|
|
35
|
+
is_parameterized,
|
|
36
|
+
is_special_form,
|
|
33
37
|
is_typed_dict,
|
|
34
38
|
isinstance_generic,
|
|
35
39
|
)
|
|
@@ -101,6 +105,25 @@ class TestChecks(TestCase):
|
|
|
101
105
|
assert not is_dataclass_instance(nt1)
|
|
102
106
|
assert not is_dataclass_instance(nt2)
|
|
103
107
|
|
|
108
|
+
def test_is_parameterized(self) -> None:
|
|
109
|
+
assert is_parameterized(Iterable[int])
|
|
110
|
+
assert is_parameterized(Tuple[int, ...])
|
|
111
|
+
assert not is_parameterized(Iterable)
|
|
112
|
+
assert not is_parameterized(Tuple)
|
|
113
|
+
|
|
114
|
+
def test_is_collection_alias(self) -> None:
|
|
115
|
+
assert not is_collection_alias(Iterable[int])
|
|
116
|
+
assert not is_collection_alias(Tuple[int, ...])
|
|
117
|
+
assert is_collection_alias(Iterable)
|
|
118
|
+
assert not is_collection_alias(Tuple)
|
|
119
|
+
assert not is_collection_alias(list)
|
|
120
|
+
|
|
121
|
+
def test_is_special_form(self) -> None:
|
|
122
|
+
assert not is_special_form(Iterable[int])
|
|
123
|
+
assert not is_special_form(Tuple[int, ...])
|
|
124
|
+
assert is_special_form(Any)
|
|
125
|
+
assert is_special_form(typing_extensions.Any)
|
|
126
|
+
|
|
104
127
|
|
|
105
128
|
class TestIsInstanceGuard(TestCase):
|
|
106
129
|
def test_docstring_examples(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|