Merge pull request #7553 from tirkarthi/namedtuple-diff
Add support to display field names in namedtuple diffs.
This commit is contained in:
		
						commit
						1c18fb8ccc
					
				
							
								
								
									
										1
									
								
								AUTHORS
								
								
								
								
							
							
						
						
									
										1
									
								
								AUTHORS
								
								
								
								
							|  | @ -156,6 +156,7 @@ Justyna Janczyszyn | |||
| Kale Kundert | ||||
| Kamran Ahmad | ||||
| Karl O. Pinc | ||||
| Karthikeyan Singaravelan | ||||
| Katarzyna Jachim | ||||
| Katarzyna Król | ||||
| 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. | ||||
|  | @ -9,7 +9,6 @@ from typing import List | |||
| from typing import Mapping | ||||
| from typing import Optional | ||||
| from typing import Sequence | ||||
| from typing import Tuple | ||||
| 
 | ||||
| import _pytest._code | ||||
| from _pytest import outcomes | ||||
|  | @ -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 | ||||
| 
 | ||||
|  | @ -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): | ||||
|  | @ -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 = [] | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import collections | ||||
| import sys | ||||
| import textwrap | ||||
| from typing import Any | ||||
|  | @ -987,6 +988,44 @@ class TestAssert_reprcompare_attrsclass: | |||
|         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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue