dataclass-args 1.2.2__tar.gz → 1.4.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.
Files changed (33) hide show
  1. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/PKG-INFO +413 -2
  2. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/README.md +412 -1
  3. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/__init__.py +36 -1
  4. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/annotations.py +451 -107
  5. dataclass_args-1.4.0/dataclass_args/append_action.py +48 -0
  6. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/builder.py +652 -9
  7. dataclass_args-1.4.0/dataclass_args/formatter.py +45 -0
  8. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args.egg-info/PKG-INFO +413 -2
  9. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args.egg-info/SOURCES.txt +4 -0
  10. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/pyproject.toml +2 -2
  11. dataclass_args-1.4.0/tests/test_cli_append.py +841 -0
  12. dataclass_args-1.4.0/tests/test_cli_nested.py +765 -0
  13. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/LICENSE +0 -0
  14. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/exceptions.py +0 -0
  15. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/file_loading.py +0 -0
  16. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args/utils.py +0 -0
  17. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args.egg-info/dependency_links.txt +0 -0
  18. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args.egg-info/requires.txt +0 -0
  19. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/dataclass_args.egg-info/top_level.txt +0 -0
  20. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/setup.cfg +0 -0
  21. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_annotations.py +0 -0
  22. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_basic.py +0 -0
  23. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_boolean_base_configs.py +0 -0
  24. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_boolean_flags.py +0 -0
  25. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_builder_advanced.py +0 -0
  26. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_cli_choices.py +0 -0
  27. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_cli_short.py +0 -0
  28. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_combine_annotations.py +0 -0
  29. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_config_merging_simple.py +0 -0
  30. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_description.py +0 -0
  31. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_file_loading.py +0 -0
  32. {dataclass_args-1.2.2 → dataclass_args-1.4.0}/tests/test_positional.py +0 -0
  33. {dataclass_args-1.2.2 → dataclass_args-1.4.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.2.2
3
+ Version: 1.4.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
  [![Tests](https://github.com/bassmanitram/dataclass-args/actions/workflows/test.yml/badge.svg)](https://github.com/bassmanitram/dataclass-args/actions/workflows/test.yml)
55
- [![Code Quality](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml/badge.svg)](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml)
55
+ [![Lint](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml/badge.svg)](https://github.com/bassmanitram/dataclass-args/actions/workflows/lint.yml)
56
+ [![Code Quality](https://github.com/bassmanitram/dataclass-args/actions/workflows/quality.yml/badge.svg)](https://github.com/bassmanitram/dataclass-args/actions/workflows/quality.yml)
56
57
  [![Examples](https://github.com/bassmanitram/dataclass-args/actions/workflows/examples.yml/badge.svg)](https://github.com/bassmanitram/dataclass-args/actions/workflows/examples.yml)
57
58
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
58
59
  [![PyPI version](https://img.shields.io/pypi/v/dataclass-args.svg)](https://pypi.org/project/dataclass-args/)
@@ -62,10 +63,12 @@ Generate command-line interfaces from Python dataclasses.
62
63
 
63
64
  - **[Automatic CLI Generation](#quick-start)** - Generate CLI from dataclass definitions
64
65
  - **[Type-Safe Parsing](#type-support)** - Type-aware argument parsing for standard Python types
66
+ - **[Nested Dataclasses](#nested-dataclasses)** - Organize configs hierarchically with automatic flattening
65
67
  - **[Positional Arguments](#positional-arguments)** - Support for positional args with `cli_positional()`
66
68
  - **[Short Options](#short-options)** - Concise `-n` flags in addition to `--name`
67
69
  - **[Boolean Flags](#boolean-flags)** - Proper `--flag` and `--no-flag` boolean handling
68
70
  - **[Value Validation](#value-choices)** - Restrict values with `cli_choices()`
71
+ - **[Repeatable Options](#repeatable-options)** - Allow options to be specified multiple times with `cli_append()`
69
72
  - **[File Loading](#file-loadable-parameters)** - Load parameters from files using `@filename` syntax
70
73
  - **[Config Merging](#configuration-merging)** - Combine configuration sources with hierarchical overrides
71
74
  - **[Flexible Types](#type-support)** - Support for `List`, `Dict`, `Optional`, and custom types
@@ -209,6 +212,108 @@ error: argument --environment: invalid choice: 'invalid' (choose from 'dev', 'st
209
212
 
210
213
 
211
214
 
215
+ ### Repeatable Options
216
+
217
+ Use `cli_append()` to allow an option to be specified multiple times, with each occurrence collecting its own arguments:
218
+
219
+ ```python
220
+ from dataclass_args import cli_append
221
+
222
+ @dataclass
223
+ class Config:
224
+ # Simple tags: each -t adds one value
225
+ tags: List[str] = combine_annotations(
226
+ cli_short('t'),
227
+ cli_append(),
228
+ cli_help("Add a tag"),
229
+ default_factory=list
230
+ )
231
+ ```
232
+
233
+ ```bash
234
+ # Each -t occurrence accumulates
235
+ $ python app.py -t python -t cli -t tool
236
+ # Result: ['python', 'cli', 'tool']
237
+ ```
238
+
239
+ #### Repeatable with Multiple Arguments
240
+
241
+ Each occurrence can take multiple arguments using `nargs`:
242
+
243
+ ```python
244
+ @dataclass
245
+ class DockerConfig:
246
+ # Each -p takes exactly 2 arguments (HOST CONTAINER)
247
+ ports: List[List[str]] = combine_annotations(
248
+ cli_short('p'),
249
+ cli_append(nargs=2),
250
+ cli_help("Port mapping (HOST CONTAINER)"),
251
+ default_factory=list
252
+ )
253
+
254
+ # Each -v takes exactly 2 arguments (SOURCE TARGET)
255
+ volumes: List[List[str]] = combine_annotations(
256
+ cli_short('v'),
257
+ cli_append(nargs=2),
258
+ cli_help("Volume mount (SOURCE TARGET)"),
259
+ default_factory=list
260
+ )
261
+ ```
262
+
263
+ ```bash
264
+ $ python docker.py -p 8080 80 -p 8443 443 -v /host/data /container/data
265
+ ```
266
+
267
+ #### Variable Arguments with Validation
268
+
269
+ Use `min_args` and `max_args` for flexible argument counts with automatic validation:
270
+
271
+ ```python
272
+ @dataclass
273
+ class UploadConfig:
274
+ files: List[List[str]] = combine_annotations(
275
+ cli_short('f'),
276
+ cli_append(min_args=1, max_args=2, metavar="FILE [MIMETYPE]"),
277
+ cli_help("File with optional MIME type"),
278
+ default_factory=list
279
+ )
280
+ # No __post_init__ needed - validation is automatic!
281
+ ```
282
+
283
+ ```bash
284
+ # Mix files with and without MIME types
285
+ $ python upload.py -f doc.pdf application/pdf -f image.png -f video.mp4 video/mp4
286
+ # Result: [['doc.pdf', 'application/pdf'], ['image.png'], ['video.mp4', 'video/mp4']]
287
+
288
+ # Validation catches errors automatically
289
+ $ python upload.py -f file1 arg2 arg3 arg4
290
+ # Error: Expected at most 2 argument(s), got 4
291
+ ```
292
+
293
+ **Clean help display:**
294
+ ```
295
+ -f FILE [MIMETYPE], --files FILE [MIMETYPE]
296
+ File with optional MIME type (can be repeated, 1-2 args each)
297
+ ```
298
+
299
+ **Parameters:**
300
+ - `min_args`: Minimum arguments per occurrence
301
+ - `max_args`: Maximum arguments per occurrence
302
+ - Must be used together (both or neither)
303
+ - Mutually exclusive with `nargs`
304
+
305
+ **nargs Options:**
306
+ - `None` - One value per occurrence → `List[T]`
307
+ - `int` (e.g., `2`) - Exact count per occurrence → `List[List[T]]`
308
+ - `'+'` - One or more per occurrence → `List[List[T]]`
309
+ - `'*'` - Zero or more per occurrence → `List[List[T]]`
310
+
311
+ **Use Cases:**
312
+ - Docker-style options: `-p 8080:80 -p 8443:443 -v /host:/container -e KEY=value`
313
+ - File operations: `-f file1 type1 -f file2 -f file3 type3`
314
+ - Server pools: `-s host1 port1 -s host2 port2`
315
+ - Build systems: `-I dir1 -I dir2 --define KEY VAL`
316
+
212
317
  ### Positional Arguments
213
318
 
214
319
  Add positional arguments that don't require `--` prefixes:
@@ -475,6 +580,284 @@ $ python app.py --name "MyApp" --system-prompt "@~/prompts/assistant.txt"
475
580
  - `@/absolute/path` → Used as-is
476
581
  - `@relative/path` → Relative to current working directory
477
582
 
583
+ ### Nested Dataclasses
584
+
585
+ Organize complex configurations into nested dataclasses with automatic CLI flattening. Use `cli_nested()` to create hierarchical configs that are exposed as flat CLI arguments with customizable prefixes.
586
+
587
+ ```python
588
+ from dataclass_args import cli_nested
589
+
590
+ @dataclass
591
+ class DatabaseConfig:
592
+ host: str = "localhost"
593
+ port: int = 5432
594
+ username: str = "admin"
595
+ password: str = "secret"
596
+
597
+ @dataclass
598
+ class CacheConfig:
599
+ host: str = "localhost"
600
+ port: int = 6379
601
+ ttl: int = 3600
602
+
603
+ @dataclass
604
+ class AppConfig:
605
+ app_name: str = "myapp"
606
+ debug: bool = False
607
+
608
+ # Nested with custom prefix
609
+ database: DatabaseConfig = cli_nested(prefix="db", default_factory=DatabaseConfig)
610
+
611
+ # Nested with custom prefix
612
+ cache: CacheConfig = cli_nested(prefix="cache", default_factory=CacheConfig)
613
+
614
+ config = build_config(AppConfig)
615
+ ```
616
+
617
+ ```bash
618
+ # All nested fields are flattened with prefixes
619
+ $ python app.py \
620
+ --app-name "MyApp" \
621
+ --db-host prod-db.example.com \
622
+ --db-port 5432 \
623
+ --db-username dbuser \
624
+ --cache-host redis.example.com \
625
+ --cache-ttl 7200
626
+
627
+ # Help shows all flattened fields
628
+ $ python app.py --help
629
+ options:
630
+ --app-name APP_NAME
631
+ --debug / --no-debug
632
+ --db-host DB_HOST
633
+ --db-port DB_PORT
634
+ --db-username DB_USERNAME
635
+ --db-password DB_PASSWORD
636
+ --cache-host CACHE_HOST
637
+ --cache-port CACHE_PORT
638
+ --cache-ttl CACHE_TTL
639
+ ```
640
+
641
+ #### Prefix Modes
642
+
643
+ **Custom Prefix:**
644
+ ```python
645
+ database: DatabaseConfig = cli_nested(prefix="db", default_factory=DatabaseConfig)
646
+ # CLI: --db-host, --db-port, --db-username
647
+ ```
648
+
649
+ **No Prefix (Complete Flattening):**
650
+ ```python
651
+ credentials: Credentials = cli_nested(prefix="", default_factory=Credentials)
652
+ # CLI: --username, --password (no prefix)
653
+ ```
654
+
655
+ **Auto Prefix (Uses Field Name):**
656
+ ```python
657
+ database: DatabaseConfig = cli_nested(default_factory=DatabaseConfig)
658
+ # CLI: --database-host, --database-port (field name as prefix)
659
+ ```
660
+
661
+ #### Short Options with Nested Fields
662
+
663
+ Short options behave differently based on the prefix:
664
+
665
+ **With Prefix** → Short options are **ignored** (prevents conflicts):
666
+ ```python
667
+ @dataclass
668
+ class ServerConfig:
669
+ host: str = cli_short("h", default="localhost")
670
+ port: int = cli_short("p", default=8080)
671
+
672
+ @dataclass
673
+ class Config:
674
+ server: ServerConfig = cli_nested(prefix="srv", default_factory=ServerConfig)
675
+
676
+ # CLI: --srv-host, --srv-port (no -h or -p for nested fields)
677
+ ```
678
+
679
+ **No Prefix** → Short options are **enabled**:
680
+ ```python
681
+ @dataclass
682
+ class Credentials:
683
+ username: str = cli_short("u", default="admin")
684
+ password: str = cli_short("p", default="secret")
685
+
686
+ @dataclass
687
+ class Config:
688
+ app_name: str = cli_short("a", default="app")
689
+ creds: Credentials = cli_nested(prefix="", default_factory=Credentials)
690
+
691
+ # CLI: -a, -u, -p all work! (completely flattened)
692
+ ```
693
+
694
+ ```bash
695
+ # Use short options for all fields
696
+ $ python app.py -a MyApp -u john -p secretpass
697
+ ```
698
+
699
+ #### Collision Detection
700
+
701
+ Automatic collision detection prevents field name and short option conflicts:
702
+
703
+ **Field Name Collision:**
704
+ ```python
705
+ @dataclass
706
+ class Nested:
707
+ name: str = "nested"
708
+
709
+ @dataclass
710
+ class Config:
711
+ name: str = "parent"
712
+ nested: Nested = cli_nested(prefix="", default_factory=Nested)
713
+
714
+ # ERROR: Field name collision detected:
715
+ # --name
716
+ # - name
717
+ # - nested.name
718
+ # Solution: Add prefix to nested field or rename
719
+ ```
720
+
721
+ **Short Option Collision:**
722
+ ```python
723
+ @dataclass
724
+ class Nested:
725
+ host: str = cli_short("h", default="nested-host")
726
+
727
+ @dataclass
728
+ class Config:
729
+ help_text: str = cli_short("h", default="help")
730
+ nested: Nested = cli_nested(prefix="", default_factory=Nested)
731
+
732
+ # ERROR: Short option collision detected:
733
+ # -h
734
+ # - help_text (--help-text)
735
+ # - nested.host (--host)
736
+ # Solution: Use different short options or add prefix
737
+ ```
738
+
739
+ #### Config File Merging with Nested Fields
740
+
741
+ Nested dataclasses work seamlessly with config files:
742
+
743
+ **config.yaml:**
744
+ ```yaml
745
+ app_name: "ProductionApp"
746
+ database:
747
+ host: "prod-db.example.com"
748
+ port: 5432
749
+ username: "prod_user"
750
+ cache:
751
+ host: "redis.example.com"
752
+ ttl: 7200
753
+ ```
754
+
755
+ ```python
756
+ config = build_config(AppConfig, args=[
757
+ '--config', 'config.yaml',
758
+ '--db-password', 'secret', # Override specific nested field
759
+ '--cache-ttl', '3600' # Override another nested field
760
+ ])
761
+
762
+ # Result:
763
+ # - app_name: "ProductionApp" (from file)
764
+ # - database.host: "prod-db.example.com" (from file)
765
+ # - database.password: "secret" (CLI override)
766
+ # - cache.host: "redis.example.com" (from file)
767
+ # - cache.ttl: 3600 (CLI override)
768
+ ```
769
+
770
+ #### Real-World Example
771
+
772
+ ```python
773
+ from dataclasses import dataclass
774
+ from dataclass_args import build_config, cli_nested, cli_short, cli_help, combine_annotations
775
+
776
+ @dataclass
777
+ class DatabaseConfig:
778
+ """Database connection settings."""
779
+ host: str = cli_help("Database hostname", default="localhost")
780
+ port: int = cli_help("Database port", default=5432)
781
+ database: str = cli_help("Database name", default="mydb")
782
+ username: str = cli_help("Database username", default="admin")
783
+ password: str = cli_help("Database password", default="")
784
+
785
+ @dataclass
786
+ class LoggingConfig:
787
+ """Logging configuration."""
788
+ level: str = combine_annotations(
789
+ cli_help("Log level"),
790
+ cli_choices(["DEBUG", "INFO", "WARNING", "ERROR"]),
791
+ default="INFO"
792
+ )
793
+ file: str = cli_help("Log file path", default="app.log")
794
+ format: str = cli_help("Log format string", default="%(levelname)s: %(message)s")
795
+
796
+ @dataclass
797
+ class AppConfig:
798
+ """Application configuration with nested sections."""
799
+
800
+ # Top-level settings
801
+ app_name: str = combine_annotations(
802
+ cli_short("n"),
803
+ cli_help("Application name"),
804
+ default="myapp"
805
+ )
806
+
807
+ debug: bool = combine_annotations(
808
+ cli_short("d"),
809
+ cli_help("Enable debug mode"),
810
+ default=False
811
+ )
812
+
813
+ workers: int = combine_annotations(
814
+ cli_short("w"),
815
+ cli_help("Number of worker threads"),
816
+ default=4
817
+ )
818
+
819
+ # Nested configurations
820
+ database: DatabaseConfig = cli_nested(
821
+ prefix="db",
822
+ default_factory=DatabaseConfig
823
+ )
824
+
825
+ logging: LoggingConfig = cli_nested(
826
+ prefix="log",
827
+ default_factory=LoggingConfig
828
+ )
829
+
830
+ if __name__ == "__main__":
831
+ config = build_config(AppConfig)
832
+
833
+ print(f"App: {config.app_name}")
834
+ print(f"Database: {config.database.host}:{config.database.port}/{config.database.database}")
835
+ print(f"Logging: {config.logging.level} -> {config.logging.file}")
836
+ ```
837
+
838
+ ```bash
839
+ # Production deployment with nested config
840
+ $ python app.py \
841
+ -n "ProductionApp" \
842
+ -w 16 \
843
+ --db-host prod-db.example.com \
844
+ --db-database prod_db \
845
+ --db-username prod_user \
846
+ --log-level WARNING \
847
+ --log-file /var/log/app.log
848
+
849
+ # Or load base config and override specific fields
850
+ $ python app.py \
851
+ --config production.yaml \
852
+ --db-password "${DB_PASSWORD}" \
853
+ --log-level DEBUG
854
+ ```
855
+
856
+ **See also:**
857
+ - [`examples/nested_dataclass.py`](examples/nested_dataclass.py) - Complete nested dataclass example
858
+ - [`examples/nested_short_options.py`](examples/nested_short_options.py) - Short options with nested fields
859
+
860
+
478
861
  # config.yaml
479
862
  name: "DefaultApp"
480
863
  count: 100
@@ -830,6 +1213,30 @@ input: str = combine_annotations(
830
1213
 
831
1214
  **Important:** At most one positional can use `nargs='*'` or `'+'`, and it must be the last positional.
832
1215
 
1216
+ #### `cli_nested(prefix=None, default_factory=None, **kwargs)`
1217
+
1218
+ Mark a dataclass field as a nested configuration that should be flattened into CLI arguments.
1219
+
1220
+ ```python
1221
+ # Custom prefix
1222
+ database: DatabaseConfig = cli_nested(prefix="db", default_factory=DatabaseConfig)
1223
+ # CLI: --db-host, --db-port
1224
+
1225
+ # No prefix (complete flattening)
1226
+ credentials: Credentials = cli_nested(prefix="", default_factory=Credentials)
1227
+ # CLI: --username, --password
1228
+
1229
+ # Auto prefix (uses field name)
1230
+ logging: LogConfig = cli_nested(default_factory=LogConfig)
1231
+ # CLI: --logging-level, --logging-file
1232
+ ```
1233
+
1234
+ **Important:**
1235
+ - Fields with prefixes don't support short options
1236
+ - Fields without prefix (prefix="") support short options
1237
+ - Automatic collision detection for field names and short options
1238
+ - Works seamlessly with config file merging
1239
+
833
1240
  #### `cli_exclude(**kwargs)`
834
1241
 
835
1242
  Exclude fields from CLI argument generation.
@@ -1071,6 +1478,7 @@ from dataclass_args import (
1071
1478
  cli_positional, # Positional args
1072
1479
  cli_choices, # Value validation
1073
1480
  cli_help, # Custom help text
1481
+ cli_nested, # Nested dataclasses
1074
1482
  cli_exclude, # Hide from CLI
1075
1483
  cli_file_loadable, # @file loading
1076
1484
  combine_annotations, # Combine features
@@ -1107,6 +1515,9 @@ class Config:
1107
1515
  # File-loadable
1108
1516
  config_text: str = cli_file_loadable(default="")
1109
1517
 
1518
+ # Nested dataclass
1519
+ database: DatabaseConfig = cli_nested(prefix="db", default_factory=DatabaseConfig)
1520
+
1110
1521
  # Build and use
1111
1522
  config = build_config(Config)
1112
1523
  ```