Add support for numpy arrays (and dicts) to approx.

This fixes #1994.  It turned out to require a lot of refactoring because
subclassing numpy.ndarray was necessary to coerce python into calling
the right `__eq__` operator.
This commit is contained in:
Kale Kundert
2017-06-11 19:27:41 -07:00
parent 467c526307
commit 9f3122fec6
2 changed files with 408 additions and 171 deletions

View File

@@ -9,7 +9,6 @@ from decimal import Decimal
from fractions import Fraction
inf, nan = float('inf'), float('nan')
class MyDocTestRunner(doctest.DocTestRunner):
def __init__(self):
@@ -29,12 +28,19 @@ class TestApprox(object):
if sys.version_info[:2] == (2, 6):
tol1, tol2, infr = '???', '???', '???'
assert repr(approx(1.0)) == '1.0 {pm} {tol1}'.format(pm=plus_minus, tol1=tol1)
assert repr(approx([1.0, 2.0])) == '1.0 {pm} {tol1}, 2.0 {pm} {tol2}'.format(pm=plus_minus, tol1=tol1, tol2=tol2)
assert repr(approx([1.0, 2.0])) == '[1.0 {pm} {tol1}, 2.0 {pm} {tol2}]'.format(pm=plus_minus, tol1=tol1, tol2=tol2)
assert repr(approx((1.0, 2.0))) == '(1.0 {pm} {tol1}, 2.0 {pm} {tol2})'.format(pm=plus_minus, tol1=tol1, tol2=tol2)
assert repr(approx(inf)) == 'inf'
assert repr(approx(1.0, rel=nan)) == '1.0 {pm} ???'.format(pm=plus_minus)
assert repr(approx(1.0, rel=inf)) == '1.0 {pm} {infr}'.format(pm=plus_minus, infr=infr)
assert repr(approx(1.0j, rel=inf)) == '1j'
# Dictionaries aren't ordered, so we need to check both orders.
assert repr(approx({'a': 1.0, 'b': 2.0})) in (
"{{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}}".format(pm=plus_minus, tol1=tol1, tol2=tol2),
"{{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}}".format(pm=plus_minus, tol1=tol1, tol2=tol2),
)
def test_operator_overloading(self):
assert 1 == approx(1, rel=1e-6, abs=1e-12)
assert not (1 != approx(1, rel=1e-6, abs=1e-12))
@@ -228,18 +234,38 @@ class TestApprox(object):
# tolerance, so only an absolute tolerance is calculated.
assert a != approx(x, abs=inf)
def test_expecting_sequence(self):
within_1e8 = [
(1e8 + 1e0, 1e8),
(1e0 + 1e-8, 1e0),
(1e-8 + 1e-16, 1e-8),
def test_int(self):
within_1e6 = [
(1000001, 1000000),
(-1000001, -1000000),
]
actual, expected = zip(*within_1e8)
assert actual == approx(expected, rel=5e-8, abs=0.0)
for a, x in within_1e6:
assert a == approx(x, rel=5e-6, abs=0)
assert a != approx(x, rel=5e-7, abs=0)
assert approx(x, rel=5e-6, abs=0) == a
assert approx(x, rel=5e-7, abs=0) != a
def test_expecting_sequence_wrong_len(self):
assert [1, 2] != approx([1])
assert [1, 2] != approx([1,2,3])
def test_decimal(self):
within_1e6 = [
(Decimal('1.000001'), Decimal('1.0')),
(Decimal('-1.000001'), Decimal('-1.0')),
]
for a, x in within_1e6:
assert a == approx(x, rel=Decimal('5e-6'), abs=0)
assert a != approx(x, rel=Decimal('5e-7'), abs=0)
assert approx(x, rel=Decimal('5e-6'), abs=0) == a
assert approx(x, rel=Decimal('5e-7'), abs=0) != a
def test_fraction(self):
within_1e6 = [
(1 + Fraction(1, 1000000), Fraction(1)),
(-1 - Fraction(-1, 1000000), Fraction(-1)),
]
for a, x in within_1e6:
assert a == approx(x, rel=5e-6, abs=0)
assert a != approx(x, rel=5e-7, abs=0)
assert approx(x, rel=5e-6, abs=0) == a
assert approx(x, rel=5e-7, abs=0) != a
def test_complex(self):
within_1e6 = [
@@ -251,33 +277,86 @@ class TestApprox(object):
for a, x in within_1e6:
assert a == approx(x, rel=5e-6, abs=0)
assert a != approx(x, rel=5e-7, abs=0)
assert approx(x, rel=5e-6, abs=0) == a
assert approx(x, rel=5e-7, abs=0) != a
def test_int(self):
within_1e6 = [
(1000001, 1000000),
(-1000001, -1000000),
]
for a, x in within_1e6:
assert a == approx(x, rel=5e-6, abs=0)
assert a != approx(x, rel=5e-7, abs=0)
def test_list(self):
actual = [1 + 1e-7, 2 + 1e-8]
expected = [1, 2]
def test_decimal(self):
within_1e6 = [
(Decimal('1.000001'), Decimal('1.0')),
(Decimal('-1.000001'), Decimal('-1.0')),
]
for a, x in within_1e6:
assert a == approx(x, rel=Decimal('5e-6'), abs=0)
assert a != approx(x, rel=Decimal('5e-7'), abs=0)
# Return false if any element is outside the tolerance.
assert actual == approx(expected, rel=5e-7, abs=0)
assert actual != approx(expected, rel=5e-8, abs=0)
assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual
def test_fraction(self):
within_1e6 = [
(1 + Fraction(1, 1000000), Fraction(1)),
(-1 - Fraction(-1, 1000000), Fraction(-1)),
]
for a, x in within_1e6:
assert a == approx(x, rel=5e-6, abs=0)
assert a != approx(x, rel=5e-7, abs=0)
def test_list_wrong_len(self):
assert [1, 2] != approx([1])
assert [1, 2] != approx([1,2,3])
def test_tuple(self):
actual = (1 + 1e-7, 2 + 1e-8)
expected = (1, 2)
# Return false if any element is outside the tolerance.
assert actual == approx(expected, rel=5e-7, abs=0)
assert actual != approx(expected, rel=5e-8, abs=0)
assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual
def test_tuple_wrong_len(self):
assert (1, 2) != approx((1,))
assert (1, 2) != approx((1,2,3))
def test_dict(self):
actual = {'a': 1 + 1e-7, 'b': 2 + 1e-8}
expected = {'b': 2, 'a': 1} # Dictionaries became ordered in python3.6,
# so make sure the order doesn't matter
# Return false if any element is outside the tolerance.
assert actual == approx(expected, rel=5e-7, abs=0)
assert actual != approx(expected, rel=5e-8, abs=0)
assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual
def test_dict_wrong_len(self):
assert {'a': 1, 'b': 2} != approx({'a': 1})
assert {'a': 1, 'b': 2} != approx({'a': 1, 'c': 2})
assert {'a': 1, 'b': 2} != approx({'a': 1, 'b': 2, 'c': 3})
def test_numpy_array(self):
try:
import numpy as np
except ImportError:
pytest.skip("numpy not installed")
actual = np.array([1 + 1e-7, 2 + 1e-8])
expected = np.array([1, 2])
# Return false if any element is outside the tolerance.
assert actual == approx(expected, rel=5e-7, abs=0)
assert actual != approx(expected, rel=5e-8, abs=0)
assert approx(expected, rel=5e-7, abs=0) == expected
assert approx(expected, rel=5e-8, abs=0) != actual
def test_numpy_array_wrong_shape(self):
try:
import numpy as np
except ImportError:
pytest.skip("numpy not installed")
import numpy as np
a12 = np.array([[1, 2]])
a21 = np.array([[1],[2]])
assert a12 != approx(a21)
assert a21 != approx(a12)
def test_non_number(self):
with pytest.raises(ValueError):
1 == approx("1")
with pytest.raises(ValueError):
"1" == approx(1)
def test_doctests(self):
parser = doctest.DocTestParser()