pythonwrench 0.6.7__tar.gz → 0.6.8__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.6.7 → pythonwrench-0.6.8}/CHANGELOG.md +11 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/CITATION.cff +2 -2
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/PKG-INFO +1 -1
- pythonwrench-0.6.8/pythonwrench.code-workspace +13 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/__init__.py +14 -3
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/_core.py +135 -1
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/dataclass_.py +137 -27
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/cast.py +7 -1
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/collections.py +26 -15
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/reducers.py +2 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/functools.py +8 -29
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/importlib.py +1 -1
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/os.py +42 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/re.py +70 -3
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/semver.py +2 -2
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/__init__.py +2 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/checks.py +135 -68
- pythonwrench-0.6.8/src/pythonwrench/warnings.py +8 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_argparse.py +81 -5
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_collections.py +9 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_typing.py +52 -20
- pythonwrench-0.6.7/src/pythonwrench/warnings.py +0 -118
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.github/workflows/test.yaml +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.gitignore +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.pre-commit-config.yaml +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.readthedocs.yaml +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/LICENSE +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/README.md +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/Makefile +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/conf.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/index.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/make.bat +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/modules.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.abc.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.dataclass_.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.parsers.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.cast.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.checksum.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.collections.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.prop.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.reducers.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.concurrent.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.csv.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.dataclasses.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.datetime.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.difflib.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.disk_cache.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.enum.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.functools.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.hashlib.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.importlib.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.inspect.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.json.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.jsonl.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.logging.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.math.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.os.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.pickle.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.random.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.re.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.semver.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.csv.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.json.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.jsonl.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.pickle.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.time.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.checks.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.classes.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.warnings.rst +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/update.bash +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/pyproject.toml +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/setup.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/__main__.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/abc.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/__init__.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/parsers.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/checksum.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/__init__.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/prop.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/concurrent.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/csv.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/dataclasses.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/datetime.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/difflib.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/disk_cache.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/info.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/tree.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/enum.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/hashlib.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/inspect.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/json.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/jsonl.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/logging.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/math.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/pickle.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/random.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/__init__.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/_core.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/csv.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/json.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/jsonl.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/pickle.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/time.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/classes.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_abc.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_cast.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_checksum.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_core.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_csv.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_dataclasses.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_difflib.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_disk_cache.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_entrypoints.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_enum.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_functools.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_hashlib.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_importlib.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_inspect.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_json.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_jsonl.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_logging.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_math.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_os.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_random.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_readme.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_semver.py +0 -0
- {pythonwrench-0.6.7 → pythonwrench-0.6.8}/uv.lock +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.8] 2026-10-26
|
|
6
|
+
### Added
|
|
7
|
+
- `remove_children` function to remove all content of a directory.
|
|
8
|
+
- `filter_dict_with_patterns` function.
|
|
9
|
+
- `parse_args_using_dataclass` and `add_dataclass_fields_to_parser` functions now supports nested dataclasses with subparsers.
|
|
10
|
+
|
|
11
|
+
### Modified
|
|
12
|
+
- `Version` is now converted to str by default in `as_builtin`.
|
|
13
|
+
- `isinstance_generic` now raises explicit exception when forwarded ref is given.
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
## [0.6.7] 2026-09-07
|
|
6
17
|
### Added
|
|
7
18
|
- `on_error` and `default` arguments to `load_json`, `load_pickle`, `load_jsonl` functions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pythonwrench
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
4
4
|
Summary: Python library with tools for typing, manipulating collections, and more!
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/pythonwrench/
|
|
6
6
|
Project-URL: Documentation, https://pythonwrench.readthedocs.io/
|
|
@@ -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.6.
|
|
12
|
+
__version__ = "0.6.8"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
@@ -157,12 +157,14 @@ if TYPE_CHECKING or lazy is None:
|
|
|
157
157
|
setup_logging_verbose,
|
|
158
158
|
)
|
|
159
159
|
from .math import argmax, argmin, argsort, clamp, clip
|
|
160
|
-
from .os import get_num_cpus_available, safe_rmdir, tree_iter
|
|
160
|
+
from .os import get_num_cpus_available, remove_children, safe_rmdir, tree_iter
|
|
161
161
|
from .random import randstr
|
|
162
162
|
from .re import (
|
|
163
163
|
PatternLike,
|
|
164
164
|
PatternListLike,
|
|
165
165
|
compile_patterns,
|
|
166
|
+
filter_dict_with_patterns,
|
|
167
|
+
filter_iter_with_patterns,
|
|
166
168
|
filter_with_patterns,
|
|
167
169
|
find_patterns,
|
|
168
170
|
get_key_fn,
|
|
@@ -245,6 +247,7 @@ if TYPE_CHECKING or lazy is None:
|
|
|
245
247
|
is_special_form,
|
|
246
248
|
is_typed_dict,
|
|
247
249
|
isinstance_generic,
|
|
250
|
+
isinstance_parametrized,
|
|
248
251
|
)
|
|
249
252
|
from .warnings import deprecated_alias, deprecated_function, warn_once
|
|
250
253
|
|
|
@@ -414,13 +417,20 @@ else:
|
|
|
414
417
|
"setup_logging_verbose",
|
|
415
418
|
],
|
|
416
419
|
"math": ["argmax", "argmin", "argsort", "clamp", "clip"],
|
|
417
|
-
"os": [
|
|
420
|
+
"os": [
|
|
421
|
+
"get_num_cpus_available",
|
|
422
|
+
"remove_children",
|
|
423
|
+
"safe_rmdir",
|
|
424
|
+
"tree_iter",
|
|
425
|
+
],
|
|
418
426
|
"random": ["randstr"],
|
|
419
427
|
"re": [
|
|
420
428
|
"PatternLike",
|
|
421
429
|
"PatternListLike",
|
|
422
430
|
"compile_patterns",
|
|
423
431
|
"filter_with_patterns",
|
|
432
|
+
"filter_iter_with_patterns",
|
|
433
|
+
"filter_dict_with_patterns",
|
|
424
434
|
"find_patterns",
|
|
425
435
|
"get_key_fn",
|
|
426
436
|
"match_patterns",
|
|
@@ -476,6 +486,7 @@ else:
|
|
|
476
486
|
"is_special_form",
|
|
477
487
|
"is_typed_dict",
|
|
478
488
|
"isinstance_generic",
|
|
489
|
+
"isinstance_parametrized",
|
|
479
490
|
],
|
|
480
491
|
"warnings": ["deprecated_alias", "deprecated_function", "warn_once"],
|
|
481
492
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import warnings
|
|
5
|
+
from functools import lru_cache, partial, wraps
|
|
5
6
|
from typing import (
|
|
6
7
|
Any,
|
|
7
8
|
Callable,
|
|
@@ -11,8 +12,10 @@ from typing import (
|
|
|
11
12
|
Optional,
|
|
12
13
|
Protocol,
|
|
13
14
|
Tuple,
|
|
15
|
+
Type,
|
|
14
16
|
Union,
|
|
15
17
|
get_args,
|
|
18
|
+
overload,
|
|
16
19
|
runtime_checkable,
|
|
17
20
|
)
|
|
18
21
|
|
|
@@ -21,6 +24,7 @@ from typing_extensions import ParamSpec, TypeVar
|
|
|
21
24
|
P = ParamSpec("P")
|
|
22
25
|
T = TypeVar("T", covariant=True)
|
|
23
26
|
U = TypeVar("U", covariant=True)
|
|
27
|
+
|
|
24
28
|
T_Output = TypeVar("T_Output", default=Any)
|
|
25
29
|
T_PredicateArg = TypeVar("T_PredicateArg", contravariant=True, default=Any)
|
|
26
30
|
T_Function = TypeVar("T_Function", bound=Callable[..., Any])
|
|
@@ -41,6 +45,31 @@ def return_none(*args, **kwargs) -> None:
|
|
|
41
45
|
return None
|
|
42
46
|
|
|
43
47
|
|
|
48
|
+
def function_alias(
|
|
49
|
+
alternative: T_Function,
|
|
50
|
+
*,
|
|
51
|
+
pre_fn: Optional[Callable[..., Any]] = None,
|
|
52
|
+
post_fn: Optional[Callable[..., Any]] = None,
|
|
53
|
+
) -> Callable[..., T_Function]:
|
|
54
|
+
"""Decorator to wrap function aliases.
|
|
55
|
+
|
|
56
|
+
Unlike setting directly an alias, this preserve the nature of the alias for language server.
|
|
57
|
+
|
|
58
|
+
Example
|
|
59
|
+
-------
|
|
60
|
+
>>> def f(a: int, b: str) -> str:
|
|
61
|
+
>>> return a * b
|
|
62
|
+
>>> @function_alias(f)
|
|
63
|
+
>>> def g(*args, **kwargs): ...
|
|
64
|
+
>>> f(2, "a")
|
|
65
|
+
... "aa"
|
|
66
|
+
>>> g(3, "b") # calls function f() internally.
|
|
67
|
+
... "bbb"
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return _decorator_factory(alternative, pre_fn=pre_fn, post_fn=post_fn)
|
|
71
|
+
|
|
72
|
+
|
|
44
73
|
def _decorator_factory(
|
|
45
74
|
inner_fn: Optional[T_Function],
|
|
46
75
|
*,
|
|
@@ -190,3 +219,108 @@ def _get_insertion_index(lst: list, priority: int, priority_key: int) -> int:
|
|
|
190
219
|
if priority >= other_priority:
|
|
191
220
|
return i
|
|
192
221
|
return len(lst)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@overload
|
|
225
|
+
def warn_once(
|
|
226
|
+
message: str,
|
|
227
|
+
category: Optional[Type[Warning]] = None,
|
|
228
|
+
stacklevel: int = 1,
|
|
229
|
+
source: Any = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Warn message once using warnings module."""
|
|
232
|
+
...
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@overload
|
|
236
|
+
def warn_once(
|
|
237
|
+
message: Warning,
|
|
238
|
+
category: Any = None,
|
|
239
|
+
stacklevel: int = 1,
|
|
240
|
+
source: Any = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Warn message once using warnings module."""
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@lru_cache(maxsize=None)
|
|
247
|
+
def warn_once(
|
|
248
|
+
message: Union[str, Warning],
|
|
249
|
+
category: Optional[Type[Warning]] = None,
|
|
250
|
+
stacklevel: int = 1,
|
|
251
|
+
source: Any = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Warn message once using warnings module."""
|
|
254
|
+
warnings.warn(message, category, stacklevel, source)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def deprecated_alias(
|
|
258
|
+
alternative: T_Function,
|
|
259
|
+
msg_fmt: str = "Deprecated call to '{fn_name}', use '{alternative_name}' instead.",
|
|
260
|
+
warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
|
|
261
|
+
*,
|
|
262
|
+
pre_fn: Optional[Callable[..., Any]] = None,
|
|
263
|
+
post_fn: Optional[Callable[..., Any]] = None,
|
|
264
|
+
) -> Callable[..., T_Function]:
|
|
265
|
+
"""Decorator to wrap deprecated function aliases."""
|
|
266
|
+
alternative_name = alternative.__name__ if alternative is not None else "None"
|
|
267
|
+
if pre_fn is None:
|
|
268
|
+
pre_fn = return_none
|
|
269
|
+
|
|
270
|
+
def inner_pre_fn(fn, *args, **kwargs) -> None:
|
|
271
|
+
"""Perform the inner pre fn operation."""
|
|
272
|
+
msg = msg_fmt.format(fn_name=fn.__name__, alternative_name=alternative_name)
|
|
273
|
+
warn_fn(msg)
|
|
274
|
+
pre_fn(fn, *args, **kwargs)
|
|
275
|
+
|
|
276
|
+
return _decorator_factory(alternative, pre_fn=inner_pre_fn, post_fn=post_fn)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@overload
|
|
280
|
+
def deprecated_function(
|
|
281
|
+
fn: None = None,
|
|
282
|
+
/,
|
|
283
|
+
*,
|
|
284
|
+
msg_fmt: str = "Deprecated call to '{fn_name}'.",
|
|
285
|
+
warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
|
|
286
|
+
) -> Callable[[T_Function], T_Function]:
|
|
287
|
+
"""Perform the deprecated function operation."""
|
|
288
|
+
...
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@overload
|
|
292
|
+
def deprecated_function(
|
|
293
|
+
fn: T_Function,
|
|
294
|
+
/,
|
|
295
|
+
*,
|
|
296
|
+
msg_fmt: str = "Deprecated call to '{fn_name}'.",
|
|
297
|
+
warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
|
|
298
|
+
) -> T_Function:
|
|
299
|
+
"""Perform the deprecated function operation."""
|
|
300
|
+
...
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def deprecated_function(
|
|
304
|
+
fn: Optional[T_Function] = None,
|
|
305
|
+
/,
|
|
306
|
+
*,
|
|
307
|
+
msg_fmt: str = "Deprecated call to '{fn_name}'.",
|
|
308
|
+
warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
|
|
309
|
+
pre_fn: Optional[Callable[..., Any]] = None,
|
|
310
|
+
post_fn: Optional[Callable[..., Any]] = None,
|
|
311
|
+
) -> Union[Callable[[T_Function], T_Function], T_Function]:
|
|
312
|
+
"""Decorator to wrap deprecated functions."""
|
|
313
|
+
if pre_fn is None:
|
|
314
|
+
pre_fn = return_none
|
|
315
|
+
|
|
316
|
+
def inner_pre_fn(fn, *args, **kwargs):
|
|
317
|
+
"""Perform the inner pre fn operation."""
|
|
318
|
+
msg = msg_fmt.format(fn_name=fn.__qualname__)
|
|
319
|
+
warn_fn(msg)
|
|
320
|
+
pre_fn(fn, *args, **kwargs)
|
|
321
|
+
|
|
322
|
+
decorator = _decorator_factory(None, pre_fn=inner_pre_fn, post_fn=post_fn)
|
|
323
|
+
if fn is None:
|
|
324
|
+
return decorator
|
|
325
|
+
else:
|
|
326
|
+
return decorator(fn)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
+
import copy
|
|
4
5
|
from argparse import ArgumentParser, Namespace
|
|
5
6
|
from dataclasses import MISSING, fields
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
9
|
Dict,
|
|
9
10
|
Iterable,
|
|
11
|
+
List,
|
|
10
12
|
Literal,
|
|
11
13
|
Optional,
|
|
12
14
|
Tuple,
|
|
@@ -21,9 +23,11 @@ from pythonwrench.argparse.parsers import (
|
|
|
21
23
|
ListParsing,
|
|
22
24
|
_is_iterable_type_like,
|
|
23
25
|
_is_literal_type,
|
|
26
|
+
_is_union_type,
|
|
24
27
|
_search_parse_fn,
|
|
25
28
|
)
|
|
26
29
|
from pythonwrench.functools import filter_and_call
|
|
30
|
+
from pythonwrench.typing.checks import is_dataclass_type
|
|
27
31
|
from pythonwrench.typing.classes import (
|
|
28
32
|
Dataclass,
|
|
29
33
|
DataclassInstance,
|
|
@@ -165,8 +169,8 @@ def parse_args_using_dataclass(
|
|
|
165
169
|
|
|
166
170
|
|
|
167
171
|
def parse_args_using_dataclass(
|
|
168
|
-
dataclass_type:
|
|
169
|
-
*dataclass_types:
|
|
172
|
+
dataclass_type: Dataclass,
|
|
173
|
+
*dataclass_types: Dataclass,
|
|
170
174
|
args: Optional[Iterable[str]] = None,
|
|
171
175
|
parser: Optional[ArgumentParser] = None,
|
|
172
176
|
list_parsing: ListParsing = "argparse",
|
|
@@ -198,25 +202,21 @@ def parse_args_using_dataclass(
|
|
|
198
202
|
)
|
|
199
203
|
assert parser is not None
|
|
200
204
|
|
|
201
|
-
parsed,
|
|
202
|
-
if len(
|
|
203
|
-
msg = f"Found {len(
|
|
205
|
+
parsed, other_args = parser.parse_known_args(args)
|
|
206
|
+
if len(other_args) > 0:
|
|
207
|
+
msg = f"Found {len(other_args)} unknown arguments: {other_args}. (with source {args=})"
|
|
204
208
|
raise ValueError(msg)
|
|
205
209
|
|
|
206
210
|
if return_single_namespace:
|
|
207
211
|
return parsed
|
|
208
212
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
_fill_all_arguments=True,
|
|
217
|
-
**parsed.__dict__,
|
|
218
|
-
)
|
|
219
|
-
dataclass_insts.append(instance)
|
|
213
|
+
accept_more_args = (init_parser is not None) or (len(dataclass_types) != 1)
|
|
214
|
+
# breakpoint() # TODO
|
|
215
|
+
dataclass_insts = _instantiate_dataclasses(
|
|
216
|
+
dataclass_types,
|
|
217
|
+
parsed,
|
|
218
|
+
accept_more_args=accept_more_args,
|
|
219
|
+
)
|
|
220
220
|
|
|
221
221
|
if len(dataclass_insts) == 1:
|
|
222
222
|
return dataclass_insts[0]
|
|
@@ -224,6 +224,79 @@ def parse_args_using_dataclass(
|
|
|
224
224
|
return tuple(dataclass_insts)
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
def _instantiate_dataclasses(
|
|
228
|
+
dcls_types: Tuple[Dataclass, ...],
|
|
229
|
+
parsed: Union[Namespace, Dict[str, Any]],
|
|
230
|
+
accept_more_args: bool,
|
|
231
|
+
) -> List[DataclassInstance]:
|
|
232
|
+
if isinstance(parsed, Namespace):
|
|
233
|
+
parsed = parsed.__dict__
|
|
234
|
+
|
|
235
|
+
subparser_fields = {
|
|
236
|
+
field.name
|
|
237
|
+
for dcls_type in dcls_types
|
|
238
|
+
for field in fields(dcls_type)
|
|
239
|
+
if _is_dataclass_or_union_of_dataclasses(field.type)
|
|
240
|
+
}
|
|
241
|
+
base_kwds = {
|
|
242
|
+
k: v for k, v in parsed.items() if "." not in k and k not in subparser_fields
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
dcls_insts = []
|
|
246
|
+
|
|
247
|
+
for dcls_type_i in dcls_types:
|
|
248
|
+
sub_dclasses = {
|
|
249
|
+
field.name: field.type
|
|
250
|
+
for field in fields(dcls_type_i)
|
|
251
|
+
if _is_dataclass_or_union_of_dataclasses(field.type)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
kwds_i = copy.copy(base_kwds)
|
|
255
|
+
for name, dcls_union in sub_dclasses.items():
|
|
256
|
+
selected = parsed.get(name)
|
|
257
|
+
if selected is None:
|
|
258
|
+
# note: an optional subparser that was not selected is None,
|
|
259
|
+
# it must not replace the dataclass field's default/default_factory.
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
if _is_union_type(dcls_union):
|
|
263
|
+
dclasses = get_args(dcls_union)
|
|
264
|
+
dclasses_names = {dcls.__name__: dcls for dcls in dclasses}
|
|
265
|
+
try:
|
|
266
|
+
dclass_type = dclasses_names[selected]
|
|
267
|
+
except KeyError as err:
|
|
268
|
+
msg = f"Invalid subparser {selected!r} for field {name!r}."
|
|
269
|
+
raise ValueError(msg) from err
|
|
270
|
+
else:
|
|
271
|
+
dclass_type = dcls_union
|
|
272
|
+
|
|
273
|
+
field_prefix = f"{name}."
|
|
274
|
+
field_kwds = {
|
|
275
|
+
key[len(field_prefix) :]: value
|
|
276
|
+
for key, value in parsed.items()
|
|
277
|
+
if key.startswith(field_prefix)
|
|
278
|
+
}
|
|
279
|
+
dclass_inst = _instantiate_dataclasses(
|
|
280
|
+
(dclass_type,), # type: ignore
|
|
281
|
+
parsed=field_kwds,
|
|
282
|
+
accept_more_args=True,
|
|
283
|
+
)[0]
|
|
284
|
+
kwds_i[name] = dclass_inst
|
|
285
|
+
|
|
286
|
+
if accept_more_args:
|
|
287
|
+
instance = filter_and_call(
|
|
288
|
+
dcls_type_i,
|
|
289
|
+
_fill_all_arguments=True,
|
|
290
|
+
**kwds_i,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
instance = dcls_type_i(**kwds_i)
|
|
294
|
+
|
|
295
|
+
dcls_insts.append(instance)
|
|
296
|
+
|
|
297
|
+
return dcls_insts
|
|
298
|
+
|
|
299
|
+
|
|
227
300
|
def add_dataclass_fields_to_parser(
|
|
228
301
|
dataclass_type: Type[T_DataclassInstance],
|
|
229
302
|
*,
|
|
@@ -232,12 +305,13 @@ def add_dataclass_fields_to_parser(
|
|
|
232
305
|
bool_action: BoolAction = "store",
|
|
233
306
|
add_dashed_arg: bool = True,
|
|
234
307
|
posonly_for_required: bool = False,
|
|
308
|
+
argprefix: str = "",
|
|
235
309
|
) -> ArgumentParser:
|
|
236
310
|
"""Perform the add dataclass fields to parser operation."""
|
|
237
311
|
if posonly_for_required:
|
|
238
|
-
prefix =
|
|
312
|
+
prefix = argprefix
|
|
239
313
|
else:
|
|
240
|
-
prefix = "--"
|
|
314
|
+
prefix = f"--{argprefix}"
|
|
241
315
|
|
|
242
316
|
if parser is None:
|
|
243
317
|
parser = ArgumentParser()
|
|
@@ -258,19 +332,45 @@ def add_dataclass_fields_to_parser(
|
|
|
258
332
|
elif field.default is not MISSING:
|
|
259
333
|
kwds["default"] = field.default
|
|
260
334
|
elif field.default_factory is not MISSING:
|
|
261
|
-
kwds["default"] = field.default_factory()
|
|
335
|
+
kwds["default"] = field.default_factory()
|
|
262
336
|
else:
|
|
263
|
-
msg = f"Invalid field {field.name}: found values for default and default_factory."
|
|
337
|
+
msg = f"Invalid field '{field.name}': found values for default and default_factory."
|
|
264
338
|
raise ValueError(msg)
|
|
265
339
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
340
|
+
if is_dataclass_type(field.type):
|
|
341
|
+
subclasses = (field.type,)
|
|
342
|
+
elif _is_union_of_dataclasses(field.type):
|
|
343
|
+
subclasses = get_args(field.type)
|
|
344
|
+
else:
|
|
345
|
+
subclasses = None
|
|
346
|
+
|
|
347
|
+
if subclasses is not None:
|
|
348
|
+
subparsers = parser.add_subparsers(
|
|
349
|
+
dest=f"{argprefix}{field.name}",
|
|
350
|
+
required=kwds.get("required", False),
|
|
351
|
+
)
|
|
352
|
+
for subcls in subclasses:
|
|
353
|
+
subparser = subparsers.add_parser(subcls.__name__)
|
|
354
|
+
add_dataclass_fields_to_parser(
|
|
355
|
+
subcls,
|
|
356
|
+
parser=subparser,
|
|
357
|
+
list_parsing=list_parsing,
|
|
358
|
+
bool_action=bool_action,
|
|
359
|
+
add_dashed_arg=add_dashed_arg,
|
|
360
|
+
posonly_for_required=posonly_for_required,
|
|
361
|
+
argprefix=f"{argprefix}{field.name}.",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
else:
|
|
365
|
+
try:
|
|
366
|
+
inner_kwds = _get_kwds_for_type(field.type, list_parsing, bool_action)
|
|
367
|
+
except (ValueError, TypeError, RuntimeError) as err:
|
|
368
|
+
msg = f"Invalid field '{field.name}': field type '{field.type}' is not supported."
|
|
369
|
+
raise type(err)(msg) from err
|
|
370
|
+
|
|
371
|
+
kwds.update(inner_kwds)
|
|
271
372
|
|
|
272
|
-
|
|
273
|
-
parser.add_argument(*posargs, **kwds)
|
|
373
|
+
parser.add_argument(*posargs, **kwds)
|
|
274
374
|
|
|
275
375
|
return parser
|
|
276
376
|
|
|
@@ -314,6 +414,16 @@ def _get_kwds_for_type(
|
|
|
314
414
|
raise TypeError(msg)
|
|
315
415
|
|
|
316
416
|
|
|
417
|
+
def _is_dataclass_or_union_of_dataclasses(x: Any) -> bool:
|
|
418
|
+
return is_dataclass_type(x) or _is_union_of_dataclasses(x)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _is_union_of_dataclasses(x: Any) -> bool:
|
|
422
|
+
return _is_union_type(x) and all(
|
|
423
|
+
is_dataclass_type(subcls) for subcls in get_args(x)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
317
427
|
# ALIASES
|
|
318
428
|
@deprecated_alias(add_dataclass_fields_to_parser)
|
|
319
429
|
def new_parser_from_dataclass(*args, **kwargs):
|
|
@@ -22,6 +22,7 @@ from typing import (
|
|
|
22
22
|
|
|
23
23
|
from pythonwrench._core import ClassOrTuple, Predicate, _FunctionRegistry
|
|
24
24
|
from pythonwrench.functools import identity
|
|
25
|
+
from pythonwrench.semver import Version
|
|
25
26
|
from pythonwrench.typing import (
|
|
26
27
|
DataclassInstance,
|
|
27
28
|
NamedTupleInstance,
|
|
@@ -136,11 +137,16 @@ def _dataclass_to_builtin(x: DataclassInstance, **kwargs) -> Any:
|
|
|
136
137
|
|
|
137
138
|
|
|
138
139
|
@register_as_builtin_fn(NamedTupleInstance)
|
|
139
|
-
def _namedtuple_to_builtin(x: NamedTupleInstance, **kwargs) -> Any:
|
|
140
|
+
def _namedtuple_to_builtin(x: NamedTupleInstance, **kwargs) -> Dict[str, Any]:
|
|
140
141
|
"""Perform the namedtuple to builtin operation."""
|
|
141
142
|
return as_builtin(x._asdict(), **kwargs)
|
|
142
143
|
|
|
143
144
|
|
|
145
|
+
@register_as_builtin_fn(Version)
|
|
146
|
+
def _version_to_str(x: Version, **kwargs) -> str:
|
|
147
|
+
return x.to_str()
|
|
148
|
+
|
|
149
|
+
|
|
144
150
|
@register_as_builtin_fn(Mapping, priority=-100)
|
|
145
151
|
def _mapping_to_builtin(x: Mapping, **kwargs) -> Any:
|
|
146
152
|
"""Perform the mapping to builtin operation."""
|
|
@@ -47,6 +47,8 @@ Y = TypeVar("Y", covariant=True)
|
|
|
47
47
|
|
|
48
48
|
KeyMode = Literal["intersect", "same", "union"]
|
|
49
49
|
Order = Literal["left", "right"]
|
|
50
|
+
DictByKV = Literal["key", "value"]
|
|
51
|
+
DictByKVI = Literal["key", "value", "item"]
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
class SizedGenerator(Generic[T]):
|
|
@@ -304,9 +306,8 @@ def find(
|
|
|
304
306
|
|
|
305
307
|
match_fn = revert(match_fn)
|
|
306
308
|
else:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
309
|
+
msg = f"Invalid argument {order=}. (expected one of {get_args(Order)})"
|
|
310
|
+
raise ValueError(msg)
|
|
310
311
|
|
|
311
312
|
for i, xi in enumerate(it):
|
|
312
313
|
if match_fn(xi, target):
|
|
@@ -684,7 +685,7 @@ def sorted_dict(
|
|
|
684
685
|
x: Mapping[K, V],
|
|
685
686
|
/,
|
|
686
687
|
*,
|
|
687
|
-
by:
|
|
688
|
+
by: DictByKVI = "key",
|
|
688
689
|
key: Optional[Callable[[Any], Any]] = None,
|
|
689
690
|
reverse: bool = False,
|
|
690
691
|
) -> Dict[K, V]:
|
|
@@ -709,7 +710,7 @@ def sorted_dict(
|
|
|
709
710
|
impl_key = by_value_fn
|
|
710
711
|
|
|
711
712
|
else:
|
|
712
|
-
msg = f"Invalid argument {by=}. (expected one of {(
|
|
713
|
+
msg = f"Invalid argument {by=}. (expected one of {get_args(DictByKVI)})"
|
|
713
714
|
raise ValueError(msg)
|
|
714
715
|
|
|
715
716
|
return {k: v for k, v in sorted(x.items(), key=impl_key, reverse=reverse)} # type: ignore
|
|
@@ -738,7 +739,9 @@ def shuffled(
|
|
|
738
739
|
return x
|
|
739
740
|
|
|
740
741
|
|
|
741
|
-
def unflat_dict_of_dict(
|
|
742
|
+
def unflat_dict_of_dict(
|
|
743
|
+
dic: Mapping[str, Any], *, sep: str = ".", overwrite: bool = True
|
|
744
|
+
) -> Dict[str, Any]:
|
|
742
745
|
"""Unflat a dictionary.
|
|
743
746
|
|
|
744
747
|
Example 1
|
|
@@ -754,18 +757,26 @@ def unflat_dict_of_dict(dic: Mapping[str, Any], *, sep: str = ".") -> Dict[str,
|
|
|
754
757
|
"""
|
|
755
758
|
output = {}
|
|
756
759
|
for k, v in dic.items():
|
|
757
|
-
if
|
|
760
|
+
if not overwrite and k in output:
|
|
761
|
+
msg = f"Ambiguous flatten dict with key {k=}. (with value {v})"
|
|
762
|
+
raise ValueError(msg)
|
|
763
|
+
elif sep not in k:
|
|
758
764
|
output[k] = v
|
|
765
|
+
continue
|
|
766
|
+
|
|
767
|
+
idx = k.index(sep)
|
|
768
|
+
k, kk = k[:idx], k[idx + 1 :]
|
|
769
|
+
if k not in output:
|
|
770
|
+
output[k] = {}
|
|
771
|
+
elif isinstance(output[k], Mapping):
|
|
772
|
+
pass
|
|
773
|
+
elif overwrite:
|
|
774
|
+
output[k] = {}
|
|
759
775
|
else:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if k not in output:
|
|
763
|
-
output[k] = {}
|
|
764
|
-
elif not isinstance(output[k], Mapping):
|
|
765
|
-
msg = f"Invalid dict argument. (found keys {k} and {k}{sep}{kk})"
|
|
766
|
-
raise ValueError(msg)
|
|
776
|
+
msg = f"Invalid dict argument. (found keys {k} and {k}{sep}{kk})"
|
|
777
|
+
raise ValueError(msg)
|
|
767
778
|
|
|
768
|
-
|
|
779
|
+
output[k][kk] = v
|
|
769
780
|
|
|
770
781
|
output = {
|
|
771
782
|
k: (unflat_dict_of_dict(v) if isinstance(v, Mapping) else v)
|
|
@@ -277,6 +277,7 @@ def sum(
|
|
|
277
277
|
...
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
# note: we do not use function_alias because start arg has default value of 0
|
|
280
281
|
def sum(*args, start: Any = 0):
|
|
281
282
|
"""Compute sum of elements."""
|
|
282
283
|
return reduce_add(*args, start=start)
|
|
@@ -313,6 +314,7 @@ def prod(
|
|
|
313
314
|
...
|
|
314
315
|
|
|
315
316
|
|
|
317
|
+
# note: we do not use function_alias because start arg has default value of 1
|
|
316
318
|
def prod(*args, start: Any = 1):
|
|
317
319
|
"""Compute product of elements."""
|
|
318
320
|
return reduce_mul(*args, start=start)
|