Skip to content

Commit 678c1a0

Browse files
Vlad-Radzvlad-nc
andauthored
assertion: improve diff output of recursive dataclass/attrs
Co-authored-by: Vlad <[email protected]>
1 parent 9caca5c commit 678c1a0

File tree

6 files changed

+100
-58
lines changed

6 files changed

+100
-58
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ Vidar T. Fauske
293293
Virgil Dupras
294294
Vitaly Lashmanov
295295
Vlad Dragos
296+
Vlad Radziuk
296297
Vladyslav Rachek
297298
Volodymyr Piskun
298299
Wei Lin

changelog/7348.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve recursive diff report for comparison asserts on dataclasses / attrs.

src/_pytest/assertion/util.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
169169

170170

171171
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
172-
explanation = [] # type: List[str]
172+
explanation = []
173173
if istext(left) and istext(right):
174174
explanation = _diff_text(left, right, verbose)
175175
else:
@@ -424,6 +424,7 @@ def _compare_eq_cls(
424424
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
425425
]
426426

427+
indent = " "
427428
same = []
428429
diff = []
429430
for field in fields_to_check:
@@ -433,19 +434,27 @@ def _compare_eq_cls(
433434
diff.append(field)
434435

435436
explanation = []
437+
if same or diff:
438+
explanation += [""]
436439
if same and verbose < 2:
437440
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
438441
elif same:
439442
explanation += ["Matching attributes:"]
440443
explanation += pprint.pformat(same).splitlines()
441444
if diff:
442445
explanation += ["Differing attributes:"]
446+
explanation += pprint.pformat(diff).splitlines()
443447
for field in diff:
448+
field_left = getattr(left, field)
449+
field_right = getattr(right, field)
444450
explanation += [
445-
("%s: %r != %r") % (field, getattr(left, field), getattr(right, field)),
446451
"",
447452
"Drill down into differing attribute %s:" % field,
448-
*_compare_eq_any(getattr(left, field), getattr(right, field), verbose),
453+
("%s%s: %r != %r") % (indent, field, field_left, field_right),
454+
]
455+
explanation += [
456+
indent + line
457+
for line in _compare_eq_any(field_left, field_right, verbose)
449458
]
450459
return explanation
451460

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
from dataclasses import dataclass
2-
from dataclasses import field
32

43

54
@dataclass
6-
class SimpleDataObject:
7-
field_a: int = field()
8-
field_b: str = field()
5+
class S:
6+
a: int
7+
b: str
98

109

1110
@dataclass
12-
class ComplexDataObject2:
13-
field_a: SimpleDataObject = field()
14-
field_b: SimpleDataObject = field()
11+
class C:
12+
c: S
13+
d: S
1514

1615

1716
@dataclass
18-
class ComplexDataObject:
19-
field_a: SimpleDataObject = field()
20-
field_b: ComplexDataObject2 = field()
17+
class C2:
18+
e: C
19+
f: S
2120

2221

23-
def test_recursive_dataclasses():
22+
@dataclass
23+
class C3:
24+
g: S
25+
h: C2
26+
i: str
27+
j: str
2428

25-
left = ComplexDataObject(
26-
SimpleDataObject(1, "b"),
27-
ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(2, "c"),),
29+
30+
def test_recursive_dataclasses():
31+
left = C3(
32+
S(10, "ten"), C2(C(S(1, "one"), S(2, "two")), S(2, "three")), "equal", "left",
2833
)
29-
right = ComplexDataObject(
30-
SimpleDataObject(1, "b"),
31-
ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(3, "c"),),
34+
right = C3(
35+
S(20, "xxx"), C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), "equal", "right",
3236
)
3337

3438
assert left == right

testing/test_assertion.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -778,12 +778,16 @@ def test_dataclasses(self, testdir):
778778
result.assert_outcomes(failed=1, passed=0)
779779
result.stdout.fnmatch_lines(
780780
[
781-
"*Omitting 1 identical items, use -vv to show*",
782-
"*Differing attributes:*",
783-
"*field_b: 'b' != 'c'*",
784-
"*- c*",
785-
"*+ b*",
786-
]
781+
"E Omitting 1 identical items, use -vv to show",
782+
"E Differing attributes:",
783+
"E ['field_b']",
784+
"E ",
785+
"E Drill down into differing attribute field_b:",
786+
"E field_b: 'b' != 'c'...",
787+
"E ",
788+
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
789+
],
790+
consecutive=True,
787791
)
788792

789793
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@@ -793,14 +797,16 @@ def test_recursive_dataclasses(self, testdir):
793797
result.assert_outcomes(failed=1, passed=0)
794798
result.stdout.fnmatch_lines(
795799
[
796-
"*Omitting 1 identical items, use -vv to show*",
797-
"*Differing attributes:*",
798-
"*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa
799-
"*Drill down into differing attribute field_b:*",
800-
"*Omitting 1 identical items, use -vv to show*",
801-
"*Differing attributes:*",
802-
"*Full output truncated*",
803-
]
800+
"E Omitting 1 identical items, use -vv to show",
801+
"E Differing attributes:",
802+
"E ['g', 'h', 'j']",
803+
"E ",
804+
"E Drill down into differing attribute g:",
805+
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
806+
"E ",
807+
"E ...Full output truncated (52 lines hidden), use '-vv' to show",
808+
],
809+
consecutive=True,
804810
)
805811

806812
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@@ -810,19 +816,30 @@ def test_recursive_dataclasses_verbose(self, testdir):
810816
result.assert_outcomes(failed=1, passed=0)
811817
result.stdout.fnmatch_lines(
812818
[
813-
"*Matching attributes:*",
814-
"*['field_a']*",
815-
"*Differing attributes:*",
816-
"*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa
817-
"*Matching attributes:*",
818-
"*['field_a']*",
819-
"*Differing attributes:*",
820-
"*field_b: SimpleDataObject(field_a=2, field_b='c') != SimpleDataObject(field_a=3, field_b='c')*", # noqa
821-
"*Matching attributes:*",
822-
"*['field_b']*",
823-
"*Differing attributes:*",
824-
"*field_a: 2 != 3",
825-
]
819+
"E Matching attributes:",
820+
"E ['i']",
821+
"E Differing attributes:",
822+
"E ['g', 'h', 'j']",
823+
"E ",
824+
"E Drill down into differing attribute g:",
825+
"E g: S(a=10, b='ten') != S(a=20, b='xxx')",
826+
"E ",
827+
"E Differing attributes:",
828+
"E ['a', 'b']",
829+
"E ",
830+
"E Drill down into differing attribute a:",
831+
"E a: 10 != 20",
832+
"E +10",
833+
"E -20",
834+
"E ",
835+
"E Drill down into differing attribute b:",
836+
"E b: 'ten' != 'xxx'",
837+
"E - xxx",
838+
"E + ten",
839+
"E ",
840+
"E Drill down into differing attribute h:",
841+
],
842+
consecutive=True,
826843
)
827844

828845
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
@@ -868,9 +885,9 @@ class SimpleDataObject:
868885

869886
lines = callequal(left, right)
870887
assert lines is not None
871-
assert lines[1].startswith("Omitting 1 identical item")
888+
assert lines[2].startswith("Omitting 1 identical item")
872889
assert "Matching attributes" not in lines
873-
for line in lines[1:]:
890+
for line in lines[2:]:
874891
assert "field_a" not in line
875892

876893
def test_attrs_recursive(self) -> None:
@@ -910,7 +927,8 @@ class SimpleDataObject:
910927

911928
lines = callequal(left, right)
912929
assert lines is not None
913-
assert "field_d: 'a' != 'b'" in lines
930+
# indentation in output because of nested object structure
931+
assert " field_d: 'a' != 'b'" in lines
914932

915933
def test_attrs_verbose(self) -> None:
916934
@attr.s
@@ -923,9 +941,9 @@ class SimpleDataObject:
923941

924942
lines = callequal(left, right, verbose=2)
925943
assert lines is not None
926-
assert lines[1].startswith("Matching attributes:")
927-
assert "Omitting" not in lines[1]
928-
assert lines[2] == "['field_a']"
944+
assert lines[2].startswith("Matching attributes:")
945+
assert "Omitting" not in lines[2]
946+
assert lines[3] == "['field_a']"
929947

930948
def test_attrs_with_attribute_comparison_off(self):
931949
@attr.s
@@ -937,11 +955,12 @@ class SimpleDataObject:
937955
right = SimpleDataObject(1, "b")
938956

939957
lines = callequal(left, right, verbose=2)
958+
print(lines)
940959
assert lines is not None
941-
assert lines[1].startswith("Matching attributes:")
960+
assert lines[2].startswith("Matching attributes:")
942961
assert "Omitting" not in lines[1]
943-
assert lines[2] == "['field_a']"
944-
for line in lines[2:]:
962+
assert lines[3] == "['field_a']"
963+
for line in lines[3:]:
945964
assert "field_b" not in line
946965

947966
def test_comparing_two_different_attrs_classes(self):

testing/test_error_diffs.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ def test_this():
233233
E Matching attributes:
234234
E ['b']
235235
E Differing attributes:
236-
E a: 1 != 2
236+
E ['a']
237+
E Drill down into differing attribute a:
238+
E a: 1 != 2
239+
E +1
240+
E -2
237241
""",
238242
id="Compare data classes",
239243
),
@@ -257,7 +261,11 @@ def test_this():
257261
E Matching attributes:
258262
E ['a']
259263
E Differing attributes:
260-
E b: 'spam' != 'eggs'
264+
E ['b']
265+
E Drill down into differing attribute b:
266+
E b: 'spam' != 'eggs'
267+
E - eggs
268+
E + spam
261269
""",
262270
id="Compare attrs classes",
263271
),

0 commit comments

Comments
 (0)