Repository: iterative/dvc

Issue:
run: `--no-exec` fails if there's any params
Try the following command, nothing else required
```sh
dvc run -v --no-exec -n trainme -d train.py -d users.csv -p epochs,log_file,dropout --plots logs.csv -M summary.json -o logs python train.py
ERROR: unexpected error - 'list' object has no attribute 'keys'
```


Discussion:

Resolving patch:
diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py
--- a/dvc/dvcfile.py
+++ b/dvc/dvcfile.py
@@ -175,13 +175,16 @@ class PipelineFile(FileMixin):
     def _lockfile(self):
         return Lockfile(self.repo, os.path.splitext(self.path)[0] + ".lock")
 
-    def dump(self, stage, update_pipeline=False, **kwargs):
+    def dump(self, stage, update_pipeline=False, no_lock=False):
         """Dumps given stage appropriately in the dvcfile."""
         from dvc.stage import PipelineStage
 
         assert isinstance(stage, PipelineStage)
         check_dvc_filename(self.path)
-        self._dump_lockfile(stage)
+
+        if not no_lock:
+            self._dump_lockfile(stage)
+
         if update_pipeline and not stage.is_data_source:
             self._dump_pipeline_file(stage)
 
diff --git a/dvc/repo/run.py b/dvc/repo/run.py
--- a/dvc/repo/run.py
+++ b/dvc/repo/run.py
@@ -90,5 +90,5 @@ def run(self, fname=None, no_exec=False, single_stage=False, **kwargs):
             run_cache=kwargs.get("run_cache", True),
         )
 
-    dvcfile.dump(stage, update_pipeline=True)
+    dvcfile.dump(stage, update_pipeline=True, no_lock=no_exec)
     return stage
diff --git a/dvc/serialize.py b/dvc/serialize.py
--- a/dvc/serialize.py
+++ b/dvc/serialize.py
@@ -69,14 +69,22 @@ def _serialize_params(params: List[ParamsDependency]):
     for param_dep in sort_by_path(params):
         dump = param_dep.dumpd()
         path, params = dump[PARAM_PATH], dump[PARAM_PARAMS]
-        k = list(params.keys())
-        if not k:
-            continue
-        key_vals[path] = OrderedDict([(key, params[key]) for key in sorted(k)])
+        if isinstance(params, dict):
+            k = list(params.keys())
+            if not k:
+                continue
+            key_vals[path] = OrderedDict(
+                [(key, params[key]) for key in sorted(k)]
+            )
+        else:
+            assert isinstance(params, list)
+            k = params
+            key_vals = OrderedDict()
         # params from default file is always kept at the start of the `params:`
         if path == DEFAULT_PARAMS_FILE:
             keys = k + keys
-            key_vals.move_to_end(path, last=False)
+            if key_vals:
+                key_vals.move_to_end(path, last=False)
         else:
             # if it's not a default file, change the shape
             # to: {path: k}


Test patch:
diff --git a/tests/func/test_run_multistage.py b/tests/func/test_run_multistage.py
--- a/tests/func/test_run_multistage.py
+++ b/tests/func/test_run_multistage.py
@@ -19,6 +19,30 @@ def test_run_with_name(tmp_dir, dvc, run_copy):
     assert os.path.exists(PIPELINE_LOCK)
 
 
+def test_run_no_exec(tmp_dir, dvc, run_copy):
+    from dvc.stage import PipelineStage
+    from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
+
+    tmp_dir.dvc_gen("foo", "foo")
+    assert not os.path.exists(PIPELINE_FILE)
+    stage = run_copy("foo", "bar", name="copy-foo-to-bar", no_exec=True)
+    assert isinstance(stage, PipelineStage)
+    assert stage.name == "copy-foo-to-bar"
+    assert os.path.exists(PIPELINE_FILE)
+    assert not os.path.exists(PIPELINE_LOCK)
+
+    data, _ = stage.dvcfile._load()
+    assert data == {
+        "stages": {
+            "copy-foo-to-bar": {
+                "cmd": "python copy.py foo bar",
+                "deps": ["copy.py", "foo"],
+                "outs": ["bar"],
+            }
+        }
+    }
+
+
 def test_run_with_multistage_and_single_stage(tmp_dir, dvc, run_copy):
     from dvc.stage import PipelineStage, Stage
 
@@ -252,3 +276,26 @@ def test_run_params_custom_file(tmp_dir, dvc):
     assert data["stages"]["read_params"]["params"] == [
         {"params2.yaml": ["lists"]}
     ]
+
+
+def test_run_params_no_exec(tmp_dir, dvc):
+    from dvc.dependency import ParamsDependency
+
+    with (tmp_dir / "params2.yaml").open("w+") as f:
+        yaml.dump(supported_params, f)
+
+    stage = dvc.run(
+        name="read_params",
+        params=["params2.yaml:lists"],
+        cmd="cat params2.yaml",
+        no_exec=True,
+    )
+
+    isinstance(stage.deps[0], ParamsDependency)
+    assert stage.deps[0].params == ["lists"]
+    assert not stage.dvcfile._lockfile.exists()
+
+    data, _ = stage.dvcfile._load()
+    assert data["stages"]["read_params"]["params"] == [
+        {"params2.yaml": ["lists"]}
+    ]