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.
- {pythonwrench-0.6.1/src/pythonwrench.egg-info → pythonwrench-0.6.2}/PKG-INFO +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/__init__.py +5 -3
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/_core.py +7 -6
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/argparse.py +58 -48
- pythonwrench-0.6.2/src/pythonwrench/dataclasses.py +109 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/functools.py +6 -4
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/_core.py +12 -8
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/csv.py +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/json.py +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/jsonl.py +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/pickle.py +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/__init__.py +2 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/checks.py +5 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/warnings.py +8 -8
- {pythonwrench-0.6.1 → pythonwrench-0.6.2/src/pythonwrench.egg-info}/PKG-INFO +1 -1
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_argparse.py +89 -2
- pythonwrench-0.6.2/tests/test_dataclasses.py +89 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_disk_cache.py +2 -1
- pythonwrench-0.6.1/src/pythonwrench/dataclasses.py +0 -25
- pythonwrench-0.6.1/tests/test_dataclasses.py +0 -33
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/LICENSE +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/README.md +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/pyproject.toml +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/setup.cfg +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/setup.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/__main__.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/abc.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/cast.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/checksum.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/__init__.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/collections.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/prop.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/collections/reducers.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/concurrent.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/csv.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/datetime.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/difflib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/disk_cache.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/info.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/entrypoints/tree.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/enum.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/hashlib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/importlib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/inspect.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/json.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/jsonl.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/logging.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/math.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/os.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/pickle.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/random.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/re.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/semver.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/serialization/__init__.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/time.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench/typing/classes.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/SOURCES.txt +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/dependency_links.txt +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/entry_points.txt +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/requires.txt +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/src/pythonwrench.egg-info/top_level.txt +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_abc.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_cast.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_checksum.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_collections.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_csv.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_difflib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_entrypoints.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_enum.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_functools.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_hashlib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_importlib.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_inspect.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_json.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_jsonl.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_logging.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_math.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_os.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_random.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_readme.py +0 -0
- {pythonwrench-0.6.1 → pythonwrench-0.6.2}/tests/test_semver.py +0 -0
- {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.
|
|
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.
|
|
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[
|
|
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[[
|
|
45
|
-
"""
|
|
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:
|
|
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
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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)
|
|
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
|
-
|
|
401
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
427
|
+
output_list = []
|
|
424
428
|
for xi in x_list:
|
|
425
|
-
|
|
426
|
-
if isinstance(
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
return
|
|
430
|
-
|
|
431
|
-
if
|
|
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:
|
|
165
|
+
alternative: T_Function,
|
|
166
166
|
*,
|
|
167
167
|
pre_fn: Optional[Callable[..., Any]] = None,
|
|
168
168
|
post_fn: Optional[Callable[..., Any]] = None,
|
|
169
|
-
) -> Callable[...,
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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:
|
|
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[...,
|
|
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[[
|
|
73
|
+
) -> Callable[[T_Function], T_Function]: ...
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
@overload
|
|
77
77
|
def deprecated_function(
|
|
78
|
-
fn:
|
|
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
|
-
) ->
|
|
83
|
+
) -> T_Function: ...
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def deprecated_function(
|
|
87
|
-
fn: Optional[
|
|
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.
|
|
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(
|
|
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},
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|