Repository: iterative/dvc

Issue:
BUG: JSON parsing error for `dvc metrics show`
## Bug Report

I have a simple JSON of test metrics, in file `scores.json`:
```json
{
    "test_1": {
        "precision": 0,
        "f1": 0.0
    },
    "test_2": {
        "fpr": 0.0
    },
    "notes": "if precision is zero, there were zero positive predictions; if f1 is zero, something went very wrong"
}
```

I expect the key "notes" to fail, but the numeric keys should show up. However, they don't:
```console
$ dvc metrics show -v scores.json
2020-09-28 17:16:09,022 DEBUG: Check for update is enabled.
2020-09-28 17:16:09,024 DEBUG: fetched: [(3,)]                        
2020-09-28 17:16:09,029 DEBUG: Could not parse 'precision' metric from 'scores.json' at '' due to its unsupported type: 'int'
2020-09-28 17:16:09,029 DEBUG: Could not parse 'f1' metric from 'scores.json' at '' due to its unsupported type: 'float'
2020-09-28 17:16:09,029 DEBUG: Could not parse 'test_1' metric from 'scores.json' at '' due to its unsupported type: 'dict'
2020-09-28 17:16:09,029 DEBUG: Could not parse 'fpr' metric from 'scores.json' at '' due to its unsupported type: 'float'
2020-09-28 17:16:09,029 DEBUG: Could not parse 'test_2' metric from 'scores.json' at '' due to its unsupported type: 'dict'
2020-09-28 17:16:09,029 DEBUG: Could not parse 'notes' metric from 'scores.json' at '' due to its unsupported type: 'str'
2020-09-28 17:16:09,030 DEBUG: fetched: [(11243,)]
2020-09-28 17:16:09,031 ERROR: failed to show metrics - Could not parse metric files. Use `-v` option to see more details.
```
### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version
DVC version: 1.7.9 (pip)
---------------------------------
Platform: Python 3.7.9 on Darwin-19.6.0-x86_64-i386-64bit
```

**Additional Information (if any):**

Running `dvc metrics diff -v HEAD^1` simply produces the same set of errors twice.

In the situation where this was found, I have other, longer `scores_XXX.json` files in the repo that are working just fine!

I think having a new feature when running `dvc metrics show` without `-v` that mentions the keys which failed to parse would be a user-friendly addition!



Comments:
Context: https://discordapp.com/channels/485586884165107732/485596304961962003/760249329637916682
This is quite an interesting bug. We do support reading only partial values, but it seems we are not making a distinction between `None` and 0/0.0 (falsy values). :slightly_smiling_face: 

https://github.com/iterative/dvc/blob/e3a7857b1352f1203504ed754482b9e308a48e2f/dvc/repo/metrics/show.py#L51

The fix is just to do `if m not in (None, {})` on that line.
Still, there seems to be another bug when the metrics file contains a scalar value (i.e. json file with just `0` in it), so we might need similar checks there too.

https://github.com/iterative/dvc/blob/e3a7857b1352f1203504ed754482b9e308a48e2f/dvc/repo/metrics/show.py#L83

@rgasper, would you be interested in making a fix?
If you can direct me to instructions on how to setup the codebase, sure.
Sure, @rgasper, please [follow the contributing guide](https://dvc.org/doc/user-guide/contributing/core). If you have any problems, please don't hesitate to ask in the #dev-talk channel in the Discord.


Patch:
diff --git a/dvc/command/metrics.py b/dvc/command/metrics.py
--- a/dvc/command/metrics.py
+++ b/dvc/command/metrics.py
@@ -11,7 +11,7 @@
 DEFAULT_PRECISION = 5
 
 
-def show_metrics(
+def _show_metrics(
     metrics, all_branches=False, all_tags=False, all_commits=False
 ):
     from dvc.utils.diff import format_dict
@@ -21,22 +21,25 @@ def show_metrics(
     # specified as `targets` in `repo.metrics.show` didn't contain any metrics.
     missing = metrics.pop(None, None)
 
+    lines = []
     for branch, val in metrics.items():
         if all_branches or all_tags or all_commits:
-            logger.info(f"{branch}:")
+            lines.append(f"{branch}:")
 
         for fname, metric in val.items():
             if not isinstance(metric, dict):
-                logger.info("\t{}: {}".format(fname, str(metric)))
+                lines.append("\t{}: {}".format(fname, str(metric)))
                 continue
 
-            logger.info(f"\t{fname}:")
+            lines.append(f"\t{fname}:")
             for key, value in flatten(format_dict(metric)).items():
-                logger.info(f"\t\t{key}: {value}")
+                lines.append(f"\t\t{key}: {value}")
 
     if missing:
         raise BadMetricError(missing)
 
+    return "\n".join(lines)
+
 
 class CmdMetricsBase(CmdBase):
     UNINITIALIZED = True
@@ -58,12 +61,14 @@ def run(self):
 
                 logger.info(json.dumps(metrics))
             else:
-                show_metrics(
+                table = _show_metrics(
                     metrics,
                     self.args.all_branches,
                     self.args.all_tags,
                     self.args.all_commits,
                 )
+                if table:
+                    logger.info(table)
         except DvcException:
             logger.exception("failed to show metrics")
             return 1
diff --git a/dvc/command/repro.py b/dvc/command/repro.py
--- a/dvc/command/repro.py
+++ b/dvc/command/repro.py
@@ -4,7 +4,7 @@
 
 from dvc.command import completion
 from dvc.command.base import CmdBase, append_doc_link
-from dvc.command.metrics import show_metrics
+from dvc.command.metrics import _show_metrics
 from dvc.command.status import CmdDataStatus
 from dvc.dvcfile import PIPELINE_FILE
 from dvc.exceptions import DvcException
@@ -51,7 +51,8 @@ def run(self):
 
                 if self.args.metrics:
                     metrics = self.repo.metrics.show()
-                    show_metrics(metrics)
+                    logger.info(_show_metrics(metrics))
+
             except DvcException:
                 logger.exception("")
                 ret = 1
diff --git a/dvc/repo/metrics/show.py b/dvc/repo/metrics/show.py
--- a/dvc/repo/metrics/show.py
+++ b/dvc/repo/metrics/show.py
@@ -48,7 +48,7 @@ def _extract_metrics(metrics, path, rev):
     ret = {}
     for key, val in metrics.items():
         m = _extract_metrics(val, path, rev)
-        if m:
+        if m not in (None, {}):
             ret[key] = m
         else:
             logger.debug(
@@ -80,7 +80,7 @@ def _read_metrics(repo, metrics, rev):
             continue
 
         val = _extract_metrics(val, metric, rev)
-        if val:
+        if val not in (None, {}):
             res[str(metric)] = val
 
     return res


Test patch:
diff --git a/tests/func/metrics/test_show.py b/tests/func/metrics/test_show.py
--- a/tests/func/metrics/test_show.py
+++ b/tests/func/metrics/test_show.py
@@ -160,3 +160,10 @@ def test_non_metric_and_recurisve_show(tmp_dir, dvc, run_copy_metrics):
             "metrics_t.yaml": {"foo": 1.1},
         }
     }
+
+
+def test_show_falsey(tmp_dir, dvc):
+    tmp_dir.gen("metrics.json", '{"foo": 0, "bar": 0.0, "baz": {}}')
+    assert dvc.metrics.show(targets=["metrics.json"]) == {
+        "": {"metrics.json": {"foo": 0, "bar": 0.0}}
+    }
diff --git a/tests/unit/command/test_metrics.py b/tests/unit/command/test_metrics.py
--- a/tests/unit/command/test_metrics.py
+++ b/tests/unit/command/test_metrics.py
@@ -1,7 +1,12 @@
 import textwrap
 
 from dvc.cli import parse_args
-from dvc.command.metrics import CmdMetricsDiff, CmdMetricsShow, _show_diff
+from dvc.command.metrics import (
+    CmdMetricsDiff,
+    CmdMetricsShow,
+    _show_diff,
+    _show_metrics,
+)
 
 
 def test_metrics_diff(dvc, mocker):
@@ -233,3 +238,17 @@ def test_metrics_diff_with_old():
         metrics.yaml  a.d.e     3      4      1
         metrics.yaml  x.b       5      6      1"""
     )
+
+
+def test_metrics_show_with_valid_falsey_values():
+    assert _show_metrics(
+        {"branch_1": {"metrics.json": {"a": 0, "b": {"ad": 0.0, "bc": 0.0}}}},
+        all_branches=True,
+    ) == textwrap.dedent(
+        """\
+        branch_1:
+        \tmetrics.json:
+        \t\ta: 0
+        \t\tb.ad: 0.0
+        \t\tb.bc: 0.0"""
+    )