Files
dvc/dvc/stage.py
T
2018-02-15 17:51:26 +02:00

186 lines
5.3 KiB
Python

import os
import yaml
import itertools
import subprocess
from dvc.exceptions import DvcException
from dvc.output import Output, Dependency, OutputError
class StageCmdFailedError(DvcException):
def __init__(self, stage):
msg = 'Stage {} cmd {} failed'.format(stage.relpath, stage.cmd)
super(StageCmdFailedError, self).__init__(msg)
class MissingDataSource(OutputError):
def __init__(self, missing_files):
assert len(missing_files) > 0
source = 'source'
if len(missing_files) > 1:
source += 's'
msg = u'missing data {}: {}'.format(source, ', '.join(missing_files))
super(MissingDataSource, self).__init__(msg)
class Stage(object):
STAGE_FILE = 'Dvcfile'
STAGE_FILE_SUFFIX = '.dvc'
PARAM_CMD = 'cmd'
PARAM_DEPS = 'deps'
PARAM_OUTS = 'outs'
def __init__(self, project, path=None, cmd=None, cwd=None, deps=[], outs=[]):
self.project = project
self.path = path
self.cmd = cmd
self.cwd = cwd
self.outs = outs
self.deps = deps
@property
def relpath(self):
return os.path.relpath(self.path)
@property
def dvc_path(self):
return os.path.relpath(self.path, self.project.root_dir)
@property
def is_data_source(self):
return self.cmd is None
@staticmethod
def is_stage_file(path):
if not os.path.isfile(path):
return False
if not path.endswith(Stage.STAGE_FILE_SUFFIX) and os.path.basename(path) != Stage.STAGE_FILE:
return False
return True
def changed(self):
for entry in itertools.chain(self.outs, self.deps):
if entry.changed():
self.project.logger.debug("{} changed".format(self.path))
return True
else:
self.project.logger.debug("{} didn't change".format(self.path))
return False
def remove_outs(self):
for out in self.outs:
out.remove()
if out.use_cache:
self.project.scm.ignore_remove(out.path)
def remove(self):
self.remove_outs()
os.unlink(self.path)
def reproduce(self, force=False):
if not self.changed() and not force:
return
if self.cmd:
# Removing outputs only if we actually have command to reproduce
self.remove_outs()
self.run()
@staticmethod
def loadd(project, d, path):
path = os.path.abspath(path)
cwd = os.path.dirname(path)
cmd = d[Stage.PARAM_CMD]
deps = Dependency.loadd_from(project, d[Stage.PARAM_DEPS], cwd=cwd)
outs = Output.loadd_from(project, d[Stage.PARAM_OUTS], cwd=cwd)
return Stage(project=project,
path=path,
cmd=cmd,
cwd=cwd,
deps=deps,
outs=outs)
@staticmethod
def loads(project=None,
cmd=None,
deps=[],
outs=[],
outs_no_cache=[],
fname=None,
cwd=os.curdir):
cwd = os.path.abspath(cwd)
path = os.path.join(cwd, fname)
outputs = Output.loads_from(project, outs, use_cache=True, cwd=cwd)
outputs += Output.loads_from(project, outs_no_cache, use_cache=False, cwd=cwd)
dependencies = Dependency.loads_from(project, deps, cwd=cwd)
return Stage(project=project,
path=path,
cmd=cmd,
cwd=cwd,
outs=outputs,
deps=dependencies)
@staticmethod
def load(project, fname):
with open(fname, 'r') as fd:
return Stage.loadd(project, yaml.safe_load(fd), fname)
def dumpd(self):
deps = [x.dumpd(self.cwd) for x in self.deps]
outs = [x.dumpd(self.cwd) for x in self.outs]
return {
Stage.PARAM_CMD: self.cmd,
Stage.PARAM_DEPS: deps,
Stage.PARAM_OUTS: outs,
}
def dump(self, fname=None):
if not fname:
fname = self.path
with open(fname, 'w') as fd:
yaml.safe_dump(self.dumpd(), fd, default_flow_style=False)
def save(self):
for dep in self.deps:
dep.save()
for out in self.outs:
out.save()
if out.use_cache:
self.project.scm.ignore(out.path)
def run(self):
if not self.is_data_source:
self.project.logger.info(u'Reproducing \'{}\':\n\t{}'.format(self.relpath, self.cmd))
p = subprocess.Popen(self.cmd, cwd=self.cwd, shell=True)
p.communicate()
if p.returncode != 0:
raise StageCmdFailedError(self)
self.save()
self.project.logger.debug(u'\'{}\' was reproduced'.format(self.relpath))
else:
self.project.logger.info(u'Verifying data sources in \'{}\''.format(self.relpath))
self.check_missing_outputs()
self.save()
def check_missing_outputs(self):
missing_outs = [out.rel_path for out in self.outs if not os.path.exists(out.rel_path)]
if missing_outs:
raise MissingDataSource(missing_outs)
def checkout(self):
for out in self.outs:
out.checkout()