diff --git a/.travis.yml b/.travis.yml index 0a71e7dc1..d3dce9141 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/_pytest/compat.py b/_pytest/compat.py index 8c200af5f..0554efeb7 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -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 diff --git a/_pytest/python_api.py b/_pytest/python_api.py index 1b27ba327..cb7d5e459 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -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 diff --git a/appveyor.yml b/appveyor.yml index cc72b4b70..abf033b4c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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" diff --git a/changelog/1994.feature b/changelog/1994.feature new file mode 100644 index 000000000..f3c596e63 --- /dev/null +++ b/changelog/1994.feature @@ -0,0 +1 @@ +Add support for numpy arrays (and dicts) to approx. diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 26dbd44cb..af0dd9a74 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -38,7 +38,7 @@ Examples at :ref:`assertraises`. Comparing floating point numbers -------------------------------- -.. autoclass:: approx +.. autofunction:: approx Raising a specific test outcome -------------------------------------- diff --git a/testing/python/approx.py b/testing/python/approx.py index d7063e215..a21f644f5 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -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() diff --git a/tox.ini b/tox.ini index d8b27300f..cc9e64b1c 100644 --- a/tox.ini +++ b/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