pythonwrench 0.6.0__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.0/src/pythonwrench.egg-info → pythonwrench-0.6.2}/PKG-INFO +1 -1
  2. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/pyproject.toml +3 -0
  3. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/__init__.py +11 -3
  4. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/_core.py +14 -9
  5. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/argparse.py +150 -32
  6. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/concurrent.py +6 -2
  7. pythonwrench-0.6.2/src/pythonwrench/dataclasses.py +109 -0
  8. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/difflib.py +9 -6
  9. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/info.py +9 -11
  10. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/functools.py +15 -5
  11. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/importlib.py +39 -21
  12. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/_core.py +12 -8
  13. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/csv.py +1 -1
  14. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/json.py +1 -1
  15. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/jsonl.py +1 -1
  16. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/pickle.py +1 -1
  17. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/__init__.py +2 -0
  18. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/checks.py +9 -1
  19. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/warnings.py +23 -12
  20. {pythonwrench-0.6.0 → pythonwrench-0.6.2/src/pythonwrench.egg-info}/PKG-INFO +1 -1
  21. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/entry_points.txt +3 -0
  22. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_argparse.py +108 -2
  23. pythonwrench-0.6.2/tests/test_dataclasses.py +89 -0
  24. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_disk_cache.py +2 -1
  25. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_typing.py +11 -2
  26. pythonwrench-0.6.0/src/pythonwrench/dataclasses.py +0 -25
  27. pythonwrench-0.6.0/tests/test_dataclasses.py +0 -33
  28. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/LICENSE +0 -0
  29. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/README.md +0 -0
  30. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/setup.cfg +0 -0
  31. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/setup.py +0 -0
  32. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/__main__.py +0 -0
  33. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/abc.py +0 -0
  34. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/cast.py +0 -0
  35. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/checksum.py +0 -0
  36. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/__init__.py +0 -0
  37. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/collections.py +0 -0
  38. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/prop.py +0 -0
  39. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/reducers.py +0 -0
  40. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/csv.py +0 -0
  41. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/datetime.py +0 -0
  42. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/disk_cache.py +0 -0
  43. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  44. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/tree.py +0 -0
  45. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/enum.py +0 -0
  46. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/hashlib.py +0 -0
  47. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/inspect.py +0 -0
  48. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/json.py +0 -0
  49. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/jsonl.py +0 -0
  50. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/logging.py +0 -0
  51. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/math.py +0 -0
  52. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/os.py +0 -0
  53. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/pickle.py +0 -0
  54. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/random.py +0 -0
  55. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/re.py +0 -0
  56. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/semver.py +0 -0
  57. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/__init__.py +0 -0
  58. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/time.py +0 -0
  59. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/classes.py +0 -0
  60. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/SOURCES.txt +0 -0
  61. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
  62. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/requires.txt +0 -0
  63. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
  64. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_abc.py +0 -0
  65. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_cast.py +0 -0
  66. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_checksum.py +0 -0
  67. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_collections.py +0 -0
  68. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_csv.py +0 -0
  69. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_difflib.py +0 -0
  70. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_entrypoints.py +0 -0
  71. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_enum.py +0 -0
  72. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_functools.py +0 -0
  73. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_hashlib.py +0 -0
  74. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_importlib.py +0 -0
  75. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_inspect.py +0 -0
  76. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_json.py +0 -0
  77. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_jsonl.py +0 -0
  78. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_logging.py +0 -0
  79. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_math.py +0 -0
  80. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_os.py +0 -0
  81. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_random.py +0 -0
  82. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_readme.py +0 -0
  83. {pythonwrench-0.6.0 → pythonwrench-0.6.2}/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.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>
@@ -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.2"
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,
@@ -100,7 +101,7 @@ if TYPE_CHECKING or lazy is None:
100
101
  union_lists,
101
102
  unzip,
102
103
  )
103
- from .dataclasses import get_defaults_values
104
+ from .dataclasses import add_dict_methods, dataclassdict, get_defaults_values
104
105
  from .datetime import get_now, get_now_iso8601
105
106
  from .difflib import find_closest_in_list, sequence_matcher_ratio
106
107
  from .disk_cache import disk_cache_call, disk_cache_decorator
@@ -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,
@@ -216,6 +219,7 @@ if TYPE_CHECKING or lazy is None:
216
219
  is_builtin_scalar,
217
220
  is_collection_alias,
218
221
  is_dataclass_instance,
222
+ is_dataclass_type,
219
223
  is_iterable_bool,
220
224
  is_iterable_bytes_or_list,
221
225
  is_iterable_float,
@@ -264,6 +268,7 @@ else:
264
268
  ],
265
269
  submod_attrs={
266
270
  "argparse": [
271
+ "add_dataclass_fields_to_parser",
267
272
  "new_parser_from_dataclass",
268
273
  "parse_args_using_dataclass",
269
274
  "parse_to",
@@ -340,7 +345,7 @@ else:
340
345
  "save_jsonl",
341
346
  "save_pickle",
342
347
  ],
343
- "dataclasses": ["get_defaults_values"],
348
+ "dataclasses": ["add_dict_methods", "dataclassdict", "get_defaults_values"],
344
349
  "datetime": ["get_now", "get_now_iso8601"],
345
350
  "difflib": ["find_closest_in_list", "sequence_matcher_ratio"],
346
351
  "disk_cache": ["disk_cache_call", "disk_cache_decorator"],
@@ -355,6 +360,8 @@ else:
355
360
  ],
356
361
  "hashlib": ["hash_file"],
357
362
  "importlib": [
363
+ "import_if_available",
364
+ "ModulePlaceholder",
358
365
  "is_available_package",
359
366
  "is_editable_package",
360
367
  "reload_editable_packages",
@@ -430,6 +437,7 @@ else:
430
437
  "is_builtin_scalar",
431
438
  "is_collection_alias",
432
439
  "is_dataclass_instance",
440
+ "is_dataclass_type",
433
441
  "is_iterable_bool",
434
442
  "is_iterable_bytes_or_list",
435
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,27 +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
- pre_fn: Callable[..., Any] = return_none,
43
- post_fn: Callable[..., Any] = return_none,
44
- ) -> Callable[[Callable[P, U]], Callable[P, U]]:
45
- """Deprecated decorator for function aliases."""
46
-
47
- def wrapper_factory(fn: Callable[P, U]) -> Callable[P, U]:
43
+ pre_fn: Optional[Callable[..., Any]] = None,
44
+ post_fn: Optional[Callable[..., Any]] = None,
45
+ ) -> Callable[[T_Function], T_Function]:
46
+ """Decorator for function aliases."""
47
+ if pre_fn is None:
48
+ pre_fn = return_none
49
+ if post_fn is None:
50
+ post_fn = return_none
51
+
52
+ def wrapper_factory(fn: T_Function) -> T_Function:
48
53
  if inner_fn is None:
49
54
  _inner_fn = fn
50
55
  else:
51
56
  _inner_fn = inner_fn
52
57
 
53
58
  @wraps(_inner_fn)
54
- def wrapped(*args: P.args, **kwargs: P.kwargs) -> U:
59
+ def wrapped(*args, **kwargs):
55
60
  pre_fn(fn, *args, **kwargs)
56
61
  result = _inner_fn(*args, **kwargs)
57
62
  post_fn(fn, *args, **kwargs)
58
63
  return result
59
64
 
60
- return wrapped
65
+ return wrapped # type: ignore
61
66
 
62
67
  return wrapper_factory
63
68
 
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
+ import re
4
5
  from argparse import ArgumentParser
6
+ from collections.abc import Iterable as _Iterable
5
7
  from dataclasses import MISSING, fields
6
8
  from functools import partial
7
9
  from typing import (
@@ -12,6 +14,7 @@ from typing import (
12
14
  List,
13
15
  Literal,
14
16
  Optional,
17
+ Tuple,
15
18
  Type,
16
19
  TypeVar,
17
20
  Union,
@@ -25,13 +28,17 @@ except ImportError:
25
28
  # support older python versions
26
29
  UnionType = Any
27
30
 
31
+ from pythonwrench.functools import filter_and_call
28
32
  from pythonwrench.typing.classes import Dataclass, DataclassInstance, NoneType
33
+ from pythonwrench.warnings import deprecated_alias
29
34
 
30
35
  T = TypeVar("T")
31
36
  T_Dataclass = TypeVar("T_Dataclass", bound=Dataclass)
32
37
  T_DataclassInstance = TypeVar("T_DataclassInstance", bound=DataclassInstance)
33
- TargetType = Union[Type[T], UnionType, "Type[Literal]"]
34
-
38
+ TargetType = Union[
39
+ Type[T], UnionType, "Type[Literal]", "Type[Optional]", Tuple[type, ...]
40
+ ]
41
+ ListParsing = Literal["argparse", "brackets"]
35
42
 
36
43
  DEFAULT_TRUE_VALUES = ("True", "t", "yes", "y", "1")
37
44
  DEFAULT_FALSE_VALUES = ("False", "f", "no", "n", "0")
@@ -42,24 +49,44 @@ _SCALARS_TARGET_TYPES = (str, int, float, None, NoneType, bool)
42
49
 
43
50
  def parse_args_using_dataclass(
44
51
  dataclass_type: Type[T_DataclassInstance],
52
+ *,
45
53
  args: Optional[Iterable[str]] = None,
54
+ parser: Optional[ArgumentParser] = None,
55
+ list_parsing: ListParsing = "argparse",
56
+ add_dashed_arg: bool = True,
46
57
  ) -> T_DataclassInstance:
47
58
  """Converts prog args to a typed dataclass using argparse.
48
59
 
49
60
  Currently only supports dataclasses that contains only builtin scalars: str, int, float, None, bool OR list of builtin scalars.
50
61
  """
51
- parser = ArgumentParser()
52
- parser = new_parser_from_dataclass(dataclass_type, parser)
62
+ init_parser = parser
63
+ parser = add_dataclass_fields_to_parser(
64
+ dataclass_type,
65
+ parser=parser,
66
+ list_parsing=list_parsing,
67
+ add_dashed_arg=add_dashed_arg,
68
+ )
53
69
  parsed, argv = parser.parse_known_args(args)
54
70
  if len(argv) > 0:
55
71
  raise ValueError(f"Found {len(argv)} unknown arguments: {argv}.")
56
- instance = dataclass_type(**parsed.__dict__)
72
+
73
+ if init_parser is None:
74
+ instance = dataclass_type(**parsed.__dict__)
75
+ else:
76
+ instance = filter_and_call(
77
+ dataclass_type,
78
+ _fill_all_arguments=True,
79
+ **parsed.__dict__,
80
+ )
57
81
  return instance
58
82
 
59
83
 
60
- def new_parser_from_dataclass(
84
+ def add_dataclass_fields_to_parser(
61
85
  dataclass_type: Type[T_DataclassInstance],
86
+ *,
62
87
  parser: Optional[ArgumentParser],
88
+ list_parsing: ListParsing = "argparse",
89
+ add_dashed_arg: bool = True,
63
90
  ) -> ArgumentParser:
64
91
  if parser is None:
65
92
  parser = ArgumentParser()
@@ -67,6 +94,9 @@ def new_parser_from_dataclass(
67
94
  for field in fields(dataclass_type):
68
95
  kwds = {}
69
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}")
70
100
 
71
101
  if field.default is MISSING and field.default_factory is MISSING:
72
102
  kwds["required"] = True
@@ -79,13 +109,26 @@ def new_parser_from_dataclass(
79
109
  msg = f"Invalid field {field.name}: found values for default and default_factory."
80
110
  raise ValueError(msg)
81
111
 
82
- kwds.update(_get_kwds_for_type(field.type))
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
+
118
+ kwds.update(inner_kwds)
83
119
  parser.add_argument(*posargs, **kwds)
84
120
 
85
121
  return parser
86
122
 
87
123
 
88
- def _get_kwds_for_type(field_type: Any) -> Dict[str, Any]:
124
+ @deprecated_alias(add_dataclass_fields_to_parser)
125
+ def new_parser_from_dataclass(*args, **kwargs): ...
126
+
127
+
128
+ def _get_kwds_for_type(
129
+ field_type: Any,
130
+ list_parsing: ListParsing = "argparse",
131
+ ) -> Dict[str, Any]:
89
132
  kwds = {}
90
133
 
91
134
  type_origin = get_origin(field_type)
@@ -96,35 +139,53 @@ def _get_kwds_for_type(field_type: Any) -> Dict[str, Any]:
96
139
  if not all(type(arg) in _SCALARS_TARGET_TYPES for arg in type_args):
97
140
  msg = f"Invalid argument {field_type=}. (expected homogeneous types in {type_origin})"
98
141
  raise TypeError(msg)
99
- elif type_origin in (UnionType, Union):
100
- if not all(arg in _SCALARS_TARGET_TYPES for arg in type_args):
101
- msg = f"Invalid argument {field_type=}. (expected homogeneous types in {type_origin})"
102
- raise TypeError(msg)
103
142
 
104
- if field_type in _SCALARS_TARGET_TYPES or type_origin in (
105
- Literal,
106
- Optional,
107
- UnionType,
108
- Union,
143
+ if (
144
+ (field_type in _SCALARS_TARGET_TYPES)
145
+ or (
146
+ type_origin
147
+ in (
148
+ Literal,
149
+ Optional,
150
+ UnionType,
151
+ Union,
152
+ )
153
+ )
154
+ or (_is_iterable_type_like(type_origin) and list_parsing == "brackets")
109
155
  ):
110
- kwds.update(_get_kwds_for_scalar_type(field_type, field_type))
111
- elif type_origin is list:
156
+ inner_kwds = _get_kwds_for_scalar_type(field_type, field_type, list_parsing)
157
+ kwds.update(inner_kwds)
158
+
159
+ elif _is_iterable_type_like(type_origin):
112
160
  item_type = type_args[0]
113
- kwds.update(_get_kwds_for_scalar_type(item_type, field_type))
114
- kwds["nargs"] = "*"
161
+ inner_kwds = _get_kwds_for_scalar_type(item_type, field_type, list_parsing)
162
+ inner_kwds["nargs"] = "*"
163
+ kwds.update(inner_kwds)
115
164
  else:
116
- msg = f"Unsupported type {field_type}."
165
+ msg = f"Unsupported type {field_type}. (with {type_origin=})"
117
166
  raise TypeError(msg)
118
167
 
119
168
  return kwds
120
169
 
121
170
 
122
- def _get_kwds_for_scalar_type(type_: Any, from_field_type: Any) -> Dict[str, Any]:
171
+ def _get_kwds_for_scalar_type(
172
+ type_: Any,
173
+ from_field_type: Any,
174
+ list_parsing: ListParsing,
175
+ ) -> Dict[str, Any]:
123
176
  type_origin = get_origin(type_)
124
177
  kwds = {}
125
178
 
126
- if type_ in _SCALARS_TARGET_TYPES or type_origin in (UnionType, Union, Optional):
179
+ if (
180
+ type_ in _SCALARS_TARGET_TYPES
181
+ or type_origin in (UnionType, Union, Optional)
182
+ or (
183
+ _is_iterable_type_like(get_origin(from_field_type))
184
+ and list_parsing == "brackets"
185
+ )
186
+ ):
127
187
  pass
188
+
128
189
  elif type_origin is Literal:
129
190
  type_args = get_args(type_)
130
191
  kwds["choices"] = type_args
@@ -132,7 +193,7 @@ def _get_kwds_for_scalar_type(type_: Any, from_field_type: Any) -> Dict[str, Any
132
193
  msg = f"Unsupported dataclass member type {type_} from {from_field_type}."
133
194
  raise TypeError(msg)
134
195
 
135
- kwds["type"] = parse_to(type_) # type: ignore
196
+ kwds["type"] = parse_to(type_, list_parsing=list_parsing) # type: ignore
136
197
  return kwds
137
198
 
138
199
 
@@ -143,6 +204,7 @@ def parse_to(
143
204
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
144
205
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
145
206
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
207
+ list_parsing: ListParsing = "argparse",
146
208
  ) -> Callable[[str], T]:
147
209
  """Returns a callable that convert string value to target type safely.
148
210
 
@@ -155,6 +217,7 @@ def parse_to(
155
217
  true_values=true_values,
156
218
  false_values=false_values,
157
219
  none_values=none_values,
220
+ list_parsing=list_parsing,
158
221
  )
159
222
 
160
223
 
@@ -166,6 +229,7 @@ def str_to_type(
166
229
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
167
230
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
168
231
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
232
+ list_parsing: ListParsing = "argparse",
169
233
  ) -> T:
170
234
  """Convert string values to target type safely. Intended for argparse arguments.
171
235
 
@@ -181,6 +245,7 @@ def str_to_type(
181
245
  true_values=true_values,
182
246
  false_values=false_values,
183
247
  none_values=none_values,
248
+ list_parsing=list_parsing,
184
249
  )
185
250
  if isinstance(result, Exception):
186
251
  raise result
@@ -303,12 +368,14 @@ def _str_to_type_impl(
303
368
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
304
369
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
305
370
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
371
+ list_parsing: ListParsing = "argparse",
306
372
  ) -> Union[T, Exception]:
307
373
  kwds: Dict[str, Any] = dict(
308
374
  case_sensitive=case_sensitive,
309
375
  true_values=true_values,
310
376
  false_values=false_values,
311
377
  none_values=none_values,
378
+ list_parsing=list_parsing,
312
379
  )
313
380
  if target_type in _SCALARS_TARGET_TYPES:
314
381
  return _str_to_scalar_impl(x, target_type, **kwds)
@@ -318,17 +385,55 @@ def _str_to_type_impl(
318
385
  if origin is Literal:
319
386
  args = get_args(target_type)
320
387
  literal_types = {type(value) for value in args}
321
- if len(literal_types) != 1:
322
- msg = f"Mixed Literal are not supported: {args}"
323
- raise TypeError(msg)
324
388
 
325
- literal_type = next(iter(literal_types))
326
- 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
+
327
398
  if scalar not in args:
328
399
  msg = f"Cannot convert {x} to Literal[{', '.join(args)}]"
329
400
  raise ValueError(msg)
330
401
  return scalar
331
402
 
403
+ if _is_iterable_type_like(origin):
404
+ if list_parsing != "brackets":
405
+ raise ValueError
406
+
407
+ args = get_args(target_type)
408
+
409
+ if len(args) == 0:
410
+ target_item_type = str
411
+ elif len(args) == 1:
412
+ target_item_type = args[0]
413
+ else:
414
+ raise ValueError
415
+
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
+
425
+ x_list = x.split(",")
426
+
427
+ output_list = []
428
+ for xi in x_list:
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
332
437
  elif getattr(target_type, "__name__", None) == "Optional":
333
438
  args = (None,) + get_args(target_type)
334
439
  elif origin == Union or origin.__name__ in ("Union", "UnionType"): # type: ignore
@@ -354,6 +459,10 @@ def _str_to_type_impl(
354
459
  return ValueError(f"Invalid argument {x=} with {target_type=}.")
355
460
 
356
461
 
462
+ def _is_iterable_type_like(x: Any) -> bool:
463
+ return x in (list, Iterable, _Iterable)
464
+
465
+
357
466
  def _str_to_scalar_impl(
358
467
  x: str,
359
468
  target_type: TargetType[T],
@@ -362,23 +471,31 @@ def _str_to_scalar_impl(
362
471
  true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES,
363
472
  false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES,
364
473
  none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES,
474
+ list_parsing: ListParsing = "argparse",
365
475
  ) -> Any:
476
+ del list_parsing
366
477
  if target_type is str:
367
478
  return x
479
+
368
480
  elif target_type is int:
369
481
  try:
370
482
  return int(x)
371
483
  except ValueError as err:
372
484
  return err
485
+
373
486
  elif target_type is float:
374
487
  try:
375
488
  return float(x)
376
489
  except ValueError as err:
377
490
  return err
491
+
378
492
  elif target_type in (None, NoneType):
379
493
  return _str_to_none_impl(
380
- x, case_sensitive=case_sensitive, none_values=none_values
494
+ x,
495
+ case_sensitive=case_sensitive,
496
+ none_values=none_values,
381
497
  )
498
+
382
499
  elif target_type is bool:
383
500
  return _str_to_bool_impl(
384
501
  x,
@@ -387,7 +504,8 @@ def _str_to_scalar_impl(
387
504
  false_values=false_values,
388
505
  )
389
506
  else:
390
- raise ValueError(f"Invalid argument {target_type=}. (unsupported type)")
507
+ msg = f"Invalid argument {target_type=}. (unsupported type)"
508
+ raise ValueError(msg)
391
509
 
392
510
 
393
511
  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
 
@@ -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
@@ -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()