Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid specialized assert formatting when we detect that __eq__ is overridden #9407

Merged
merged 10 commits into from Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/9326.bugfix.rst
@@ -0,0 +1 @@
Pytest will now avoid specialized assert formatting when it is detected that the default __eq__ is overridden
23 changes: 23 additions & 0 deletions src/_pytest/assertion/util.py
Expand Up @@ -135,6 +135,27 @@ def isiterable(obj: Any) -> bool:
return False


def has_default_eq(
obj: object,
) -> bool:
"""Check if an instance of an object contains the default eq

First, we check if the object's __eq__ attribute has __code__,
if so, we check the equally of the method code filename (__code__.co_filename)
to the default onces generated by the dataclass and attr module
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
"""
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
code_filename = obj.__eq__.__code__.co_filename

if isattrs(obj):
return "attrs generated eq" in code_filename

return code_filename == "<string>" # data class
return True


def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
Expand Down Expand Up @@ -427,6 +448,8 @@ def _compare_eq_dict(


def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if not has_default_eq(left):
return []
if isdatacls(left):
all_fields = left.__dataclass_fields__
fields_to_check = [field for field, info in all_fields.items() if info.compare]
Expand Down
@@ -0,0 +1,17 @@
from dataclasses import dataclass
from dataclasses import field


def test_dataclasses() -> None:
@dataclass
class SimpleDataObject:
field_a: int = field()
field_b: str = field()

def __eq__(self, __o: object) -> bool:
return super().__eq__(__o)

left = SimpleDataObject(1, "b")
right = SimpleDataObject(1, "c")

assert left == right
41 changes: 40 additions & 1 deletion testing/test_assertion.py
Expand Up @@ -899,6 +899,16 @@ def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
result = pytester.runpytest(p, "-vv")
result.assert_outcomes(failed=0, passed=1)

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


class TestAssert_reprcompare_attrsclass:
def test_attrs(self) -> None:
Expand Down Expand Up @@ -982,7 +992,6 @@ class SimpleDataObject:
right = SimpleDataObject(1, "b")

lines = callequal(left, right, verbose=2)
print(lines)
assert lines is not None
assert lines[2].startswith("Matching attributes:")
assert "Omitting" not in lines[1]
Expand All @@ -1007,6 +1016,36 @@ class SimpleDataObjectTwo:
lines = callequal(left, right)
assert lines is None

def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
@attr.s(
auto_detect=True
) # attr.s doesn’t ignore a custom eq if auto_detect=True
class SimpleDataObject:
field_a = attr.ib()

def __eq__(self, other): # pragma: no cover
return super().__eq__(other)

left = SimpleDataObject(1)
right = SimpleDataObject(2)
# issue 9362
lines = callequal(left, right, verbose=2)
assert lines is None

def test_attrs_with_custom_eq(self) -> None:
@attr.define
class SimpleDataObject:
field_a = attr.ib()

def __eq__(self, other): # pragma: no cover
return super().__eq__(other)

left = SimpleDataObject(1)
right = SimpleDataObject(2)
# issue 9362
lines = callequal(left, right, verbose=2)
assert lines is None


class TestAssert_reprcompare_namedtuple:
def test_namedtuple(self) -> None:
Expand Down