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 | 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. | ||||||
|  | @ -9,7 +9,6 @@ from typing import List | ||||||
| from typing import Mapping | from typing import Mapping | ||||||
| from typing import Optional | from typing import Optional | ||||||
| from typing import Sequence | from typing import Sequence | ||||||
| from typing import Tuple |  | ||||||
| 
 | 
 | ||||||
| import _pytest._code | import _pytest._code | ||||||
| from _pytest import outcomes | from _pytest import outcomes | ||||||
|  | @ -111,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 | ||||||
| 
 | 
 | ||||||
|  | @ -172,15 +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)): |  | ||||||
|             type_fn = (isdatacls, isattrs) |  | ||||||
|             explanation = _compare_eq_cls(left, right, verbose, type_fn) |  | ||||||
|         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): | ||||||
|  | @ -403,19 +411,17 @@ def _compare_eq_dict( | ||||||
|     return explanation |     return explanation | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _compare_eq_cls( | def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: | ||||||
|     left: Any, |  | ||||||
|     right: Any, |  | ||||||
|     verbose: int, |  | ||||||
|     type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]], |  | ||||||
| ) -> List[str]: |  | ||||||
|     isdatacls, isattrs = type_fns |  | ||||||
|     if isdatacls(left): |     if isdatacls(left): | ||||||
|         all_fields = left.__dataclass_fields__ |         all_fields = left.__dataclass_fields__ | ||||||
|         fields_to_check = [field for field, info in all_fields.items() if info.compare] |         fields_to_check = [field for field, info in all_fields.items() if info.compare] | ||||||
|     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