Skip to content

Commit

Permalink
Merge pull request pytest-dev#7553 from tirkarthi/namedtuple-diff
Browse files Browse the repository at this point in the history
Add support to display field names in namedtuple diffs.
  • Loading branch information
bluetech committed Oct 31, 2020
2 parents 2753859 + 9a0f4e5 commit 1c18fb8
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 12 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -156,6 +156,7 @@ Justyna Janczyszyn
Kale Kundert
Kamran Ahmad
Karl O. Pinc
Karthikeyan Singaravelan
Katarzyna Jachim
Katarzyna Król
Katerina Koukiou
Expand Down
1 change: 1 addition & 0 deletions changelog/7527.improvement.rst
@@ -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.
30 changes: 18 additions & 12 deletions src/_pytest/assertion/util.py
Expand Up @@ -9,7 +9,6 @@
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple

import _pytest._code
from _pytest import outcomes
Expand Down Expand Up @@ -111,6 +110,10 @@ def isset(x: Any) -> bool:
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:
return getattr(obj, "__dataclass_fields__", None) is not None

Expand Down Expand Up @@ -172,15 +175,20 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
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)
elif isset(left) and isset(right):
explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose)
elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
type_fn = (isdatacls, isattrs)
explanation = _compare_eq_cls(left, right, verbose, type_fn)
elif verbose > 0:
explanation = _compare_eq_verbose(left, right)
if isiterable(left) and isiterable(right):
Expand Down Expand Up @@ -403,19 +411,17 @@ def _compare_eq_dict(
return explanation


def _compare_eq_cls(
left: Any,
right: Any,
verbose: int,
type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]],
) -> List[str]:
isdatacls, isattrs = type_fns
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if isdatacls(left):
all_fields = left.__dataclass_fields__
fields_to_check = [field for field, info in all_fields.items() if info.compare]
elif isattrs(left):
all_fields = left.__attrs_attrs__
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 = " "
same = []
Expand Down
39 changes: 39 additions & 0 deletions testing/test_assertion.py
@@ -1,3 +1,4 @@
import collections
import sys
import textwrap
from typing import Any
Expand Down Expand Up @@ -987,6 +988,44 @@ class SimpleDataObjectTwo:
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:
def test_special_chars_full(self, pytester: Pytester) -> None:
# Issue 453, for the bug this would raise IndexError
Expand Down

0 comments on commit 1c18fb8

Please sign in to comment.