params-proto 3.1.2__tar.gz → 3.2.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.
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/workspace.xml +15 -6
- {params_proto-3.1.2 → params_proto-3.2.0}/PKG-INFO +1 -1
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/union_types.md +99 -25
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/release_notes.md +35 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/pyproject.toml +1 -1
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto/SKILL.md +2 -2
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto/references/cli-and-types.md +32 -2
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/cli/cli_parse.py +28 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/hyper/sweep.py +71 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/proto.py +28 -16
- params_proto-3.2.0/tests/test_v3/conftest.py +56 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_cli_parsing.py +0 -55
- params_proto-3.2.0/tests/test_v3/test_nested_cli.py +818 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_piter.py +121 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.claude-plugin/marketplace.json +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.claude-plugin/plugin.json +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.editorconfig +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.gitignore +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/.gitignore +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/codeStyles/codeStyleConfig.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/inspectionProfiles/Project_Default.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/markdown.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/misc.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/modules.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/params-proto.iml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/ruff.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.idea/vcs.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.readthedocs.yaml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.run/pytest for test_neo_proto_cli.run.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/.run/pytest in test_params_proto.run.xml +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/ANSI_HELP_CONSIDERATIONS.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/CLAUDE.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/LICENSE.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/Makefile +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/README +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/README.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/Makefile +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/api/hyper.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/api/proto.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/api/utils.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/advanced_features.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/basic_usage.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/environment_variables.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/hyperparameter_sweeps.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/index.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/examples/nested_configs.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_archive_v2/quick_start.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_static/ansi.css +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/_static/custom.css +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/api/index.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/api/proto.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/conf.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/examples/basic_usage.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/examples/cli_applications.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/examples/ml_training.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/examples/rl_agent.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/index.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/advanced_patterns.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/ansi_formatting.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/cli-fundamentals.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/cli-patterns.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/configuration-patterns.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/core-concepts.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/environment_variables.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/help-generation.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/hyperparameter_sweeps.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/naming-conventions.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/parameter-iteration.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/parameter-overrides.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/type-system.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/key_concepts/welcome.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/migration.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/quick_start.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/docs/requirements.txt +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/examples/union_subcommands.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/figures/man-page.png +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/figures/params-proto-autocompletion.gif +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/figures/spec_files.png +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/scratch/demo_params_proto.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/scratch/demo_v3.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/scratch/proto_DAT_scratch.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/scratch/proto_dependency_tree_pattern.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto/references/environment-vars.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto/references/patterns.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto/references/sweeps.md +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/skills/params-proto.skill +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/app.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/cli/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/cli/ansi_help.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/cli/help_gen.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/documentation.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/envvar.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/hyper/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/hyper/proxies.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/parse_env_template.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/type_utils.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v1/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v1/hyper.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v1/params_proto.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v2/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v2/hyper.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v2/partial.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v2/proto.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/src/params_proto/v2/utils.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v1/__init__.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v1/test_hyper.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v1/test_params_proto.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_Eval.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_neo_hyper.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_neo_proto.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_neo_proto_cli.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_neo_proto_partial.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v2/test_utils.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/samples/train.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_advanced_types.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_class_level_methods.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_help_strings.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_method_self_param.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_parse_env_template.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_positional_example.sh +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_comments.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_core.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_envvar.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_linebreaking.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_partial.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_proto_required.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_strings.py +0 -0
- {params_proto-3.1.2 → params_proto-3.2.0}/tests/test_v3/test_sweep.py +0 -0
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
5
|
</component>
|
|
6
6
|
<component name="ChangeListManager">
|
|
7
|
-
<list default="true" id="7a053ece-f497-4c97-ac58-a86c807155ac" name="Changes" comment="add design specs"
|
|
7
|
+
<list default="true" id="7a053ece-f497-4c97-ac58-a86c807155ac" name="Changes" comment="add design specs">
|
|
8
|
+
<change beforePath="$PROJECT_DIR$/src/params_proto/cli/cli_parse.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/params_proto/cli/cli_parse.py" afterDir="false" />
|
|
9
|
+
<change beforePath="$PROJECT_DIR$/src/params_proto/hyper/sweep.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/params_proto/hyper/sweep.py" afterDir="false" />
|
|
10
|
+
<change beforePath="$PROJECT_DIR$/src/params_proto/proto.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/params_proto/proto.py" afterDir="false" />
|
|
11
|
+
<change beforePath="$PROJECT_DIR$/tests/test_v3/test_cli_parsing.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_v3/test_cli_parsing.py" afterDir="false" />
|
|
12
|
+
<change beforePath="$PROJECT_DIR$/tests/test_v3/test_piter.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_v3/test_piter.py" afterDir="false" />
|
|
13
|
+
</list>
|
|
8
14
|
<option name="SHOW_DIALOG" value="false" />
|
|
9
15
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
10
16
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
@@ -33,6 +39,7 @@
|
|
|
33
39
|
<component name="HighlightingSettingsPerFile">
|
|
34
40
|
<setting file="file://$USER_HOME$/Library/Caches/JetBrains/PyCharm2025.2/python_stubs/-1979844046/_typing.py" root0="SKIP_INSPECTION" />
|
|
35
41
|
<setting file="file://$PROJECT_DIR$/pyproject.toml" root0="SKIP_INSPECTION" />
|
|
42
|
+
<setting file="file://$PROJECT_DIR$/src/params_proto/envvar.py" root0="SKIP_INSPECTION" />
|
|
36
43
|
</component>
|
|
37
44
|
<component name="KubernetesApiPersistence">{}</component>
|
|
38
45
|
<component name="KubernetesApiProvider">{
|
|
@@ -193,10 +200,10 @@
|
|
|
193
200
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
194
201
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
195
202
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
|
196
|
-
<option name="RUN_TOOL" value="" />
|
|
203
|
+
<option name="RUN_TOOL" value="true" />
|
|
197
204
|
<option name="_new_keywords" value="""" />
|
|
198
205
|
<option name="_new_parameters" value="""" />
|
|
199
|
-
<option name="_new_additionalArguments" value=""
|
|
206
|
+
<option name="_new_additionalArguments" value=""-n auto"" />
|
|
200
207
|
<option name="_new_target" value=""$PROJECT_DIR$/tests"" />
|
|
201
208
|
<option name="_new_targetType" value=""PATH"" />
|
|
202
209
|
<method v="2" />
|
|
@@ -223,8 +230,8 @@
|
|
|
223
230
|
<component name="SharedIndexes">
|
|
224
231
|
<attachedChunks>
|
|
225
232
|
<set>
|
|
226
|
-
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.
|
|
227
|
-
<option value="bundled-python-sdk-
|
|
233
|
+
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.30387.127" />
|
|
234
|
+
<option value="bundled-python-sdk-3944b0c99280-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.127" />
|
|
228
235
|
</set>
|
|
229
236
|
</attachedChunks>
|
|
230
237
|
</component>
|
|
@@ -255,6 +262,8 @@
|
|
|
255
262
|
<workItem from="1768126847510" duration="6729000" />
|
|
256
263
|
<workItem from="1768382679094" duration="3000" />
|
|
257
264
|
<workItem from="1769135684123" duration="4247000" />
|
|
265
|
+
<workItem from="1769569263076" duration="1863000" />
|
|
266
|
+
<workItem from="1769933004821" duration="1704000" />
|
|
258
267
|
</task>
|
|
259
268
|
<task id="LOCAL-00001" summary="add design specs">
|
|
260
269
|
<option name="closed" value="true" />
|
|
@@ -295,7 +304,7 @@
|
|
|
295
304
|
<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
305
|
<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
306
|
<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="
|
|
307
|
+
<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
308
|
<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
309
|
<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
310
|
<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.
|
|
3
|
+
Version: 3.2.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
|
|
@@ -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
|
|
40
|
-
# CLI: python train.py
|
|
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,8 +56,8 @@ class Model:
|
|
|
51
56
|
def main(model: Model):
|
|
52
57
|
pass
|
|
53
58
|
|
|
54
|
-
# CLI: python main.py
|
|
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
|
|
@@ -75,17 +80,17 @@ Use Union types when you need to **select which class to instantiate**. This cre
|
|
|
75
80
|
First, let's see how users interact with this from the command line:
|
|
76
81
|
|
|
77
82
|
```bash
|
|
78
|
-
# Positional selection (
|
|
79
|
-
python render.py perspective-camera --
|
|
83
|
+
# Positional selection with unprefixed attrs (v3.2.0+)
|
|
84
|
+
python render.py perspective-camera --fov 45 --aspect 1.77
|
|
80
85
|
|
|
81
|
-
#
|
|
82
|
-
python render.py
|
|
86
|
+
# Alternative class
|
|
87
|
+
python render.py orthographic-camera --scale 2.0
|
|
83
88
|
|
|
84
|
-
# With
|
|
85
|
-
python render.py perspective-camera --
|
|
89
|
+
# With shared parameters
|
|
90
|
+
python render.py perspective-camera --output scene.png
|
|
86
91
|
|
|
87
|
-
#
|
|
88
|
-
python render.py --camera:
|
|
92
|
+
# Prefixed syntax still works (backwards compatible)
|
|
93
|
+
python render.py --camera:perspective-camera --camera.fov 45
|
|
89
94
|
```
|
|
90
95
|
|
|
91
96
|
Now the implementation:
|
|
@@ -121,7 +126,7 @@ if __name__ == "__main__":
|
|
|
121
126
|
**How it works:**
|
|
122
127
|
- `camera` is a **required parameter** with Union type
|
|
123
128
|
- params-proto instantiates the selected class
|
|
124
|
-
-
|
|
129
|
+
- Attributes are unprefixed by default (`--fov`, `--aspect`)
|
|
125
130
|
- Use `isinstance()` to dispatch on the selected type
|
|
126
131
|
|
|
127
132
|
## Optional[T]: Simple Optional Parameters
|
|
@@ -180,10 +185,10 @@ def train(checkpoint: Optional[str] = None, epochs: int = 10):
|
|
|
180
185
|
--camera:PerspectiveCamera # Exact match
|
|
181
186
|
--camera:perspective-camera # kebab-case
|
|
182
187
|
--camera:perspectivecamera # lowercase
|
|
183
|
-
perspective-camera # Positional
|
|
188
|
+
perspective-camera # Positional (recommended)
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
**Attribute overrides** (
|
|
191
|
+
**Attribute overrides** (v3.2.0+: unprefixed by default):
|
|
187
192
|
|
|
188
193
|
```python
|
|
189
194
|
@dataclass
|
|
@@ -191,11 +196,37 @@ class Config:
|
|
|
191
196
|
batch_size: int = 32 # Python: snake_case
|
|
192
197
|
learning_rate: float = 0.001
|
|
193
198
|
|
|
194
|
-
# CLI uses kebab-case
|
|
199
|
+
# CLI uses kebab-case (unprefixed by default)
|
|
200
|
+
--batch-size 64
|
|
201
|
+
--learning-rate 0.01
|
|
202
|
+
|
|
203
|
+
# Prefixed syntax still works
|
|
195
204
|
--config.batch-size 64
|
|
196
205
|
--config.learning-rate 0.01
|
|
197
206
|
```
|
|
198
207
|
|
|
208
|
+
### Using `@proto.prefix` for Required Prefixes
|
|
209
|
+
|
|
210
|
+
If you want to **require** prefixed syntax for a class (e.g., for disambiguation), decorate it with `@proto.prefix`:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
@proto.prefix # Now requires --config.batch-size syntax
|
|
214
|
+
@dataclass
|
|
215
|
+
class Config:
|
|
216
|
+
batch_size: int = 32
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class Model:
|
|
220
|
+
batch_size: int = 16 # Same attr name, no conflict
|
|
221
|
+
|
|
222
|
+
@proto.cli
|
|
223
|
+
def train(config: Config, model: Model):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# CLI: Config requires prefix, Model doesn't
|
|
227
|
+
# python train.py config model --config.batch-size 64 --batch-size 32
|
|
228
|
+
```
|
|
229
|
+
|
|
199
230
|
## Examples
|
|
200
231
|
|
|
201
232
|
### Example 1: Optimizer Selection
|
|
@@ -203,9 +234,11 @@ class Config:
|
|
|
203
234
|
Command-line usage:
|
|
204
235
|
|
|
205
236
|
```bash
|
|
206
|
-
# Choose optimizer and override defaults
|
|
207
|
-
python train.py adam --
|
|
208
|
-
python train.py sgd --
|
|
237
|
+
# Choose optimizer and override defaults (unprefixed)
|
|
238
|
+
python train.py adam --lr 0.01
|
|
239
|
+
python train.py sgd --momentum 0.95
|
|
240
|
+
|
|
241
|
+
# Prefixed syntax also works
|
|
209
242
|
python train.py --optimizer:Adam --optimizer.beta1 0.95
|
|
210
243
|
```
|
|
211
244
|
|
|
@@ -237,10 +270,10 @@ def train(optimizer: Adam | SGD): # Union type selector
|
|
|
237
270
|
Command-line usage:
|
|
238
271
|
|
|
239
272
|
```bash
|
|
240
|
-
# Positional class selection
|
|
241
|
-
python connect.py database-config --
|
|
273
|
+
# Positional class selection with unprefixed attrs
|
|
274
|
+
python connect.py database-config --host prod.example.com --port 3306
|
|
242
275
|
|
|
243
|
-
# Or named selection
|
|
276
|
+
# Or named selection with prefixed attrs
|
|
244
277
|
python connect.py --db:DatabaseConfig --db.user root
|
|
245
278
|
```
|
|
246
279
|
|
|
@@ -264,11 +297,11 @@ def connect(db: DatabaseConfig): # Single class (still uses Union mechanism)
|
|
|
264
297
|
Command-line usage:
|
|
265
298
|
|
|
266
299
|
```bash
|
|
267
|
-
# Union option + shared parameters
|
|
268
|
-
python render.py perspective-camera --output scene.png --verbose
|
|
300
|
+
# Union option + shared parameters (unprefixed)
|
|
301
|
+
python render.py perspective-camera --fov 45 --output scene.png --verbose
|
|
269
302
|
|
|
270
303
|
# Different union option with overrides
|
|
271
|
-
python render.py
|
|
304
|
+
python render.py orthographic-camera --scale 2.0 --verbose
|
|
272
305
|
|
|
273
306
|
# Help shows all options
|
|
274
307
|
python render.py --help
|
|
@@ -303,6 +336,47 @@ def render(
|
|
|
303
336
|
|
|
304
337
|
**Key point:** Union parameters are **required**, while other parameters can be optional with defaults.
|
|
305
338
|
|
|
339
|
+
### Example 4: @proto.prefix Classes Require Prefixed Syntax
|
|
340
|
+
|
|
341
|
+
When a Union class is decorated with `@proto.prefix`, its CLI attributes require prefixed syntax:
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
@proto.prefix # Requires prefixed syntax
|
|
345
|
+
@dataclass
|
|
346
|
+
class Train:
|
|
347
|
+
lr: float = 0.001
|
|
348
|
+
epochs: int = 100
|
|
349
|
+
|
|
350
|
+
@dataclass # Regular dataclass - unprefixed works
|
|
351
|
+
class Evaluate:
|
|
352
|
+
checkpoint: str = "model.pt"
|
|
353
|
+
|
|
354
|
+
@proto.cli
|
|
355
|
+
def main(mode: Train | Evaluate):
|
|
356
|
+
if isinstance(mode, Train):
|
|
357
|
+
print(f"Training: lr={mode.lr}, epochs={mode.epochs}")
|
|
358
|
+
else:
|
|
359
|
+
print(f"Evaluating: {mode.checkpoint}")
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Command-line usage:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Train is @proto.prefix - requires prefixed syntax
|
|
369
|
+
python main.py train --mode.lr 0.01 --mode.epochs 50
|
|
370
|
+
|
|
371
|
+
# Evaluate is regular dataclass - unprefixed works
|
|
372
|
+
python main.py evaluate --checkpoint best.pt
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
This is useful when:
|
|
376
|
+
- You have multiple Union params with overlapping attribute names
|
|
377
|
+
- You want explicit namespacing for clarity
|
|
378
|
+
- You're using the class as a singleton config elsewhere
|
|
379
|
+
|
|
306
380
|
## Related
|
|
307
381
|
|
|
308
382
|
- [Core Concepts](core-concepts) - Three main decorators
|
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
This page contains the release history and changelog for params-proto.
|
|
4
4
|
|
|
5
|
+
## Version 3.2.0 (2025-02-01)
|
|
6
|
+
|
|
7
|
+
### ✨ Features
|
|
8
|
+
|
|
9
|
+
- **Unprefixed CLI Subcommand Attributes**: Subcommand attributes no longer require prefix by default
|
|
10
|
+
- Old: `python train.py train-config --config.epochs 200`
|
|
11
|
+
- New: `python train.py train-config --epochs 200`
|
|
12
|
+
- Prefixed syntax still works for backwards compatibility
|
|
13
|
+
|
|
14
|
+
- **`@proto.prefix` Controls CLI Prefix Requirement**: Classes decorated with `@proto.prefix` require prefixed CLI syntax
|
|
15
|
+
- Regular dataclasses: `--epochs 200` (unprefixed)
|
|
16
|
+
- `@proto.prefix` classes: `--config.epochs 200` (prefixed required)
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug Fixes
|
|
19
|
+
|
|
20
|
+
- **`isinstance()` for `@proto.prefix` Instances**: Fixed `isinstance(instance, DecoratedClass)` returning `False`
|
|
21
|
+
- Instances of `@proto.prefix` decorated classes now correctly pass isinstance checks
|
|
22
|
+
- Enables proper type checking in Union subcommand handlers
|
|
23
|
+
|
|
24
|
+
### 🧪 Testing
|
|
25
|
+
|
|
26
|
+
- Added comprehensive nested CLI subcommand tests (`test_nested_cli.py`)
|
|
27
|
+
- Added parallel test execution with `pytest-xdist`
|
|
28
|
+
- Run tests in parallel: `pytest -n auto`
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Version 3.1.2 (2025-01-27)
|
|
33
|
+
|
|
34
|
+
### 📚 Documentation
|
|
35
|
+
|
|
36
|
+
- **Claude Code Plugin**: Fixed install command format in plugin documentation
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
5
40
|
## Version 3.1.1 (2025-01-25)
|
|
6
41
|
|
|
7
42
|
### 🐛 Bug Fixes
|
|
@@ -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.
|
|
13
|
+
# params-proto v3.2.0
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -164,6 +164,23 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
|
|
|
164
164
|
is_bool = annotation == bool
|
|
165
165
|
prefix_params[kebab_key] = (singleton, param_name, annotation, is_bool)
|
|
166
166
|
|
|
167
|
+
# Build unprefixed union attribute map for classes NOT decorated with @proto.prefix
|
|
168
|
+
# Maps attr-name -> (union_param_name, attr_name_underscore)
|
|
169
|
+
# Classes in _SINGLETONS are @proto.prefix decorated and require prefixed attrs
|
|
170
|
+
unprefixed_attrs = {}
|
|
171
|
+
for kebab_name, (param_name, union_classes) in union_params.items():
|
|
172
|
+
for cls in union_classes:
|
|
173
|
+
# Skip if class is a @proto.prefix singleton (requires prefixed attrs)
|
|
174
|
+
is_prefix_class = cls in _SINGLETONS.values()
|
|
175
|
+
if is_prefix_class:
|
|
176
|
+
continue
|
|
177
|
+
if hasattr(cls, "__annotations__"):
|
|
178
|
+
for attr_name in cls.__annotations__:
|
|
179
|
+
kebab_attr = attr_name.replace("_", "-")
|
|
180
|
+
# Map to the union param (first one wins if multiple unions have same attr)
|
|
181
|
+
if kebab_attr not in unprefixed_attrs:
|
|
182
|
+
unprefixed_attrs[kebab_attr] = (param_name, attr_name)
|
|
183
|
+
|
|
167
184
|
# Parse arguments
|
|
168
185
|
result = {}
|
|
169
186
|
prefix_values = {} # (singleton, param_name) -> value
|
|
@@ -336,6 +353,17 @@ def parse_cli_args(wrapper) -> Dict[str, Any]:
|
|
|
336
353
|
i += 2
|
|
337
354
|
continue
|
|
338
355
|
|
|
356
|
+
# Check unprefixed union attrs when cli_prefix=False
|
|
357
|
+
if key in unprefixed_attrs:
|
|
358
|
+
union_param_name, attr_name = unprefixed_attrs[key]
|
|
359
|
+
# Get the value
|
|
360
|
+
if i + 1 >= len(args):
|
|
361
|
+
raise SystemExit(f"error: argument --{key} requires a value")
|
|
362
|
+
value_str = args[i + 1]
|
|
363
|
+
union_attrs[(union_param_name, attr_name)] = value_str
|
|
364
|
+
i += 2
|
|
365
|
+
continue
|
|
366
|
+
|
|
339
367
|
# Unknown argument
|
|
340
368
|
raise SystemExit(f"error: unrecognized argument: {arg}")
|
|
341
369
|
|
|
@@ -143,6 +143,77 @@ class ParameterIterator:
|
|
|
143
143
|
"""Return number of configs (materializes list)."""
|
|
144
144
|
return len(self.list)
|
|
145
145
|
|
|
146
|
+
def save(self, filename="sweep.jsonl", overwrite=True, verbose=True):
|
|
147
|
+
"""
|
|
148
|
+
Save parameter configurations to JSONL file.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
filename: Path to output file (str or PathLike)
|
|
152
|
+
overwrite: If True, overwrite existing file; if False, append
|
|
153
|
+
verbose: If True, print save confirmation
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
configs = piter @ {"lr": [0.001, 0.01]} * {"batch_size": [32, 64]}
|
|
157
|
+
configs.save("experiment.jsonl")
|
|
158
|
+
"""
|
|
159
|
+
import json
|
|
160
|
+
import os
|
|
161
|
+
from urllib import parse
|
|
162
|
+
|
|
163
|
+
# Convert Path objects to string
|
|
164
|
+
filename_str = os.fspath(filename) if hasattr(os, "fspath") else str(filename)
|
|
165
|
+
configs = self.list
|
|
166
|
+
|
|
167
|
+
with open(filename_str, "w" if overwrite else "a+") as f:
|
|
168
|
+
for item in configs:
|
|
169
|
+
f.write(json.dumps(item) + "\n")
|
|
170
|
+
|
|
171
|
+
if verbose:
|
|
172
|
+
try:
|
|
173
|
+
from termcolor import colored as c
|
|
174
|
+
|
|
175
|
+
print(
|
|
176
|
+
c("saved", "blue"),
|
|
177
|
+
c(len(configs), "green"),
|
|
178
|
+
c("items to", "blue"),
|
|
179
|
+
filename_str,
|
|
180
|
+
".",
|
|
181
|
+
"file://" + parse.quote(os.path.realpath(filename_str)),
|
|
182
|
+
)
|
|
183
|
+
except ImportError:
|
|
184
|
+
print(f"Saved {len(configs)} items to {filename_str}")
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def load(filename="sweep.jsonl"):
|
|
188
|
+
"""
|
|
189
|
+
Load parameter configurations from JSONL file.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
filename: Path to input file (str or PathLike)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
ParameterIterator with loaded configurations
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
configs = ParameterIterator.load("experiment.jsonl")
|
|
199
|
+
for config in configs:
|
|
200
|
+
run_experiment(**config)
|
|
201
|
+
"""
|
|
202
|
+
import json
|
|
203
|
+
import os
|
|
204
|
+
|
|
205
|
+
# Convert Path objects to string
|
|
206
|
+
filename_str = os.fspath(filename) if hasattr(os, "fspath") else str(filename)
|
|
207
|
+
|
|
208
|
+
configs = []
|
|
209
|
+
with open(filename_str, "r") as f:
|
|
210
|
+
for line in f:
|
|
211
|
+
line = line.strip()
|
|
212
|
+
if line and not line.startswith("//"):
|
|
213
|
+
configs.append(json.loads(line))
|
|
214
|
+
|
|
215
|
+
return ParameterIterator(iter(configs))
|
|
216
|
+
|
|
146
217
|
|
|
147
218
|
class PiterFactory:
|
|
148
219
|
"""
|
|
@@ -669,20 +669,32 @@ class ptype(type):
|
|
|
669
669
|
|
|
670
670
|
# Get the original class
|
|
671
671
|
original_cls = type.__getattribute__(cls, "__proto_original_class__")
|
|
672
|
+
annotations = getattr(cls, "__proto_annotations__", {})
|
|
672
673
|
|
|
673
|
-
#
|
|
674
|
-
|
|
674
|
+
# Check if this is a dataclass (has generated __init__ that accepts kwargs)
|
|
675
|
+
is_dataclass = hasattr(original_cls, "__dataclass_fields__")
|
|
675
676
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
677
|
+
if is_dataclass:
|
|
678
|
+
# For dataclasses: use the constructor directly
|
|
679
|
+
instance = original_cls(**final_kwargs)
|
|
680
|
+
else:
|
|
681
|
+
# For regular classes: create instance and set attributes manually
|
|
682
|
+
instance = object.__new__(original_cls)
|
|
683
|
+
for name in annotations.keys():
|
|
684
|
+
if name in final_kwargs:
|
|
685
|
+
setattr(instance, name, final_kwargs[name])
|
|
686
|
+
elif hasattr(cls, "__proto_defaults__") and name in cls.__proto_defaults__:
|
|
687
|
+
setattr(instance, name, cls.__proto_defaults__[name])
|
|
688
|
+
else:
|
|
689
|
+
# Required field
|
|
690
|
+
setattr(instance, name, None)
|
|
691
|
+
# Call __post_init__ if defined (dataclasses call it in __init__)
|
|
692
|
+
if hasattr(instance, '__post_init__'):
|
|
693
|
+
instance.__post_init__()
|
|
694
|
+
|
|
695
|
+
# Update the instance's class to the decorated class
|
|
696
|
+
# This allows isinstance(instance, DecoratedClass) to work
|
|
697
|
+
object.__setattr__(instance, "__class__", cls)
|
|
686
698
|
|
|
687
699
|
# Copy methods from original class and wrap to return self
|
|
688
700
|
for name in dir(original_cls):
|
|
@@ -722,10 +734,6 @@ class ptype(type):
|
|
|
722
734
|
|
|
723
735
|
setattr(instance, name, make_wrapper(method))
|
|
724
736
|
|
|
725
|
-
# Call __post_init__ if defined (like dataclasses)
|
|
726
|
-
if hasattr(instance, '__post_init__'):
|
|
727
|
-
instance.__post_init__()
|
|
728
|
-
|
|
729
737
|
return instance
|
|
730
738
|
|
|
731
739
|
|
|
@@ -1013,6 +1021,10 @@ def cli(obj: Any = None, *, prog: str = None):
|
|
|
1013
1021
|
"""
|
|
1014
1022
|
Set up an object as a CLI entry point.
|
|
1015
1023
|
|
|
1024
|
+
By default, subcommand attributes don't require prefix (--epochs works).
|
|
1025
|
+
If the subcommand class is decorated with @proto.prefix, prefix is required
|
|
1026
|
+
(--config.epochs).
|
|
1027
|
+
|
|
1016
1028
|
Args:
|
|
1017
1029
|
obj: The class, function, or Union type to setup as CLI.
|
|
1018
1030
|
If None, returns a decorator.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Shared fixtures for v3 CLI tests."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def run_cli():
|
|
13
|
+
"""Fixture for running CLI scripts in shell."""
|
|
14
|
+
|
|
15
|
+
def _run(script_content: str, args: list[str] = None, expect_error: bool = False):
|
|
16
|
+
"""
|
|
17
|
+
Run a Python script with CLI arguments.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
script_content: Python code to execute
|
|
21
|
+
args: List of command-line arguments
|
|
22
|
+
expect_error: If True, expect non-zero exit code
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
dict with keys: stdout, stderr, returncode
|
|
26
|
+
"""
|
|
27
|
+
args = args or []
|
|
28
|
+
|
|
29
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
30
|
+
f.write(script_content)
|
|
31
|
+
script_path = f.name
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
[sys.executable, script_path] + args,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=5,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not expect_error and result.returncode != 0:
|
|
42
|
+
pytest.fail(
|
|
43
|
+
f"Script failed with exit code {result.returncode}\n"
|
|
44
|
+
f"stdout: {result.stdout}\n"
|
|
45
|
+
f"stderr: {result.stderr}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"stdout": result.stdout,
|
|
50
|
+
"stderr": result.stderr,
|
|
51
|
+
"returncode": result.returncode,
|
|
52
|
+
}
|
|
53
|
+
finally:
|
|
54
|
+
Path(script_path).unlink(missing_ok=True)
|
|
55
|
+
|
|
56
|
+
return _run
|