pythonwrench 0.6.1__tar.gz → 0.6.2__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 (83) hide show
  1. {pythonwrench-0.6.1/src/pythonwrench.egg-info → pythonwrench-0.6.2}/PKG-INFO +1 -1
  2. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/__init__.py +5 -3
  3. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/_core.py +7 -6
  4. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/argparse.py +58 -48
  5. pythonwrench-0.6.2/src/pythonwrench/dataclasses.py +109 -0
  6. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/functools.py +6 -4
  7. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/_core.py +12 -8
  8. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/csv.py +1 -1
  9. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/json.py +1 -1
  10. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/jsonl.py +1 -1
  11. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/pickle.py +1 -1
  12. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/__init__.py +2 -0
  13. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/checks.py +5 -0
  14. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/warnings.py +8 -8
  15. {pythonwrench-0.6.1 → pythonwrench-0.6.2/src/pythonwrench.egg-info}/PKG-INFO +1 -1
  16. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_argparse.py +89 -2
  17. pythonwrench-0.6.2/tests/test_dataclasses.py +89 -0
  18. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_disk_cache.py +2 -1
  19. pythonwrench-0.6.1/src/pythonwrench/dataclasses.py +0 -25
  20. pythonwrench-0.6.1/tests/test_dataclasses.py +0 -33
  21. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/LICENSE +0 -0
  22. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/README.md +0 -0
  23. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/pyproject.toml +0 -0
  24. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/setup.cfg +0 -0
  25. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/setup.py +0 -0
  26. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/__main__.py +0 -0
  27. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/abc.py +0 -0
  28. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/cast.py +0 -0
  29. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/checksum.py +0 -0
  30. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/__init__.py +0 -0
  31. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/collections.py +0 -0
  32. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/prop.py +0 -0
  33. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/reducers.py +0 -0
  34. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/concurrent.py +0 -0
  35. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/csv.py +0 -0
  36. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/datetime.py +0 -0
  37. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/difflib.py +0 -0
  38. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/disk_cache.py +0 -0
  39. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/info.py +0 -0
  40. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  41. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/tree.py +0 -0
  42. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/enum.py +0 -0
  43. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/hashlib.py +0 -0
  44. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/importlib.py +0 -0
  45. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/inspect.py +0 -0
  46. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/json.py +0 -0
  47. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/jsonl.py +0 -0
  48. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/logging.py +0 -0
  49. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/math.py +0 -0
  50. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/os.py +0 -0
  51. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/pickle.py +0 -0
  52. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/random.py +0 -0
  53. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/re.py +0 -0
  54. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/semver.py +0 -0
  55. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/__init__.py +0 -0
  56. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/time.py +0 -0
  57. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/classes.py +0 -0
  58. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/SOURCES.txt +0 -0
  59. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  60. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/entry_points.txt +0 -0
  61. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/requires.txt +0 -0
  62. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
  63. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_abc.py +0 -0
  64. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_cast.py +0 -0
  65. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_checksum.py +0 -0
  66. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_collections.py +0 -0
  67. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_csv.py +0 -0
  68. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_difflib.py +0 -0
  69. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_entrypoints.py +0 -0
  70. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_enum.py +0 -0
  71. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_functools.py +0 -0
  72. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_hashlib.py +0 -0
  73. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_importlib.py +0 -0
  74. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_inspect.py +0 -0
  75. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_json.py +0 -0
  76. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_jsonl.py +0 -0
  77. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_logging.py +0 -0
  78. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_math.py +0 -0
  79. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_os.py +0 -0
  80. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_random.py +0 -0
  81. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_readme.py +0 -0
  82. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_semver.py +0 -0
  83. {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_typing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.6.1
3
+ Version: 0.6.2
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>
@@ -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.1"
12
+ __version__ = "0.6.2"
13
13
 
14
14
 
15
15
  from typing import TYPE_CHECKING
@@ -101,7 +101,7 @@ if TYPE_CHECKING or lazy is None:
101
101
  union_lists,
102
102
  unzip,
103
103
  )
104
- from .dataclasses import get_defaults_values
104
+ from .dataclasses import add_dict_methods, dataclassdict, get_defaults_values
105
105
  from .datetime import get_now, get_now_iso8601
106
106
  from .difflib import find_closest_in_list, sequence_matcher_ratio
107
107
  from .disk_cache import disk_cache_call, disk_cache_decorator
@@ -219,6 +219,7 @@ if TYPE_CHECKING or lazy is None:
219
219
  is_builtin_scalar,
220
220
  is_collection_alias,
221
221
  is_dataclass_instance,
222
+ is_dataclass_type,
222
223
  is_iterable_bool,
223
224
  is_iterable_bytes_or_list,
224
225
  is_iterable_float,
@@ -344,7 +345,7 @@ else:
344
345
  "save_jsonl",
345
346
  "save_pickle",
346
347
  ],
347
- "dataclasses": ["get_defaults_values"],
348
+ "dataclasses": ["add_dict_methods", "dataclassdict", "get_defaults_values"],
348
349
  "datetime": ["get_now", "get_now_iso8601"],
349
350
  "difflib": ["find_closest_in_list", "sequence_matcher_ratio"],
350
351
  "disk_cache": ["disk_cache_call", "disk_cache_decorator"],
@@ -436,6 +437,7 @@ else:
436
437
  "is_builtin_scalar",
437
438
  "is_collection_alias",
438
439
  "is_dataclass_instance",
440
+ "is_dataclass_type",
439
441
  "is_iterable_bool",
440
442
  "is_iterable_bytes_or_list",
441
443
  "is_iterable_float",
@@ -22,6 +22,7 @@ T = TypeVar("T", covariant=True)
22
22
  U = TypeVar("U", covariant=True)
23
23
  T_Output = TypeVar("T_Output")
24
24
  T_Any = TypeVar("T_Any", contravariant=True, default=Any)
25
+ T_Function = TypeVar("T_Function", bound=Callable[..., Any])
25
26
 
26
27
  UnkMode = Literal["identity", "error"]
27
28
  ClassOrTuple = Union[type, Tuple[type, ...]]
@@ -37,31 +38,31 @@ def return_none(*args, **kwargs) -> None:
37
38
 
38
39
 
39
40
  def _decorator_factory(
40
- inner_fn: Optional[Callable[P, U]],
41
+ inner_fn: Optional[T_Function],
41
42
  *,
42
43
  pre_fn: Optional[Callable[..., Any]] = None,
43
44
  post_fn: Optional[Callable[..., Any]] = None,
44
- ) -> Callable[[Callable[P, U]], Callable[P, U]]:
45
- """Deprecated decorator for function aliases."""
45
+ ) -> Callable[[T_Function], T_Function]:
46
+ """Decorator for function aliases."""
46
47
  if pre_fn is None:
47
48
  pre_fn = return_none
48
49
  if post_fn is None:
49
50
  post_fn = return_none
50
51
 
51
- def wrapper_factory(fn: Callable[P, U]) -> Callable[P, U]:
52
+ def wrapper_factory(fn: T_Function) -> T_Function:
52
53
  if inner_fn is None:
53
54
  _inner_fn = fn
54
55
  else:
55
56
  _inner_fn = inner_fn
56
57
 
57
58
  @wraps(_inner_fn)
58
- def wrapped(*args: P.args, **kwargs: P.kwargs) -> U:
59
+ def wrapped(*args, **kwargs):
59
60
  pre_fn(fn, *args, **kwargs)
60
61
  result = _inner_fn(*args, **kwargs)
61
62
  post_fn(fn, *args, **kwargs)
62
63
  return result
63
64
 
64
- return wrapped
65
+ return wrapped # type: ignore
65
66
 
66
67
  return wrapper_factory
67
68
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  import re
5
5
  from argparse import ArgumentParser
6
+ from collections.abc import Iterable as _Iterable
6
7
  from dataclasses import MISSING, fields
7
8
  from functools import partial
8
9
  from typing import (
@@ -13,6 +14,7 @@ from typing import (
13
14
  List,
14
15
  Literal,
15
16
  Optional,
17
+ Tuple,
16
18
  Type,
17
19
  TypeVar,
18
20
  Union,
@@ -33,7 +35,9 @@ from pythonwrench.warnings import deprecated_alias
33
35
  T = TypeVar("T")
34
36
  T_Dataclass = TypeVar("T_Dataclass", bound=Dataclass)
35
37
  T_DataclassInstance = TypeVar("T_DataclassInstance", bound=DataclassInstance)
36
- TargetType = Union[Type[T], UnionType, "Type[Literal]", "Type[Optional]"]
38
+ TargetType = Union[
39
+ Type[T], UnionType, "Type[Literal]", "Type[Optional]", Tuple[type, ...]
40
+ ]
37
41
  ListParsing = Literal["argparse", "brackets"]
38
42
 
39
43
  DEFAULT_TRUE_VALUES = ("True", "t", "yes", "y", "1")
@@ -49,6 +53,7 @@ def parse_args_using_dataclass(
49
53
  args: Optional[Iterable[str]] = None,
50
54
  parser: Optional[ArgumentParser] = None,
51
55
  list_parsing: ListParsing = "argparse",
56
+ add_dashed_arg: bool = True,
52
57
  ) -> T_DataclassInstance:
53
58
  """Converts prog args to a typed dataclass using argparse.
54
59
 
@@ -59,6 +64,7 @@ def parse_args_using_dataclass(
59
64
  dataclass_type,
60
65
  parser=parser,
61
66
  list_parsing=list_parsing,
67
+ add_dashed_arg=add_dashed_arg,
62
68
  )
63
69
  parsed, argv = parser.parse_known_args(args)
64
70
  if len(argv) > 0:
@@ -80,6 +86,7 @@ def add_dataclass_fields_to_parser(
80
86
  *,
81
87
  parser: Optional[ArgumentParser],
82
88
  list_parsing: ListParsing = "argparse",
89
+ add_dashed_arg: bool = True,
83
90
  ) -> ArgumentParser:
84
91
  if parser is None:
85
92
  parser = ArgumentParser()
@@ -87,6 +94,9 @@ def add_dataclass_fields_to_parser(
87
94
  for field in fields(dataclass_type):
88
95
  kwds = {}
89
96
  posargs = [f"--{field.name}"]
97
+ if add_dashed_arg and "_" in field.name:
98
+ dashed_arg_name = field.name.replace("_", "-")
99
+ posargs.append(f"--{dashed_arg_name}")
90
100
 
91
101
  if field.default is MISSING and field.default_factory is MISSING:
92
102
  kwds["required"] = True
@@ -99,7 +109,12 @@ def add_dataclass_fields_to_parser(
99
109
  msg = f"Invalid field {field.name}: found values for default and default_factory."
100
110
  raise ValueError(msg)
101
111
 
102
- inner_kwds = _get_kwds_for_type(field.type, list_parsing)
112
+ try:
113
+ inner_kwds = _get_kwds_for_type(field.type, list_parsing)
114
+ except (ValueError, TypeError, RuntimeError) as err:
115
+ msg = f"Invalid field {field.name}: field type '{field.type}' is not supported."
116
+ raise type(err)(msg) from err
117
+
103
118
  kwds.update(inner_kwds)
104
119
  parser.add_argument(*posargs, **kwds)
105
120
 
@@ -124,10 +139,6 @@ def _get_kwds_for_type(
124
139
  if not all(type(arg) in _SCALARS_TARGET_TYPES for arg in type_args):
125
140
  msg = f"Invalid argument {field_type=}. (expected homogeneous types in {type_origin})"
126
141
  raise TypeError(msg)
127
- elif type_origin in (UnionType, Union):
128
- if not all(arg in _SCALARS_TARGET_TYPES for arg in type_args):
129
- msg = f"Invalid argument {field_type=}. (expected homogeneous types in {type_origin})"
130
- raise TypeError(msg)
131
142
 
132
143
  if (
133
144
  (field_type in _SCALARS_TARGET_TYPES)
@@ -140,47 +151,27 @@ def _get_kwds_for_type(
140
151
  Union,
141
152
  )
142
153
  )
143
- or (type_origin in (list, Iterable) and list_parsing == "brackets")
154
+ or (_is_iterable_type_like(type_origin) and list_parsing == "brackets")
144
155
  ):
145
156
  inner_kwds = _get_kwds_for_scalar_type(field_type, field_type, list_parsing)
146
157
  kwds.update(inner_kwds)
147
158
 
148
- elif type_origin in (list, Iterable):
159
+ elif _is_iterable_type_like(type_origin):
149
160
  item_type = type_args[0]
150
161
  inner_kwds = _get_kwds_for_scalar_type(item_type, field_type, list_parsing)
151
162
  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
163
  kwds.update(inner_kwds)
175
164
  else:
176
- msg = f"Unsupported type {field_type}."
165
+ msg = f"Unsupported type {field_type}. (with {type_origin=})"
177
166
  raise TypeError(msg)
178
167
 
179
168
  return kwds
180
169
 
181
170
 
182
171
  def _get_kwds_for_scalar_type(
183
- type_: Any, from_field_type: Any, list_parsing: ListParsing
172
+ type_: Any,
173
+ from_field_type: Any,
174
+ list_parsing: ListParsing,
184
175
  ) -> Dict[str, Any]:
185
176
  type_origin = get_origin(type_)
186
177
  kwds = {}
@@ -189,11 +180,12 @@ def _get_kwds_for_scalar_type(
189
180
  type_ in _SCALARS_TARGET_TYPES
190
181
  or type_origin in (UnionType, Union, Optional)
191
182
  or (
192
- get_origin(from_field_type) in (list, Iterable)
183
+ _is_iterable_type_like(get_origin(from_field_type))
193
184
  and list_parsing == "brackets"
194
185
  )
195
186
  ):
196
187
  pass
188
+
197
189
  elif type_origin is Literal:
198
190
  type_args = get_args(type_)
199
191
  kwds["choices"] = type_args
@@ -393,18 +385,22 @@ def _str_to_type_impl(
393
385
  if origin is Literal:
394
386
  args = get_args(target_type)
395
387
  literal_types = {type(value) for value in args}
396
- if len(literal_types) != 1:
397
- msg = f"Mixed Literal are not supported: {args}"
398
- raise TypeError(msg)
399
388
 
400
- literal_type = next(iter(literal_types))
401
- scalar = _str_to_scalar_impl(x, literal_type, **kwds)
389
+ if len(literal_types) == 0:
390
+ msg = "Invalid number of arguments in Literal. (expected at least 1)"
391
+ raise ValueError(msg)
392
+ elif len(literal_types) == 1:
393
+ literal_type = next(iter(literal_types))
394
+ scalar = _str_to_scalar_impl(x, literal_type, **kwds)
395
+ else:
396
+ scalar = _str_to_type_impl(x, tuple(literal_types), **kwds)
397
+
402
398
  if scalar not in args:
403
399
  msg = f"Cannot convert {x} to Literal[{', '.join(args)}]"
404
400
  raise ValueError(msg)
405
401
  return scalar
406
402
 
407
- if origin in (list, Iterable):
403
+ if _is_iterable_type_like(origin):
408
404
  if list_parsing != "brackets":
409
405
  raise ValueError
410
406
 
@@ -417,18 +413,28 @@ def _str_to_type_impl(
417
413
  else:
418
414
  raise ValueError
419
415
 
420
- x = re.sub(r"^\s*\[\s*(|.*[^,\s])(|\s*,)\s*\]\s*$", r"\1", x)
416
+ pattern = r"^\s*\[\s*(|.*[^,\s])(|\s*,)\s*\]\s*$"
417
+ if re.match(pattern, x) is None:
418
+ msg = f"Cannot convert value to list: '{x}'. (with {list_parsing=})"
419
+ return ValueError(msg)
420
+
421
+ x = re.sub(pattern, r"\1", x)
422
+ if x == "":
423
+ return [] # type: ignore
424
+
421
425
  x_list = x.split(",")
422
426
 
423
- y_list = []
427
+ output_list = []
424
428
  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":
429
+ output_i = _str_to_type_impl(xi, target_item_type, **kwds) # type: ignore
430
+ if isinstance(output_i, Exception):
431
+ return output_i
432
+ output_list.append(output_i)
433
+ return output_list # type: ignore
434
+
435
+ if isinstance(target_type, tuple):
436
+ args = target_type
437
+ elif getattr(target_type, "__name__", None) == "Optional":
432
438
  args = (None,) + get_args(target_type)
433
439
  elif origin == Union or origin.__name__ in ("Union", "UnionType"): # type: ignore
434
440
  args = get_args(target_type)
@@ -453,6 +459,10 @@ def _str_to_type_impl(
453
459
  return ValueError(f"Invalid argument {x=} with {target_type=}.")
454
460
 
455
461
 
462
+ def _is_iterable_type_like(x: Any) -> bool:
463
+ return x in (list, Iterable, _Iterable)
464
+
465
+
456
466
  def _str_to_scalar_impl(
457
467
  x: str,
458
468
  target_type: TargetType[T],
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from dataclasses import MISSING, dataclass, is_dataclass # noqa: F401
5
+ from typing import Any, Dict, Type, TypeVar, cast
6
+
7
+ from typing_extensions import dataclass_transform
8
+
9
+ from pythonwrench.typing.checks import ( # noqa: F401
10
+ is_dataclass_instance,
11
+ is_dataclass_type,
12
+ )
13
+ from pythonwrench.typing.classes import Dataclass, DataclassInstance # noqa: F401
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ @dataclass_transform()
19
+ def dataclassdict(cls: Type[T]) -> Type[T]:
20
+ """Decorate a class so it becomes both a dataclass and a dictionary."""
21
+ return add_dict_methods(dataclass(cls))
22
+
23
+
24
+ def add_dict_methods(cls: Type[T]) -> Type[T]:
25
+ """Return a dictionary subclass of an already-defined dataclass.
26
+
27
+ Field values are kept synchronized between attribute and mapping access.
28
+ """
29
+ if not is_dataclass_type(cls):
30
+ raise TypeError("add_dict_methods expects a dataclass type.")
31
+
32
+ dataclass_cls = cls
33
+ conflicting_fields = sorted(
34
+ set(dataclass_cls.__dataclass_fields__).intersection(dir(dict)) # type: ignore
35
+ )
36
+ if conflicting_fields:
37
+ names = ", ".join(repr(name) for name in conflicting_fields)
38
+ msg = f"Dataclass fields conflict with dict attributes: {names}."
39
+ raise RuntimeError(msg)
40
+
41
+ def setattr_(self: Any, name: str, value: Any) -> None:
42
+ object.__setattr__(self, name, value)
43
+ if name in self.__dataclass_fields__:
44
+ dict.__setitem__(self, name, value)
45
+
46
+ def setitem(self: Any, key: Any, value: Any) -> None:
47
+ dict.__setitem__(self, key, value)
48
+ if key in self.__dataclass_fields__:
49
+ object.__setattr__(self, key, value)
50
+
51
+ def delitem(self: Any, key: Any) -> None:
52
+ dict.__delitem__(self, key)
53
+ if key in self.__dataclass_fields__ and hasattr(self, key):
54
+ object.__delattr__(self, key)
55
+
56
+ def update(self: Any, *args: Any, **kwargs: Any) -> None:
57
+ for key, value in dict(*args, **kwargs).items():
58
+ setitem(self, key, value)
59
+
60
+ def setdefault(self: Any, key: Any, default: Any = None) -> Any:
61
+ if key not in self:
62
+ setitem(self, key, default)
63
+ return self[key]
64
+
65
+ def pop(self: Any, key: Any, *default: Any) -> Any:
66
+ if len(default) > 1:
67
+ raise TypeError("pop expected at most 2 arguments")
68
+ if key not in self:
69
+ if default:
70
+ return default[0]
71
+ raise KeyError(key)
72
+ value = self[key]
73
+ delitem(self, key)
74
+ return value
75
+
76
+ def clear(self: Any) -> None:
77
+ for key in list(self):
78
+ delitem(self, key)
79
+
80
+ namespace = {
81
+ "__module__": cls.__module__,
82
+ "__doc__": cls.__doc__,
83
+ "__setattr__": setattr_,
84
+ "__setitem__": setitem,
85
+ "__delitem__": delitem,
86
+ "update": update,
87
+ "setdefault": setdefault,
88
+ "pop": pop,
89
+ "clear": clear,
90
+ }
91
+ result = type(cls.__name__, (dataclass_cls, dict), namespace) # type: ignore
92
+ result.__qualname__ = cls.__qualname__
93
+ result = dataclass(result)
94
+ return cast(Type[T], result)
95
+
96
+
97
+ def get_defaults_values(obj: DataclassInstance) -> Dict[str, Any]:
98
+ defaults = {}
99
+
100
+ for field in obj.__dataclass_fields__.values():
101
+ if callable(field.default_factory):
102
+ default = field.default_factory()
103
+ else:
104
+ default = field.default
105
+
106
+ if default != MISSING:
107
+ defaults[field.name] = default
108
+
109
+ return defaults
@@ -16,13 +16,13 @@ from typing import (
16
16
 
17
17
  from typing_extensions import ParamSpec
18
18
 
19
- from pythonwrench._core import _decorator_factory, return_none # noqa: F401
19
+ from pythonwrench._core import T_Function, _decorator_factory, return_none # noqa: F401
20
20
  from pythonwrench.inspect import get_argnames, get_fullname
21
21
  from pythonwrench.typing import isinstance_generic
22
22
 
23
23
  T = TypeVar("T")
24
- P = ParamSpec("P")
25
24
  U = TypeVar("U")
25
+ P = ParamSpec("P")
26
26
 
27
27
 
28
28
  class Compose(Generic[T, U]):
@@ -162,13 +162,15 @@ def filter_and_call(
162
162
 
163
163
 
164
164
  def function_alias(
165
- alternative: Callable[P, U],
165
+ alternative: T_Function,
166
166
  *,
167
167
  pre_fn: Optional[Callable[..., Any]] = None,
168
168
  post_fn: Optional[Callable[..., Any]] = None,
169
- ) -> Callable[..., Callable[P, U]]:
169
+ ) -> Callable[..., T_Function]:
170
170
  """Decorator to wrap function aliases.
171
171
 
172
+ Unlike setting directly an alias, this preserve the nature of the alias for language server.
173
+
172
174
  Example
173
175
  -------
174
176
  >>> def f(a: int, b: str) -> str:
@@ -13,8 +13,9 @@ from typing import (
13
13
  @overload
14
14
  def _setup_output_fpath(
15
15
  fpath: Union[str, Path, os.PathLike],
16
- overwrite: bool,
17
- make_parents: bool,
16
+ *,
17
+ overwrite: bool = True,
18
+ make_parents: bool = True,
18
19
  absolute: bool = True,
19
20
  ) -> Path: ...
20
21
 
@@ -22,8 +23,9 @@ def _setup_output_fpath(
22
23
  @overload
23
24
  def _setup_output_fpath(
24
25
  fpath: TextIOWrapper,
25
- overwrite: bool,
26
- make_parents: bool,
26
+ *,
27
+ overwrite: bool = True,
28
+ make_parents: bool = True,
27
29
  absolute: bool = True,
28
30
  ) -> TextIOWrapper: ...
29
31
 
@@ -31,16 +33,18 @@ def _setup_output_fpath(
31
33
  @overload
32
34
  def _setup_output_fpath(
33
35
  fpath: None,
34
- overwrite: bool,
35
- make_parents: bool,
36
+ *,
37
+ overwrite: bool = True,
38
+ make_parents: bool = True,
36
39
  absolute: bool = True,
37
40
  ) -> None: ...
38
41
 
39
42
 
40
43
  def _setup_output_fpath(
41
44
  fpath: Union[str, Path, os.PathLike, TextIOWrapper, None],
42
- overwrite: bool,
43
- make_parents: bool,
45
+ *,
46
+ overwrite: bool = True,
47
+ make_parents: bool = True,
44
48
  absolute: bool = True,
45
49
  ) -> Union[Path, None, TextIOWrapper]:
46
50
  """Resolve path, expand path and create intermediate parents."""
@@ -74,7 +74,7 @@ def dump_csv(
74
74
  )
75
75
 
76
76
  if isinstance(file, (str, Path, PathLike)):
77
- file = _setup_output_fpath(file, overwrite, make_parents)
77
+ file = _setup_output_fpath(file, overwrite=overwrite, make_parents=make_parents)
78
78
  with open(file, "w") as opened_file:
79
79
  opened_file.write(content)
80
80
  elif isinstance(file, TextIOBase):
@@ -51,7 +51,7 @@ def dump_json(
51
51
  )
52
52
 
53
53
  if isinstance(file, (str, Path, PathLike)):
54
- file = _setup_output_fpath(file, overwrite, make_parents)
54
+ file = _setup_output_fpath(file, overwrite=overwrite, make_parents=make_parents)
55
55
  with open(file, "w") as opened_file:
56
56
  opened_file.write(content)
57
57
  elif isinstance(file, TextIOBase):
@@ -64,7 +64,7 @@ def dump_jsonl(
64
64
  )
65
65
 
66
66
  if isinstance(file, (str, Path, PathLike)):
67
- file = _setup_output_fpath(file, overwrite, make_parents)
67
+ file = _setup_output_fpath(file, overwrite=overwrite, make_parents=make_parents)
68
68
  with open(file, "w") as opened_file:
69
69
  opened_file.write(content)
70
70
  elif isinstance(file, TextIOBase):
@@ -45,7 +45,7 @@ def dump_pickle(
45
45
  )
46
46
 
47
47
  if isinstance(file, (str, Path, PathLike)):
48
- file = _setup_output_fpath(file, overwrite, make_parents)
48
+ file = _setup_output_fpath(file, overwrite=overwrite, make_parents=make_parents)
49
49
  with open(file, "wb") as opened_file:
50
50
  opened_file.write(content)
51
51
  elif isinstance(file, BinaryIO):
@@ -18,6 +18,7 @@ if TYPE_CHECKING or lazy is None:
18
18
  is_builtin_scalar,
19
19
  is_collection_alias,
20
20
  is_dataclass_instance,
21
+ is_dataclass_type,
21
22
  is_iterable_bool,
22
23
  is_iterable_bytes_or_list,
23
24
  is_iterable_float,
@@ -77,6 +78,7 @@ else:
77
78
  "is_builtin_scalar",
78
79
  "is_collection_alias",
79
80
  "is_dataclass_instance",
81
+ "is_dataclass_type",
80
82
  "is_iterable_bool",
81
83
  "is_iterable_bytes_or_list",
82
84
  "is_iterable_float",
@@ -5,6 +5,7 @@ import inspect
5
5
  import logging
6
6
  import sys
7
7
  import typing
8
+ from dataclasses import is_dataclass
8
9
  from numbers import Integral
9
10
  from types import FunctionType, MethodType
10
11
  from typing import (
@@ -311,6 +312,10 @@ def is_dataclass_instance(x: Any) -> TypeIs[DataclassInstance]:
311
312
  return not isinstance(x, type) and isinstance_generic(x, DataclassInstance)
312
313
 
313
314
 
315
+ def is_dataclass_type(x: Any) -> TypeIs[Type[DataclassInstance]]:
316
+ return isinstance(x, type) and is_dataclass(x)
317
+
318
+
314
319
  def is_iterable_bool(
315
320
  x: Any,
316
321
  *,
@@ -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, return_none
10
+ from pythonwrench._core import T_Function, _decorator_factory, return_none
11
11
 
12
12
  P = ParamSpec("P")
13
13
  U = TypeVar("U")
@@ -43,13 +43,13 @@ def warn_once(
43
43
 
44
44
 
45
45
  def deprecated_alias(
46
- alternative: Callable[P, U],
46
+ alternative: T_Function,
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
49
  *,
50
50
  pre_fn: Optional[Callable[..., Any]] = None,
51
51
  post_fn: Optional[Callable[..., Any]] = None,
52
- ) -> Callable[..., Callable[P, U]]:
52
+ ) -> Callable[..., T_Function]:
53
53
  """Decorator to wrap deprecated function aliases."""
54
54
  alternative_name = alternative.__name__ if alternative is not None else "None"
55
55
  if pre_fn is None:
@@ -70,28 +70,28 @@ def deprecated_function(
70
70
  *,
71
71
  msg_fmt: str = "Deprecated call to '{fn_name}'.",
72
72
  warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
73
- ) -> Callable[[Callable[P, U]], Callable[P, U]]: ...
73
+ ) -> Callable[[T_Function], T_Function]: ...
74
74
 
75
75
 
76
76
  @overload
77
77
  def deprecated_function(
78
- fn: Callable[P, U],
78
+ fn: T_Function,
79
79
  /,
80
80
  *,
81
81
  msg_fmt: str = "Deprecated call to '{fn_name}'.",
82
82
  warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
83
- ) -> Callable[P, U]: ...
83
+ ) -> T_Function: ...
84
84
 
85
85
 
86
86
  def deprecated_function(
87
- fn: Optional[Callable[P, U]] = None,
87
+ fn: Optional[T_Function] = None,
88
88
  /,
89
89
  *,
90
90
  msg_fmt: str = "Deprecated call to '{fn_name}'.",
91
91
  warn_fn: Callable[[str], Any] = partial(warn_once, category=DeprecationWarning),
92
92
  pre_fn: Optional[Callable[..., Any]] = None,
93
93
  post_fn: Optional[Callable[..., Any]] = None,
94
- ) -> Callable:
94
+ ) -> Union[Callable[[T_Function], T_Function], T_Function]:
95
95
  """Decorator to wrap deprecated functions."""
96
96
  if pre_fn is None:
97
97
  pre_fn = return_none
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonwrench
3
- Version: 0.6.1
3
+ Version: 0.6.2
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>
@@ -4,7 +4,7 @@
4
4
  import unittest
5
5
  from argparse import ArgumentParser
6
6
  from dataclasses import dataclass, field
7
- from typing import List, Literal, Optional, Tuple, Union
7
+ from typing import Iterable, List, Literal, Optional, Tuple, Union
8
8
  from unittest import TestCase
9
9
 
10
10
  from pythonwrench.argparse import (
@@ -113,7 +113,7 @@ class TestArgparse(TestCase):
113
113
  class B:
114
114
  arg_b: Union[str, List[str]] = ""
115
115
 
116
- with self.assertRaises(TypeError):
116
+ with self.assertRaises(SystemExit):
117
117
  _ = parse_args_using_dataclass(B, args=[])
118
118
 
119
119
  @dataclass
@@ -149,6 +149,93 @@ class TestArgparse(TestCase):
149
149
  )
150
150
  assert target == result
151
151
 
152
+ def test_parse_args_using_dataclass_example_5(self) -> None:
153
+ @dataclass
154
+ class A:
155
+ seed: int
156
+
157
+ @dataclass
158
+ class B(A):
159
+ constraint_names: Optional[Iterable[str]] = ("b",)
160
+
161
+ result = parse_args_using_dataclass(
162
+ B,
163
+ args=["--seed", "42"],
164
+ list_parsing="brackets",
165
+ )
166
+ assert result == B(42, ("b",))
167
+
168
+ result = parse_args_using_dataclass(
169
+ B,
170
+ args=["--constraint_names", "[a,b]", "--seed", "42"],
171
+ list_parsing="brackets",
172
+ )
173
+ assert result == B(42, ["a", "b"]), f"{result=}"
174
+
175
+ result = parse_args_using_dataclass(
176
+ B,
177
+ args=["--constraint_names", "[a]", "--seed", "42"],
178
+ list_parsing="brackets",
179
+ )
180
+ assert result == B(42, ["a"])
181
+
182
+ result = parse_args_using_dataclass(
183
+ B,
184
+ args=["--constraint_names", "[]", "--seed", "42"],
185
+ list_parsing="brackets",
186
+ )
187
+ assert result == B(42, [])
188
+
189
+ result = parse_args_using_dataclass(
190
+ B,
191
+ args=["--constraint_names", "none", "--seed", "42"],
192
+ list_parsing="brackets",
193
+ )
194
+ assert result == B(42, None)
195
+
196
+ with self.assertRaises(SystemExit):
197
+ result = parse_args_using_dataclass(
198
+ B,
199
+ args=["--constraint_names", "truc", "--seed", "42"],
200
+ list_parsing="brackets",
201
+ )
202
+ assert result == B(42, "truc")
203
+
204
+ def test_parse_args_using_dataclass_example_6(self) -> None:
205
+ @dataclass
206
+ class A:
207
+ seed: Optional[Iterable[Literal[0, "a"]]] = None
208
+
209
+ result = parse_args_using_dataclass(
210
+ A,
211
+ args=[],
212
+ list_parsing="brackets",
213
+ )
214
+ assert result == A()
215
+
216
+ result = parse_args_using_dataclass(
217
+ A,
218
+ args=["--seed", "[0,a]"],
219
+ list_parsing="brackets",
220
+ )
221
+ assert result == A([0, "a"])
222
+
223
+ with self.assertRaises(SystemExit):
224
+ result = parse_args_using_dataclass(
225
+ A,
226
+ args=["--seed", "[b]"],
227
+ list_parsing="brackets",
228
+ )
229
+ assert result == A(["b"]) # type: ignore
230
+
231
+ with self.assertRaises(SystemExit):
232
+ result = parse_args_using_dataclass(
233
+ A,
234
+ args=["--seed", "[1]"],
235
+ list_parsing="brackets",
236
+ )
237
+ assert result == A([1]) # type: ignore
238
+
152
239
 
153
240
  if __name__ == "__main__":
154
241
  unittest.main()
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import unittest
5
+ from dataclasses import asdict, dataclass, field, is_dataclass
6
+ from typing import List, Tuple
7
+ from unittest import TestCase
8
+
9
+ from pythonwrench.dataclasses import (
10
+ add_dict_methods,
11
+ dataclassdict,
12
+ get_defaults_values,
13
+ is_dataclass_instance,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class Dummy:
19
+ a: int
20
+ b: str = "b"
21
+ c: Tuple[int, ...] = ()
22
+ d: List[str] = field(default_factory=list)
23
+
24
+
25
+ class TestDataclass(TestCase):
26
+ def test_example_1(self) -> None:
27
+ dummy = Dummy(2)
28
+
29
+ assert is_dataclass(dummy)
30
+ assert is_dataclass(Dummy)
31
+ assert is_dataclass_instance(dummy)
32
+ assert not is_dataclass_instance(Dummy)
33
+
34
+ assert get_defaults_values(dummy) == {"b": "b", "c": (), "d": []}
35
+
36
+ def test_dataclassdict(self) -> None:
37
+ @dataclassdict
38
+ class Point:
39
+ x: int
40
+ y: int = 0
41
+
42
+ point = Point(1)
43
+ assert is_dataclass(point)
44
+ assert isinstance(point, dict)
45
+ assert dict(point) == {"x": 1, "y": 0}
46
+ assert len(point) == 2
47
+ assert asdict(point) == {"x": 1, "y": 0}
48
+
49
+ point.x = 2
50
+ assert point["x"] == 2
51
+
52
+ point["y"] = 3
53
+ assert point.y == 3
54
+
55
+ point.update(x=4, extra=True)
56
+ assert point.x == 4
57
+ assert point["extra"] is True
58
+
59
+ def test_dataclassdict_rejects_dict_attribute_as_field(self) -> None:
60
+ with self.assertRaisesRegex(RuntimeError, "'keys'"):
61
+
62
+ @dataclassdict
63
+ class Invalid:
64
+ keys: float = 0.0
65
+
66
+ def test_add_dict_methods(self) -> None:
67
+ @dataclass
68
+ class Existing:
69
+ a: int = 1
70
+ b: str = ""
71
+
72
+ ExistingDict = add_dict_methods(Existing)
73
+ value = ExistingDict()
74
+
75
+ assert isinstance(value, Existing)
76
+ assert isinstance(value, dict)
77
+ assert value["a"] == 1
78
+ assert value["b"] == ""
79
+
80
+ def test_add_dict_methods_rejects_non_dataclass(self) -> None:
81
+ class Regular:
82
+ pass
83
+
84
+ with self.assertRaisesRegex(TypeError, "expects a dataclass type"):
85
+ add_dict_methods(Regular)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()
@@ -48,7 +48,8 @@ class TestDiskCache(TestCase):
48
48
  )
49
49
  def disk_cache_example_3(num: int) -> List[Dict[str, str]]:
50
50
  return pw.dict_list_to_list_dict(
51
- {"a": ["a"] * num, "b": ["b"] * num}, "same"
51
+ {"a": ["a"] * num, "b": ["b"] * num},
52
+ "same",
52
53
  )
53
54
 
54
55
  outputs = disk_cache_example_3(10)
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- from dataclasses import MISSING
5
- from typing import Any, Dict, TypeVar
6
-
7
- from pythonwrench.typing.checks import is_dataclass_instance # noqa: F401
8
- from pythonwrench.typing.classes import Dataclass, DataclassInstance # noqa: F401
9
-
10
- T = TypeVar("T")
11
-
12
-
13
- def get_defaults_values(obj: DataclassInstance) -> Dict[str, Any]:
14
- defaults = {}
15
-
16
- for field in obj.__dataclass_fields__.values():
17
- if callable(field.default_factory):
18
- default = field.default_factory()
19
- else:
20
- default = field.default
21
-
22
- if default != MISSING:
23
- defaults[field.name] = default
24
-
25
- return defaults
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- import unittest
5
- from dataclasses import dataclass, field, is_dataclass
6
- from typing import List, Tuple
7
- from unittest import TestCase
8
-
9
- from pythonwrench.dataclasses import get_defaults_values, is_dataclass_instance
10
-
11
-
12
- @dataclass
13
- class Dummy:
14
- a: int
15
- b: str = "b"
16
- c: Tuple[int, ...] = ()
17
- d: List[str] = field(default_factory=list)
18
-
19
-
20
- class TestDataclass(TestCase):
21
- def test_example_1(self) -> None:
22
- dummy = Dummy(2)
23
-
24
- assert is_dataclass(dummy)
25
- assert is_dataclass(Dummy)
26
- assert is_dataclass_instance(dummy)
27
- assert not is_dataclass_instance(Dummy)
28
-
29
- assert get_defaults_values(dummy) == {"b": "b", "c": (), "d": []}
30
-
31
-
32
- if __name__ == "__main__":
33
- unittest.main()
File without changes
File without changes
File without changes
File without changes