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 math
|
||||||
import pprint
|
import pprint
|
||||||
|
from collections.abc import Collection
|
||||||
from collections.abc import Sized
|
from collections.abc import Sized
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from numbers import Complex
|
from numbers import Complex
|
||||||
|
@ -8,7 +9,6 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -306,12 +306,12 @@ class ApproxMapping(ApproxBase):
|
||||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
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."""
|
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
seq_type = type(self.expected)
|
seq_type = type(self.expected)
|
||||||
if seq_type not in (tuple, list, set):
|
if seq_type not in (tuple, list):
|
||||||
seq_type = list
|
seq_type = list
|
||||||
return "approx({!r})".format(
|
return "approx({!r})".format(
|
||||||
seq_type(self._approx_scalar(x) for x in self.expected)
|
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:
|
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.
|
within some tolerance.
|
||||||
|
|
||||||
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
|
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)
|
>>> 0.1 + 0.2 == approx(0.3)
|
||||||
True
|
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))
|
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
|
||||||
True
|
True
|
||||||
|
|
||||||
Dictionary *values*::
|
|
||||||
|
|
||||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
|
||||||
True
|
|
||||||
|
|
||||||
``numpy`` arrays::
|
``numpy`` arrays::
|
||||||
|
|
||||||
>>> import numpy as np # doctest: +SKIP
|
>>> 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
|
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
|
||||||
True
|
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
|
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.
|
``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
|
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)
|
expected = _as_numpy_array(expected)
|
||||||
cls = ApproxNumpy
|
cls = ApproxNumpy
|
||||||
elif (
|
elif (
|
||||||
isinstance(expected, Iterable)
|
hasattr(expected, "__getitem__")
|
||||||
and isinstance(expected, Sized)
|
and isinstance(expected, Sized)
|
||||||
# Type ignored because the error is wrong -- not unreachable.
|
# Type ignored because the error is wrong -- not unreachable.
|
||||||
and not isinstance(expected, STRING_TYPES) # type: ignore[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:
|
else:
|
||||||
cls = ApproxScalar
|
cls = ApproxScalar
|
||||||
|
|
||||||
|
|
|
@ -858,13 +858,21 @@ class TestApprox:
|
||||||
assert approx(expected, rel=5e-7, abs=0) == actual
|
assert approx(expected, rel=5e-7, abs=0) == actual
|
||||||
assert approx(expected, rel=5e-8, abs=0) != actual
|
assert approx(expected, rel=5e-8, abs=0) != actual
|
||||||
|
|
||||||
def test_generic_sized_iterable_object(self):
|
def test_generic_ordered_sequence(self):
|
||||||
class MySizedIterable:
|
class MySequence:
|
||||||
def __iter__(self):
|
def __getitem__(self, i):
|
||||||
return iter([1, 2, 3, 4])
|
return [1, 2, 3, 4][i]
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
expected = MySizedIterable()
|
expected = MySequence()
|
||||||
assert [1, 2, 3, 4] == approx(expected)
|
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