From 76f3f3da00b67ff5fc8b84f111eae7bfa4436dc1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 15 Jan 2024 12:20:23 +0100 Subject: [PATCH] fix #11797: be more lenient on SequenceLike approx this needs a validation as it allows partially implemented sequences --- changelog/11797.bugfix.rst | 1 + src/_pytest/python_api.py | 16 ++++++++++----- testing/python/approx.py | 40 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 changelog/11797.bugfix.rst diff --git a/changelog/11797.bugfix.rst b/changelog/11797.bugfix.rst new file mode 100644 index 00000000000..94b72da00fd --- /dev/null +++ b/changelog/11797.bugfix.rst @@ -0,0 +1 @@ +:func:`pytest.approx` now correctly handles :class:`Sequence `-like objects. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9b17e5cb786..bfc8dc33445 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -129,6 +129,8 @@ def _recursive_sequence_map(f, x): if isinstance(x, (list, tuple)): seq_type = type(x) return seq_type(_recursive_sequence_map(f, xi) for xi in x) + elif _is_sequence_like(x): + return [_recursive_sequence_map(f, xi) for xi in x] else: return f(x) @@ -721,11 +723,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: elif _is_numpy_array(expected): expected = _as_numpy_array(expected) cls = ApproxNumpy - elif ( - hasattr(expected, "__getitem__") - and isinstance(expected, Sized) - and not isinstance(expected, (str, bytes)) - ): + elif _is_sequence_like(expected): cls = ApproxSequenceLike elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)): msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" @@ -736,6 +734,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: return cls(expected, rel, abs, nan_ok) +def _is_sequence_like(expected: object) -> bool: + return ( + hasattr(expected, "__getitem__") + and isinstance(expected, Sized) + and not isinstance(expected, (str, bytes)) + ) + + def _is_numpy_array(obj: object) -> bool: """ Return true if the given object is implicitly convertible to ndarray, diff --git a/testing/python/approx.py b/testing/python/approx.py index 968e8828512..31e64c4df3d 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -954,6 +954,43 @@ def test_allow_ordered_sequences_only(self) -> None: with pytest.raises(TypeError, match="only supports ordered sequences"): assert {1, 2, 3} == approx({1, 2, 3}) + def test_strange_sequence(self): + """https://github.com/pytest-dev/pytest/issues/11797""" + a = MyVec3(1, 2, 3) + b = MyVec3(0, 1, 2) + + # this would trigger the error inside the test + pytest.approx(a, abs=0.5)._repr_compare(b) + + assert b == pytest.approx(a, abs=2) + assert b != pytest.approx(a, abs=0.5) + + +class MyVec3: # incomplete + """sequence like""" + + _x: int + _y: int + _z: int + + def __init__(self, x: int, y: int, z: int): + self._x, self._y, self._z = x, y, z + + def __repr__(self) -> str: + return f"" + + def __len__(self) -> int: + return 3 + + def __getitem__(self, key: int) -> int: + if key == 0: + return self._x + if key == 1: + return self._y + if key == 2: + return self._z + raise IndexError(key) + class TestRecursiveSequenceMap: def test_map_over_scalar(self): @@ -981,3 +1018,6 @@ def test_map_over_mixed_sequence(self): (5, 8), [(7)], ] + + def test_map_over_sequence_like(self): + assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3]