Add support to display field names in namedtuple diffs.

This commit is contained in:
Karthikeyan Singaravelan 2020-07-28 12:24:24 +00:00 committed by Ran Benita
parent 5913cd20ec
commit 9a0f4e57ee
4 changed files with 58 additions and 3 deletions

View File

@ -155,6 +155,7 @@ Justyna Janczyszyn
Kale Kundert Kale Kundert
Kamran Ahmad Kamran Ahmad
Karl O. Pinc Karl O. Pinc
Karthikeyan Singaravelan
Katarzyna Jachim Katarzyna Jachim
Katarzyna Król Katarzyna Król
Katerina Koukiou Katerina Koukiou

View File

@ -0,0 +1 @@
When a comparison between `namedtuple` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes.

View File

@ -110,6 +110,10 @@ def isset(x: Any) -> bool:
return isinstance(x, (set, frozenset)) return isinstance(x, (set, frozenset))
def isnamedtuple(obj: Any) -> bool:
return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
def isdatacls(obj: Any) -> bool: def isdatacls(obj: Any) -> bool:
return getattr(obj, "__dataclass_fields__", None) is not None return getattr(obj, "__dataclass_fields__", None) is not None
@ -171,14 +175,20 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
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:
if issequence(left) and issequence(right): if type(left) == type(right) and (
isdatacls(left) or isattrs(left) or isnamedtuple(left)
):
# Note: unlike dataclasses/attrs, namedtuples compare only the
# field values, not the type or field names. But this branch
# intentionally only handles the same-type case, which was often
# used in older code bases before dataclasses/attrs were available.
explanation = _compare_eq_cls(left, right, verbose)
elif issequence(left) and issequence(right):
explanation = _compare_eq_sequence(left, right, verbose) explanation = _compare_eq_sequence(left, right, verbose)
elif isset(left) and isset(right): elif isset(left) and isset(right):
explanation = _compare_eq_set(left, right, verbose) explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right): elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose) explanation = _compare_eq_dict(left, right, verbose)
elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
explanation = _compare_eq_cls(left, right, verbose)
elif verbose > 0: elif verbose > 0:
explanation = _compare_eq_verbose(left, right) explanation = _compare_eq_verbose(left, right)
if isiterable(left) and isiterable(right): if isiterable(left) and isiterable(right):
@ -408,6 +418,10 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
elif isattrs(left): elif isattrs(left):
all_fields = left.__attrs_attrs__ all_fields = left.__attrs_attrs__
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
elif isnamedtuple(left):
fields_to_check = left._fields
else:
assert False
indent = " " indent = " "
same = [] same = []

View File

@ -1,3 +1,4 @@
import collections
import sys import sys
import textwrap import textwrap
from typing import Any from typing import Any
@ -987,6 +988,44 @@ class TestAssert_reprcompare_attrsclass:
assert lines is None assert lines is None
class TestAssert_reprcompare_namedtuple:
def test_namedtuple(self) -> None:
NT = collections.namedtuple("NT", ["a", "b"])
left = NT(1, "b")
right = NT(1, "c")
lines = callequal(left, right)
assert lines == [
"NT(a=1, b='b') == NT(a=1, b='c')",
"",
"Omitting 1 identical items, use -vv to show",
"Differing attributes:",
"['b']",
"",
"Drill down into differing attribute b:",
" b: 'b' != 'c'",
" - c",
" + b",
"Use -v to get the full diff",
]
def test_comparing_two_different_namedtuple(self) -> None:
NT1 = collections.namedtuple("NT1", ["a", "b"])
NT2 = collections.namedtuple("NT2", ["a", "b"])
left = NT1(1, "b")
right = NT2(2, "b")
lines = callequal(left, right)
# Because the types are different, uses the generic sequence matcher.
assert lines == [
"NT1(a=1, b='b') == NT2(a=2, b='b')",
"At index 0 diff: 1 != 2",
"Use -v to get the full diff",
]
class TestFormatExplanation: class TestFormatExplanation:
def test_special_chars_full(self, pytester: Pytester) -> None: def test_special_chars_full(self, pytester: Pytester) -> None:
# Issue 453, for the bug this would raise IndexError # Issue 453, for the bug this would raise IndexError