Skip to content

Commit

Permalink
Disallow unordered sequences in pytest.approx
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Feb 23, 2022
1 parent 9af3e23 commit 5805d58
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 17 deletions.
3 changes: 3 additions & 0 deletions changelog/9692.improvement.rst
@@ -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.
40 changes: 28 additions & 12 deletions src/_pytest/python_api.py
@@ -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
Expand All @@ -8,7 +9,6 @@
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
Expand Down Expand Up @@ -306,12 +306,12 @@ def _check_type(self) -> None:
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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
15 changes: 10 additions & 5 deletions testing/python/approx.py
Expand Up @@ -858,13 +858,18 @@ def test_numpy_scalar_with_array(self):
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})

0 comments on commit 5805d58

Please sign in to comment.