From e4897b62ec01e219d098aa54038674a6b9f1c0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Markowski?= Date: Fri, 5 Sep 2025 14:58:48 +0200 Subject: [PATCH] exp: add support for --no-hydra flag Introduces --no-hydra in the CLI of dvc exp run, which disables automatically inserting Hydra config into params.yaml. Fixes #10863 --- dvc/commands/experiments/run.py | 11 +++ dvc/repo/experiments/queue/base.py | 9 ++- dvc/repo/experiments/run.py | 5 +- tests/func/experiments/test_set_params.py | 83 +++++++++++++++++++++-- tests/unit/command/test_experiments.py | 2 + 5 files changed, 102 insertions(+), 8 deletions(-) diff --git a/dvc/commands/experiments/run.py b/dvc/commands/experiments/run.py index dc46a82c25..901ce15cf1 100644 --- a/dvc/commands/experiments/run.py +++ b/dvc/commands/experiments/run.py @@ -20,6 +20,7 @@ def run(self): tmp_dir=self.args.tmp_dir, copy_paths=self.args.copy_paths, message=self.args.message, + no_hydra=self.args.no_hydra, **self._common_kwargs, ) @@ -107,4 +108,14 @@ def _add_run_common(parser): default=None, help="Custom commit message to use when committing the experiment.", ) + parser.add_argument( + "--no-hydra", + action="store_true", + default=False, + help=( + "Disables automatically updating `params.yaml` with Hydra configuration. " + " You can still use `--set-param` to update individual params if needed." + " Default is False." + ), + ) parser.add_argument("-M", dest="message", help=argparse.SUPPRESS) # obsolete diff --git a/dvc/repo/experiments/queue/base.py b/dvc/repo/experiments/queue/base.py index c74c801d54..0312b08e4d 100644 --- a/dvc/repo/experiments/queue/base.py +++ b/dvc/repo/experiments/queue/base.py @@ -287,6 +287,7 @@ def _stash_exp( baseline_rev: Optional[str] = None, branch: Optional[str] = None, name: Optional[str] = None, + no_hydra: bool = False, **kwargs, ) -> QueueEntry: """Stash changes from the workspace as an experiment. @@ -302,6 +303,7 @@ def _stash_exp( name: Optional experiment name. If specified this will be used as the human-readable name in the experiment branch ref. Has no effect of branch is specified. + no_hydra: Disable Hydra from automatically overwriting all params. .. _Hydra Override: https://hydra.cc/docs/next/advanced/override_grammar/basic/ @@ -318,7 +320,7 @@ def _stash_exp( # update experiment params from command line if params: - self._update_params(params) + self._update_params(params, no_hydra=no_hydra) # DVC commit data deps to preserve state across workspace # & tempdir runs @@ -441,12 +443,13 @@ def _format_new_params_msg(new_params, config_path): f"from '{config_path}': {param_list}" ) - def _update_params(self, params: dict[str, list[str]]): + def _update_params(self, params: dict[str, list[str]], no_hydra: bool = False): """Update param files with the provided `Hydra Override`_ patterns. Args: params: Dict mapping paths to `Hydra Override`_ patterns, provided via `exp run --set-param`. + no_hydra: Disable Hydra from automatically overwriting all params. .. _Hydra Override: https://hydra.cc/docs/advanced/override_grammar/basic/ @@ -456,7 +459,7 @@ def _update_params(self, params: dict[str, list[str]]): logger.debug("Using experiment params '%s'", params) hydra_config = self.repo.config.get("hydra", {}) - hydra_enabled = hydra_config.get("enabled", False) + hydra_enabled = hydra_config.get("enabled", False) and not no_hydra hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE for path, overrides in params.items(): if hydra_enabled and path == hydra_output_file: diff --git a/dvc/repo/experiments/run.py b/dvc/repo/experiments/run.py index 3f08e9e835..a51334b38d 100644 --- a/dvc/repo/experiments/run.py +++ b/dvc/repo/experiments/run.py @@ -22,6 +22,7 @@ def run( # noqa: C901, PLR0912 queue: bool = False, copy_paths: Optional[Iterable[str]] = None, message: Optional[str] = None, + no_hydra: bool = False, **kwargs, ) -> dict[str, str]: """Reproduce the specified targets as an experiment. @@ -67,7 +68,7 @@ def run( # noqa: C901, PLR0912 else: path_overrides = {} - hydra_enabled = repo.config.get("hydra", {}).get("enabled", False) + hydra_enabled = repo.config.get("hydra", {}).get("enabled", False) and not no_hydra hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE if hydra_enabled and hydra_output_file not in path_overrides: # Force `_update_params` even if `--set-param` was not used @@ -80,6 +81,7 @@ def run( # noqa: C901, PLR0912 tmp_dir=tmp_dir, copy_paths=copy_paths, message=message, + no_hydra=no_hydra, **kwargs, ) @@ -100,6 +102,7 @@ def run( # noqa: C901, PLR0912 params=sweep_overrides, copy_paths=copy_paths, message=message, + no_hydra=no_hydra, **kwargs, ) if sweep_overrides: diff --git a/tests/func/experiments/test_set_params.py b/tests/func/experiments/test_set_params.py index a479b2ce59..7d3e04af03 100644 --- a/tests/func/experiments/test_set_params.py +++ b/tests/func/experiments/test_set_params.py @@ -26,8 +26,9 @@ def test_modify_params(params_repo, dvc, changes, expected): ("conf", "bar"), ], ) +@pytest.mark.parametrize("no_hydra", [True, False]) def test_hydra_compose_and_dump( - tmp_dir, params_repo, dvc, hydra_enabled, config_dir, config_name + tmp_dir, params_repo, dvc, hydra_enabled, config_dir, config_name, no_hydra ): hydra_setup( tmp_dir, @@ -50,14 +51,14 @@ def test_hydra_compose_and_dump( if config_name is not None: conf["hydra"]["config_name"] = config_name - dvc.experiments.run() + dvc.experiments.run(no_hydra=no_hydra) - if hydra_enabled: + if hydra_enabled and not no_hydra: assert (tmp_dir / "params.yaml").parse() == { "db": {"driver": "mysql", "user": "omry", "pass": "secret"}, } - dvc.experiments.run(params=["db=postgresql"]) + dvc.experiments.run(params=["db=postgresql"], no_hydra=no_hydra) assert (tmp_dir / "params.yaml").parse() == { "db": { "driver": "postgresql", @@ -117,6 +118,7 @@ def test_hydra_sweep( targets=None, copy_paths=None, message=None, + no_hydra=False, ) @@ -137,3 +139,76 @@ def test_hydra_sweep_prefix_name(tmp_dir, params_repo, dvc): exp_names = [entry.name for entry in dvc.experiments.celery_queue.iter_queued()] for name, expected in zip(exp_names, expected_names): assert name == expected + + +def test_mixing_no_hydra_and_params_flags(tmp_dir, params_repo, dvc): + # Passing no_hydra should not prevent user from + # using --set-param on unmodified params.yaml + hydra_setup( + tmp_dir, + config_dir="conf", + config_name="config", + ) + + with dvc.config.edit() as conf: + conf["hydra"]["enabled"] = True + conf["hydra"]["config_dir"] = "conf" + conf["hydra"]["config_name"] = "config" + + dvc.experiments.run(no_hydra=True, params=["goo.bag=10.0"]) + + assert (tmp_dir / "params.yaml").parse() == { + "foo": [{"bar": 1}, {"baz": 2}], + "goo": {"bag": 10.0}, + "lorem": False, + } + + +@pytest.mark.parametrize( + "hydra_enabled,overrides,expected", + [ + ( + True, + ["db=mysql,postgresql"], + [ + {"params.yaml": ["db=mysql"]}, + {"params.yaml": ["db=postgresql"]}, + ], + ), + ( + False, + ["foo=bar,baz"], + [{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}], + ), + ( + False, + [], + [{}], + ), + ], +) +@pytest.mark.parametrize("no_hydra", [True, False]) +def test_mixing_no_hydra_and_sweeps( + tmp_dir, params_repo, dvc, mocker, hydra_enabled, overrides, expected, no_hydra +): + # Passing no_hydra should not prevent user from + # queuing sweeps with --set-param and --queue + patched = mocker.patch.object(dvc.experiments, "queue_one") + + if hydra_enabled: + hydra_setup(tmp_dir, config_dir="conf", config_name="config") + with dvc.config.edit() as conf: + conf["hydra"]["enabled"] = True + + dvc.experiments.run(params=overrides, queue=True, no_hydra=no_hydra) + + assert patched.call_count == len(expected) + for e in expected: + patched.assert_any_call( + mocker.ANY, + params=e, + targets=None, + copy_paths=None, + message=None, + no_hydra=no_hydra, + ) diff --git a/tests/unit/command/test_experiments.py b/tests/unit/command/test_experiments.py index ce51e8a346..3537d39934 100644 --- a/tests/unit/command/test_experiments.py +++ b/tests/unit/command/test_experiments.py @@ -130,6 +130,7 @@ def test_experiments_run(dvc, scm, mocker): "tmp_dir": False, "copy_paths": [], "message": None, + "no_hydra": False, } default_arguments.update(repro_arguments) @@ -151,6 +152,7 @@ def test_experiments_run_message(dvc, scm, mocker, flag): "tmp_dir": False, "copy_paths": [], "message": "mymessage", + "no_hydra": False, } default_arguments.update(repro_arguments)