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,672 @@
1
+ """
2
+ Generic configuration builder for dataclass types from CLI arguments.
3
+
4
+ Provides type-aware parsing of command-line arguments and merging
5
+ with optional base configuration files for any dataclass.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ from dataclasses import MISSING, fields, is_dataclass
12
+ from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
13
+
14
+ # Import typing utilities with Python 3.8+ compatibility
15
+ try:
16
+ from typing import ( # type: ignore[attr-defined,no-redef]
17
+ get_args,
18
+ get_origin,
19
+ get_type_hints,
20
+ )
21
+ except ImportError:
22
+ from typing_extensions import get_args, get_origin, get_type_hints # type: ignore[assignment,no-redef]
23
+
24
+ from .annotations import (
25
+ get_cli_choices,
26
+ get_cli_help,
27
+ get_cli_positional_metavar,
28
+ get_cli_positional_nargs,
29
+ get_cli_short,
30
+ is_cli_excluded,
31
+ is_cli_positional,
32
+ )
33
+ from .exceptions import ConfigBuilderError, ConfigurationError
34
+ from .file_loading import process_file_loadable_value
35
+ from .utils import load_structured_file
36
+
37
+
38
+ class GenericConfigBuilder:
39
+ """
40
+ Builds dataclass instances from CLI arguments and optional base config file.
41
+
42
+ Supports any dataclass type with:
43
+ - Optional base config file loading
44
+ - Type-aware CLI argument parsing
45
+ - List parameter accumulation
46
+ - Object parameter file loading with property overrides
47
+ - File-loadable string parameters via '@' prefix
48
+ - Hierarchical merging of configuration sources
49
+ - Field filtering via annotations or custom filters
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ config_class: Type,
55
+ exclude_fields: Optional[Set[str]] = None,
56
+ include_fields: Optional[Set[str]] = None,
57
+ field_filter: Optional[Callable[[str, Dict[str, Any]], bool]] = None,
58
+ use_annotations: bool = True,
59
+ ):
60
+ """
61
+ Initialize builder for a specific dataclass type.
62
+
63
+ Args:
64
+ config_class: Dataclass type to build configurations for
65
+ exclude_fields: Set of field names to exclude from CLI
66
+ include_fields: Set of field names to include in CLI (exclusive with exclude_fields)
67
+ field_filter: Custom function to determine field inclusion
68
+ use_annotations: Whether to respect cli_exclude() annotations (default: True)
69
+
70
+ Raises:
71
+ ConfigBuilderError: If config_class is not a dataclass or conflicting filters provided
72
+ """
73
+ if not is_dataclass(config_class):
74
+ raise ConfigBuilderError(
75
+ f"config_class must be a dataclass, got {config_class}"
76
+ )
77
+
78
+ if exclude_fields and include_fields:
79
+ raise ConfigBuilderError(
80
+ "Cannot specify both exclude_fields and include_fields"
81
+ )
82
+
83
+ self.config_class = config_class
84
+ self.exclude_fields = exclude_fields or set()
85
+ self.include_fields = include_fields
86
+ self.field_filter = field_filter
87
+ self.use_annotations = use_annotations
88
+ self._config_fields = self._analyze_config_fields()
89
+
90
+ def _should_include_field(
91
+ self, field_name: str, field_info: Dict[str, Any]
92
+ ) -> bool:
93
+ """Determine if a field should be included in CLI arguments."""
94
+
95
+ # Apply annotation filter first if enabled
96
+ if self.use_annotations and is_cli_excluded(field_info):
97
+ return False
98
+
99
+ # Apply custom filter if provided
100
+ if self.field_filter:
101
+ return self.field_filter(field_name, field_info)
102
+
103
+ # Apply include_fields filter (exclusive)
104
+ if self.include_fields is not None:
105
+ return field_name in self.include_fields
106
+
107
+ # Apply exclude_fields filter
108
+ if field_name in self.exclude_fields:
109
+ return False
110
+
111
+ # Default: include all fields
112
+ return True
113
+
114
+ def _analyze_config_fields(self) -> Dict[str, Dict[str, Any]]:
115
+ """Analyze dataclass fields for type information."""
116
+ fields_info = {}
117
+ type_hints = get_type_hints(self.config_class)
118
+
119
+ for field_obj in fields(self.config_class):
120
+ field_type = type_hints.get(field_obj.name, field_obj.type)
121
+ origin = get_origin(field_type)
122
+ args = get_args(field_type)
123
+
124
+ # Determine field category
125
+ is_optional = origin is Union and type(None) in args
126
+ if is_optional:
127
+ # Extract the non-None type from Optional[T]
128
+ field_type = next(arg for arg in args if arg is not type(None))
129
+ origin = get_origin(field_type)
130
+ args = get_args(field_type)
131
+
132
+ is_list = origin is list
133
+ is_dict = origin is dict
134
+
135
+ # Extract default value or factory
136
+ has_default = field_obj.default is not MISSING
137
+ has_default_factory = field_obj.default_factory is not MISSING
138
+ default_value = None
139
+ if has_default:
140
+ default_value = field_obj.default
141
+ elif has_default_factory and callable(field_obj.default_factory):
142
+ # Call factory to get default value
143
+ default_value = field_obj.default_factory()
144
+
145
+ field_info = {
146
+ "type": field_type,
147
+ "origin": origin,
148
+ "args": args,
149
+ "is_optional": is_optional,
150
+ "is_list": is_list,
151
+ "is_dict": is_dict,
152
+ "default": default_value,
153
+ "has_default": has_default or has_default_factory,
154
+ "cli_name": self._field_to_cli_name(field_obj.name),
155
+ "override_name": self._field_to_override_name(field_obj.name),
156
+ "field_obj": field_obj, # Include field object for metadata access
157
+ }
158
+
159
+ # Only include field if it passes filtering
160
+ if self._should_include_field(field_obj.name, field_info):
161
+ fields_info[field_obj.name] = field_info
162
+
163
+ # Validate positional arguments
164
+ self._validate_positional_arguments(fields_info)
165
+
166
+ return fields_info
167
+
168
+ def _validate_positional_arguments(
169
+ self, fields_info: Dict[str, Dict[str, Any]]
170
+ ) -> None:
171
+ """
172
+ Validate positional argument constraints.
173
+
174
+ Rules:
175
+ 1. At most ONE positional field can use nargs='*' or '+'
176
+ 2. If present, positional list must be the LAST positional argument
177
+
178
+ Raises:
179
+ ConfigBuilderError: If validation fails
180
+ """
181
+ positional_fields = []
182
+ positional_list_fields = []
183
+
184
+ for field_name, info in fields_info.items():
185
+ if is_cli_positional(info):
186
+ positional_fields.append((field_name, info))
187
+
188
+ nargs = get_cli_positional_nargs(info)
189
+ # Check if this is a "list" positional (greedy)
190
+ if nargs in ("*", "+"):
191
+ positional_list_fields.append((field_name, nargs))
192
+
193
+ # Rule 1: At most one positional list
194
+ if len(positional_list_fields) > 1:
195
+ field_names = [
196
+ f"'{name}' (nargs='{nargs}')" for name, nargs in positional_list_fields
197
+ ]
198
+ raise ConfigBuilderError(
199
+ f"Only one positional list argument allowed, found {len(positional_list_fields)}: "
200
+ f"{', '.join(field_names)}. Use optional lists with flags for additional lists:\n"
201
+ f" Example: field: List[str] = cli_short('f') # Use --field instead"
202
+ )
203
+
204
+ # Rule 2: Positional list must be last
205
+ if positional_list_fields:
206
+ list_field_name, list_nargs = positional_list_fields[0]
207
+ list_field_index = next(
208
+ i
209
+ for i, (name, _) in enumerate(positional_fields)
210
+ if name == list_field_name
211
+ )
212
+
213
+ # Check if there are any positionals after the list
214
+ if list_field_index < len(positional_fields) - 1:
215
+ later_fields = [
216
+ name for name, _ in positional_fields[list_field_index + 1 :]
217
+ ]
218
+ raise ConfigBuilderError(
219
+ f"Positional list argument '{list_field_name}' (nargs='{list_nargs}') must be last.\n"
220
+ f"Found positional argument(s) after it: {', '.join([repr(f) for f in later_fields])}.\n"
221
+ f"Consider making them optional arguments with flags:\n"
222
+ f" Example: {later_fields[0]}: str = cli_short('{later_fields[0][0]}', default='value')"
223
+ )
224
+
225
+ def _field_to_cli_name(self, field_name: str) -> str:
226
+ """Convert field name to CLI argument name."""
227
+ return "--" + field_name.replace("_", "-")
228
+
229
+ def _field_to_override_name(self, field_name: str) -> str:
230
+ """Convert field name to override argument name."""
231
+ # Use abbreviation for override arguments
232
+ words = field_name.split("_")
233
+ if len(words) == 1:
234
+ return "--" + field_name[0]
235
+ else:
236
+ return "--" + "".join(word[0] for word in words if word)
237
+
238
+ def add_arguments(
239
+ self,
240
+ parser: argparse.ArgumentParser,
241
+ base_config_name: str = "config",
242
+ base_config_help: str = "Base configuration file (JSON, YAML, or TOML)",
243
+ ) -> None:
244
+ """
245
+ Add all dataclass arguments to parser.
246
+
247
+ Args:
248
+ parser: ArgumentParser to add arguments to
249
+ base_config_name: Name for base config file argument
250
+ base_config_help: Help text for base config file argument
251
+ """
252
+
253
+ # Base config file argument
254
+ parser.add_argument(f"--{base_config_name}", type=str, help=base_config_help)
255
+
256
+ # IMPORTANT: Add positional arguments first (argparse requirement)
257
+ for field_name, info in self._config_fields.items():
258
+ if is_cli_positional(info):
259
+ self._add_positional_argument(parser, field_name, info)
260
+
261
+ # Then add optional arguments
262
+ for field_name, info in self._config_fields.items():
263
+ if not is_cli_positional(info):
264
+ self._add_field_argument(parser, field_name, info)
265
+
266
+ def _add_positional_argument(
267
+ self, parser: argparse.ArgumentParser, field_name: str, info: Dict[str, Any]
268
+ ) -> None:
269
+ """Add positional argument to parser."""
270
+ # Positional arguments use the field name directly (no -- prefix)
271
+ arg_name = field_name
272
+
273
+ # Get nargs from metadata
274
+ nargs = get_cli_positional_nargs(info)
275
+
276
+ # Get metavar from metadata or default to uppercase field name
277
+ metavar = get_cli_positional_metavar(info)
278
+ if not metavar:
279
+ metavar = field_name.upper()
280
+
281
+ # Get help text
282
+ help_text = get_cli_help(info) or f"{field_name}"
283
+
284
+ # Get choices if specified
285
+ choices = get_cli_choices(info)
286
+
287
+ # Get type converter - for lists, need to convert element type
288
+ if info["is_list"] and info["args"]:
289
+ # Get the element type from List[T]
290
+ element_type = info["args"][0]
291
+ arg_type = self._get_argument_type(element_type)
292
+ else:
293
+ arg_type = self._get_argument_type(info["type"])
294
+
295
+ # Build kwargs
296
+ # Build kwargs with explicit type for mypy
297
+ kwargs: Dict[str, Any] = {
298
+ "help": help_text,
299
+ "metavar": metavar,
300
+ }
301
+
302
+ if nargs is not None:
303
+ kwargs["nargs"] = nargs
304
+
305
+ if choices:
306
+ kwargs["choices"] = choices
307
+
308
+ # Type handling: for list-like nargs, type applies to each element
309
+ kwargs["type"] = arg_type
310
+
311
+ # Add default if specified and nargs allows it
312
+ if nargs in ("?", "*"):
313
+ default = info.get("default")
314
+ if default is not None:
315
+ kwargs["default"] = default
316
+
317
+ parser.add_argument(arg_name, **kwargs)
318
+
319
+ def _add_field_argument(
320
+ self, parser: argparse.ArgumentParser, field_name: str, info: Dict[str, Any]
321
+ ) -> None:
322
+ """Add CLI argument for a specific config field."""
323
+ # Special handling for boolean fields
324
+ if info["type"] == bool:
325
+ self._add_boolean_argument(parser, field_name, info)
326
+ return
327
+
328
+ cli_name = info["cli_name"]
329
+
330
+ # Get short option from metadata
331
+ short_option = get_cli_short(info)
332
+
333
+ # Build argument names list
334
+ arg_names = []
335
+ if short_option:
336
+ arg_names.append(f"-{short_option}") # Short comes first: -n
337
+ arg_names.append(cli_name) # Then long: --name
338
+
339
+ # Get custom help text from annotations or use default
340
+ custom_help = get_cli_help(info)
341
+ help_text = custom_help if custom_help else f"{field_name}"
342
+
343
+ # Get restricted choices if specified
344
+ choices = get_cli_choices(info)
345
+ if choices:
346
+ # Add choices hint to help text
347
+ choices_str = ", ".join(str(c) for c in choices)
348
+ if help_text:
349
+ help_text += f" (choices: {choices_str})"
350
+ else:
351
+ help_text = f"choices: {choices_str}"
352
+
353
+ if info["is_list"]:
354
+ # List parameters accept multiple values after a single flag
355
+ # Use nargs='+' for required lists (one or more values)
356
+ # Use nargs='*' for optional lists (zero or more values)
357
+ if info["is_optional"]:
358
+ nargs_val = "*" # Zero or more values for Optional[List[T]]
359
+ help_text += " (specify zero or more values)"
360
+ else:
361
+ nargs_val = "+" # One or more values for List[T]
362
+ help_text += " (specify one or more values)"
363
+
364
+ parser.add_argument(
365
+ *arg_names, nargs=nargs_val, choices=choices, help=help_text
366
+ )
367
+ elif info["is_dict"]:
368
+ # Dict parameters are file paths
369
+ dict_help = (
370
+ f"{help_text} configuration file path"
371
+ if help_text
372
+ else "configuration file path"
373
+ )
374
+ parser.add_argument(*arg_names, type=str, help=dict_help)
375
+ # Add override argument for dict fields (no short form for overrides)
376
+ override_help = (
377
+ f"{help_text} property override (format: key.path:value)"
378
+ if help_text
379
+ else "property override (format: key.path:value)"
380
+ )
381
+ parser.add_argument(
382
+ info["override_name"],
383
+ action="append",
384
+ help=override_help,
385
+ )
386
+ else:
387
+ # Simple scalar parameters
388
+ arg_type = self._get_argument_type(info["type"])
389
+ parser.add_argument(
390
+ *arg_names, type=arg_type, choices=choices, help=help_text
391
+ )
392
+
393
+ def _add_boolean_argument(
394
+ self, parser: argparse.ArgumentParser, field_name: str, info: Dict[str, Any]
395
+ ) -> None:
396
+ """Add boolean argument with positive and negative forms."""
397
+ cli_name = info["cli_name"]
398
+ dest_name = field_name.replace("-", "_")
399
+
400
+ # Get short option from metadata
401
+ short_option = get_cli_short(info)
402
+
403
+ # Build argument names for positive form
404
+ positive_args = []
405
+ if short_option:
406
+ positive_args.append(f"-{short_option}")
407
+ positive_args.append(cli_name)
408
+
409
+ # Get custom help text
410
+ custom_help = get_cli_help(info)
411
+ help_text = custom_help if custom_help else field_name
412
+
413
+ # Get default value
414
+ default_value = info.get("default", False)
415
+
416
+ # Set parser default to the field's default value
417
+ parser.set_defaults(**{dest_name: default_value})
418
+
419
+ # Add positive form (--flag or -f)
420
+ parser.add_argument(
421
+ *positive_args,
422
+ action="store_true",
423
+ dest=dest_name,
424
+ help=f"{help_text} (default: {default_value})",
425
+ )
426
+
427
+ # Add negative form (--no-flag)
428
+ negative_name = f"--no-{field_name.replace('_', '-')}"
429
+ parser.add_argument(
430
+ negative_name,
431
+ action="store_false",
432
+ dest=dest_name,
433
+ help=f"Disable {help_text}",
434
+ )
435
+
436
+ def _get_argument_type(self, field_type: Type) -> Callable[[str], Any]:
437
+ """Get appropriate argparse type for field type."""
438
+ # Note: bool is handled separately in _add_boolean_argument
439
+ if field_type in (int, float, str):
440
+ return field_type
441
+ else:
442
+ # For complex types, use string and let validation handle it
443
+ return str
444
+
445
+ def build_config(
446
+ self, args: argparse.Namespace, base_config_name: str = "config"
447
+ ) -> Any:
448
+ """
449
+ Build dataclass instance from parsed CLI arguments.
450
+
451
+ Args:
452
+ args: Parsed CLI arguments
453
+ base_config_name: Name of base config argument
454
+
455
+ Returns:
456
+ Instance of the configured dataclass type
457
+
458
+ Raises:
459
+ ConfigurationError: If configuration is invalid
460
+ """
461
+
462
+ # Start with base config
463
+ base_config = {}
464
+ base_config_value = getattr(args, base_config_name.replace("-", "_"), None)
465
+ if base_config_value:
466
+ try:
467
+ base_config = load_structured_file(base_config_value)
468
+ except Exception as e:
469
+ raise ConfigurationError(
470
+ f"Failed to load base config from {base_config_value}: {e}"
471
+ ) from e
472
+
473
+ # Apply CLI argument overrides (only for included fields)
474
+ config_dict = self._merge_cli_args(base_config, args)
475
+
476
+ # Create and return config
477
+ try:
478
+ return self.config_class(**config_dict)
479
+ except Exception as e:
480
+ raise ConfigurationError(
481
+ f"Failed to create {self.config_class.__name__}: {e}"
482
+ ) from e
483
+
484
+ def _merge_cli_args(
485
+ self, base_config: Dict[str, Any], args: argparse.Namespace
486
+ ) -> Dict[str, Any]:
487
+ """Merge CLI arguments into base config."""
488
+ config = base_config.copy()
489
+
490
+ # Only process fields that were included in CLI
491
+ for field_name, info in self._config_fields.items():
492
+ # Convert CLI arg name back to field name
493
+ arg_name = field_name.replace("-", "_")
494
+ cli_value = getattr(args, arg_name, None)
495
+
496
+ # Get override argument name
497
+ override_arg_name = info["override_name"][2:].replace("-", "_")
498
+ override_value = getattr(args, override_arg_name, None)
499
+
500
+ if cli_value is not None:
501
+ if info["is_list"]:
502
+ # CLI values replace base config values (standard argparse behavior)
503
+ # With nargs='+' or '*', cli_value is already a list
504
+ config[field_name] = cli_value
505
+ elif info["is_dict"]:
506
+ # For dicts, load from file
507
+ try:
508
+ dict_config = load_structured_file(cli_value)
509
+ existing = config.get(field_name, {})
510
+ if isinstance(existing, dict):
511
+ existing.update(dict_config)
512
+ config[field_name] = existing
513
+ else:
514
+ config[field_name] = dict_config
515
+ except Exception as e:
516
+ raise ConfigurationError(
517
+ f"Failed to load dictionary config for field '{field_name}' from {cli_value}: {e}"
518
+ ) from e
519
+ else:
520
+ # Simple override - check for file-loadable fields
521
+ try:
522
+ processed_value = process_file_loadable_value(
523
+ cli_value, field_name, info
524
+ )
525
+ config[field_name] = processed_value
526
+ except (ValueError, Exception) as e:
527
+ raise ConfigurationError(
528
+ f"Failed to process field '{field_name}': {e}"
529
+ ) from e
530
+
531
+ # Apply property overrides for dict fields
532
+ if info["is_dict"] and override_value:
533
+ if field_name not in config:
534
+ config[field_name] = {}
535
+ try:
536
+ self._apply_property_overrides(config[field_name], override_value)
537
+ except Exception as e:
538
+ raise ConfigurationError(
539
+ f"Failed to apply property overrides for field '{field_name}': {e}"
540
+ ) from e
541
+
542
+ return config
543
+
544
+ def _apply_property_overrides(
545
+ self, target_dict: Dict[str, Any], overrides: List[str]
546
+ ) -> None:
547
+ """Apply property path overrides to target dictionary."""
548
+ for override in overrides:
549
+ if ":" not in override:
550
+ raise ValueError(
551
+ f"Invalid override format: {override} (expected key.path:value)"
552
+ )
553
+
554
+ path, value = override.split(":", 1)
555
+ self._set_nested_property(target_dict, path, self._parse_value(value))
556
+
557
+ def _set_nested_property(
558
+ self, target: Dict[str, Any], path: str, value: Any
559
+ ) -> None:
560
+ """Set nested property using dot notation."""
561
+ keys = path.split(".")
562
+ current = target
563
+
564
+ # Navigate to parent of target key
565
+ for key in keys[:-1]:
566
+ if key not in current:
567
+ current[key] = {}
568
+ current = current[key]
569
+
570
+ # Set final value
571
+ current[keys[-1]] = value
572
+
573
+ def _parse_value(self, value_str: str) -> Any:
574
+ """Parse string value to appropriate type."""
575
+ # Try to parse as JSON first (handles numbers, booleans, etc.)
576
+ try:
577
+ return json.loads(value_str)
578
+ except json.JSONDecodeError:
579
+ # Return as string if not valid JSON
580
+ return value_str
581
+
582
+
583
+ # Convenience functions
584
+
585
+
586
+ def build_config_from_cli(
587
+ config_class: Type,
588
+ args: Optional[List[str]] = None,
589
+ base_config_name: str = "config",
590
+ exclude_fields: Optional[Set[str]] = None,
591
+ include_fields: Optional[Set[str]] = None,
592
+ field_filter: Optional[Callable[[str, Dict[str, Any]], bool]] = None,
593
+ use_annotations: bool = True,
594
+ ) -> Any:
595
+ """
596
+ Convenience function to build any dataclass from CLI arguments.
597
+
598
+ Args:
599
+ config_class: Dataclass type to build
600
+ args: Command-line arguments (defaults to sys.argv[1:])
601
+ base_config_name: Name for base config file argument
602
+ exclude_fields: Set of field names to exclude from CLI
603
+ include_fields: Set of field names to include in CLI
604
+ field_filter: Custom function to determine field inclusion
605
+ use_annotations: Whether to respect cli_exclude() annotations (default: True)
606
+
607
+ Returns:
608
+ Instance of config_class built from CLI arguments
609
+
610
+ Example:
611
+ from dataclasses import dataclass
612
+ from dataclass_args import cli_exclude, cli_help, cli_file_loadable
613
+
614
+ @dataclass
615
+ class MyConfig:
616
+ name: str = cli_help("Service name")
617
+ message: str = cli_file_loadable(cli_help("Message text"))
618
+ items: Optional[List[str]] = None
619
+ settings: Optional[dict] = None
620
+ _secret: str = cli_exclude(default="hidden")
621
+
622
+ # Usage:
623
+ config = build_config_from_cli(MyConfig, [
624
+ '--name', 'test',
625
+ '--items', 'a', 'b', 'c',
626
+ '--message', '@/path/to/message.txt'
627
+ ])
628
+ """
629
+ if args is None:
630
+ args = sys.argv[1:]
631
+
632
+ builder = GenericConfigBuilder(
633
+ config_class,
634
+ exclude_fields=exclude_fields,
635
+ include_fields=include_fields,
636
+ field_filter=field_filter,
637
+ use_annotations=use_annotations,
638
+ )
639
+ parser = argparse.ArgumentParser(
640
+ description=f"Build {config_class.__name__} from CLI"
641
+ )
642
+ builder.add_arguments(parser, base_config_name)
643
+
644
+ parsed_args = parser.parse_args(args)
645
+ return builder.build_config(parsed_args, base_config_name)
646
+
647
+
648
+ def build_config(config_class: Type, args: Optional[List[str]] = None) -> Any:
649
+ """
650
+ Simplified convenience function to build any dataclass from CLI arguments.
651
+
652
+ Uses default settings suitable for most use cases.
653
+
654
+ Args:
655
+ config_class: Dataclass type to build
656
+ args: Command-line arguments (defaults to sys.argv[1:])
657
+
658
+ Returns:
659
+ Instance of config_class built from CLI arguments
660
+
661
+ Example:
662
+ from dataclasses import dataclass
663
+ from dataclass_args import build_config
664
+
665
+ @dataclass
666
+ class Config:
667
+ name: str
668
+ count: int = 10
669
+
670
+ config = build_config(Config) # Parses sys.argv automatically
671
+ """
672
+ return build_config_from_cli(config_class, args)
@@ -0,0 +1,21 @@
1
+ """
2
+ Exceptions for dataclass CLI configuration system.
3
+ """
4
+
5
+
6
+ class ConfigBuilderError(Exception):
7
+ """Base exception for configuration builder errors."""
8
+
9
+ pass
10
+
11
+
12
+ class ConfigurationError(ConfigBuilderError):
13
+ """Exception raised for configuration validation errors."""
14
+
15
+ pass
16
+
17
+
18
+ class FileLoadingError(ConfigBuilderError):
19
+ """Exception raised when file loading fails."""
20
+
21
+ pass