diff --git a/dvc/cli.py b/dvc/cli.py index 5381426a4..405bb5f96 100644 --- a/dvc/cli.py +++ b/dvc/cli.py @@ -17,6 +17,7 @@ from dvc.command.add import CmdAdd from dvc.command.config import CmdConfig from dvc.command.checkout import CmdCheckout from dvc.command.remote import CmdRemoteAdd, CmdRemoteRemove, CmdRemoteModify +from dvc.command.metrics import CmdMetrics from dvc.stage import Stage from dvc import VERSION @@ -321,6 +322,24 @@ def parse_args(argv=None): help='Use local config') remote_modify_parser.set_defaults(func=CmdRemoteModify) + # Metrics + metrics_parser = subparsers.add_parser( + 'metrics', + parents=[parent_parser], + help='Get metrics from all branches') + metrics_parser.add_argument( + 'path', + help='Path to metrics file') + metrics_parser.add_argument( + '--json-path', + help='JSON path') + metrics_parser.add_argument( + '--tsv-path', + help='TSV path \'row,column\'(e.g. \'1,2\')') + metrics_parser.add_argument( + '--htsv-path', + help='Headed TSV path \'row,column\'(e.g. \'Name,3\'') + metrics_parser.set_defaults(func=CmdMetrics) args = parser.parse_args(argv) diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py index 68c69980c..de447df51 100644 --- a/dvc/command/metrics.py +++ b/dvc/command/metrics.py @@ -3,6 +3,9 @@ from dvc.command.common.base import CmdBase class CmdMetrics(CmdBase): def run(self): - for branch, metric in self.project.metrics(self.args.path).items(): + for branch, metric in self.project.metrics(self.args.path, + json_path=self.args.json_path, + tsv_path=self.args.tsv_path, + htsv_path=self.args.htsv_path).items(): self.project.logger.info("{}: {}".format(branch, metric)) return 0 diff --git a/dvc/project.py b/dvc/project.py index 62627f58d..7f90bcdf4 100644 --- a/dvc/project.py +++ b/dvc/project.py @@ -1,6 +1,9 @@ import os +import csv import stat +import json import networkx as nx +from jsonpath_rw import parse import dvc.cloud.base as cloud @@ -258,6 +261,61 @@ class Project(object): return self._cloud_status(target, jobs, remote=remote) return self._local_status(target) + def _read_metric_json(self, fd, json_path): + parser = parse(json_path) + return [x.value for x in parser.find(json.load(fd))] + + def _do_read_metric_tsv(self, reader, row, col): + if col != None and row != None: + return [reader[row][col]] + elif col != None: + return [r[col] for r in reader] + elif row != None: + return reader[row] + return None + + def _read_metric_htsv(self, fd, htsv_path): + col, row = htsv_path.split(',') + row = int(row) + reader = list(csv.DictReader(fd, delimiter='\t')) + return self._do_read_metric_tsv(reader, row, col) + + def _read_metric_tsv(self, fd, tsv_path): + col, row = tsv_path.split(',') + row = int(row) + col = int(col) + reader = list(csv.reader(fd, delimiter='\t')) + return self._do_read_metric_tsv(reader, row, col) + + def _read_metric(self, path, json_path=None, tsv_path=None, htsv_path=None): + ret = None + try: + with open(path, 'r') as fd: + if json_path: + ret = self._read_metric_json(fd, json_path) + elif tsv_path: + ret = self._read_metric_tsv(fd, tsv_path) + elif htsv_path: + ret = self._read_metric_htsv(fd, htsv_path) + else: + ret = fd.read() + except Exception as exc: + self.logger.error('Unable to read metric in \'{}\''.format(path), exc) + + return ret + + def metrics(self, path, json_path=None, tsv_path=None, htsv_path=None): + res = {} + saved = self.scm.active_branch() + for branch in self.scm.list_branches(): + self.scm.checkout(branch) + res[branch] = self._read_metric(path, + json_path=json_path, + tsv_path=tsv_path, + htsv_path=htsv_path) + self.scm.checkout(saved) + return res + def graph(self): G = nx.DiGraph() diff --git a/dvc/scm.py b/dvc/scm.py index 5667c9d55..3c4755bfe 100644 --- a/dvc/scm.py +++ b/dvc/scm.py @@ -49,6 +49,12 @@ class Base(object): def is_tracked(self, path): pass + def active_branch(self): + pass + + def list_branches(self): + pass + class Git(Base): GITIGNORE = '.gitignore' @@ -133,6 +139,12 @@ class Git(Base): def is_tracked(self, path): return len(self.repo.git.ls_files(path)) != 0 + def active_branch(self): + return self.repo.active_branch.name + + def list_branches(self): + return [h.name for h in self.repo.heads] + def SCM(root_dir=os.curdir, no_scm=False): if Git.is_repo(root_dir): diff --git a/requirements.txt b/requirements.txt index 3ef8074dd..e95d2d3e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ pyasn1>=0.4.1 schema>=0.6.7 paramiko>=2.4.1 binaryornot==0.4.4 +jsonpath-rw==1.4.0 diff --git a/setup.py b/setup.py index 8ef831b6f..298d45953 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ install_requires = [ "schema>=0.6.7", "paramiko>=2.4.1", "binaryornot==0.4.4", + "jsonpath-rw==1.4.0", ] setup( diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 000000000..71b3e95bb --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,72 @@ +import os +import json + +from dvc.main import main +from tests.basic_env import TestDvc + +class TestMetrics(TestDvc): + def setUp(self): + super(TestMetrics, self).setUp() + for branch in ['foo', 'bar', 'baz']: + self.dvc.scm.checkout(branch, create_new=True) + + with open('metric', 'w+') as fd: + fd.write(branch) + + with open('metric_json', 'w+') as fd: + json.dump({'branch': branch}, fd) + + with open('metric_tsv', 'w+') as fd: + fd.write(branch) + + with open('metric_htsv', 'w+') as fd: + fd.write('branch\n') + fd.write(branch) + + self.dvc.scm.add(['metric', 'metric_json', 'metric_tsv', 'metric_htsv']) + self.dvc.scm.commit('metric') + + self.dvc.scm.checkout('master') + + def test(self): + ret = self.dvc.metrics('metric') + self.assertEqual(len(ret), 4) + self.assertTrue(ret['master'] == None) + self.assertTrue(ret['foo'] == 'foo') + self.assertTrue(ret['bar'] == 'bar') + self.assertTrue(ret['baz'] == 'baz') + + ret = self.dvc.metrics('metric_json', json_path='branch') + self.assertEqual(len(ret), 4) + self.assertTrue(ret['master'] == None) + self.assertTrue(ret['foo'] == ['foo']) + self.assertTrue(ret['bar'] == ['bar']) + self.assertTrue(ret['baz'] == ['baz']) + + ret = self.dvc.metrics('metric_tsv', tsv_path='0,0') + self.assertEqual(len(ret), 4) + self.assertTrue(ret['master'] == None) + self.assertTrue(ret['foo'] == ['foo']) + self.assertTrue(ret['bar'] == ['bar']) + self.assertTrue(ret['baz'] == ['baz']) + + ret = self.dvc.metrics('metric_htsv', htsv_path='branch,0') + self.assertEqual(len(ret), 4) + self.assertTrue(ret['master'] == None) + self.assertTrue(ret['foo'] == ['foo']) + self.assertTrue(ret['bar'] == ['bar']) + self.assertTrue(ret['baz'] == ['baz']) + + def test_cli(self): + #FIXME check output + ret = main(['metrics', 'metric']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'metric_json', '--json-path', 'branch']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'metric_tsv', '--tsv-path', '0,0']) + self.assertEqual(ret, 0) + + ret = main(['metrics', 'metric_htsv', '--htsv-path', 'branch,0']) + self.assertEqual(ret, 0)