From 79efc62949951fadf91f3c04ffdeb70d51f12717 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 | 18 +++++++++++------ testing/python/approx.py | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 6 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..481d81ddb41 --- /dev/null +++ b/changelog/11797.bugfix.rst @@ -0,0 +1 @@ +Ensure approx for SequenceLike objects doesn't wrap those sequences in a scalar. \ No newline at end of file diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f914d70e83f..cf0bb40f72a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -128,6 +128,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,12 +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) - # Type ignored because the error is wrong -- not unreachable. - and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] - ): + elif _is_sequence_like(expected): cls = ApproxSequenceLike elif ( isinstance(expected, Collection) @@ -741,6 +738,15 @@ 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) + # Type ignored because the error is wrong -- not unreachable. + and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] + ) + + 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 3b87e58f91a..10a31cbd495 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -937,6 +937,44 @@ 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): @@ -964,3 +1002,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]