From 7b78549e6114f19ee7310fcd2def1864ed3f6ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:27:34 +0200 Subject: [PATCH 1/9] add `Sequence.index` to `memoryview` objects --- Lib/test/test_memoryview.py | 28 ++++++++++++- Objects/clinic/memoryobject.c.h | 48 +++++++++++++++++++++- Objects/memoryobject.c | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 2d4bf5f1408df8..767e07deb739c2 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -15,8 +15,9 @@ import pickle import struct +from itertools import product from test.support import import_helper - +from types import NotImplementedType class MyObject: pass @@ -58,6 +59,31 @@ def test_getitem(self): for tp in self._types: self.check_getitem_with_type(tp) + def test_index(self): + for tp in self._types: + b = tp(self._source) + m = self._view(b) # may be a sub-view + l = m.tolist() + k = 2 * len(self._source) + + for chi in self._source: + if chi in l: + self.assertEqual(m.index(chi), l.index(chi)) + else: + self.assertRaises(ValueError, m.index, chi) + + for start, stop in product(range(-k, k), range(-k, k)): + index = -1 + try: + index = l.index(chi, start, stop) + except ValueError: + pass + + if index == -1: + self.assertRaises(ValueError, m.index, chi, start, stop) + else: + self.assertEqual(m.index(chi, start, stop), index) + def test_iter(self): for tp in self._types: b = tp(self._source) diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index f199434dacb9e8..70e9736394f18b 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -413,4 +413,50 @@ memoryview_hex(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs exit: return return_value; } -/*[clinic end generated code: output=7e76a09106921ba2 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(memoryview_index__doc__, +"index($self, value, start=0, stop=sys.maxsize, /)\n" +"--\n" +"\n" +"Return the index of the first occurrence of a value.\n" +"\n" +"Raises ValueError if the value is not present."); + +#define MEMORYVIEW_INDEX_METHODDEF \ + {"index", _PyCFunction_CAST(memoryview_index), METH_FASTCALL, memoryview_index__doc__}, + +static PyObject * +memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop); + +static PyObject * +memoryview_index(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *value; + Py_ssize_t start = 0; + Py_ssize_t stop = PY_SSIZE_T_MAX; + + if (!_PyArg_CheckPositional("index", nargs, 1, 3)) { + goto exit; + } + value = args[0]; + if (nargs < 2) { + goto skip_optional; + } + if (!_PyEval_SliceIndexNotNone(args[1], &start)) { + goto exit; + } + if (nargs < 3) { + goto skip_optional; + } + if (!_PyEval_SliceIndexNotNone(args[2], &stop)) { + goto exit; + } +skip_optional: + return_value = memoryview_index_impl(self, value, start, stop); + +exit: + return return_value; +} +/*[clinic end generated code: output=643b92b4c02069f9 input=a9049054013a1b77]*/ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index a2472d4873641d..9f436c79a3033b 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2748,6 +2748,77 @@ static PySequenceMethods memory_as_sequence = { }; +/**************************************************************************/ +/* Lookup */ +/**************************************************************************/ + +/*[clinic input] +memoryview.index + + value: object + start: slice_index(accept={int}) = 0 + stop: slice_index(accept={int}, c_default="PY_SSIZE_T_MAX") = sys.maxsize + / + +Return the index of the first occurrence of a value. + +Raises ValueError if the value is not present. +[clinic start generated code]*/ + +static PyObject * +memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop) +/*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ +{ + Py_buffer *view = &(self->view); + CHECK_RELEASED(self); + + if (view->ndim == 0) { + PyErr_SetString(PyExc_TypeError, "invalid indexing of 0-dim memory"); + return NULL; + } + + if (view->ndim == 1) { + Py_ssize_t n = view->shape[0]; + + if (start < 0) { + start = Py_MAX(start + n, 0); + } + + if (stop < 0) { + stop = Py_MAX(stop + n, 0); + } + + stop = Py_MIN(stop, n); + start = Py_MIN(start, stop); + + for (Py_ssize_t index = start; index < stop; index++) { + PyObject *item = memory_item((PyObject *)self, index); + if (item == NULL) { + return NULL; + } + int contained = PyObject_RichCompareBool(item, value, Py_EQ); + Py_DECREF(item); + if (contained > 0) { + return PyLong_FromSsize_t(index); + } + else if (contained < 0) { + return NULL; + } + } + + PyErr_SetString(PyExc_ValueError, + "memoryview.index(x): x not in list"); + return NULL; + } + + PyErr_SetString(PyExc_NotImplementedError, + "multi-dimensional lookup is not implemented"); + return NULL; + +} + + /**************************************************************************/ /* Comparisons */ /**************************************************************************/ @@ -3284,6 +3355,7 @@ static PyMethodDef memory_methods[] = { MEMORYVIEW_CAST_METHODDEF MEMORYVIEW_TOREADONLY_METHODDEF MEMORYVIEW__FROM_FLAGS_METHODDEF + MEMORYVIEW_INDEX_METHODDEF {"__enter__", memory_enter, METH_NOARGS, NULL}, {"__exit__", memory_exit, METH_VARARGS, memory_exit_doc}, {NULL, NULL} From a06692430ec2592c6eb1df6b217d4ea9cbeb378c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:31:34 +0200 Subject: [PATCH 2/9] docs --- Doc/library/stdtypes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index a6e2e3b8928ebe..fc39a4d3d3fe4d 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4140,6 +4140,15 @@ copying. .. versionchanged:: 3.5 The source format is no longer restricted when casting to a byte view. + .. method:: count(value, start=0, stop=sys.maxsize, /) + + Return the index of the first occurrence of *value* (at or after + index *start* and before index *stop*). + + Raises a :exc:`ValueError` if *value* cannot be found. + + .. versionadded:: 3.14 + There are also several readonly attributes available: .. attribute:: obj From 5c2cf0863471636836d671de94d906d6dff3644d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:31:35 +0200 Subject: [PATCH 3/9] blurb --- .../2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst new file mode 100644 index 00000000000000..a9ffe4acd56ef6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst @@ -0,0 +1,2 @@ +Add :meth:`memoryview.index` to :class:`memoryview` objects. Patch by +Béndikt Tran. From 10245f887352f7b602fd7a3c415e3fc34d35a96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:34:46 +0200 Subject: [PATCH 4/9] remove IDE imports --- Lib/test/test_memoryview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 767e07deb739c2..8f07828c19790d 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -17,7 +17,7 @@ from itertools import product from test.support import import_helper -from types import NotImplementedType + class MyObject: pass From 02f94c996c69832813236c130e8afd9bd3092656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:35:05 +0200 Subject: [PATCH 5/9] fixup typo! --- .../2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst index a9ffe4acd56ef6..6ed823175f6754 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-13-28-16.gh-issue-125420.hNKixM.rst @@ -1,2 +1,2 @@ Add :meth:`memoryview.index` to :class:`memoryview` objects. Patch by -Béndikt Tran. +Bénédikt Tran. From ea0125db859ca0ca93f9ce300ae9d73cdd406cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:35:33 +0200 Subject: [PATCH 6/9] fixup docs --- Doc/library/stdtypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index fc39a4d3d3fe4d..1fd11bba0c7acc 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4140,7 +4140,7 @@ copying. .. versionchanged:: 3.5 The source format is no longer restricted when casting to a byte view. - .. method:: count(value, start=0, stop=sys.maxsize, /) + .. method:: index(value, start=0, stop=sys.maxsize, /) Return the index of the first occurrence of *value* (at or after index *start* and before index *stop*). From 9f2efa124f6f59e7b4cec293f073bcd4f772b032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:59:12 +0200 Subject: [PATCH 7/9] add fast path --- Objects/memoryobject.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 9f436c79a3033b..9405b675d9fc62 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2770,11 +2770,11 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, Py_ssize_t start, Py_ssize_t stop) /*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ { - Py_buffer *view = &(self->view); + const Py_buffer *view = &self->view; CHECK_RELEASED(self); if (view->ndim == 0) { - PyErr_SetString(PyExc_TypeError, "invalid indexing of 0-dim memory"); + PyErr_SetString(PyExc_TypeError, "invalid lookup on 0-dim memory"); return NULL; } @@ -2790,16 +2790,32 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, } stop = Py_MIN(stop, n); + assert(stop >= 0); + assert(stop <= n); + start = Py_MIN(start, stop); + assert(0 <= start); + assert(start <= stop); + PyObject *obj = _PyObject_CAST(self); for (Py_ssize_t index = start; index < stop; index++) { - PyObject *item = memory_item((PyObject *)self, index); + // Note: while memoryviews can be mutated during iterations + // when calling the == operator, their shape cannot. As such, + // it is safe to assume that the index remains valid for the + // entire loop. + assert(index < Py_SIZE(obj)); + + PyObject *item = memory_item(obj, index); if (item == NULL) { return NULL; } + if (item == value) { + Py_DECREF(item); + return PyLong_FromSsize_t(index); + } int contained = PyObject_RichCompareBool(item, value, Py_EQ); Py_DECREF(item); - if (contained > 0) { + if (contained > 0) { // more likely than 'contained < 0' return PyLong_FromSsize_t(index); } else if (contained < 0) { @@ -2807,8 +2823,7 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, } } - PyErr_SetString(PyExc_ValueError, - "memoryview.index(x): x not in list"); + PyErr_SetString(PyExc_ValueError, "memoryview.index(x): x not found"); return NULL; } From beca7afa3a16e10a493ad35958bebffb2e12a6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 27 Oct 2024 12:06:28 +0100 Subject: [PATCH 8/9] fixup docs --- Doc/library/stdtypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 1fd11bba0c7acc..ca79bdbee06b44 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4147,7 +4147,7 @@ copying. Raises a :exc:`ValueError` if *value* cannot be found. - .. versionadded:: 3.14 + .. versionadded:: next There are also several readonly attributes available: From 5458b14664898866d7a9dd0850ed6fdc59d47416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 27 Oct 2024 12:19:09 +0100 Subject: [PATCH 9/9] fixup tests! --- Objects/memoryobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 9405b675d9fc62..3205e7d3d7bddf 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2803,7 +2803,7 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, // when calling the == operator, their shape cannot. As such, // it is safe to assume that the index remains valid for the // entire loop. - assert(index < Py_SIZE(obj)); + assert(index < n); PyObject *item = memory_item(obj, index); if (item == NULL) {