dataclass-args 1.2.0__tar.gz → 1.3.0__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.
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/PKG-INFO +106 -2
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/README.md +105 -1
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/__init__.py +13 -1
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/annotations.py +217 -0
- dataclass_args-1.3.0/dataclass_args/append_action.py +48 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/builder.py +186 -7
- dataclass_args-1.3.0/dataclass_args/formatter.py +45 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/utils.py +5 -5
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args.egg-info/PKG-INFO +106 -2
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args.egg-info/SOURCES.txt +5 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/pyproject.toml +1 -2
- dataclass_args-1.3.0/tests/test_boolean_base_configs.py +326 -0
- dataclass_args-1.3.0/tests/test_cli_append.py +841 -0
- dataclass_args-1.3.0/tests/test_description.py +243 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/LICENSE +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/exceptions.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args/file_loading.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args.egg-info/dependency_links.txt +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args.egg-info/requires.txt +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/dataclass_args.egg-info/top_level.txt +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/setup.cfg +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_annotations.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_basic.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_boolean_flags.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_builder_advanced.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_cli_choices.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_cli_short.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_combine_annotations.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_config_merging_simple.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_file_loading.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_positional.py +0 -0
- {dataclass_args-1.2.0 → dataclass_args-1.3.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataclass-args
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Zero-boilerplate CLI generation from Python dataclasses with advanced type support and file loading
|
|
5
5
|
Author-email: Martin Bartlett <martin.j.bartlett@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -52,7 +52,8 @@ Dynamic: license-file
|
|
|
52
52
|
Generate command-line interfaces from Python dataclasses.
|
|
53
53
|
|
|
54
54
|
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/test.yml)
|
|
55
|
-
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml)
|
|
56
|
+
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/quality.yml)
|
|
56
57
|
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/examples.yml)
|
|
57
58
|
[](https://www.python.org/downloads/)
|
|
58
59
|
[](https://pypi.org/project/dataclass-args/)
|
|
@@ -66,6 +67,7 @@ Generate command-line interfaces from Python dataclasses.
|
|
|
66
67
|
- **[Short Options](#short-options)** - Concise `-n` flags in addition to `--name`
|
|
67
68
|
- **[Boolean Flags](#boolean-flags)** - Proper `--flag` and `--no-flag` boolean handling
|
|
68
69
|
- **[Value Validation](#value-choices)** - Restrict values with `cli_choices()`
|
|
70
|
+
- **[Repeatable Options](#repeatable-options)** - Allow options to be specified multiple times with `cli_append()`
|
|
69
71
|
- **[File Loading](#file-loadable-parameters)** - Load parameters from files using `@filename` syntax
|
|
70
72
|
- **[Config Merging](#configuration-merging)** - Combine configuration sources with hierarchical overrides
|
|
71
73
|
- **[Flexible Types](#type-support)** - Support for `List`, `Dict`, `Optional`, and custom types
|
|
@@ -209,6 +211,108 @@ error: argument --environment: invalid choice: 'invalid' (choose from 'dev', 'st
|
|
|
209
211
|
|
|
210
212
|
|
|
211
213
|
|
|
214
|
+
### Repeatable Options
|
|
215
|
+
|
|
216
|
+
Use `cli_append()` to allow an option to be specified multiple times, with each occurrence collecting its own arguments:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from dataclass_args import cli_append
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class Config:
|
|
223
|
+
# Simple tags: each -t adds one value
|
|
224
|
+
tags: List[str] = combine_annotations(
|
|
225
|
+
cli_short('t'),
|
|
226
|
+
cli_append(),
|
|
227
|
+
cli_help("Add a tag"),
|
|
228
|
+
default_factory=list
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Each -t occurrence accumulates
|
|
234
|
+
$ python app.py -t python -t cli -t tool
|
|
235
|
+
# Result: ['python', 'cli', 'tool']
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Repeatable with Multiple Arguments
|
|
239
|
+
|
|
240
|
+
Each occurrence can take multiple arguments using `nargs`:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
@dataclass
|
|
244
|
+
class DockerConfig:
|
|
245
|
+
# Each -p takes exactly 2 arguments (HOST CONTAINER)
|
|
246
|
+
ports: List[List[str]] = combine_annotations(
|
|
247
|
+
cli_short('p'),
|
|
248
|
+
cli_append(nargs=2),
|
|
249
|
+
cli_help("Port mapping (HOST CONTAINER)"),
|
|
250
|
+
default_factory=list
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Each -v takes exactly 2 arguments (SOURCE TARGET)
|
|
254
|
+
volumes: List[List[str]] = combine_annotations(
|
|
255
|
+
cli_short('v'),
|
|
256
|
+
cli_append(nargs=2),
|
|
257
|
+
cli_help("Volume mount (SOURCE TARGET)"),
|
|
258
|
+
default_factory=list
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
$ python docker.py -p 8080 80 -p 8443 443 -v /host/data /container/data
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Variable Arguments with Validation
|
|
267
|
+
|
|
268
|
+
Use `min_args` and `max_args` for flexible argument counts with automatic validation:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
@dataclass
|
|
272
|
+
class UploadConfig:
|
|
273
|
+
files: List[List[str]] = combine_annotations(
|
|
274
|
+
cli_short('f'),
|
|
275
|
+
cli_append(min_args=1, max_args=2, metavar="FILE [MIMETYPE]"),
|
|
276
|
+
cli_help("File with optional MIME type"),
|
|
277
|
+
default_factory=list
|
|
278
|
+
)
|
|
279
|
+
# No __post_init__ needed - validation is automatic!
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
# Mix files with and without MIME types
|
|
284
|
+
$ python upload.py -f doc.pdf application/pdf -f image.png -f video.mp4 video/mp4
|
|
285
|
+
# Result: [['doc.pdf', 'application/pdf'], ['image.png'], ['video.mp4', 'video/mp4']]
|
|
286
|
+
|
|
287
|
+
# Validation catches errors automatically
|
|
288
|
+
$ python upload.py -f file1 arg2 arg3 arg4
|
|
289
|
+
# Error: Expected at most 2 argument(s), got 4
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Clean help display:**
|
|
293
|
+
```
|
|
294
|
+
-f FILE [MIMETYPE], --files FILE [MIMETYPE]
|
|
295
|
+
File with optional MIME type (can be repeated, 1-2 args each)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Parameters:**
|
|
299
|
+
- `min_args`: Minimum arguments per occurrence
|
|
300
|
+
- `max_args`: Maximum arguments per occurrence
|
|
301
|
+
- Must be used together (both or neither)
|
|
302
|
+
- Mutually exclusive with `nargs`
|
|
303
|
+
|
|
304
|
+
**nargs Options:**
|
|
305
|
+
- `None` - One value per occurrence → `List[T]`
|
|
306
|
+
- `int` (e.g., `2`) - Exact count per occurrence → `List[List[T]]`
|
|
307
|
+
- `'+'` - One or more per occurrence → `List[List[T]]`
|
|
308
|
+
- `'*'` - Zero or more per occurrence → `List[List[T]]`
|
|
309
|
+
|
|
310
|
+
**Use Cases:**
|
|
311
|
+
- Docker-style options: `-p 8080:80 -p 8443:443 -v /host:/container -e KEY=value`
|
|
312
|
+
- File operations: `-f file1 type1 -f file2 -f file3 type3`
|
|
313
|
+
- Server pools: `-s host1 port1 -s host2 port2`
|
|
314
|
+
- Build systems: `-I dir1 -I dir2 --define KEY VAL`
|
|
315
|
+
|
|
212
316
|
### Positional Arguments
|
|
213
317
|
|
|
214
318
|
Add positional arguments that don't require `--` prefixes:
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Generate command-line interfaces from Python dataclasses.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/test.yml)
|
|
6
|
-
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml)
|
|
7
|
+
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/quality.yml)
|
|
7
8
|
[](https://github.com/bassmanitram/dataclass-args/actions/workflows/examples.yml)
|
|
8
9
|
[](https://www.python.org/downloads/)
|
|
9
10
|
[](https://pypi.org/project/dataclass-args/)
|
|
@@ -17,6 +18,7 @@ Generate command-line interfaces from Python dataclasses.
|
|
|
17
18
|
- **[Short Options](#short-options)** - Concise `-n` flags in addition to `--name`
|
|
18
19
|
- **[Boolean Flags](#boolean-flags)** - Proper `--flag` and `--no-flag` boolean handling
|
|
19
20
|
- **[Value Validation](#value-choices)** - Restrict values with `cli_choices()`
|
|
21
|
+
- **[Repeatable Options](#repeatable-options)** - Allow options to be specified multiple times with `cli_append()`
|
|
20
22
|
- **[File Loading](#file-loadable-parameters)** - Load parameters from files using `@filename` syntax
|
|
21
23
|
- **[Config Merging](#configuration-merging)** - Combine configuration sources with hierarchical overrides
|
|
22
24
|
- **[Flexible Types](#type-support)** - Support for `List`, `Dict`, `Optional`, and custom types
|
|
@@ -160,6 +162,108 @@ error: argument --environment: invalid choice: 'invalid' (choose from 'dev', 'st
|
|
|
160
162
|
|
|
161
163
|
|
|
162
164
|
|
|
165
|
+
### Repeatable Options
|
|
166
|
+
|
|
167
|
+
Use `cli_append()` to allow an option to be specified multiple times, with each occurrence collecting its own arguments:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from dataclass_args import cli_append
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class Config:
|
|
174
|
+
# Simple tags: each -t adds one value
|
|
175
|
+
tags: List[str] = combine_annotations(
|
|
176
|
+
cli_short('t'),
|
|
177
|
+
cli_append(),
|
|
178
|
+
cli_help("Add a tag"),
|
|
179
|
+
default_factory=list
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Each -t occurrence accumulates
|
|
185
|
+
$ python app.py -t python -t cli -t tool
|
|
186
|
+
# Result: ['python', 'cli', 'tool']
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Repeatable with Multiple Arguments
|
|
190
|
+
|
|
191
|
+
Each occurrence can take multiple arguments using `nargs`:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
@dataclass
|
|
195
|
+
class DockerConfig:
|
|
196
|
+
# Each -p takes exactly 2 arguments (HOST CONTAINER)
|
|
197
|
+
ports: List[List[str]] = combine_annotations(
|
|
198
|
+
cli_short('p'),
|
|
199
|
+
cli_append(nargs=2),
|
|
200
|
+
cli_help("Port mapping (HOST CONTAINER)"),
|
|
201
|
+
default_factory=list
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Each -v takes exactly 2 arguments (SOURCE TARGET)
|
|
205
|
+
volumes: List[List[str]] = combine_annotations(
|
|
206
|
+
cli_short('v'),
|
|
207
|
+
cli_append(nargs=2),
|
|
208
|
+
cli_help("Volume mount (SOURCE TARGET)"),
|
|
209
|
+
default_factory=list
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
$ python docker.py -p 8080 80 -p 8443 443 -v /host/data /container/data
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Variable Arguments with Validation
|
|
218
|
+
|
|
219
|
+
Use `min_args` and `max_args` for flexible argument counts with automatic validation:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
@dataclass
|
|
223
|
+
class UploadConfig:
|
|
224
|
+
files: List[List[str]] = combine_annotations(
|
|
225
|
+
cli_short('f'),
|
|
226
|
+
cli_append(min_args=1, max_args=2, metavar="FILE [MIMETYPE]"),
|
|
227
|
+
cli_help("File with optional MIME type"),
|
|
228
|
+
default_factory=list
|
|
229
|
+
)
|
|
230
|
+
# No __post_init__ needed - validation is automatic!
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Mix files with and without MIME types
|
|
235
|
+
$ python upload.py -f doc.pdf application/pdf -f image.png -f video.mp4 video/mp4
|
|
236
|
+
# Result: [['doc.pdf', 'application/pdf'], ['image.png'], ['video.mp4', 'video/mp4']]
|
|
237
|
+
|
|
238
|
+
# Validation catches errors automatically
|
|
239
|
+
$ python upload.py -f file1 arg2 arg3 arg4
|
|
240
|
+
# Error: Expected at most 2 argument(s), got 4
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Clean help display:**
|
|
244
|
+
```
|
|
245
|
+
-f FILE [MIMETYPE], --files FILE [MIMETYPE]
|
|
246
|
+
File with optional MIME type (can be repeated, 1-2 args each)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Parameters:**
|
|
250
|
+
- `min_args`: Minimum arguments per occurrence
|
|
251
|
+
- `max_args`: Maximum arguments per occurrence
|
|
252
|
+
- Must be used together (both or neither)
|
|
253
|
+
- Mutually exclusive with `nargs`
|
|
254
|
+
|
|
255
|
+
**nargs Options:**
|
|
256
|
+
- `None` - One value per occurrence → `List[T]`
|
|
257
|
+
- `int` (e.g., `2`) - Exact count per occurrence → `List[List[T]]`
|
|
258
|
+
- `'+'` - One or more per occurrence → `List[List[T]]`
|
|
259
|
+
- `'*'` - Zero or more per occurrence → `List[List[T]]`
|
|
260
|
+
|
|
261
|
+
**Use Cases:**
|
|
262
|
+
- Docker-style options: `-p 8080:80 -p 8443:443 -v /host:/container -e KEY=value`
|
|
263
|
+
- File operations: `-f file1 type1 -f file2 -f file3 type3`
|
|
264
|
+
- Server pools: `-s host1 port1 -s host2 port2`
|
|
265
|
+
- Build systems: `-I dir1 -I dir2 --define KEY VAL`
|
|
266
|
+
|
|
163
267
|
### Positional Arguments
|
|
164
268
|
|
|
165
269
|
Add positional arguments that don't require `--` prefixes:
|
|
@@ -43,6 +43,7 @@ Advanced Usage:
|
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
45
|
from .annotations import (
|
|
46
|
+
cli_append,
|
|
46
47
|
cli_choices,
|
|
47
48
|
cli_exclude,
|
|
48
49
|
cli_file_loadable,
|
|
@@ -51,10 +52,15 @@ from .annotations import (
|
|
|
51
52
|
cli_positional,
|
|
52
53
|
cli_short,
|
|
53
54
|
combine_annotations,
|
|
55
|
+
get_cli_append_max_args,
|
|
56
|
+
get_cli_append_metavar,
|
|
57
|
+
get_cli_append_min_args,
|
|
58
|
+
get_cli_append_nargs,
|
|
54
59
|
get_cli_choices,
|
|
55
60
|
get_cli_positional_metavar,
|
|
56
61
|
get_cli_positional_nargs,
|
|
57
62
|
get_cli_short,
|
|
63
|
+
is_cli_append,
|
|
58
64
|
is_cli_excluded,
|
|
59
65
|
is_cli_file_loadable,
|
|
60
66
|
is_cli_included,
|
|
@@ -65,7 +71,7 @@ from .exceptions import ConfigBuilderError, ConfigurationError, FileLoadingError
|
|
|
65
71
|
from .file_loading import is_file_loadable_value, load_file_content
|
|
66
72
|
from .utils import load_structured_file
|
|
67
73
|
|
|
68
|
-
__version__ = "1.
|
|
74
|
+
__version__ = "1.3.0"
|
|
69
75
|
|
|
70
76
|
__all__ = [
|
|
71
77
|
# Main API
|
|
@@ -80,15 +86,21 @@ __all__ = [
|
|
|
80
86
|
"cli_include",
|
|
81
87
|
"cli_file_loadable",
|
|
82
88
|
"cli_positional",
|
|
89
|
+
"cli_append",
|
|
83
90
|
"combine_annotations",
|
|
84
91
|
"get_cli_short",
|
|
85
92
|
"get_cli_choices",
|
|
86
93
|
"get_cli_positional_nargs",
|
|
87
94
|
"get_cli_positional_metavar",
|
|
95
|
+
"get_cli_append_nargs",
|
|
96
|
+
"get_cli_append_metavar",
|
|
97
|
+
"get_cli_append_min_args",
|
|
98
|
+
"get_cli_append_max_args",
|
|
88
99
|
"is_cli_file_loadable",
|
|
89
100
|
"is_cli_excluded",
|
|
90
101
|
"is_cli_included",
|
|
91
102
|
"is_cli_positional",
|
|
103
|
+
"is_cli_append",
|
|
92
104
|
# File loading
|
|
93
105
|
"load_file_content",
|
|
94
106
|
"is_file_loadable_value",
|
|
@@ -219,6 +219,143 @@ def cli_file_loadable(**kwargs) -> Any:
|
|
|
219
219
|
return field(**field_kwargs)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def cli_append(
|
|
223
|
+
nargs: Optional[Any] = None,
|
|
224
|
+
min_args: Optional[int] = None,
|
|
225
|
+
max_args: Optional[int] = None,
|
|
226
|
+
metavar: Optional[str] = None,
|
|
227
|
+
**kwargs,
|
|
228
|
+
) -> Any:
|
|
229
|
+
"""
|
|
230
|
+
Mark a field for append action - allows repeating the option multiple times.
|
|
231
|
+
|
|
232
|
+
Each occurrence of the option collects its arguments into a sub-list,
|
|
233
|
+
and all sub-lists are collected into the final list.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
nargs: Number of arguments per option occurrence (traditional argparse style)
|
|
237
|
+
None = exactly one (each -f takes 1 arg)
|
|
238
|
+
'?' = zero or one
|
|
239
|
+
'*' = zero or more
|
|
240
|
+
'+' = one or more
|
|
241
|
+
int = exact count (e.g., 2 for pairs)
|
|
242
|
+
Mutually exclusive with min_args/max_args
|
|
243
|
+
min_args: Minimum arguments per occurrence (e.g., 1 for "at least 1")
|
|
244
|
+
Must be used together with max_args
|
|
245
|
+
Mutually exclusive with nargs
|
|
246
|
+
max_args: Maximum arguments per occurrence (e.g., 2 for "at most 2")
|
|
247
|
+
Must be used together with min_args
|
|
248
|
+
Mutually exclusive with nargs
|
|
249
|
+
metavar: Name for display in help text (e.g., "FILE [MIMETYPE]")
|
|
250
|
+
**kwargs: Additional field parameters (default_factory, etc.)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Field object with append metadata
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
Basic append with single values:
|
|
257
|
+
|
|
258
|
+
>>> @dataclass
|
|
259
|
+
... class Config:
|
|
260
|
+
... tags: List[str] = cli_append()
|
|
261
|
+
|
|
262
|
+
CLI: -t python -t cli -t dataclass
|
|
263
|
+
Result: ['python', 'cli', 'dataclass']
|
|
264
|
+
|
|
265
|
+
Append with pairs (nargs=2):
|
|
266
|
+
|
|
267
|
+
>>> @dataclass
|
|
268
|
+
... class Config:
|
|
269
|
+
... files: List[List[str]] = cli_append(nargs=2)
|
|
270
|
+
|
|
271
|
+
CLI: -f file1.txt text/plain -f file2.jpg image/jpeg
|
|
272
|
+
Result: [['file1.txt', 'text/plain'], ['file2.jpg', 'image/jpeg']]
|
|
273
|
+
|
|
274
|
+
Append with variable args (nargs='+'):
|
|
275
|
+
|
|
276
|
+
>>> @dataclass
|
|
277
|
+
... class Config:
|
|
278
|
+
... groups: List[List[str]] = cli_append(nargs='+')
|
|
279
|
+
|
|
280
|
+
CLI: -g file1 file2 -g file3 -g file4 file5 file6
|
|
281
|
+
Result: [['file1', 'file2'], ['file3'], ['file4', 'file5', 'file6']]
|
|
282
|
+
|
|
283
|
+
Combined with other annotations:
|
|
284
|
+
|
|
285
|
+
>>> @dataclass
|
|
286
|
+
... class Config:
|
|
287
|
+
... files: List[List[str]] = combine_annotations(
|
|
288
|
+
... cli_short('f'),
|
|
289
|
+
... cli_append(nargs='+'),
|
|
290
|
+
... cli_help("File with optional MIME type"),
|
|
291
|
+
... default_factory=list
|
|
292
|
+
... )
|
|
293
|
+
|
|
294
|
+
CLI: -f doc.pdf application/pdf -f image.png -f video.mp4 video/mp4
|
|
295
|
+
Result: [['doc.pdf', 'application/pdf'], ['image.png'], ['video.mp4', 'video/mp4']]
|
|
296
|
+
|
|
297
|
+
With metavar for better help text:
|
|
298
|
+
|
|
299
|
+
>>> @dataclass
|
|
300
|
+
... class Config:
|
|
301
|
+
... files: List[List[str]] = combine_annotations(
|
|
302
|
+
... cli_short('f'),
|
|
303
|
+
... cli_append(nargs='+', metavar="FILE [MIMETYPE]"),
|
|
304
|
+
... cli_help("File with optional MIME type"),
|
|
305
|
+
... default_factory=list
|
|
306
|
+
... )
|
|
307
|
+
|
|
308
|
+
Note:
|
|
309
|
+
- Field type should be List[T] for nargs=None, or List[List[T]] for nargs with multiple values
|
|
310
|
+
- Always use default_factory=list for append fields
|
|
311
|
+
- Cannot be combined with cli_positional()
|
|
312
|
+
"""
|
|
313
|
+
# Validate mutually exclusive parameters
|
|
314
|
+
if nargs is not None and (min_args is not None or max_args is not None):
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"cli_append: 'nargs' and 'min_args'/'max_args' are mutually exclusive. "
|
|
317
|
+
"Use either nargs for standard argparse behavior, or min_args/max_args for range validation."
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Validate min_args/max_args must be used together
|
|
321
|
+
if (min_args is not None) != (max_args is not None):
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"cli_append: 'min_args' and 'max_args' must be used together. "
|
|
324
|
+
f"Got min_args={min_args}, max_args={max_args}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Validate range constraints
|
|
328
|
+
if min_args is not None and max_args is not None:
|
|
329
|
+
if min_args < 1:
|
|
330
|
+
raise ValueError(f"cli_append: 'min_args' must be >= 1, got {min_args}")
|
|
331
|
+
if max_args < min_args:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"cli_append: 'max_args' ({max_args}) must be >= min_args ({min_args})"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
field_kwargs = kwargs.copy()
|
|
337
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
338
|
+
metadata["cli_append"] = True
|
|
339
|
+
|
|
340
|
+
if nargs is not None:
|
|
341
|
+
metadata["cli_append_nargs"] = nargs
|
|
342
|
+
|
|
343
|
+
if metavar is not None:
|
|
344
|
+
metadata["cli_append_metavar"] = metavar
|
|
345
|
+
|
|
346
|
+
if min_args is not None:
|
|
347
|
+
metadata["cli_append_min_args"] = min_args
|
|
348
|
+
|
|
349
|
+
if max_args is not None:
|
|
350
|
+
metadata["cli_append_max_args"] = max_args
|
|
351
|
+
# Move 'help' to metadata if present (dataclass field() doesn't accept it)
|
|
352
|
+
if "help" in field_kwargs:
|
|
353
|
+
metadata["cli_help"] = field_kwargs.pop("help")
|
|
354
|
+
|
|
355
|
+
field_kwargs["metadata"] = metadata
|
|
356
|
+
return field(**field_kwargs)
|
|
357
|
+
|
|
358
|
+
|
|
222
359
|
def combine_annotations(*annotations, **field_kwargs) -> Any:
|
|
223
360
|
"""
|
|
224
361
|
Combine multiple CLI annotations into a single field.
|
|
@@ -316,6 +453,22 @@ def is_cli_file_loadable(field_info: Dict[str, Any]) -> bool:
|
|
|
316
453
|
return False
|
|
317
454
|
|
|
318
455
|
|
|
456
|
+
def is_cli_append(field_info: Dict[str, Any]) -> bool:
|
|
457
|
+
"""
|
|
458
|
+
Check if a field uses append action for repeated options.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
True if field uses append action
|
|
465
|
+
"""
|
|
466
|
+
field_obj = field_info.get("field_obj")
|
|
467
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
468
|
+
return field_obj.metadata.get("cli_append", False)
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
|
|
319
472
|
def get_cli_short(field_info: Dict[str, Any]) -> Optional[str]:
|
|
320
473
|
"""
|
|
321
474
|
Get short option character for a CLI argument.
|
|
@@ -348,6 +501,38 @@ def get_cli_choices(field_info: Dict[str, Any]) -> Optional[List[Any]]:
|
|
|
348
501
|
return None
|
|
349
502
|
|
|
350
503
|
|
|
504
|
+
def get_cli_append_nargs(field_info: Dict[str, Any]) -> Optional[Any]:
|
|
505
|
+
"""
|
|
506
|
+
Get nargs value for an append CLI argument.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
nargs value if specified, otherwise None (meaning exactly one per occurrence)
|
|
513
|
+
"""
|
|
514
|
+
field_obj = field_info.get("field_obj")
|
|
515
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
516
|
+
return field_obj.metadata.get("cli_append_nargs")
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def get_cli_append_metavar(field_info: Dict[str, Any]) -> Optional[str]:
|
|
521
|
+
"""
|
|
522
|
+
Get metavar for an append CLI argument.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Metavar string if specified, otherwise None
|
|
529
|
+
"""
|
|
530
|
+
field_obj = field_info.get("field_obj")
|
|
531
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
532
|
+
return field_obj.metadata.get("cli_append_metavar")
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
|
|
351
536
|
def get_cli_help(field_info: Dict[str, Any]) -> str:
|
|
352
537
|
"""
|
|
353
538
|
Get custom help text for a CLI argument.
|
|
@@ -521,3 +706,35 @@ def get_cli_positional_metavar(field_info: Dict[str, Any]) -> Optional[str]:
|
|
|
521
706
|
if field_obj and hasattr(field_obj, "metadata"):
|
|
522
707
|
return field_obj.metadata.get("cli_positional_metavar")
|
|
523
708
|
return None
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def get_cli_append_min_args(field_info: Dict[str, Any]) -> Optional[int]:
|
|
712
|
+
"""
|
|
713
|
+
Get minimum arguments for an append CLI argument.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Minimum argument count if specified, otherwise None
|
|
720
|
+
"""
|
|
721
|
+
field_obj = field_info.get("field_obj")
|
|
722
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
723
|
+
return field_obj.metadata.get("cli_append_min_args")
|
|
724
|
+
return None
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def get_cli_append_max_args(field_info: Dict[str, Any]) -> Optional[int]:
|
|
728
|
+
"""
|
|
729
|
+
Get maximum arguments for an append CLI argument.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Maximum argument count if specified, otherwise None
|
|
736
|
+
"""
|
|
737
|
+
field_obj = field_info.get("field_obj")
|
|
738
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
739
|
+
return field_obj.metadata.get("cli_append_max_args")
|
|
740
|
+
return None
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Custom argparse action for range-validated append arguments."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Any, Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RangeAppendAction(argparse._AppendAction): # type: ignore[misc]
|
|
8
|
+
"""
|
|
9
|
+
Append action for fields with min/max argument validation.
|
|
10
|
+
|
|
11
|
+
Works with RangeAppendHelpFormatter to display metavar cleanly without repetition.
|
|
12
|
+
|
|
13
|
+
Standard: -f X Y [X Y ...]
|
|
14
|
+
With this: -f X Y
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
option_strings: Sequence[str],
|
|
20
|
+
dest: str,
|
|
21
|
+
nargs: Optional[Union[int, str]] = None,
|
|
22
|
+
const: Any = None,
|
|
23
|
+
default: Any = None,
|
|
24
|
+
type: Any = None,
|
|
25
|
+
choices: Optional[Sequence[Any]] = None,
|
|
26
|
+
required: bool = False,
|
|
27
|
+
help: Optional[str] = None,
|
|
28
|
+
metavar: Optional[str] = None,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize with custom metavar storage."""
|
|
32
|
+
# Store metavar for custom formatter to use
|
|
33
|
+
self._custom_metavar = metavar
|
|
34
|
+
|
|
35
|
+
# Call parent without metavar to avoid argparse's automatic handling
|
|
36
|
+
super().__init__(
|
|
37
|
+
option_strings=option_strings,
|
|
38
|
+
dest=dest,
|
|
39
|
+
nargs=nargs,
|
|
40
|
+
const=const,
|
|
41
|
+
default=default,
|
|
42
|
+
type=type,
|
|
43
|
+
choices=choices,
|
|
44
|
+
required=required,
|
|
45
|
+
help=help,
|
|
46
|
+
metavar=None, # Don't pass to parent
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|