dvc: bring back dvc lock with dvc unlock

Fixes #523

Signed-off-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
Ruslan Kuprieiev
2018-06-26 02:53:07 +03:00
parent b876a446d8
commit b8371d1c76
5 changed files with 119 additions and 9 deletions
+22
View File
@@ -22,6 +22,7 @@ from dvc.command.remote import CmdRemoteAdd, CmdRemoteRemove, CmdRemoteModify, C
from dvc.command.metrics import CmdMetricsShow, CmdMetricsAdd, CmdMetricsRemove
from dvc.command.install import CmdInstall
from dvc.command.root import CmdRoot
from dvc.command.lock import CmdLock, CmdUnlock
from dvc.stage import Stage
from dvc import VERSION
@@ -498,6 +499,27 @@ def parse_args(argv=None):
help='Relative path to project\'s directory')
root_parser.set_defaults(func=CmdRoot)
# Lock
lock_parser = subparsers.add_parser(
'lock',
parents=[parent_parser],
help='Lock DVC file')
lock_parser.add_argument('targets',
nargs='+',
help='DVC files.')
lock_parser.set_defaults(func=CmdLock)
# Unlock
unlock_parser = subparsers.add_parser(
'unlock',
parents=[parent_parser],
help='Unlock DVC file')
unlock_parser.add_argument('targets',
nargs='+',
help='DVC files.')
unlock_parser.set_defaults(func=CmdUnlock)
args = parser.parse_args(argv)
return args
+24
View File
@@ -0,0 +1,24 @@
from dvc.exceptions import DvcException
from dvc.command.common.base import CmdBase
class CmdLockBase(CmdBase):
def run(self, unlock=False):
for target in self.args.targets:
try:
self.project.lock_stage(target, unlock=unlock)
except DvcException as ex:
msg = 'Failed to {}lock \'{}\''.format('un' if unlock else '', target)
self.project.logger.error(msg, ex)
return 1
return 0
class CmdLock(CmdLockBase):
def run(self):
return super(CmdLock, self).run(False)
class CmdUnlock(CmdLockBase):
def run(self):
return super(CmdUnlock, self).run(True)
+12
View File
@@ -138,6 +138,16 @@ class Project(object):
return stage
def lock_stage(self, target, unlock=False):
if not Stage.is_stage_file(target):
raise NotDvcFileError(target)
stage = Stage.load(self, target)
stage.locked = False if unlock else True
stage.dump()
return stage
def move(self, from_path, to_path):
from_out = Output.loads_from(Stage(self, cwd=os.curdir), [from_path])[0]
@@ -431,6 +441,8 @@ class Project(object):
for stage in stages:
node = os.path.relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
if stage.locked:
continue
for dep in stage.deps:
for out in outs:
if out.path != dep.path and not dep.path.startswith(out.path + out.sep):
+29 -8
View File
@@ -42,15 +42,17 @@ class Stage(object):
PARAM_CMD = 'cmd'
PARAM_DEPS = 'deps'
PARAM_OUTS = 'outs'
PARAM_LOCKED = 'locked'
SCHEMA = {
schema.Optional(PARAM_MD5): schema.Or(str, None),
schema.Optional(PARAM_CMD): schema.Or(str, None),
schema.Optional(PARAM_DEPS): schema.Or(schema.And(list, schema.Schema([dependency.SCHEMA])), None),
schema.Optional(PARAM_OUTS): schema.Or(schema.And(list, schema.Schema([output.SCHEMA])), None),
schema.Optional(PARAM_LOCKED): bool,
}
def __init__(self, project, path=None, cmd=None, cwd=None, deps=[], outs=[], md5=None):
def __init__(self, project, path=None, cmd=None, cwd=None, deps=[], outs=[], md5=None, locked=False):
self.project = project
self.path = path
self.cmd = cmd
@@ -58,6 +60,7 @@ class Stage(object):
self.outs = outs
self.deps = deps
self.md5 = md5
self.locked = locked
@property
def relpath(self):
@@ -108,7 +111,12 @@ class Stage(object):
if self.is_callback:
ret = True
for entry in itertools.chain(self.outs, self.deps):
if self.locked:
entries = self.outs
else:
entries = itertools.chain(self.outs, self.deps)
for entry in entries:
if entry.changed():
ret = True
@@ -134,7 +142,7 @@ class Stage(object):
if not self.changed() and not force:
return None
if self.cmd or self.is_import:
if (self.cmd or self.is_import) and not self.locked:
# Removing outputs only if we actually have command to reproduce
self.remove_outs(ignore_remove=False)
@@ -162,12 +170,14 @@ class Stage(object):
cwd = os.path.dirname(path)
cmd = d.get(Stage.PARAM_CMD, None)
md5 = d.get(Stage.PARAM_MD5, None)
locked = d.get(Stage.PARAM_LOCKED, False)
stage = Stage(project=project,
path=path,
cmd=cmd,
cwd=cwd,
md5=md5)
md5=md5,
locked=locked)
stage.deps = dependency.loadd_from(stage, d.get(Stage.PARAM_DEPS, []))
stage.outs = output.loadd_from(stage, d.get(Stage.PARAM_OUTS, []))
@@ -182,14 +192,16 @@ class Stage(object):
outs_no_cache=[],
metrics_no_cache=[],
fname=None,
cwd=os.curdir):
cwd=os.curdir,
locked=False):
cwd = os.path.abspath(cwd)
path = os.path.join(cwd, fname)
stage = Stage(project=project,
path=path,
cmd=cmd,
cwd=cwd)
cwd=cwd,
locked=locked)
stage.outs = output.loads_from(stage, outs, use_cache=True)
stage.outs += output.loads_from(stage, outs_no_cache, use_cache=False)
@@ -217,6 +229,9 @@ class Stage(object):
if len(outs):
ret[Stage.PARAM_OUTS] = outs
if self.locked:
ret[Stage.PARAM_LOCKED] = self.locked
ret[Stage.PARAM_MD5] = dict_md5(ret)
return ret
@@ -236,7 +251,10 @@ class Stage(object):
out.save()
def run(self):
if self.is_import:
if self.locked:
self.project.logger.info(u'Verifying outputs in locked stage \'{}\''.format(self.relpath))
self.check_missing_outputs()
elif self.is_import:
msg = u'Importing \'{}\' -> \'{}\''
self.project.logger.info(msg.format(self.deps[0].path, self.outs[0].rel_path))
@@ -280,7 +298,10 @@ class Stage(object):
def status(self):
ret = {}
ret.update(self._status(self.deps, 'deps'))
if not self.locked:
ret.update(self._status(self.deps, 'deps'))
ret.update(self._status(self.outs, 'outs'))
if ret or self.changed_md5() or self.is_callback:
+32 -1
View File
@@ -14,7 +14,7 @@ from google.cloud import storage as gc
from dvc.main import main
from dvc.command.repro import CmdRepro
from dvc.project import Project, ReproductionError
from dvc.project import Project, ReproductionError, NotDvcFileError
from dvc.utils import file_md5
from dvc.remote.local import RemoteLOCAL
from dvc.stage import Stage
@@ -130,6 +130,37 @@ class TestReproChangedDeepData(TestReproChangedData):
self.assertEqual(len(stages), 3)
class TestReproLocked(TestReproChangedData):
def test(self):
file2 = 'file2'
file2_stage = file2 + '.dvc'
self.dvc.run(fname=file2_stage,
outs=[file2],
deps=[self.file1, self.CODE],
cmd='python {} {} {}'.format(self.CODE, self.file1, file2))
self.swap_foo_with_bar()
ret = main(['lock', file2_stage])
self.assertEqual(ret, 0)
stages = self.dvc.reproduce(file2_stage)
self.assertEqual(len(stages), 0)
ret = main(['unlock', file2_stage])
self.assertEqual(ret, 0)
stages = self.dvc.reproduce(file2_stage)
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
self.assertTrue(filecmp.cmp(file2, self.BAR))
self.assertEqual(len(stages), 3)
def test_non_existing(self):
with self.assertRaises(NotDvcFileError):
self.dvc.lock_stage('non-existing-stage')
ret = main(['lock', 'non-existing-stage'])
self.assertNotEqual(ret, 0)
class TestReproPhony(TestReproChangedData):
def test(self):
stage = self.dvc.run(deps=[self.file1])