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

Use duck-typing to check for non-numeric types in approx() #8136

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/8132.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises
``TypeError`` when dealing with non-numeric types, falling back to normal comparison,
however the check was done using ``isinstance`` which left out types which implemented
the necessary methods for ``approx`` to work, such as tensorflow's ``DeviceArray``.

The code has been changed to check for the necessary methods to accommodate those cases.
15 changes: 10 additions & 5 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,17 @@ def __eq__(self, actual) -> bool:
if actual == self.expected:
return True

# If either type is non-numeric, fall back to strict equality.
# NB: we need Complex, rather than just Number, to ensure that __abs__,
# __sub__, and __float__ are defined.
# Check types are non-numeric using duck-typing; if they are not numeric types,
# we consider them unequal because the short-circuit above failed.
required_attrs = [
"__abs__",
"__float__",
"__rsub__",
"__sub__",
]
if not (
isinstance(self.expected, (Complex, Decimal))
and isinstance(actual, (Complex, Decimal))
all(hasattr(self.expected, attr) for attr in required_attrs)
and all(hasattr(actual, attr) for attr in required_attrs)
):
return False

Expand Down
36 changes: 36 additions & 0 deletions testing/python/approx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from operator import ne
from typing import Optional

import attr

import pytest
from _pytest.pytester import Pytester
from pytest import approx
Expand Down Expand Up @@ -582,3 +584,37 @@ def __len__(self):

expected = MySizedIterable()
assert [1, 2, 3, 4] == approx(expected)

def test_duck_typing(self):
"""
Check that approx() works for objects which implemented the required
numeric methods (#8132).
"""

@attr.s(auto_attribs=True)
class Container:
value: float

def __abs__(self) -> float:
return abs(self.value)

def __sub__(self, other):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try type-annotating the return value like this:

Suggested change
def __sub__(self, other):
def __sub__(self, other: Any) -> Union[float, "Container"]:

However mypy complained with:

testing/python/approx.py:601:59: F821 undefined name 'Container'
testing/python/approx.py:608:60: F821 undefined name 'Container'

Is that because it is an inner class? Suggestions welcome. 👍

if isinstance(other, Container):
return Container(self.value - other.value)
elif isinstance(other, (float, int)):
return self.value - other
return NotImplemented

def __rsub__(self, other):
if isinstance(other, Container):
return other.value - self.value
elif isinstance(other, (float, int)):
return other - self.value
return NotImplemented

def __float__(self) -> float:
return self.value

assert Container(1.0) == approx(1 + 1e-7, rel=5e-7)
assert Container(1.0) != approx(1 + 1e-7, rel=1e-8)
assert Container(1.0) == approx(1.0)