pythonwrench 0.6.6__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.6 → pythonwrench-0.6.8}/CHANGELOG.md +21 -0
  2. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/CITATION.cff +2 -2
  3. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/PKG-INFO +1 -1
  4. pythonwrench-0.6.8/pythonwrench.code-workspace +13 -0
  5. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/__init__.py +14 -3
  6. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/_core.py +135 -1
  7. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/argparse/dataclass_.py +151 -27
  8. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/cast.py +7 -1
  9. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/collections/collections.py +26 -15
  10. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/collections/reducers.py +2 -0
  11. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/functools.py +8 -29
  12. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/importlib.py +1 -1
  13. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/logging.py +1 -1
  14. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/os.py +42 -0
  15. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/re.py +70 -3
  16. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/semver.py +2 -2
  17. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/_core.py +3 -0
  18. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/csv.py +6 -2
  19. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/json.py +47 -9
  20. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/jsonl.py +34 -9
  21. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/pickle.py +50 -10
  22. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/typing/__init__.py +2 -0
  23. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/typing/checks.py +135 -68
  24. pythonwrench-0.6.8/src/pythonwrench/warnings.py +8 -0
  25. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_argparse.py +90 -4
  26. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_collections.py +9 -0
  27. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_typing.py +52 -20
  28. pythonwrench-0.6.6/src/pythonwrench/warnings.py +0 -118
  29. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/.github/workflows/test.yaml +0 -0
  30. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/.gitignore +0 -0
  31. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/.pre-commit-config.yaml +0 -0
  32. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/.readthedocs.yaml +0 -0
  33. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/LICENSE +0 -0
  34. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/README.md +0 -0
  35. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/Makefile +0 -0
  36. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/conf.py +0 -0
  37. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/index.rst +0 -0
  38. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/make.bat +0 -0
  39. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/modules.rst +0 -0
  40. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.abc.rst +0 -0
  41. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.dataclass_.rst +0 -0
  42. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.parsers.rst +0 -0
  43. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.argparse.rst +0 -0
  44. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.cast.rst +0 -0
  45. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.checksum.rst +0 -0
  46. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.collections.collections.rst +0 -0
  47. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.collections.prop.rst +0 -0
  48. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.collections.reducers.rst +0 -0
  49. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.collections.rst +0 -0
  50. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.concurrent.rst +0 -0
  51. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.csv.rst +0 -0
  52. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.dataclasses.rst +0 -0
  53. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.datetime.rst +0 -0
  54. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.difflib.rst +0 -0
  55. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.disk_cache.rst +0 -0
  56. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.enum.rst +0 -0
  57. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.functools.rst +0 -0
  58. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.hashlib.rst +0 -0
  59. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.importlib.rst +0 -0
  60. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.inspect.rst +0 -0
  61. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.json.rst +0 -0
  62. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.jsonl.rst +0 -0
  63. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.logging.rst +0 -0
  64. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.math.rst +0 -0
  65. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.os.rst +0 -0
  66. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.pickle.rst +0 -0
  67. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.random.rst +0 -0
  68. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.re.rst +0 -0
  69. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.rst +0 -0
  70. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.semver.rst +0 -0
  71. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.csv.rst +0 -0
  72. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.json.rst +0 -0
  73. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.jsonl.rst +0 -0
  74. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.pickle.rst +0 -0
  75. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.serialization.rst +0 -0
  76. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.time.rst +0 -0
  77. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.typing.checks.rst +0 -0
  78. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.typing.classes.rst +0 -0
  79. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.typing.rst +0 -0
  80. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/pythonwrench.warnings.rst +0 -0
  81. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/docs/update.bash +0 -0
  82. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/pyproject.toml +0 -0
  83. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/setup.py +0 -0
  84. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/__main__.py +0 -0
  85. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/abc.py +0 -0
  86. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/argparse/__init__.py +0 -0
  87. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/argparse/parsers.py +0 -0
  88. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/checksum.py +0 -0
  89. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/collections/__init__.py +0 -0
  90. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/collections/prop.py +0 -0
  91. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/concurrent.py +0 -0
  92. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/csv.py +0 -0
  93. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/dataclasses.py +0 -0
  94. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/datetime.py +0 -0
  95. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/difflib.py +0 -0
  96. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/disk_cache.py +0 -0
  97. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/info.py +0 -0
  98. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  99. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/entrypoints/tree.py +0 -0
  100. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/enum.py +0 -0
  101. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/hashlib.py +0 -0
  102. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/inspect.py +0 -0
  103. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/json.py +0 -0
  104. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/jsonl.py +0 -0
  105. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/math.py +0 -0
  106. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/pickle.py +0 -0
  107. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/random.py +0 -0
  108. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/serialization/__init__.py +0 -0
  109. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/time.py +0 -0
  110. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/src/pythonwrench/typing/classes.py +0 -0
  111. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_abc.py +0 -0
  112. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_cast.py +0 -0
  113. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_checksum.py +0 -0
  114. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_core.py +0 -0
  115. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_csv.py +0 -0
  116. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_dataclasses.py +0 -0
  117. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_difflib.py +0 -0
  118. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_disk_cache.py +0 -0
  119. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_entrypoints.py +0 -0
  120. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_enum.py +0 -0
  121. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_functools.py +0 -0
  122. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_hashlib.py +0 -0
  123. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_importlib.py +0 -0
  124. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_inspect.py +0 -0
  125. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_json.py +0 -0
  126. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_jsonl.py +0 -0
  127. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_logging.py +0 -0
  128. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_math.py +0 -0
  129. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_os.py +0 -0
  130. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_random.py +0 -0
  131. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_readme.py +0 -0
  132. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/tests/test_semver.py +0 -0
  133. {pythonwrench-0.6.6 → pythonwrench-0.6.8}/uv.lock +0 -0
@@ -2,10 +2,31 @@
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
+
16
+ ## [0.6.7] 2026-09-07
17
+ ### Added
18
+ - `on_error` and `default` arguments to `load_json`, `load_pickle`, `load_jsonl` functions.
19
+ - `posonly_for_required` argument for `parse_args_using_dataclass`.
20
+
21
+ ### Fixed
22
+ - `setup_logging_level` when stderr is not set in root logger.
23
+
24
+
5
25
  ## [0.6.6] 2026-08-20
6
26
  ### Fixed
7
27
  - `setup_logging_level` isinstance check.
8
28
 
29
+
9
30
  ## [0.6.5] 2026-08-20
10
31
  ### Added
11
32
  - `return_single_namespace` option for `parse_args_using_dataclass` function.
@@ -17,5 +17,5 @@ keywords:
17
17
  - tools
18
18
  - utilities
19
19
  license: MIT
20
- version: 0.6.6
21
- date-released: '2026-08-20'
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.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/
@@ -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.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": ["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,
@@ -58,6 +62,7 @@ def parse_args_using_dataclass(
58
62
  bool_action: BoolAction = "store",
59
63
  add_dashed_arg: bool = True,
60
64
  return_single_namespace: Literal[True],
65
+ posonly_for_required: bool = False,
61
66
  ) -> Namespace: ...
62
67
 
63
68
 
@@ -71,6 +76,7 @@ def parse_args_using_dataclass(
71
76
  bool_action: BoolAction = "store",
72
77
  add_dashed_arg: bool = True,
73
78
  return_single_namespace: Literal[False] = False,
79
+ posonly_for_required: bool = False,
74
80
  ) -> T_DataclassInstance: ...
75
81
 
76
82
 
@@ -86,6 +92,7 @@ def parse_args_using_dataclass(
86
92
  bool_action: BoolAction = "store",
87
93
  add_dashed_arg: bool = True,
88
94
  return_single_namespace: Literal[False] = False,
95
+ posonly_for_required: bool = False,
89
96
  ) -> Tuple[
90
97
  T_DataclassInstance,
91
98
  T_DataclassInstance_2,
@@ -105,6 +112,7 @@ def parse_args_using_dataclass(
105
112
  bool_action: BoolAction = "store",
106
113
  add_dashed_arg: bool = True,
107
114
  return_single_namespace: Literal[False] = False,
115
+ posonly_for_required: bool = False,
108
116
  ) -> Tuple[
109
117
  T_DataclassInstance,
110
118
  T_DataclassInstance_2,
@@ -126,6 +134,7 @@ def parse_args_using_dataclass(
126
134
  bool_action: BoolAction = "store",
127
135
  add_dashed_arg: bool = True,
128
136
  return_single_namespace: Literal[False] = False,
137
+ posonly_for_required: bool = False,
129
138
  ) -> Tuple[
130
139
  T_DataclassInstance,
131
140
  T_DataclassInstance_2,
@@ -149,6 +158,7 @@ def parse_args_using_dataclass(
149
158
  bool_action: BoolAction = "store",
150
159
  add_dashed_arg: bool = True,
151
160
  return_single_namespace: Literal[False] = False,
161
+ posonly_for_required: bool = False,
152
162
  ) -> Tuple[
153
163
  T_DataclassInstance,
154
164
  T_DataclassInstance_2,
@@ -159,14 +169,15 @@ def parse_args_using_dataclass(
159
169
 
160
170
 
161
171
  def parse_args_using_dataclass(
162
- dataclass_type: Type[DataclassInstance],
163
- *dataclass_types: Type[DataclassInstance],
172
+ dataclass_type: Dataclass,
173
+ *dataclass_types: Dataclass,
164
174
  args: Optional[Iterable[str]] = None,
165
175
  parser: Optional[ArgumentParser] = None,
166
176
  list_parsing: ListParsing = "argparse",
167
177
  bool_action: BoolAction = "store",
168
178
  add_dashed_arg: bool = True,
169
179
  return_single_namespace: bool = False,
180
+ posonly_for_required: bool = False,
170
181
  ) -> Union[
171
182
  DataclassInstance,
172
183
  Tuple[DataclassInstance, ...],
@@ -187,28 +198,25 @@ def parse_args_using_dataclass(
187
198
  list_parsing=list_parsing,
188
199
  bool_action=bool_action,
189
200
  add_dashed_arg=add_dashed_arg,
201
+ posonly_for_required=posonly_for_required,
190
202
  )
191
203
  assert parser is not None
192
204
 
193
- parsed, argv = parser.parse_known_args(args)
194
- if len(argv) > 0:
195
- 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=})"
196
208
  raise ValueError(msg)
197
209
 
198
210
  if return_single_namespace:
199
211
  return parsed
200
212
 
201
- dataclass_insts = []
202
- for dataclass_type_i in dataclass_types:
203
- if init_parser is None and len(dataclass_types) == 1:
204
- instance = dataclass_type_i(**parsed.__dict__)
205
- else:
206
- instance = filter_and_call(
207
- dataclass_type_i,
208
- _fill_all_arguments=True,
209
- **parsed.__dict__,
210
- )
211
- 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
+ )
212
220
 
213
221
  if len(dataclass_insts) == 1:
214
222
  return dataclass_insts[0]
@@ -216,6 +224,79 @@ def parse_args_using_dataclass(
216
224
  return tuple(dataclass_insts)
217
225
 
218
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
+
219
300
  def add_dataclass_fields_to_parser(
220
301
  dataclass_type: Type[T_DataclassInstance],
221
302
  *,
@@ -223,17 +304,24 @@ def add_dataclass_fields_to_parser(
223
304
  list_parsing: ListParsing = "argparse",
224
305
  bool_action: BoolAction = "store",
225
306
  add_dashed_arg: bool = True,
307
+ posonly_for_required: bool = False,
308
+ argprefix: str = "",
226
309
  ) -> ArgumentParser:
227
310
  """Perform the add dataclass fields to parser operation."""
311
+ if posonly_for_required:
312
+ prefix = argprefix
313
+ else:
314
+ prefix = f"--{argprefix}"
315
+
228
316
  if parser is None:
229
317
  parser = ArgumentParser()
230
318
 
231
319
  for field in fields(dataclass_type):
232
320
  kwds = {}
233
- posargs = [f"--{field.name}"]
321
+ posargs = [f"{prefix}{field.name}"]
234
322
  if add_dashed_arg and "_" in field.name:
235
323
  dashed_arg_name = field.name.replace("_", "-")
236
- posargs.append(f"--{dashed_arg_name}")
324
+ posargs.append(f"{prefix}{dashed_arg_name}")
237
325
 
238
326
  if field.default is MISSING and field.default_factory is MISSING:
239
327
  if bool_action != "store" and field.type is bool:
@@ -244,19 +332,45 @@ def add_dataclass_fields_to_parser(
244
332
  elif field.default is not MISSING:
245
333
  kwds["default"] = field.default
246
334
  elif field.default_factory is not MISSING:
247
- kwds["default"] = field.default_factory() # type: ignore
335
+ kwds["default"] = field.default_factory()
248
336
  else:
249
- 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."
250
338
  raise ValueError(msg)
251
339
 
252
- try:
253
- inner_kwds = _get_kwds_for_type(field.type, list_parsing, bool_action)
254
- except (ValueError, TypeError, RuntimeError) as err:
255
- msg = f"Invalid field {field.name}: field type '{field.type}' is not supported."
256
- 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
257
346
 
258
- kwds.update(inner_kwds)
259
- parser.add_argument(*posargs, **kwds)
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)
372
+
373
+ parser.add_argument(*posargs, **kwds)
260
374
 
261
375
  return parser
262
376
 
@@ -300,6 +414,16 @@ def _get_kwds_for_type(
300
414
  raise TypeError(msg)
301
415
 
302
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
+
303
427
  # ALIASES
304
428
  @deprecated_alias(add_dataclass_fields_to_parser)
305
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."""