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.
- {pythonwrench-0.6.0/src/pythonwrench.egg-info → pythonwrench-0.6.2}/PKG-INFO +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/pyproject.toml +3 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/__init__.py +11 -3
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/_core.py +14 -9
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/argparse.py +150 -32
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/concurrent.py +6 -2
- pythonwrench-0.6.2/src/pythonwrench/dataclasses.py +109 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/difflib.py +9 -6
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/info.py +9 -11
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/functools.py +15 -5
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/importlib.py +39 -21
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/_core.py +12 -8
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/csv.py +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/json.py +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/jsonl.py +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/pickle.py +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/__init__.py +2 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/checks.py +9 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/warnings.py +23 -12
- {pythonwrench-0.6.0 → pythonwrench-0.6.2/src/pythonwrench.egg-info}/PKG-INFO +1 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/entry_points.txt +3 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_argparse.py +108 -2
- pythonwrench-0.6.2/tests/test_dataclasses.py +89 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_disk_cache.py +2 -1
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_typing.py +11 -2
- pythonwrench-0.6.0/src/pythonwrench/dataclasses.py +0 -25
- pythonwrench-0.6.0/tests/test_dataclasses.py +0 -33
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/LICENSE +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/README.md +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/setup.cfg +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/setup.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/__main__.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/abc.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/cast.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/checksum.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/__init__.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/collections.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/prop.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/collections/reducers.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/csv.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/datetime.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/disk_cache.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/tree.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/enum.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/hashlib.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/inspect.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/json.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/jsonl.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/logging.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/math.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/os.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/pickle.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/random.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/re.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/semver.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/serialization/__init__.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/time.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench/typing/classes.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/SOURCES.txt +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/requires.txt +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_abc.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_cast.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_checksum.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_collections.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_csv.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_difflib.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_entrypoints.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_enum.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_functools.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_hashlib.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_importlib.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_inspect.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_json.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_jsonl.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_logging.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_math.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_os.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_random.py +0 -0
- {pythonwrench-0.6.0 → pythonwrench-0.6.2}/tests/test_readme.py +0 -0
- {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.
|
|
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.
|
|
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[
|
|
41
|
+
inner_fn: Optional[T_Function],
|
|
41
42
|
*,
|
|
42
|
-
pre_fn: Callable[..., Any] =
|
|
43
|
-
post_fn: Callable[..., Any] =
|
|
44
|
-
) -> Callable[[
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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[
|
|
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
|
-
|
|
52
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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] =
|
|
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()
|