run: Check CWD specified as output in any stage (#1375)

* run: Check CWD specified as output in any stage

Fix #1123

* Fix some typo

Co-Authored-By: mroutis <[email protected]>

* normalize paths and make stage an obligatory argument

* Pass stages() to _check_output_duplication

* Update dvc/project.py

Co-Authored-By: mroutis <[email protected]>
This commit is contained in:
Mr. Outis
2018-11-29 21:03:10 +02:00
committed by Ruslan Kuprieiev
parent 44e67d4cdc
commit 22d38cda9f
3 changed files with 56 additions and 6 deletions
+12
View File
@@ -31,6 +31,18 @@ class OutputDuplicationError(DvcException):
super(OutputDuplicationError, self).__init__(msg.format(output, s))
class WorkingDirectoryAsOutputError(DvcException):
def __init__(self, cwd, fname):
assert isinstance(cwd, str)
assert isinstance(fname, str)
msg = (
"Current working directory '{cwd}' is specified as an output in"
" '{fname}'. Use another CWD to prevent any data removal."
.format(cwd=cwd, fname=fname)
)
super(WorkingDirectoryAsOutputError, self).__init__(msg)
class CircularDependencyError(DvcException):
def __init__(self, dependency):
assert isinstance(dependency, str)
+28 -5
View File
@@ -165,10 +165,20 @@ class Project(object):
def to_dvc_path(self, path):
return os.path.relpath(path, self.root_dir)
def _check_output_duplication(self, outs):
def _check_cwd_specified_as_output(self, cwd, stages):
from dvc.exceptions import WorkingDirectoryAsOutputError
cwd_path = os.path.abspath(os.path.normpath(cwd))
for stage in stages:
for output in stage.outs:
if os.path.isdir(output.path) and output.path == cwd_path:
raise WorkingDirectoryAsOutputError(cwd, stage.relpath)
def _check_output_duplication(self, outs, stages):
from dvc.exceptions import OutputDuplicationError
for stage in self.stages():
for stage in stages:
for o in stage.outs:
for out in outs:
if o.path == out.path and o.stage.path != out.stage.path:
@@ -203,6 +213,7 @@ class Project(object):
else:
fnames = [fname]
all_stages = self.stages()
stages = []
self._files_to_git_add = []
with self.state:
@@ -211,7 +222,7 @@ class Project(object):
outs=[f],
add=True)
self._check_output_duplication(stage.outs)
self._check_output_duplication(stage.outs, all_stages)
stage.save()
stage.dump()
@@ -332,7 +343,10 @@ class Project(object):
deps=deps,
overwrite=overwrite)
self._check_output_duplication(stage.outs)
all_stages = self.stages()
self._check_cwd_specified_as_output(cwd, all_stages)
self._check_output_duplication(stage.outs, all_stages)
self._check_circular_dependency(stage.deps, stage.outs)
self._files_to_git_add = []
@@ -352,7 +366,8 @@ class Project(object):
deps=[url],
outs=[out])
self._check_output_duplication(stage.outs)
self._check_output_duplication(stage.outs, self.stages())
self._check_circular_dependency(stage.deps, stage.outs)
self._files_to_git_add = []
with self.state:
@@ -1116,6 +1131,14 @@ class Project(object):
return pipelines
def stages(self):
"""
Walks down the root directory looking for Dvcfiles,
skipping the directories that are related with
any SCM (e.g. `.git`) or DVC itself (`.dvc`).
NOTE: For large projects, this could be an expensive
operation. Consider using some memorization.
"""
stages = []
outs = []
for root, dirs, files in os.walk(self.root_dir):
+16 -1
View File
@@ -6,7 +6,9 @@ from dvc.utils import file_md5
from dvc.stage import Stage, StageFileBadNameError, MissingDep
from dvc.stage import StageBadCwdError
from dvc.command.run import CmdRun
from dvc.exceptions import OutputDuplicationError, CircularDependencyError
from dvc.exceptions import (OutputDuplicationError,
CircularDependencyError,
WorkingDirectoryAsOutputError)
from tests.basic_env import TestDvc
@@ -117,6 +119,19 @@ class TestRunCircularDependency(TestDvc):
fname='circular-dependency.dvc')
class TestRunWorkingDirectoryAsOutput(TestDvc):
def test(self):
self.dvc.run(cmd='',
deps=[],
outs=[self.DATA_DIR])
with self.assertRaises(WorkingDirectoryAsOutputError):
self.dvc.run(cmd='',
cwd=self.DATA_DIR,
outs=[self.FOO],
fname='inside-cwd.dvc')
class TestRunBadCwd(TestDvc):
def test(self):
with self.assertRaises(StageBadCwdError):