From 1597860ac8c38cb91e759f8ac874a691b3767f2e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 7 Nov 2022 14:55:45 +0300 Subject: [PATCH 1/2] gh-94808: add tests covering `PySequence_[InPlace_]Repeat` --- Lib/test/test_capi.py | 166 ++++++++++++++++++++++++++++++++++++++ Modules/_testcapimodule.c | 26 ++++++ 2 files changed, 192 insertions(+) diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 213b6d4feb63226..e0b24090ddb2d6f 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -496,6 +496,172 @@ def __delitem__(self, index): _testcapi.sequence_del_slice(mapping, 1, 3) self.assertEqual(mapping, {1: 'a', 2: 'b', 3: 'c'}) + def test_sequence_repeat(self): + # Basic test: + sequence = [1, 2, 3] + repeated_sequence = _testcapi.sequence_repeat(sequence, 2) + self.assertEqual(repeated_sequence, [1, 2, 3, 1, 2, 3]) + self.assertEqual(repeated_sequence, sequence * 2) + self.assertEqual(sequence, [1, 2, 3]) # it must not change + + # Corner values: + sequence = [1, 2, 3] + for value, res in [ + (0, []), + (1, [1, 2, 3]), + (-1, []), + ]: + repeated_sequence = _testcapi.sequence_repeat(sequence, value) + self.assertEqual(repeated_sequence, res) + self.assertEqual(repeated_sequence, sequence * value) + self.assertEqual(sequence, [1, 2, 3]) # it must not change + + def test_sequence_repeat_subtypes(self): + # List subtype: + class CustomList(list): ... + + sequence = CustomList(['a', 'b']) + repeated_sequence = _testcapi.sequence_repeat(sequence, 3) + self.assertEqual(repeated_sequence, + CustomList(['a', 'b', 'a', 'b', 'a', 'b'])) + self.assertEqual(repeated_sequence, sequence * 3) + self.assertEqual(sequence, CustomList(['a', 'b'])) + + # Tuple subtype: + class CustomTuple(tuple): ... + + sequence = CustomTuple(('a', 'b')) + repeated_sequence = _testcapi.sequence_repeat(sequence, 3) + self.assertEqual(repeated_sequence, + CustomTuple(('a', 'b', 'a', 'b', 'a', 'b'))) + self.assertEqual(repeated_sequence, sequence * 3) + self.assertEqual(sequence, CustomTuple(('a', 'b'))) + + # String subtype: + class CustomStr(str): ... + + sequence = CustomStr('abc') + repeated_sequence = _testcapi.sequence_repeat(sequence, 2) + self.assertEqual(repeated_sequence, + CustomStr('abcabc')) + self.assertEqual(repeated_sequence, sequence * 2) + self.assertEqual(sequence, CustomStr('abc')) + + def test_sequence_repeat_errors(self): + # Corner case, custom subtype returns `NotImplemented`: + class CustomListWithNI(list): + def __mul__(self, other): + return NotImplemented + + sequence = CustomListWithNI(['a', 'b']) + with self.assertRaises(TypeError): + _testcapi.sequence_repeat(sequence, 3) + self.assertEqual(sequence, CustomListWithNI(['a', 'b'])) # not changed + + # Non-sequences do not work (even with `__mul__`): + class CustomTypeWithMul: + def __mul__(self, other): + return 5 # won't be called + + not_sequence = CustomTypeWithMul() + with self.assertRaises(TypeError): + _testcapi.sequence_repeat(not_sequence, 1) + + def test_sequence_inplace_repeat(self): + # Basic list test: + sequence = [1, 2, 3] + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) + self.assertEqual(repeated_sequence, [1, 2, 3, 1, 2, 3]) + self.assertEqual(repeated_sequence, sequence) + self.assertEqual(sequence, [1, 2, 3, 1, 2, 3]) # changed + sequence_copy = [1, 2, 3] + sequence_copy *= 2 # It is the same thing + self.assertEqual(repeated_sequence, sequence_copy) + + # Basic tuple test: + sequence = (1, 2, 3) + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) + self.assertEqual(repeated_sequence, (1, 2, 3, 1, 2, 3)) + self.assertNotEqual(repeated_sequence, sequence) + self.assertEqual(sequence, (1, 2, 3)) # not changed + sequence *= 2 + self.assertEqual(repeated_sequence, sequence) + + # Basic string test: + sequence = 'abc' + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) + self.assertEqual(repeated_sequence, 'abcabc') + self.assertNotEqual(repeated_sequence, sequence) + self.assertEqual(sequence, 'abc') # not changed + sequence *= 2 + self.assertEqual(repeated_sequence, sequence) + + # Corner values: + sequence = (1, 2, 3) + for value, res in [ + (0, ()), + (1, (1, 2, 3)), + (-1, ()), + ]: + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, + value) + self.assertEqual(repeated_sequence, res) + self.assertEqual(repeated_sequence, sequence * value) + self.assertEqual(sequence, (1, 2, 3)) # tuple must not change + + def test_sequence_inplace_repeat_subtypes(self): + # List subtype: + class CustomList(list): ... + + sequence = CustomList(['a', 'b']) + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 3) + self.assertEqual(repeated_sequence, + CustomList(['a', 'b', 'a', 'b', 'a', 'b'])) + self.assertEqual(repeated_sequence, sequence) + + # Tuple subtype: + class CustomTuple(tuple): ... + + sequence = CustomTuple(('a', 'b')) + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 3) + self.assertEqual(repeated_sequence, + CustomTuple(('a', 'b', 'a', 'b', 'a', 'b'))) + self.assertEqual(repeated_sequence, sequence * 3) + self.assertEqual(sequence, CustomTuple(('a', 'b'))) + + # String subtype: + class CustomStr(str): ... + + sequence = CustomStr('abc') + repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) + self.assertEqual(repeated_sequence, CustomStr('abcabc')) + self.assertEqual(repeated_sequence, sequence * 2) + self.assertEqual(sequence, CustomStr('abc')) + + def test_sequence_inplace_repeat_errors(self): + # Corner case, custom subtype returns `NotImplemented`: + class CustomListWithNI(list): + def __mul__(self, other): + return NotImplemented + def __imul__(self, other): + return NotImplemented + + sequence = CustomListWithNI(['a', 'b']) + with self.assertRaises(TypeError): + _testcapi.sequence_inplace_repeat(sequence, 3) + self.assertEqual(sequence, CustomListWithNI(['a', 'b'])) # not changed + + # Non-sequences do not work (even with `__mul__` and `__imul__`): + class CustomTypeWithMul: + def __mul__(self, other): + return 0 + def __imul__(self, other): + return 1 # won't be called + + not_sequence = CustomTypeWithMul() + with self.assertRaises(TypeError): + _testcapi.sequence_inplace_repeat(not_sequence, 1) + @unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'), 'need _testcapi.negative_refcount') def test_negative_refcount(self): diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 66d1d476328d3fc..404172d8ad03875 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -4826,6 +4826,30 @@ sequence_del_slice(PyObject* self, PyObject *args) Py_RETURN_NONE; } +static PyObject * +sequence_repeat(PyObject* self, PyObject *args) +{ + PyObject *sequence; + Py_ssize_t count; + if (!PyArg_ParseTuple(args, "On", &sequence, &count)) { + return NULL; + } + + return PySequence_Repeat(sequence, count); +} + +static PyObject * +sequence_inplace_repeat(PyObject* self, PyObject *args) +{ + PyObject *sequence; + Py_ssize_t count; + if (!PyArg_ParseTuple(args, "On", &sequence, &count)) { + return NULL; + } + + return PySequence_InPlaceRepeat(sequence, count); +} + static PyObject * test_pythread_tss_key_state(PyObject *self, PyObject *args) { @@ -6256,6 +6280,8 @@ static PyMethodDef TestMethods[] = { {"mapping_has_key", mapping_has_key, METH_VARARGS}, {"sequence_set_slice", sequence_set_slice, METH_VARARGS}, {"sequence_del_slice", sequence_del_slice, METH_VARARGS}, + {"sequence_repeat", sequence_repeat, METH_VARARGS}, + {"sequence_inplace_repeat", sequence_inplace_repeat, METH_VARARGS}, {"test_pythread_tss_key_state", test_pythread_tss_key_state, METH_VARARGS}, {"hamt", new_hamt, METH_NOARGS}, {"bad_get", _PyCFunction_CAST(bad_get), METH_FASTCALL}, From a26a76c1a75ae56a6bdd85e9a6c5bec53be48c9c Mon Sep 17 00:00:00 2001 From: Lukasz Langa Date: Wed, 21 Oct 2026 21:23:25 +0200 Subject: [PATCH 2/2] Deduplicate repeat tests against test_abstract.py coverage Drop the basic repeat/in-place-repeat tests: test_abstract.py now covers those paths via _testlimitedcapi (including corner counts, MemoryError, TypeError and NULL cases). Keep the subtype and error tests unique to this PR. Add test_sequence_repeat_numeric_fallback for the previously untested nb_multiply fallback in PySequence_Repeat. Assert exact result types with assertIs: comparing against CustomList(...) only checks contents, and repeat of a builtin subtype returns the exact builtin type. Co-authored-by: Muse Code (Meta Muse Spark) --- Lib/test/test_capi/test_misc.py | 95 ++++++++------------------------- 1 file changed, 21 insertions(+), 74 deletions(-) diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 3f8a15078a2c2dd..b9c6882444ab767 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -404,34 +404,13 @@ def test_buildvalue_ints(self): def test_buildvalue_N(self): _testcapi.test_buildvalue_N() - def test_sequence_repeat(self): - # Basic test: - sequence = [1, 2, 3] - repeated_sequence = _testcapi.sequence_repeat(sequence, 2) - self.assertEqual(repeated_sequence, [1, 2, 3, 1, 2, 3]) - self.assertEqual(repeated_sequence, sequence * 2) - self.assertEqual(sequence, [1, 2, 3]) # it must not change - - # Corner values: - sequence = [1, 2, 3] - for value, res in [ - (0, []), - (1, [1, 2, 3]), - (-1, []), - ]: - repeated_sequence = _testcapi.sequence_repeat(sequence, value) - self.assertEqual(repeated_sequence, res) - self.assertEqual(repeated_sequence, sequence * value) - self.assertEqual(sequence, [1, 2, 3]) # it must not change - def test_sequence_repeat_subtypes(self): # List subtype: class CustomList(list): ... sequence = CustomList(['a', 'b']) repeated_sequence = _testcapi.sequence_repeat(sequence, 3) - self.assertEqual(repeated_sequence, - CustomList(['a', 'b', 'a', 'b', 'a', 'b'])) + self.assertIs(type(repeated_sequence), list) self.assertEqual(repeated_sequence, sequence * 3) self.assertEqual(sequence, CustomList(['a', 'b'])) @@ -440,8 +419,7 @@ class CustomTuple(tuple): ... sequence = CustomTuple(('a', 'b')) repeated_sequence = _testcapi.sequence_repeat(sequence, 3) - self.assertEqual(repeated_sequence, - CustomTuple(('a', 'b', 'a', 'b', 'a', 'b'))) + self.assertIs(type(repeated_sequence), tuple) self.assertEqual(repeated_sequence, sequence * 3) self.assertEqual(sequence, CustomTuple(('a', 'b'))) @@ -450,11 +428,24 @@ class CustomStr(str): ... sequence = CustomStr('abc') repeated_sequence = _testcapi.sequence_repeat(sequence, 2) - self.assertEqual(repeated_sequence, - CustomStr('abcabc')) + self.assertIs(type(repeated_sequence), str) self.assertEqual(repeated_sequence, sequence * 2) self.assertEqual(sequence, CustomStr('abc')) + def test_sequence_repeat_numeric_fallback(self): + # PySequence_Repeat falls back to nb_multiply (__mul__) when the + # object looks like a sequence but has no sq_repeat slot. + class CustomSequenceWithMul: + def __getitem__(self, index): + return index # so that it looks like a sequence + + def __mul__(self, count): + return f'repeated {count} times' + + sequence = CustomSequenceWithMul() + self.assertEqual(_testcapi.sequence_repeat(sequence, 3), + 'repeated 3 times') + def test_sequence_repeat_errors(self): # Corner case, custom subtype returns `NotImplemented`: class CustomListWithNI(list): @@ -475,65 +466,21 @@ def __mul__(self, other): with self.assertRaises(TypeError): _testcapi.sequence_repeat(not_sequence, 1) - def test_sequence_inplace_repeat(self): - # Basic list test: - sequence = [1, 2, 3] - repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) - self.assertEqual(repeated_sequence, [1, 2, 3, 1, 2, 3]) - self.assertEqual(repeated_sequence, sequence) - self.assertEqual(sequence, [1, 2, 3, 1, 2, 3]) # changed - sequence_copy = [1, 2, 3] - sequence_copy *= 2 # It is the same thing - self.assertEqual(repeated_sequence, sequence_copy) - - # Basic tuple test: - sequence = (1, 2, 3) - repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) - self.assertEqual(repeated_sequence, (1, 2, 3, 1, 2, 3)) - self.assertNotEqual(repeated_sequence, sequence) - self.assertEqual(sequence, (1, 2, 3)) # not changed - sequence *= 2 - self.assertEqual(repeated_sequence, sequence) - - # Basic string test: - sequence = 'abc' - repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) - self.assertEqual(repeated_sequence, 'abcabc') - self.assertNotEqual(repeated_sequence, sequence) - self.assertEqual(sequence, 'abc') # not changed - sequence *= 2 - self.assertEqual(repeated_sequence, sequence) - - # Corner values: - sequence = (1, 2, 3) - for value, res in [ - (0, ()), - (1, (1, 2, 3)), - (-1, ()), - ]: - repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, - value) - self.assertEqual(repeated_sequence, res) - self.assertEqual(repeated_sequence, sequence * value) - self.assertEqual(sequence, (1, 2, 3)) # tuple must not change - def test_sequence_inplace_repeat_subtypes(self): # List subtype: class CustomList(list): ... sequence = CustomList(['a', 'b']) repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 3) - self.assertEqual(repeated_sequence, - CustomList(['a', 'b', 'a', 'b', 'a', 'b'])) - self.assertEqual(repeated_sequence, sequence) + self.assertIs(repeated_sequence, sequence) + self.assertEqual(repeated_sequence, ['a', 'b', 'a', 'b', 'a', 'b']) # Tuple subtype: class CustomTuple(tuple): ... sequence = CustomTuple(('a', 'b')) repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 3) - self.assertEqual(repeated_sequence, - CustomTuple(('a', 'b', 'a', 'b', 'a', 'b'))) + self.assertIs(type(repeated_sequence), tuple) self.assertEqual(repeated_sequence, sequence * 3) self.assertEqual(sequence, CustomTuple(('a', 'b'))) @@ -542,7 +489,7 @@ class CustomStr(str): ... sequence = CustomStr('abc') repeated_sequence = _testcapi.sequence_inplace_repeat(sequence, 2) - self.assertEqual(repeated_sequence, CustomStr('abcabc')) + self.assertIs(type(repeated_sequence), str) self.assertEqual(repeated_sequence, sequence * 2) self.assertEqual(sequence, CustomStr('abc'))