Repository: iterative/dvc

Issue:
run-cache: fails for params
```sh
$ echo "train: 1\nlr: 2" > params.yaml
$ dvc run  -n test -p train,lr -o params2.yaml "cat params.yaml > params2.yaml"
ERROR: failed to reproduce 'dvc.yaml': 'deps'
```
Verbose log:
```sh
------------------------------------------------------------
Traceback (most recent call last):
  File "/home/saugat/repos/iterative/dvc/dvc/main.py", line 48, in main
    ret = cmd.run()
  File "/home/saugat/repos/iterative/dvc/dvc/command/run.py", line 51, in run
    single_stage=self.args.single_stage,
  File "/home/saugat/repos/iterative/dvc/dvc/repo/__init__.py", line 25, in wrapper
    ret = f(repo, *args, **kwargs)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/scm_context.py", line 4, in run
    result = method(repo, *args, **kw)
  File "/home/saugat/repos/iterative/dvc/dvc/repo/run.py", line 88, in run
    run_cache=kwargs.get("run_cache", True),
  File "/home/saugat/repos/iterative/dvc/.env/py36/lib/python3.6/site-packages/funcy/decorators.py", line 39, in wrapper
    return deco(call, *dargs, **dkwargs)
  File "/home/saugat/repos/iterative/dvc/dvc/stage/decorators.py", line 35, in rwlocked
    return call()
  File "/home/saugat/repos/iterative/dvc/.env/py36/lib/python3.6/site-packages/funcy/decorators.py", line 60, in __call__
    return self._func(*self._args, **self._kwargs)
  File "/home/saugat/repos/iterative/dvc/dvc/stage/__init__.py", line 417, in run
    self.save()
  File "/home/saugat/repos/iterative/dvc/dvc/stage/__init__.py", line 368, in save
    self.repo.stage_cache.save(self)
  File "/home/saugat/repos/iterative/dvc/dvc/stage/cache.py", line 130, in save
    for out in self._uncached_outs(stage, cache):
  File "/home/saugat/repos/iterative/dvc/dvc/stage/cache.py", line 107, in _uncached_outs
    cached_stage = self._create_stage(cache)
  File "/home/saugat/repos/iterative/dvc/dvc/stage/cache.py", line 88, in _create_stage
    deps=[dep["path"] for dep in cache["deps"]],
KeyError: 'deps'
```




Discussion:

Patch:
diff --git a/dvc/output/base.py b/dvc/output/base.py
index fef7e592c..edaf3bdce 100644
--- a/dvc/output/base.py
+++ b/dvc/output/base.py
@@ -32,8 +32,8 @@ class OutputIsNotFileOrDirError(DvcException):
 class OutputAlreadyTrackedError(DvcException):
     def __init__(self, path):
         msg = f""" output '{path}' is already tracked by SCM (e.g. Git).
-    You can remove it from git, then add to DVC.
-        To stop tracking from git:
+    You can remove it from Git, then add to DVC.
+        To stop tracking from Git:
             git rm -r --cached '{path}'
             git commit -m "stop tracking {path}" """
         super().__init__(msg)
diff --git a/dvc/stage/cache.py b/dvc/stage/cache.py
index 7a9bd216b..63c326fab 100644
--- a/dvc/stage/cache.py
+++ b/dvc/stage/cache.py
@@ -80,12 +80,26 @@ class StageCache:
     def _create_stage(self, cache):
         from dvc.stage import create_stage, PipelineStage
 
+        params = []
+        for param in cache.get("params", []):
+            if isinstance(param, str):
+                params.append(param)
+                continue
+
+            assert isinstance(param, dict)
+            assert len(param) == 1
+            path = list(param.keys())[0]
+            params_list = param[path]
+            assert isinstance(params_list, list)
+            params.append(f"{path}:" + ",".join(params_list))
+
         stage = create_stage(
             PipelineStage,
             repo=self.repo,
             path="dvc.yaml",
             cmd=cache["cmd"],
-            deps=[dep["path"] for dep in cache["deps"]],
+            params=params,
+            deps=[dep["path"] for dep in cache.get("deps", [])],
             outs=[out["path"] for out in cache["outs"]],
         )
         StageLoader.fill_from_lock(stage, cache)


Test patch:
diff --git a/tests/unit/stage/test_cache.py b/tests/unit/stage/test_cache.py
new file mode 100644
index 000000000..7b923c630
--- /dev/null
+++ b/tests/unit/stage/test_cache.py
@@ -0,0 +1,106 @@
+import os
+
+
+def test_stage_cache(tmp_dir, dvc, run_copy, mocker):
+    tmp_dir.gen("dep", "dep")
+    tmp_dir.gen(
+        "script.py",
+        (
+            'open("out", "w+").write("out"); '
+            'open("out_no_cache", "w+").write("out_no_cache")'
+        ),
+    )
+    stage = dvc.run(
+        cmd="python script.py",
+        deps=["script.py", "dep"],
+        outs=["out"],
+        outs_no_cache=["out_no_cache"],
+        single_stage=True,
+    )
+
+    with dvc.lock, dvc.state:
+        stage.remove(remove_outs=True, force=True)
+
+    assert not (tmp_dir / "out").exists()
+    assert not (tmp_dir / "out_no_cache").exists()
+    assert not (tmp_dir / "out.dvc").exists()
+
+    cache_dir = os.path.join(
+        dvc.stage_cache.cache_dir,
+        "10",
+        "10b45372fdf4ec14d3f779c5b256378d7a12780e4c7f549a44138e492f098bfe",
+    )
+    cache_file = os.path.join(
+        cache_dir,
+        "bb32e04c6da96a7192513390acedbe4cd6123f8fe5b0ba5fffe39716fe87f6f4",
+    )
+
+    assert os.path.isdir(cache_dir)
+    assert os.listdir(cache_dir) == [os.path.basename(cache_file)]
+    assert os.path.isfile(cache_file)
+
+    run_spy = mocker.patch("dvc.stage.run.cmd_run")
+    checkout_spy = mocker.spy(stage, "checkout")
+    with dvc.lock, dvc.state:
+        stage.run()
+
+    assert not run_spy.called
+    assert checkout_spy.call_count == 1
+
+    assert (tmp_dir / "out").exists()
+    assert (tmp_dir / "out_no_cache").exists()
+    assert (tmp_dir / "out").read_text() == "out"
+    assert (tmp_dir / "out_no_cache").read_text() == "out_no_cache"
+
+
+def test_stage_cache_params(tmp_dir, dvc, run_copy, mocker):
+    tmp_dir.gen("params.yaml", "foo: 1\nbar: 2")
+    tmp_dir.gen("myparams.yaml", "baz: 3\nqux: 4")
+    tmp_dir.gen(
+        "script.py",
+        (
+            'open("out", "w+").write("out"); '
+            'open("out_no_cache", "w+").write("out_no_cache")'
+        ),
+    )
+    stage = dvc.run(
+        cmd="python script.py",
+        params=["foo,bar", "myparams.yaml:baz,qux"],
+        outs=["out"],
+        outs_no_cache=["out_no_cache"],
+        single_stage=True,
+    )
+
+    with dvc.lock, dvc.state:
+        stage.remove(remove_outs=True, force=True)
+
+    assert not (tmp_dir / "out").exists()
+    assert not (tmp_dir / "out_no_cache").exists()
+    assert not (tmp_dir / "out.dvc").exists()
+
+    cache_dir = os.path.join(
+        dvc.stage_cache.cache_dir,
+        "65",
+        "651d0a5b82e05e48b03acf44954f6a8599760e652a143d517a17d1065eca61a1",
+    )
+    cache_file = os.path.join(
+        cache_dir,
+        "2196a5a4dd24c5759437511fcf9d6aa66b259e1dac58e3f212aefd1797a6f114",
+    )
+
+    assert os.path.isdir(cache_dir)
+    assert os.listdir(cache_dir) == [os.path.basename(cache_file)]
+    assert os.path.isfile(cache_file)
+
+    run_spy = mocker.patch("dvc.stage.run.cmd_run")
+    checkout_spy = mocker.spy(stage, "checkout")
+    with dvc.lock, dvc.state:
+        stage.run()
+
+    assert not run_spy.called
+    assert checkout_spy.call_count == 1
+
+    assert (tmp_dir / "out").exists()
+    assert (tmp_dir / "out_no_cache").exists()
+    assert (tmp_dir / "out").read_text() == "out"
+    assert (tmp_dir / "out_no_cache").read_text() == "out_no_cache"
diff --git a/tests/unit/stage/test_stage.py b/tests/unit/stage/test_stage.py
index 05e308aed..1419db418 100644
--- a/tests/unit/stage/test_stage.py
+++ b/tests/unit/stage/test_stage.py
@@ -102,55 +102,3 @@ def test_always_changed(dvc):
     with dvc.lock:
         assert stage.changed()
         assert stage.status()["path"] == ["always changed"]
-
-
-def test_stage_cache(tmp_dir, dvc, run_copy, mocker):
-    tmp_dir.gen("dep", "dep")
-    tmp_dir.gen(
-        "script.py",
-        (
-            'open("out", "w+").write("out"); '
-            'open("out_no_cache", "w+").write("out_no_cache")'
-        ),
-    )
-    stage = dvc.run(
-        cmd="python script.py",
-        deps=["script.py", "dep"],
-        outs=["out"],
-        outs_no_cache=["out_no_cache"],
-        single_stage=True,
-    )
-
-    with dvc.lock, dvc.state:
-        stage.remove(remove_outs=True, force=True)
-
-    assert not (tmp_dir / "out").exists()
-    assert not (tmp_dir / "out_no_cache").exists()
-    assert not (tmp_dir / "out.dvc").exists()
-
-    cache_dir = os.path.join(
-        dvc.stage_cache.cache_dir,
-        "10",
-        "10b45372fdf4ec14d3f779c5b256378d7a12780e4c7f549a44138e492f098bfe",
-    )
-    cache_file = os.path.join(
-        cache_dir,
-        "bb32e04c6da96a7192513390acedbe4cd6123f8fe5b0ba5fffe39716fe87f6f4",
-    )
-
-    assert os.path.isdir(cache_dir)
-    assert os.listdir(cache_dir) == [os.path.basename(cache_file)]
-    assert os.path.isfile(cache_file)
-
-    run_spy = mocker.patch("dvc.stage.run.cmd_run")
-    checkout_spy = mocker.spy(stage, "checkout")
-    with dvc.lock, dvc.state:
-        stage.run()
-
-    assert not run_spy.called
-    assert checkout_spy.call_count == 1
-
-    assert (tmp_dir / "out").exists()
-    assert (tmp_dir / "out_no_cache").exists()
-    assert (tmp_dir / "out").read_text() == "out"
-    assert (tmp_dir / "out_no_cache").read_text() == "out_no_cache"