From 678c1a0745f1cf175c442c719906a1f13e496910 Mon Sep 17 00:00:00 2001 From: Vlad-Radz <57449367+Vlad-Radz@users.noreply.github.com> Date: Wed, 8 Jul 2020 18:04:56 +0200 Subject: [PATCH] assertion: improve diff output of recursive dataclass/attrs Co-authored-by: Vlad --- AUTHORS | 1 + changelog/7348.improvement.rst | 1 + src/_pytest/assertion/util.py | 15 ++- .../test_compare_recursive_dataclasses.py | 38 ++++---- testing/test_assertion.py | 91 +++++++++++-------- testing/test_error_diffs.py | 12 ++- 6 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 changelog/7348.improvement.rst diff --git a/AUTHORS b/AUTHORS index e40237682..4cdf231b1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -293,6 +293,7 @@ Vidar T. Fauske Virgil Dupras Vitaly Lashmanov Vlad Dragos +Vlad Radziuk Vladyslav Rachek Volodymyr Piskun Wei Lin diff --git a/changelog/7348.improvement.rst b/changelog/7348.improvement.rst new file mode 100644 index 000000000..714892e6e --- /dev/null +++ b/changelog/7348.improvement.rst @@ -0,0 +1 @@ +Improve recursive diff report for comparison asserts on dataclasses / attrs. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c2f0431d4..554aec77f 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -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]: - explanation = [] # type: List[str] + explanation = [] if istext(left) and istext(right): explanation = _diff_text(left, right, verbose) else: @@ -424,6 +424,7 @@ def _compare_eq_cls( field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) ] + indent = " " same = [] diff = [] for field in fields_to_check: @@ -433,6 +434,8 @@ def _compare_eq_cls( diff.append(field) explanation = [] + if same or diff: + explanation += [""] if same and verbose < 2: explanation.append("Omitting %s identical items, use -vv to show" % len(same)) elif same: @@ -440,12 +443,18 @@ def _compare_eq_cls( explanation += pprint.pformat(same).splitlines() if diff: explanation += ["Differing attributes:"] + explanation += pprint.pformat(diff).splitlines() for field in diff: + field_left = getattr(left, field) + field_right = getattr(right, field) explanation += [ - ("%s: %r != %r") % (field, getattr(left, field), getattr(right, 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 diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 516e36e5c..167140e16 100644 --- a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -1,34 +1,38 @@ from dataclasses import dataclass -from dataclasses import field @dataclass -class SimpleDataObject: - field_a: int = field() - field_b: str = field() +class S: + a: int + b: str @dataclass -class ComplexDataObject2: - field_a: SimpleDataObject = field() - field_b: SimpleDataObject = field() +class C: + c: S + d: S @dataclass -class ComplexDataObject: - field_a: SimpleDataObject = field() - field_b: ComplexDataObject2 = field() +class C2: + e: C + f: S + + +@dataclass +class C3: + g: S + h: C2 + i: str + j: str def test_recursive_dataclasses(): - - left = ComplexDataObject( - SimpleDataObject(1, "b"), - ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(2, "c"),), + left = C3( + S(10, "ten"), C2(C(S(1, "one"), S(2, "two")), S(2, "three")), "equal", "left", ) - right = ComplexDataObject( - SimpleDataObject(1, "b"), - ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(3, "c"),), + right = C3( + S(20, "xxx"), C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), "equal", "right", ) assert left == right diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5c9bb35fa..6723a707e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -778,12 +778,16 @@ class TestAssert_reprcompare_dataclass: result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*field_b: 'b' != 'c'*", - "*- c*", - "*+ b*", - ] + "E Omitting 1 identical items, use -vv to show", + "E Differing attributes:", + "E ['field_b']", + "E ", + "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+") @@ -793,14 +797,16 @@ class TestAssert_reprcompare_dataclass: result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa - "*Drill down into differing attribute field_b:*", - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*Full output truncated*", - ] + "E Omitting 1 identical items, use -vv to show", + "E Differing attributes:", + "E ['g', 'h', 'j']", + "E ", + "E Drill down into differing attribute g:", + "E g: S(a=10, b='ten') != S(a=20, b='xxx')...", + "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+") @@ -810,19 +816,30 @@ class TestAssert_reprcompare_dataclass: result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Matching attributes:*", - "*['field_a']*", - "*Differing attributes:*", - "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa - "*Matching attributes:*", - "*['field_a']*", - "*Differing attributes:*", - "*field_b: SimpleDataObject(field_a=2, field_b='c') != SimpleDataObject(field_a=3, field_b='c')*", # noqa - "*Matching attributes:*", - "*['field_b']*", - "*Differing attributes:*", - "*field_a: 2 != 3", - ] + "E Matching attributes:", + "E ['i']", + "E Differing attributes:", + "E ['g', 'h', 'j']", + "E ", + "E Drill down into differing attribute g:", + "E g: S(a=10, b='ten') != S(a=20, b='xxx')", + "E ", + "E Differing attributes:", + "E ['a', 'b']", + "E ", + "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+") @@ -868,9 +885,9 @@ class TestAssert_reprcompare_attrsclass: lines = callequal(left, right) 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 - for line in lines[1:]: + for line in lines[2:]: assert "field_a" not in line def test_attrs_recursive(self) -> None: @@ -910,7 +927,8 @@ class TestAssert_reprcompare_attrsclass: lines = callequal(left, right) 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: @attr.s @@ -923,9 +941,9 @@ class TestAssert_reprcompare_attrsclass: lines = callequal(left, right, verbose=2) assert lines is not None - assert lines[1].startswith("Matching attributes:") - assert "Omitting" not in lines[1] - assert lines[2] == "['field_a']" + assert lines[2].startswith("Matching attributes:") + assert "Omitting" not in lines[2] + assert lines[3] == "['field_a']" def test_attrs_with_attribute_comparison_off(self): @attr.s @@ -937,11 +955,12 @@ class TestAssert_reprcompare_attrsclass: right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) + print(lines) assert lines is not None - assert lines[1].startswith("Matching attributes:") + assert lines[2].startswith("Matching attributes:") assert "Omitting" not in lines[1] - assert lines[2] == "['field_a']" - for line in lines[2:]: + assert lines[3] == "['field_a']" + for line in lines[3:]: assert "field_b" not in line def test_comparing_two_different_attrs_classes(self): diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 473c62a75..2857df832 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -233,7 +233,11 @@ if sys.version_info[:2] >= (3, 7): E Matching attributes: E ['b'] 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", ), @@ -257,7 +261,11 @@ if sys.version_info[:2] >= (3, 7): E Matching attributes: E ['a'] 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", ),