Skip to content

Commit 3bbadda

Browse files
authored
Merge pull request #9407 from yuvalshi0/remove-eq-format
Avoid specialized assert formatting when we detect that __eq__ is overridden
2 parents d8ff487 + 0ea039d commit 3bbadda

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

changelog/9326.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pytest will now avoid specialized assert formatting when it is detected that the default __eq__ is overridden

src/_pytest/assertion/util.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,27 @@ def isiterable(obj: Any) -> bool:
135135
return False
136136

137137

138+
def has_default_eq(
139+
obj: object,
140+
) -> bool:
141+
"""Check if an instance of an object contains the default eq
142+
143+
First, we check if the object's __eq__ attribute has __code__,
144+
if so, we check the equally of the method code filename (__code__.co_filename)
145+
to the default onces generated by the dataclass and attr module
146+
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
147+
"""
148+
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
149+
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
150+
code_filename = obj.__eq__.__code__.co_filename
151+
152+
if isattrs(obj):
153+
return "attrs generated eq" in code_filename
154+
155+
return code_filename == "<string>" # data class
156+
return True
157+
158+
138159
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
139160
"""Return specialised explanations for some operators/operands."""
140161
verbose = config.getoption("verbose")
@@ -427,6 +448,8 @@ def _compare_eq_dict(
427448

428449

429450
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
451+
if not has_default_eq(left):
452+
return []
430453
if isdatacls(left):
431454
all_fields = left.__dataclass_fields__
432455
fields_to_check = [field for field, info in all_fields.items() if info.compare]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from dataclasses import dataclass
2+
from dataclasses import field
3+
4+
5+
def test_dataclasses() -> None:
6+
@dataclass
7+
class SimpleDataObject:
8+
field_a: int = field()
9+
field_b: str = field()
10+
11+
def __eq__(self, __o: object) -> bool:
12+
return super().__eq__(__o)
13+
14+
left = SimpleDataObject(1, "b")
15+
right = SimpleDataObject(1, "c")
16+
17+
assert left == right

testing/test_assertion.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,16 @@ def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
899899
result = pytester.runpytest(p, "-vv")
900900
result.assert_outcomes(failed=0, passed=1)
901901

902+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
903+
def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None:
904+
p = pytester.copy_example(
905+
"dataclasses/test_compare_dataclasses_with_custom_eq.py"
906+
)
907+
# issue 9362
908+
result = pytester.runpytest(p, "-vv")
909+
result.assert_outcomes(failed=1, passed=0)
910+
result.stdout.no_re_match_line(".*Differing attributes.*")
911+
902912

903913
class TestAssert_reprcompare_attrsclass:
904914
def test_attrs(self) -> None:
@@ -982,7 +992,6 @@ class SimpleDataObject:
982992
right = SimpleDataObject(1, "b")
983993

984994
lines = callequal(left, right, verbose=2)
985-
print(lines)
986995
assert lines is not None
987996
assert lines[2].startswith("Matching attributes:")
988997
assert "Omitting" not in lines[1]
@@ -1007,6 +1016,36 @@ class SimpleDataObjectTwo:
10071016
lines = callequal(left, right)
10081017
assert lines is None
10091018

1019+
def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
1020+
@attr.s(
1021+
auto_detect=True
1022+
) # attr.s doesn’t ignore a custom eq if auto_detect=True
1023+
class SimpleDataObject:
1024+
field_a = attr.ib()
1025+
1026+
def __eq__(self, other): # pragma: no cover
1027+
return super().__eq__(other)
1028+
1029+
left = SimpleDataObject(1)
1030+
right = SimpleDataObject(2)
1031+
# issue 9362
1032+
lines = callequal(left, right, verbose=2)
1033+
assert lines is None
1034+
1035+
def test_attrs_with_custom_eq(self) -> None:
1036+
@attr.define
1037+
class SimpleDataObject:
1038+
field_a = attr.ib()
1039+
1040+
def __eq__(self, other): # pragma: no cover
1041+
return super().__eq__(other)
1042+
1043+
left = SimpleDataObject(1)
1044+
right = SimpleDataObject(2)
1045+
# issue 9362
1046+
lines = callequal(left, right, verbose=2)
1047+
assert lines is None
1048+
10101049

10111050
class TestAssert_reprcompare_namedtuple:
10121051
def test_namedtuple(self) -> None:

0 commit comments

Comments
 (0)