1"""Exceptions raised by the dvc."""
2
3from __future__ import unicode_literals
4
5from dvc.utils.compat import str, builtin_str
6
7import os
8import traceback
9
10
11class DvcException(Exception):
12    """Base class for all dvc exceptions.
13
14    Args:
15        msg (unicode): message for this exception.
16        cause (Exception): optional cause exception.
17    """
18
19    def __init__(self, msg, cause=None):
20        # NOTE: unlike python 3, python 2 doesn't have built-in support
21        # for chained exceptions, so we are using our own implementation.
22        self.cause = cause
23        self.cause_tb = None
24        if cause:
25            try:
26                self.cause_tb = traceback.format_exc()
27            except AttributeError:  # pragma: no cover
28                pass
29        super(DvcException, self).__init__(msg)
30
31
32class OutputDuplicationError(DvcException):
33    """Thrown if a file/directory is specified as an output in more than one
34    stage.
35
36    Args:
37        output (unicode): path to the file/directory.
38        stages (list): list of paths to stages.
39    """
40
41    def __init__(self, output, stages):
42        assert isinstance(output, str) or isinstance(output, builtin_str)
43        assert isinstance(stages, list)
44        assert all(
45            isinstance(stage, str) or isinstance(stage, builtin_str)
46            for stage in stages
47        )
48        msg = (
49            "file/directory '{}' is specified as an output in more than one"
50            "stage: {}"
51        ).format(output, "\n    ".join(stages))
52        super(OutputDuplicationError, self).__init__(msg)
53
54
55class OutputNotFoundError(DvcException):
56    """Thrown if a file/directory not found in repository pipelines.
57
58    Args:
59        output (unicode): path to the file/directory.
60    """
61
62    def __init__(self, output):
63        super(OutputNotFoundError, self).__init__(
64            "unable to find stage file with output '{path}'".format(
65                path=os.path.relpath(output)
66            )
67        )
68
69
70class StagePathAsOutputError(DvcException):
71    """Thrown if directory that stage is going to be saved in is specified as
72    an output of another stage.
73
74    Args:
75        cwd (str): path to the directory.
76        fname (str): path to the stage file that has cwd specified as an
77            output.
78    """
79
80    def __init__(self, wdir, fname):
81        assert isinstance(wdir, str) or isinstance(wdir, builtin_str)
82        assert isinstance(fname, str) or isinstance(fname, builtin_str)
83        msg = (
84            "current working directory '{cwd}' is specified as an output in"
85            " '{fname}'. Use another CWD to prevent any data removal.".format(
86                cwd=wdir, fname=fname
87            )
88        )
89        super(StagePathAsOutputError, self).__init__(msg)
90
91
92class CircularDependencyError(DvcException):
93    """Thrown if a file/directory specified both as an output and as a
94    dependency.
95
96    Args:
97        dependency (str): path to the dependency.
98    """
99
100    def __init__(self, dependency):
101        assert isinstance(dependency, str) or isinstance(
102            dependency, builtin_str
103        )
104
105        msg = (
106            "file/directory '{}' is specified as an output and as a "
107            "dependency."
108        )
109        super(CircularDependencyError, self).__init__(msg.format(dependency))
110
111
112class ArgumentDuplicationError(DvcException):
113    """Thrown if a file/directory is specified as a dependency/output more
114    than once.
115
116    Args:
117        path (str): path to the file/directory.
118    """
119
120    def __init__(self, path):
121        assert isinstance(path, str) or isinstance(path, builtin_str)
122        msg = "file '{}' is specified more than once."
123        super(ArgumentDuplicationError, self).__init__(msg.format(path))
124
125
126class MoveNotDataSourceError(DvcException):
127    """Thrown if attempted to move a file/directory that is not an output
128    in a data source stage.
129
130    Args:
131        path (str): path to the file/directory.
132    """
133
134    def __init__(self, path):
135        msg = (
136            "move is not permitted for stages that are not data sources. "
137            "You need to either move '{path}' to a new location and edit "
138            "it by hand, or remove '{path}' and create a new one at the "
139            "desired location."
140        )
141        super(MoveNotDataSourceError, self).__init__(msg.format(path=path))
142
143
144class NotDvcRepoError(DvcException):
145    """Thrown if a directory is not a dvc repo.
146
147    Args:
148        root (str): path to the directory.
149    """
150
151    def __init__(self, root):
152        msg = "not a dvc repository (checked up to mount point '{}')"
153        super(NotDvcRepoError, self).__init__(msg.format(root))
154
155
156class DvcParserError(DvcException):
157    """Base class for CLI parser errors."""
158
159    def __init__(self):
160        super(DvcParserError, self).__init__("parser error")
161
162
163class CyclicGraphError(DvcException):
164    def __init__(self, stages):
165        assert isinstance(stages, list)
166        stages = "\n".join("\t- {}".format(stage) for stage in stages)
167        msg = (
168            "you've introduced a cycle in your pipeline that involves"
169            " the following stages:"
170            "\n"
171            "{stages}".format(stages=stages)
172        )
173        super(CyclicGraphError, self).__init__(msg)
174
175
176class ConfirmRemoveError(DvcException):
177    def __init__(self, path):
178        super(ConfirmRemoveError, self).__init__(
179            "unable to remove '{}' without a confirmation from the user. Use "
180            "'-f' to force.".format(path)
181        )
182
183
184class InitError(DvcException):
185    def __init__(self, msg):
186        super(InitError, self).__init__(msg)
187
188
189class ReproductionError(DvcException):
190    def __init__(self, dvc_file_name, ex):
191        self.path = dvc_file_name
192        msg = "failed to reproduce '{}'".format(dvc_file_name)
193        super(ReproductionError, self).__init__(msg, cause=ex)
194
195
196class BadMetricError(DvcException):
197    def __init__(self, path):
198        super(BadMetricError, self).__init__(
199            "'{}' does not exist, not a metric or is malformed".format(
200                os.path.relpath(path)
201            )
202        )
203
204
205class NoMetricsError(DvcException):
206    def __init__(self):
207        super(NoMetricsError, self).__init__(
208            "no metric files in this repository. "
209            "Use 'dvc metrics add' to add a metric file to track."
210        )
211