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.
Files changed (133) hide show
  1. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/CHANGELOG.md +11 -0
  2. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/CITATION.cff +2 -2
  3. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/PKG-INFO +1 -1
  4. pythonwrench-0.6.8/pythonwrench.code-workspace +13 -0
  5. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/__init__.py +14 -3
  6. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/_core.py +135 -1
  7. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/dataclass_.py +137 -27
  8. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/cast.py +7 -1
  9. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/collections.py +26 -15
  10. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/reducers.py +2 -0
  11. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/functools.py +8 -29
  12. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/importlib.py +1 -1
  13. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/os.py +42 -0
  14. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/re.py +70 -3
  15. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/semver.py +2 -2
  16. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/__init__.py +2 -0
  17. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/checks.py +135 -68
  18. pythonwrench-0.6.8/src/pythonwrench/warnings.py +8 -0
  19. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_argparse.py +81 -5
  20. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_collections.py +9 -0
  21. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_typing.py +52 -20
  22. pythonwrench-0.6.7/src/pythonwrench/warnings.py +0 -118
  23. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.github/workflows/test.yaml +0 -0
  24. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.gitignore +0 -0
  25. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.pre-commit-config.yaml +0 -0
  26. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/.readthedocs.yaml +0 -0
  27. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/LICENSE +0 -0
  28. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/README.md +0 -0
  29. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/Makefile +0 -0
  30. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/conf.py +0 -0
  31. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/index.rst +0 -0
  32. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/make.bat +0 -0
  33. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/modules.rst +0 -0
  34. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.abc.rst +0 -0
  35. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.dataclass_.rst +0 -0
  36. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.parsers.rst +0 -0
  37. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.rst +0 -0
  38. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.cast.rst +0 -0
  39. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.checksum.rst +0 -0
  40. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.collections.rst +0 -0
  41. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.prop.rst +0 -0
  42. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.reducers.rst +0 -0
  43. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.collections.rst +0 -0
  44. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.concurrent.rst +0 -0
  45. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.csv.rst +0 -0
  46. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.dataclasses.rst +0 -0
  47. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.datetime.rst +0 -0
  48. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.difflib.rst +0 -0
  49. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.disk_cache.rst +0 -0
  50. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.enum.rst +0 -0
  51. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.functools.rst +0 -0
  52. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.hashlib.rst +0 -0
  53. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.importlib.rst +0 -0
  54. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.inspect.rst +0 -0
  55. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.json.rst +0 -0
  56. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.jsonl.rst +0 -0
  57. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.logging.rst +0 -0
  58. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.math.rst +0 -0
  59. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.os.rst +0 -0
  60. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.pickle.rst +0 -0
  61. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.random.rst +0 -0
  62. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.re.rst +0 -0
  63. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.rst +0 -0
  64. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.semver.rst +0 -0
  65. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.csv.rst +0 -0
  66. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.json.rst +0 -0
  67. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.jsonl.rst +0 -0
  68. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.pickle.rst +0 -0
  69. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.rst +0 -0
  70. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.time.rst +0 -0
  71. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.checks.rst +0 -0
  72. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.classes.rst +0 -0
  73. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.typing.rst +0 -0
  74. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/pythonwrench.warnings.rst +0 -0
  75. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/docs/update.bash +0 -0
  76. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/pyproject.toml +0 -0
  77. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/setup.py +0 -0
  78. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/__main__.py +0 -0
  79. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/abc.py +0 -0
  80. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/__init__.py +0 -0
  81. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/argparse/parsers.py +0 -0
  82. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/checksum.py +0 -0
  83. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/__init__.py +0 -0
  84. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/collections/prop.py +0 -0
  85. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/concurrent.py +0 -0
  86. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/csv.py +0 -0
  87. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/dataclasses.py +0 -0
  88. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/datetime.py +0 -0
  89. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/difflib.py +0 -0
  90. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/disk_cache.py +0 -0
  91. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/info.py +0 -0
  92. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  93. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/tree.py +0 -0
  94. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/enum.py +0 -0
  95. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/hashlib.py +0 -0
  96. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/inspect.py +0 -0
  97. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/json.py +0 -0
  98. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/jsonl.py +0 -0
  99. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/logging.py +0 -0
  100. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/math.py +0 -0
  101. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/pickle.py +0 -0
  102. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/random.py +0 -0
  103. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/__init__.py +0 -0
  104. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/_core.py +0 -0
  105. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/csv.py +0 -0
  106. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/json.py +0 -0
  107. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/jsonl.py +0 -0
  108. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/serialization/pickle.py +0 -0
  109. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/time.py +0 -0
  110. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/src/pythonwrench/typing/classes.py +0 -0
  111. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_abc.py +0 -0
  112. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_cast.py +0 -0
  113. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_checksum.py +0 -0
  114. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_core.py +0 -0
  115. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_csv.py +0 -0
  116. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_dataclasses.py +0 -0
  117. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_difflib.py +0 -0
  118. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_disk_cache.py +0 -0
  119. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_entrypoints.py +0 -0
  120. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_enum.py +0 -0
  121. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_functools.py +0 -0
  122. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_hashlib.py +0 -0
  123. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_importlib.py +0 -0
  124. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_inspect.py +0 -0
  125. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_json.py +0 -0
  126. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_jsonl.py +0 -0
  127. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_logging.py +0 -0
  128. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_math.py +0 -0
  129. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_os.py +0 -0
  130. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_random.py +0 -0
  131. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_readme.py +0 -0
  132. {pythonwrench-0.6.7 → pythonwrench-0.6.8}/tests/test_semver.py +0 -0
  133. {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.
@@ -17,5 +17,5 @@ keywords:
17
17
  - tools
18
18
  - utilities
19
19
  license: MIT
20
- version: 0.6.7
21
- date-released: '2026-09-07'
20
+ version: 0.6.8
21
+ date-released: '2026-10-26'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pythonwrench
3
- Version: 0.6.7
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/
@@ -0,0 +1,13 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ }
6
+ ],
7
+ "settings": {
8
+ "files.exclude": {
9
+ "**/.coverage": true,
10
+ "**/*.egg-info": true
11
+ }
12
+ }
13
+ }
@@ -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.7"
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": ["get_num_cpus_available", "safe_rmdir", "tree_iter"],
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
- from functools import wraps
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: Type[DataclassInstance],
169
- *dataclass_types: Type[DataclassInstance],
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, argv = parser.parse_known_args(args)
202
- if len(argv) > 0:
203
- msg = f"Found {len(argv)} unknown arguments: {argv}."
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
- dataclass_insts = []
210
- for dataclass_type_i in dataclass_types:
211
- if init_parser is None and len(dataclass_types) == 1:
212
- instance = dataclass_type_i(**parsed.__dict__)
213
- else:
214
- instance = filter_and_call(
215
- dataclass_type_i,
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() # type: ignore
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
- try:
267
- inner_kwds = _get_kwds_for_type(field.type, list_parsing, bool_action)
268
- except (ValueError, TypeError, RuntimeError) as err:
269
- msg = f"Invalid field {field.name}: field type '{field.type}' is not supported."
270
- raise type(err)(msg) from err
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
- kwds.update(inner_kwds)
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
- raise ValueError(
308
- f"Invalid argument {order=}. (expected one of {get_args(Order)})"
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: Literal["key", "value", "item"] = "key",
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 {('key', 'value', 'item')})"
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(dic: Mapping[str, Any], *, sep: str = ".") -> Dict[str, Any]:
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 sep not in k:
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
- idx = k.index(sep)
761
- k, kk = k[:idx], k[idx + 1 :]
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
- output[k][kk] = v
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)