Repository: iterative/dvc

Issue:
exp run: Incorrect string handling
# Bug Report

<!--
## Issue name

Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.

Example: `repro: doesn't detect input changes`
-->

## Description

DVC with hydra composition does not write quotes in the strings to `param.yaml` and changes type of parameter from a string to a bool when a string is `'no'`
<!--
A clear and concise description of what the bug is.
-->

### Reproduce

<!--
Step list of how to reproduce the bug
-->

<!--
Example:
-->


1. [Enable hydra composition](https://dvc.org/doc/user-guide/experiment-management/hydra-composition)
2. Create simple config `conf/config.yaml`: 
```yaml
param_as_string: 'no'
```
3. Create `dvc.yaml`:
```yaml
stages:
  test:
    cmd: "python --version"
```
4. `dvc exp run`

### Expected

<!--
A clear and concise description of what you expect to happen.
-->

`params.yaml`:
```yaml
param_as_string: 'no' # string type
```
### Actual

`params.yaml`:
```yaml
param_as_string: no # bool type
```


### Environment information

<!--
This is required to ensure that we can reproduce the bug.
-->

**Output of `dvc doctor`:**

```console
DVC version: 2.34.2 (pip)
---------------------------------
Platform: Python 3.9.13 on Linux-5.15.0-52-generic-x86_64-with-glibc2.31
Subprojects:
        dvc_data = 0.25.3
        dvc_objects = 0.12.2
        dvc_render = 0.0.12
        dvc_task = 0.1.5
        dvclive = 1.0.1
        scmrepo = 0.1.3
Supports:
        http (aiohttp = 3.8.3, aiohttp-retry = 2.8.3),
        https (aiohttp = 3.8.3, aiohttp-retry = 2.8.3),
        s3 (s3fs = 2022.10.0, boto3 = 1.24.59)
Cache types: hardlink, symlink
Cache directory: ext4 on /dev/nvme2n1p2
Caches: local
Remotes: s3
Workspace directory: ext4 on /dev/nvme2n1p2
Repo: dvc, git
```


<!--
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
-->



Comments:
Hi @KernelA , thanks for the report. It should be fixed with #8587

Patch:
diff --git a/dvc/utils/hydra.py b/dvc/utils/hydra.py
--- a/dvc/utils/hydra.py
+++ b/dvc/utils/hydra.py
@@ -54,8 +54,12 @@ def compose_and_dump(
     with initialize_config_dir(config_dir, version_base=None):
         cfg = compose(config_name=config_name, overrides=overrides)
 
-    dumper = DUMPERS[Path(output_file).suffix.lower()]
-    dumper(output_file, OmegaConf.to_object(cfg))
+    suffix = Path(output_file).suffix.lower()
+    if suffix not in [".yml", ".yaml"]:
+        dumper = DUMPERS[suffix]
+        dumper(output_file, OmegaConf.to_object(cfg))
+    else:
+        Path(output_file).write_text(OmegaConf.to_yaml(cfg), encoding="utf-8")
 
 
 def apply_overrides(path: "StrPath", overrides: List[str]) -> None:


Test patch:
diff --git a/tests/func/utils/test_hydra.py b/tests/func/utils/test_hydra.py
--- a/tests/func/utils/test_hydra.py
+++ b/tests/func/utils/test_hydra.py
@@ -174,12 +174,25 @@ def test_compose_and_dump(tmp_dir, suffix, overrides, expected):
     from dvc.utils.hydra import compose_and_dump
 
     config_name = "config"
-    config_dir = hydra_setup(tmp_dir, "conf", "config")
     output_file = tmp_dir / f"params.{suffix}"
+    config_dir = hydra_setup(tmp_dir, "conf", "config")
     compose_and_dump(output_file, config_dir, config_name, overrides)
     assert output_file.parse() == expected
 
 
+@pytest.mark.skipif(sys.version_info >= (3, 11), reason="unsupported on 3.11")
+def test_compose_and_dump_yaml_handles_string(tmp_dir):
+    """Regression test for 8583"""
+    from dvc.utils.hydra import compose_and_dump
+
+    config = tmp_dir / "conf" / "config.yaml"
+    config.parent.mkdir()
+    config.write_text("foo: 'no'\n")
+    output_file = tmp_dir / "params.yaml"
+    compose_and_dump(output_file, str(config.parent), "config", [])
+    assert output_file.read_text() == "foo: 'no'\n"
+
+
 @pytest.mark.parametrize(
     "overrides, expected",
     [