Add support to display field names in namedtuple diffs.
This commit is contained in:
parent
5913cd20ec
commit
9a0f4e57ee
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||||
|
|
|
@ -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.
|
|
@ -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 = []
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue