From 236305716554f4a25c1a63aa56abada45a0d330f Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Mon, 23 Jul 2018 00:00:16 +0300 Subject: [PATCH] metrics: add '-t|--type' and '-x|--xpath' options Fixes #914 Signed-off-by: Ruslan Kuprieiev --- dvc/cli.py | 34 ++++++++++++++++- dvc/command/metrics.py | 44 ++++++++++++++++++--- dvc/output/__init__.py | 2 +- dvc/output/local.py | 16 +++++++- dvc/project.py | 86 +++++++++++++++++++++++++----------------- tests/test_metrics.py | 58 ++++++++++++++++++++++++---- 6 files changed, 189 insertions(+), 51 deletions(-) diff --git a/dvc/cli.py b/dvc/cli.py index f4b6ef0ef..3d7d53166 100644 --- a/dvc/cli.py +++ b/dvc/cli.py @@ -19,8 +19,10 @@ from dvc.command.add import CmdAdd from dvc.command.imp import CmdImport from dvc.command.config import CmdConfig from dvc.command.checkout import CmdCheckout -from dvc.command.remote import CmdRemoteAdd, CmdRemoteRemove, CmdRemoteModify, CmdRemoteList -from dvc.command.metrics import CmdMetricsShow, CmdMetricsAdd, CmdMetricsRemove +from dvc.command.remote import CmdRemoteAdd, CmdRemoteRemove +from dvc.command.remote import CmdRemoteModify, CmdRemoteList +from dvc.command.metrics import CmdMetricsShow, CmdMetricsAdd +from dvc.command.metrics import CmdMetricsRemove, CmdMetricsModify from dvc.command.install import CmdInstall from dvc.command.root import CmdRoot from dvc.command.lock import CmdLock, CmdUnlock @@ -477,6 +479,12 @@ def parse_args(argv=None): 'path', nargs='?', help='Path to metrics file') + metrics_show_parser.add_argument('-t', + '--type', + help='Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)') + metrics_show_parser.add_argument('-x', + '--xpath', + help='JSON/TSV/HTSV/CSV/HCSV path') metrics_show_parser_group = metrics_show_parser.add_mutually_exclusive_group() metrics_show_parser_group.add_argument( '--json-path', @@ -505,12 +513,34 @@ def parse_args(argv=None): 'add', parents=[parent_parser], help='Add metrics') + metrics_add_parser.add_argument('-t', + '--type', + help='Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)') + metrics_add_parser.add_argument('-x', + '--xpath', + help='JSON/TSV/HTSV/CSV/HCSV path') metrics_add_parser.add_argument( 'path', help='Path to metrics file') metrics_add_parser.set_defaults(func=CmdMetricsAdd) + metrics_modify_parser = metrics_subparsers.add_parser( + 'modify', + parents=[parent_parser], + help='Modify metrics') + metrics_modify_parser.add_argument('-t', + '--type', + help='Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)') + metrics_modify_parser.add_argument('-x', + '--xpath', + help='JSON/TSV/HTSV/CSV/HCSV path') + metrics_modify_parser.add_argument( + 'path', + help='Metrics file') + metrics_modify_parser.set_defaults(func=CmdMetricsModify) + + metrics_remove_parser = metrics_subparsers.add_parser( 'remove', parents=[parent_parser], diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py index 7014826c6..e9eac7324 100644 --- a/dvc/command/metrics.py +++ b/dvc/command/metrics.py @@ -5,12 +5,29 @@ from dvc.exceptions import DvcException class CmdMetricsShow(CmdBase): def run(self): try: + # backward compatibility + if self.args.json_path: + typ = 'json' + xpath = self.args.json_path + elif self.args.tsv_path: + typ = 'tsv' + xpath = self.args.tsv_path + elif self.args.htsv_path: + typ = 'htsv' + xpath = self.args.htsv_path + elif self.args.csv_path: + typ = 'csv' + xpath = self.args.csv_path + elif self.args.hcsv_path: + typ = 'hcsv' + xpath = self.args.hcsv_path + else: + typ = self.args.type + xpath = self.args.xpath + self.project.metrics_show(self.args.path, - json_path=self.args.json_path, - tsv_path=self.args.tsv_path, - htsv_path=self.args.htsv_path, - csv_path=self.args.csv_path, - hcsv_path=self.args.hcsv_path, + typ=typ, + xpath=xpath, all_branches=self.args.all_branches) except DvcException as exc: self.project.logger.error('Failed to show metrics', exc) @@ -19,10 +36,25 @@ class CmdMetricsShow(CmdBase): return 0 +class CmdMetricsModify(CmdBase): + def run(self): + try: + self.project.metrics_modify(self.args.path, + typ=self.args.type, + xpath=self.args.xpath) + except DvcException as exc: + self.project.logger.error('Failed to modify metrics', exc) + return 1 + + return 0 + + class CmdMetricsAdd(CmdBase): def run(self): try: - self.project.metrics_add(self.args.path) + self.project.metrics_add(self.args.path, + self.args.type, + self.args.xpath) except DvcException as exc: msg = 'Failed to add metric file \'{}\''.format(self.args.path) self.project.logger.error(msg, exc) diff --git a/dvc/output/__init__.py b/dvc/output/__init__.py index 9c478d6c2..baffdbc2c 100644 --- a/dvc/output/__init__.py +++ b/dvc/output/__init__.py @@ -23,7 +23,7 @@ OUTS_MAP = {'hdfs': OutputHDFS, '': OutputLOCAL} SCHEMA[schema.Optional(OutputLOCAL.PARAM_CACHE)] = bool -SCHEMA[schema.Optional(OutputLOCAL.PARAM_METRIC)] = bool +SCHEMA[schema.Optional(OutputLOCAL.PARAM_METRIC)] = OutputLOCAL.METRIC_SCHEMA def _get(stage, p, info, cache, metric): diff --git a/dvc/output/local.py b/dvc/output/local.py index 8f7cbbe9b..8be5220d1 100644 --- a/dvc/output/local.py +++ b/dvc/output/local.py @@ -14,6 +14,12 @@ from dvc.istextfile import istextfile class OutputLOCAL(DependencyLOCAL): PARAM_CACHE = 'cache' PARAM_METRIC = 'metric' + PARAM_METRIC_TYPE = 'type' + PARAM_METRIC_XPATH = 'xpath' + + METRIC_SCHEMA = schema.Or(None, bool, + {schema.Optional(PARAM_METRIC_TYPE): schema.Or(str, None), + schema.Optional(PARAM_METRIC_XPATH): schema.Or(str, None)}) DoesNotExistError = OutputDoesNotExistError IsNotFileOrDirError = OutputIsNotFileOrDirError @@ -38,7 +44,15 @@ class OutputLOCAL(DependencyLOCAL): def dumpd(self): ret = super(OutputLOCAL, self).dumpd() ret[self.PARAM_CACHE] = self.use_cache - ret[self.PARAM_METRIC] = self.metric + + if isinstance(self.metric, dict): + if self.PARAM_METRIC_XPATH in self.metric and \ + not self.metric[self.PARAM_METRIC_XPATH]: + del self.metric[self.PARAM_METRIC_XPATH] + + if self.metric: + ret[self.PARAM_METRIC] = self.metric + return ret def changed(self): diff --git a/dvc/project.py b/dvc/project.py index 0819fcfcf..dbddfee7f 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -426,11 +426,7 @@ class Project(object): reader = list(csv.reader(fd, delimiter=delimiter)) return self._do_read_metric_xsv(reader, row, col) - def _read_metric(self, path, json_path=None, - tsv_path=None, - htsv_path=None, - csv_path=None, - hcsv_path=None): + def _read_metric(self, path, typ=None, xpath=None): ret = None if not os.path.exists(path): @@ -438,16 +434,16 @@ class Project(object): try: with open(path, 'r') as fd: - if json_path: - ret = self._read_metric_json(fd, json_path) - elif csv_path: - ret = self._read_metric_xsv(fd, csv_path, ',') - elif tsv_path: - ret = self._read_metric_xsv(fd, tsv_path, '\t') - elif hcsv_path: - ret = self._read_metric_hxsv(fd, hcsv_path, ',') - elif htsv_path: - ret = self._read_metric_hxsv(fd, htsv_path, '\t') + if typ == 'json': + ret = self._read_metric_json(fd, xpath) + elif typ == 'csv': + ret = self._read_metric_xsv(fd, xpath, ',') + elif typ == 'tsv': + ret = self._read_metric_xsv(fd, xpath, '\t') + elif typ == 'hcsv': + ret = self._read_metric_hxsv(fd, xpath, ',') + elif typ == 'htsv': + ret = self._read_metric_hxsv(fd, xpath, '\t') else: ret = fd.read() except Exception as exc: @@ -470,12 +466,7 @@ class Project(object): return matched[0] if matched else None - def metrics_show(self, path=None, json_path=None, - tsv_path=None, - htsv_path=None, - csv_path=None, - hcsv_path=None, - all_branches=False): + def metrics_show(self, path=None, typ=None, xpath=None, all_branches=False): res = {} for branch in self.scm.brancher(all_branches=all_branches): outs = [out for stage in self.active_stages() for out in stage.outs] @@ -483,23 +474,33 @@ class Project(object): if path: out = self._find_output_by_path(path, outs=outs) stage = out.stage.path if out else None - fnames = [path] + 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))] + else: + entries = [(path, typ, xpath)] else: metrics = filter(lambda o: o.metric, outs) stage = None - fnames = map(lambda o: o.path, metrics) + entries = [] + for o in metrics: + if not typ and isinstance(o.metric, dict): + t = o.metric.get(o.PARAM_METRIC_TYPE, None) + x = o.metric.get(o.PARAM_METRIC_XPATH, None) + else: + t = typ + x = xpath + entries.append((o.path, t, x)) - for fname in fnames: + for fname, t, x in entries: if stage: self.checkout(stage) rel = os.path.relpath(fname) metric = self._read_metric(fname, - json_path=json_path, - tsv_path=tsv_path, - htsv_path=htsv_path, - csv_path=csv_path, - hcsv_path=hcsv_path) + typ=t, + xpath=x) if not metric: continue @@ -524,7 +525,7 @@ class Project(object): 'Use \'dvc metrics add\' to add a metric file to track.' raise DvcException(msg) - def _metrics_modify(self, path, val): + def _metrics_modify(self, path, typ=None, xpath=None, delete=False): found = False out = self._find_output_by_path(path) if not out: @@ -539,16 +540,33 @@ class Project(object): msg = 'Cached output \'{}\' is not supported for metrics' raise DvcException(msg.format(out.rel_path)) - out.metric = val + if typ: + if not isinstance(out.metric, dict): + out.metric = {} + out.metric[out.PARAM_METRIC_TYPE] = typ + + if xpath: + if not isinstance(out.metric, dict): + out.metric = {} + out.metric[out.PARAM_METRIC_XPATH] = xpath + + if delete: + out.metric = None + out._verify_metric() out.stage.dump() - def metrics_add(self, path): - self._metrics_modify(path, True) + def metrics_modify(self, path=None, typ=None, xpath=None): + self._metrics_modify(path, typ, xpath) + + def metrics_add(self, path, typ=None, xpath=None): + if not typ: + typ = 'raw' + self._metrics_modify(path, typ, xpath) def metrics_remove(self, path): - self._metrics_modify(path, False) + self._metrics_modify(path, delete=True) def graph(self): import networkx as nx diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 059b68c6f..b6e31d267 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -47,31 +47,46 @@ class TestMetrics(TestDvc): self.assertTrue(ret['bar']['metric'] == 'bar') self.assertTrue(ret['baz']['metric'] == 'baz') - ret = self.dvc.metrics_show('metric_json', json_path='branch', all_branches=True) + ret = self.dvc.metrics_show('metric_json', + typ='json', + xpath='branch', + all_branches=True) self.assertEqual(len(ret), 3) self.assertTrue(ret['foo']['metric_json'] == ['foo']) self.assertTrue(ret['bar']['metric_json'] == ['bar']) self.assertTrue(ret['baz']['metric_json'] == ['baz']) - ret = self.dvc.metrics_show('metric_tsv', tsv_path='0,0', all_branches=True) + ret = self.dvc.metrics_show('metric_tsv', + typ='tsv', + xpath='0,0', + all_branches=True) self.assertEqual(len(ret), 3) self.assertTrue(ret['foo']['metric_tsv'] == ['foo']) self.assertTrue(ret['bar']['metric_tsv'] == ['bar']) self.assertTrue(ret['baz']['metric_tsv'] == ['baz']) - ret = self.dvc.metrics_show('metric_htsv', htsv_path='branch,0', all_branches=True) + ret = self.dvc.metrics_show('metric_htsv', + typ='htsv', + xpath='branch,0', + all_branches=True) self.assertEqual(len(ret), 3) self.assertTrue(ret['foo']['metric_htsv'] == ['foo']) self.assertTrue(ret['bar']['metric_htsv'] == ['bar']) self.assertTrue(ret['baz']['metric_htsv'] == ['baz']) - ret = self.dvc.metrics_show('metric_csv', csv_path='0,0', all_branches=True) + ret = self.dvc.metrics_show('metric_csv', + typ='csv', + xpath='0,0', + all_branches=True) self.assertEqual(len(ret), 3) self.assertTrue(ret['foo']['metric_csv'] == ['foo']) self.assertTrue(ret['bar']['metric_csv'] == ['bar']) self.assertTrue(ret['baz']['metric_csv'] == ['baz']) - ret = self.dvc.metrics_show('metric_hcsv', hcsv_path='branch,0', all_branches=True) + ret = self.dvc.metrics_show('metric_hcsv', + typ='hcsv', + xpath='branch,0', + all_branches=True) self.assertEqual(len(ret), 3) self.assertTrue(ret['foo']['metric_hcsv'] == ['foo']) self.assertTrue(ret['bar']['metric_hcsv'] == ['bar']) @@ -112,7 +127,7 @@ class TestMetricsReproCLI(TestDvc): self.dvc.run(metrics_no_cache=['metrics_bin']) -class TestMetricsCLI(TestMetrics): +class TestMetricsCLICompat(TestMetrics): def test(self): #FIXME check output ret = main(['metrics', 'show', '--all-branches', 'metric', '-v']) @@ -133,6 +148,34 @@ class TestMetricsCLI(TestMetrics): ret = main(['metrics', 'show', '--all-branches', 'metric_hcsv', '--hcsv-path', 'branch,0']) self.assertEqual(ret, 0) + +class TestMetricsCLI(TestMetrics): + def test(self): + #FIXME check output + ret = main(['metrics', 'show', '-a', 'metric', '-v']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'show', '-a', 'metric_json', '-t', 'json', + '-x', 'branch']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'show', '-a', 'metric_tsv', '-t', 'tsv', + '-x', '0,0']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'show', '-a', 'metric_htsv', '-t', 'htsv', + '-x', 'branch,0']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'show', '-a', 'metric_csv', '-t', 'csv', + '-x', '0,0']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'show', '-a', 'metric_hcsv', '-t', 'hcsv', + '-x', 'branch,0']) + self.assertEqual(ret, 0) + + def test_dir(self): os.mkdir('metrics_dir') @@ -190,7 +233,8 @@ class TestCachedMetrics(TestDvc): res = self.dvc.metrics_show('metrics.json', all_branches=True, - json_path='metrics') + typ='json', + xpath='metrics') self.assertEqual(res, {"master": {"metrics.json": ["master"]}, "one": {"metrics.json": ["one"]},