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.
@@ -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