dataclass-args 1.0.0__py3-none-any.whl
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/__init__.py +102 -0
- dataclass_args/annotations.py +522 -0
- dataclass_args/builder.py +672 -0
- dataclass_args/exceptions.py +21 -0
- dataclass_args/file_loading.py +113 -0
- dataclass_args/utils.py +148 -0
- dataclass_args-1.0.0.dist-info/METADATA +931 -0
- dataclass_args-1.0.0.dist-info/RECORD +11 -0
- dataclass_args-1.0.0.dist-info/WHEEL +5 -0
- dataclass_args-1.0.0.dist-info/licenses/LICENSE +21 -0
- dataclass_args-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataclass CLI - Zero-boilerplate CLI generation for Python dataclasses.
|
|
3
|
+
|
|
4
|
+
This package provides automatic CLI interface generation from Python dataclasses
|
|
5
|
+
with advanced features including:
|
|
6
|
+
|
|
7
|
+
- Type-safe argument parsing for all standard Python types
|
|
8
|
+
- Short-form options for concise command lines
|
|
9
|
+
- Restricted value choices with validation
|
|
10
|
+
- File-loadable string parameters using @filename syntax
|
|
11
|
+
- Configuration file merging with CLI overrides
|
|
12
|
+
- Hierarchical property overrides for dictionary fields
|
|
13
|
+
- Comprehensive validation and error handling
|
|
14
|
+
- Automatic help text generation
|
|
15
|
+
|
|
16
|
+
Basic Usage:
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from dataclass_args import build_config
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Config:
|
|
22
|
+
name: str
|
|
23
|
+
count: int = 10
|
|
24
|
+
|
|
25
|
+
config = build_config(Config) # Automatically parses sys.argv
|
|
26
|
+
|
|
27
|
+
Advanced Usage:
|
|
28
|
+
from dataclass_args import cli_help, cli_short, cli_choices, cli_file_loadable
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Config:
|
|
32
|
+
name: str = cli_short('n', cli_help("Application name"))
|
|
33
|
+
environment: str = cli_choices(['dev', 'staging', 'prod'])
|
|
34
|
+
region: str = combine_annotations(
|
|
35
|
+
cli_short('r'),
|
|
36
|
+
cli_choices(['us-east', 'us-west', 'eu-west']),
|
|
37
|
+
default='us-east'
|
|
38
|
+
)
|
|
39
|
+
message: str = cli_file_loadable() # Supports @filename loading
|
|
40
|
+
|
|
41
|
+
# Usage: -n MyApp --environment prod -r us-west --message @file.txt
|
|
42
|
+
config = build_config(Config)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from .annotations import (
|
|
46
|
+
cli_choices,
|
|
47
|
+
cli_exclude,
|
|
48
|
+
cli_file_loadable,
|
|
49
|
+
cli_help,
|
|
50
|
+
cli_include,
|
|
51
|
+
cli_positional,
|
|
52
|
+
cli_short,
|
|
53
|
+
combine_annotations,
|
|
54
|
+
get_cli_choices,
|
|
55
|
+
get_cli_positional_metavar,
|
|
56
|
+
get_cli_positional_nargs,
|
|
57
|
+
get_cli_short,
|
|
58
|
+
is_cli_excluded,
|
|
59
|
+
is_cli_file_loadable,
|
|
60
|
+
is_cli_included,
|
|
61
|
+
is_cli_positional,
|
|
62
|
+
)
|
|
63
|
+
from .builder import GenericConfigBuilder, build_config, build_config_from_cli
|
|
64
|
+
from .exceptions import ConfigBuilderError, ConfigurationError, FileLoadingError
|
|
65
|
+
from .file_loading import is_file_loadable_value, load_file_content
|
|
66
|
+
from .utils import exclude_internal_fields, load_structured_file
|
|
67
|
+
|
|
68
|
+
__version__ = "1.0.0"
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
# Main API
|
|
72
|
+
"build_config",
|
|
73
|
+
"build_config_from_cli",
|
|
74
|
+
"GenericConfigBuilder",
|
|
75
|
+
# Annotations
|
|
76
|
+
"cli_help",
|
|
77
|
+
"cli_short",
|
|
78
|
+
"cli_choices",
|
|
79
|
+
"cli_exclude",
|
|
80
|
+
"cli_include",
|
|
81
|
+
"cli_file_loadable",
|
|
82
|
+
"cli_positional",
|
|
83
|
+
"combine_annotations",
|
|
84
|
+
"get_cli_short",
|
|
85
|
+
"get_cli_choices",
|
|
86
|
+
"get_cli_positional_nargs",
|
|
87
|
+
"get_cli_positional_metavar",
|
|
88
|
+
"is_cli_file_loadable",
|
|
89
|
+
"is_cli_excluded",
|
|
90
|
+
"is_cli_included",
|
|
91
|
+
"is_cli_positional",
|
|
92
|
+
# File loading
|
|
93
|
+
"load_file_content",
|
|
94
|
+
"is_file_loadable_value",
|
|
95
|
+
# Utilities
|
|
96
|
+
"exclude_internal_fields",
|
|
97
|
+
"load_structured_file",
|
|
98
|
+
# Exceptions
|
|
99
|
+
"ConfigBuilderError",
|
|
100
|
+
"ConfigurationError",
|
|
101
|
+
"FileLoadingError",
|
|
102
|
+
]
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Annotations for controlling CLI field exposure.
|
|
3
|
+
|
|
4
|
+
Provides decorators and metadata for marking dataclass fields that should
|
|
5
|
+
be excluded from CLI argument generation or have special behaviors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import field
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cli_exclude(**kwargs) -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Mark a dataclass field to be excluded from CLI arguments.
|
|
15
|
+
|
|
16
|
+
This is a convenience function that adds metadata to a dataclass field
|
|
17
|
+
to indicate it should not be exposed as a CLI argument.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
**kwargs: Additional field parameters (default, default_factory, etc.)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Field object with CLI exclusion metadata
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
@dataclass
|
|
27
|
+
class Config:
|
|
28
|
+
public_field: str # Will be CLI argument
|
|
29
|
+
private_field: str = cli_exclude() # Won't be CLI argument
|
|
30
|
+
secret: str = cli_exclude(default="hidden") # Won't be CLI argument
|
|
31
|
+
"""
|
|
32
|
+
field_kwargs = kwargs.copy()
|
|
33
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
34
|
+
metadata["cli_exclude"] = True
|
|
35
|
+
field_kwargs["metadata"] = metadata
|
|
36
|
+
return field(**field_kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cli_include(**kwargs) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Explicitly mark a dataclass field to be included in CLI arguments.
|
|
42
|
+
|
|
43
|
+
This is useful when using include-only mode or for documentation purposes.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
**kwargs: Additional field parameters (default, default_factory, etc.)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Field object with CLI inclusion metadata
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
@dataclass
|
|
53
|
+
class Config:
|
|
54
|
+
included_field: str = cli_include()
|
|
55
|
+
other_field: str = "default" # Included by default anyway
|
|
56
|
+
"""
|
|
57
|
+
field_kwargs = kwargs.copy()
|
|
58
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
59
|
+
metadata["cli_include"] = True
|
|
60
|
+
field_kwargs["metadata"] = metadata
|
|
61
|
+
return field(**field_kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cli_help(help_text: str, **kwargs) -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Add custom help text for a CLI argument.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
help_text: Custom help text for the CLI argument
|
|
70
|
+
**kwargs: Additional field parameters
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Field object with help text metadata
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
@dataclass
|
|
77
|
+
class Config:
|
|
78
|
+
host: str = cli_help("Database host address")
|
|
79
|
+
port: int = cli_help("Database port number", default=5432)
|
|
80
|
+
"""
|
|
81
|
+
field_kwargs = kwargs.copy()
|
|
82
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
83
|
+
metadata["cli_help"] = help_text
|
|
84
|
+
field_kwargs["metadata"] = metadata
|
|
85
|
+
return field(**field_kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cli_short(short: str, **kwargs) -> Any:
|
|
89
|
+
"""
|
|
90
|
+
Add short-form option for a CLI argument.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
short: Single character for short option (e.g., 'n' for -n)
|
|
94
|
+
**kwargs: Additional field parameters
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Field object with short option metadata
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If short is not a single character
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
@dataclass
|
|
104
|
+
class Config:
|
|
105
|
+
name: str = cli_short('n')
|
|
106
|
+
host: str = cli_short('H', default="localhost")
|
|
107
|
+
port: int = cli_short('p', default=8080)
|
|
108
|
+
|
|
109
|
+
# Usage: -n MyApp -H 0.0.0.0 -p 9000
|
|
110
|
+
# or: --name MyApp --host 0.0.0.0 --port 9000
|
|
111
|
+
# mixed: -n MyApp --host 0.0.0.0 -p 9000
|
|
112
|
+
"""
|
|
113
|
+
if not isinstance(short, str) or len(short) != 1:
|
|
114
|
+
raise ValueError(f"Short option must be a single character, got: {repr(short)}")
|
|
115
|
+
|
|
116
|
+
field_kwargs = kwargs.copy()
|
|
117
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
118
|
+
metadata["cli_short"] = short
|
|
119
|
+
field_kwargs["metadata"] = metadata
|
|
120
|
+
return field(**field_kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cli_choices(choices: List[Any], **kwargs) -> Any:
|
|
124
|
+
"""
|
|
125
|
+
Restrict field to a specific set of valid choices.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
choices: List of valid values for the field
|
|
129
|
+
**kwargs: Additional field parameters
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Field object with choices metadata
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If choices is empty
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
@dataclass
|
|
139
|
+
class Config:
|
|
140
|
+
# Simple choices
|
|
141
|
+
environment: str = cli_choices(['dev', 'staging', 'prod'])
|
|
142
|
+
size: str = cli_choices(['small', 'medium', 'large'], default='medium')
|
|
143
|
+
|
|
144
|
+
# Combined with other annotations
|
|
145
|
+
region: str = combine_annotations(
|
|
146
|
+
cli_short('r'),
|
|
147
|
+
cli_choices(['us-east-1', 'us-west-2', 'eu-west-1']),
|
|
148
|
+
cli_help("AWS region"),
|
|
149
|
+
default='us-east-1'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Usage: --environment prod --size large --region us-west-2
|
|
153
|
+
# Invalid: --environment invalid # Error with valid choices shown
|
|
154
|
+
"""
|
|
155
|
+
if not choices:
|
|
156
|
+
raise ValueError("cli_choices requires at least one choice")
|
|
157
|
+
|
|
158
|
+
field_kwargs = kwargs.copy()
|
|
159
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
160
|
+
metadata["cli_choices"] = list(choices) # Convert to list for consistency
|
|
161
|
+
field_kwargs["metadata"] = metadata
|
|
162
|
+
return field(**field_kwargs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cli_file_loadable(**kwargs) -> Any:
|
|
166
|
+
"""
|
|
167
|
+
Mark a string field as file-loadable via '@' prefix.
|
|
168
|
+
|
|
169
|
+
When a CLI argument value starts with '@', the remaining part is treated as a file path.
|
|
170
|
+
The file is read as UTF-8 encoded text and used as the field value.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
**kwargs: Additional field parameters (default, default_factory, etc.)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Field object with file-loadable metadata
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
@dataclass
|
|
180
|
+
class Config:
|
|
181
|
+
message: str = cli_file_loadable()
|
|
182
|
+
system_prompt: str = cli_file_loadable(default="You are a helpful assistant.")
|
|
183
|
+
|
|
184
|
+
# For combining with help text, use field() directly:
|
|
185
|
+
enhanced: str = field(
|
|
186
|
+
default="",
|
|
187
|
+
metadata={'cli_help': "Message content", 'cli_file_loadable': True}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Usage:
|
|
191
|
+
# --message "Hello world" # Uses literal value
|
|
192
|
+
# --message "@/path/to/file.txt" # Reads file content
|
|
193
|
+
"""
|
|
194
|
+
field_kwargs = kwargs.copy()
|
|
195
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
196
|
+
metadata["cli_file_loadable"] = True
|
|
197
|
+
field_kwargs["metadata"] = metadata
|
|
198
|
+
return field(**field_kwargs)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def combine_annotations(*annotations, **field_kwargs) -> Any:
|
|
202
|
+
"""
|
|
203
|
+
Combine multiple CLI annotations into a single field.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
*annotations: List of annotation functions (cli_help, cli_file_loadable, etc.)
|
|
207
|
+
**field_kwargs: Additional field parameters
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Field object with combined metadata
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
@dataclass
|
|
214
|
+
class Config:
|
|
215
|
+
message: str = combine_annotations(
|
|
216
|
+
cli_help("Message content"),
|
|
217
|
+
cli_file_loadable(),
|
|
218
|
+
default="Default message"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# With short option
|
|
222
|
+
name: str = combine_annotations(
|
|
223
|
+
cli_short('n'),
|
|
224
|
+
cli_help("Application name")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# With choices
|
|
228
|
+
region: str = combine_annotations(
|
|
229
|
+
cli_short('r'),
|
|
230
|
+
cli_choices(['us-east', 'us-west']),
|
|
231
|
+
cli_help("Region"),
|
|
232
|
+
default='us-east'
|
|
233
|
+
)
|
|
234
|
+
"""
|
|
235
|
+
combined_metadata = field_kwargs.pop("metadata", {})
|
|
236
|
+
|
|
237
|
+
# Extract metadata from each annotation
|
|
238
|
+
for annotation in annotations:
|
|
239
|
+
if hasattr(annotation, "metadata") and annotation.metadata:
|
|
240
|
+
combined_metadata.update(annotation.metadata)
|
|
241
|
+
|
|
242
|
+
field_kwargs["metadata"] = combined_metadata
|
|
243
|
+
return field(**field_kwargs)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_cli_excluded(field_info: Dict[str, Any]) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Check if a field should be excluded from CLI arguments.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if field should be excluded from CLI
|
|
255
|
+
"""
|
|
256
|
+
# Check for explicit CLI exclusion metadata
|
|
257
|
+
field_obj = field_info.get("field_obj")
|
|
258
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
259
|
+
return field_obj.metadata.get("cli_exclude", False)
|
|
260
|
+
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def is_cli_included(field_info: Dict[str, Any]) -> bool:
|
|
265
|
+
"""
|
|
266
|
+
Check if a field is explicitly marked for CLI inclusion.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if field is explicitly marked for CLI inclusion
|
|
273
|
+
"""
|
|
274
|
+
field_obj = field_info.get("field_obj")
|
|
275
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
276
|
+
return field_obj.metadata.get("cli_include", False)
|
|
277
|
+
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def is_cli_file_loadable(field_info: Dict[str, Any]) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Check if a field is marked as file-loadable via '@' prefix.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
True if field supports file loading via '@' prefix
|
|
290
|
+
"""
|
|
291
|
+
field_obj = field_info.get("field_obj")
|
|
292
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
293
|
+
return field_obj.metadata.get("cli_file_loadable", False)
|
|
294
|
+
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_cli_short(field_info: Dict[str, Any]) -> Optional[str]:
|
|
299
|
+
"""
|
|
300
|
+
Get short option character for a CLI argument.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Short option character if available, otherwise None
|
|
307
|
+
"""
|
|
308
|
+
field_obj = field_info.get("field_obj")
|
|
309
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
310
|
+
return field_obj.metadata.get("cli_short")
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_cli_choices(field_info: Dict[str, Any]) -> Optional[List[Any]]:
|
|
315
|
+
"""
|
|
316
|
+
Get restricted choices for a CLI argument.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of valid choices if available, otherwise None
|
|
323
|
+
"""
|
|
324
|
+
field_obj = field_info.get("field_obj")
|
|
325
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
326
|
+
return field_obj.metadata.get("cli_choices")
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_cli_help(field_info: Dict[str, Any]) -> str:
|
|
331
|
+
"""
|
|
332
|
+
Get custom help text for a CLI argument.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Custom help text if available, otherwise empty string
|
|
339
|
+
"""
|
|
340
|
+
field_obj = field_info.get("field_obj")
|
|
341
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
342
|
+
help_text = field_obj.metadata.get("cli_help", "")
|
|
343
|
+
|
|
344
|
+
# Add file-loadable hint to help text if applicable
|
|
345
|
+
if field_obj.metadata.get("cli_file_loadable", False):
|
|
346
|
+
if help_text:
|
|
347
|
+
help_text += " (supports @file.txt to load from file)"
|
|
348
|
+
else:
|
|
349
|
+
help_text = "supports @file.txt to load from file"
|
|
350
|
+
|
|
351
|
+
return help_text
|
|
352
|
+
|
|
353
|
+
return ""
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def annotation_filter(field_name: str, field_info: Dict[str, Any]) -> bool:
|
|
357
|
+
"""
|
|
358
|
+
Filter function that respects field annotations.
|
|
359
|
+
|
|
360
|
+
This filter function excludes fields marked with cli_exclude() and
|
|
361
|
+
can be used with GenericConfigBuilder.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
field_name: Name of the field
|
|
365
|
+
field_info: Field information dictionary
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
True if field should be included in CLI
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
builder = GenericConfigBuilder(MyConfig, field_filter=annotation_filter)
|
|
372
|
+
"""
|
|
373
|
+
return not is_cli_excluded(field_info)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def cli_positional(
|
|
377
|
+
nargs: Optional[Any] = None, metavar: Optional[str] = None, **kwargs
|
|
378
|
+
) -> Any:
|
|
379
|
+
"""
|
|
380
|
+
Mark a dataclass field as a positional CLI argument.
|
|
381
|
+
|
|
382
|
+
Positional arguments don't use -- prefix and are matched by position.
|
|
383
|
+
|
|
384
|
+
IMPORTANT CONSTRAINTS:
|
|
385
|
+
- At most ONE positional field can use nargs='*' or '+'
|
|
386
|
+
- If present, positional list must be the LAST positional argument
|
|
387
|
+
- For multiple lists, use optional arguments with flags instead
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
nargs: Number of arguments
|
|
391
|
+
None = exactly one (required)
|
|
392
|
+
'?' = zero or one (optional)
|
|
393
|
+
'*' = zero or more (list, optional)
|
|
394
|
+
'+' = one or more (list, required)
|
|
395
|
+
int = exact count (list)
|
|
396
|
+
metavar: Name for display in help text (default: FIELD_NAME)
|
|
397
|
+
**kwargs: Additional field parameters (default, default_factory, etc.)
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Field object with positional metadata
|
|
401
|
+
|
|
402
|
+
Examples:
|
|
403
|
+
@dataclass
|
|
404
|
+
class CopyArgs:
|
|
405
|
+
# Required positional
|
|
406
|
+
source: str = cli_positional(help="Source file")
|
|
407
|
+
dest: str = cli_positional(help="Destination file")
|
|
408
|
+
|
|
409
|
+
# Optional flag
|
|
410
|
+
recursive: bool = cli_short('r', default=False)
|
|
411
|
+
|
|
412
|
+
# Usage: prog source.txt dest.txt -r
|
|
413
|
+
|
|
414
|
+
@dataclass
|
|
415
|
+
class GitCommit:
|
|
416
|
+
# Required command
|
|
417
|
+
command: str = cli_positional(help="Git command")
|
|
418
|
+
|
|
419
|
+
# Variable files (must be last!)
|
|
420
|
+
files: List[str] = cli_positional(nargs='+', help="Files to commit")
|
|
421
|
+
|
|
422
|
+
# Optional message
|
|
423
|
+
message: str = cli_short('m', default="")
|
|
424
|
+
|
|
425
|
+
# Usage: prog commit file1.py file2.py -m "Message"
|
|
426
|
+
|
|
427
|
+
@dataclass
|
|
428
|
+
class PlotPoint:
|
|
429
|
+
# Exact count
|
|
430
|
+
coordinates: List[float] = cli_positional(
|
|
431
|
+
nargs=2,
|
|
432
|
+
metavar='X Y',
|
|
433
|
+
help="X and Y coordinates"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Optional label
|
|
437
|
+
label: str = cli_positional(nargs='?', default='', help="Point label")
|
|
438
|
+
|
|
439
|
+
# Usage: prog 1.5 2.5 "Point A"
|
|
440
|
+
# Usage: prog 1.5 2.5 # Uses default label
|
|
441
|
+
|
|
442
|
+
@dataclass
|
|
443
|
+
class Convert:
|
|
444
|
+
# With combine_annotations
|
|
445
|
+
input: str = combine_annotations(
|
|
446
|
+
cli_positional(),
|
|
447
|
+
cli_help("Input file to convert")
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
output: str = combine_annotations(
|
|
451
|
+
cli_positional(nargs='?'),
|
|
452
|
+
cli_help("Output file (default: stdout)"),
|
|
453
|
+
default='stdout'
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
See Also:
|
|
457
|
+
POSITIONAL_LIST_CONFLICTS.md for detailed discussion of constraints
|
|
458
|
+
"""
|
|
459
|
+
field_kwargs = kwargs.copy()
|
|
460
|
+
metadata = field_kwargs.pop("metadata", {})
|
|
461
|
+
metadata["cli_positional"] = True
|
|
462
|
+
|
|
463
|
+
if nargs is not None:
|
|
464
|
+
metadata["cli_positional_nargs"] = nargs
|
|
465
|
+
|
|
466
|
+
if metavar is not None:
|
|
467
|
+
metadata["cli_positional_metavar"] = metavar
|
|
468
|
+
|
|
469
|
+
# Move 'help' to metadata (dataclass field() doesn't accept it)
|
|
470
|
+
if "help" in field_kwargs:
|
|
471
|
+
metadata["cli_help"] = field_kwargs.pop("help")
|
|
472
|
+
|
|
473
|
+
field_kwargs["metadata"] = metadata
|
|
474
|
+
return field(**field_kwargs)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def is_cli_positional(field_info: Dict[str, Any]) -> bool:
|
|
478
|
+
"""
|
|
479
|
+
Check if a field is marked as a positional CLI argument.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
True if field is a positional argument
|
|
486
|
+
"""
|
|
487
|
+
field_obj = field_info.get("field_obj")
|
|
488
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
489
|
+
return field_obj.metadata.get("cli_positional", False)
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def get_cli_positional_nargs(field_info: Dict[str, Any]) -> Optional[Any]:
|
|
494
|
+
"""
|
|
495
|
+
Get nargs value for a positional CLI argument.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
nargs value if specified, otherwise None (meaning exactly one)
|
|
502
|
+
"""
|
|
503
|
+
field_obj = field_info.get("field_obj")
|
|
504
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
505
|
+
return field_obj.metadata.get("cli_positional_nargs")
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def get_cli_positional_metavar(field_info: Dict[str, Any]) -> Optional[str]:
|
|
510
|
+
"""
|
|
511
|
+
Get metavar for a positional CLI argument.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
field_info: Field information dictionary from GenericConfigBuilder
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Metavar string if specified, otherwise None
|
|
518
|
+
"""
|
|
519
|
+
field_obj = field_info.get("field_obj")
|
|
520
|
+
if field_obj and hasattr(field_obj, "metadata"):
|
|
521
|
+
return field_obj.metadata.get("cli_positional_metavar")
|
|
522
|
+
return None
|