From 12a429ef0669bd153e7064b7501115f2a378ee4d 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 12:46:15 +0200 Subject: [PATCH 01/11] add `count` to memoryview objects --- Lib/test/test_memoryview.py | 16 +++++++++ Objects/clinic/memoryobject.c.h | 57 ++++++++++++++++++++++++++++++++- Objects/memoryobject.c | 49 ++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 2d4bf5f1408df8..7b449d3add76f0 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -64,6 +64,22 @@ def test_iter(self): m = self._view(b) self.assertEqual(list(m), [m[i] for i in range(len(m))]) + def test_count(self): + for tp in self._types: + b = tp(self._source) + m = self._view(b) + l = m.tolist() + for ch in list(m): + self.assertEqual(m.count(ch), l.count(ch)) + + b = tp((b'a' * 5) + (b'c' * 3)) + m = self._view(b) # may be sliced + l = m.tolist() + with self.subTest('count', buffer=b): + self.assertEqual(m.count(ord('a')), l.count(ord('a'))) + self.assertEqual(m.count(ord('b')), l.count(ord('b'))) + self.assertEqual(m.count(ord('c')), l.count(ord('c'))) + def test_setitem_readonly(self): if not self.ro_type: self.skipTest("no read-only type to test") diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index f199434dacb9e8..2e1093ca077dbe 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -413,4 +413,59 @@ 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_count__doc__, +"count($self, /, value)\n" +"--\n" +"\n" +"Count the number of occurrences of a value."); + +#define MEMORYVIEW_COUNT_METHODDEF \ + {"count", _PyCFunction_CAST(memoryview_count), METH_FASTCALL|METH_KEYWORDS, memoryview_count__doc__}, + +static PyObject * +memoryview_count_impl(PyMemoryViewObject *self, PyObject *value); + +static PyObject * +memoryview_count(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(value), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"value", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "count", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *value; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf); + if (!args) { + goto exit; + } + value = args[0]; + return_value = memoryview_count_impl(self, value); + +exit: + return return_value; +} +/*[clinic end generated code: output=008d9d2ec9323b05 input=a9049054013a1b77]*/ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index a2472d4873641d..ce200e5827df04 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2748,6 +2748,54 @@ static PySequenceMethods memory_as_sequence = { }; +/****************************************************************************/ +/* Counting */ +/****************************************************************************/ + +/*[clinic input] +memoryview.count + + value: object + +Count the number of occurrences of a value. +[clinic start generated code]*/ + +static PyObject * +memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) +/*[clinic end generated code: output=a15cb19311985063 input=9015233dd6045385]*/ +{ + PyObject *iter = PyObject_GetIter((PyObject *)self); + if (iter == NULL) { + return NULL; + } + + Py_ssize_t count = 0; + PyObject *item = NULL; + while (PyIter_NextItem(iter, &item)) { + if (item == NULL) { + Py_DECREF(iter); + return NULL; + } + if (item == value) { // fast path + count++; + Py_DECREF(item); + continue; + } + int contained = PyObject_RichCompareBool(item, value, Py_EQ); + Py_DECREF(item); + if (contained > 0) { // more likely than contained < 0 + count += 1; + } + else if (contained < 0) { + Py_DECREF(iter); + return NULL; + } + } + Py_DECREF(iter); + return PyLong_FromSsize_t(count); +} + + /**************************************************************************/ /* Comparisons */ /**************************************************************************/ @@ -3284,6 +3332,7 @@ static PyMethodDef memory_methods[] = { MEMORYVIEW_CAST_METHODDEF MEMORYVIEW_TOREADONLY_METHODDEF MEMORYVIEW__FROM_FLAGS_METHODDEF + MEMORYVIEW_COUNT_METHODDEF {"__enter__", memory_enter, METH_NOARGS, NULL}, {"__exit__", memory_exit, METH_VARARGS, memory_exit_doc}, {NULL, NULL} From 8234c5fd05962299d4527ca7bb4fa85dd99d1a16 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 12:46:19 +0200 Subject: [PATCH 02/11] docs --- Doc/library/stdtypes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index a6e2e3b8928ebe..3498ea3e78b7fd 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4140,6 +4140,12 @@ copying. .. versionchanged:: 3.5 The source format is no longer restricted when casting to a byte view. + .. method:: count(value) + + Count the number of occurrences of *value*. + + .. versionadded:: 3.14 + There are also several readonly attributes available: .. attribute:: obj From a253c9b88f957f353badbc9bc2575a2457b8b438 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 12:46:21 +0200 Subject: [PATCH 03/11] blurb --- .../2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst new file mode 100644 index 00000000000000..0cdd9f0275b3dc --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst @@ -0,0 +1,2 @@ +Add :meth:`memoryview.count` to :class:`memoryview` objects. Patch by +Béndikt Tran. From 35c3c0a1c1b3ee4ee4e118882c95080288ea01d4 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 12:50:26 +0200 Subject: [PATCH 04/11] remove redundant fast path --- Objects/memoryobject.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index ce200e5827df04..c7c4083339d469 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2776,11 +2776,6 @@ memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) Py_DECREF(iter); return NULL; } - if (item == value) { // fast path - count++; - Py_DECREF(item); - continue; - } int contained = PyObject_RichCompareBool(item, value, Py_EQ); Py_DECREF(item); if (contained > 0) { // more likely than contained < 0 From be66e19e1f25680a2ca626656add5cada9736fc6 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 12:51:18 +0200 Subject: [PATCH 05/11] use of `count++` instead of `count += 1` --- Objects/memoryobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index c7c4083339d469..59efd145771bec 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2779,7 +2779,7 @@ memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) int contained = PyObject_RichCompareBool(item, value, Py_EQ); Py_DECREF(item); if (contained > 0) { // more likely than contained < 0 - count += 1; + count++; } else if (contained < 0) { Py_DECREF(iter); From 697b10c76801edaad56709d82f19f20eae4010f6 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:41:40 +0200 Subject: [PATCH 06/11] fixups --- ...-10-14-12-34-51.gh-issue-125420.jABXoZ.rst | 2 +- Objects/clinic/memoryobject.c.h | 52 ++----------------- Objects/memoryobject.c | 5 +- 3 files changed, 7 insertions(+), 52 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst index 0cdd9f0275b3dc..ef120802b5dc59 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-14-12-34-51.gh-issue-125420.jABXoZ.rst @@ -1,2 +1,2 @@ Add :meth:`memoryview.count` to :class:`memoryview` objects. Patch by -Béndikt Tran. +Bénédikt Tran. diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index 2e1093ca077dbe..72568655ee587b 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -415,57 +415,11 @@ memoryview_hex(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs } PyDoc_STRVAR(memoryview_count__doc__, -"count($self, /, value)\n" +"count($self, value, /)\n" "--\n" "\n" "Count the number of occurrences of a value."); #define MEMORYVIEW_COUNT_METHODDEF \ - {"count", _PyCFunction_CAST(memoryview_count), METH_FASTCALL|METH_KEYWORDS, memoryview_count__doc__}, - -static PyObject * -memoryview_count_impl(PyMemoryViewObject *self, PyObject *value); - -static PyObject * -memoryview_count(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 1 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_item = { &_Py_ID(value), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"value", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "count", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[1]; - PyObject *value; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf); - if (!args) { - goto exit; - } - value = args[0]; - return_value = memoryview_count_impl(self, value); - -exit: - return return_value; -} -/*[clinic end generated code: output=008d9d2ec9323b05 input=a9049054013a1b77]*/ + {"count", (PyCFunction)memoryview_count, METH_O, memoryview_count__doc__}, +/*[clinic end generated code: output=286d22a0d04e1b91 input=a9049054013a1b77]*/ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 59efd145771bec..cc1a19ce2f32eb 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2756,13 +2756,14 @@ static PySequenceMethods memory_as_sequence = { memoryview.count value: object + / Count the number of occurrences of a value. [clinic start generated code]*/ static PyObject * -memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) -/*[clinic end generated code: output=a15cb19311985063 input=9015233dd6045385]*/ +memoryview_count(PyMemoryViewObject *self, PyObject *value) +/*[clinic end generated code: output=e2c255a8d54eaa12 input=e3036ce1ed7d1823]*/ { PyObject *iter = PyObject_GetIter((PyObject *)self); if (iter == NULL) { From 2e3d8a79cbad6935298094c33b9f5a8157e908e2 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:43:18 +0200 Subject: [PATCH 07/11] update 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 3498ea3e78b7fd..9ab1be4acd231a 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) + .. method:: count(value, /) Count the number of occurrences of *value*. From d311794a148c55fd4d1bb23dfc4c4e549becd6fd 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 15:26:00 +0200 Subject: [PATCH 08/11] increase test coverage --- Lib/test/test_memoryview.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 7b449d3add76f0..cac6d33fea21ef 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -454,6 +454,18 @@ def _view(self, obj): def _check_contents(self, tp, obj, contents): self.assertEqual(obj, tp(contents)) + def test_count(self): + super().test_count() + for tp in self._types: + b = tp((b'a' * 5) + (b'c' * 3)) + m = self._view(b) # should not be sliced + self.assertEqual(len(b), len(m)) + with self.subTest('count', buffer=b): + self.assertEqual(m.count(ord('a')), 5) + self.assertEqual(m.count(ord('b')), 0) + self.assertEqual(m.count(ord('c')), 3) + + class BaseMemorySliceTests: source_bytes = b"XabcdefY" From 41d977a8088b5e0f0910b0d95eabecc348450365 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 15:28:14 +0200 Subject: [PATCH 09/11] add a fast path for comparison --- Objects/memoryobject.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index cc1a19ce2f32eb..6d1175f9b88eb2 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2777,6 +2777,11 @@ memoryview_count(PyMemoryViewObject *self, PyObject *value) Py_DECREF(iter); return NULL; } + if (item == value) { + Py_DECREF(item); + count++; + continue; + } int contained = PyObject_RichCompareBool(item, value, Py_EQ); Py_DECREF(item); if (contained > 0) { // more likely than contained < 0 From 0c8d8ec2bc521bff4c05dce79deea6bcf8dcc349 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:17:07 +0200 Subject: [PATCH 10/11] Add note on the impossibility of overflows --- Objects/memoryobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 6d1175f9b88eb2..a32085ab6d4e89 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2765,7 +2765,7 @@ static PyObject * memoryview_count(PyMemoryViewObject *self, PyObject *value) /*[clinic end generated code: output=e2c255a8d54eaa12 input=e3036ce1ed7d1823]*/ { - PyObject *iter = PyObject_GetIter((PyObject *)self); + PyObject *iter = PyObject_GetIter(_PyObject_CAST(self)); if (iter == NULL) { return NULL; } @@ -2779,13 +2779,13 @@ memoryview_count(PyMemoryViewObject *self, PyObject *value) } if (item == value) { Py_DECREF(item); - count++; + count++; // no overflow since count <= len(mv) <= PY_SSIZE_T_MAX continue; } int contained = PyObject_RichCompareBool(item, value, Py_EQ); Py_DECREF(item); - if (contained > 0) { // more likely than contained < 0 - count++; + if (contained > 0) { // more likely than 'contained < 0' + count++; // no overflow since count <= len(mv) <= PY_SSIZE_T_MAX } else if (contained < 0) { Py_DECREF(iter); From 2a387d5c77b9733e7b65c21923dd78a985a9d17e 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:09 +0100 Subject: [PATCH 11/11] 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 9ab1be4acd231a..ac05996b218f79 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4144,7 +4144,7 @@ copying. Count the number of occurrences of *value*. - .. versionadded:: 3.14 + .. versionadded:: next There are also several readonly attributes available: