Disallow unordered sequences in pytest.approx

Fix #9692
This commit is contained in:
Bruno Oliveira 2022-02-23 08:25:00 -03:00
parent 9af3e23695
commit 5805d58adf
3 changed files with 41 additions and 17 deletions

View File

@ -0,0 +1,3 @@
:func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`).
Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order.

View File

@ -1,5 +1,6 @@
import math
import pprint
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
from numbers import Complex
@ -8,7 +9,6 @@ from typing import Any
from typing import Callable
from typing import cast
from typing import Generic
from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional
@ -306,12 +306,12 @@ class ApproxMapping(ApproxBase):
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
class ApproxSequencelike(ApproxBase):
class ApproxSequenceLike(ApproxBase):
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
def __repr__(self) -> str:
seq_type = type(self.expected)
if seq_type not in (tuple, list, set):
if seq_type not in (tuple, list):
seq_type = list
return "approx({!r})".format(
seq_type(self._approx_scalar(x) for x in self.expected)
@ -515,7 +515,7 @@ class ApproxDecimal(ApproxScalar):
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
"""Assert that two numbers (or two sets of numbers) are equal to each other
"""Assert that two numbers (or two ordered sequences of numbers) are equal to each other
within some tolerance.
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
@ -547,16 +547,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> 0.1 + 0.2 == approx(0.3)
True
The same syntax also works for sequences of numbers::
The same syntax also works for ordered 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
``numpy`` arrays::
>>> import numpy as np # doctest: +SKIP
@ -569,6 +564,20 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
True
Only ordered sequences are supported, because ``approx`` needs
to infer the relative position of the sequences without ambiguity. This means
``sets`` and other unordered sequences are not supported.
Finally, dictionary *values* can also be compared::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
The comparision will be true if both mappings have the same keys and their
respective values match the expected tolerances.
**Tolerances**
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
@ -708,12 +717,19 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
expected = _as_numpy_array(expected)
cls = ApproxNumpy
elif (
isinstance(expected, Iterable)
hasattr(expected, "__getitem__")
and isinstance(expected, Sized)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
cls = ApproxSequencelike
cls = ApproxSequenceLike
elif (
isinstance(expected, Collection)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
raise TypeError(msg)
else:
cls = ApproxScalar

View File

@ -858,13 +858,18 @@ class TestApprox:
assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual
def test_generic_sized_iterable_object(self):
class MySizedIterable:
def __iter__(self):
return iter([1, 2, 3, 4])
def test_generic_ordered_sequence(self):
class MySequence:
def __getitem__(self, i):
return [1, 2, 3, 4][i]
def __len__(self):
return 4
expected = MySizedIterable()
expected = MySequence()
assert [1, 2, 3, 4] == approx(expected)
def test_allow_ordered_sequences_only(self) -> None:
"""pytest.approx() should raise an error on unordered sequences (#9692)."""
with pytest.raises(TypeError, match="only supports ordered sequences"):
assert {1, 2, 3} == approx({1, 2, 3})