typedparser 0.6.4__tar.gz → 0.20.3__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.
- {typedparser-0.6.4 → typedparser-0.20.3}/LICENSE +1 -1
- {typedparser-0.6.4 → typedparser-0.20.3}/PKG-INFO +69 -43
- {typedparser-0.6.4 → typedparser-0.20.3}/README.md +59 -39
- {typedparser-0.6.4 → typedparser-0.20.3}/pyproject.toml +8 -2
- typedparser-0.20.3/requirements.txt +4 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/__init__.py +14 -3
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/_typedattr.py +85 -27
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/_typedparser.py +38 -4
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/funcs.py +35 -7
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/objects.py +8 -5
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser.egg-info/PKG-INFO +69 -43
- typedparser-0.20.3/src/typedparser.egg-info/requires.txt +8 -0
- typedparser-0.6.4/requirements.txt +0 -2
- typedparser-0.6.4/src/typedparser.egg-info/requires.txt +0 -2
- {typedparser-0.6.4 → typedparser-0.20.3}/setup.cfg +0 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser/custom_format.py +0 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser.egg-info/SOURCES.txt +0 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser.egg-info/dependency_links.txt +0 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser.egg-info/top_level.txt +0 -0
- {typedparser-0.6.4 → typedparser-0.20.3}/src/typedparser.egg-info/zip-safe +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2023
|
|
189
|
+
Copyright 2023 simon-ging
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: typedparser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.3
|
|
4
4
|
Summary: Extension for python argparse with typehints and typechecks.
|
|
5
|
-
Author:
|
|
5
|
+
Author: simon-ging
|
|
6
6
|
License: Apache-2.0
|
|
7
|
-
Project-URL: Project-URL, https://github.com/
|
|
7
|
+
Project-URL: Project-URL, https://github.com/simon-ging/typedparser
|
|
8
8
|
Keywords: attrs,typing,dict,attr
|
|
9
9
|
Platform: any
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -14,24 +14,48 @@ Classifier: Natural Language :: English
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.7
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.8
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
20
|
Classifier: Operating System :: OS Independent
|
|
18
21
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
22
|
Requires-Python: >=3.7
|
|
20
23
|
Description-Content-Type: text/markdown
|
|
21
24
|
License-File: LICENSE
|
|
22
25
|
Requires-Dist: attrs
|
|
23
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: importlib_metadata; python_version < "3.8"
|
|
27
|
+
Requires-Dist: typing_extensions; python_version < "3.8"
|
|
28
|
+
Provides-Extra: full
|
|
29
|
+
Requires-Dist: numpy; extra == "full"
|
|
24
30
|
|
|
25
31
|
# typedparser
|
|
26
32
|
|
|
27
33
|
<p align="center">
|
|
28
|
-
<a href="https://github.com/
|
|
29
|
-
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/
|
|
34
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37.yml">
|
|
35
|
+
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37.yml?branch=main&label=python%203.7" />
|
|
30
36
|
</a>
|
|
31
|
-
<a href="https://github.com/
|
|
32
|
-
<img alt="build 3.
|
|
37
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py38.yml">
|
|
38
|
+
<img alt="build 3.8 status" title="build 3.8 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py38.yml?branch=main&label=python%203.8" />
|
|
33
39
|
</a>
|
|
34
|
-
<
|
|
40
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py39.yml">
|
|
41
|
+
<img alt="build 3.9 status" title="build 3.9 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py39.yml?branch=main&label=python%203.9" />
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py310.yml">
|
|
44
|
+
<img alt="build 3.10 status" title="build 3.10 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py310.yml?branch=main&label=python%203.10" />
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py311.yml">
|
|
47
|
+
<img alt="build 3.11 status" title="build 3.11 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py311.yml?branch=main&label=python%203.11" />
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312.yml">
|
|
50
|
+
<img alt="build 3.12 status" title="build 3.12 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312.yml?branch=main&label=python%203.12" />
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37-full.yml">
|
|
53
|
+
<img alt="build 3.7 full status" title="build 3.7 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37-full.yml?branch=main&label=python%203.7%20full" />
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312-full.yml">
|
|
56
|
+
<img alt="build 3.12 full status" title="build 3.12 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312-full.yml?branch=main&label=python%203.12%20full" />
|
|
57
|
+
</a>
|
|
58
|
+
<img alt="coverage" title="coverage" src="https://raw.githubusercontent.com/simon-ging/typedparser/main/docs/coverage.svg" />
|
|
35
59
|
<a href="https://pypi.org/project/typedparser/">
|
|
36
60
|
<img alt="version" title="version" src="https://img.shields.io/pypi/v/typedparser?color=success" />
|
|
37
61
|
</a>
|
|
@@ -41,22 +65,6 @@ Typing extension for python argparse using [attrs](https://www.attrs.org/en/stab
|
|
|
41
65
|
|
|
42
66
|
Includes typechecking and conversion utilities to parse a dictionary into an attrs instance.
|
|
43
67
|
|
|
44
|
-
## Features
|
|
45
|
-
|
|
46
|
-
* Create commandline arguments with type hints and checks while
|
|
47
|
-
staying very close to the syntax of the standard library's argparse.
|
|
48
|
-
* Utilities for typechecking and converting nested objects:
|
|
49
|
-
* Nested checking and conversion of python standard types
|
|
50
|
-
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
51
|
-
* Supports positional and keyword arguments in classes
|
|
52
|
-
* Can also typecheck existing attrs instances
|
|
53
|
-
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
54
|
-
`int` to `float`
|
|
55
|
-
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
56
|
-
containers (list, dict, etc.)
|
|
57
|
-
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
58
|
-
* Some object utilities in `typedparser.objects` required for everything else
|
|
59
|
-
|
|
60
68
|
## Install
|
|
61
69
|
|
|
62
70
|
Requires `python>=3.7`
|
|
@@ -65,11 +73,11 @@ Requires `python>=3.7`
|
|
|
65
73
|
pip install typedparser
|
|
66
74
|
```
|
|
67
75
|
|
|
68
|
-
##
|
|
76
|
+
## Basic usage
|
|
69
77
|
|
|
70
|
-
1. Create an attrs class (decorate with `@attr.define`)
|
|
71
|
-
2. Define the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
72
|
-
3. Parse the args with `TypedParser
|
|
78
|
+
1. Create an attrs class (decorate with `@attr.define`). Note that optional arguments must also be typed as optional.
|
|
79
|
+
2. Define and type the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
80
|
+
3. Parse the args with `TypedParser` and enjoy args with type hints. Disable typechecking by setting `strict=False`.
|
|
73
81
|
|
|
74
82
|
~~~python
|
|
75
83
|
from typing import Optional
|
|
@@ -78,21 +86,21 @@ from typedparser import add_argument, TypedParser
|
|
|
78
86
|
|
|
79
87
|
|
|
80
88
|
@define
|
|
81
|
-
class Args:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
class Args:
|
|
90
|
+
# omit the argument name to have it inferred from the field name
|
|
91
|
+
foo: str = add_argument(positional=True)
|
|
92
|
+
bar: int = add_argument(shortcut="-b", type=int, default=0)
|
|
93
|
+
opt: Optional[str] = add_argument()
|
|
94
|
+
|
|
95
|
+
# # in case you prefer the regular argparse syntax:
|
|
96
|
+
# foo: str = add_argument("foo")
|
|
97
|
+
# bar: int = add_argument("-b", "--bar", type=int, default=0)
|
|
98
|
+
# opt: Optional[str] = add_argument("--opt")
|
|
86
99
|
|
|
87
|
-
# Omit argument name to create an optional argument --opt
|
|
88
|
-
opt: Optional[int] = add_argument(type=int)
|
|
89
100
|
|
|
90
|
-
# Use shortcut to create an optional argument -s / --short
|
|
91
|
-
short: Optional[int] = add_argument(shortcut="-s", type=int)
|
|
92
|
-
|
|
93
101
|
|
|
94
102
|
def main():
|
|
95
|
-
parser = TypedParser.create_parser(Args)
|
|
103
|
+
parser = TypedParser.create_parser(Args, strict=True)
|
|
96
104
|
args: Args = parser.parse_args()
|
|
97
105
|
print(args)
|
|
98
106
|
|
|
@@ -102,14 +110,31 @@ if __name__ == "__main__":
|
|
|
102
110
|
|
|
103
111
|
~~~
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
|
|
114
|
+
## Features
|
|
115
|
+
|
|
116
|
+
* Create commandline arguments with type hints and checks while
|
|
117
|
+
staying close to the syntax of the standard library's argparse.
|
|
118
|
+
* Utilities for typechecking and converting nested objects:
|
|
119
|
+
* Nested checking and conversion of python standard types
|
|
120
|
+
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
121
|
+
* Supports positional and keyword arguments in classes
|
|
122
|
+
* Can also typecheck existing attrs instances
|
|
123
|
+
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
124
|
+
`int` to `float`
|
|
125
|
+
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
126
|
+
containers (list, dict, etc.)
|
|
127
|
+
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
128
|
+
* Some object utilities in `typedparser.objects` required for everything else
|
|
129
|
+
|
|
130
|
+
## Advanced usage
|
|
106
131
|
|
|
107
132
|
* Use `TypedParser.from_parser(parser, Args)` to add typing to an existing parser. This is useful
|
|
108
133
|
to cover usecases like subparsers or argument groups.
|
|
109
134
|
* Snippet for argument lists `xarg: List[int] = add_argument(shortcut="-x", type=int, action="append", help="Xarg", default=[])`,
|
|
110
135
|
use as `-x 1 -x 2` to get `[1, 2]` in the args instance.
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
### Usage of attr utilities
|
|
113
138
|
|
|
114
139
|
Define the class hierarchy and parse the input using `attrs_from_dict`.
|
|
115
140
|
Use `@define(slots=False)` to allow multiple inheritance and setting attributes later.
|
|
@@ -143,6 +168,7 @@ print(attrs_from_dict(CfgNested, {"sub_cfg": {"foo": 1, "bar": 2}}))
|
|
|
143
168
|
the output will be a tuple
|
|
144
169
|
* Raise errors if types cannot be matched, there are unknown fields in the input or
|
|
145
170
|
abstract annotation types are used (e.g. Sequence)
|
|
171
|
+
* Set `_allow_extra_keys = True` in the class definition to allow unknown fields in the input
|
|
146
172
|
|
|
147
173
|
### Non-strict mode
|
|
148
174
|
|
|
@@ -174,7 +200,7 @@ Note: Some tests are skipped for python 3.7.
|
|
|
174
200
|
|
|
175
201
|
~~~bash
|
|
176
202
|
pip install -e .
|
|
177
|
-
pip install pytest pytest-cov pylint
|
|
203
|
+
pip install pytest pytest-cov pylint
|
|
178
204
|
pylint typedparser
|
|
179
205
|
|
|
180
206
|
# run tests
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
# typedparser
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://github.com/
|
|
5
|
-
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/
|
|
4
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37.yml">
|
|
5
|
+
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37.yml?branch=main&label=python%203.7" />
|
|
6
6
|
</a>
|
|
7
|
-
<a href="https://github.com/
|
|
8
|
-
<img alt="build 3.
|
|
7
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py38.yml">
|
|
8
|
+
<img alt="build 3.8 status" title="build 3.8 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py38.yml?branch=main&label=python%203.8" />
|
|
9
9
|
</a>
|
|
10
|
-
<
|
|
10
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py39.yml">
|
|
11
|
+
<img alt="build 3.9 status" title="build 3.9 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py39.yml?branch=main&label=python%203.9" />
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py310.yml">
|
|
14
|
+
<img alt="build 3.10 status" title="build 3.10 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py310.yml?branch=main&label=python%203.10" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py311.yml">
|
|
17
|
+
<img alt="build 3.11 status" title="build 3.11 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py311.yml?branch=main&label=python%203.11" />
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312.yml">
|
|
20
|
+
<img alt="build 3.12 status" title="build 3.12 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312.yml?branch=main&label=python%203.12" />
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37-full.yml">
|
|
23
|
+
<img alt="build 3.7 full status" title="build 3.7 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37-full.yml?branch=main&label=python%203.7%20full" />
|
|
24
|
+
</a>
|
|
25
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312-full.yml">
|
|
26
|
+
<img alt="build 3.12 full status" title="build 3.12 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312-full.yml?branch=main&label=python%203.12%20full" />
|
|
27
|
+
</a>
|
|
28
|
+
<img alt="coverage" title="coverage" src="https://raw.githubusercontent.com/simon-ging/typedparser/main/docs/coverage.svg" />
|
|
11
29
|
<a href="https://pypi.org/project/typedparser/">
|
|
12
30
|
<img alt="version" title="version" src="https://img.shields.io/pypi/v/typedparser?color=success" />
|
|
13
31
|
</a>
|
|
@@ -17,22 +35,6 @@ Typing extension for python argparse using [attrs](https://www.attrs.org/en/stab
|
|
|
17
35
|
|
|
18
36
|
Includes typechecking and conversion utilities to parse a dictionary into an attrs instance.
|
|
19
37
|
|
|
20
|
-
## Features
|
|
21
|
-
|
|
22
|
-
* Create commandline arguments with type hints and checks while
|
|
23
|
-
staying very close to the syntax of the standard library's argparse.
|
|
24
|
-
* Utilities for typechecking and converting nested objects:
|
|
25
|
-
* Nested checking and conversion of python standard types
|
|
26
|
-
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
27
|
-
* Supports positional and keyword arguments in classes
|
|
28
|
-
* Can also typecheck existing attrs instances
|
|
29
|
-
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
30
|
-
`int` to `float`
|
|
31
|
-
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
32
|
-
containers (list, dict, etc.)
|
|
33
|
-
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
34
|
-
* Some object utilities in `typedparser.objects` required for everything else
|
|
35
|
-
|
|
36
38
|
## Install
|
|
37
39
|
|
|
38
40
|
Requires `python>=3.7`
|
|
@@ -41,11 +43,11 @@ Requires `python>=3.7`
|
|
|
41
43
|
pip install typedparser
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
##
|
|
46
|
+
## Basic usage
|
|
45
47
|
|
|
46
|
-
1. Create an attrs class (decorate with `@attr.define`)
|
|
47
|
-
2. Define the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
48
|
-
3. Parse the args with `TypedParser
|
|
48
|
+
1. Create an attrs class (decorate with `@attr.define`). Note that optional arguments must also be typed as optional.
|
|
49
|
+
2. Define and type the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
50
|
+
3. Parse the args with `TypedParser` and enjoy args with type hints. Disable typechecking by setting `strict=False`.
|
|
49
51
|
|
|
50
52
|
~~~python
|
|
51
53
|
from typing import Optional
|
|
@@ -54,21 +56,21 @@ from typedparser import add_argument, TypedParser
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
@define
|
|
57
|
-
class Args:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
class Args:
|
|
60
|
+
# omit the argument name to have it inferred from the field name
|
|
61
|
+
foo: str = add_argument(positional=True)
|
|
62
|
+
bar: int = add_argument(shortcut="-b", type=int, default=0)
|
|
63
|
+
opt: Optional[str] = add_argument()
|
|
64
|
+
|
|
65
|
+
# # in case you prefer the regular argparse syntax:
|
|
66
|
+
# foo: str = add_argument("foo")
|
|
67
|
+
# bar: int = add_argument("-b", "--bar", type=int, default=0)
|
|
68
|
+
# opt: Optional[str] = add_argument("--opt")
|
|
62
69
|
|
|
63
|
-
# Omit argument name to create an optional argument --opt
|
|
64
|
-
opt: Optional[int] = add_argument(type=int)
|
|
65
70
|
|
|
66
|
-
# Use shortcut to create an optional argument -s / --short
|
|
67
|
-
short: Optional[int] = add_argument(shortcut="-s", type=int)
|
|
68
|
-
|
|
69
71
|
|
|
70
72
|
def main():
|
|
71
|
-
parser = TypedParser.create_parser(Args)
|
|
73
|
+
parser = TypedParser.create_parser(Args, strict=True)
|
|
72
74
|
args: Args = parser.parse_args()
|
|
73
75
|
print(args)
|
|
74
76
|
|
|
@@ -78,14 +80,31 @@ if __name__ == "__main__":
|
|
|
78
80
|
|
|
79
81
|
~~~
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
* Create commandline arguments with type hints and checks while
|
|
87
|
+
staying close to the syntax of the standard library's argparse.
|
|
88
|
+
* Utilities for typechecking and converting nested objects:
|
|
89
|
+
* Nested checking and conversion of python standard types
|
|
90
|
+
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
91
|
+
* Supports positional and keyword arguments in classes
|
|
92
|
+
* Can also typecheck existing attrs instances
|
|
93
|
+
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
94
|
+
`int` to `float`
|
|
95
|
+
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
96
|
+
containers (list, dict, etc.)
|
|
97
|
+
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
98
|
+
* Some object utilities in `typedparser.objects` required for everything else
|
|
99
|
+
|
|
100
|
+
## Advanced usage
|
|
82
101
|
|
|
83
102
|
* Use `TypedParser.from_parser(parser, Args)` to add typing to an existing parser. This is useful
|
|
84
103
|
to cover usecases like subparsers or argument groups.
|
|
85
104
|
* Snippet for argument lists `xarg: List[int] = add_argument(shortcut="-x", type=int, action="append", help="Xarg", default=[])`,
|
|
86
105
|
use as `-x 1 -x 2` to get `[1, 2]` in the args instance.
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
### Usage of attr utilities
|
|
89
108
|
|
|
90
109
|
Define the class hierarchy and parse the input using `attrs_from_dict`.
|
|
91
110
|
Use `@define(slots=False)` to allow multiple inheritance and setting attributes later.
|
|
@@ -119,6 +138,7 @@ print(attrs_from_dict(CfgNested, {"sub_cfg": {"foo": 1, "bar": 2}}))
|
|
|
119
138
|
the output will be a tuple
|
|
120
139
|
* Raise errors if types cannot be matched, there are unknown fields in the input or
|
|
121
140
|
abstract annotation types are used (e.g. Sequence)
|
|
141
|
+
* Set `_allow_extra_keys = True` in the class definition to allow unknown fields in the input
|
|
122
142
|
|
|
123
143
|
### Non-strict mode
|
|
124
144
|
|
|
@@ -150,7 +170,7 @@ Note: Some tests are skipped for python 3.7.
|
|
|
150
170
|
|
|
151
171
|
~~~bash
|
|
152
172
|
pip install -e .
|
|
153
|
-
pip install pytest pytest-cov pylint
|
|
173
|
+
pip install pytest pytest-cov pylint
|
|
154
174
|
pylint typedparser
|
|
155
175
|
|
|
156
176
|
# run tests
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "typedparser"
|
|
7
7
|
description = "Extension for python argparse with typehints and typechecks."
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
authors = [{ "name" = "
|
|
9
|
+
authors = [{ "name" = "simon-ging" }]
|
|
10
10
|
requires-python = ">=3.7"
|
|
11
11
|
license = { text = "Apache-2.0" }
|
|
12
12
|
keywords = ["attrs", "typing", "dict", "attr"]
|
|
@@ -18,13 +18,19 @@ classifiers = [
|
|
|
18
18
|
"Programming Language :: Python :: 3.7",
|
|
19
19
|
"Programming Language :: Python :: 3.8",
|
|
20
20
|
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
21
24
|
"Operating System :: OS Independent",
|
|
22
25
|
"Topic :: Software Development :: Version Control :: Git"
|
|
23
26
|
]
|
|
24
27
|
dynamic = ["version", "dependencies"]
|
|
25
28
|
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
full = ["numpy"]
|
|
31
|
+
|
|
26
32
|
[project.urls]
|
|
27
|
-
Project-URL = "https://github.com/
|
|
33
|
+
Project-URL = "https://github.com/simon-ging/typedparser"
|
|
28
34
|
|
|
29
35
|
[tool.setuptools]
|
|
30
36
|
zip-safe = true
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
from ._typedattr import definenumpy, attrs_from_dict, NamedTupleMixin
|
|
2
|
-
from ._typedparser import
|
|
2
|
+
from ._typedparser import (
|
|
3
|
+
add_argument,
|
|
4
|
+
TypedParser,
|
|
5
|
+
VerboseQuietArgs,
|
|
6
|
+
TaskSplitterArgs,
|
|
7
|
+
split_list_given_task_splitter_args,
|
|
8
|
+
split_list_for_processing,
|
|
9
|
+
)
|
|
3
10
|
from .custom_format import CustomArgparseFmt
|
|
4
11
|
from .objects import get_attr_names
|
|
5
12
|
|
|
@@ -9,9 +16,13 @@ __all__ = [
|
|
|
9
16
|
"NamedTupleMixin",
|
|
10
17
|
"add_argument",
|
|
11
18
|
"TypedParser",
|
|
12
|
-
"define",
|
|
13
19
|
"VerboseQuietArgs",
|
|
14
20
|
"CustomArgparseFmt",
|
|
15
21
|
"get_attr_names",
|
|
22
|
+
"TaskSplitterArgs",
|
|
23
|
+
"split_list_given_task_splitter_args",
|
|
24
|
+
"split_list_for_processing",
|
|
16
25
|
]
|
|
17
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__version__ = "0.20.3"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import collections
|
|
2
4
|
from functools import partial
|
|
3
5
|
from inspect import isclass
|
|
@@ -15,6 +17,7 @@ from typing import (
|
|
|
15
17
|
List,
|
|
16
18
|
Optional,
|
|
17
19
|
get_type_hints,
|
|
20
|
+
Callable,
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
import attrs
|
|
@@ -23,19 +26,28 @@ from attrs import define, fields_dict, fields, Attribute
|
|
|
23
26
|
|
|
24
27
|
from .objects import check_object_equality, RecursorInterface, StrictRecursor, AttrsClass
|
|
25
28
|
|
|
29
|
+
try:
|
|
30
|
+
from types import UnionType
|
|
31
|
+
except ImportError:
|
|
32
|
+
UnionType = None
|
|
33
|
+
|
|
26
34
|
try:
|
|
27
35
|
# python>=3.8
|
|
28
36
|
from typing import get_origin, get_args
|
|
29
37
|
except ImportError:
|
|
30
38
|
# python<3.8
|
|
39
|
+
# note that attrs automatically installs this and importlib-metadata for old python versions
|
|
31
40
|
from typing_extensions import get_origin, get_args
|
|
32
41
|
|
|
33
42
|
# default conversions allow to convert instead of raising errors in case of matching types
|
|
34
43
|
# e.g. create Path given str, or create float given int
|
|
35
|
-
|
|
44
|
+
# syntax: ((source_type1, source_type2, ...), target_type, conversion_function)
|
|
45
|
+
# if conversion_function is none, use target_type as conversion function
|
|
46
|
+
conversion_type = List[Tuple[Tuple[Type, ...], Type, Optional[Callable]]]
|
|
36
47
|
default_conversions: conversion_type = [
|
|
37
|
-
((str,), Path),
|
|
38
|
-
((int,), float),
|
|
48
|
+
((str,), Path, None),
|
|
49
|
+
((int,), float, None),
|
|
50
|
+
((Path,), str, Path.as_posix),
|
|
39
51
|
]
|
|
40
52
|
|
|
41
53
|
|
|
@@ -129,7 +141,16 @@ def _attrs_from_dict(
|
|
|
129
141
|
skip_unknowns: bool = False,
|
|
130
142
|
conversions: Optional[conversion_type] = None,
|
|
131
143
|
more_error_info: str = "",
|
|
144
|
+
current_position: Optional[List] = None,
|
|
132
145
|
):
|
|
146
|
+
if current_position is None:
|
|
147
|
+
current_position = []
|
|
148
|
+
if isinstance(input_dict_or_attrs, str):
|
|
149
|
+
raise TypeError(
|
|
150
|
+
f"Expected a dictionary or attrs instance, got type '{type(input_dict_or_attrs)}' "
|
|
151
|
+
f"value '{input_dict_or_attrs}' at position '{'.'.join(current_position)}' (root "
|
|
152
|
+
f"if empty).\n\nError context: {more_error_info}."
|
|
153
|
+
)
|
|
133
154
|
_print_fn(f"Parsing {cls} from {input_dict_or_attrs}")
|
|
134
155
|
input_cls = type(input_dict_or_attrs)
|
|
135
156
|
if has(input_cls):
|
|
@@ -169,7 +190,7 @@ def _attrs_from_dict(
|
|
|
169
190
|
for field_name, field_value in matching_input.items():
|
|
170
191
|
field_att = cls_fields_dict[field_name]
|
|
171
192
|
try:
|
|
172
|
-
is_positional = bool(field_att.default == attrs.NOTHING)
|
|
193
|
+
is_positional = bool(field_att.default == attrs.NOTHING) and not field_att.kw_only
|
|
173
194
|
except ValueError:
|
|
174
195
|
# some field defaults, e.g. numpy arrays do not have comparison defined here
|
|
175
196
|
# but that means that the default is set and the field is not positional
|
|
@@ -181,10 +202,30 @@ def _attrs_from_dict(
|
|
|
181
202
|
|
|
182
203
|
# create an attrs instance from the dict
|
|
183
204
|
# the instance will be flat (nested dicts are not resolved yet) and not typechecked.
|
|
184
|
-
|
|
205
|
+
try:
|
|
206
|
+
cls_inst = cls(*in_args, **in_kwargs) # noqa
|
|
207
|
+
attrs_inst = cls_inst
|
|
208
|
+
# # not sure why evolve was used before
|
|
209
|
+
# attrs_inst = attrs.evolve(cls_inst, **in_kwargs) # noqa
|
|
210
|
+
except TypeError as e:
|
|
211
|
+
raise TypeError(
|
|
212
|
+
f"Failed creating instance of {cls} from input {input_dict} "
|
|
213
|
+
f"with args {in_args} and kwargs {in_kwargs}. Error occurred at: "
|
|
214
|
+
f"{'.'.join(current_position)}. {e}\n\nError context: {more_error_info}."
|
|
215
|
+
) from e
|
|
185
216
|
|
|
186
217
|
# typecheck and unfold nested values in the attrs instance
|
|
187
|
-
|
|
218
|
+
try:
|
|
219
|
+
type_hints = get_type_hints(cls)
|
|
220
|
+
except TypeError as e:
|
|
221
|
+
raise TypeError(
|
|
222
|
+
f"Failed getting type hints for {cls} with input '{input_dict}'. Error occurred at: "
|
|
223
|
+
f"{'.'.join(current_position)}. {e}\n\nError context: {more_error_info}."
|
|
224
|
+
"Potential reasons: You used an old python version <=3.8, used from __future__ "
|
|
225
|
+
"import annotations, and annotated something as dict[str, int]. "
|
|
226
|
+
"This does not work together with get_type_hints. Solution: "
|
|
227
|
+
"Annotate with typing.Dict[str, int] instead of dict[str, int], or upgrade python."
|
|
228
|
+
) from e
|
|
188
229
|
for att in all_atts:
|
|
189
230
|
name = att.name
|
|
190
231
|
value = getattr(attrs_inst, name)
|
|
@@ -201,12 +242,13 @@ def _attrs_from_dict(
|
|
|
201
242
|
skip_unknowns=skip_unknowns,
|
|
202
243
|
conversions=conversions,
|
|
203
244
|
more_error_info=more_error_info,
|
|
245
|
+
current_position=current_position + [name],
|
|
204
246
|
)
|
|
205
247
|
setattr(attrs_inst, name, new_value)
|
|
206
248
|
|
|
207
249
|
# handle unknown fields
|
|
208
250
|
if len(nonmatching_input) > 0 and not skip_unknowns:
|
|
209
|
-
if strict:
|
|
251
|
+
if strict and not getattr(cls, "_allow_extra_keys", False):
|
|
210
252
|
raise TypeError(
|
|
211
253
|
f"Keys in input {list(nonmatching_input.keys())} not defined "
|
|
212
254
|
f"for class {cls} with attributes {sorted(all_att_names)}"
|
|
@@ -238,14 +280,17 @@ def _parse_nested(
|
|
|
238
280
|
conversions: conversion_type = None,
|
|
239
281
|
depth: int = 0,
|
|
240
282
|
more_error_info: str = "",
|
|
283
|
+
current_position: Optional[List] = None,
|
|
241
284
|
):
|
|
242
285
|
conversions = default_conversions if conversions is None else conversions
|
|
243
286
|
parse_recursive = partial(
|
|
244
287
|
_parse_nested,
|
|
245
288
|
recursor,
|
|
246
|
-
depth=depth + 1,
|
|
247
289
|
skip_unknowns=skip_unknowns,
|
|
290
|
+
conversions=conversions,
|
|
291
|
+
depth=depth + 1,
|
|
248
292
|
more_error_info=more_error_info,
|
|
293
|
+
current_position=current_position,
|
|
249
294
|
)
|
|
250
295
|
|
|
251
296
|
if isinstance(typ, str):
|
|
@@ -265,11 +310,11 @@ def _parse_nested(
|
|
|
265
310
|
value_type_name = value_type.__name__ if hasattr(value_type, "__name__") else str(value_type)
|
|
266
311
|
err_msg = (
|
|
267
312
|
f"Could not parse {name}={value} (type {value_type_name}) as type "
|
|
268
|
-
f"{target_type_name} with strict={strict}
|
|
269
|
-
# f"depth={depth} type annotation origin={origin} args={args}"
|
|
313
|
+
f"{target_type_name} with strict={strict}"
|
|
314
|
+
# f" depth={depth} type annotation origin={origin} args={args}"
|
|
270
315
|
)
|
|
271
316
|
|
|
272
|
-
def
|
|
317
|
+
def maybe_raise_typeerror(full_err_msg):
|
|
273
318
|
if strict:
|
|
274
319
|
raise TypeError(full_err_msg)
|
|
275
320
|
_print_fn(f"Caught: {full_err_msg}. Returning as is.")
|
|
@@ -284,22 +329,29 @@ def _parse_nested(
|
|
|
284
329
|
strict=strict,
|
|
285
330
|
skip_unknowns=skip_unknowns,
|
|
286
331
|
conversions=conversions,
|
|
332
|
+
more_error_info=more_error_info,
|
|
333
|
+
current_position=current_position,
|
|
287
334
|
)
|
|
288
335
|
|
|
289
336
|
# resolve any
|
|
290
|
-
if typ
|
|
337
|
+
if typ in [Any, any]:
|
|
291
338
|
return value
|
|
292
339
|
|
|
293
340
|
# resolve unions (mostly "optional")
|
|
294
|
-
if origin == Union:
|
|
341
|
+
if origin == Union or (UnionType is not None and isinstance(typ, UnionType)):
|
|
342
|
+
collected_errors = []
|
|
295
343
|
for new_typ in args:
|
|
296
344
|
try:
|
|
297
345
|
# force strict parsing and catch errors to try all different types
|
|
298
346
|
return parse_recursive(name, value, new_typ, strict=True)
|
|
299
|
-
except TypeError:
|
|
300
|
-
|
|
347
|
+
except TypeError as e:
|
|
348
|
+
collected_errors.append(f"{e}")
|
|
301
349
|
# if we get here, none of the types worked
|
|
302
|
-
|
|
350
|
+
collected_error_str = "\n".join(collected_errors)
|
|
351
|
+
return maybe_raise_typeerror(
|
|
352
|
+
f"No type in Union matches. Errors per type:\n{collected_error_str}\n"
|
|
353
|
+
f"Final error: {err_msg}"
|
|
354
|
+
)
|
|
303
355
|
|
|
304
356
|
final_list_type = None
|
|
305
357
|
|
|
@@ -308,9 +360,9 @@ def _parse_nested(
|
|
|
308
360
|
if not (len(args) == 2 and args[1] == Ellipsis):
|
|
309
361
|
# resolve fixed tuple
|
|
310
362
|
if not recursor.is_iterable_fn(value):
|
|
311
|
-
return
|
|
363
|
+
return maybe_raise_typeerror(f"{err_msg}. Expect a sequence, got {type(value)}")
|
|
312
364
|
if len(value) != len(args):
|
|
313
|
-
return
|
|
365
|
+
return maybe_raise_typeerror(
|
|
314
366
|
f"{err_msg}. Expect a sequence with length {len(args)}, got length {len(value)}"
|
|
315
367
|
)
|
|
316
368
|
new_tuple = []
|
|
@@ -324,7 +376,7 @@ def _parse_nested(
|
|
|
324
376
|
# check dict
|
|
325
377
|
if origin in (dict, collections.defaultdict):
|
|
326
378
|
if not recursor.is_mapping_fn(value):
|
|
327
|
-
return
|
|
379
|
+
return maybe_raise_typeerror(f"{err_msg}. Expect a mapping, got {type(value)}")
|
|
328
380
|
if len(args) == 0:
|
|
329
381
|
args = [Any, Any]
|
|
330
382
|
dict_keytype, dict_valuetype = args
|
|
@@ -336,9 +388,9 @@ def _parse_nested(
|
|
|
336
388
|
dict_output = {}
|
|
337
389
|
|
|
338
390
|
for key, val in value.items():
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
391
|
+
parsed_key = parse_recursive(f"{name}/dict_key_type", key, dict_keytype, strict=strict)
|
|
392
|
+
parsed_value = parse_recursive(f"{name}/{key}", val, dict_valuetype, strict=strict)
|
|
393
|
+
dict_output[parsed_key] = parsed_value
|
|
342
394
|
|
|
343
395
|
return dict_output
|
|
344
396
|
|
|
@@ -352,7 +404,7 @@ def _parse_nested(
|
|
|
352
404
|
# resolve list
|
|
353
405
|
if origin == list:
|
|
354
406
|
if not recursor.is_iterable_fn(value):
|
|
355
|
-
return
|
|
407
|
+
return maybe_raise_typeerror(f"{err_msg}. Expect iterable, got {type(value)}")
|
|
356
408
|
list_arg_type = args[0]
|
|
357
409
|
list_output = []
|
|
358
410
|
for item in value:
|
|
@@ -381,19 +433,25 @@ def _parse_nested(
|
|
|
381
433
|
except TypeError:
|
|
382
434
|
is_sc = False
|
|
383
435
|
if is_sc:
|
|
384
|
-
return
|
|
436
|
+
return maybe_raise_typeerror(f"{err_msg}. Abstract collections are not supported.")
|
|
385
437
|
|
|
386
438
|
# add explicit conversions
|
|
387
|
-
for convert_source, convert_target in conversions:
|
|
439
|
+
for convert_source, convert_target, convert_callable in conversions:
|
|
388
440
|
if not isclass(typ):
|
|
389
441
|
continue
|
|
390
442
|
if issubclass(typ, convert_target) and isinstance(value, convert_source):
|
|
391
|
-
|
|
443
|
+
if convert_callable is None:
|
|
444
|
+
convert_callable = convert_target
|
|
445
|
+
return convert_callable(value)
|
|
392
446
|
|
|
393
447
|
if strict:
|
|
394
448
|
add_info = ""
|
|
395
449
|
if typ is None:
|
|
396
|
-
add_info =
|
|
450
|
+
add_info = (
|
|
451
|
+
"Untyped fields not allowed in strict mode. "
|
|
452
|
+
"Add type annotations or set strict=False. "
|
|
453
|
+
)
|
|
454
|
+
|
|
397
455
|
raise TypeError(f"{add_info}{err_msg}. Wrong type or type not supported.")
|
|
398
456
|
_print_fn(f"{err_msg}. Returning value {value} as-is")
|
|
399
457
|
return value
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# from __future__ import annotations # do not use here, it breaks everything
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
from attr import field, define
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from typing import Optional, Type, Any
|
|
6
7
|
|
|
7
|
-
from attr import field, define
|
|
8
|
-
|
|
9
8
|
from ._typedattr import AttrsClass
|
|
10
9
|
from .custom_format import CustomArgparseFmt
|
|
11
10
|
from .funcs import parse_typed_args, add_typed_args
|
|
@@ -55,7 +54,9 @@ class TypedParser:
|
|
|
55
54
|
return typed_args
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
def add_argument(
|
|
57
|
+
def add_argument(
|
|
58
|
+
*name_or_flags: str, shortcut: Optional[str] = None, positional: bool = False, **kwargs
|
|
59
|
+
):
|
|
59
60
|
"""
|
|
60
61
|
Interface matches ArgumentParser.add_argument:
|
|
61
62
|
https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
|
|
@@ -66,6 +67,7 @@ def add_argument(*name_or_flags: str, shortcut: Optional[str] = None, **kwargs):
|
|
|
66
67
|
shortcut: Shortcut for the argument including leading single dash.
|
|
67
68
|
If given, will be added as a shortcut to the argument.
|
|
68
69
|
Cannot be used if name_or_flags are given.
|
|
70
|
+
positional: If True, will be added as a positional argument.
|
|
69
71
|
**kwargs: will be passed to argparse parser.add_argument
|
|
70
72
|
"""
|
|
71
73
|
assert "name_or_flags" not in kwargs, "Pass name_or_flags as positional arguments"
|
|
@@ -86,7 +88,12 @@ def add_argument(*name_or_flags: str, shortcut: Optional[str] = None, **kwargs):
|
|
|
86
88
|
# other actions are assumed to have default None output in argparse
|
|
87
89
|
|
|
88
90
|
return field(
|
|
89
|
-
metadata={
|
|
91
|
+
metadata={
|
|
92
|
+
"name_or_flags": name_or_flags,
|
|
93
|
+
"shortcut": shortcut,
|
|
94
|
+
"positional": positional,
|
|
95
|
+
**kwargs,
|
|
96
|
+
},
|
|
90
97
|
kw_only=True,
|
|
91
98
|
default=default,
|
|
92
99
|
)
|
|
@@ -96,3 +103,30 @@ def add_argument(*name_or_flags: str, shortcut: Optional[str] = None, **kwargs):
|
|
|
96
103
|
class VerboseQuietArgs:
|
|
97
104
|
verbose: bool = add_argument(shortcut="-v", help="Increase verbosity", action="store_true")
|
|
98
105
|
quiet: bool = add_argument(shortcut="-q", help="Reduce verbosity", action="store_true")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@define(slots=False) # slots false to allow multi inheritance
|
|
109
|
+
class TaskSplitterArgs:
|
|
110
|
+
start: int = add_argument(shortcut="-s", type=int, default=0, help="Where to start processing")
|
|
111
|
+
num: Optional[int] = add_argument(shortcut="-n", type=int, help="Number of tasks to process.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def split_list_given_task_splitter_args(
|
|
115
|
+
in_list, task_splitter_args: TaskSplitterArgs, print_fn=None
|
|
116
|
+
):
|
|
117
|
+
start, num = task_splitter_args.start, task_splitter_args.num
|
|
118
|
+
return split_list_for_processing(in_list, start, num, print_fn=print_fn)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def split_list_for_processing(in_list, start: int = 0, num: Optional[int] = None, print_fn=None):
|
|
122
|
+
len_in_list = len(in_list)
|
|
123
|
+
if start > 0:
|
|
124
|
+
in_list = in_list[start:]
|
|
125
|
+
if num is not None:
|
|
126
|
+
in_list = in_list[:num]
|
|
127
|
+
if print_fn is not None:
|
|
128
|
+
print_fn(
|
|
129
|
+
f"Split list for processing, input length {len_in_list}, starting at {start}, "
|
|
130
|
+
f"processing max {num} reduced to {len(in_list)}"
|
|
131
|
+
)
|
|
132
|
+
return in_list
|
|
@@ -2,7 +2,7 @@ import argparse
|
|
|
2
2
|
from typing import Any, Dict
|
|
3
3
|
|
|
4
4
|
from attr import AttrsInstance
|
|
5
|
-
from attrs import has, fields_dict
|
|
5
|
+
from attrs import has, fields_dict, asdict
|
|
6
6
|
|
|
7
7
|
from ._typedattr import attrs_from_dict
|
|
8
8
|
from .objects import get_attr_names
|
|
@@ -47,11 +47,36 @@ def add_typed_args(parser: argparse.ArgumentParser, typed_args_class) -> None:
|
|
|
47
47
|
continue
|
|
48
48
|
|
|
49
49
|
shortcut = field_metadata.pop("shortcut", None)
|
|
50
|
+
if shortcut is not None and len(name_or_flags) > 0:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Argument '{field_name}' invalid. With shortcut='{shortcut}' "
|
|
53
|
+
f"the argument name will be inferred from the field name and therefore the "
|
|
54
|
+
f"positional arguments must be empty, but got {name_or_flags}."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
positional = field_metadata.pop("positional", False)
|
|
58
|
+
if positional and len(name_or_flags) > 0:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Argument '{field_name}' invalid. With positional='{positional} "
|
|
61
|
+
f"the argument name will be inferred from the field name and therefore the "
|
|
62
|
+
f"positional arguments must be empty, but got {name_or_flags}."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if shortcut is not None and positional:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Argument '{field_name}' invalid. Cannot use positional and shortcut at the same "
|
|
68
|
+
f"time but got positional='{positional}' and shortcut='{shortcut}'."
|
|
69
|
+
)
|
|
70
|
+
|
|
50
71
|
if len(name_or_flags) == 0:
|
|
51
72
|
name_or_flags = []
|
|
52
|
-
if
|
|
53
|
-
name_or_flags.append(
|
|
54
|
-
|
|
73
|
+
if positional:
|
|
74
|
+
name_or_flags.append(field_name)
|
|
75
|
+
else:
|
|
76
|
+
if shortcut is not None:
|
|
77
|
+
name_or_flags.append(shortcut)
|
|
78
|
+
name_or_flags.append(f"--{field_name}")
|
|
79
|
+
|
|
55
80
|
try:
|
|
56
81
|
parser.add_argument(*name_or_flags, **field_metadata)
|
|
57
82
|
except TypeError as e:
|
|
@@ -92,8 +117,11 @@ def parse_typed_args(
|
|
|
92
117
|
if len(missing_args) > 0:
|
|
93
118
|
args_desc = {k: args_dict[k] for k in sorted(missing_args)}
|
|
94
119
|
missing_err = (
|
|
95
|
-
f"
|
|
96
|
-
f"'{typed_args_class.__name__}'
|
|
120
|
+
f"Arguments received from argparse {args_desc} "
|
|
121
|
+
f"are missing from argument definition '{typed_args_class.__name__}'. "
|
|
122
|
+
f"This probably means there is a mismatch between the call to add_argument() "
|
|
123
|
+
f"and the attribute name. "
|
|
124
|
+
f"Available keys in '{typed_args_class.__name__}': {fields_keys}."
|
|
97
125
|
)
|
|
98
126
|
if strict:
|
|
99
127
|
raise KeyError(missing_err)
|
|
@@ -113,7 +141,7 @@ def parse_typed_args(
|
|
|
113
141
|
raise AttributeError(
|
|
114
142
|
f"Arguments are missing from configuration and cannot be added. "
|
|
115
143
|
f"Either add it to the configuration or decorate with @attrs.define(slots=False) "
|
|
116
|
-
f"to allow adding unknown fields. Missing: {
|
|
144
|
+
f"to allow adding unknown fields. Missing: {missing_args}"
|
|
117
145
|
) from e # noqa
|
|
118
146
|
return out_args
|
|
119
147
|
|
|
@@ -4,17 +4,20 @@ Generic utilities for python objects
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from abc import ABCMeta, abstractmethod
|
|
7
|
+
from attr import has, AttrsInstance
|
|
8
|
+
from attrs import fields
|
|
7
9
|
from collections.abc import Mapping, Iterable
|
|
8
10
|
from copy import deepcopy
|
|
9
11
|
from functools import partial
|
|
10
12
|
from typing import Any, Callable, Type, List
|
|
11
13
|
|
|
12
|
-
import numpy as np
|
|
13
|
-
from attr import has, AttrsInstance
|
|
14
|
-
from attrs import fields
|
|
15
|
-
|
|
16
14
|
AttrsClass = Type[AttrsInstance]
|
|
17
15
|
|
|
16
|
+
try:
|
|
17
|
+
import numpy as np
|
|
18
|
+
except ImportError:
|
|
19
|
+
np = None
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
def get_attr_names(cls: AttrsClass) -> List[str]:
|
|
20
23
|
"""Get all attribute names of an attrs class."""
|
|
@@ -270,7 +273,7 @@ def _compare_leaf(
|
|
|
270
273
|
d1: Any, d2: Any, _depth: int, rtol=1.0e-5, atol=1.0e-8, equal_nan=False, prefix=""
|
|
271
274
|
) -> List[str]:
|
|
272
275
|
# at this point the 2 leaves are guaranteed to be the same type
|
|
273
|
-
if isinstance(d1, np.ndarray):
|
|
276
|
+
if np is not None and isinstance(d1, np.ndarray):
|
|
274
277
|
comp = np.allclose(d1, d2, rtol=rtol, atol=atol, equal_nan=equal_nan)
|
|
275
278
|
else:
|
|
276
279
|
comp = d1 == d2
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: typedparser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.3
|
|
4
4
|
Summary: Extension for python argparse with typehints and typechecks.
|
|
5
|
-
Author:
|
|
5
|
+
Author: simon-ging
|
|
6
6
|
License: Apache-2.0
|
|
7
|
-
Project-URL: Project-URL, https://github.com/
|
|
7
|
+
Project-URL: Project-URL, https://github.com/simon-ging/typedparser
|
|
8
8
|
Keywords: attrs,typing,dict,attr
|
|
9
9
|
Platform: any
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -14,24 +14,48 @@ Classifier: Natural Language :: English
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.7
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.8
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
20
|
Classifier: Operating System :: OS Independent
|
|
18
21
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
22
|
Requires-Python: >=3.7
|
|
20
23
|
Description-Content-Type: text/markdown
|
|
21
24
|
License-File: LICENSE
|
|
22
25
|
Requires-Dist: attrs
|
|
23
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: importlib_metadata; python_version < "3.8"
|
|
27
|
+
Requires-Dist: typing_extensions; python_version < "3.8"
|
|
28
|
+
Provides-Extra: full
|
|
29
|
+
Requires-Dist: numpy; extra == "full"
|
|
24
30
|
|
|
25
31
|
# typedparser
|
|
26
32
|
|
|
27
33
|
<p align="center">
|
|
28
|
-
<a href="https://github.com/
|
|
29
|
-
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/
|
|
34
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37.yml">
|
|
35
|
+
<img alt="build 3.7 status" title="build 3.7 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37.yml?branch=main&label=python%203.7" />
|
|
30
36
|
</a>
|
|
31
|
-
<a href="https://github.com/
|
|
32
|
-
<img alt="build 3.
|
|
37
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py38.yml">
|
|
38
|
+
<img alt="build 3.8 status" title="build 3.8 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py38.yml?branch=main&label=python%203.8" />
|
|
33
39
|
</a>
|
|
34
|
-
<
|
|
40
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py39.yml">
|
|
41
|
+
<img alt="build 3.9 status" title="build 3.9 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py39.yml?branch=main&label=python%203.9" />
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py310.yml">
|
|
44
|
+
<img alt="build 3.10 status" title="build 3.10 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py310.yml?branch=main&label=python%203.10" />
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py311.yml">
|
|
47
|
+
<img alt="build 3.11 status" title="build 3.11 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py311.yml?branch=main&label=python%203.11" />
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312.yml">
|
|
50
|
+
<img alt="build 3.12 status" title="build 3.12 status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312.yml?branch=main&label=python%203.12" />
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py37-full.yml">
|
|
53
|
+
<img alt="build 3.7 full status" title="build 3.7 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py37-full.yml?branch=main&label=python%203.7%20full" />
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://github.com/simon-ging/typedparser/actions/workflows/build-py312-full.yml">
|
|
56
|
+
<img alt="build 3.12 full status" title="build 3.12 full status" src="https://img.shields.io/github/actions/workflow/status/simon-ging/typedparser/build-py312-full.yml?branch=main&label=python%203.12%20full" />
|
|
57
|
+
</a>
|
|
58
|
+
<img alt="coverage" title="coverage" src="https://raw.githubusercontent.com/simon-ging/typedparser/main/docs/coverage.svg" />
|
|
35
59
|
<a href="https://pypi.org/project/typedparser/">
|
|
36
60
|
<img alt="version" title="version" src="https://img.shields.io/pypi/v/typedparser?color=success" />
|
|
37
61
|
</a>
|
|
@@ -41,22 +65,6 @@ Typing extension for python argparse using [attrs](https://www.attrs.org/en/stab
|
|
|
41
65
|
|
|
42
66
|
Includes typechecking and conversion utilities to parse a dictionary into an attrs instance.
|
|
43
67
|
|
|
44
|
-
## Features
|
|
45
|
-
|
|
46
|
-
* Create commandline arguments with type hints and checks while
|
|
47
|
-
staying very close to the syntax of the standard library's argparse.
|
|
48
|
-
* Utilities for typechecking and converting nested objects:
|
|
49
|
-
* Nested checking and conversion of python standard types
|
|
50
|
-
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
51
|
-
* Supports positional and keyword arguments in classes
|
|
52
|
-
* Can also typecheck existing attrs instances
|
|
53
|
-
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
54
|
-
`int` to `float`
|
|
55
|
-
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
56
|
-
containers (list, dict, etc.)
|
|
57
|
-
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
58
|
-
* Some object utilities in `typedparser.objects` required for everything else
|
|
59
|
-
|
|
60
68
|
## Install
|
|
61
69
|
|
|
62
70
|
Requires `python>=3.7`
|
|
@@ -65,11 +73,11 @@ Requires `python>=3.7`
|
|
|
65
73
|
pip install typedparser
|
|
66
74
|
```
|
|
67
75
|
|
|
68
|
-
##
|
|
76
|
+
## Basic usage
|
|
69
77
|
|
|
70
|
-
1. Create an attrs class (decorate with `@attr.define`)
|
|
71
|
-
2. Define the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
72
|
-
3. Parse the args with `TypedParser
|
|
78
|
+
1. Create an attrs class (decorate with `@attr.define`). Note that optional arguments must also be typed as optional.
|
|
79
|
+
2. Define and type the fields with `typedparser.add_argument` - the syntax extends [add_argument from argparse](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
|
80
|
+
3. Parse the args with `TypedParser` and enjoy args with type hints. Disable typechecking by setting `strict=False`.
|
|
73
81
|
|
|
74
82
|
~~~python
|
|
75
83
|
from typing import Optional
|
|
@@ -78,21 +86,21 @@ from typedparser import add_argument, TypedParser
|
|
|
78
86
|
|
|
79
87
|
|
|
80
88
|
@define
|
|
81
|
-
class Args:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
class Args:
|
|
90
|
+
# omit the argument name to have it inferred from the field name
|
|
91
|
+
foo: str = add_argument(positional=True)
|
|
92
|
+
bar: int = add_argument(shortcut="-b", type=int, default=0)
|
|
93
|
+
opt: Optional[str] = add_argument()
|
|
94
|
+
|
|
95
|
+
# # in case you prefer the regular argparse syntax:
|
|
96
|
+
# foo: str = add_argument("foo")
|
|
97
|
+
# bar: int = add_argument("-b", "--bar", type=int, default=0)
|
|
98
|
+
# opt: Optional[str] = add_argument("--opt")
|
|
86
99
|
|
|
87
|
-
# Omit argument name to create an optional argument --opt
|
|
88
|
-
opt: Optional[int] = add_argument(type=int)
|
|
89
100
|
|
|
90
|
-
# Use shortcut to create an optional argument -s / --short
|
|
91
|
-
short: Optional[int] = add_argument(shortcut="-s", type=int)
|
|
92
|
-
|
|
93
101
|
|
|
94
102
|
def main():
|
|
95
|
-
parser = TypedParser.create_parser(Args)
|
|
103
|
+
parser = TypedParser.create_parser(Args, strict=True)
|
|
96
104
|
args: Args = parser.parse_args()
|
|
97
105
|
print(args)
|
|
98
106
|
|
|
@@ -102,14 +110,31 @@ if __name__ == "__main__":
|
|
|
102
110
|
|
|
103
111
|
~~~
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
|
|
114
|
+
## Features
|
|
115
|
+
|
|
116
|
+
* Create commandline arguments with type hints and checks while
|
|
117
|
+
staying close to the syntax of the standard library's argparse.
|
|
118
|
+
* Utilities for typechecking and converting nested objects:
|
|
119
|
+
* Nested checking and conversion of python standard types
|
|
120
|
+
* Supports old and new style typing (e.g. `typing.List` and `list`)
|
|
121
|
+
* Supports positional and keyword arguments in classes
|
|
122
|
+
* Can also typecheck existing attrs instances
|
|
123
|
+
* Allows custom conversions, by default converts source type `str` to target type `Path` and
|
|
124
|
+
`int` to `float`
|
|
125
|
+
* Allows to redefine which objects will be recursed into, by default recurses into standard
|
|
126
|
+
containers (list, dict, etc.)
|
|
127
|
+
* `@definenumpy` decorator for equality check if the instances contains numpy arrays
|
|
128
|
+
* Some object utilities in `typedparser.objects` required for everything else
|
|
129
|
+
|
|
130
|
+
## Advanced usage
|
|
106
131
|
|
|
107
132
|
* Use `TypedParser.from_parser(parser, Args)` to add typing to an existing parser. This is useful
|
|
108
133
|
to cover usecases like subparsers or argument groups.
|
|
109
134
|
* Snippet for argument lists `xarg: List[int] = add_argument(shortcut="-x", type=int, action="append", help="Xarg", default=[])`,
|
|
110
135
|
use as `-x 1 -x 2` to get `[1, 2]` in the args instance.
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
### Usage of attr utilities
|
|
113
138
|
|
|
114
139
|
Define the class hierarchy and parse the input using `attrs_from_dict`.
|
|
115
140
|
Use `@define(slots=False)` to allow multiple inheritance and setting attributes later.
|
|
@@ -143,6 +168,7 @@ print(attrs_from_dict(CfgNested, {"sub_cfg": {"foo": 1, "bar": 2}}))
|
|
|
143
168
|
the output will be a tuple
|
|
144
169
|
* Raise errors if types cannot be matched, there are unknown fields in the input or
|
|
145
170
|
abstract annotation types are used (e.g. Sequence)
|
|
171
|
+
* Set `_allow_extra_keys = True` in the class definition to allow unknown fields in the input
|
|
146
172
|
|
|
147
173
|
### Non-strict mode
|
|
148
174
|
|
|
@@ -174,7 +200,7 @@ Note: Some tests are skipped for python 3.7.
|
|
|
174
200
|
|
|
175
201
|
~~~bash
|
|
176
202
|
pip install -e .
|
|
177
|
-
pip install pytest pytest-cov pylint
|
|
203
|
+
pip install pytest pytest-cov pylint
|
|
178
204
|
pylint typedparser
|
|
179
205
|
|
|
180
206
|
# run tests
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|