Repository: iterative/dvc
Pull request #912 (2018-07-18 09:10:46)

metrics: csv support and additional type verification 


Patch:
diff --git a/dvc/cli.py b/dvc/cli.py
index 07fc400d5..ca655d4f4 100644
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -477,15 +477,22 @@ def parse_args(argv=None):
                         'path',
                         nargs='?',
                         help='Path to metrics file')
-    metrics_show_parser.add_argument(
+    metrics_show_parser_group = metrics_show_parser.add_mutually_exclusive_group()
+    metrics_show_parser_group.add_argument(
                         '--json-path',
                         help='JSON path')
-    metrics_show_parser.add_argument(
+    metrics_show_parser_group.add_argument(
                         '--tsv-path',
                         help='TSV path \'row,column\'(e.g. \'1,2\')')
-    metrics_show_parser.add_argument(
+    metrics_show_parser_group.add_argument(
                         '--htsv-path',
                         help='Headed TSV path \'row,column\'(e.g. \'Name,3\'')
+    metrics_show_parser_group.add_argument(
+                        '--csv-path',
+                        help='CSV path \'row,column\'(e.g. \'1,2\')')
+    metrics_show_parser_group.add_argument(
+                        '--hcsv-path',
+                        help='Headed CSV path \'row,column\'(e.g. \'Name,3\'')
     metrics_show_parser.add_argument(
                         '--all-branches',
                         action='store_true',
diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py
index 75cb558d4..fae601c4f 100644
--- a/dvc/command/metrics.py
+++ b/dvc/command/metrics.py
@@ -7,6 +7,8 @@ class CmdMetricsShow(CmdBase):
                                   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,
                                   all_branches=self.args.all_branches)
         return 0
 
diff --git a/dvc/output/local.py b/dvc/output/local.py
index 53fc11aab..05a863521 100644
--- a/dvc/output/local.py
+++ b/dvc/output/local.py
@@ -8,6 +8,7 @@ from dvc.dependency.local import DependencyLOCAL
 from dvc.exceptions import DvcException
 from dvc.logger import Logger
 from dvc.utils import remove
+from dvc.istextfile import istextfile
 
 
 class OutputLOCAL(DependencyLOCAL):
@@ -57,9 +58,22 @@ class OutputLOCAL(DependencyLOCAL):
 
         self.project.cache.local.checkout(self.path_info, self.info)
 
+    def _verify_metric(self):
+        if not self.metric:
+            return
+
+        if os.path.isdir(self.path):
+            msg = 'Directory \'{}\' cannot be used as metrics.'
+            raise DvcException(msg.format(self.rel_path))
+
+        if not istextfile(self.path):
+            msg = 'Binary file \'{}\' cannot be used as metrics.'
+            raise DvcException(msg.format(self.rel_path))
+
     def save(self):
         if not self.use_cache:
             super(OutputLOCAL, self).save()
+            self._verify_metric()
             msg = 'Output \'{}\' doesn\'t use cache. Skipping saving.'
             self.project.logger.debug(msg.format(self.rel_path))
             return
diff --git a/dvc/project.py b/dvc/project.py
index 7dbc919ff..38ad4473f 100644
--- a/dvc/project.py
+++ b/dvc/project.py
@@ -400,7 +400,7 @@ class Project(object):
         parser = parse(json_path)
         return [x.value for x in parser.find(json.load(fd))]
 
-    def _do_read_metric_tsv(self, reader, row, col):
+    def _do_read_metric_xsv(self, reader, row, col):
         if col != None and row != None:
             return [reader[row][col]]
         elif col != None:
@@ -409,24 +409,28 @@ class Project(object):
             return reader[row]
         return None
 
-    def _read_metric_htsv(self, fd, htsv_path):
+    def _read_metric_hxsv(self, fd, hxsv_path, delimiter):
         import csv
 
-        col, row = htsv_path.split(',')
+        col, row = hxsv_path.split(',')
         row = int(row)
-        reader = list(csv.DictReader(fd, delimiter='\t'))
-        return self._do_read_metric_tsv(reader, row, col)
+        reader = list(csv.DictReader(fd, delimiter=delimiter))
+        return self._do_read_metric_xsv(reader, row, col)
 
-    def _read_metric_tsv(self, fd, tsv_path):
+    def _read_metric_xsv(self, fd, xsv_path, delimiter):
         import csv
 
-        col, row = tsv_path.split(',')
+        col, row = xsv_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):
+        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):
         ret = None
 
         if not os.path.exists(path):
@@ -436,10 +440,14 @@ class Project(object):
             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_tsv(fd, 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_htsv(fd, htsv_path)
+                    ret = self._read_metric_hxsv(fd, htsv_path, '\t')
                 else:
                     ret = fd.read()
         except Exception as exc:
@@ -447,7 +455,12 @@ class Project(object):
 
         return ret
 
-    def metrics_show(self, path=None, json_path=None, tsv_path=None, htsv_path=None, all_branches=False):
+    def metrics_show(self, path=None, json_path=None,
+                                      tsv_path=None,
+                                      htsv_path=None,
+                                      csv_path=None,
+                                      hcsv_path=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]
@@ -458,7 +471,9 @@ class Project(object):
                 metric = self._read_metric(fname,
                                            json_path=json_path,
                                            tsv_path=tsv_path,
-                                           htsv_path=htsv_path)
+                                           htsv_path=htsv_path,
+                                           csv_path=csv_path,
+                                           hcsv_path=hcsv_path)
                 if not metric:
                     continue
 
@@ -491,6 +506,7 @@ class Project(object):
                     raise DvcException(msg.format(out.rel_path))
 
                 out.metric = val
+                out._verify_metric()
 
             stage.dump()
 


Test patch:
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index 2af0d578c..921152044 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -2,6 +2,7 @@ import os
 import json
 
 from dvc.main import main
+from dvc.exceptions import DvcException
 from tests.basic_env import TestDvc
 
 
@@ -19,6 +20,13 @@ class TestMetrics(TestDvc):
             with open('metric_json', 'w+') as fd:
                 json.dump({'branch': branch}, fd)
 
+            with open('metric_csv', 'w+') as fd:
+                fd.write(branch)
+
+            with open('metric_hcsv', 'w+') as fd:
+                fd.write('branch\n')
+                fd.write(branch)
+
             with open('metric_tsv', 'w+') as fd:
                 fd.write(branch)
 
@@ -26,7 +34,7 @@ class TestMetrics(TestDvc):
                 fd.write('branch\n')
                 fd.write(branch)
 
-            self.dvc.scm.add(['metric', 'metric_json', 'metric_tsv', 'metric_htsv'])
+            self.dvc.scm.add(['metric', 'metric_json', 'metric_tsv', 'metric_htsv', 'metric_csv', 'metric_hcsv'])
             self.dvc.scm.commit('metric')
 
         self.dvc.scm.checkout('master')
@@ -56,16 +64,24 @@ class TestMetrics(TestDvc):
         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)
+        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)
+        self.assertEqual(len(ret), 3)
+        self.assertTrue(ret['foo']['metric_hcsv'] == ['foo'])
+        self.assertTrue(ret['bar']['metric_hcsv'] == ['bar'])
+        self.assertTrue(ret['baz']['metric_hcsv'] == ['baz'])
+
 
 class TestMetricsReproCLI(TestDvc):
     def test(self):
-
-        stage = self.dvc.run(outs_no_cache=['metrics'],
+        stage = self.dvc.run(metrics_no_cache=['metrics'],
                              cmd='python {} {} {}'.format(self.CODE, self.FOO, 'metrics'))
 
-        ret = main(['metrics', 'add', 'metrics'])
-        self.assertEqual(ret, 0)
-
         ret = main(['repro', '-m', stage.path])
         self.assertEqual(ret, 0)
 
@@ -75,6 +91,19 @@ class TestMetricsReproCLI(TestDvc):
         ret = main(['repro', '-f', '-m', stage.path])
         self.assertEqual(ret, 0)
 
+    def test_dir(self):
+        os.mkdir('metrics_dir')
+
+        with self.assertRaises(DvcException):
+            self.dvc.run(metrics_no_cache=['metrics_dir'])
+
+    def test_binary(self):
+        with open('metrics_bin', 'w+') as fd:
+            fd.write('\0\0\0\0\0\0\0\0')
+
+        with self.assertRaises(DvcException):
+            self.dvc.run(metrics_no_cache=['metrics_bin'])
+
 
 class TestMetricsCLI(TestMetrics):
     def test(self):
@@ -90,3 +119,24 @@ class TestMetricsCLI(TestMetrics):
 
         ret = main(['metrics', 'show', '--all-branches', 'metric_htsv', '--htsv-path', 'branch,0'])
         self.assertEqual(ret, 0)
+
+        ret = main(['metrics', 'show', '--all-branches', 'metric_csv', '--csv-path', '0,0'])
+        self.assertEqual(ret, 0)
+
+        ret = main(['metrics', 'show', '--all-branches', 'metric_hcsv', '--hcsv-path', 'branch,0'])
+        self.assertEqual(ret, 0)
+
+    def test_dir(self):
+        os.mkdir('metrics_dir')
+
+        with self.assertRaises(DvcException):
+            self.dvc.run(outs_no_cache=['metrics_dir'])
+            self.dvc.metrics_add('metrics_dir')
+
+    def test_binary(self):
+        with open('metrics_bin', 'w+') as fd:
+            fd.write('\0\0\0\0\0\0\0\0')
+
+        with self.assertRaises(DvcException):
+            self.dvc.run(outs_no_cache=['metrics_bin'])
+            self.dvc.metrics_add('metrics_bin')