assertion: improve diff output of recursive dataclass/attrs

Co-authored-by: Vlad <uladzislau.radziuk@nordcloud.com>
This commit is contained in:
Vlad-Radz 2020-07-08 18:04:56 +02:00 committed by GitHub
parent 9caca5c434
commit 678c1a0745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 58 deletions

View File

@ -293,6 +293,7 @@ Vidar T. Fauske
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vlad Dragos Vlad Dragos
Vlad Radziuk
Vladyslav Rachek Vladyslav Rachek
Volodymyr Piskun Volodymyr Piskun
Wei Lin Wei Lin

View File

@ -0,0 +1 @@
Improve recursive diff report for comparison asserts on dataclasses / attrs.

View File

@ -169,7 +169,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
explanation = [] # type: List[str] explanation = []
if istext(left) and istext(right): if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose) explanation = _diff_text(left, right, verbose)
else: else:
@ -424,6 +424,7 @@ def _compare_eq_cls(
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
] ]
indent = " "
same = [] same = []
diff = [] diff = []
for field in fields_to_check: for field in fields_to_check:
@ -433,6 +434,8 @@ def _compare_eq_cls(
diff.append(field) diff.append(field)
explanation = [] explanation = []
if same or diff:
explanation += [""]
if same and verbose < 2: if same and verbose < 2:
explanation.append("Omitting %s identical items, use -vv to show" % len(same)) explanation.append("Omitting %s identical items, use -vv to show" % len(same))
elif same: elif same:
@ -440,12 +443,18 @@ def _compare_eq_cls(
explanation += pprint.pformat(same).splitlines() explanation += pprint.pformat(same).splitlines()
if diff: if diff:
explanation += ["Differing attributes:"] explanation += ["Differing attributes:"]
explanation += pprint.pformat(diff).splitlines()
for field in diff: for field in diff:
field_left = getattr(left, field)
field_right = getattr(right, field)
explanation += [ explanation += [
("%s: %r != %r") % (field, getattr(left, field), getattr(right, field)),
"", "",
"Drill down into differing attribute %s:" % field, "Drill down into differing attribute %s:" % field,
*_compare_eq_any(getattr(left, field), getattr(right, field), verbose), ("%s%s: %r != %r") % (indent, field, field_left, field_right),
]
explanation += [
indent + line
for line in _compare_eq_any(field_left, field_right, verbose)
] ]
return explanation return explanation

View File

@ -1,34 +1,38 @@
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field
@dataclass @dataclass
class SimpleDataObject: class S:
field_a: int = field() a: int
field_b: str = field() b: str
@dataclass @dataclass
class ComplexDataObject2: class C:
field_a: SimpleDataObject = field() c: S
field_b: SimpleDataObject = field() d: S
@dataclass @dataclass
class ComplexDataObject: class C2:
field_a: SimpleDataObject = field() e: C
field_b: ComplexDataObject2 = field() f: S
@dataclass
class C3:
g: S
h: C2
i: str
j: str
def test_recursive_dataclasses(): def test_recursive_dataclasses():
left = C3(
left = ComplexDataObject( S(10, "ten"), C2(C(S(1, "one"), S(2, "two")), S(2, "three")), "equal", "left",
SimpleDataObject(1, "b"),
ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(2, "c"),),
) )
right = ComplexDataObject( right = C3(
SimpleDataObject(1, "b"), S(20, "xxx"), C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), "equal", "right",
ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(3, "c"),),
) )
assert left == right assert left == right

View File

@ -778,12 +778,16 @@ class TestAssert_reprcompare_dataclass:
result.assert_outcomes(failed=1, passed=0) result.assert_outcomes(failed=1, passed=0)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*Omitting 1 identical items, use -vv to show*", "E Omitting 1 identical items, use -vv to show",
"*Differing attributes:*", "E Differing attributes:",
"*field_b: 'b' != 'c'*", "E ['field_b']",
"*- c*", "E ",
"*+ b*", "E Drill down into differing attribute field_b:",
] "E field_b: 'b' != 'c'...",
"E ",
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
],
consecutive=True,
) )
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@ -793,14 +797,16 @@ class TestAssert_reprcompare_dataclass:
result.assert_outcomes(failed=1, passed=0) result.assert_outcomes(failed=1, passed=0)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*Omitting 1 identical items, use -vv to show*", "E Omitting 1 identical items, use -vv to show",
"*Differing attributes:*", "E Differing attributes:",
"*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa "E ['g', 'h', 'j']",
"*Drill down into differing attribute field_b:*", "E ",
"*Omitting 1 identical items, use -vv to show*", "E Drill down into differing attribute g:",
"*Differing attributes:*", "E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
"*Full output truncated*", "E ",
] "E ...Full output truncated (52 lines hidden), use '-vv' to show",
],
consecutive=True,
) )
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@ -810,19 +816,30 @@ class TestAssert_reprcompare_dataclass:
result.assert_outcomes(failed=1, passed=0) result.assert_outcomes(failed=1, passed=0)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*Matching attributes:*", "E Matching attributes:",
"*['field_a']*", "E ['i']",
"*Differing attributes:*", "E Differing attributes:",
"*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa "E ['g', 'h', 'j']",
"*Matching attributes:*", "E ",
"*['field_a']*", "E Drill down into differing attribute g:",
"*Differing attributes:*", "E g: S(a=10, b='ten') != S(a=20, b='xxx')",
"*field_b: SimpleDataObject(field_a=2, field_b='c') != SimpleDataObject(field_a=3, field_b='c')*", # noqa "E ",
"*Matching attributes:*", "E Differing attributes:",
"*['field_b']*", "E ['a', 'b']",
"*Differing attributes:*", "E ",
"*field_a: 2 != 3", "E Drill down into differing attribute a:",
] "E a: 10 != 20",
"E +10",
"E -20",
"E ",
"E Drill down into differing attribute b:",
"E b: 'ten' != 'xxx'",
"E - xxx",
"E + ten",
"E ",
"E Drill down into differing attribute h:",
],
consecutive=True,
) )
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@ -868,9 +885,9 @@ class TestAssert_reprcompare_attrsclass:
lines = callequal(left, right) lines = callequal(left, right)
assert lines is not None assert lines is not None
assert lines[1].startswith("Omitting 1 identical item") assert lines[2].startswith("Omitting 1 identical item")
assert "Matching attributes" not in lines assert "Matching attributes" not in lines
for line in lines[1:]: for line in lines[2:]:
assert "field_a" not in line assert "field_a" not in line
def test_attrs_recursive(self) -> None: def test_attrs_recursive(self) -> None:
@ -910,7 +927,8 @@ class TestAssert_reprcompare_attrsclass:
lines = callequal(left, right) lines = callequal(left, right)
assert lines is not None assert lines is not None
assert "field_d: 'a' != 'b'" in lines # indentation in output because of nested object structure
assert " field_d: 'a' != 'b'" in lines
def test_attrs_verbose(self) -> None: def test_attrs_verbose(self) -> None:
@attr.s @attr.s
@ -923,9 +941,9 @@ class TestAssert_reprcompare_attrsclass:
lines = callequal(left, right, verbose=2) lines = callequal(left, right, verbose=2)
assert lines is not None assert lines is not None
assert lines[1].startswith("Matching attributes:") assert lines[2].startswith("Matching attributes:")
assert "Omitting" not in lines[1] assert "Omitting" not in lines[2]
assert lines[2] == "['field_a']" assert lines[3] == "['field_a']"
def test_attrs_with_attribute_comparison_off(self): def test_attrs_with_attribute_comparison_off(self):
@attr.s @attr.s
@ -937,11 +955,12 @@ class TestAssert_reprcompare_attrsclass:
right = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b")
lines = callequal(left, right, verbose=2) lines = callequal(left, right, verbose=2)
print(lines)
assert lines is not None assert lines is not None
assert lines[1].startswith("Matching attributes:") assert lines[2].startswith("Matching attributes:")
assert "Omitting" not in lines[1] assert "Omitting" not in lines[1]
assert lines[2] == "['field_a']" assert lines[3] == "['field_a']"
for line in lines[2:]: for line in lines[3:]:
assert "field_b" not in line assert "field_b" not in line
def test_comparing_two_different_attrs_classes(self): def test_comparing_two_different_attrs_classes(self):

View File

@ -233,7 +233,11 @@ if sys.version_info[:2] >= (3, 7):
E Matching attributes: E Matching attributes:
E ['b'] E ['b']
E Differing attributes: E Differing attributes:
E a: 1 != 2 E ['a']
E Drill down into differing attribute a:
E a: 1 != 2
E +1
E -2
""", """,
id="Compare data classes", id="Compare data classes",
), ),
@ -257,7 +261,11 @@ if sys.version_info[:2] >= (3, 7):
E Matching attributes: E Matching attributes:
E ['a'] E ['a']
E Differing attributes: E Differing attributes:
E b: 'spam' != 'eggs' E ['b']
E Drill down into differing attribute b:
E b: 'spam' != 'eggs'
E - eggs
E + spam
""", """,
id="Compare attrs classes", id="Compare attrs classes",
), ),