Skip to content

Commit

Permalink
Use duck-typing to check for non-numeric types in approx()
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Dec 13, 2020
1 parent 7e2e663 commit e97863a
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 5 deletions.
6 changes: 6 additions & 0 deletions changelog/8132.bugfix.rst
@@ -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
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
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):
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)

0 comments on commit e97863a

Please sign in to comment.