diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 88de78d024..e820417bd9 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -2032,7 +2032,13 @@ class sequence : public object { return object::operator[](std::forward(o)); } detail::sequence_iterator begin() const { return {*this, 0}; } - detail::sequence_iterator end() const { return {*this, PySequence_Size(m_ptr)}; } + detail::sequence_iterator end() const { + ssize_t result = PySequence_Size(m_ptr); + if (result == -1) { + throw error_already_set(); + } + return {*this, result}; + } }; class list : public object { diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index b500eb7972..2764645466 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -461,6 +461,20 @@ TEST_SUBMODULE(pytypes, m) { return py::tuple(); }); + m.def("capi_sequence_end", [](const py::sequence &seq) { + py::detail::sequence_iterator end; + try { + end = seq.end(); + } catch (const py::error_already_set &) { + throw; + } + if (PyErr_Occurred()) { + py::raise_from(PyExc_AssertionError, "sequence::end returned with a pending error"); + throw py::error_already_set(); + } + return end - seq.begin(); + }); + m.def("accessor_assignment", []() { auto l = py::list(1); l[0] = 0; diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 3c7b31e093..b633c1cde0 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -837,6 +837,49 @@ def test_cpp_iterators(): assert m.passed_iterator(iter((-7, 3))) == -4 +@pytest.mark.parametrize("error_type", [RuntimeError, KeyboardInterrupt, SystemExit]) +def test_capi_sequence_end_error(error_type): + error = error_type("sequence length failed") + + class BadLength: + def __getitem__(self, index): + raise AssertionError("end must not retrieve items") + + def __len__(self): + raise error + + with pytest.raises(error_type) as excinfo: + m.capi_sequence_end(BadLength()) + assert excinfo.value is error + assert "__len__" in [entry.name for entry in excinfo.traceback] + + +def test_capi_sequence_end_negative_length(): + class NegativeLength: + def __getitem__(self, index): + raise AssertionError("end must not retrieve items") + + def __len__(self): + return -1 # noqa: PLE0303 - deliberately invalid length + + with pytest.raises(ValueError): + m.capi_sequence_end(NegativeLength()) + + +@pytest.mark.parametrize("values", [[], [4, 7]]) +@pytest.mark.parametrize("kind", [list, tuple, "custom"]) +def test_capi_sequence_end_valid(values, kind): + class CustomSequence: + def __getitem__(self, index): + return values[index] + + def __len__(self): + return len(values) + + sequence = CustomSequence() if kind == "custom" else kind(values) + assert m.capi_sequence_end(sequence) == len(values) + + def test_implementation_details(): lst = [39, 43, 92, 49, 22, 29, 93, 98, 26, 57, 8] tup = tuple(lst)