pythonwrench 0.3.0__tar.gz → 0.4.0__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 (76) hide show
  1. {pythonwrench-0.3.0/src/pythonwrench.egg-info → pythonwrench-0.4.0}/PKG-INFO +1 -1
  2. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/pyproject.toml +4 -0
  3. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/__init__.py +24 -4
  4. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/_core.py +4 -2
  5. pythonwrench-0.4.0/src/pythonwrench/argparse.py +313 -0
  6. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/checksum.py +0 -37
  7. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/collections/__init__.py +1 -0
  8. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/collections/collections.py +23 -0
  9. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/collections/reducers.py +6 -0
  10. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/csv.py +295 -119
  11. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/difflib.py +2 -0
  12. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/disk_cache.py +52 -10
  13. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/entries.py +7 -6
  14. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/enum.py +11 -6
  15. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/functools.py +24 -1
  16. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/importlib.py +51 -4
  17. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/inspect.py +17 -7
  18. pythonwrench-0.4.0/src/pythonwrench/json.py +170 -0
  19. pythonwrench-0.4.0/src/pythonwrench/jsonl.py +198 -0
  20. pythonwrench-0.4.0/src/pythonwrench/pickle.py +150 -0
  21. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/semver.py +29 -24
  22. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/warnings.py +30 -3
  23. {pythonwrench-0.3.0 → pythonwrench-0.4.0/src/pythonwrench.egg-info}/PKG-INFO +1 -1
  24. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench.egg-info/SOURCES.txt +4 -0
  25. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench.egg-info/entry_points.txt +3 -0
  26. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_argparse.py +29 -1
  27. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_cast.py +2 -0
  28. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_checksum.py +3 -0
  29. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_collections.py +9 -1
  30. pythonwrench-0.4.0/tests/test_csv.py +38 -0
  31. pythonwrench-0.4.0/tests/test_enum.py +29 -0
  32. pythonwrench-0.4.0/tests/test_importlib.py +54 -0
  33. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_inspect.py +19 -3
  34. pythonwrench-0.4.0/tests/test_json.py +24 -0
  35. pythonwrench-0.4.0/tests/test_jsonl.py +18 -0
  36. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_semver.py +4 -1
  37. pythonwrench-0.3.0/src/pythonwrench/argparse.py +0 -119
  38. pythonwrench-0.3.0/src/pythonwrench/json.py +0 -88
  39. pythonwrench-0.3.0/src/pythonwrench/pickle.py +0 -72
  40. pythonwrench-0.3.0/tests/test_csv.py +0 -25
  41. pythonwrench-0.3.0/tests/test_importlib.py +0 -30
  42. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/LICENSE +0 -0
  43. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/README.md +0 -0
  44. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/setup.cfg +0 -0
  45. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/setup.py +0 -0
  46. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/__main__.py +0 -0
  47. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/abc.py +0 -0
  48. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/cast.py +0 -0
  49. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/collections/prop.py +0 -0
  50. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/dataclasses.py +0 -0
  51. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/datetime.py +0 -0
  52. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/hashlib.py +0 -0
  53. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/io.py +0 -0
  54. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/logging.py +0 -0
  55. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/math.py +0 -0
  56. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/os.py +0 -0
  57. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/random.py +0 -0
  58. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/re.py +0 -0
  59. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/typing/__init__.py +0 -0
  60. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/typing/checks.py +0 -0
  61. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench/typing/classes.py +0 -0
  62. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  63. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench.egg-info/requires.txt +0 -0
  64. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/src/pythonwrench.egg-info/top_level.txt +0 -0
  65. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_abc.py +0 -0
  66. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_dataclasses.py +0 -0
  67. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_difflib.py +0 -0
  68. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_entries.py +0 -0
  69. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_functools.py +0 -0
  70. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_hashlib.py +0 -0
  71. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_logging.py +0 -0
  72. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_math.py +0 -0
  73. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_os.py +0 -0
  74. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_random.py +0 -0
  75. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_readme.py +0 -0
  76. {pythonwrench-0.3.0 → pythonwrench-0.4.0}/tests/test_typing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Set of tools for Python that could be in the standard library.
5
5
  Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
6
6
  Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
@@ -8,6 +8,7 @@ readme = "README.md"
8
8
  requires-python = ">=3.8"
9
9
  keywords = ["python", "tools", "utilities"]
10
10
  license = {file = "LICENSE"}
11
+ # license-files = ["LICENSE"] # unsupported by python 3.8, but will be required in 2026
11
12
  classifiers = [
12
13
  "Intended Audience :: Developers",
13
14
  "Intended Audience :: Science/Research",
@@ -40,6 +41,9 @@ Tracker = "https://github.com/Labbeti/pythonwrench/issues"
40
41
  pythonwrench-info = "pythonwrench.entries:print_install_info"
41
42
  pythonwrench-tree = "pythonwrench.entries:main_tree"
42
43
  pythonwrench-safe-rmdir = "pythonwrench.entries:main_safe_rmdir"
44
+ pyw-info = "pythonwrench.entries:print_install_info"
45
+ pyw-tree = "pythonwrench.entries:main_tree"
46
+ pyw-safe-rmdir = "pythonwrench.entries:main_safe_rmdir"
43
47
 
44
48
  [build-system]
45
49
  requires = ["setuptools >= 61.0"]
@@ -9,7 +9,7 @@ __author_email__ = "labbeti.pub@gmail.com"
9
9
  __license__ = "MIT"
10
10
  __maintainer__ = "Étienne Labbé (Labbeti)"
11
11
  __status__ = "Development"
12
- __version__ = "0.3.0"
12
+ __version__ = "0.4.0"
13
13
 
14
14
 
15
15
  # Re-import for language servers
@@ -40,11 +40,14 @@ from . import warnings as warnings
40
40
  # Global library imports
41
41
  from .abc import Singleton
42
42
  from .argparse import (
43
+ parse_to,
43
44
  str_to_bool,
45
+ str_to_none,
44
46
  str_to_optional_bool,
45
47
  str_to_optional_float,
46
48
  str_to_optional_int,
47
49
  str_to_optional_str,
50
+ str_to_type,
48
51
  )
49
52
  from .cast import as_builtin
50
53
  from .checksum import checksum_any, register_checksum_fn
@@ -54,6 +57,7 @@ from .collections import (
54
57
  contained,
55
58
  dict_list_to_list_dict,
56
59
  dump_dict,
60
+ duplicate_list,
57
61
  filter_iterable,
58
62
  find,
59
63
  flat_dict_of_dict,
@@ -81,7 +85,7 @@ from .collections import (
81
85
  union_lists,
82
86
  unzip,
83
87
  )
84
- from .csv import dump_csv, load_csv
88
+ from .csv import dump_csv, dumps_csv, load_csv, loads_csv, read_csv, save_csv
85
89
  from .dataclasses import get_defaults_values
86
90
  from .datetime import get_now, get_now_iso8601
87
91
  from .difflib import find_closest_in_list, sequence_matcher_ratio
@@ -93,6 +97,7 @@ from .functools import (
93
97
  filter_and_call,
94
98
  function_alias,
95
99
  identity,
100
+ repeat_fn,
96
101
  )
97
102
  from .hashlib import hash_file
98
103
  from .importlib import (
@@ -103,7 +108,15 @@ from .importlib import (
103
108
  search_submodules,
104
109
  )
105
110
  from .inspect import get_argnames, get_current_fn_name, get_fullname
106
- from .json import dump_json, load_json
111
+ from .json import dump_json, dumps_json, load_json, loads_json, read_json, save_json
112
+ from .jsonl import (
113
+ dump_jsonl,
114
+ dumps_jsonl,
115
+ load_jsonl,
116
+ loads_jsonl,
117
+ read_jsonl,
118
+ save_jsonl,
119
+ )
107
120
  from .logging import (
108
121
  VERBOSE_DEBUG,
109
122
  VERBOSE_ERROR,
@@ -122,7 +135,14 @@ from .logging import (
122
135
  )
123
136
  from .math import argmax, argmin, argsort, clamp, clip
124
137
  from .os import get_num_cpus_available, safe_rmdir, tree_iter
125
- from .pickle import dump_pickle, load_pickle
138
+ from .pickle import (
139
+ dump_pickle,
140
+ dumps_pickle,
141
+ load_pickle,
142
+ loads_pickle,
143
+ read_pickle,
144
+ save_pickle,
145
+ )
126
146
  from .random import randstr
127
147
  from .re import (
128
148
  PatternLike,
@@ -37,7 +37,7 @@ def _decorator_factory(
37
37
  *,
38
38
  pre_fn: Callable[..., Any] = return_none,
39
39
  post_fn: Callable[..., Any] = return_none,
40
- ) -> Callable[..., Callable[P, U]]:
40
+ ) -> Callable[[Callable[P, U]], Callable[P, U]]:
41
41
  """Deprecated decorator for function aliases."""
42
42
 
43
43
  def wrapper_factory(fn: Callable[P, U]) -> Callable[P, U]:
@@ -77,7 +77,9 @@ class _FunctionRegistry(Generic[T_Output]):
77
77
  priority: int = 0,
78
78
  ) -> Callable[[T], T_Output]:
79
79
  return self.register_decorator(
80
- class_or_tuple, custom_predicate=custom_predicate
80
+ class_or_tuple,
81
+ custom_predicate=custom_predicate,
82
+ priority=priority,
81
83
  )(fn)
82
84
 
83
85
  def register_decorator(
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from functools import partial
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ Iterable,
9
+ List,
10
+ Optional,
11
+ Type,
12
+ TypeVar,
13
+ Union,
14
+ get_args,
15
+ get_origin,
16
+ )
17
+
18
+ from pythonwrench.typing.classes import NoneType
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ DEFAULT_TRUE_VALUES = ("True", "t", "yes", "y", "1")
24
+ DEFAULT_FALSE_VALUES = ("False", "f", "no", "n", "0")
25
+ DEFAULT_NONE_VALUES = ("None", "null")
26
+
27
+
28
+ def parse_to(
29
+ target_type: Type[T],
30
+ *,
31
+ case_sensitive: bool = False,
32
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
33
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
34
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
35
+ ) -> Callable[[str], T]:
36
+ """Returns a callable that convert string value to target type safely.
37
+
38
+ Intended for argparse boolean arguments.
39
+ """
40
+ return partial(
41
+ str_to_type,
42
+ target_type=target_type,
43
+ case_sensitive=case_sensitive,
44
+ true_values=true_values,
45
+ false_values=false_values,
46
+ none_values=none_values,
47
+ )
48
+
49
+
50
+ def str_to_type(
51
+ x: str,
52
+ target_type: Type[T],
53
+ *,
54
+ case_sensitive: bool = False,
55
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
56
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
57
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
58
+ ) -> T:
59
+ """Convert string values to target type safely. Intended for argparse boolean arguments.
60
+
61
+ - True values: 'True', 'T', 'yes', 'y', '1'.
62
+ - False values: 'False', 'F', 'no', 'n', '0'.
63
+ - None values: 'None', 'null'
64
+ - Other raises ValueError.
65
+ """
66
+ result = _str_to_type_impl(
67
+ x,
68
+ target_type,
69
+ case_sensitive=case_sensitive,
70
+ true_values=true_values,
71
+ false_values=false_values,
72
+ none_values=none_values,
73
+ )
74
+ if isinstance(result, Exception):
75
+ raise result
76
+ else:
77
+ return result
78
+
79
+
80
+ def str_to_bool(
81
+ x: str,
82
+ *,
83
+ case_sensitive: bool = False,
84
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
85
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
86
+ ) -> bool:
87
+ """Convert string values to bool safely. Intended for argparse boolean arguments.
88
+
89
+ - True values: 'True', 'T', 'yes', 'y', '1'.
90
+ - False values: 'False', 'F', 'no', 'n', '0'.
91
+ - Other raises ValueError.
92
+ """
93
+ return str_to_type(
94
+ x,
95
+ bool,
96
+ case_sensitive=case_sensitive,
97
+ true_values=true_values,
98
+ false_values=false_values,
99
+ )
100
+
101
+
102
+ def str_to_none(
103
+ x: str,
104
+ *,
105
+ case_sensitive: bool = False,
106
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
107
+ ) -> None:
108
+ """Convert string values to None safely. Intended for argparse boolean arguments.
109
+
110
+ - None values: 'None', 'null'
111
+ - Other raises ValueError.
112
+ """
113
+ return str_to_type(
114
+ x, NoneType, case_sensitive=case_sensitive, none_values=none_values
115
+ )
116
+
117
+
118
+ def str_to_optional_bool(
119
+ x: str,
120
+ *,
121
+ case_sensitive: bool = False,
122
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
123
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
124
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
125
+ ) -> Optional[bool]:
126
+ """Convert string values to optional bool safely. Intended for argparse boolean arguments.
127
+
128
+ - True values: 'True', 'T', 'yes', 'y', '1'.
129
+ - False values: 'False', 'F', 'no', 'n', '0'.
130
+ - None values: 'None', 'null'
131
+ - Other raises ValueError.
132
+ """
133
+ return str_to_type(
134
+ x, Optional[bool], case_sensitive=case_sensitive, none_values=none_values
135
+ )
136
+
137
+
138
+ def str_to_optional_float(
139
+ x: str,
140
+ *,
141
+ case_sensitive: bool = False,
142
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
143
+ ) -> Optional[float]:
144
+ """Convert string values to optional float safely. Intended for argparse boolean arguments."""
145
+ return str_to_type(
146
+ x, Optional[float], case_sensitive=case_sensitive, none_values=none_values
147
+ )
148
+
149
+
150
+ def str_to_optional_int(
151
+ x: str,
152
+ *,
153
+ case_sensitive: bool = False,
154
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
155
+ ) -> Optional[int]:
156
+ """Convert string values to optional int safely. Intended for argparse boolean arguments."""
157
+ return str_to_type(
158
+ x, Optional[int], case_sensitive=case_sensitive, none_values=none_values
159
+ )
160
+
161
+
162
+ def str_to_optional_str(
163
+ x: str,
164
+ *,
165
+ case_sensitive: bool = False,
166
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
167
+ ) -> Optional[str]:
168
+ """Convert string values to optional str safely. Intended for argparse boolean arguments."""
169
+ return str_to_type(
170
+ x, Optional[str], case_sensitive=case_sensitive, none_values=none_values
171
+ )
172
+
173
+
174
+ def _str_to_type_impl(
175
+ x: str,
176
+ target_type: Type[T],
177
+ *,
178
+ case_sensitive: bool = False,
179
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
180
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
181
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
182
+ ) -> Union[T, Exception]:
183
+ if target_type in (str, int, float, None, NoneType, bool):
184
+ return _str_to_scalar_impl(
185
+ x,
186
+ target_type,
187
+ case_sensitive=case_sensitive,
188
+ true_values=true_values,
189
+ false_values=false_values,
190
+ none_values=none_values,
191
+ )
192
+
193
+ origin = get_origin(target_type)
194
+ if getattr(target_type, "__name__", None) == "Optional":
195
+ args = (None,) + get_args(target_type)
196
+ elif origin == Union or origin.__name__ in ("Union", "UnionType"): # type: ignore
197
+ args = get_args(target_type)
198
+ else:
199
+ msg = f"Invalid argument {target_type=}. (unsupported type)"
200
+ raise ValueError(msg)
201
+
202
+ # str is always at the end
203
+ def key_fn(xi: Any) -> int:
204
+ if xi is str:
205
+ return 1
206
+ else:
207
+ return 0
208
+
209
+ args = sorted(args, key=key_fn)
210
+
211
+ for arg in args:
212
+ result = _str_to_type_impl(
213
+ x,
214
+ arg, # type: ignore
215
+ case_sensitive=case_sensitive,
216
+ true_values=true_values,
217
+ false_values=false_values,
218
+ )
219
+ if not isinstance(result, Exception):
220
+ return result
221
+
222
+ return ValueError(f"Invalid argument {x=} with {target_type=}.")
223
+
224
+
225
+ def _str_to_scalar_impl(
226
+ x: str,
227
+ target_type: Type[T],
228
+ *,
229
+ case_sensitive: bool = False,
230
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
231
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
232
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
233
+ ) -> Any:
234
+ if target_type is str:
235
+ return x
236
+ elif target_type is int:
237
+ try:
238
+ return int(x)
239
+ except ValueError as err:
240
+ return err
241
+ elif target_type is float:
242
+ try:
243
+ return float(x)
244
+ except ValueError as err:
245
+ return err
246
+ elif target_type in (None, NoneType):
247
+ return _str_to_none_impl(
248
+ x, case_sensitive=case_sensitive, none_values=none_values
249
+ )
250
+ elif target_type is bool:
251
+ return _str_to_bool_impl(
252
+ x,
253
+ case_sensitive=case_sensitive,
254
+ true_values=true_values,
255
+ false_values=false_values,
256
+ )
257
+ else:
258
+ raise ValueError(f"Invalid argument {target_type=}. (unsupported type)")
259
+
260
+
261
+ def _str_to_bool_impl(
262
+ x: str,
263
+ *,
264
+ case_sensitive: bool = False,
265
+ true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
266
+ false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
267
+ ) -> Union[bool, Exception]:
268
+ true_values = _sanitize_values(true_values)
269
+ if _str_in(x, true_values, case_sensitive):
270
+ return True
271
+
272
+ false_values = _sanitize_values(false_values)
273
+ if _str_in(x, false_values, case_sensitive):
274
+ return False
275
+
276
+ values = tuple(true_values + false_values)
277
+ err = ValueError(f"Invalid argument '{x}'. (expected one of {values})")
278
+ return err
279
+
280
+
281
+ def _str_to_none_impl(
282
+ x: str,
283
+ *,
284
+ case_sensitive: bool = False,
285
+ none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
286
+ ) -> Union[None, Exception]:
287
+ """Convert string values to None safely. Intended for argparse boolean arguments.
288
+
289
+ - None values: 'None', 'null'
290
+ - Other raises ValueError.
291
+ """
292
+ none_values = _sanitize_values(none_values)
293
+ if _str_in(x, none_values, case_sensitive):
294
+ return None
295
+
296
+ values = tuple(none_values)
297
+ err = ValueError(f"Invalid argument '{x}'. (expected one of {values})")
298
+ return err
299
+
300
+
301
+ def _sanitize_values(values: Union[str, Iterable[str]]) -> List[str]:
302
+ if isinstance(values, str):
303
+ values = [values]
304
+ else:
305
+ values = list(values)
306
+ return values
307
+
308
+
309
+ def _str_in(x: str, values: List[str], case_sensitive: bool) -> bool:
310
+ if case_sensitive:
311
+ return x in values
312
+ else:
313
+ return x.lower() in map(str.lower, values)
@@ -18,20 +18,15 @@ from typing import (
18
18
  Optional,
19
19
  TypeVar,
20
20
  Union,
21
- get_args,
22
21
  overload,
23
22
  )
24
23
 
25
24
  from pythonwrench._core import ClassOrTuple, Predicate, _FunctionRegistry
26
25
  from pythonwrench.inspect import get_fullname
27
26
  from pythonwrench.typing import (
28
- BuiltinNumber,
29
- BuiltinScalar,
30
27
  DataclassInstance,
31
28
  NamedTupleInstance,
32
29
  NoneType,
33
- is_builtin_number,
34
- is_builtin_scalar,
35
30
  )
36
31
 
37
32
  T = TypeVar("T")
@@ -127,38 +122,6 @@ def checksum_int(x: int, **kwargs) -> int:
127
122
 
128
123
 
129
124
  # Intermediate functions
130
- @register_checksum_fn(None, custom_predicate=is_builtin_scalar)
131
- def checksum_builtin_scalar(x: BuiltinScalar, **kwargs) -> int:
132
- if is_builtin_number(x):
133
- return checksum_builtin_number(x, **kwargs)
134
- elif isinstance(x, bytes):
135
- return checksum_bytes(x, **kwargs)
136
- elif x is None:
137
- return checksum_none(x, **kwargs)
138
- elif isinstance(x, str):
139
- return checksum_str(x, **kwargs)
140
- else:
141
- msg = f"Invalid argument type {type(x)}. (expected one of {get_args(BuiltinScalar)})"
142
- raise TypeError(msg)
143
-
144
-
145
- @register_checksum_fn(None, custom_predicate=is_builtin_number)
146
- def checksum_builtin_number(x: BuiltinNumber, **kwargs) -> int:
147
- """Compute a simple checksum of a builtin scalar number."""
148
- # Note: instance check must follow this order: bool, int, float, complex, because isinstance(True, int) returns True !
149
- if isinstance(x, bool):
150
- return checksum_bool(x, **kwargs)
151
- elif isinstance(x, int):
152
- return checksum_int(x, **kwargs)
153
- elif isinstance(x, float):
154
- return checksum_float(x, **kwargs)
155
- elif isinstance(x, complex):
156
- return checksum_complex(x, **kwargs)
157
- else:
158
- msg = f"Invalid argument type {type(x)}. (expected one of {get_args(BuiltinNumber)})"
159
- raise TypeError(msg)
160
-
161
-
162
125
  @register_checksum_fn(bytearray)
163
126
  def checksum_bytearray(x: bytearray, **kwargs) -> int:
164
127
  kwargs["accumulator"] = kwargs.get("accumulator", 0) + _cached_checksum_str(
@@ -5,6 +5,7 @@ from .collections import (
5
5
  contained,
6
6
  dict_list_to_list_dict,
7
7
  dump_dict,
8
+ duplicate_list,
8
9
  filter_iterable,
9
10
  find,
10
11
  flat_dict_of_dict,
@@ -736,3 +736,26 @@ def unzip(lst):
736
736
  ... [1, 2, 3, 4], [5, 6, 7, 8]
737
737
  """
738
738
  return tuple(map(list, zip(*lst)))
739
+
740
+
741
+ def duplicate_list(lst: List[T], sizes: List[int]) -> List[T]:
742
+ """Duplicate elements elements of a list with the corresponding sizes.
743
+
744
+ Example 1
745
+ ----------
746
+ >>> lst = ["a", "b", "c", "d", "e"]
747
+ >>> sizes = [1, 0, 2, 1, 3]
748
+ >>> duplicate_list(lst, sizes)
749
+ ... ["a", "c", "c", "d", "e", "e", "e"]
750
+ """
751
+ if len(lst) != len(sizes):
752
+ msg = f"Invalid arguments lengths. (found {len(lst)=} != {len(sizes)=})"
753
+ raise ValueError(msg)
754
+
755
+ out_size = sum(sizes)
756
+ out: List[T] = [None for _ in range(out_size)] # type: ignore
757
+ curidx = 0
758
+ for size, elt in zip(sizes, lst):
759
+ out[curidx : curidx + size] = [elt] * size
760
+ curidx += size
761
+ return out
@@ -56,6 +56,7 @@ def reduce_add(
56
56
 
57
57
 
58
58
  def reduce_add(*args, start=None):
59
+ """Reduce elements using "add" operator (+)."""
59
60
  return _reduce(*args, start=start, op_fn=operator.add, type_=SupportsAdd)
60
61
 
61
62
 
@@ -85,6 +86,7 @@ def reduce_and(
85
86
 
86
87
 
87
88
  def reduce_and(*args, start=None):
89
+ """Reduce elements using "and" operator (&)."""
88
90
  return _reduce(*args, start=start, op_fn=operator.and_, type_=SupportsAnd)
89
91
 
90
92
 
@@ -114,6 +116,7 @@ def reduce_mul(
114
116
 
115
117
 
116
118
  def reduce_mul(*args, start=None):
119
+ """Reduce elements using "mul" operator (*)."""
117
120
  return _reduce(*args, start=start, op_fn=operator.mul, type_=SupportsMul)
118
121
 
119
122
 
@@ -143,6 +146,7 @@ def reduce_or(
143
146
 
144
147
 
145
148
  def reduce_or(*args, start=None):
149
+ """Reduce elements using "or" operator (|)."""
146
150
  return _reduce(*args, start=start, op_fn=operator.or_, type_=SupportsOr)
147
151
 
148
152
 
@@ -204,6 +208,7 @@ def sum(
204
208
 
205
209
 
206
210
  def sum(*args, start: Any = 0):
211
+ """Compute sum of elements."""
207
212
  return reduce_add(*args, start=start)
208
213
 
209
214
 
@@ -233,6 +238,7 @@ def prod(
233
238
 
234
239
 
235
240
  def prod(*args, start: Any = 1):
241
+ """Compute product of elements."""
236
242
  return reduce_mul(*args, start=start)
237
243
 
238
244