From 7d8688d54b48daba9ec0bfdbaec8c054060c90dd Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 30 Jul 2018 23:22:06 -0700 Subject: [PATCH 01/16] Reflect dimension in approx repr for numpy arrays. --- src/_pytest/python_api.py | 14 +++++++++----- testing/python/approx.py | 22 ++++++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 5331d8a84a3..092008d642a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -82,14 +82,18 @@ class ApproxNumpy(ApproxBase): """ def __repr__(self): - # It might be nice to rewrite this function to account for the - # shape of the array... import numpy as np - list_scalars = [] - for x in np.ndindex(self.expected.shape): - list_scalars.append(self._approx_scalar(np.asscalar(self.expected[x]))) + def recursive_map(f, x): + if isinstance(x, list): + return list(recursive_map(f, xi) for xi in x) + else: + return f(x) + list_scalars = recursive_map( + self._approx_scalar, + self.expected.tolist()) + return "approx({!r})".format(list_scalars) if sys.version_info[0] == 2: diff --git a/testing/python/approx.py b/testing/python/approx.py index 0509fa67238..427d6bb4608 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -59,17 +59,19 @@ def test_repr_string(self, plus_minus): ), ) - def test_repr_0d_array(self, plus_minus): + def test_repr_nd_array(self, plus_minus): + # Make sure that arrays of all different dimensions are repr'd + # correctly. np = pytest.importorskip("numpy") - np_array = np.array(5.) - assert approx(np_array) == 5.0 - string_expected = "approx([5.0 {} 5.0e-06])".format(plus_minus) - - assert repr(approx(np_array)) == string_expected - - np_array = np.array([5.]) - assert approx(np_array) == 5.0 - assert repr(approx(np_array)) == string_expected + examples = [ + (np.array(5.), 'approx(5.0 {pm} 5.0e-06)'), + (np.array([5.]), 'approx([5.0 {pm} 5.0e-06])'), + (np.array([[5.]]), 'approx([[5.0 {pm} 5.0e-06]])'), + (np.array([[5., 6.]]), 'approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])'), + (np.array([[5.], [6.]]), 'approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])'), + ] + for np_array, repr_string in examples: + assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) From ad305e71d7df867c49d074c8fab00fa9f51024a1 Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 30 Jul 2018 23:26:57 -0700 Subject: [PATCH 02/16] Improve docstrings for Approx classes. --- src/_pytest/python_api.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 092008d642a..1767cee2a16 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -78,7 +78,7 @@ def _yield_comparisons(self, actual): class ApproxNumpy(ApproxBase): """ - Perform approximate comparisons for numpy arrays. + Perform approximate comparisons where the expected value is numpy array. """ def __repr__(self): @@ -132,8 +132,8 @@ def _yield_comparisons(self, actual): class ApproxMapping(ApproxBase): """ - Perform approximate comparisons for mappings where the values are numbers - (the keys can be anything). + Perform approximate comparisons where the expected value is a mapping with + numeric values (the keys can be anything). """ def __repr__(self): @@ -154,7 +154,8 @@ def _yield_comparisons(self, actual): class ApproxSequence(ApproxBase): """ - Perform approximate comparisons for sequences of numbers. + Perform approximate comparisons where the expected value is a sequence of + numbers. """ def __repr__(self): @@ -176,7 +177,7 @@ def _yield_comparisons(self, actual): class ApproxScalar(ApproxBase): """ - Perform approximate comparisons for single numbers only. + Perform approximate comparisons where the expected value is a single number. """ DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 @@ -290,6 +291,9 @@ def set_default(x, default): class ApproxDecimal(ApproxScalar): + """ + Perform approximate comparisons where the expected value is a decimal. + """ from decimal import Decimal DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") From cd2085ee718fb297537638bb33a70fc781ea450f Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Tue, 31 Jul 2018 00:26:35 -0700 Subject: [PATCH 03/16] approx(): Detect type errors earlier. --- src/_pytest/python_api.py | 68 ++++++++++++++++++++++++++++----------- testing/python/approx.py | 7 ++++ 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1767cee2a16..eeaac09e3b5 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,5 +1,7 @@ import math import sys +from numbers import Number +from decimal import Decimal import py from six.moves import zip, filterfalse @@ -29,6 +31,9 @@ def _cmp_raises_type_error(self, other): "Comparison operators other than == and != not supported by approx objects" ) +def _non_numeric_type_error(value): + return TypeError("cannot make approximate comparisons to non-numeric values, e.g. {}".format(value)) + # builtin pytest.approx helper @@ -39,7 +44,7 @@ class ApproxBase(object): or sequences of numbers. """ - # Tell numpy to use our `__eq__` operator instead of its + # Tell numpy to use our `__eq__` operator instead of its. __array_ufunc__ = None __array_priority__ = 100 @@ -48,6 +53,7 @@ def __init__(self, expected, rel=None, abs=None, nan_ok=False): self.abs = abs self.rel = rel self.nan_ok = nan_ok + self._check_type() def __repr__(self): raise NotImplementedError @@ -75,6 +81,17 @@ def _yield_comparisons(self, actual): """ raise NotImplementedError + def _check_type(self): + """ + Raise a TypeError if the expected value is not a valid type. + """ + # This is only a concern if the expected value is a sequence. In every + # other case, the approx() function ensures that the expected value has + # a numeric type. For this reason, the default is to do nothing. The + # classes that deal with sequences should reimplement this method to + # raise if there are any non-numeric elements in the sequence. + pass + class ApproxNumpy(ApproxBase): """ @@ -151,6 +168,13 @@ def _yield_comparisons(self, actual): for k in self.expected.keys(): yield actual[k], self.expected[k] + def _check_type(self): + for x in self.expected.values(): + if isinstance(x, type(self.expected)): + raise TypeError("pytest.approx() does not support nested dictionaries, e.g. {}".format(self.expected)) + elif not isinstance(x, Number): + raise _non_numeric_type_error(self.expected) + class ApproxSequence(ApproxBase): """ @@ -174,6 +198,13 @@ def __eq__(self, actual): def _yield_comparisons(self, actual): return zip(actual, self.expected) + def _check_type(self): + for x in self.expected: + if isinstance(x, type(self.expected)): + raise TypeError("pytest.approx() does not support nested data structures, e.g. {}".format(self.expected)) + elif not isinstance(x, Number): + raise _non_numeric_type_error(self.expected) + class ApproxScalar(ApproxBase): """ @@ -294,8 +325,6 @@ class ApproxDecimal(ApproxScalar): """ Perform approximate comparisons where the expected value is a decimal. """ - from decimal import Decimal - DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") @@ -453,32 +482,33 @@ def approx(expected, rel=None, abs=None, nan_ok=False): __ https://docs.python.org/3/reference/datamodel.html#object.__ge__ """ - from decimal import Decimal - # 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. + # The primary responsibility of these classes is to implement ``__eq__()`` + # and ``__repr__()``. The former is used to actually check if some + # "actual" value is equivalent to the given expected value within the + # allowed tolerance. The latter is used to show the user the expected + # value and tolerance, in the case that a test failed. # - # 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. + # The actual logic for making approximate comparisons can be found in + # ApproxScalar, which is used to compare individual numbers. All of the + # other Approx classes eventually delegate to this class. The ApproxBase + # class provides some convenient methods and overloads, but isn't really + # essential. - if _is_numpy_array(expected): - cls = ApproxNumpy + if isinstance(expected, Decimal): + cls = ApproxDecimal + elif isinstance(expected, Number): + cls = ApproxScalar elif isinstance(expected, Mapping): cls = ApproxMapping elif isinstance(expected, Sequence) and not isinstance(expected, STRING_TYPES): cls = ApproxSequence - elif isinstance(expected, Decimal): - cls = ApproxDecimal + elif _is_numpy_array(expected): + cls = ApproxNumpy else: - cls = ApproxScalar + raise _non_numeric_type_error(expected) return cls(expected, rel, abs, nan_ok) diff --git a/testing/python/approx.py b/testing/python/approx.py index 427d6bb4608..4ae4b149a28 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -441,6 +441,13 @@ def test_foo(): ["*At index 0 diff: 3 != 4 * {}".format(expected), "=* 1 failed in *="] ) + @pytest.mark.parametrize( + 'x', [None, 'string', ['string'], [[1]], {'key': 'string'}, {'key': {'key': 1}}] + ) + def test_expected_value_type_error(self, x): + with pytest.raises(TypeError): + approx(x) + @pytest.mark.parametrize( "op", [ From 032db159c997e10cd8750fa817944d42fb108323 Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Tue, 31 Jul 2018 11:23:23 -0700 Subject: [PATCH 04/16] Let black reformat the code... --- src/_pytest/python_api.py | 56 ++++++++++++++++++++++++--------------- testing/python/approx.py | 17 +++++++----- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index eeaac09e3b5..26e5aa51218 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -31,8 +31,13 @@ def _cmp_raises_type_error(self, other): "Comparison operators other than == and != not supported by approx objects" ) + def _non_numeric_type_error(value): - return TypeError("cannot make approximate comparisons to non-numeric values, e.g. {}".format(value)) + return TypeError( + "cannot make approximate comparisons to non-numeric values, e.g. {}".format( + value + ) + ) # builtin pytest.approx helper @@ -85,10 +90,10 @@ def _check_type(self): """ Raise a TypeError if the expected value is not a valid type. """ - # This is only a concern if the expected value is a sequence. In every - # other case, the approx() function ensures that the expected value has - # a numeric type. For this reason, the default is to do nothing. The - # classes that deal with sequences should reimplement this method to + # This is only a concern if the expected value is a sequence. In every + # other case, the approx() function ensures that the expected value has + # a numeric type. For this reason, the default is to do nothing. The + # classes that deal with sequences should reimplement this method to # raise if there are any non-numeric elements in the sequence. pass @@ -107,10 +112,8 @@ def recursive_map(f, x): else: return f(x) - list_scalars = recursive_map( - self._approx_scalar, - self.expected.tolist()) - + list_scalars = recursive_map(self._approx_scalar, self.expected.tolist()) + return "approx({!r})".format(list_scalars) if sys.version_info[0] == 2: @@ -149,7 +152,7 @@ def _yield_comparisons(self, actual): class ApproxMapping(ApproxBase): """ - Perform approximate comparisons where the expected value is a mapping with + Perform approximate comparisons where the expected value is a mapping with numeric values (the keys can be anything). """ @@ -171,14 +174,18 @@ def _yield_comparisons(self, actual): def _check_type(self): for x in self.expected.values(): if isinstance(x, type(self.expected)): - raise TypeError("pytest.approx() does not support nested dictionaries, e.g. {}".format(self.expected)) + raise TypeError( + "pytest.approx() does not support nested dictionaries, e.g. {}".format( + self.expected + ) + ) elif not isinstance(x, Number): raise _non_numeric_type_error(self.expected) class ApproxSequence(ApproxBase): """ - Perform approximate comparisons where the expected value is a sequence of + Perform approximate comparisons where the expected value is a sequence of numbers. """ @@ -201,7 +208,11 @@ def _yield_comparisons(self, actual): def _check_type(self): for x in self.expected: if isinstance(x, type(self.expected)): - raise TypeError("pytest.approx() does not support nested data structures, e.g. {}".format(self.expected)) + raise TypeError( + "pytest.approx() does not support nested data structures, e.g. {}".format( + self.expected + ) + ) elif not isinstance(x, Number): raise _non_numeric_type_error(self.expected) @@ -325,6 +336,7 @@ class ApproxDecimal(ApproxScalar): """ Perform approximate comparisons where the expected value is a decimal. """ + DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") @@ -485,17 +497,17 @@ def approx(expected, rel=None, abs=None, nan_ok=False): # 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). # - # The primary responsibility of these classes is to implement ``__eq__()`` - # and ``__repr__()``. The former is used to actually check if some - # "actual" value is equivalent to the given expected value within the - # allowed tolerance. The latter is used to show the user the expected + # The primary responsibility of these classes is to implement ``__eq__()`` + # and ``__repr__()``. The former is used to actually check if some + # "actual" value is equivalent to the given expected value within the + # allowed tolerance. The latter is used to show the user the expected # value and tolerance, in the case that a test failed. # - # The actual logic for making approximate comparisons can be found in - # ApproxScalar, which is used to compare individual numbers. All of the - # other Approx classes eventually delegate to this class. The ApproxBase - # class provides some convenient methods and overloads, but isn't really - # essential. + # The actual logic for making approximate comparisons can be found in + # ApproxScalar, which is used to compare individual numbers. All of the + # other Approx classes eventually delegate to this class. The ApproxBase + # class provides some convenient methods and overloads, but isn't really + # essential. if isinstance(expected, Decimal): cls = ApproxDecimal diff --git a/testing/python/approx.py b/testing/python/approx.py index 4ae4b149a28..8c77b4945f5 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -60,15 +60,18 @@ def test_repr_string(self, plus_minus): ) def test_repr_nd_array(self, plus_minus): - # Make sure that arrays of all different dimensions are repr'd + # Make sure that arrays of all different dimensions are repr'd # correctly. np = pytest.importorskip("numpy") examples = [ - (np.array(5.), 'approx(5.0 {pm} 5.0e-06)'), - (np.array([5.]), 'approx([5.0 {pm} 5.0e-06])'), - (np.array([[5.]]), 'approx([[5.0 {pm} 5.0e-06]])'), - (np.array([[5., 6.]]), 'approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])'), - (np.array([[5.], [6.]]), 'approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])'), + (np.array(5.), "approx(5.0 {pm} 5.0e-06)"), + (np.array([5.]), "approx([5.0 {pm} 5.0e-06])"), + (np.array([[5.]]), "approx([[5.0 {pm} 5.0e-06]])"), + (np.array([[5., 6.]]), "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"), + ( + np.array([[5.], [6.]]), + "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])", + ), ] for np_array, repr_string in examples: assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) @@ -442,7 +445,7 @@ def test_foo(): ) @pytest.mark.parametrize( - 'x', [None, 'string', ['string'], [[1]], {'key': 'string'}, {'key': {'key': 1}}] + "x", [None, "string", ["string"], [[1]], {"key": "string"}, {"key": {"key": 1}}] ) def test_expected_value_type_error(self, x): with pytest.raises(TypeError): From d02491931ac4eee6d93d8853881c321619c460c3 Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Tue, 31 Jul 2018 11:33:46 -0700 Subject: [PATCH 05/16] Fix the unused import. --- src/_pytest/python_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 26e5aa51218..84cac38620e 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -104,8 +104,6 @@ class ApproxNumpy(ApproxBase): """ def __repr__(self): - import numpy as np - def recursive_map(f, x): if isinstance(x, list): return list(recursive_map(f, xi) for xi in x) From 327fe4cfcc366f7b2e7fb8f2f90749ecc6edd785 Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Tue, 31 Jul 2018 11:40:02 -0700 Subject: [PATCH 06/16] Update the changelog. --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b384062010..3cd0c43b17f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -54,6 +54,9 @@ Bug Fixes - `#3695 `_: Fix ``ApproxNumpy`` initialisation argument mixup, ``abs`` and ``rel`` tolerances were flipped causing strange comparsion results. Add tests to check ``abs`` and ``rel`` tolerances for ``np.array`` and test for expecting ``nan`` with ``np.array()`` +- `#3712 `_: Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``. + +- `#3473 `_: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.) - `#980 `_: Fix truncated locals output in verbose mode. From bf7c188cc0eeb14963d2006f705cdede126400cb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 31 Jul 2018 21:08:24 -0300 Subject: [PATCH 07/16] Improve error message for invalid types passed to pytest.approx() * Hide the internal traceback * Use !r representation instead of !s (the default for {} formatting) --- src/_pytest/python_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 84cac38620e..88e41bab50f 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -34,7 +34,7 @@ def _cmp_raises_type_error(self, other): def _non_numeric_type_error(value): return TypeError( - "cannot make approximate comparisons to non-numeric values, e.g. {}".format( + "cannot make approximate comparisons to non-numeric values, e.g. {!r}".format( value ) ) @@ -507,6 +507,8 @@ def approx(expected, rel=None, abs=None, nan_ok=False): # class provides some convenient methods and overloads, but isn't really # essential. + __tracebackhide__ = True + if isinstance(expected, Decimal): cls = ApproxDecimal elif isinstance(expected, Number): From 8e2ed7622742fcf5ab1857284ec823cbf8a88b22 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 31 Jul 2018 21:11:26 -0300 Subject: [PATCH 08/16] Create appropriate CHANGELOG entries --- CHANGELOG.rst | 3 --- changelog/3473.bugfix.rst | 1 + changelog/3712.bugfix.rst | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 changelog/3473.bugfix.rst create mode 100644 changelog/3712.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3cd0c43b17f..3b384062010 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -54,9 +54,6 @@ Bug Fixes - `#3695 `_: Fix ``ApproxNumpy`` initialisation argument mixup, ``abs`` and ``rel`` tolerances were flipped causing strange comparsion results. Add tests to check ``abs`` and ``rel`` tolerances for ``np.array`` and test for expecting ``nan`` with ``np.array()`` -- `#3712 `_: Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``. - -- `#3473 `_: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.) - `#980 `_: Fix truncated locals output in verbose mode. diff --git a/changelog/3473.bugfix.rst b/changelog/3473.bugfix.rst new file mode 100644 index 00000000000..ef434b29f16 --- /dev/null +++ b/changelog/3473.bugfix.rst @@ -0,0 +1 @@ +Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.). diff --git a/changelog/3712.bugfix.rst b/changelog/3712.bugfix.rst new file mode 100644 index 00000000000..649ef639487 --- /dev/null +++ b/changelog/3712.bugfix.rst @@ -0,0 +1 @@ +Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``. From 098dca3a9fda8dd6ed999e5e38d36877191690b5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 31 Jul 2018 21:14:51 -0300 Subject: [PATCH 09/16] Use {!r} for a few other messages as well --- src/_pytest/python_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 88e41bab50f..f51d44fe16b 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -173,7 +173,7 @@ def _check_type(self): for x in self.expected.values(): if isinstance(x, type(self.expected)): raise TypeError( - "pytest.approx() does not support nested dictionaries, e.g. {}".format( + "pytest.approx() does not support nested dictionaries, e.g. {!r}".format( self.expected ) ) @@ -207,7 +207,7 @@ def _check_type(self): for x in self.expected: if isinstance(x, type(self.expected)): raise TypeError( - "pytest.approx() does not support nested data structures, e.g. {}".format( + "pytest.approx() does not support nested data structures, e.g. {!r}".format( self.expected ) ) From 611d254ed5725a098b308e7c18b58e7e670a3529 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:01:00 -0300 Subject: [PATCH 10/16] Improve error checking messages: add position and use pprint --- src/_pytest/python_api.py | 41 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f51d44fe16b..529c64222dc 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,4 +1,5 @@ import math +import pprint import sys from numbers import Number from decimal import Decimal @@ -32,10 +33,11 @@ def _cmp_raises_type_error(self, other): ) -def _non_numeric_type_error(value): +def _non_numeric_type_error(value, at): + at_str = "at {}".format(at) if at else "" return TypeError( - "cannot make approximate comparisons to non-numeric values, e.g. {!r}".format( - value + "cannot make approximate comparisons to non-numeric values: {!r} ".format( + value, at_str ) ) @@ -54,6 +56,7 @@ class ApproxBase(object): __array_priority__ = 100 def __init__(self, expected, rel=None, abs=None, nan_ok=False): + __tracebackhide__ = True self.expected = expected self.abs = abs self.rel = rel @@ -170,15 +173,13 @@ def _yield_comparisons(self, actual): yield actual[k], self.expected[k] def _check_type(self): - for x in self.expected.values(): - if isinstance(x, type(self.expected)): - raise TypeError( - "pytest.approx() does not support nested dictionaries, e.g. {!r}".format( - self.expected - ) - ) - elif not isinstance(x, Number): - raise _non_numeric_type_error(self.expected) + __tracebackhide__ = True + for key, value in self.expected.items(): + if isinstance(value, type(self.expected)): + msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" + raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) + elif not isinstance(value, Number): + raise _non_numeric_type_error(self.expected, at="key={!r}".format(key)) class ApproxSequence(ApproxBase): @@ -204,15 +205,15 @@ def _yield_comparisons(self, actual): return zip(actual, self.expected) def _check_type(self): - for x in self.expected: + __tracebackhide__ = True + for index, x in enumerate(self.expected): if isinstance(x, type(self.expected)): - raise TypeError( - "pytest.approx() does not support nested data structures, e.g. {!r}".format( - self.expected - ) - ) + msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" + raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) elif not isinstance(x, Number): - raise _non_numeric_type_error(self.expected) + raise _non_numeric_type_error( + self.expected, at="index {}".format(index) + ) class ApproxScalar(ApproxBase): @@ -520,7 +521,7 @@ def approx(expected, rel=None, abs=None, nan_ok=False): elif _is_numpy_array(expected): cls = ApproxNumpy else: - raise _non_numeric_type_error(expected) + raise _non_numeric_type_error(expected, at=None) return cls(expected, rel, abs, nan_ok) From 6e32a1f73d91c6040960a6e84841cdcffb0393e5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:04:25 -0300 Subject: [PATCH 11/16] Use parametrize in repr test for nd arrays --- testing/python/approx.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 8c77b4945f5..c31b8bc5a67 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -59,22 +59,21 @@ def test_repr_string(self, plus_minus): ), ) - def test_repr_nd_array(self, plus_minus): - # Make sure that arrays of all different dimensions are repr'd - # correctly. + @pytest.mark.parametrize( + "value, repr_string", + [ + (5., "approx(5.0 {pm} 5.0e-06)"), + ([5.], "approx([5.0 {pm} 5.0e-06])"), + ([[5.]], "approx([[5.0 {pm} 5.0e-06]])"), + ([[5., 6.]], "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"), + ([[5.], [6.]], "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])"), + ], + ) + def test_repr_nd_array(self, plus_minus, value, repr_string): + """Make sure that arrays of all different dimensions are repr'd correctly.""" np = pytest.importorskip("numpy") - examples = [ - (np.array(5.), "approx(5.0 {pm} 5.0e-06)"), - (np.array([5.]), "approx([5.0 {pm} 5.0e-06])"), - (np.array([[5.]]), "approx([[5.0 {pm} 5.0e-06]])"), - (np.array([[5., 6.]]), "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"), - ( - np.array([[5.], [6.]]), - "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])", - ), - ] - for np_array, repr_string in examples: - assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) + np_array = np.array(value) + assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) From 5003bae0de4e1dc3ded16950c24c2aad275c78a4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:07:37 -0300 Subject: [PATCH 12/16] Fix 'at' string for non-numeric messages in approx() --- src/_pytest/python_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 529c64222dc..bbbc144eeed 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -34,9 +34,9 @@ def _cmp_raises_type_error(self, other): def _non_numeric_type_error(value, at): - at_str = "at {}".format(at) if at else "" + at_str = " at {}".format(at) if at else "" return TypeError( - "cannot make approximate comparisons to non-numeric values: {!r} ".format( + "cannot make approximate comparisons to non-numeric values: {!r} {}".format( value, at_str ) ) From ad5ddaf55a71ebf640043d34c79cf3672c86be5c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:28:39 -0300 Subject: [PATCH 13/16] Simplify is_numpy_array as suggested in review --- src/_pytest/python_api.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index bbbc144eeed..66b9473b710 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -531,17 +531,11 @@ def _is_numpy_array(obj): 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 - - for cls in inspect.getmro(type(obj)): - if cls.__module__ == "numpy": - try: - import numpy as np - - return isinstance(obj, np.ndarray) - except ImportError: - pass + import sys + np = sys.modules.get("numpy") + if np is not None: + return isinstance(obj, np.ndarray) return False From 2a2f888909e31f4fc9d26593a4addffd75469dd0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:30:40 -0300 Subject: [PATCH 14/16] Move recursive_map from local to free function --- src/_pytest/python_api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 66b9473b710..25b76742c69 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -101,20 +101,20 @@ def _check_type(self): pass +def recursive_map(f, x): + if isinstance(x, list): + return list(recursive_map(f, xi) for xi in x) + else: + return f(x) + + class ApproxNumpy(ApproxBase): """ Perform approximate comparisons where the expected value is numpy array. """ def __repr__(self): - def recursive_map(f, x): - if isinstance(x, list): - return list(recursive_map(f, xi) for xi in x) - else: - return f(x) - list_scalars = recursive_map(self._approx_scalar, self.expected.tolist()) - return "approx({!r})".format(list_scalars) if sys.version_info[0] == 2: From 43664d784167011fabd5fb13398084504a6c4016 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 07:34:08 -0300 Subject: [PATCH 15/16] Use ids for parametrized values in test_expected_value_type_error --- testing/python/approx.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index c31b8bc5a67..b1440adeece 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -444,7 +444,15 @@ def test_foo(): ) @pytest.mark.parametrize( - "x", [None, "string", ["string"], [[1]], {"key": "string"}, {"key": {"key": 1}}] + "x", + [ + pytest.param(None), + pytest.param("string"), + pytest.param(["string"], id="nested-str"), + pytest.param([[1]], id="nested-list"), + pytest.param({"key": "string"}, id="dict-with-string"), + pytest.param({"key": {"key": 1}}, id="nested-dict"), + ], ) def test_expected_value_type_error(self, x): with pytest.raises(TypeError): From a5c0fb7f6b8d6213111c70d07ace773e6e794614 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Aug 2018 15:17:58 -0300 Subject: [PATCH 16/16] Rename recursive_map -> _recursive_list_map as requested in review --- src/_pytest/python_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 25b76742c69..8f15ea7e799 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -101,9 +101,9 @@ def _check_type(self): pass -def recursive_map(f, x): +def _recursive_list_map(f, x): if isinstance(x, list): - return list(recursive_map(f, xi) for xi in x) + return list(_recursive_list_map(f, xi) for xi in x) else: return f(x) @@ -114,7 +114,7 @@ class ApproxNumpy(ApproxBase): """ def __repr__(self): - list_scalars = recursive_map(self._approx_scalar, self.expected.tolist()) + list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) return "approx({!r})".format(list_scalars) if sys.version_info[0] == 2: