diff --git a/dvc/command/run.py b/dvc/command/run.py index 4d9b62515d..6b9ac1d5d6 100644 --- a/dvc/command/run.py +++ b/dvc/command/run.py @@ -54,7 +54,7 @@ def run(self): name=self.args.name, ) except DvcException: - logger.exception("failed to run command") + logger.exception("") return 1 return 0 diff --git a/dvc/exceptions.py b/dvc/exceptions.py index 6e6a5e588e..117553c82f 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -1,4 +1,5 @@ """Exceptions raised by the dvc.""" +from funcy import first from dvc.utils import relpath, format_link @@ -6,6 +7,10 @@ class DvcException(Exception): """Base class for all dvc exceptions.""" + def __init__(self, msg, *args): + assert msg + super().__init__(msg, *args) + class InvalidArgumentError(ValueError, DvcException): """Thrown if arguments are invalid.""" @@ -23,13 +28,15 @@ class OutputDuplicationError(DvcException): def __init__(self, output, stages): assert isinstance(output, str) assert all(hasattr(stage, "relpath") for stage in stages) - msg = ( - "file/directory '{}' is specified as an output in more than one " - "stage: \n{}\n" - "This is not allowed. Consider using a different output name." - ).format( - output, "\n".join("\t{}".format(s.addressing) for s in stages) - ) + if len(stages) == 1: + msg = "output '{}' is already specified in {}.".format( + output, first(stages) + ) + else: + msg = "output '{}' is already specified in stages:\n{}".format( + output, + "\n".join("\t- {}".format(s.addressing) for s in stages), + ) super().__init__(msg) self.stages = stages self.output = output @@ -81,10 +88,7 @@ class CircularDependencyError(DvcException): def __init__(self, dependency): assert isinstance(dependency, str) - msg = ( - "file/directory '{}' is specified as an output and as a " - "dependency." - ) + msg = "'{}' is specified as an output and as a dependency." super().__init__(msg.format(dependency)) @@ -133,14 +137,8 @@ def __init__(self): class CyclicGraphError(DvcException): def __init__(self, stages): assert isinstance(stages, list) - stages = "\n".join( - "\t- {}".format(stage.addressing) for stage in stages - ) - msg = ( - "you've introduced a cycle in your pipeline that involves " - "the following stages:" - "\n" - "{stages}".format(stages=stages) + msg = "Pipeline has a cycle involving: {}.".format( + ", ".join(s.addressing for s in stages) ) super().__init__(msg) @@ -148,7 +146,7 @@ def __init__(self, stages): class ConfirmRemoveError(DvcException): def __init__(self, path): super().__init__( - "unable to remove '{}' without a confirmation from the user. Use " + "unable to remove '{}' without a confirmation. Use " "`-f` to force.".format(path) ) diff --git a/dvc/main.py b/dvc/main.py index 91b2a33585..f4a7edb603 100644 --- a/dvc/main.py +++ b/dvc/main.py @@ -6,7 +6,7 @@ from dvc import analytics from dvc.cli import parse_args from dvc.config import ConfigError -from dvc.exceptions import DvcParserError +from dvc.exceptions import DvcParserError, DvcException from dvc.exceptions import NotDvcRepoError from dvc.external_repo import clean_repos from dvc.logger import disable_other_loggers, FOOTER @@ -58,6 +58,9 @@ def main(argv=None): ret = 253 except DvcParserError: ret = 254 + except DvcException: + ret = 255 + logger.exception("") except Exception as exc: # pylint: disable=broad-except if isinstance(exc, OSError) and exc.errno == errno.EMFILE: logger.exception( diff --git a/dvc/stage/__init__.py b/dvc/stage/__init__.py index e6afdeaa74..22158384d3 100644 --- a/dvc/stage/__init__.py +++ b/dvc/stage/__init__.py @@ -608,8 +608,9 @@ def _run(self): if old_handler: signal.signal(signal.SIGINT, old_handler) - if (p is None) or (p.returncode != 0): - raise StageCmdFailedError(self) + retcode = None if not p else p.returncode + if retcode != 0: + raise StageCmdFailedError(self, retcode) @rwlocked(read=["deps"], write=["outs"]) def run( diff --git a/dvc/stage/exceptions.py b/dvc/stage/exceptions.py index 4d10bc40f8..19d07aea67 100644 --- a/dvc/stage/exceptions.py +++ b/dvc/stage/exceptions.py @@ -2,8 +2,10 @@ class StageCmdFailedError(DvcException): - def __init__(self, stage): - msg = "{} cmd '{}' failed".format(stage, stage.cmd) + def __init__(self, stage, status=None): + msg = "failed to run: {}".format(stage.cmd) + if status is not None: + msg += ", exited with {}".format(status) super().__init__(msg) @@ -88,9 +90,7 @@ def __init__(self, missing_files): class StageNotFound(KeyError, DvcException): def __init__(self, file, name): super().__init__( - "Stage with '{}' name not found inside '{}' file".format( - name, file.relpath - ) + "Stage '{}' not found inside '{}' file".format(name, file.relpath) ) @@ -105,7 +105,7 @@ def __init__(self, file): class DuplicateStageName(DvcException): def __init__(self, name, file): super().__init__( - "Stage with name '{name}' already exists in '{relpath}'.".format( + "Stage '{name}' already exists in '{relpath}'.".format( name=name, relpath=file.relpath ) )