Skip to content

Commit 8c056c3

Browse files
serhiy-storchakaclaudevstinner
authored
gh-158102: Add tests for the slice C API (GH-158120)
Test PySlice_Check(), PySlice_New(), PySlice_GetIndices(), PySlice_Unpack(), PySlice_AdjustIndices() and PySlice_GetIndicesEx() -- both the macro and the deprecated function. Document the length and step precondition of PySlice_AdjustIndices() Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent c043873 commit 8c056c3

8 files changed

Lines changed: 559 additions & 1 deletion

File tree

‎Doc/c-api/slice.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Slice Objects
5353
length *length*, and store the length of the slice in *slicelength*. Out
5454
of bounds indices are clipped in a manner consistent with the handling of
5555
normal slices.
56+
*length* must not be negative.
5657
5758
Return ``0`` on success and ``-1`` on error with an exception set.
5859
@@ -108,6 +109,10 @@ Slice Objects
108109
Out of bounds indices are clipped in a manner consistent with the handling
109110
of normal slices.
110111
112+
*length* must not be negative.
113+
*step* must not be zero and must not be less than ``-PY_SSIZE_T_MAX``,
114+
as guaranteed by :c:func:`PySlice_Unpack`.
115+
111116
Return the length of the slice. Always successful. Doesn't call Python
112117
code.
113118

‎Lib/test/test_capi/test_slice.py‎

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import sys
2+
import unittest
3+
from test.support import import_helper
4+
5+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
6+
7+
NULL = None
8+
SSIZE_MAX = sys.maxsize
9+
SSIZE_MIN = -sys.maxsize - 1
10+
11+
VALUES = [None, 0, 1, 3, 7, -1, -3, -7]
12+
STEPS = [None, 1, 3, 5, -1, -3, -5]
13+
LENGTHS = [0, 1, 3, 10]
14+
15+
16+
class Index:
17+
def __init__(self, value):
18+
self.value = value
19+
20+
def __index__(self):
21+
return self.value
22+
23+
24+
class BadIndex:
25+
def __index__(self):
26+
raise RuntimeError('bad index')
27+
28+
29+
class PopIndex:
30+
# __index__() removes the last item of the list.
31+
def __init__(self, value, seq):
32+
self.value = value
33+
self.seq = seq
34+
35+
def __index__(self):
36+
self.seq.pop()
37+
return self.value
38+
39+
40+
class SliceTest(unittest.TestCase):
41+
42+
def test_check(self):
43+
# Test PySlice_Check()
44+
check = _testlimitedcapi.slice_check
45+
self.assertTrue(check(slice(1, 7, 2)))
46+
self.assertFalse(check(object()))
47+
48+
# CRASHES check(NULL)
49+
50+
def test_new(self):
51+
# Test PySlice_New()
52+
new = _testlimitedcapi.slice_new
53+
self.assertEqual(new(1, 7, 2), slice(1, 7, 2))
54+
self.assertEqual(new(7, 1, -2), slice(7, 1, -2))
55+
self.assertEqual(new('a', 'b', 'c'), slice('a', 'b', 'c'))
56+
self.assertEqual(new(NULL, NULL, NULL), slice(None, None, None))
57+
58+
def test_getindices(self):
59+
# Test PySlice_GetIndices()
60+
getindices = _testlimitedcapi.slice_getindices
61+
self.assertEqual(getindices(slice(1, 7, 2), 10), (1, 7, 2))
62+
self.assertEqual(getindices(slice(None), 10), (0, 10, 1))
63+
self.assertEqual(getindices(slice(None, None, -1), 10), (9, -1, -1))
64+
self.assertEqual(getindices(slice(-3, -1), 10), (7, 9, 1))
65+
self.assertEqual(getindices(slice(-1, -3, -1), 10), (9, 7, -1))
66+
self.assertEqual(getindices(slice(None, None, -2), 0), (-1, -1, -2))
67+
self.assertEqual(getindices(slice(-3, -5, 1), 0), (-3, -5, 1))
68+
69+
# It fails without setting an exception for out of bounds indices,
70+
# a zero step and non-integer indices.
71+
self.assertIsNone(getindices(slice(1, 11), 10))
72+
self.assertIsNone(getindices(slice(10, 1), 10))
73+
self.assertIsNone(getindices(slice(1, 7, 0), 10))
74+
self.assertIsNone(getindices(slice(Index(1)), 10))
75+
self.assertIsNone(getindices(slice('a'), 10))
76+
self.assertIsNone(getindices(slice(1, 'a'), 10))
77+
self.assertIsNone(getindices(slice(1, 7, 'a'), 10))
78+
79+
# Negative length is not supported, but does not fail.
80+
self.assertIsNone(getindices(slice(None), -3))
81+
self.assertIsNone(getindices(slice(1, 7, 2), -3))
82+
self.assertEqual(getindices(slice(-10, -5, 1), -3), (-13, -8, 1))
83+
self.assertEqual(getindices(slice(-5, -10, -2), -3), (-8, -13, -2))
84+
85+
# CRASHES getindices(NULL, 10)
86+
# CRASHES getindices(object(), 10)
87+
88+
def test_unpack(self):
89+
# Test PySlice_Unpack()
90+
unpack = _testlimitedcapi.slice_unpack
91+
self.assertEqual(unpack(slice(1, 7)), (1, 7, 1))
92+
self.assertEqual(unpack(slice(1, 7, 2)), (1, 7, 2))
93+
self.assertEqual(unpack(slice(7, 1, -2)), (7, 1, -2))
94+
self.assertEqual(unpack(slice(None, 7, 2)), (0, 7, 2))
95+
self.assertEqual(unpack(slice(None, 7, -2)), (SSIZE_MAX, 7, -2))
96+
self.assertEqual(unpack(slice(1, None, 2)), (1, SSIZE_MAX, 2))
97+
self.assertEqual(unpack(slice(1, None, -2)), (1, SSIZE_MIN, -2))
98+
self.assertEqual(unpack(slice(None)), (0, SSIZE_MAX, 1))
99+
self.assertEqual(unpack(slice(None, None, -1)),
100+
(SSIZE_MAX, SSIZE_MIN, -1))
101+
# Negative indices are not adjusted.
102+
self.assertEqual(unpack(slice(-3, -1)), (-3, -1, 1))
103+
self.assertEqual(unpack(slice(Index(1), Index(7), Index(2))),
104+
(1, 7, 2))
105+
106+
# Values which do not fit in Py_ssize_t are silently clipped.
107+
self.assertEqual(unpack(slice(1, 2**1000)), (1, SSIZE_MAX, 1))
108+
self.assertEqual(unpack(slice(1, -2**1000)), (1, SSIZE_MIN, 1))
109+
self.assertEqual(unpack(slice(2**1000, 7)), (SSIZE_MAX, 7, 1))
110+
self.assertEqual(unpack(slice(-2**1000, 7)), (SSIZE_MIN, 7, 1))
111+
self.assertEqual(unpack(slice(1, 7, 2**1000)), (1, 7, SSIZE_MAX))
112+
# The step is boosted to -PY_SSIZE_T_MAX, not PY_SSIZE_T_MIN, so
113+
# that negating it is safe.
114+
self.assertEqual(unpack(slice(7, 1, -2**1000)), (7, 1, -SSIZE_MAX))
115+
self.assertEqual(unpack(slice(7, 1, SSIZE_MIN)), (7, 1, -SSIZE_MAX))
116+
117+
with self.assertRaisesRegex(ValueError, 'slice step cannot be zero'):
118+
unpack(slice(1, 1, 0))
119+
with self.assertRaisesRegex(TypeError,
120+
'slice indices must be integers'):
121+
unpack(slice('a', 7))
122+
with self.assertRaisesRegex(TypeError,
123+
'slice indices must be integers'):
124+
unpack(slice(1, 'a'))
125+
with self.assertRaisesRegex(TypeError,
126+
'slice indices must be integers'):
127+
unpack(slice(1, 7, 'a'))
128+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
129+
unpack(slice(BadIndex(), 7))
130+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
131+
unpack(slice(1, BadIndex()))
132+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
133+
unpack(slice(1, 7, BadIndex()))
134+
135+
# CRASHES unpack(NULL)
136+
# CRASHES unpack(object())
137+
138+
def test_adjustindices(self):
139+
# Test PySlice_AdjustIndices()
140+
adjust = _testlimitedcapi.slice_adjustindices
141+
self.assertEqual(adjust(10, 1, 7, 1), (6, 1, 7))
142+
self.assertEqual(adjust(10, 1, 7, 2), (3, 1, 7))
143+
self.assertEqual(adjust(10, 7, 1, -1), (6, 7, 1))
144+
self.assertEqual(adjust(10, 7, 1, -2), (3, 7, 1))
145+
# An empty slice keeps the adjusted indices.
146+
self.assertEqual(adjust(10, 7, 1, 1), (0, 7, 1))
147+
self.assertEqual(adjust(10, 1, 7, -1), (0, 1, 7))
148+
149+
# Negative indices are added to the length.
150+
self.assertEqual(adjust(10, -9, -3, 1), (6, 1, 7))
151+
self.assertEqual(adjust(10, -3, -9, -1), (6, 7, 1))
152+
153+
# Out of bounds indices are clipped.
154+
self.assertEqual(adjust(10, -100, 100, 1), (10, 0, 10))
155+
self.assertEqual(adjust(10, 100, -100, -1), (10, 9, -1))
156+
self.assertEqual(adjust(10, SSIZE_MIN, SSIZE_MAX, 1), (10, 0, 10))
157+
self.assertEqual(adjust(10, SSIZE_MAX, SSIZE_MIN, -1), (10, 9, -1))
158+
self.assertEqual(adjust(0, 1, 7, 1), (0, 0, 0))
159+
self.assertEqual(adjust(0, 7, 1, -1), (0, -1, -1))
160+
161+
# The returned length is the length of the corresponding range.
162+
for length in LENGTHS:
163+
for start in VALUES[1:]:
164+
for stop in VALUES[1:]:
165+
for step in STEPS[1:]:
166+
with self.subTest(length=length, start=start,
167+
stop=stop, step=step):
168+
slicelength, start2, stop2 = adjust(length, start,
169+
stop, step)
170+
self.assertEqual(slicelength,
171+
len(range(start2, stop2, step)))
172+
173+
# Negative length is not supported, but does not fail.
174+
self.assertEqual(adjust(-3, 1, 7, 1), (0, -3, -3))
175+
self.assertEqual(adjust(-3, 7, 1, -1), (0, -4, -4))
176+
self.assertEqual(adjust(-3, -10, -5, 1), (0, 0, 0))
177+
178+
# The step is asserted to be neither zero nor less than
179+
# -PY_SSIZE_T_MAX.
180+
# CRASHES adjust(10, 0, 10, 0)
181+
# CRASHES adjust(10, 0, 10, SSIZE_MIN)
182+
183+
184+
class GetIndicesExMacroTest(unittest.TestCase):
185+
# PySlice_GetIndicesEx() is a macro using PySlice_Unpack() and
186+
# PySlice_AdjustIndices(). It is also a deprecated function, exported
187+
# for the stable ABI.
188+
getindicesex = staticmethod(_testlimitedcapi.slice_getindicesex_macro)
189+
getindicesex_seq = staticmethod(
190+
_testlimitedcapi.slice_getindicesex_seq_macro)
191+
# The macro evaluates the length after calling PySlice_Unpack(), so the
192+
# size of the list after removing an item is used.
193+
resized = (6, 8, 1, 2)
194+
195+
def test_getindicesex(self):
196+
# Test PySlice_GetIndicesEx()
197+
getindicesex = self.getindicesex
198+
self.assertEqual(getindicesex(slice(1, 7, 2), 10), (1, 7, 2, 3))
199+
self.assertEqual(getindicesex(slice(7, 1, -2), 10), (7, 1, -2, 3))
200+
self.assertEqual(getindicesex(slice(Index(1), Index(7), Index(2)), 10),
201+
(1, 7, 2, 3))
202+
203+
# The result agrees with slice.indices() and the slice length is
204+
# the length of the corresponding range.
205+
for length in LENGTHS:
206+
for start in VALUES:
207+
for stop in VALUES:
208+
for step in STEPS:
209+
s = slice(start, stop, step)
210+
with self.subTest(slice=s, length=length):
211+
indices = s.indices(length)
212+
self.assertEqual(getindicesex(s, length),
213+
indices + (len(range(*indices)),))
214+
215+
# Negative indices are added to the length.
216+
self.assertEqual(getindicesex(slice(-9, -3), 10), (1, 7, 1, 6))
217+
self.assertEqual(getindicesex(slice(-3, -9, -1), 10), (7, 1, -1, 6))
218+
219+
# Out of bounds indices are clipped.
220+
self.assertEqual(getindicesex(slice(-100, 100), 10), (0, 10, 1, 10))
221+
self.assertEqual(getindicesex(slice(100, -100, -1), 10),
222+
(9, -1, -1, 10))
223+
self.assertEqual(getindicesex(slice(None), 0), (0, 0, 1, 0))
224+
self.assertEqual(getindicesex(slice(1, 7, 2), 0), (0, 0, 2, 0))
225+
self.assertEqual(getindicesex(slice(None, None, -1), 0),
226+
(-1, -1, -1, 0))
227+
228+
# Indices which do not fit in Py_ssize_t are clipped, not rejected.
229+
# Note that slice.indices() does not clip the step.
230+
self.assertEqual(getindicesex(slice(1, 2**1000), 10), (1, 10, 1, 9))
231+
self.assertEqual(getindicesex(slice(2**1000, 7), 10), (10, 7, 1, 0))
232+
self.assertEqual(getindicesex(slice(1, 7, 2**1000), 10),
233+
(1, 7, SSIZE_MAX, 1))
234+
# -PY_SSIZE_T_MAX-1 is replaced with -PY_SSIZE_T_MAX.
235+
self.assertEqual(getindicesex(slice(7, 1, -2**1000), 10),
236+
(7, 1, -SSIZE_MAX, 1))
237+
self.assertEqual(getindicesex(slice(7, 1, SSIZE_MIN), 10),
238+
(7, 1, -SSIZE_MAX, 1))
239+
240+
with self.assertRaisesRegex(ValueError, 'slice step cannot be zero'):
241+
getindicesex(slice(1, 7, 0), 10)
242+
with self.assertRaisesRegex(TypeError,
243+
'slice indices must be integers'):
244+
getindicesex(slice('a', 7), 10)
245+
with self.assertRaisesRegex(TypeError,
246+
'slice indices must be integers'):
247+
getindicesex(slice(1, 'a'), 10)
248+
with self.assertRaisesRegex(TypeError,
249+
'slice indices must be integers'):
250+
getindicesex(slice(1, 7, 'a'), 10)
251+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
252+
getindicesex(slice(BadIndex(), 7), 10)
253+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
254+
getindicesex(slice(1, BadIndex()), 10)
255+
with self.assertRaisesRegex(RuntimeError, 'bad index'):
256+
getindicesex(slice(1, 7, BadIndex()), 10)
257+
258+
# Negative length is not supported, but does not fail.
259+
self.assertEqual(getindicesex(slice(None), -3), (-3, -3, 1, 0))
260+
self.assertEqual(getindicesex(slice(1, 7, 2), -3), (-3, -3, 2, 0))
261+
self.assertEqual(getindicesex(slice(7, 1, -2), -3), (-4, -4, -2, 0))
262+
263+
# CRASHES getindicesex(NULL, 10)
264+
# CRASHES getindicesex(object(), 10)
265+
266+
def test_getindicesex_seq(self):
267+
# The length is the size of a sequence.
268+
getindicesex_seq = self.getindicesex_seq
269+
seq = list(range(10))
270+
self.assertEqual(getindicesex_seq(slice(-3, -1), seq), (7, 9, 1, 2))
271+
self.assertEqual(getindicesex_seq(slice(-3, -1), []), (0, 0, 1, 0))
272+
273+
# gh-72054: __index__() can resize the sequence. Negative indices
274+
# are adjusted by the length, so the result depends on when it is
275+
# evaluated.
276+
seq = list(range(10))
277+
self.assertEqual(getindicesex_seq(slice(PopIndex(-3, seq), -1), seq),
278+
self.resized)
279+
self.assertEqual(len(seq), 9, seq)
280+
281+
seq = list(range(10))
282+
self.assertEqual(getindicesex_seq(slice(-3, PopIndex(-1, seq)), seq),
283+
self.resized)
284+
self.assertEqual(len(seq), 9, seq)
285+
286+
seq = list(range(10))
287+
self.assertEqual(
288+
getindicesex_seq(slice(-3, -1, PopIndex(1, seq)), seq),
289+
self.resized)
290+
self.assertEqual(len(seq), 9, seq)
291+
292+
# CRASHES getindicesex_seq(slice(None), NULL)
293+
# CRASHES getindicesex_seq(slice(None), object())
294+
295+
296+
class GetIndicesExFuncTest(GetIndicesExMacroTest):
297+
# The deprecated function is equivalent to the macro, except that the
298+
# length is evaluated before the call.
299+
getindicesex = staticmethod(_testlimitedcapi.slice_getindicesex_func)
300+
getindicesex_seq = staticmethod(
301+
_testlimitedcapi.slice_getindicesex_seq_func)
302+
# The size of the list before removing an item is used.
303+
resized = (7, 9, 1, 2)
304+
305+
306+
if __name__ == "__main__":
307+
unittest.main()

‎Modules/Setup.stdlib.in‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@
174174
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
175175
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tokenizer.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c
176176
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c
177-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c
177+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slice.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c
178178
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
179179
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
180180

‎Modules/_testlimitedcapi.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ module_exec(PyObject *mod)
5858
if (_PyTestLimitedCAPI_Init_Set(mod) < 0) {
5959
return -1;
6060
}
61+
if (_PyTestLimitedCAPI_Init_Slice(mod) < 0) {
62+
return -1;
63+
}
6164
if (_PyTestLimitedCAPI_Init_Slots(mod) < 0) {
6265
return -1;
6366
}

‎Modules/_testlimitedcapi/parts.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ int _PyTestLimitedCAPI_Init_List(PyObject *module);
4040
int _PyTestLimitedCAPI_Init_Long(PyObject *module);
4141
int _PyTestLimitedCAPI_Init_PyOS(PyObject *module);
4242
int _PyTestLimitedCAPI_Init_Set(PyObject *module);
43+
int _PyTestLimitedCAPI_Init_Slice(PyObject *module);
4344
int _PyTestLimitedCAPI_Init_Slots(PyObject *module);
4445
int _PyTestLimitedCAPI_Init_Sys(PyObject *module);
4546
int _PyTestLimitedCAPI_Init_ThreadState(PyObject *module);

0 commit comments

Comments
 (0)