Repository: iterative/dvc
Pull request: output: prevent stage file from being used as output
* [x] Have you followed the guidelines in our
      [Contributing document](https://dvc.org/doc/user-guide/contributing)?

* [x] Does your PR affect documented changes or does it add new functionality
      that should be documented? If yes, have you created a PR for
      [dvc.org](https://github.com/iterative/dvc.org) documenting it or at
      least opened an issue for it? If so, please add a link to it.

-----

Fixes #2345 

Problem statement:
run: warning/exception when dependency is a dvc-file
It doesn't make a lot of sense to depend on a dvc-file, however, the interface is not intuitive enough to prevent this.

If we want to allow this behavior, it should be with a warning.

Hints from issue discussion:
As a first step we might just add a check that dependency is not a dvc file and throw an error. Having it as a feature also sounds quite neat, so one could do `-d my.dvc` which would mean "depend on all outputs of my.dvc". Not sure if that feature is desirable though, since it might be used by accident causing some obscure bugs in the pipeline.
Discord context https://discordapp.com/channels/485586884165107732/485596304961962003/605892459461083140
ohh, @efiop, that sounds really neat! never thought about it, and it make sense since "the output of  a stage become the input of the next one".

Commit message: output: prevent stage file from being used as output

Patch to review:
diff --git a/dvc/dependency/base.py b/dvc/dependency/base.py
index f76f4e022e..3fba3960d5 100644
--- a/dvc/dependency/base.py
+++ b/dvc/dependency/base.py
@@ -15,11 +15,19 @@ def __init__(self, path):
         super(DependencyIsNotFileOrDirError, self).__init__(msg)
 
 
+class DependencyIsStageFileError(DvcException):
+    def __init__(self, path):
+        super(DependencyIsStageFileError, self).__init__(
+            "Stage file '{}' cannot be dependency.".format(path)
+        )
+
+
 class DependencyBase(object):
     IS_DEPENDENCY = True
 
     DoesNotExistError = DependencyDoesNotExistError
     IsNotFileOrDirError = DependencyIsNotFileOrDirError
+    IsStageFileError = DependencyIsStageFileError
 
     def update(self):
         pass
diff --git a/dvc/output/base.py b/dvc/output/base.py
index 48fd5bebbd..5a66efb99e 100644
--- a/dvc/output/base.py
+++ b/dvc/output/base.py
@@ -32,6 +32,13 @@ def __init__(self, path):
         super(OutputAlreadyTrackedError, self).__init__(msg)
 
 
+class OutputIsStageFileError(DvcException):
+    def __init__(self, path):
+        super(OutputIsStageFileError, self).__init__(
+            "Stage file '{}' cannot be output.".format(path)
+        )
+
+
 class OutputBase(object):
     IS_DEPENDENCY = False
 
@@ -57,6 +64,7 @@ class OutputBase(object):
 
     DoesNotExistError = OutputDoesNotExistError
     IsNotFileOrDirError = OutputIsNotFileOrDirError
+    IsStageFileError = OutputIsStageFileError
 
     sep = "/"
 
@@ -71,6 +79,7 @@ def __init__(
         persist=False,
         tags=None,
     ):
+        self._validate_output_path(path)
         # This output (and dependency) objects have too many paths/urls
         # here is a list and comments:
         #
@@ -415,3 +424,10 @@ def get_used_cache(self, **kwargs):
         ret.extend(self._collect_used_dir_cache(**kwargs))
 
         return ret
+
+    @classmethod
+    def _validate_output_path(cls, path):
+        from dvc.stage import Stage
+
+        if Stage.is_valid_filename(path):
+            raise cls.IsStageFileError(path)
diff --git a/tests/func/test_run.py b/tests/func/test_run.py
index 567f30be04..d4abed8948 100644
--- a/tests/func/test_run.py
+++ b/tests/func/test_run.py
@@ -7,8 +7,10 @@
 import filecmp
 import pytest
 
+from dvc.dependency.base import DependencyIsStageFileError
 from dvc.main import main
 from dvc.output import OutputBase
+from dvc.output.base import OutputIsStageFileError
 from dvc.repo import Repo as DvcRepo
 from dvc.utils import file_md5
 from dvc.utils.stage import load_stage_file
@@ -941,3 +943,25 @@ def test_bad_stage_fname(repo_dir, dvc_repo):
 
     # Check that command hasn't been run
     assert not os.path.exists("out")
+
+
+def test_should_raise_on_stage_dependency(repo_dir, dvc_repo):
+    stage_file_name = "name.dvc"
+
+    with pytest.raises(DependencyIsStageFileError):
+        dvc_repo.run(
+            cmd="python {} {} {}".format(repo_dir.CODE, repo_dir.FOO, "out"),
+            deps=[repo_dir.FOO, stage_file_name],
+            outs=["out"],
+        )
+
+
+def test_should_raise_on_stage_output(repo_dir, dvc_repo):
+    stage_file_name = "name.dvc"
+
+    with pytest.raises(OutputIsStageFileError):
+        dvc_repo.run(
+            cmd="python {} {} {}".format(repo_dir.CODE, repo_dir.FOO, "out"),
+            deps=[repo_dir.FOO],
+            outs=[stage_file_name],
+        )
diff --git a/tests/unit/test_stage.py b/tests/unit/test_stage.py
index f87fbe0f66..d805a5d6f6 100644
--- a/tests/unit/test_stage.py
+++ b/tests/unit/test_stage.py
@@ -70,7 +70,7 @@ def test_stage_fname(add):
 
 
 def test_stage_update(mocker):
-    dep = DependencyREPO({"url": "example.com"}, None, None)
+    dep = DependencyREPO({"url": "example.com"}, None, "dep_path")
     mocker.patch.object(dep, "update", return_value=None)
 
     stage = Stage(None, "path", deps=[dep])


Code review comments:
diff_hunk: @@ -15,11 +15,19 @@ def __init__(self, path):
         super(DependencyIsNotFileOrDirError, self).__init__(msg)
 
 
+class DependencyIsStageFileError(DvcException):
+    def __init__(self, path):
+        super(DependencyIsStageFileError, self).__init__(
+            "Stage file '{}' cannot be dependency.".format(path)
path: dvc/dependency/base.py
text: @user1:
```suggestion
            "Stage file '{}' cannot be a dependency.".format(path)
```

diff_hunk: @@ -32,6 +32,13 @@ def __init__(self, path):
         super(OutputAlreadyTrackedError, self).__init__(msg)
 
 
+class OutputIsStageFileError(DvcException):
+    def __init__(self, path):
+        super(OutputIsStageFileError, self).__init__(
+            "Stage file '{}' cannot be output.".format(path)
path: dvc/output/base.py
text: @user1:
```suggestion
            "Stage file '{}' cannot be an output.".format(path)
```