parent
bcc826d0fb
commit
5f3d94c47e
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
|
|
@ -858,13 +858,21 @@ 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()
|
||||
assert [1, 2, 3, 4] == approx(expected)
|
||||
expected = MySequence()
|
||||
assert [1, 2, 3, 4] == approx(expected, abs=1e-4)
|
||||
|
||||
expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])"
|
||||
assert repr(approx(expected)) == expected_repr
|
||||
|
||||
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})
|
||||
|
|
Loading…
Reference in New Issue