Feature/hydra config modules (#9783)

* hydra: enable config modules

Lets the user specify Hydra configs in installed Python modules as well
as inside directories. Adds `hydra.config_module` to `.dvc/config`.

Fixes #9740

old tests green

tests for config modules

* hydra: fix linter errors

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* hydra: fix mypy error

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* hydra: fix tests

* Update dvc/utils/hydra.py

Co-authored-by: David de la Iglesia Castro <[email protected]>

* address comments

* convince mypy not to worry

* split long line

* fix test

* check for raised error in test

---------

Co-authored-by: Dom Miketa <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: David de la Iglesia Castro <[email protected]>
This commit is contained in:
Dom Miketa
2023-08-06 10:39:09 +02:00
committed by GitHub
co-authored by David de la Iglesia Castro Dom Miketa pre-commit-ci[bot]
parent 43b371c43a
commit 009b238807
4 changed files with 86 additions and 12 deletions
+3 -1
View File
@@ -8,6 +8,7 @@ from voluptuous import (
All, All,
Any, Any,
Coerce, Coerce,
Exclusive,
Invalid, Invalid,
Lower, Lower,
Optional, Optional,
@@ -324,7 +325,8 @@ SCHEMA = {
}, },
"hydra": { "hydra": {
Optional("enabled", default=False): Bool, Optional("enabled", default=False): Bool,
"config_dir": str, Exclusive("config_dir", "config_source"): str,
Exclusive("config_module", "config_source"): str,
"config_name": str, "config_name": str,
}, },
"studio": { "studio": {
+7 -3
View File
@@ -476,13 +476,17 @@ class BaseStashQueue(ABC):
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
for path, overrides in params.items(): for path, overrides in params.items():
if hydra_enabled and path == hydra_output_file: if hydra_enabled and path == hydra_output_file:
config_dir = os.path.join( if (config_module := hydra_config.get("config_module")) is None:
self.repo.root_dir, hydra_config.get("config_dir", "conf") config_dir = os.path.join(
) self.repo.root_dir, hydra_config.get("config_dir", "conf")
)
else:
config_dir = None
config_name = hydra_config.get("config_name", "config") config_name = hydra_config.get("config_name", "config")
compose_and_dump( compose_and_dump(
path, path,
config_dir, config_dir,
config_module,
config_name, config_name,
overrides, overrides,
) )
+16 -4
View File
@@ -1,6 +1,6 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List, Optional
from dvc.exceptions import InvalidArgumentError from dvc.exceptions import InvalidArgumentError
@@ -15,7 +15,8 @@ logger = logging.getLogger(__name__)
def compose_and_dump( def compose_and_dump(
output_file: "StrPath", output_file: "StrPath",
config_dir: str, config_dir: Optional[str],
config_module: Optional[str],
config_name: str, config_name: str,
overrides: List[str], overrides: List[str],
) -> None: ) -> None:
@@ -25,6 +26,8 @@ def compose_and_dump(
output_file: File where the composed config will be dumped. output_file: File where the composed config will be dumped.
config_dir: Folder containing the Hydra config files. config_dir: Folder containing the Hydra config files.
Must be absolute file system path. Must be absolute file system path.
config_module: Module containing the Hydra config files.
Ignored if `config_dir` is not `None`.
config_name: Name of the config file containing defaults, config_name: Name of the config file containing defaults,
without the .yaml extension. without the .yaml extension.
overrides: List of `Hydra Override`_ patterns. overrides: List of `Hydra Override`_ patterns.
@@ -32,12 +35,21 @@ def compose_and_dump(
.. _Hydra Override: .. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/ https://hydra.cc/docs/advanced/override_grammar/basic/
""" """
from hydra import compose, initialize_config_dir from hydra import compose, initialize_config_dir, initialize_config_module
from omegaconf import OmegaConf from omegaconf import OmegaConf
from .serialize import DUMPERS from .serialize import DUMPERS
with initialize_config_dir(config_dir, version_base=None): config_source = config_dir or config_module
if not config_source:
raise ValueError("Either `config_dir` or `config_module` should be provided.")
initialize_config = (
initialize_config_dir if config_dir else initialize_config_module
)
with initialize_config( # type: ignore[attr-defined]
config_source, version_base=None
):
cfg = compose(config_name=config_name, overrides=overrides) cfg = compose(config_name=config_name, overrides=overrides)
OmegaConf.resolve(cfg) OmegaConf.resolve(cfg)
+60 -4
View File
@@ -1,3 +1,5 @@
from contextlib import nullcontext as does_not_raise
import pytest import pytest
from dvc.exceptions import InvalidArgumentError from dvc.exceptions import InvalidArgumentError
@@ -167,16 +169,70 @@ def hydra_setup(tmp_dir, config_dir, config_name):
), ),
], ],
) )
def test_compose_and_dump(tmp_dir, suffix, overrides, expected): def test_compose_and_dump_overrides(tmp_dir, suffix, overrides, expected):
from dvc.utils.hydra import compose_and_dump from dvc.utils.hydra import compose_and_dump
config_name = "config" config_name = "config"
output_file = tmp_dir / f"params.{suffix}" output_file = tmp_dir / f"params.{suffix}"
config_dir = hydra_setup(tmp_dir, "conf", "config") config_dir = hydra_setup(tmp_dir, "conf", "config")
compose_and_dump(output_file, config_dir, config_name, overrides) config_module = None
compose_and_dump(output_file, config_dir, config_module, config_name, overrides)
assert output_file.parse() == expected assert output_file.parse() == expected
def hydra_setup_dir_basic(tmp_dir, config_subdir, config_name, config_content):
if config_subdir is None:
return None
config_dir = tmp_dir / config_subdir
config_dir.mkdir()
(config_dir / f"{config_name}.yaml").dump(config_content)
return str(config_dir)
@pytest.mark.parametrize(
"config_subdir,config_module,config_content,error_context",
[
("conf", None, {"normal_yaml_config": False}, does_not_raise()),
(
None,
"hydra.test_utils.configs",
{"normal_yaml_config": True},
does_not_raise(),
),
(
"conf",
"hydra.test_utils.configs",
{"normal_yaml_config": False},
does_not_raise(),
),
(
None,
None,
None,
pytest.raises(
ValueError,
match="Either `config_dir` or `config_module` should be provided.",
),
),
],
)
def test_compose_and_dump_dir_module(
tmp_dir, config_subdir, config_module, config_content, error_context
):
from dvc.utils.hydra import compose_and_dump
output_file = tmp_dir / "params.yaml"
config_name = "config"
config_dir = hydra_setup_dir_basic(
tmp_dir, config_subdir, config_name, config_content
)
with error_context:
compose_and_dump(output_file, config_dir, config_module, config_name, [])
assert output_file.parse() == config_content
def test_compose_and_dump_yaml_handles_string(tmp_dir): def test_compose_and_dump_yaml_handles_string(tmp_dir):
"""Regression test for https://github.com/iterative/dvc/issues/8583""" """Regression test for https://github.com/iterative/dvc/issues/8583"""
from dvc.utils.hydra import compose_and_dump from dvc.utils.hydra import compose_and_dump
@@ -185,7 +241,7 @@ def test_compose_and_dump_yaml_handles_string(tmp_dir):
config.parent.mkdir() config.parent.mkdir()
config.write_text("foo: 'no'\n") config.write_text("foo: 'no'\n")
output_file = tmp_dir / "params.yaml" output_file = tmp_dir / "params.yaml"
compose_and_dump(output_file, str(config.parent), "config", []) compose_and_dump(output_file, str(config.parent), None, "config", [])
assert output_file.read_text() == "foo: 'no'\n" assert output_file.read_text() == "foo: 'no'\n"
@@ -197,7 +253,7 @@ def test_compose_and_dump_resolves_interpolation(tmp_dir):
config.parent.mkdir() config.parent.mkdir()
config.dump({"data": {"root": "path/to/root", "raw": "${.root}/raw"}}) config.dump({"data": {"root": "path/to/root", "raw": "${.root}/raw"}})
output_file = tmp_dir / "params.yaml" output_file = tmp_dir / "params.yaml"
compose_and_dump(output_file, str(config.parent), "config", []) compose_and_dump(output_file, str(config.parent), None, "config", [])
assert output_file.parse() == { assert output_file.parse() == {
"data": {"root": "path/to/root", "raw": "path/to/root/raw"} "data": {"root": "path/to/root", "raw": "path/to/root/raw"}
} }