params-proto 3.2.4__tar.gz → 3.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.
Files changed (133) hide show
  1. {params_proto-3.2.4 → params_proto-3.3.0}/PKG-INFO +1 -1
  2. {params_proto-3.2.4 → params_proto-3.3.0}/docs/release_notes.md +18 -0
  3. {params_proto-3.2.4 → params_proto-3.3.0}/pyproject.toml +1 -1
  4. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/cli/cli_parse.py +169 -17
  5. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_nested_cli.py +8 -15
  6. {params_proto-3.2.4 → params_proto-3.3.0}/.claude-plugin/marketplace.json +0 -0
  7. {params_proto-3.2.4 → params_proto-3.3.0}/.claude-plugin/plugin.json +0 -0
  8. {params_proto-3.2.4 → params_proto-3.3.0}/.editorconfig +0 -0
  9. {params_proto-3.2.4 → params_proto-3.3.0}/.gitignore +0 -0
  10. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/.gitignore +0 -0
  11. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/codeStyles/codeStyleConfig.xml +0 -0
  12. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/inspectionProfiles/Project_Default.xml +0 -0
  13. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  14. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/markdown.xml +0 -0
  15. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/misc.xml +0 -0
  16. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/modules.xml +0 -0
  17. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/params-proto.iml +0 -0
  18. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/ruff.xml +0 -0
  19. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/vcs.xml +0 -0
  20. {params_proto-3.2.4 → params_proto-3.3.0}/.idea/workspace.xml +0 -0
  21. {params_proto-3.2.4 → params_proto-3.3.0}/.readthedocs.yaml +0 -0
  22. {params_proto-3.2.4 → params_proto-3.3.0}/.run/pytest for test_neo_proto_cli.run.xml +0 -0
  23. {params_proto-3.2.4 → params_proto-3.3.0}/.run/pytest in test_params_proto.run.xml +0 -0
  24. {params_proto-3.2.4 → params_proto-3.3.0}/ANSI_HELP_CONSIDERATIONS.md +0 -0
  25. {params_proto-3.2.4 → params_proto-3.3.0}/CLAUDE.md +0 -0
  26. {params_proto-3.2.4 → params_proto-3.3.0}/LICENSE.md +0 -0
  27. {params_proto-3.2.4 → params_proto-3.3.0}/Makefile +0 -0
  28. {params_proto-3.2.4 → params_proto-3.3.0}/README +0 -0
  29. {params_proto-3.2.4 → params_proto-3.3.0}/README.md +0 -0
  30. {params_proto-3.2.4 → params_proto-3.3.0}/docs/Makefile +0 -0
  31. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/api/hyper.md +0 -0
  32. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/api/proto.md +0 -0
  33. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/api/utils.md +0 -0
  34. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/advanced_features.md +0 -0
  35. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/basic_usage.md +0 -0
  36. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/environment_variables.md +0 -0
  37. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/hyperparameter_sweeps.md +0 -0
  38. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/index.md +0 -0
  39. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/examples/nested_configs.md +0 -0
  40. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_archive_v2/quick_start.md +0 -0
  41. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_static/ansi.css +0 -0
  42. {params_proto-3.2.4 → params_proto-3.3.0}/docs/_static/custom.css +0 -0
  43. {params_proto-3.2.4 → params_proto-3.3.0}/docs/api/index.md +0 -0
  44. {params_proto-3.2.4 → params_proto-3.3.0}/docs/api/proto.md +0 -0
  45. {params_proto-3.2.4 → params_proto-3.3.0}/docs/conf.py +0 -0
  46. {params_proto-3.2.4 → params_proto-3.3.0}/docs/examples/basic_usage.md +0 -0
  47. {params_proto-3.2.4 → params_proto-3.3.0}/docs/examples/cli_applications.md +0 -0
  48. {params_proto-3.2.4 → params_proto-3.3.0}/docs/examples/ml_training.md +0 -0
  49. {params_proto-3.2.4 → params_proto-3.3.0}/docs/examples/rl_agent.md +0 -0
  50. {params_proto-3.2.4 → params_proto-3.3.0}/docs/index.md +0 -0
  51. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/advanced_patterns.md +0 -0
  52. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/ansi_formatting.md +0 -0
  53. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/cli-fundamentals.md +0 -0
  54. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/cli-patterns.md +0 -0
  55. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/configuration-patterns.md +0 -0
  56. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/core-concepts.md +0 -0
  57. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/environment_variables.md +0 -0
  58. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/help-generation.md +0 -0
  59. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/hyperparameter_sweeps.md +0 -0
  60. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/naming-conventions.md +0 -0
  61. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/parameter-iteration.md +0 -0
  62. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/parameter-overrides.md +0 -0
  63. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/type-system.md +0 -0
  64. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/union_types.md +0 -0
  65. {params_proto-3.2.4 → params_proto-3.3.0}/docs/key_concepts/welcome.md +0 -0
  66. {params_proto-3.2.4 → params_proto-3.3.0}/docs/migration.md +0 -0
  67. {params_proto-3.2.4 → params_proto-3.3.0}/docs/quick_start.md +0 -0
  68. {params_proto-3.2.4 → params_proto-3.3.0}/docs/requirements.txt +0 -0
  69. {params_proto-3.2.4 → params_proto-3.3.0}/examples/union_subcommands.py +0 -0
  70. {params_proto-3.2.4 → params_proto-3.3.0}/figures/man-page.png +0 -0
  71. {params_proto-3.2.4 → params_proto-3.3.0}/figures/params-proto-autocompletion.gif +0 -0
  72. {params_proto-3.2.4 → params_proto-3.3.0}/figures/spec_files.png +0 -0
  73. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/demo_params_proto.py +0 -0
  74. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/demo_v3.py +0 -0
  75. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/proto_DAT_scratch.py +0 -0
  76. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/proto_dependency_tree_pattern.py +0 -0
  77. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/test_super.py +0 -0
  78. {params_proto-3.2.4 → params_proto-3.3.0}/scratch/test_super_minimal.py +0 -0
  79. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto/SKILL.md +0 -0
  80. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto/references/cli-and-types.md +0 -0
  81. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto/references/environment-vars.md +0 -0
  82. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto/references/patterns.md +0 -0
  83. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto/references/sweeps.md +0 -0
  84. {params_proto-3.2.4 → params_proto-3.3.0}/skills/params-proto.skill +0 -0
  85. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/__init__.py +0 -0
  86. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/app.py +0 -0
  87. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/cli/__init__.py +0 -0
  88. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/cli/ansi_help.py +0 -0
  89. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/cli/help_gen.py +0 -0
  90. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/documentation.py +0 -0
  91. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/envvar.py +0 -0
  92. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/hyper/__init__.py +0 -0
  93. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/hyper/proxies.py +0 -0
  94. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/hyper/sweep.py +0 -0
  95. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/parse_env_template.py +0 -0
  96. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/proto.py +0 -0
  97. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/type_utils.py +0 -0
  98. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v1/__init__.py +0 -0
  99. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v1/hyper.py +0 -0
  100. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v1/params_proto.py +0 -0
  101. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v2/__init__.py +0 -0
  102. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v2/hyper.py +0 -0
  103. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v2/partial.py +0 -0
  104. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v2/proto.py +0 -0
  105. {params_proto-3.2.4 → params_proto-3.3.0}/src/params_proto/v2/utils.py +0 -0
  106. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v1/__init__.py +0 -0
  107. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v1/test_hyper.py +0 -0
  108. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v1/test_params_proto.py +0 -0
  109. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_Eval.py +0 -0
  110. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_neo_hyper.py +0 -0
  111. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_neo_proto.py +0 -0
  112. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_neo_proto_cli.py +0 -0
  113. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_neo_proto_partial.py +0 -0
  114. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v2/test_utils.py +0 -0
  115. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/conftest.py +0 -0
  116. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/samples/train.py +0 -0
  117. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_advanced_types.py +0 -0
  118. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_class_level_methods.py +0 -0
  119. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_cli_parsing.py +0 -0
  120. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_help_strings.py +0 -0
  121. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_method_self_param.py +0 -0
  122. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_parse_env_template.py +0 -0
  123. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_piter.py +0 -0
  124. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_positional_example.sh +0 -0
  125. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_comments.py +0 -0
  126. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_context_manager.py +0 -0
  127. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_core.py +0 -0
  128. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_envvar.py +0 -0
  129. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_linebreaking.py +0 -0
  130. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_partial.py +0 -0
  131. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_proto_required.py +0 -0
  132. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_strings.py +0 -0
  133. {params_proto-3.2.4 → params_proto-3.3.0}/tests/test_v3/test_sweep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: params-proto
3
- Version: 3.2.4
3
+ Version: 3.3.0
4
4
  Summary: Modern Hyper Parameter Management for Machine Learning
5
5
  Project-URL: Homepage, https://github.com/geyang/params-proto
6
6
  Project-URL: Documentation, https://params-proto.readthedocs.io
@@ -2,6 +2,24 @@
2
2
 
3
3
  This page contains the release history and changelog for params-proto.
4
4
 
5
+ ## Version 3.3.0 (2025-02-04)
6
+
7
+ ### ✨ Features
8
+
9
+ - **Deep Nested Dot Notation for CLI**: Support for overriding nested dataclass fields via CLI
10
+ ```bash
11
+ # Before (not supported):
12
+ python train.py train-config --model.hidden-size 512 # error
13
+
14
+ # Now works:
15
+ python train.py train-config --epochs 200 --model.hidden-size 512 --model.num-layers 8
16
+ ```
17
+ - Recursively detects nested dataclass fields in type annotations
18
+ - Supports arbitrary nesting depth (e.g., `--model.encoder.layers 12`)
19
+ - Automatically constructs nested dataclass instances
20
+
21
+ ---
22
+
5
23
  ## Version 3.2.4 (2025-02-04)
6
24
 
7
25
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "params-proto"
3
- version = "3.2.4"
3
+ version = "3.3.0"
4
4
  description = "Modern Hyper Parameter Management for Machine Learning"
5
5
  authors = [
6
6
  { name = "Ge Yang" }
@@ -83,6 +83,101 @@ def _get_required_fields(cls) -> List[str]:
83
83
  return required
84
84
 
85
85
 
86
+ def _is_nested_dataclass(annotation) -> bool:
87
+ """Check if annotation is a nested dataclass type."""
88
+ import dataclasses
89
+
90
+ # Skip primitive types and common non-dataclass types
91
+ if annotation in {int, str, float, bool, list, dict, tuple, set, Path, type(None)}:
92
+ return False
93
+
94
+ # Check if it's a type and a dataclass
95
+ if isinstance(annotation, type) and dataclasses.is_dataclass(annotation):
96
+ return True
97
+
98
+ return False
99
+
100
+
101
+ def _get_nested_attrs(cls, prefix: str = "") -> Dict[str, tuple]:
102
+ """Recursively get all nested dataclass attributes.
103
+
104
+ Returns dict mapping dotted-kebab-name -> (dotted_underscore_path, leaf_type)
105
+
106
+ Example for TrainConfig with nested ModelConfig:
107
+ {
108
+ "model.hidden-size": ("model.hidden_size", int),
109
+ "model.num-layers": ("model.num_layers", int),
110
+ }
111
+ """
112
+ import dataclasses
113
+
114
+ result = {}
115
+ annotations = getattr(cls, "__annotations__", {})
116
+
117
+ for attr_name, attr_type in annotations.items():
118
+ kebab_attr = attr_name.replace("_", "-")
119
+ full_kebab = f"{prefix}{kebab_attr}" if prefix else kebab_attr
120
+ full_underscore = f"{prefix.replace('-', '_')}{attr_name}" if prefix else attr_name
121
+
122
+ if _is_nested_dataclass(attr_type):
123
+ # Recursively get nested attributes
124
+ nested = _get_nested_attrs(attr_type, prefix=f"{full_kebab}.")
125
+ result.update(nested)
126
+ else:
127
+ # Leaf attribute
128
+ result[full_kebab] = (full_underscore, attr_type)
129
+
130
+ return result
131
+
132
+
133
+ def _set_nested_value(d: dict, path: str, value: Any) -> None:
134
+ """Set a value in a nested dict using dot notation path.
135
+
136
+ Example: _set_nested_value({}, "model.hidden_size", 512)
137
+ Results in {"model": {"hidden_size": 512}}
138
+ """
139
+ parts = path.split(".")
140
+ current = d
141
+ for part in parts[:-1]:
142
+ if part not in current:
143
+ current[part] = {}
144
+ current = current[part]
145
+ current[parts[-1]] = value
146
+
147
+
148
+ def _build_nested_instance(cls, flat_attrs: dict, nested_attrs: dict):
149
+ """Build a dataclass instance with nested dataclass fields.
150
+
151
+ Args:
152
+ cls: The dataclass class to instantiate
153
+ flat_attrs: Dict of top-level attribute values
154
+ nested_attrs: Dict of nested dicts for nested dataclass fields
155
+
156
+ Returns:
157
+ Instance of cls with nested dataclasses properly constructed
158
+ """
159
+ import dataclasses
160
+
161
+ final_attrs = dict(flat_attrs)
162
+ annotations = getattr(cls, "__annotations__", {})
163
+
164
+ for attr_name, attr_type in annotations.items():
165
+ if attr_name in nested_attrs and _is_nested_dataclass(attr_type):
166
+ # Recursively build nested dataclass
167
+ nested_data = nested_attrs[attr_name]
168
+ # Separate flat and nested for the nested class
169
+ nested_flat = {}
170
+ nested_nested = {}
171
+ for k, v in nested_data.items():
172
+ if isinstance(v, dict):
173
+ nested_nested[k] = v
174
+ else:
175
+ nested_flat[k] = v
176
+ final_attrs[attr_name] = _build_nested_instance(attr_type, nested_flat, nested_nested)
177
+
178
+ return cls(**final_attrs)
179
+
180
+
86
181
  def _match_class_by_name(name: str, classes: list) -> Union[type, None]:
87
182
  """Match a string to one of the Union classes.
88
183
 
@@ -193,6 +288,7 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
193
288
  # Build unprefixed union attribute map for classes NOT decorated with @proto.prefix
194
289
  # Maps attr-name -> (union_param_name, attr_name_underscore)
195
290
  # Classes in _SINGLETONS are @proto.prefix decorated and require prefixed attrs
291
+ # Also includes nested dataclass attributes (e.g., "model.hidden-size")
196
292
  unprefixed_attrs = {}
197
293
  for kebab_name, (param_name, union_classes) in union_params.items():
198
294
  for cls in union_classes:
@@ -201,12 +297,20 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
201
297
  if is_prefix_class:
202
298
  continue
203
299
  if hasattr(cls, "__annotations__"):
204
- for attr_name in cls.__annotations__:
300
+ for attr_name, attr_type in cls.__annotations__.items():
205
301
  kebab_attr = attr_name.replace("_", "-")
206
302
  # Map to the union param (first one wins if multiple unions have same attr)
207
303
  if kebab_attr not in unprefixed_attrs:
208
304
  unprefixed_attrs[kebab_attr] = (param_name, attr_name)
209
305
 
306
+ # Check for nested dataclass and add its attributes
307
+ if _is_nested_dataclass(attr_type):
308
+ nested_attrs = _get_nested_attrs(attr_type, prefix=f"{kebab_attr}.")
309
+ for nested_kebab, (nested_path, nested_type) in nested_attrs.items():
310
+ full_path = f"{attr_name}.{nested_path.split('.', 1)[1] if '.' in nested_path else nested_path}"
311
+ if nested_kebab not in unprefixed_attrs:
312
+ unprefixed_attrs[nested_kebab] = (param_name, full_path)
313
+
210
314
  # Parse arguments
211
315
  result = {}
212
316
  prefix_values = {} # (singleton, param_name) -> value
@@ -451,21 +555,65 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
451
555
  # Instantiate Union classes with collected attributes
452
556
  for param_name, selected_class in union_selections.items():
453
557
  # Collect attributes for this Union parameter
454
- attrs = {}
455
- for (union_param, attr_name), value_str in union_attrs.items():
558
+ # Separate flat (top-level) and nested attributes
559
+ flat_attrs = {}
560
+ nested_attrs = {} # For nested dataclass fields
561
+
562
+ for (union_param, attr_path), value_str in union_attrs.items():
456
563
  if union_param == param_name:
457
- # Get the type annotation for this attribute
458
- if hasattr(selected_class, "__annotations__"):
459
- attr_type = selected_class.__annotations__.get(attr_name, str)
460
- try:
461
- attrs[attr_name] = _convert_type(value_str, attr_type)
462
- except (ValueError, TypeError):
463
- raise SystemExit(
464
- f"error: invalid value for --{param_name.replace('_', '-')}.{attr_name.replace('_', '-')}: {value_str}"
465
- )
564
+ # Check if this is a nested path (contains dots)
565
+ if "." in attr_path:
566
+ # Nested attribute like "model.hidden_size"
567
+ parts = attr_path.split(".")
568
+ top_level = parts[0]
569
+ rest_path = ".".join(parts[1:])
570
+
571
+ # Get the type of the nested field
572
+ if hasattr(selected_class, "__annotations__"):
573
+ top_type = selected_class.__annotations__.get(top_level)
574
+ if top_type and _is_nested_dataclass(top_type):
575
+ # Find the leaf type by traversing the path
576
+ current_type = top_type
577
+ for part in parts[1:]:
578
+ if hasattr(current_type, "__annotations__"):
579
+ current_type = current_type.__annotations__.get(part, str)
580
+ else:
581
+ current_type = str
582
+ break
583
+
584
+ try:
585
+ value = _convert_type(value_str, current_type)
586
+ except (ValueError, TypeError):
587
+ raise SystemExit(
588
+ f"error: invalid value for --{attr_path.replace('_', '-')}: {value_str}"
589
+ )
590
+
591
+ # Store in nested structure
592
+ if top_level not in nested_attrs:
593
+ nested_attrs[top_level] = {}
594
+ _set_nested_value(nested_attrs[top_level], rest_path, value)
595
+ continue
596
+
597
+ # Fallback: treat as string
598
+ if top_level not in nested_attrs:
599
+ nested_attrs[top_level] = {}
600
+ _set_nested_value(nested_attrs[top_level], rest_path, value_str)
466
601
  else:
467
- # No annotations, treat as string
468
- attrs[attr_name] = value_str
602
+ # Top-level attribute
603
+ if hasattr(selected_class, "__annotations__"):
604
+ attr_type = selected_class.__annotations__.get(attr_path, str)
605
+ try:
606
+ flat_attrs[attr_path] = _convert_type(value_str, attr_type)
607
+ except (ValueError, TypeError):
608
+ raise SystemExit(
609
+ f"error: invalid value for --{param_name.replace('_', '-')}.{attr_path.replace('_', '-')}: {value_str}"
610
+ )
611
+ else:
612
+ # No annotations, treat as string
613
+ flat_attrs[attr_path] = value_str
614
+
615
+ # Merge flat_attrs into attrs for compatibility with existing code
616
+ attrs = flat_attrs
469
617
 
470
618
  # Assign positional args to required fields of the selected class
471
619
  if param_name in union_positional and union_positional[param_name]:
@@ -508,17 +656,21 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
508
656
  attrs[key] = value
509
657
  break
510
658
 
511
- # Check for missing required fields
659
+ # Check for missing required fields (only check top-level, nested have defaults)
512
660
  required_fields = _get_required_fields(selected_class)
513
661
  for field_name in required_fields:
514
- if field_name not in attrs:
662
+ if field_name not in attrs and field_name not in nested_attrs:
515
663
  raise SystemExit(
516
664
  f"error: {selected_class.__name__} requires argument: {field_name}"
517
665
  )
518
666
 
519
667
  # Instantiate the class with collected attributes
520
668
  try:
521
- instance = selected_class(**attrs)
669
+ if nested_attrs:
670
+ # Build with nested dataclass support
671
+ instance = _build_nested_instance(selected_class, attrs, nested_attrs)
672
+ else:
673
+ instance = selected_class(**attrs)
522
674
  result[param_name] = instance
523
675
  except TypeError as e:
524
676
  raise SystemExit(f"error: failed to instantiate {selected_class.__name__}: {e}")
@@ -171,7 +171,7 @@ class TestThreeLayerNesting:
171
171
  main()
172
172
  """)
173
173
 
174
- # Try to override nested model config via deep dot notation
174
+ # Override nested model config via deep dot notation
175
175
  # Plain dataclasses use unprefixed syntax (--epochs, not --config.epochs)
176
176
  # Only @proto.prefix decorated classes require prefixed syntax
177
177
  result = run_cli(
@@ -180,25 +180,18 @@ class TestThreeLayerNesting:
180
180
  "train-config", # subcommand name (kebab-case)
181
181
  "--epochs",
182
182
  "200",
183
- "--model.hidden_size",
183
+ "--model.hidden-size",
184
184
  "512",
185
- "--model.num_layers",
185
+ "--model.num-layers",
186
186
  "8",
187
187
  ],
188
- expect_error=True, # Deep nested dot notation may not be supported yet
189
188
  )
190
189
 
191
- # Check if it succeeded or failed
192
- if result["returncode"] == 0:
193
- lines = result["stdout"].strip().split("\n")
194
- assert lines[0] == "epochs=200"
195
- assert lines[1] == "model.hidden_size=512"
196
- assert lines[2] == "model.num_layers=8"
197
- else:
198
- # If it failed, this documents the current limitation
199
- pytest.skip(
200
- "Deep nested dot notation (--model.hidden_size) not yet supported"
201
- )
190
+ assert result["returncode"] == 0, f"CLI failed: {result['stderr']}"
191
+ lines = result["stdout"].strip().split("\n")
192
+ assert lines[0] == "epochs=200"
193
+ assert lines[1] == "model.hidden_size=512"
194
+ assert lines[2] == "model.num_layers=8"
202
195
 
203
196
 
204
197
  class TestThreeLayerUnionNesting:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes