Merge pull request #2492 from kalekundert/features
Add support for numpy arrays (and dicts) to approx.
This commit is contained in:
commit
ef62b86335
|
@ -20,9 +20,11 @@ env:
|
|||
- TOXENV=py27-pexpect
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py27-numpy
|
||||
- TOXENV=py35-pexpect
|
||||
- TOXENV=py35-xdist
|
||||
- TOXENV=py35-trial
|
||||
- TOXENV=py35-numpy
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=freeze
|
||||
|
|
|
@ -125,6 +125,7 @@ if sys.version_info[:2] == (2, 6):
|
|||
if _PY3:
|
||||
import codecs
|
||||
imap = map
|
||||
izip = zip
|
||||
STRING_TYPES = bytes, str
|
||||
UNICODE_TYPES = str,
|
||||
|
||||
|
@ -160,7 +161,7 @@ else:
|
|||
STRING_TYPES = bytes, str, unicode
|
||||
UNICODE_TYPES = unicode,
|
||||
|
||||
from itertools import imap # NOQA
|
||||
from itertools import imap, izip # NOQA
|
||||
|
||||
def _escape_strings(val):
|
||||
"""In py2 bytes and str are the same type, so return if it's a bytes
|
||||
|
|
|
@ -3,13 +3,279 @@ import sys
|
|||
|
||||
import py
|
||||
|
||||
from _pytest.compat import isclass
|
||||
from _pytest.compat import isclass, izip
|
||||
from _pytest.runner import fail
|
||||
import _pytest._code
|
||||
|
||||
# builtin pytest.approx helper
|
||||
|
||||
class ApproxBase(object):
|
||||
"""
|
||||
Provide shared utilities for making approximate comparisons between numbers
|
||||
or sequences of numbers.
|
||||
"""
|
||||
|
||||
class approx(object):
|
||||
def __init__(self, expected, rel=None, abs=None, nan_ok=False):
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
self.nan_ok = nan_ok
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __eq__(self, actual):
|
||||
return all(
|
||||
a == self._approx_scalar(x)
|
||||
for a, x in self._yield_comparisons(actual))
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, actual):
|
||||
return not (actual == self)
|
||||
|
||||
def _approx_scalar(self, x):
|
||||
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
"""
|
||||
Yield all the pairs of numbers to be compared. This is used to
|
||||
implement the `__eq__` method.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ApproxNumpyBase(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for numpy arrays.
|
||||
|
||||
This class should not be used directly. Instead, the `inherit_ndarray()`
|
||||
class method should be used to make a subclass that also inherits from
|
||||
`np.ndarray`. This indirection is necessary because the object doing the
|
||||
approximate comparison must inherit from `np.ndarray`, or it will only work
|
||||
on the left side of the `==` operator. But importing numpy is relatively
|
||||
expensive, so we also want to avoid that unless we actually have a numpy
|
||||
array to compare.
|
||||
|
||||
The reason why the approx object needs to inherit from `np.ndarray` has to
|
||||
do with how python decides whether to call `a.__eq__()` or `b.__eq__()`
|
||||
when it parses `a == b`. If `a` and `b` are not related by inheritance,
|
||||
`a` gets priority. So as long as `a.__eq__` is defined, it will be called.
|
||||
Because most implementations of `a.__eq__` end up calling `b.__eq__`, this
|
||||
detail usually doesn't matter. However, `np.ndarray.__eq__` treats the
|
||||
approx object as a scalar and builds a new array by comparing it to each
|
||||
item in the original array. `b.__eq__` is called to compare against each
|
||||
individual element in the array, but it has no way (that I can see) to
|
||||
prevent the return value from being an boolean array, and boolean arrays
|
||||
can't be used with assert because "the truth value of an array with more
|
||||
than one element is ambiguous."
|
||||
|
||||
The trick is that the priority rules change if `a` and `b` are related
|
||||
by inheritance. Specifically, `b.__eq__` gets priority if `b` is a
|
||||
subclass of `a`. So by inheriting from `np.ndarray`, we can guarantee that
|
||||
`ApproxNumpy.__eq__` gets called no matter which side of the `==` operator
|
||||
it appears on.
|
||||
"""
|
||||
|
||||
subclass = None
|
||||
|
||||
@classmethod
|
||||
def inherit_ndarray(cls):
|
||||
import numpy as np
|
||||
assert not isinstance(cls, np.ndarray)
|
||||
|
||||
if cls.subclass is None:
|
||||
cls.subclass = type('ApproxNumpy', (cls, np.ndarray), {})
|
||||
|
||||
return cls.subclass
|
||||
|
||||
def __new__(cls, expected, rel=None, abs=None, nan_ok=False):
|
||||
"""
|
||||
Numpy uses __new__ (rather than __init__) to initialize objects.
|
||||
|
||||
The `expected` argument must be a numpy array. This should be
|
||||
ensured by the approx() delegator function.
|
||||
"""
|
||||
obj = super(ApproxNumpyBase, cls).__new__(cls, ())
|
||||
obj.__init__(expected, rel, abs, nan_ok)
|
||||
return obj
|
||||
|
||||
def __repr__(self):
|
||||
# It might be nice to rewrite this function to account for the
|
||||
# shape of the array...
|
||||
return "approx({0!r})".format(list(
|
||||
self._approx_scalar(x) for x in self.expected))
|
||||
|
||||
def __eq__(self, actual):
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
actual = np.asarray(actual)
|
||||
except:
|
||||
raise ValueError("cannot cast '{0}' to numpy.ndarray".format(actual))
|
||||
|
||||
if actual.shape != self.expected.shape:
|
||||
return False
|
||||
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
import numpy as np
|
||||
|
||||
# We can be sure that `actual` is a numpy array, because it's
|
||||
# casted in `__eq__` before being passed to `ApproxBase.__eq__`,
|
||||
# which is the only method that calls this one.
|
||||
for i in np.ndindex(self.expected.shape):
|
||||
yield actual[i], self.expected[i]
|
||||
|
||||
|
||||
class ApproxMapping(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for mappings where the values are numbers
|
||||
(the keys can be anything).
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return "approx({0!r})".format(dict(
|
||||
(k, self._approx_scalar(v))
|
||||
for k,v in self.expected.items()))
|
||||
|
||||
def __eq__(self, actual):
|
||||
if set(actual.keys()) != set(self.expected.keys()):
|
||||
return False
|
||||
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
for k in self.expected.keys():
|
||||
yield actual[k], self.expected[k]
|
||||
|
||||
|
||||
class ApproxSequence(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for sequences of numbers.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
seq_type = type(self.expected)
|
||||
if seq_type not in (tuple, list, set):
|
||||
seq_type = list
|
||||
return "approx({0!r})".format(seq_type(
|
||||
self._approx_scalar(x) for x in self.expected))
|
||||
|
||||
def __eq__(self, actual):
|
||||
if len(actual) != len(self.expected):
|
||||
return False
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
return izip(actual, self.expected)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return a string communicating both the expected value and the tolerance
|
||||
for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode
|
||||
plus/minus symbol if this is python3 (it's too hard to get right for
|
||||
python2).
|
||||
"""
|
||||
if isinstance(self.expected, complex):
|
||||
return str(self.expected)
|
||||
|
||||
# Infinities aren't compared using tolerances, so don't show a
|
||||
# tolerance.
|
||||
if math.isinf(self.expected):
|
||||
return str(self.expected)
|
||||
|
||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||
# raise a ValueError. In this case, display '???'.
|
||||
try:
|
||||
vetted_tolerance = '{:.1e}'.format(self.tolerance)
|
||||
except ValueError:
|
||||
vetted_tolerance = '???'
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
return '{0} +- {1}'.format(self.expected, vetted_tolerance)
|
||||
else:
|
||||
return u'{0} \u00b1 {1}'.format(self.expected, vetted_tolerance)
|
||||
|
||||
def __eq__(self, actual):
|
||||
"""
|
||||
Return true if the given value is equal to the expected value within
|
||||
the pre-specified tolerance.
|
||||
"""
|
||||
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
return True
|
||||
|
||||
# Allow the user to control whether NaNs are considered equal to each
|
||||
# other or not. The abs() calls are for compatibility with complex
|
||||
# numbers.
|
||||
if math.isnan(abs(self.expected)):
|
||||
return self.nan_ok and math.isnan(abs(actual))
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
# will seem approximately equal to everything. The equal-to-itself
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)):
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
return abs(self.expected - actual) <= self.tolerance
|
||||
|
||||
__hash__ = None
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
"""
|
||||
Return the tolerance for the comparison. This could be either an
|
||||
absolute tolerance or a relative tolerance, depending on what the user
|
||||
specified or which would be larger.
|
||||
"""
|
||||
set_default = lambda x, default: x if x is not None else default
|
||||
|
||||
# Figure out what the absolute tolerance should be. ``self.abs`` is
|
||||
# either None or a value specified by the user.
|
||||
absolute_tolerance = set_default(self.abs, 1e-12)
|
||||
|
||||
if absolute_tolerance < 0:
|
||||
raise ValueError("absolute tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(absolute_tolerance):
|
||||
raise ValueError("absolute tolerance can't be NaN.")
|
||||
|
||||
# If the user specified an absolute tolerance but not a relative one,
|
||||
# just return the absolute tolerance.
|
||||
if self.rel is None:
|
||||
if self.abs is not None:
|
||||
return absolute_tolerance
|
||||
|
||||
# Figure out what the relative tolerance should be. ``self.rel`` is
|
||||
# either None or a value specified by the user. This is done after
|
||||
# we've made sure the user didn't ask for an absolute tolerance only,
|
||||
# because we don't want to raise errors about the relative tolerance if
|
||||
# we aren't even going to use it.
|
||||
relative_tolerance = set_default(self.rel, 1e-6) * abs(self.expected)
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError("relative tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(relative_tolerance):
|
||||
raise ValueError("relative tolerance can't be NaN.")
|
||||
|
||||
# Return the larger of the relative and absolute tolerances.
|
||||
return max(relative_tolerance, absolute_tolerance)
|
||||
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
"""
|
||||
Assert that two numbers (or two sets of numbers) are equal to each other
|
||||
within some tolerance.
|
||||
|
@ -45,21 +311,36 @@ class approx(object):
|
|||
>>> 0.1 + 0.2 == approx(0.3)
|
||||
True
|
||||
|
||||
The same syntax also works on sequences of numbers::
|
||||
The same syntax also works for sequences of numbers::
|
||||
|
||||
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
|
||||
True
|
||||
|
||||
Dictionary *values*::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
And ``numpy`` arrays::
|
||||
|
||||
>>> import numpy as np # doctest: +SKIP
|
||||
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
|
||||
True
|
||||
|
||||
By default, ``approx`` considers numbers within a relative tolerance of
|
||||
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
|
||||
This treatment would lead to surprising results if the expected value was
|
||||
``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
|
||||
To handle this case less surprisingly, ``approx`` also considers numbers
|
||||
within an absolute tolerance of ``1e-12`` of its expected value to be
|
||||
equal. Infinite numbers are another special case. They are only
|
||||
considered equal to themselves, regardless of the relative tolerance. Both
|
||||
the relative and absolute tolerances can be changed by passing arguments to
|
||||
the ``approx`` constructor::
|
||||
equal. Infinity and NaN are special cases. Infinity is only considered
|
||||
equal to itself, regardless of the relative tolerance. NaN is not
|
||||
considered equal to anything by default, but you can make it be equal to
|
||||
itself by setting the ``nan_ok`` argument to True. (This is meant to
|
||||
facilitate comparing arrays that use NaN to mean "no data".)
|
||||
|
||||
Both the relative and absolute tolerances can be changed by passing
|
||||
arguments to the ``approx`` constructor::
|
||||
|
||||
>>> 1.0001 == approx(1)
|
||||
False
|
||||
|
@ -123,138 +404,54 @@ class approx(object):
|
|||
relative tolerance, only the absolute tolerance is considered.
|
||||
"""
|
||||
|
||||
def __init__(self, expected, rel=None, abs=None):
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
from collections import Mapping, Sequence
|
||||
from _pytest.compat import STRING_TYPES as String
|
||||
|
||||
def __repr__(self):
|
||||
return ', '.join(repr(x) for x in self.expected)
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
|
||||
#
|
||||
# This architecture is really driven by the need to support numpy arrays.
|
||||
# The only way to override `==` for arrays without requiring that approx be
|
||||
# the left operand is to inherit the approx object from `numpy.ndarray`.
|
||||
# But that can't be a general solution, because it requires (1) numpy to be
|
||||
# installed and (2) the expected value to be a numpy array. So the general
|
||||
# solution is to delegate each type of expected value to a different class.
|
||||
#
|
||||
# This has the advantage that it made it easy to support mapping types
|
||||
# (i.e. dict). The old code accepted mapping types, but would only compare
|
||||
# their keys, which is probably not what most people would expect.
|
||||
|
||||
def __eq__(self, actual):
|
||||
from collections import Iterable
|
||||
if not isinstance(actual, Iterable):
|
||||
actual = [actual]
|
||||
if len(actual) != len(self.expected):
|
||||
return False
|
||||
return all(a == x for a, x in zip(actual, self.expected))
|
||||
if _is_numpy_array(expected):
|
||||
# Create the delegate class on the fly. This allow us to inherit from
|
||||
# ``np.ndarray`` while still not importing numpy unless we need to.
|
||||
cls = ApproxNumpyBase.inherit_ndarray()
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif isinstance(expected, Sequence) and not isinstance(expected, String):
|
||||
cls = ApproxSequence
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, actual):
|
||||
return not (actual == self)
|
||||
|
||||
@property
|
||||
def expected(self):
|
||||
# Regardless of whether the user-specified expected value is a number
|
||||
# or a sequence of numbers, return a list of ApproxNotIterable objects
|
||||
# that can be compared against.
|
||||
from collections import Iterable
|
||||
approx_non_iter = lambda x: ApproxNonIterable(x, self.rel, self.abs)
|
||||
if isinstance(self._expected, Iterable):
|
||||
return [approx_non_iter(x) for x in self._expected]
|
||||
else:
|
||||
return [approx_non_iter(self._expected)]
|
||||
|
||||
@expected.setter
|
||||
def expected(self, expected):
|
||||
self._expected = expected
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
|
||||
class ApproxNonIterable(object):
|
||||
def _is_numpy_array(obj):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
|
||||
In other words, the ``expected`` attribute for objects of this class must
|
||||
be some sort of number. This is in contrast to the ``approx`` class, where
|
||||
the ``expected`` attribute can either be a number of a sequence of numbers.
|
||||
This class is responsible for making comparisons, while ``approx`` is
|
||||
responsible for abstracting the difference between numbers and sequences of
|
||||
numbers. Although this class can stand on its own, it's only meant to be
|
||||
used within ``approx``.
|
||||
Return true if the given object is a numpy array. Make a special effort to
|
||||
avoid importing numpy unless it's really necessary.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
def __init__(self, expected, rel=None, abs=None):
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
for cls in inspect.getmro(type(obj)):
|
||||
if cls.__module__ == 'numpy':
|
||||
try:
|
||||
import numpy as np
|
||||
return isinstance(obj, np.ndarray)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
if isinstance(self.expected, complex):
|
||||
return str(self.expected)
|
||||
return False
|
||||
|
||||
# Infinities aren't compared using tolerances, so don't show a
|
||||
# tolerance.
|
||||
if math.isinf(self.expected):
|
||||
return str(self.expected)
|
||||
|
||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||
# raise a ValueError. In this case, display '???'.
|
||||
try:
|
||||
vetted_tolerance = '{:.1e}'.format(self.tolerance)
|
||||
except ValueError:
|
||||
vetted_tolerance = '???'
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
return '{0} +- {1}'.format(self.expected, vetted_tolerance)
|
||||
else:
|
||||
return u'{0} \u00b1 {1}'.format(self.expected, vetted_tolerance)
|
||||
|
||||
def __eq__(self, actual):
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
return True
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
# will seem approximately equal to everything. The equal-to-itself
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)):
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
return abs(self.expected - actual) <= self.tolerance
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, actual):
|
||||
return not (actual == self)
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
set_default = lambda x, default: x if x is not None else default
|
||||
|
||||
# Figure out what the absolute tolerance should be. ``self.abs`` is
|
||||
# either None or a value specified by the user.
|
||||
absolute_tolerance = set_default(self.abs, 1e-12)
|
||||
|
||||
if absolute_tolerance < 0:
|
||||
raise ValueError("absolute tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(absolute_tolerance):
|
||||
raise ValueError("absolute tolerance can't be NaN.")
|
||||
|
||||
# If the user specified an absolute tolerance but not a relative one,
|
||||
# just return the absolute tolerance.
|
||||
if self.rel is None:
|
||||
if self.abs is not None:
|
||||
return absolute_tolerance
|
||||
|
||||
# Figure out what the relative tolerance should be. ``self.rel`` is
|
||||
# either None or a value specified by the user. This is done after
|
||||
# we've made sure the user didn't ask for an absolute tolerance only,
|
||||
# because we don't want to raise errors about the relative tolerance if
|
||||
# we aren't even going to use it.
|
||||
relative_tolerance = set_default(self.rel, 1e-6) * abs(self.expected)
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError("relative tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(relative_tolerance):
|
||||
raise ValueError("relative tolerance can't be NaN.")
|
||||
|
||||
# Return the larger of the relative and absolute tolerances.
|
||||
return max(relative_tolerance, absolute_tolerance)
|
||||
|
||||
# builtin pytest.raises helper
|
||||
|
||||
|
@ -282,7 +479,6 @@ def raises(expected_exception, *args, **kwargs):
|
|||
...
|
||||
Failed: Expecting ZeroDivisionError
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
When using ``pytest.raises`` as a context manager, it's worthwhile to
|
||||
|
@ -315,7 +511,6 @@ def raises(expected_exception, *args, **kwargs):
|
|||
>>> with raises(ValueError, match=r'must be \d+$'):
|
||||
... raise ValueError("value must be 42")
|
||||
|
||||
|
||||
Or you can specify a callable by passing a to-be-called lambda::
|
||||
|
||||
>>> raises(ZeroDivisionError, lambda: 1/0)
|
||||
|
@ -398,7 +593,6 @@ def raises(expected_exception, *args, **kwargs):
|
|||
|
||||
raises.Exception = fail.Exception
|
||||
|
||||
|
||||
class RaisesContext(object):
|
||||
def __init__(self, expected_exception, message, match_expr):
|
||||
self.expected_exception = expected_exception
|
||||
|
|
|
@ -20,9 +20,11 @@ environment:
|
|||
- TOXENV: "py27-pexpect"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py27-numpy"
|
||||
- TOXENV: "py35-pexpect"
|
||||
- TOXENV: "py35-xdist"
|
||||
- TOXENV: "py35-trial"
|
||||
- TOXENV: "py35-numpy"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "freeze"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Add support for numpy arrays (and dicts) to approx.
|
|
@ -38,7 +38,7 @@ Examples at :ref:`assertraises`.
|
|||
Comparing floating point numbers
|
||||
--------------------------------
|
||||
|
||||
.. autoclass:: approx
|
||||
.. autofunction:: approx
|
||||
|
||||
Raising a specific test outcome
|
||||
--------------------------------------
|
||||
|
|
|
@ -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])) == 'approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])'.format(pm=plus_minus, tol1=tol1, tol2=tol2)
|
||||
assert repr(approx((1.0, 2.0))) == 'approx((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 (
|
||||
"approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format(pm=plus_minus, tol1=tol1, tol2=tol2),
|
||||
"approx({{'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))
|
||||
|
@ -212,34 +218,51 @@ class TestApprox(object):
|
|||
|
||||
def test_expecting_nan(self):
|
||||
examples = [
|
||||
(nan, nan),
|
||||
(-nan, -nan),
|
||||
(nan, -nan),
|
||||
(0.0, nan),
|
||||
(inf, nan),
|
||||
(eq, nan, nan),
|
||||
(eq, -nan, -nan),
|
||||
(eq, nan, -nan),
|
||||
(ne, 0.0, nan),
|
||||
(ne, inf, nan),
|
||||
]
|
||||
for a, x in examples:
|
||||
# If there is a relative tolerance and the expected value is NaN,
|
||||
# the actual tolerance is a NaN, which should be an error.
|
||||
with pytest.raises(ValueError):
|
||||
a != approx(x, rel=inf)
|
||||
for op, a, x in examples:
|
||||
# Nothing is equal to NaN by default.
|
||||
assert a != approx(x)
|
||||
|
||||
# You can make comparisons against NaN by not specifying a relative
|
||||
# tolerance, so only an absolute tolerance is calculated.
|
||||
assert a != approx(x, abs=inf)
|
||||
# If ``nan_ok=True``, then NaN is equal to NaN.
|
||||
assert op(a, approx(x, nan_ok=True))
|
||||
|
||||
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 +274,80 @@ 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}
|
||||
# Dictionaries became ordered in python3.6, so switch up the order here
|
||||
# to make sure it doesn't matter.
|
||||
expected = {'b': 2, 'a': 1}
|
||||
|
||||
# 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):
|
||||
np = pytest.importorskip('numpy')
|
||||
|
||||
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
|
||||
|
||||
# Should be able to compare lists with numpy arrays.
|
||||
assert list(actual) == approx(expected, rel=5e-7, abs=0)
|
||||
assert list(actual) != approx(expected, rel=5e-8, abs=0)
|
||||
assert actual == approx(list(expected), rel=5e-7, abs=0)
|
||||
assert actual != approx(list(expected), rel=5e-8, abs=0)
|
||||
|
||||
def test_numpy_array_wrong_shape(self):
|
||||
np = pytest.importorskip('numpy')
|
||||
|
||||
a12 = np.array([[1, 2]])
|
||||
a21 = np.array([[1],[2]])
|
||||
|
||||
assert a12 != approx(a21)
|
||||
assert a21 != approx(a12)
|
||||
|
||||
def test_doctests(self):
|
||||
parser = doctest.DocTestParser()
|
||||
|
|
12
tox.ini
12
tox.ini
|
@ -12,7 +12,7 @@ envlist=
|
|||
py36
|
||||
py37
|
||||
pypy
|
||||
{py27,py35}-{pexpect,xdist,trial}
|
||||
{py27,py35}-{pexpect,xdist,trial,numpy}
|
||||
py27-nobyte
|
||||
doctesting
|
||||
freeze
|
||||
|
@ -108,6 +108,16 @@ deps={[testenv:py27-trial]deps}
|
|||
commands=
|
||||
pytest -ra {posargs:testing/test_unittest.py}
|
||||
|
||||
[testenv:py27-numpy]
|
||||
deps=numpy
|
||||
commands=
|
||||
pytest -rfsxX {posargs:testing/python/approx.py}
|
||||
|
||||
[testenv:py35-numpy]
|
||||
deps=numpy
|
||||
commands=
|
||||
pytest -rfsxX {posargs:testing/python/approx.py}
|
||||
|
||||
[testenv:docs]
|
||||
skipsdist=True
|
||||
usedevelop=True
|
||||
|
|
Loading…
Reference in New Issue