Skip to content

Commit c0d450e

Browse files
oestebaneffigies
andauthored
ENH: Stop printing false positive differences when logging cached nodes (#3376)
* enh: cast lists into tuples when printing inputs diffs * fix: correctly deal with dictionaries, insert ellipsis for very long diffs * sty: fix some style errors * Apply suggestions from code review Co-authored-by: Chris Markiewicz <[email protected]> * enh: apply comments from review Co-authored-by: Chris Markiewicz <[email protected]> * TEST: Thoroughly test dict_diff() * FIX: Correct call of indent; drop compatibility shim Co-authored-by: Chris Markiewicz <[email protected]> Co-authored-by: Christopher J. Markiewicz <[email protected]>
1 parent ab6c616 commit c0d450e

File tree

2 files changed

+81
-30
lines changed

2 files changed

+81
-30
lines changed

nipype/utils/misc.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,7 @@
1313

1414
import numpy as np
1515

16-
try:
17-
from textwrap import indent as textwrap_indent
18-
except ImportError:
19-
20-
def textwrap_indent(text, prefix):
21-
"""A textwrap.indent replacement for Python < 3.3"""
22-
if not prefix:
23-
return text
24-
splittext = text.splitlines(True)
25-
return prefix + prefix.join(splittext)
16+
import textwrap
2617

2718

2819
def human_order_sorted(l):
@@ -296,12 +287,16 @@ def dict_diff(dold, dnew, indent=0):
296287
297288
typical use -- log difference for hashed_inputs
298289
"""
299-
# First check inputs, since they usually are lists of tuples
300-
# and dicts are required.
301-
if isinstance(dnew, list):
302-
dnew = dict(dnew)
303-
if isinstance(dold, list):
304-
dold = dict(dold)
290+
try:
291+
dnew, dold = dict(dnew), dict(dold)
292+
except Exception:
293+
return textwrap.indent(
294+
f"""\
295+
Diff between nipype inputs failed:
296+
* Cached inputs: {dold}
297+
* New inputs: {dnew}""",
298+
" " * indent,
299+
)
305300

306301
# Compare against hashed_inputs
307302
# Keys: should rarely differ
@@ -321,26 +316,36 @@ def dict_diff(dold, dnew, indent=0):
321316

322317
diffkeys = len(diff)
323318

319+
def _shorten(value):
320+
if isinstance(value, str) and len(value) > 50:
321+
return f"{value[:10]}...{value[-10:]}"
322+
if isinstance(value, (tuple, list)) and len(value) > 10:
323+
return tuple(list(value[:2]) + ["..."] + list(value[-2:]))
324+
return value
325+
326+
def _uniformize(val):
327+
if isinstance(val, dict):
328+
return {k: _uniformize(v) for k, v in val.items()}
329+
if isinstance(val, (list, tuple)):
330+
return tuple(_uniformize(el) for el in val)
331+
return val
332+
324333
# Values in common keys would differ quite often,
325334
# so we need to join the messages together
326335
for k in new_keys.intersection(old_keys):
327-
try:
328-
new, old = dnew[k], dold[k]
329-
same = new == old
330-
if not same:
331-
# Since JSON does not discriminate between lists and
332-
# tuples, we might need to cast them into the same type
333-
# as the last resort. And lets try to be more generic
334-
same = old.__class__(new) == old
335-
except Exception:
336-
same = False
337-
if not same:
338-
diff += [" * %s: %r != %r" % (k, dnew[k], dold[k])]
336+
# Reading from JSON produces lists, but internally we typically
337+
# use tuples. At this point these dictionary values can be
338+
# immutable (and therefore the preference for tuple).
339+
new = _uniformize(dnew[k])
340+
old = _uniformize(dold[k])
341+
342+
if new != old:
343+
diff += [" * %s: %r != %r" % (k, _shorten(new), _shorten(old))]
339344

340345
if len(diff) > diffkeys:
341346
diff.insert(diffkeys, "Some dictionary entries had differing values:")
342347

343-
return textwrap_indent("\n".join(diff), " " * indent)
348+
return textwrap.indent("\n".join(diff), " " * indent)
344349

345350

346351
def rgetcwd(error=True):

nipype/utils/tests/test_misc.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
import pytest
88

9-
from nipype.utils.misc import container_to_string, str2bool, flatten, unflatten
9+
from nipype.utils.misc import (
10+
container_to_string,
11+
str2bool,
12+
flatten,
13+
unflatten,
14+
dict_diff,
15+
)
1016

1117

1218
def test_cont_to_str():
@@ -95,3 +101,43 @@ def test_rgetcwd(monkeypatch, tmpdir):
95101
monkeypatch.delenv("PWD")
96102
with pytest.raises(OSError):
97103
rgetcwd(error=False)
104+
105+
106+
def test_dict_diff():
107+
abtuple = [("a", "b")]
108+
abdict = dict(abtuple)
109+
110+
# Unchanged
111+
assert dict_diff(abdict, abdict) == ""
112+
assert dict_diff(abdict, abtuple) == ""
113+
assert dict_diff(abtuple, abdict) == ""
114+
assert dict_diff(abtuple, abtuple) == ""
115+
116+
# Changed keys
117+
diff = dict_diff({"a": "b"}, {"b": "a"})
118+
assert "Dictionaries had differing keys" in diff
119+
assert "keys not previously seen: {'b'}" in diff
120+
assert "keys not presently seen: {'a'}" in diff
121+
122+
# Trigger recursive uniformization
123+
complicated_val1 = [{"a": ["b"], "c": ("d", "e")}]
124+
complicated_val2 = [{"a": ["x"], "c": ("d", "e")}]
125+
uniformized_val1 = ({"a": ("b",), "c": ("d", "e")},)
126+
uniformized_val2 = ({"a": ("x",), "c": ("d", "e")},)
127+
128+
diff = dict_diff({"a": complicated_val1}, {"a": complicated_val2})
129+
assert "Some dictionary entries had differing values:" in diff
130+
assert "a: {!r} != {!r}".format(uniformized_val2, uniformized_val1) in diff
131+
132+
# Trigger shortening
133+
diff = dict_diff({"a": "b" * 60}, {"a": "c" * 70})
134+
assert "Some dictionary entries had differing values:" in diff
135+
assert "a: 'cccccccccc...cccccccccc' != 'bbbbbbbbbb...bbbbbbbbbb'" in diff
136+
137+
# Fail the dict conversion
138+
diff = dict_diff({}, "not a dict")
139+
assert diff == (
140+
"Diff between nipype inputs failed:\n"
141+
"* Cached inputs: {}\n"
142+
"* New inputs: not a dict"
143+
)

0 commit comments

Comments
 (0)