params-proto 3.1.2__tar.gz → 3.2.1__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 (130) hide show
  1. {params_proto-3.1.2 → params_proto-3.2.1}/.claude-plugin/plugin.json +1 -1
  2. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/workspace.xml +8 -5
  3. {params_proto-3.1.2 → params_proto-3.2.1}/PKG-INFO +1 -1
  4. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/union_types.md +113 -43
  5. {params_proto-3.1.2 → params_proto-3.2.1}/docs/release_notes.md +51 -0
  6. {params_proto-3.1.2 → params_proto-3.2.1}/pyproject.toml +1 -1
  7. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto/SKILL.md +2 -2
  8. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto/references/cli-and-types.md +32 -2
  9. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto.skill +0 -0
  10. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/cli/cli_parse.py +98 -1
  11. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/hyper/sweep.py +71 -0
  12. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/proto.py +28 -16
  13. params_proto-3.2.1/tests/test_v3/conftest.py +56 -0
  14. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_cli_parsing.py +0 -55
  15. params_proto-3.2.1/tests/test_v3/test_nested_cli.py +956 -0
  16. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_piter.py +121 -0
  17. {params_proto-3.1.2 → params_proto-3.2.1}/.claude-plugin/marketplace.json +0 -0
  18. {params_proto-3.1.2 → params_proto-3.2.1}/.editorconfig +0 -0
  19. {params_proto-3.1.2 → params_proto-3.2.1}/.gitignore +0 -0
  20. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/.gitignore +0 -0
  21. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/codeStyles/codeStyleConfig.xml +0 -0
  22. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/inspectionProfiles/Project_Default.xml +0 -0
  23. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  24. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/markdown.xml +0 -0
  25. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/misc.xml +0 -0
  26. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/modules.xml +0 -0
  27. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/params-proto.iml +0 -0
  28. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/ruff.xml +0 -0
  29. {params_proto-3.1.2 → params_proto-3.2.1}/.idea/vcs.xml +0 -0
  30. {params_proto-3.1.2 → params_proto-3.2.1}/.readthedocs.yaml +0 -0
  31. {params_proto-3.1.2 → params_proto-3.2.1}/.run/pytest for test_neo_proto_cli.run.xml +0 -0
  32. {params_proto-3.1.2 → params_proto-3.2.1}/.run/pytest in test_params_proto.run.xml +0 -0
  33. {params_proto-3.1.2 → params_proto-3.2.1}/ANSI_HELP_CONSIDERATIONS.md +0 -0
  34. {params_proto-3.1.2 → params_proto-3.2.1}/CLAUDE.md +0 -0
  35. {params_proto-3.1.2 → params_proto-3.2.1}/LICENSE.md +0 -0
  36. {params_proto-3.1.2 → params_proto-3.2.1}/Makefile +0 -0
  37. {params_proto-3.1.2 → params_proto-3.2.1}/README +0 -0
  38. {params_proto-3.1.2 → params_proto-3.2.1}/README.md +0 -0
  39. {params_proto-3.1.2 → params_proto-3.2.1}/docs/Makefile +0 -0
  40. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/api/hyper.md +0 -0
  41. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/api/proto.md +0 -0
  42. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/api/utils.md +0 -0
  43. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/advanced_features.md +0 -0
  44. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/basic_usage.md +0 -0
  45. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/environment_variables.md +0 -0
  46. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/hyperparameter_sweeps.md +0 -0
  47. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/index.md +0 -0
  48. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/examples/nested_configs.md +0 -0
  49. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_archive_v2/quick_start.md +0 -0
  50. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_static/ansi.css +0 -0
  51. {params_proto-3.1.2 → params_proto-3.2.1}/docs/_static/custom.css +0 -0
  52. {params_proto-3.1.2 → params_proto-3.2.1}/docs/api/index.md +0 -0
  53. {params_proto-3.1.2 → params_proto-3.2.1}/docs/api/proto.md +0 -0
  54. {params_proto-3.1.2 → params_proto-3.2.1}/docs/conf.py +0 -0
  55. {params_proto-3.1.2 → params_proto-3.2.1}/docs/examples/basic_usage.md +0 -0
  56. {params_proto-3.1.2 → params_proto-3.2.1}/docs/examples/cli_applications.md +0 -0
  57. {params_proto-3.1.2 → params_proto-3.2.1}/docs/examples/ml_training.md +0 -0
  58. {params_proto-3.1.2 → params_proto-3.2.1}/docs/examples/rl_agent.md +0 -0
  59. {params_proto-3.1.2 → params_proto-3.2.1}/docs/index.md +0 -0
  60. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/advanced_patterns.md +0 -0
  61. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/ansi_formatting.md +0 -0
  62. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/cli-fundamentals.md +0 -0
  63. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/cli-patterns.md +0 -0
  64. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/configuration-patterns.md +0 -0
  65. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/core-concepts.md +0 -0
  66. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/environment_variables.md +0 -0
  67. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/help-generation.md +0 -0
  68. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/hyperparameter_sweeps.md +0 -0
  69. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/naming-conventions.md +0 -0
  70. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/parameter-iteration.md +0 -0
  71. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/parameter-overrides.md +0 -0
  72. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/type-system.md +0 -0
  73. {params_proto-3.1.2 → params_proto-3.2.1}/docs/key_concepts/welcome.md +0 -0
  74. {params_proto-3.1.2 → params_proto-3.2.1}/docs/migration.md +0 -0
  75. {params_proto-3.1.2 → params_proto-3.2.1}/docs/quick_start.md +0 -0
  76. {params_proto-3.1.2 → params_proto-3.2.1}/docs/requirements.txt +0 -0
  77. {params_proto-3.1.2 → params_proto-3.2.1}/examples/union_subcommands.py +0 -0
  78. {params_proto-3.1.2 → params_proto-3.2.1}/figures/man-page.png +0 -0
  79. {params_proto-3.1.2 → params_proto-3.2.1}/figures/params-proto-autocompletion.gif +0 -0
  80. {params_proto-3.1.2 → params_proto-3.2.1}/figures/spec_files.png +0 -0
  81. {params_proto-3.1.2 → params_proto-3.2.1}/scratch/demo_params_proto.py +0 -0
  82. {params_proto-3.1.2 → params_proto-3.2.1}/scratch/demo_v3.py +0 -0
  83. {params_proto-3.1.2 → params_proto-3.2.1}/scratch/proto_DAT_scratch.py +0 -0
  84. {params_proto-3.1.2 → params_proto-3.2.1}/scratch/proto_dependency_tree_pattern.py +0 -0
  85. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto/references/environment-vars.md +0 -0
  86. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto/references/patterns.md +0 -0
  87. {params_proto-3.1.2 → params_proto-3.2.1}/skills/params-proto/references/sweeps.md +0 -0
  88. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/__init__.py +0 -0
  89. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/app.py +0 -0
  90. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/cli/__init__.py +0 -0
  91. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/cli/ansi_help.py +0 -0
  92. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/cli/help_gen.py +0 -0
  93. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/documentation.py +0 -0
  94. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/envvar.py +0 -0
  95. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/hyper/__init__.py +0 -0
  96. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/hyper/proxies.py +0 -0
  97. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/parse_env_template.py +0 -0
  98. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/type_utils.py +0 -0
  99. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v1/__init__.py +0 -0
  100. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v1/hyper.py +0 -0
  101. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v1/params_proto.py +0 -0
  102. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v2/__init__.py +0 -0
  103. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v2/hyper.py +0 -0
  104. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v2/partial.py +0 -0
  105. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v2/proto.py +0 -0
  106. {params_proto-3.1.2 → params_proto-3.2.1}/src/params_proto/v2/utils.py +0 -0
  107. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v1/__init__.py +0 -0
  108. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v1/test_hyper.py +0 -0
  109. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v1/test_params_proto.py +0 -0
  110. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_Eval.py +0 -0
  111. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_neo_hyper.py +0 -0
  112. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_neo_proto.py +0 -0
  113. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_neo_proto_cli.py +0 -0
  114. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_neo_proto_partial.py +0 -0
  115. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v2/test_utils.py +0 -0
  116. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/samples/train.py +0 -0
  117. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_advanced_types.py +0 -0
  118. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_class_level_methods.py +0 -0
  119. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_help_strings.py +0 -0
  120. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_method_self_param.py +0 -0
  121. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_parse_env_template.py +0 -0
  122. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_positional_example.sh +0 -0
  123. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_comments.py +0 -0
  124. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_core.py +0 -0
  125. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_envvar.py +0 -0
  126. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_linebreaking.py +0 -0
  127. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_partial.py +0 -0
  128. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_proto_required.py +0 -0
  129. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_strings.py +0 -0
  130. {params_proto-3.1.2 → params_proto-3.2.1}/tests/test_v3/test_sweep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "params-proto",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Declarative hyperparameter management skills for ML/AI experiments",
5
5
  "author": {
6
6
  "name": "Ge Yang"
@@ -33,6 +33,7 @@
33
33
  <component name="HighlightingSettingsPerFile">
34
34
  <setting file="file://$USER_HOME$/Library/Caches/JetBrains/PyCharm2025.2/python_stubs/-1979844046/_typing.py" root0="SKIP_INSPECTION" />
35
35
  <setting file="file://$PROJECT_DIR$/pyproject.toml" root0="SKIP_INSPECTION" />
36
+ <setting file="file://$PROJECT_DIR$/src/params_proto/envvar.py" root0="SKIP_INSPECTION" />
36
37
  </component>
37
38
  <component name="KubernetesApiPersistence">{}</component>
38
39
  <component name="KubernetesApiProvider">{
@@ -193,10 +194,10 @@
193
194
  <option name="ADD_CONTENT_ROOTS" value="true" />
194
195
  <option name="ADD_SOURCE_ROOTS" value="true" />
195
196
  <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
196
- <option name="RUN_TOOL" value="" />
197
+ <option name="RUN_TOOL" value="true" />
197
198
  <option name="_new_keywords" value="&quot;&quot;" />
198
199
  <option name="_new_parameters" value="&quot;&quot;" />
199
- <option name="_new_additionalArguments" value="&quot;&quot;" />
200
+ <option name="_new_additionalArguments" value="&quot;-n auto&quot;" />
200
201
  <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
201
202
  <option name="_new_targetType" value="&quot;PATH&quot;" />
202
203
  <method v="2" />
@@ -223,8 +224,8 @@
223
224
  <component name="SharedIndexes">
224
225
  <attachedChunks>
225
226
  <set>
226
- <option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.29346.308" />
227
- <option value="bundled-python-sdk-ca5e2b39c7df-6e1f45a539f7-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.29346.308" />
227
+ <option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.30387.127" />
228
+ <option value="bundled-python-sdk-3944b0c99280-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.127" />
228
229
  </set>
229
230
  </attachedChunks>
230
231
  </component>
@@ -255,6 +256,8 @@
255
256
  <workItem from="1768126847510" duration="6729000" />
256
257
  <workItem from="1768382679094" duration="3000" />
257
258
  <workItem from="1769135684123" duration="4247000" />
259
+ <workItem from="1769569263076" duration="1863000" />
260
+ <workItem from="1769933004821" duration="3502000" />
258
261
  </task>
259
262
  <task id="LOCAL-00001" summary="add design specs">
260
263
  <option name="closed" value="true" />
@@ -295,7 +298,7 @@
295
298
  <SUITE FILE_PATH="coverage/params_proto$scratch.coverage" NAME="scratch Coverage Results" MODIFIED="1762422882594" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
296
299
  <SUITE FILE_PATH="coverage/params_proto$demo_params_proto.coverage" NAME="demo_params_proto Coverage Results" MODIFIED="1762230413342" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/scratch" />
297
300
  <SUITE FILE_PATH="coverage/params_proto$pytest_for_tests_test_v3_test_proto_cli_test_proto_cli.coverage" NAME="pytest for tests.test_v3.test_proto_cli.test_proto_cli Coverage Results" MODIFIED="1762386112096" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
298
- <SUITE FILE_PATH="coverage/params_proto$pytest_in_tests.coverage" NAME="pytest in tests Coverage Results" MODIFIED="1767991865711" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
301
+ <SUITE FILE_PATH="coverage/params_proto$pytest_in_tests.coverage" NAME="pytest in tests Coverage Results" MODIFIED="1769934711414" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
299
302
  <SUITE FILE_PATH="coverage/params_proto$pytest_in_test_parse_env_template_py.coverage" NAME="pytest in test_parse_env_template.py Coverage Results" MODIFIED="1762242493577" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
300
303
  <SUITE FILE_PATH="coverage/params_proto$pytest_in_test_v2.coverage" NAME="pytest in test_v2 Coverage Results" MODIFIED="1762232660046" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests/test_v2" />
301
304
  <SUITE FILE_PATH="coverage/params_proto$pytest_in_test_neo_proto_py.coverage" NAME="pytest in test_neo_proto.py Coverage Results" MODIFIED="1762233469671" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: params-proto
3
- Version: 3.1.2
3
+ Version: 3.2.1
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
@@ -36,8 +36,13 @@ class SGD:
36
36
  def train(optimizer: Adam | SGD, epochs: int = 10):
37
37
  pass
38
38
 
39
- # CLI: python train.py --optimizer:Adam --optimizer.lr 0.01
40
- # CLI: python train.py adam --epochs 50
39
+ # CLI: python train.py adam --lr 0.01 --epochs 50
40
+ # CLI: python train.py sgd --momentum 0.95
41
+ ```
42
+
43
+ ```{note}
44
+ **Version 3.2.0+**: Subcommand attributes are **unprefixed by default**.
45
+ Use `--lr` instead of `--optimizer.lr`. The prefixed syntax still works for backwards compatibility.
41
46
  ```
42
47
 
43
48
  ```python
@@ -51,15 +56,16 @@ class Model:
51
56
  def main(model: Model):
52
57
  pass
53
58
 
54
- # CLI: python main.py --model:Model --model.hidden-size 512
55
- # CLI: python main.py model
59
+ # CLI: python main.py model --hidden-size 512
60
+ # CLI: python main.py model --name vit
56
61
  ```
57
62
 
58
63
  ```python
59
- # Pattern 3: Optional simple parameters (workaround)
60
- # Note: Optional[str] is not fully supported; use str with default instead
64
+ # Pattern 3: Optional simple parameters
65
+ from typing import Optional
66
+
61
67
  @proto.cli
62
- def process(checkpoint: str = None, batch_size: int = 32):
68
+ def process(checkpoint: Optional[str] = None, batch_size: int = 32):
63
69
  pass
64
70
 
65
71
  # CLI: python process.py --checkpoint model.pt
@@ -75,17 +81,17 @@ Use Union types when you need to **select which class to instantiate**. This cre
75
81
  First, let's see how users interact with this from the command line:
76
82
 
77
83
  ```bash
78
- # Positional selection (simplest)
79
- python render.py perspective-camera --output scene.png
84
+ # Positional selection with unprefixed attrs (v3.2.0+)
85
+ python render.py perspective-camera --fov 45 --aspect 1.77
80
86
 
81
- # Named selection (explicit)
82
- python render.py --camera:perspective-camera
87
+ # Alternative class
88
+ python render.py orthographic-camera --scale 2.0
83
89
 
84
- # With attribute overrides
85
- python render.py perspective-camera --camera.fov 45 --camera.aspect 1.77
90
+ # With shared parameters
91
+ python render.py perspective-camera --output scene.png
86
92
 
87
- # Alternative class
88
- python render.py --camera:OrthographicCamera --camera.scale 2.0
93
+ # Prefixed syntax still works (backwards compatible)
94
+ python render.py --camera:perspective-camera --camera.fov 45
89
95
  ```
90
96
 
91
97
  Now the implementation:
@@ -121,7 +127,7 @@ if __name__ == "__main__":
121
127
  **How it works:**
122
128
  - `camera` is a **required parameter** with Union type
123
129
  - params-proto instantiates the selected class
124
- - Each class has its own parameter namespace (`--camera.fov`, `--camera.aspect`, etc.)
130
+ - Attributes are unprefixed by default (`--fov`, `--aspect`)
125
131
  - Use `isinstance()` to dispatch on the selected type
126
132
 
127
133
  ## Optional[T]: Simple Optional Parameters
@@ -129,17 +135,18 @@ if __name__ == "__main__":
129
135
  `Optional[T]` is for parameters that **may or may not be provided**:
130
136
 
131
137
  ```python
138
+ from typing import Optional
139
+
132
140
  @proto.cli
133
141
  def train(
134
- checkpoint: str = None, # Works (workaround)
135
- # checkpoint: Optional[str] = None, # ⚠️ Doesn't fully work yet
142
+ checkpoint: Optional[str] = None, # Optional with None default
136
143
  epochs: int = 10,
137
144
  ):
138
145
  """Train model."""
139
146
  pass
140
147
  ```
141
148
 
142
- **Expected CLI usage:**
149
+ **CLI usage:**
143
150
 
144
151
  ```bash
145
152
  python train.py --checkpoint model.pt # Provide value
@@ -147,21 +154,15 @@ python train.py # Omit for None default
147
154
  python train.py --checkpoint model.pt --epochs 50
148
155
  ```
149
156
 
150
- ```{note}
151
- **Current limitation:** `Optional[str]`, `Optional[int]`, etc. are not fully supported.
152
- Use regular parameters with defaults as a workaround:
157
+ Both `Optional[str]` and `str = None` work equivalently:
153
158
 
154
159
  ```python
155
- # Works
160
+ # Both work
156
161
  @proto.cli
157
- def train(checkpoint: str = None, epochs: int = 10):
158
- pass
162
+ def train(checkpoint: Optional[str] = None): ...
159
163
 
160
- # ⚠️ Doesn't work yet
161
164
  @proto.cli
162
- def train(checkpoint: Optional[str] = None, epochs: int = 10):
163
- pass
164
- ```
165
+ def train(checkpoint: str = None): ...
165
166
  ```
166
167
 
167
168
  ## Key Differences
@@ -169,8 +170,8 @@ def train(checkpoint: Optional[str] = None, epochs: int = 10):
169
170
  | Type | Purpose | CLI Syntax | When to Use |
170
171
  |------|---------|-----------|-------------|
171
172
  | `Union[ClassA, ClassB]` | Choose which class instance | `--param:ClassName` or positional | Multiple configurations (optimizers, models, etc.) |
172
- | `Optional[str]` | Value may or may not be provided | `--param value` | Optional simple parameters (**currently use workaround**) |
173
- | `str` with default | Same as Optional | `--param value` | Simple optional parameters (**recommended workaround**) |
173
+ | `Optional[str]` | Value may or may not be provided | `--param value` | Optional simple parameters |
174
+ | `str = None` | Same as Optional | `--param value` | Alternative syntax for optional parameters |
174
175
 
175
176
  ## Syntax Variations
176
177
 
@@ -180,10 +181,10 @@ def train(checkpoint: Optional[str] = None, epochs: int = 10):
180
181
  --camera:PerspectiveCamera # Exact match
181
182
  --camera:perspective-camera # kebab-case
182
183
  --camera:perspectivecamera # lowercase
183
- perspective-camera # Positional
184
+ perspective-camera # Positional (recommended)
184
185
  ```
185
186
 
186
- **Attribute overrides** (kebab-case conversion):
187
+ **Attribute overrides** (v3.2.0+: unprefixed by default):
187
188
 
188
189
  ```python
189
190
  @dataclass
@@ -191,11 +192,37 @@ class Config:
191
192
  batch_size: int = 32 # Python: snake_case
192
193
  learning_rate: float = 0.001
193
194
 
194
- # CLI uses kebab-case
195
+ # CLI uses kebab-case (unprefixed by default)
196
+ --batch-size 64
197
+ --learning-rate 0.01
198
+
199
+ # Prefixed syntax still works
195
200
  --config.batch-size 64
196
201
  --config.learning-rate 0.01
197
202
  ```
198
203
 
204
+ ### Using `@proto.prefix` for Required Prefixes
205
+
206
+ If you want to **require** prefixed syntax for a class (e.g., for disambiguation), decorate it with `@proto.prefix`:
207
+
208
+ ```python
209
+ @proto.prefix # Now requires --config.batch-size syntax
210
+ @dataclass
211
+ class Config:
212
+ batch_size: int = 32
213
+
214
+ @dataclass
215
+ class Model:
216
+ batch_size: int = 16 # Same attr name, no conflict
217
+
218
+ @proto.cli
219
+ def train(config: Config, model: Model):
220
+ pass
221
+
222
+ # CLI: Config requires prefix, Model doesn't
223
+ # python train.py config model --config.batch-size 64 --batch-size 32
224
+ ```
225
+
199
226
  ## Examples
200
227
 
201
228
  ### Example 1: Optimizer Selection
@@ -203,9 +230,11 @@ class Config:
203
230
  Command-line usage:
204
231
 
205
232
  ```bash
206
- # Choose optimizer and override defaults
207
- python train.py adam --optimizer.lr 0.01
208
- python train.py sgd --optimizer.momentum 0.95
233
+ # Choose optimizer and override defaults (unprefixed)
234
+ python train.py adam --lr 0.01
235
+ python train.py sgd --momentum 0.95
236
+
237
+ # Prefixed syntax also works
209
238
  python train.py --optimizer:Adam --optimizer.beta1 0.95
210
239
  ```
211
240
 
@@ -237,10 +266,10 @@ def train(optimizer: Adam | SGD): # Union type selector
237
266
  Command-line usage:
238
267
 
239
268
  ```bash
240
- # Positional class selection
241
- python connect.py database-config --db.host prod.example.com --db.port 3306
269
+ # Positional class selection with unprefixed attrs
270
+ python connect.py database-config --host prod.example.com --port 3306
242
271
 
243
- # Or named selection
272
+ # Or named selection with prefixed attrs
244
273
  python connect.py --db:DatabaseConfig --db.user root
245
274
  ```
246
275
 
@@ -264,11 +293,11 @@ def connect(db: DatabaseConfig): # Single class (still uses Union mechanism)
264
293
  Command-line usage:
265
294
 
266
295
  ```bash
267
- # Union option + shared parameters
268
- python render.py perspective-camera --output scene.png --verbose
296
+ # Union option + shared parameters (unprefixed)
297
+ python render.py perspective-camera --fov 45 --output scene.png --verbose
269
298
 
270
299
  # Different union option with overrides
271
- python render.py --camera:OrthographicCamera --camera.scale 2.0 --verbose
300
+ python render.py orthographic-camera --scale 2.0 --verbose
272
301
 
273
302
  # Help shows all options
274
303
  python render.py --help
@@ -303,6 +332,47 @@ def render(
303
332
 
304
333
  **Key point:** Union parameters are **required**, while other parameters can be optional with defaults.
305
334
 
335
+ ### Example 4: @proto.prefix Classes Require Prefixed Syntax
336
+
337
+ When a Union class is decorated with `@proto.prefix`, its CLI attributes require prefixed syntax:
338
+
339
+ ```python
340
+ @proto.prefix # Requires prefixed syntax
341
+ @dataclass
342
+ class Train:
343
+ lr: float = 0.001
344
+ epochs: int = 100
345
+
346
+ @dataclass # Regular dataclass - unprefixed works
347
+ class Evaluate:
348
+ checkpoint: str = "model.pt"
349
+
350
+ @proto.cli
351
+ def main(mode: Train | Evaluate):
352
+ if isinstance(mode, Train):
353
+ print(f"Training: lr={mode.lr}, epochs={mode.epochs}")
354
+ else:
355
+ print(f"Evaluating: {mode.checkpoint}")
356
+
357
+ if __name__ == "__main__":
358
+ main()
359
+ ```
360
+
361
+ Command-line usage:
362
+
363
+ ```bash
364
+ # Train is @proto.prefix - requires prefixed syntax
365
+ python main.py train --mode.lr 0.01 --mode.epochs 50
366
+
367
+ # Evaluate is regular dataclass - unprefixed works
368
+ python main.py evaluate --checkpoint best.pt
369
+ ```
370
+
371
+ This is useful when:
372
+ - You have multiple Union params with overlapping attribute names
373
+ - You want explicit namespacing for clarity
374
+ - You're using the class as a singleton config elsewhere
375
+
306
376
  ## Related
307
377
 
308
378
  - [Core Concepts](core-concepts) - Three main decorators
@@ -2,6 +2,57 @@
2
2
 
3
3
  This page contains the release history and changelog for params-proto.
4
4
 
5
+ ## Version 3.2.1 (2025-02-01)
6
+
7
+ ### 🐛 Bug Fixes
8
+
9
+ - **Positional Arguments in Subcommands**: Fixed positional arguments being silently ignored after subcommand name
10
+ - Before: `myapp add my-env/v1.2.3` → positional arg silently dropped
11
+ - After: `myapp add my-env/v1.2.3` → positional arg captured by subcommand's required field
12
+ - Enables CLI patterns like `pip install requests`, `cargo add serde`
13
+ - Raises clear error for extra unrecognized positional arguments
14
+
15
+ ### 📚 Documentation
16
+
17
+ - Fixed outdated notes claiming `Optional[str]` was not supported (it works)
18
+
19
+ ---
20
+
21
+ ## Version 3.2.0 (2025-02-01)
22
+
23
+ ### ✨ Features
24
+
25
+ - **Unprefixed CLI Subcommand Attributes**: Subcommand attributes no longer require prefix by default
26
+ - Old: `python train.py train-config --config.epochs 200`
27
+ - New: `python train.py train-config --epochs 200`
28
+ - Prefixed syntax still works for backwards compatibility
29
+
30
+ - **`@proto.prefix` Controls CLI Prefix Requirement**: Classes decorated with `@proto.prefix` require prefixed CLI syntax
31
+ - Regular dataclasses: `--epochs 200` (unprefixed)
32
+ - `@proto.prefix` classes: `--config.epochs 200` (prefixed required)
33
+
34
+ ### 🐛 Bug Fixes
35
+
36
+ - **`isinstance()` for `@proto.prefix` Instances**: Fixed `isinstance(instance, DecoratedClass)` returning `False`
37
+ - Instances of `@proto.prefix` decorated classes now correctly pass isinstance checks
38
+ - Enables proper type checking in Union subcommand handlers
39
+
40
+ ### 🧪 Testing
41
+
42
+ - Added comprehensive nested CLI subcommand tests (`test_nested_cli.py`)
43
+ - Added parallel test execution with `pytest-xdist`
44
+ - Run tests in parallel: `pytest -n auto`
45
+
46
+ ---
47
+
48
+ ## Version 3.1.2 (2025-01-27)
49
+
50
+ ### 📚 Documentation
51
+
52
+ - **Claude Code Plugin**: Fixed install command format in plugin documentation
53
+
54
+ ---
55
+
5
56
  ## Version 3.1.1 (2025-01-25)
6
57
 
7
58
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "params-proto"
3
- version = "3.1.2"
3
+ version = "3.2.1"
4
4
  description = "Modern Hyper Parameter Management for Machine Learning"
5
5
  authors = [
6
6
  { name = "Ge Yang" }
@@ -10,14 +10,14 @@ description: |
10
10
  (6) Work with Union types for subcommand-like CLI patterns
11
11
  ---
12
12
 
13
- # params-proto v3.1.1
13
+ # params-proto v3.2.1
14
14
 
15
15
  Declarative hyperparameter management for ML experiments with automatic CLI generation.
16
16
 
17
17
  ## Installation
18
18
 
19
19
  ```bash
20
- pip install params-proto==3.1.1
20
+ pip install params-proto==3.2.0
21
21
  ```
22
22
 
23
23
  ## Three Decorators
@@ -309,9 +309,39 @@ def main(mode: Train | Evaluate):
309
309
  ```
310
310
 
311
311
  ```bash
312
- python main.py train --lr 0.01
312
+ # v3.2.0+: Attributes are unprefixed by default
313
+ python main.py train --lr 0.01 --epochs 50
314
+ python main.py evaluate --checkpoint best.pt
315
+
316
+ # Prefixed syntax still works (backwards compatible)
317
+ python main.py --mode:train --mode.lr 0.01
318
+ ```
319
+
320
+ ### @proto.prefix Classes Require Prefixed Syntax
321
+
322
+ When a Union class is decorated with `@proto.prefix`, its attributes require prefixed CLI syntax:
323
+
324
+ ```python
325
+ @proto.prefix # Requires prefixed syntax
326
+ @dataclass
327
+ class Train:
328
+ lr: float = 0.001
329
+
330
+ @dataclass # Regular dataclass - unprefixed works
331
+ class Evaluate:
332
+ checkpoint: str = "model.pt"
333
+
334
+ @proto.cli
335
+ def main(mode: Train | Evaluate):
336
+ ...
337
+ ```
338
+
339
+ ```bash
340
+ # Train is @proto.prefix - requires prefix
341
+ python main.py train --mode.lr 0.01
342
+
343
+ # Evaluate is regular - unprefixed works
313
344
  python main.py evaluate --checkpoint best.pt
314
- # Also: --mode:train, --mode:Train, --mode:perspective-camera
315
345
  ```
316
346
 
317
347
  ### Enum Types
@@ -57,6 +57,32 @@ def _normalize_class_name(class_name: str) -> str:
57
57
  return class_name.replace("-", "").replace("_", "").lower()
58
58
 
59
59
 
60
+ def _get_required_fields(cls) -> List[str]:
61
+ """Get list of required field names (fields without defaults) in order."""
62
+ import dataclasses
63
+
64
+ required = []
65
+
66
+ # Check if it's a dataclass
67
+ if dataclasses.is_dataclass(cls):
68
+ for field in dataclasses.fields(cls):
69
+ has_default = (
70
+ field.default is not dataclasses.MISSING
71
+ or field.default_factory is not dataclasses.MISSING
72
+ )
73
+ if not has_default:
74
+ required.append(field.name)
75
+ else:
76
+ # For regular classes, check annotations and class-level defaults
77
+ annotations = getattr(cls, "__annotations__", {})
78
+ for name in annotations:
79
+ if not hasattr(cls, name):
80
+ # No class-level default
81
+ required.append(name)
82
+
83
+ return required
84
+
85
+
60
86
  def _match_class_by_name(name: str, classes: list) -> Union[type, None]:
61
87
  """Match a string to one of the Union classes.
62
88
 
@@ -164,12 +190,31 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
164
190
  is_bool = annotation == bool
165
191
  prefix_params[kebab_key] = (singleton, param_name, annotation, is_bool)
166
192
 
193
+ # Build unprefixed union attribute map for classes NOT decorated with @proto.prefix
194
+ # Maps attr-name -> (union_param_name, attr_name_underscore)
195
+ # Classes in _SINGLETONS are @proto.prefix decorated and require prefixed attrs
196
+ unprefixed_attrs = {}
197
+ for kebab_name, (param_name, union_classes) in union_params.items():
198
+ for cls in union_classes:
199
+ # Skip if class is a @proto.prefix singleton (requires prefixed attrs)
200
+ is_prefix_class = cls in _SINGLETONS.values()
201
+ if is_prefix_class:
202
+ continue
203
+ if hasattr(cls, "__annotations__"):
204
+ for attr_name in cls.__annotations__:
205
+ kebab_attr = attr_name.replace("_", "-")
206
+ # Map to the union param (first one wins if multiple unions have same attr)
207
+ if kebab_attr not in unprefixed_attrs:
208
+ unprefixed_attrs[kebab_attr] = (param_name, attr_name)
209
+
167
210
  # Parse arguments
168
211
  result = {}
169
212
  prefix_values = {} # (singleton, param_name) -> value
170
213
  positional_values = []
171
214
  union_selections = {} # param_name -> selected_class
172
215
  union_attrs = {} # (param_name, attr_name) -> value
216
+ union_positional = {} # param_name -> [positional_args] for subcommand fields
217
+ current_union_param = None # Track which union we're collecting positionals for
173
218
 
174
219
  args = sys.argv[1:]
175
220
  i = 0
@@ -336,6 +381,17 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
336
381
  i += 2
337
382
  continue
338
383
 
384
+ # Check unprefixed union attrs when cli_prefix=False
385
+ if key in unprefixed_attrs:
386
+ union_param_name, attr_name = unprefixed_attrs[key]
387
+ # Get the value
388
+ if i + 1 >= len(args):
389
+ raise SystemExit(f"error: argument --{key} requires a value")
390
+ value_str = args[i + 1]
391
+ union_attrs[(union_param_name, attr_name)] = value_str
392
+ i += 2
393
+ continue
394
+
339
395
  # Unknown argument
340
396
  raise SystemExit(f"error: unrecognized argument: {arg}")
341
397
 
@@ -349,12 +405,18 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
349
405
  selected_class = _match_class_by_name(arg, union_classes)
350
406
  if selected_class:
351
407
  union_selections[param_name] = selected_class
408
+ current_union_param = param_name # Track for following positionals
409
+ union_positional[param_name] = []
352
410
  matched_union = True
353
411
  i += 1
354
412
  break
355
413
 
356
414
  if not matched_union:
357
- positional_values.append(arg)
415
+ # If we have a current union, add positional to its list
416
+ if current_union_param is not None:
417
+ union_positional[current_union_param].append(arg)
418
+ else:
419
+ positional_values.append(arg)
358
420
  i += 1
359
421
 
360
422
  # Assign positional arguments to required parameters
@@ -405,6 +467,33 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
405
467
  # No annotations, treat as string
406
468
  attrs[attr_name] = value_str
407
469
 
470
+ # Assign positional args to required fields of the selected class
471
+ if param_name in union_positional and union_positional[param_name]:
472
+ positionals = union_positional[param_name]
473
+ required_fields = _get_required_fields(selected_class)
474
+
475
+ for field_idx, field_name in enumerate(required_fields):
476
+ if field_name in attrs:
477
+ # Already set by named arg, skip
478
+ continue
479
+ if field_idx < len(positionals):
480
+ # Get type annotation for conversion
481
+ if hasattr(selected_class, "__annotations__"):
482
+ field_type = selected_class.__annotations__.get(field_name, str)
483
+ try:
484
+ attrs[field_name] = _convert_type(positionals[field_idx], field_type)
485
+ except (ValueError, TypeError):
486
+ raise SystemExit(
487
+ f"error: invalid value for {field_name}: {positionals[field_idx]}"
488
+ )
489
+ else:
490
+ attrs[field_name] = positionals[field_idx]
491
+
492
+ # Check for extra positional args
493
+ if len(positionals) > len(required_fields):
494
+ extra = positionals[len(required_fields):]
495
+ raise SystemExit(f"error: unrecognized arguments: {' '.join(extra)}")
496
+
408
497
  # If selected_class is a proto.prefix singleton, merge its overrides
409
498
  from params_proto.proto import _SINGLETONS, ptype
410
499
 
@@ -419,6 +508,14 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
419
508
  attrs[key] = value
420
509
  break
421
510
 
511
+ # Check for missing required fields
512
+ required_fields = _get_required_fields(selected_class)
513
+ for field_name in required_fields:
514
+ if field_name not in attrs:
515
+ raise SystemExit(
516
+ f"error: {selected_class.__name__} requires argument: {field_name}"
517
+ )
518
+
422
519
  # Instantiate the class with collected attributes
423
520
  try:
424
521
  instance = selected_class(**attrs)