diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py index 285ec3a84..5b56defee 100644 --- a/dvc/command/metrics.py +++ b/dvc/command/metrics.py @@ -34,6 +34,7 @@ class CmdMetricsShow(CmdBase): xpath=xpath, all_branches=self.args.all_branches, all_tags=self.args.all_tags, + recursive=self.args.recursive, ) except DvcException: logger.error("failed to show metrics") @@ -98,7 +99,9 @@ def add_parser(subparsers, parent_parser): description=METRICS_SHOW_HELP, help=METRICS_SHOW_HELP, ) - metrics_show_parser.add_argument("path", nargs="?", help="Path to metrics file.") + metrics_show_parser.add_argument( + "path", nargs="?", help="Path to metrics file or directory" + ) metrics_show_parser.add_argument( "-t", "--type", help="Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)." ) @@ -133,6 +136,13 @@ def add_parser(subparsers, parent_parser): default=False, help="Show metrics for all tags.", ) + metrics_show_parser.add_argument( + "-R", + "--recursive", + action="store_true", + default=False, + help="If path is a directory, recursively search and process metric files in path.", + ) metrics_show_parser.set_defaults(func=CmdMetricsShow) METRICS_ADD_HELP = "Add metrics." diff --git a/dvc/project.py b/dvc/project.py index 8d9199dbc..4ba57fa3a 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -1020,13 +1020,13 @@ class Project(object): elif typ == "htsv": ret = self._read_metric_hxsv(fd, xpath, "\t") else: - ret = fd.read() + ret = fd.read().strip() except Exception: logger.error("unable to read metric in '{}'".format(path)) return ret - def _find_output_by_path(self, path, outs=None): + def _find_output_by_path(self, path, outs=None, recursive=False): from dvc.exceptions import OutputDuplicationError if not outs: @@ -1034,37 +1034,62 @@ class Project(object): outs = [out for stage in astages for out in stage.outs] abs_path = os.path.abspath(path) - matched = [out for out in outs if out.path == abs_path] - stages = [out.stage.relpath for out in matched] - if len(stages) > 1: - raise OutputDuplicationError(path, stages) + if os.path.isdir(abs_path) and recursive: + matched = [ + out for out in outs if os.path.abspath(out.path).startswith(abs_path) + ] + else: + matched = [out for out in outs if out.path == abs_path] + stages = [out.stage.relpath for out in matched] + if len(stages) > 1: + raise OutputDuplicationError(path, stages) - return matched[0] if matched else None + return matched if matched else [] def metrics_show( - self, path=None, typ=None, xpath=None, all_branches=False, all_tags=False + self, + path=None, + typ=None, + xpath=None, + all_branches=False, + all_tags=False, + recursive=False, ): res = {} for branch in self.scm.brancher(all_branches=all_branches, all_tags=all_tags): + astages = self.active_stages() outs = [out for stage in astages for out in stage.outs] if path: - out = self._find_output_by_path(path, outs=outs) - stage = out.stage.path if out else None - if out and all([out.metric, not typ, isinstance(out.metric, dict)]): - entries = [ - ( - path, - out.metric.get(out.PARAM_METRIC_TYPE, None), - out.metric.get(out.PARAM_METRIC_XPATH, None), + outs = self._find_output_by_path(path, outs=outs, recursive=recursive) + stages = [out.stage.path for out in outs] + entries = [] + for out in outs: + if all([out.metric, not typ, isinstance(out.metric, dict)]): + entries += [ + ( + out.path, + out.metric.get(out.PARAM_METRIC_TYPE, None), + out.metric.get(out.PARAM_METRIC_XPATH, None), + ) + ] + else: + entries += [(out.path, typ, xpath)] + if not entries: + if os.path.isdir(path): + logger.warning( + "Path '{path}' is a directory. " + "Consider runnig with '-R'.".format(path=path) ) - ] - else: - entries = [(path, typ, xpath)] + return {} + + else: + entries += [(path, typ, xpath)] + else: metrics = filter(lambda o: o.metric, outs) - stage = None + stages = None entries = [] for o in metrics: if not typ and isinstance(o.metric, dict): @@ -1076,8 +1101,9 @@ class Project(object): entries.append((o.path, t, x)) for fname, t, x in entries: - if stage: - self.checkout(stage, force=True) + if stages: + for stage in stages: + self.checkout(stage, force=True) rel = os.path.relpath(fname) metric = self._read_metric(fname, typ=t, xpath=x) @@ -1098,6 +1124,10 @@ class Project(object): if res: return res + if path and os.path.isdir(path): + msg = "file '{}' does not exist".format(path) + return res + if path: msg = "file '{}' does not exist".format(path) else: @@ -1109,11 +1139,21 @@ class Project(object): raise DvcException(msg) def _metrics_modify(self, path, typ=None, xpath=None, delete=False): - out = self._find_output_by_path(path) - if not out: + outs = self._find_output_by_path(path) + + if not outs: msg = "unable to find file '{}' in the pipeline".format(path) raise DvcException(msg) + if len(outs) != 1: + msg = ( + "-R not yet supported for metrics modify. " + "Make sure only one metric is referred to by '{}'".format(path) + ) + raise DvcException(msg) + + out = outs[0] + if out.scheme != "local": msg = "output '{}' scheme '{}' is not supported for metrics" raise DvcException(msg.format(out.path, out.path_info["scheme"])) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 825883f43..9f3628615 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -97,6 +97,65 @@ class TestMetrics(TestDvc): self.assertTrue(ret["baz"]["metric_hcsv"] == ["baz"]) +class TestMetricsRecursive(TestDvc): + def setUp(self): + super(TestMetricsRecursive, self).setUp() + self.dvc.scm.commit("init") + + self.dvc.scm.checkout("nested", create_new=True) + + os.mkdir("nested") + os.mkdir(os.path.join("nested", "subnested")) + + ret = main( + [ + "run", + "-M", + os.path.join("nested", "metric_nested"), + "echo", + "nested", + ">>", + os.path.join("nested", "metric_nested"), + ] + ) + + self.assertEqual(ret, 0) + + ret = main( + [ + "run", + "-M", + os.path.join("nested", "subnested", "metric_subnested"), + "echo", + "subnested", + ">>", + os.path.join("nested", "subnested", "metric_subnested"), + ] + ) + + self.assertEqual(ret, 0) + + self.dvc.scm.add(["nested"]) + self.dvc.scm.commit("nested metrics") + + self.dvc.scm.checkout("master") + + def test(self): + + ret = self.dvc.metrics_show("nested", all_branches=True, recursive=False) + self.assertEqual(len(ret), 0) + + ret = self.dvc.metrics_show("nested", all_branches=True, recursive=True) + self.assertEqual(len(ret), 1) + self.assertEqual( + ret["nested"][os.path.join("nested", "subnested", "metric_subnested")], + "subnested", + ) + self.assertEqual( + ret["nested"][os.path.join("nested", "metric_nested")], "nested" + ) + + class TestMetricsReproCLI(TestDvc): def test(self): stage = self.dvc.run(