pythonwrench 0.6.0__tar.gz → 0.6.1__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 (81) hide show
  1. {pythonwrench-0.6.0/src/pythonwrench.egg-info → pythonwrench-0.6.1}/PKG-INFO +1 -1
  2. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/pyproject.toml +3 -0
  3. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/__init__.py +7 -1
  4. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/_core.py +6 -2
  5. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/argparse.py +131 -23
  6. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/concurrent.py +6 -2
  7. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/difflib.py +9 -6
  8. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/entrypoints/info.py +9 -11
  9. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/functools.py +11 -3
  10. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/importlib.py +39 -21
  11. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/typing/checks.py +4 -1
  12. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/warnings.py +16 -5
  13. {pythonwrench-0.6.0 → pythonwrench-0.6.1/src/pythonwrench.egg-info}/PKG-INFO +1 -1
  14. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench.egg-info/entry_points.txt +3 -0
  15. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_argparse.py +19 -0
  16. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_typing.py +11 -2
  17. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/LICENSE +0 -0
  18. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/README.md +0 -0
  19. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/setup.cfg +0 -0
  20. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/setup.py +0 -0
  21. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/__main__.py +0 -0
  22. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/abc.py +0 -0
  23. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/cast.py +0 -0
  24. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/checksum.py +0 -0
  25. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/collections/__init__.py +0 -0
  26. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/collections/collections.py +0 -0
  27. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/collections/prop.py +0 -0
  28. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/collections/reducers.py +0 -0
  29. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/csv.py +0 -0
  30. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/dataclasses.py +0 -0
  31. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/datetime.py +0 -0
  32. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/disk_cache.py +0 -0
  33. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  34. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/entrypoints/tree.py +0 -0
  35. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/enum.py +0 -0
  36. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/hashlib.py +0 -0
  37. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/inspect.py +0 -0
  38. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/json.py +0 -0
  39. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/jsonl.py +0 -0
  40. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/logging.py +0 -0
  41. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/math.py +0 -0
  42. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/os.py +0 -0
  43. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/pickle.py +0 -0
  44. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/random.py +0 -0
  45. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/re.py +0 -0
  46. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/semver.py +0 -0
  47. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/__init__.py +0 -0
  48. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/_core.py +0 -0
  49. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/csv.py +0 -0
  50. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/json.py +0 -0
  51. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/jsonl.py +0 -0
  52. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/serialization/pickle.py +0 -0
  53. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/time.py +0 -0
  54. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/typing/__init__.py +0 -0
  55. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench/typing/classes.py +0 -0
  56. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench.egg-info/SOURCES.txt +0 -0
  57. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  58. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench.egg-info/requires.txt +0 -0
  59. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/src/pythonwrench.egg-info/top_level.txt +0 -0
  60. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_abc.py +0 -0
  61. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_cast.py +0 -0
  62. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_checksum.py +0 -0
  63. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_collections.py +0 -0
  64. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_csv.py +0 -0
  65. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_dataclasses.py +0 -0
  66. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_difflib.py +0 -0
  67. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_disk_cache.py +0 -0
  68. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_entrypoints.py +0 -0
  69. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_enum.py +0 -0
  70. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_functools.py +0 -0
  71. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_hashlib.py +0 -0
  72. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_importlib.py +0 -0
  73. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_inspect.py +0 -0
  74. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_json.py +0 -0
  75. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_jsonl.py +0 -0
  76. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_logging.py +0 -0
  77. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_math.py +0 -0
  78. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_os.py +0 -0
  79. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_random.py +0 -0
  80. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_readme.py +0 -0
  81. {pythonwrench-0.6.0 → pythonwrench-0.6.1}/tests/test_semver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Python library with tools for typing, manipulating collections, and more!
5
5
  Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
6
6
  Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
@@ -45,6 +45,9 @@ Tracker = "https://github.com/Labbeti/pythonwrench/issues"
45
45
  pythonwrench-info = "pythonwrench.entrypoints.info:main_info"
46
46
  pythonwrench-tree = "pythonwrench.entrypoints.tree:main_tree"
47
47
  pythonwrench-safe-rmdir = "pythonwrench.entrypoints.safe_rmdir:main_safe_rmdir"
48
+ pw-info = "pythonwrench.entrypoints.info:main_info"
49
+ pw-tree = "pythonwrench.entrypoints.tree:main_tree"
50
+ pw-safe-rmdir = "pythonwrench.entrypoints.safe_rmdir:main_safe_rmdir"
48
51
  pyw-info = "pythonwrench.entrypoints.info:main_info"
49
52
  pyw-tree = "pythonwrench.entrypoints.tree:main_tree"
50
53
  pyw-safe-rmdir = "pythonwrench.entrypoints.safe_rmdir:main_safe_rmdir"
@@ -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.0"
12
+ __version__ = "0.6.1"
13
13
 
14
14
 
15
15
  from typing import TYPE_CHECKING
@@ -51,6 +51,7 @@ if TYPE_CHECKING or lazy is None:
51
51
  # Global library imports
52
52
  from .abc import Singleton
53
53
  from .argparse import (
54
+ add_dataclass_fields_to_parser,
54
55
  new_parser_from_dataclass,
55
56
  parse_args_using_dataclass,
56
57
  parse_to,
@@ -115,7 +116,9 @@ if TYPE_CHECKING or lazy is None:
115
116
  )
116
117
  from .hashlib import hash_file
117
118
  from .importlib import (
119
+ ModulePlaceholder,
118
120
  Placeholder,
121
+ import_if_available,
119
122
  is_available_package,
120
123
  is_editable_package,
121
124
  reload_editable_packages,
@@ -264,6 +267,7 @@ else:
264
267
  ],
265
268
  submod_attrs={
266
269
  "argparse": [
270
+ "add_dataclass_fields_to_parser",
267
271
  "new_parser_from_dataclass",
268
272
  "parse_args_using_dataclass",
269
273
  "parse_to",
@@ -355,6 +359,8 @@ else:
355
359
  ],
356
360
  "hashlib": ["hash_file"],
357
361
  "importlib": [
362
+ "import_if_available",
363
+ "ModulePlaceholder",
358
364
  "is_available_package",
359
365
  "is_editable_package",
360
366
  "reload_editable_packages",
@@ -39,10 +39,14 @@ def return_none(*args, **kwargs) -> None:
39
39
  def _decorator_factory(
40
40
  inner_fn: Optional[Callable[P, U]],
41
41
  *,
42
- pre_fn: Callable[..., Any] = return_none,
43
- post_fn: Callable[..., Any] = return_none,
42
+ pre_fn: Optional[Callable[..., Any]] = None,
43
+ post_fn: Optional[Callable[..., Any]] = None,
44
44
  ) -> Callable[[Callable[P, U]], Callable[P, U]]:
45
45
  """Deprecated decorator for function aliases."""
46
+ if pre_fn is None:
47
+ pre_fn = return_none
48
+ if post_fn is None:
49
+ post_fn = return_none
46
50
 
47
51
  def wrapper_factory(fn: Callable[P, U]) -> Callable[P, U]:
48
52
  if inner_fn is None:
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
+ import re
4
5
  from argparse import ArgumentParser
5
6
  from dataclasses import MISSING, fields
6
7
  from functools import partial
@@ -25,13 +26,15 @@ except ImportError:
25
26
  # support older python versions
26
27
  UnionType = Any
27
28
 
29
+ from pythonwrench.functools import filter_and_call
28
30
  from pythonwrench.typing.classes import Dataclass, DataclassInstance, NoneType
31
+ from pythonwrench.warnings import deprecated_alias
29
32
 
30
33
  T = TypeVar("T")
31
34
  T_Dataclass = TypeVar("T_Dataclass", bound=Dataclass)
32
35
  T_DataclassInstance = TypeVar("T_DataclassInstance", bound=DataclassInstance)
33
- TargetType = Union[Type[T], UnionType, "Type[Literal]"]
34
-
36
+ TargetType = Union[Type[T], UnionType, "Type[Literal]", "Type[Optional]"]
37
+ ListParsing = Literal["argparse", "brackets"]
35
38
 
36
39
  DEFAULT_TRUE_VALUES = ("True", "t", "yes", "y", "1")
37
40
  DEFAULT_FALSE_VALUES = ("False", "f", "no", "n", "0")
@@ -42,24 +45,41 @@ _SCALARS_TARGET_TYPES = (str, int, float, None, NoneType, bool)
42
45
 
43
46
  def parse_args_using_dataclass(
44
47
  dataclass_type: Type[T_DataclassInstance],
48
+ *,
45
49
  args: Optional[Iterable[str]] = None,
50
+ parser: Optional[ArgumentParser] = None,
51
+ list_parsing: ListParsing = "argparse",
46
52
  ) -> T_DataclassInstance:
47
53
  """Converts prog args to a typed dataclass using argparse.
48
54
 
49
55
  Currently only supports dataclasses that contains only builtin scalars: str, int, float, None, bool OR list of builtin scalars.
50
56
  """
51
- parser = ArgumentParser()
52
- parser = new_parser_from_dataclass(dataclass_type, parser)
57
+ init_parser = parser
58
+ parser = add_dataclass_fields_to_parser(
59
+ dataclass_type,
60
+ parser=parser,
61
+ list_parsing=list_parsing,
62
+ )
53
63
  parsed, argv = parser.parse_known_args(args)
54
64
  if len(argv) > 0:
55
65
  raise ValueError(f"Found {len(argv)} unknown arguments: {argv}.")
56
- instance = dataclass_type(**parsed.__dict__)
66
+
67
+ if init_parser is None:
68
+ instance = dataclass_type(**parsed.__dict__)
69
+ else:
70
+ instance = filter_and_call(
71
+ dataclass_type,
72
+ _fill_all_arguments=True,
73
+ **parsed.__dict__,
74
+ )
57
75
  return instance
58
76
 
59
77
 
60
- def new_parser_from_dataclass(
78
+ def add_dataclass_fields_to_parser(
61
79
  dataclass_type: Type[T_DataclassInstance],
80
+ *,
62
81
  parser: Optional[ArgumentParser],
82
+ list_parsing: ListParsing = "argparse",
63
83
  ) -> ArgumentParser:
64
84
  if parser is None:
65
85
  parser = ArgumentParser()
@@ -79,13 +99,21 @@ def new_parser_from_dataclass(
79
99
  msg = f"Invalid field {field.name}: found values for default and default_factory."
80
100
  raise ValueError(msg)
81
101
 
82
- kwds.update(_get_kwds_for_type(field.type))
102
+ inner_kwds = _get_kwds_for_type(field.type, list_parsing)
103
+ kwds.update(inner_kwds)
83
104
  parser.add_argument(*posargs, **kwds)
84
105
 
85
106
  return parser
86
107
 
87
108
 
88
- def _get_kwds_for_type(field_type: Any) -> Dict[str, Any]:
109
+ @deprecated_alias(add_dataclass_fields_to_parser)
110
+ def new_parser_from_dataclass(*args, **kwargs): ...
111
+
112
+
113
+ def _get_kwds_for_type(
114
+ field_type: Any,
115
+ list_parsing: ListParsing = "argparse",
116
+ ) -> Dict[str, Any]:
89
117
  kwds = {}
90
118
 
91
119
  type_origin = get_origin(field_type)
@@ -101,17 +129,49 @@ def _get_kwds_for_type(field_type: Any) -> Dict[str, Any]:
101
129
  msg = f"Invalid argument {field_type=}. (expected homogeneous types in {type_origin})"
102
130
  raise TypeError(msg)
103
131
 
104
- if field_type in _SCALARS_TARGET_TYPES or type_origin in (
105
- Literal,
106
- Optional,
107
- UnionType,
108
- Union,
132
+ if (
133
+ (field_type in _SCALARS_TARGET_TYPES)
134
+ or (
135
+ type_origin
136
+ in (
137
+ Literal,
138
+ Optional,
139
+ UnionType,
140
+ Union,
141
+ )
142
+ )
143
+ or (type_origin in (list, Iterable) and list_parsing == "brackets")
109
144
  ):
110
- kwds.update(_get_kwds_for_scalar_type(field_type, field_type))
111
- elif type_origin is list:
145
+ inner_kwds = _get_kwds_for_scalar_type(field_type, field_type, list_parsing)
146
+ kwds.update(inner_kwds)
147
+
148
+ elif type_origin in (list, Iterable):
112
149
  item_type = type_args[0]
113
- kwds.update(_get_kwds_for_scalar_type(item_type, field_type))
114
- kwds["nargs"] = "*"
150
+ inner_kwds = _get_kwds_for_scalar_type(item_type, field_type, list_parsing)
151
+ inner_kwds["nargs"] = "*"
152
+
153
+ # TODO: rm
154
+ # if list_parsing == "argparse":
155
+ # kwds["nargs"] = "*"
156
+ # elif list_parsing == "brackets":
157
+ # parse_fn = inner_kwds.pop("type")
158
+
159
+ # def brackets_parse(x: str) -> Any:
160
+ # x = (
161
+ # x.strip()
162
+ # .removeprefix("[")
163
+ # .removesuffix("]")
164
+ # .removesuffix(",")
165
+ # .strip()
166
+ # )
167
+ # return list(map(parse_fn, x.split(",")))
168
+
169
+ # inner_kwds["type"] = brackets_parse
170
+ # else:
171
+ # msg = f"Invalid argument {list_parsing=}. (expected one of {get_args(ListParsing)})"
172
+ # raise ValueError(msg)
173
+
174
+ kwds.update(inner_kwds)
115
175
  else:
116
176
  msg = f"Unsupported type {field_type}."
117
177
  raise TypeError(msg)
@@ -119,11 +179,20 @@ def _get_kwds_for_type(field_type: Any) -> Dict[str, Any]:
119
179
  return kwds
120
180
 
121
181
 
122
- def _get_kwds_for_scalar_type(type_: Any, from_field_type: Any) -> Dict[str, Any]:
182
+ def _get_kwds_for_scalar_type(
183
+ type_: Any, from_field_type: Any, list_parsing: ListParsing
184
+ ) -> Dict[str, Any]:
123
185
  type_origin = get_origin(type_)
124
186
  kwds = {}
125
187
 
126
- if type_ in _SCALARS_TARGET_TYPES or type_origin in (UnionType, Union, Optional):
188
+ if (
189
+ type_ in _SCALARS_TARGET_TYPES
190
+ or type_origin in (UnionType, Union, Optional)
191
+ or (
192
+ get_origin(from_field_type) in (list, Iterable)
193
+ and list_parsing == "brackets"
194
+ )
195
+ ):
127
196
  pass
128
197
  elif type_origin is Literal:
129
198
  type_args = get_args(type_)
@@ -132,7 +201,7 @@ def _get_kwds_for_scalar_type(type_: Any, from_field_type: Any) -> Dict[str, Any
132
201
  msg = f"Unsupported dataclass member type {type_} from {from_field_type}."
133
202
  raise TypeError(msg)
134
203
 
135
- kwds["type"] = parse_to(type_) # type: ignore
204
+ kwds["type"] = parse_to(type_, list_parsing=list_parsing) # type: ignore
136
205
  return kwds
137
206
 
138
207
 
@@ -143,6 +212,7 @@ def parse_to(
143
212
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
144
213
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
145
214
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
215
+ list_parsing: ListParsing = "argparse",
146
216
  ) -> Callable[[str], T]:
147
217
  """Returns a callable that convert string value to target type safely.
148
218
 
@@ -155,6 +225,7 @@ def parse_to(
155
225
  true_values=true_values,
156
226
  false_values=false_values,
157
227
  none_values=none_values,
228
+ list_parsing=list_parsing,
158
229
  )
159
230
 
160
231
 
@@ -166,6 +237,7 @@ def str_to_type(
166
237
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
167
238
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
168
239
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
240
+ list_parsing: ListParsing = "argparse",
169
241
  ) -> T:
170
242
  """Convert string values to target type safely. Intended for argparse arguments.
171
243
 
@@ -181,6 +253,7 @@ def str_to_type(
181
253
  true_values=true_values,
182
254
  false_values=false_values,
183
255
  none_values=none_values,
256
+ list_parsing=list_parsing,
184
257
  )
185
258
  if isinstance(result, Exception):
186
259
  raise result
@@ -303,12 +376,14 @@ def _str_to_type_impl(
303
376
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
304
377
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
305
378
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
379
+ list_parsing: ListParsing = "argparse",
306
380
  ) -> Union[T, Exception]:
307
381
  kwds: Dict[str, Any] = dict(
308
382
  case_sensitive=case_sensitive,
309
383
  true_values=true_values,
310
384
  false_values=false_values,
311
385
  none_values=none_values,
386
+ list_parsing=list_parsing,
312
387
  )
313
388
  if target_type in _SCALARS_TARGET_TYPES:
314
389
  return _str_to_scalar_impl(x, target_type, **kwds)
@@ -329,7 +404,31 @@ def _str_to_type_impl(
329
404
  raise ValueError(msg)
330
405
  return scalar
331
406
 
332
- elif getattr(target_type, "__name__", None) == "Optional":
407
+ if origin in (list, Iterable):
408
+ if list_parsing != "brackets":
409
+ raise ValueError
410
+
411
+ args = get_args(target_type)
412
+
413
+ if len(args) == 0:
414
+ target_item_type = str
415
+ elif len(args) == 1:
416
+ target_item_type = args[0]
417
+ else:
418
+ raise ValueError
419
+
420
+ x = re.sub(r"^\s*\[\s*(|.*[^,\s])(|\s*,)\s*\]\s*$", r"\1", x)
421
+ x_list = x.split(",")
422
+
423
+ y_list = []
424
+ for xi in x_list:
425
+ yi = _str_to_type_impl(xi, target_item_type, **kwds) # type: ignore
426
+ if isinstance(yi, Exception):
427
+ return yi
428
+ y_list.append(yi)
429
+ return y_list # type: ignore
430
+
431
+ if getattr(target_type, "__name__", None) == "Optional":
333
432
  args = (None,) + get_args(target_type)
334
433
  elif origin == Union or origin.__name__ in ("Union", "UnionType"): # type: ignore
335
434
  args = get_args(target_type)
@@ -362,23 +461,31 @@ def _str_to_scalar_impl(
362
461
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
363
462
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
364
463
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
464
+ list_parsing: ListParsing = "argparse",
365
465
  ) -> Any:
466
+ del list_parsing
366
467
  if target_type is str:
367
468
  return x
469
+
368
470
  elif target_type is int:
369
471
  try:
370
472
  return int(x)
371
473
  except ValueError as err:
372
474
  return err
475
+
373
476
  elif target_type is float:
374
477
  try:
375
478
  return float(x)
376
479
  except ValueError as err:
377
480
  return err
481
+
378
482
  elif target_type in (None, NoneType):
379
483
  return _str_to_none_impl(
380
- x, case_sensitive=case_sensitive, none_values=none_values
484
+ x,
485
+ case_sensitive=case_sensitive,
486
+ none_values=none_values,
381
487
  )
488
+
382
489
  elif target_type is bool:
383
490
  return _str_to_bool_impl(
384
491
  x,
@@ -387,7 +494,8 @@ def _str_to_scalar_impl(
387
494
  false_values=false_values,
388
495
  )
389
496
  else:
390
- raise ValueError(f"Invalid argument {target_type=}. (unsupported type)")
497
+ msg = f"Invalid argument {target_type=}. (unsupported type)"
498
+ raise ValueError(msg)
391
499
 
392
500
 
393
501
  def _str_to_bool_impl(
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
+ import copy
4
5
  import logging
5
6
  from concurrent.futures import Future, ThreadPoolExecutor
6
7
  from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, TypeVar
@@ -42,8 +43,11 @@ class ThreadPoolExecutorHelper(Generic[P, T]):
42
43
  executor_kwds = {}
43
44
  self.executor = ThreadPoolExecutor(**executor_kwds)
44
45
 
45
- kwargs = self.default_kwargs | kwargs # type: ignore
46
- future = self.executor.submit(self.fn, *args, **kwargs)
46
+ default_kwargs = copy.copy(self.default_kwargs)
47
+ default_kwargs.update(kwargs)
48
+ del kwargs
49
+
50
+ future = self.executor.submit(self.fn, *args, **default_kwargs)
47
51
  self.futures.append(future)
48
52
  return future
49
53
 
@@ -6,18 +6,16 @@ from difflib import SequenceMatcher
6
6
  from typing import Callable, Iterable, Optional
7
7
 
8
8
 
9
- def sequence_matcher_ratio(a: str, b: str) -> float:
10
- """Compute distance ratio of two strings."""
11
- return SequenceMatcher(None, a, b).ratio()
12
-
13
-
14
9
  def find_closest_in_list(
15
10
  x: str,
16
11
  lst: Iterable[str],
17
- sim_fn: Callable[[str, str], float] = sequence_matcher_ratio,
12
+ sim_fn: Optional[Callable[[str, str], float]] = None,
18
13
  higher_is_closer: bool = True,
19
14
  ) -> Optional[str]:
20
15
  """Find closest element in a list based on matches ratio."""
16
+ if sim_fn is None:
17
+ sim_fn = sequence_matcher_ratio
18
+
21
19
  best_sim = -int(higher_is_closer) * math.inf
22
20
  closest = None
23
21
 
@@ -34,3 +32,8 @@ def find_closest_in_list(
34
32
  raise ValueError(msg)
35
33
 
36
34
  return closest
35
+
36
+
37
+ def sequence_matcher_ratio(a: str, b: str) -> float:
38
+ """Compute distance ratio of two strings."""
39
+ return SequenceMatcher(None, a, b).ratio()
@@ -6,7 +6,7 @@ import platform
6
6
  import sys
7
7
  import warnings
8
8
  from pathlib import Path
9
- from typing import Dict, Union
9
+ from typing import Any, Dict
10
10
 
11
11
  import pythonwrench
12
12
  from pythonwrench.os import get_num_cpus_available
@@ -17,27 +17,25 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  def main_info() -> None:
19
19
  """Show main packages versions."""
20
- warnings.filterwarnings("ignore", category=UserWarning)
21
- install_info = get_install_info()
22
- warnings.filterwarnings("default", category=UserWarning)
20
+ with warnings.catch_warnings():
21
+ install_info = get_install_info()
23
22
  dumped = dumps_json(install_info, to_builtins=True)
24
23
  print(dumped)
25
24
 
26
25
 
27
- def get_install_info() -> Dict[str, Union[str, int]]:
26
+ def get_install_info() -> Dict[str, Any]:
28
27
  """Returns current installation information. Meant for debugging."""
29
- install_info = {
30
- "pythonwrench": pythonwrench.__version__,
31
- "python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
28
+ return {
32
29
  "os": platform.system(),
33
30
  "architecture": platform.architecture()[0],
34
31
  "num_cpus": get_num_cpus_available(),
35
- "package_path": get_package_repository_path(),
32
+ "python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
33
+ "pythonwrench": pythonwrench.__version__,
34
+ "pythonwrench_path": get_pythonwrench_repository_path(),
36
35
  }
37
- return install_info
38
36
 
39
37
 
40
- def get_package_repository_path() -> str:
38
+ def get_pythonwrench_repository_path() -> str:
41
39
  """Return the absolute path where the source code of this package is installed."""
42
40
  return str(Path(__file__).parent.parent.parent)
43
41
 
@@ -8,6 +8,7 @@ from typing import (
8
8
  Callable,
9
9
  Generic,
10
10
  Iterable,
11
+ Optional,
11
12
  Tuple,
12
13
  TypeVar,
13
14
  overload,
@@ -113,7 +114,9 @@ compose = Compose # type: ignore
113
114
 
114
115
 
115
116
  def filter_and_call(
116
- fn: Callable[..., T], _fill_all_arguments: bool = False, **kwargs: Any
117
+ fn: Callable[..., T],
118
+ _fill_all_arguments: bool = False,
119
+ **kwargs: Any,
117
120
  ) -> T:
118
121
  """Call object only with the valid keyword arguments. Non-valid arguments are ignored.
119
122
 
@@ -158,7 +161,12 @@ def filter_and_call(
158
161
  return result
159
162
 
160
163
 
161
- def function_alias(alternative: Callable[P, U]) -> Callable[..., Callable[P, U]]:
164
+ def function_alias(
165
+ alternative: Callable[P, U],
166
+ *,
167
+ pre_fn: Optional[Callable[..., Any]] = None,
168
+ post_fn: Optional[Callable[..., Any]] = None,
169
+ ) -> Callable[..., Callable[P, U]]:
162
170
  """Decorator to wrap function aliases.
163
171
 
164
172
  Example
@@ -173,7 +181,7 @@ def function_alias(alternative: Callable[P, U]) -> Callable[..., Callable[P, U]]
173
181
  ... "bbb"
174
182
 
175
183
  """
176
- return _decorator_factory(alternative)
184
+ return _decorator_factory(alternative, pre_fn=pre_fn, post_fn=post_fn)
177
185
 
178
186
 
179
187
  def identity(x: T, **kwargs) -> T:
@@ -153,27 +153,6 @@ def reload_editable_packages(*, verbose: int = 0) -> List[ModuleType]:
153
153
  )
154
154
 
155
155
 
156
- class Placeholder:
157
- """Placeholder object. All instances attributes always returns the object itself."""
158
-
159
- def __init__(self, *args, **kwargs) -> None: ...
160
-
161
- def __getattr__(self, name: str) -> Any:
162
- return self
163
-
164
- def __call__(self, *args, **kwargs) -> Any:
165
- return self
166
-
167
- def __getitem__(self, *args, **kwargs) -> Any:
168
- return self
169
-
170
- def __eq__(self, other) -> Any:
171
- return self == other
172
-
173
- def __ne__(self, other) -> Any:
174
- return self != other
175
-
176
-
177
156
  def requires_packages(
178
157
  arg0: Union[Iterable[str], str],
179
158
  /,
@@ -213,3 +192,42 @@ def requires_packages(
213
192
  return _impl
214
193
 
215
194
  return _wrap
195
+
196
+
197
+ class Placeholder:
198
+ """Placeholder object. All instances attributes always returns the object itself."""
199
+
200
+ def __init__(self, *args, **kwargs) -> None:
201
+ super().__init__()
202
+ self.__excluded_self_attrs = [
203
+ "__file__",
204
+ "__eq__",
205
+ "__ne__",
206
+ "__str__",
207
+ "__repr__",
208
+ ]
209
+
210
+ def __getattr__(self, name: str) -> Any:
211
+ if name in self.__excluded_self_attrs:
212
+ return self.__getattribute__(name)
213
+ else:
214
+ return self
215
+
216
+ def __call__(self, *args, **kwargs) -> Any:
217
+ return self
218
+
219
+ def __getitem__(self, *args, **kwargs) -> Any:
220
+ return self
221
+
222
+
223
+ class ModulePlaceholder(ModuleType, Placeholder):
224
+ def __init__(self, *args, **kwargs) -> None:
225
+ Placeholder.__init__(self, *args, **kwargs)
226
+ ModuleType.__init__(self, *args, **kwargs)
227
+
228
+
229
+ def import_if_available(name: str) -> ModuleType:
230
+ if is_available_package(name):
231
+ return __import__(name)
232
+ else:
233
+ return ModulePlaceholder(name)
@@ -199,7 +199,10 @@ def isinstance_generic(
199
199
  return False
200
200
 
201
201
  if check_only_first:
202
- return isinstance_generic(next(iter(obj)), args[0])
202
+ try:
203
+ return isinstance_generic(next(iter(obj)), args[0])
204
+ except StopIteration:
205
+ return True
203
206
  else:
204
207
  return all(isinstance_generic(xi, args[0]) for xi in obj)
205
208
 
@@ -7,7 +7,7 @@ from typing import Any, Callable, Optional, Type, TypeVar, Union, overload
7
7
 
8
8
  from typing_extensions import ParamSpec
9
9
 
10
- from pythonwrench._core import _decorator_factory
10
+ from pythonwrench._core import _decorator_factory, return_none
11
11
 
12
12
  P = ParamSpec("P")
13
13
  U = TypeVar("U")
@@ -46,15 +46,21 @@ def deprecated_alias(
46
46
  alternative: Callable[P, U],
47
47
  msg_fmt: str = "Deprecated call to '{fn_name}', use '{alternative_name}' instead.",
48
48
  warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
49
+ *,
50
+ pre_fn: Optional[Callable[..., Any]] = None,
51
+ post_fn: Optional[Callable[..., Any]] = None,
49
52
  ) -> Callable[..., Callable[P, U]]:
50
53
  """Decorator to wrap deprecated function aliases."""
51
54
  alternative_name = alternative.__name__ if alternative is not None else "None"
55
+ if pre_fn is None:
56
+ pre_fn = return_none
52
57
 
53
- def pre_fn(fn, *args, **kwargs):
58
+ def inner_pre_fn(fn, *args, **kwargs) -> None:
54
59
  msg = msg_fmt.format(fn_name=fn.__name__, alternative_name=alternative_name)
55
60
  warn_fn(msg)
61
+ pre_fn(fn, *args, **kwargs)
56
62
 
57
- return _decorator_factory(alternative, pre_fn=pre_fn)
63
+ return _decorator_factory(alternative, pre_fn=inner_pre_fn, post_fn=post_fn)
58
64
 
59
65
 
60
66
  @overload
@@ -83,14 +89,19 @@ def deprecated_function(
83
89
  *,
84
90
  msg_fmt: str = "Deprecated call to '{fn_name}'.",
85
91
  warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
92
+ pre_fn: Optional[Callable[..., Any]] = None,
93
+ post_fn: Optional[Callable[..., Any]] = None,
86
94
  ) -> Callable:
87
95
  """Decorator to wrap deprecated functions."""
96
+ if pre_fn is None:
97
+ pre_fn = return_none
88
98
 
89
- def pre_fn(fn, *args, **kwargs):
99
+ def inner_pre_fn(fn, *args, **kwargs):
90
100
  msg = msg_fmt.format(fn_name=fn.__qualname__)
91
101
  warn_fn(msg)
102
+ pre_fn(fn, *args, **kwargs)
92
103
 
93
- decorator = _decorator_factory(None, pre_fn=pre_fn)
104
+ decorator = _decorator_factory(None, pre_fn=inner_pre_fn, post_fn=post_fn)
94
105
  if fn is None:
95
106
  return decorator
96
107
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Python library with tools for typing, manipulating collections, and more!
5
5
  Author-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
6
6
  Maintainer-email: "Étienne Labbé (Labbeti)" <labbeti.pub@gmail.com>
@@ -1,4 +1,7 @@
1
1
  [console_scripts]
2
+ pw-info = pythonwrench.entrypoints.info:main_info
3
+ pw-safe-rmdir = pythonwrench.entrypoints.safe_rmdir:main_safe_rmdir
4
+ pw-tree = pythonwrench.entrypoints.tree:main_tree
2
5
  pythonwrench-info = pythonwrench.entrypoints.info:main_info
3
6
  pythonwrench-safe-rmdir = pythonwrench.entrypoints.safe_rmdir:main_safe_rmdir
4
7
  pythonwrench-tree = pythonwrench.entrypoints.tree:main_tree
@@ -130,6 +130,25 @@ class TestArgparse(TestCase):
130
130
  with self.assertRaises(SystemExit):
131
131
  _ = parse_args_using_dataclass(D, args=["--arg_d", "1.1"])
132
132
 
133
+ def test_parse_args_using_dataclass_example_4(self) -> None:
134
+ @dataclass
135
+ class A:
136
+ nums: List[int] = field(default_factory=list)
137
+
138
+ target = A(nums=[1, -1, 10])
139
+
140
+ nums_args = list(map(str, target.nums))
141
+ result = parse_args_using_dataclass(
142
+ A, args=["--nums"] + nums_args, list_parsing="argparse"
143
+ )
144
+ assert target == result
145
+
146
+ nums_args = [f"[{','.join(map(str, target.nums))}]"]
147
+ result = parse_args_using_dataclass(
148
+ A, args=["--nums"] + nums_args, list_parsing="brackets"
149
+ )
150
+ assert target == result
151
+
133
152
 
134
153
  if __name__ == "__main__":
135
154
  unittest.main()
@@ -22,10 +22,10 @@ from typing import (
22
22
  )
23
23
  from unittest import TestCase
24
24
 
25
- import pandas as pd
26
25
  import typing_extensions
27
26
  from typing_extensions import NotRequired
28
27
 
28
+ from pythonwrench.importlib import import_if_available, is_available_package
29
29
  from pythonwrench.typing import (
30
30
  NoneType,
31
31
  check_args_types,
@@ -39,6 +39,8 @@ from pythonwrench.typing import (
39
39
  isinstance_generic,
40
40
  )
41
41
 
42
+ pd = import_if_available("pandas")
43
+
42
44
 
43
45
  class ExampleDict(TypedDict):
44
46
  a: int
@@ -125,7 +127,8 @@ class TestChecks(TestCase):
125
127
  assert is_special_form(Any)
126
128
  assert is_special_form(typing_extensions.Any)
127
129
 
128
- assert not is_special_form(pd.DataFrame({"a": [1, 2, 3]}))
130
+ if is_available_package("pandas"):
131
+ assert not is_special_form(pd.DataFrame({"a": [1, 2, 3]}))
129
132
 
130
133
 
131
134
  class TestIsInstanceGuard(TestCase):
@@ -314,6 +317,12 @@ class TestIsInstanceGuard(TestCase):
314
317
  with self.assertRaises(NotImplementedError):
315
318
  assert isinstance_generic(Path, Callable[[str], Path])
316
319
 
320
+ def test_check_only_first(self) -> None:
321
+ assert isinstance_generic([1, 2], List[int], check_only_first=True)
322
+ assert isinstance_generic([1, "2", 1], List[int], check_only_first=True)
323
+ assert isinstance_generic([], List[int], check_only_first=True)
324
+ assert isinstance_generic([1, 2], List, check_only_first=True)
325
+
317
326
 
318
327
  class TestCheckArgsType(TestCase):
319
328
  def test_example_1(self) -> None:
File without changes
File without changes
File without changes
File without changes