Skip to content

Commit e97863a

Browse files
committed
Use duck-typing to check for non-numeric types in approx()
Fix pytest-dev#8132
1 parent 7e2e663 commit e97863a

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

changelog/8132.bugfix.rst

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises
2+
``TypeError`` when dealing with non-numeric types, falling back to normal comparison,
3+
however the check was done using ``isinstance`` which left out types which implemented
4+
the necessary methods for ``approx`` to work, such as tensorflow's ``DeviceArray``.
5+
6+
The code has been changed to check for the necessary methods to accommodate those cases.

src/_pytest/python_api.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,17 @@ def __eq__(self, actual) -> bool:
241241
if actual == self.expected:
242242
return True
243243

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

testing/python/approx.py

+36
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from operator import ne
77
from typing import Optional
88

9+
import attr
10+
911
import pytest
1012
from _pytest.pytester import Pytester
1113
from pytest import approx
@@ -582,3 +584,37 @@ def __len__(self):
582584

583585
expected = MySizedIterable()
584586
assert [1, 2, 3, 4] == approx(expected)
587+
588+
def test_duck_typing(self):
589+
"""
590+
Check that approx() works for objects which implemented the required
591+
numeric methods (#8132).
592+
"""
593+
594+
@attr.s(auto_attribs=True)
595+
class Container:
596+
value: float
597+
598+
def __abs__(self) -> float:
599+
return abs(self.value)
600+
601+
def __sub__(self, other):
602+
if isinstance(other, Container):
603+
return Container(self.value - other.value)
604+
elif isinstance(other, (float, int)):
605+
return self.value - other
606+
return NotImplemented
607+
608+
def __rsub__(self, other):
609+
if isinstance(other, Container):
610+
return other.value - self.value
611+
elif isinstance(other, (float, int)):
612+
return other - self.value
613+
return NotImplemented
614+
615+
def __float__(self) -> float:
616+
return self.value
617+
618+
assert Container(1.0) == approx(1 + 1e-7, rel=5e-7)
619+
assert Container(1.0) != approx(1 + 1e-7, rel=1e-8)
620+
assert Container(1.0) == approx(1.0)

0 commit comments

Comments
 (0)