Files
dvc/dvc/utils/diff.py
T
Ruslan Kuprieiev 52fe76cefc metrics: diff: make table more intuitive (#4655)
* metrics: diff: make table more intuitive

Currently we have a "diff not supported" marker in the table, that is
used whenever dvc is not able to diff the values (e.g. no old one, no
new one or format error). It is quite ugly and it is much more intuitive
to just show both old and new values and now show that marker at all, as
if there is no old/new value it is intuitive that we can't calculate the
change. Next step is to add old/new "bad format"/etc markers for
unsupported types or malformed/missing metric files.

E.g., before:

```
Path          Metric    Value    Change
metrics.json  foo       1.2      diff not supported
```

after this PR:

```
Path          Metric    Old    New    Change
metrics.json  foo       None   1.2
```

* metrics/params: use em dash in diff table
2020-10-05 19:37:34 +03:00

121 lines
2.7 KiB
Python

import json
from collections import defaultdict
from .flatten import flatten
def _parse(raw):
if raw is None or isinstance(raw, (dict, list, int, float)):
return raw
assert isinstance(raw, str)
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw
def _diff_vals(old, new, with_unchanged):
if (
isinstance(new, list)
and isinstance(old, list)
and len(old) == len(new) == 1
):
return _diff_vals(old[0], new[0], with_unchanged)
if not with_unchanged and old == new:
return {}
res = {"old": old, "new": new}
if isinstance(new, (int, float)) and isinstance(old, (int, float)):
res["diff"] = new - old
return res
def _flatten(d):
if not d:
return defaultdict(lambda: None)
if isinstance(d, dict):
return defaultdict(lambda: None, flatten(d))
return defaultdict(lambda: "unable to parse")
def _diff_dicts(old_dict, new_dict, with_unchanged):
new = _flatten(new_dict)
old = _flatten(old_dict)
res = defaultdict(dict)
xpaths = set(old.keys())
xpaths.update(set(new.keys()))
for xpath in xpaths:
old_val = old[xpath]
new_val = new[xpath]
val_diff = _diff_vals(old_val, new_val, with_unchanged)
if val_diff:
res[xpath] = val_diff
return dict(res)
def _diff(old_raw, new_raw, with_unchanged):
old = _parse(old_raw)
new = _parse(new_raw)
if isinstance(new, dict) or isinstance(old, dict):
return _diff_dicts(old, new, with_unchanged)
val_diff = _diff_vals(old, new, with_unchanged)
if val_diff:
return {"": val_diff}
return {}
def diff(old, new, with_unchanged=False):
paths = set(old.keys())
paths.update(set(new.keys()))
res = defaultdict(dict)
for path in paths:
path_diff = _diff(old.get(path), new.get(path), with_unchanged)
if path_diff:
res[path] = path_diff
return dict(res)
def table(header, rows, markdown=False):
from tabulate import tabulate
if not rows and not markdown:
return ""
ret = tabulate(
rows,
header,
tablefmt="github" if markdown else "plain",
disable_numparse=True,
# None will be shown as "" by default, overriding
missingval="—",
)
if markdown:
# NOTE: md table is incomplete without the trailing newline
ret += "\n"
return ret
def format_dict(d):
ret = {}
for key, val in d.items():
if isinstance(val, dict):
new_val = format_dict(val)
elif isinstance(val, list):
new_val = str(val)
else:
new_val = val
ret[key] = new_val
return ret