Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/c-api/slice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Slice Objects
length *length*, and store the length of the slice in *slicelength*. Out
of bounds indices are clipped in a manner consistent with the handling of
normal slices.
*length* must not be negative.

Return ``0`` on success and ``-1`` on error with an exception set.

Expand Down Expand Up @@ -108,6 +109,10 @@ Slice Objects
Out of bounds indices are clipped in a manner consistent with the handling
of normal slices.

*length* must not be negative.
*step* must not be zero and must not be less than ``-PY_SSIZE_T_MAX``,
as guaranteed by :c:func:`PySlice_Unpack`.

Return the length of the slice. Always successful. Doesn't call Python
code.

Expand Down
307 changes: 307 additions & 0 deletions Lib/test/test_capi/test_slice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import sys
import unittest
from test.support import import_helper

_testlimitedcapi = import_helper.import_module('_testlimitedcapi')

NULL = None
SSIZE_MAX = sys.maxsize
SSIZE_MIN = -sys.maxsize - 1

VALUES = [None, 0, 1, 3, 7, -1, -3, -7]
STEPS = [None, 1, 3, 5, -1, -3, -5]
LENGTHS = [0, 1, 3, 10]


class Index:
def __init__(self, value):
self.value = value

def __index__(self):
return self.value


class BadIndex:
def __index__(self):
raise RuntimeError('bad index')


class PopIndex:
# __index__() removes the last item of the list.
def __init__(self, value, seq):
self.value = value
self.seq = seq

def __index__(self):
self.seq.pop()
return self.value


class SliceTest(unittest.TestCase):

def test_check(self):
# Test PySlice_Check()
check = _testlimitedcapi.slice_check
self.assertTrue(check(slice(1, 7, 2)))
self.assertFalse(check(object()))

# CRASHES check(NULL)

def test_new(self):
# Test PySlice_New()
new = _testlimitedcapi.slice_new
self.assertEqual(new(1, 7, 2), slice(1, 7, 2))
self.assertEqual(new(7, 1, -2), slice(7, 1, -2))
self.assertEqual(new('a', 'b', 'c'), slice('a', 'b', 'c'))
self.assertEqual(new(NULL, NULL, NULL), slice(None, None, None))

def test_getindices(self):
# Test PySlice_GetIndices()
getindices = _testlimitedcapi.slice_getindices
self.assertEqual(getindices(slice(1, 7, 2), 10), (1, 7, 2))
self.assertEqual(getindices(slice(None), 10), (0, 10, 1))
self.assertEqual(getindices(slice(None, None, -1), 10), (9, -1, -1))
self.assertEqual(getindices(slice(-3, -1), 10), (7, 9, 1))
self.assertEqual(getindices(slice(-1, -3, -1), 10), (9, 7, -1))
Comment thread
serhiy-storchaka marked this conversation as resolved.
self.assertEqual(getindices(slice(None, None, -2), 0), (-1, -1, -2))
self.assertEqual(getindices(slice(-3, -5, 1), 0), (-3, -5, 1))

# It fails without setting an exception for out of bounds indices,
# a zero step and non-integer indices.
self.assertIsNone(getindices(slice(1, 11), 10))
self.assertIsNone(getindices(slice(10, 1), 10))
self.assertIsNone(getindices(slice(1, 7, 0), 10))
self.assertIsNone(getindices(slice(Index(1)), 10))
self.assertIsNone(getindices(slice('a'), 10))
self.assertIsNone(getindices(slice(1, 'a'), 10))
self.assertIsNone(getindices(slice(1, 7, 'a'), 10))
Comment thread
serhiy-storchaka marked this conversation as resolved.

# Negative length is not supported, but does not fail.
self.assertIsNone(getindices(slice(None), -3))
self.assertIsNone(getindices(slice(1, 7, 2), -3))
self.assertEqual(getindices(slice(-10, -5, 1), -3), (-13, -8, 1))
self.assertEqual(getindices(slice(-5, -10, -2), -3), (-8, -13, -2))

# CRASHES getindices(NULL, 10)
# CRASHES getindices(object(), 10)

def test_unpack(self):
# Test PySlice_Unpack()
unpack = _testlimitedcapi.slice_unpack
self.assertEqual(unpack(slice(1, 7)), (1, 7, 1))
self.assertEqual(unpack(slice(1, 7, 2)), (1, 7, 2))
self.assertEqual(unpack(slice(7, 1, -2)), (7, 1, -2))
self.assertEqual(unpack(slice(None, 7, 2)), (0, 7, 2))
self.assertEqual(unpack(slice(None, 7, -2)), (SSIZE_MAX, 7, -2))
self.assertEqual(unpack(slice(1, None, 2)), (1, SSIZE_MAX, 2))
self.assertEqual(unpack(slice(1, None, -2)), (1, SSIZE_MIN, -2))
self.assertEqual(unpack(slice(None)), (0, SSIZE_MAX, 1))
self.assertEqual(unpack(slice(None, None, -1)),
(SSIZE_MAX, SSIZE_MIN, -1))
# Negative indices are not adjusted.
self.assertEqual(unpack(slice(-3, -1)), (-3, -1, 1))
self.assertEqual(unpack(slice(Index(1), Index(7), Index(2))),
(1, 7, 2))

# Values which do not fit in Py_ssize_t are silently clipped.
self.assertEqual(unpack(slice(1, 2**1000)), (1, SSIZE_MAX, 1))
self.assertEqual(unpack(slice(1, -2**1000)), (1, SSIZE_MIN, 1))
self.assertEqual(unpack(slice(2**1000, 7)), (SSIZE_MAX, 7, 1))
self.assertEqual(unpack(slice(-2**1000, 7)), (SSIZE_MIN, 7, 1))
self.assertEqual(unpack(slice(1, 7, 2**1000)), (1, 7, SSIZE_MAX))
# The step is boosted to -PY_SSIZE_T_MAX, not PY_SSIZE_T_MIN, so
# that negating it is safe.
self.assertEqual(unpack(slice(7, 1, -2**1000)), (7, 1, -SSIZE_MAX))
self.assertEqual(unpack(slice(7, 1, SSIZE_MIN)), (7, 1, -SSIZE_MAX))

with self.assertRaisesRegex(ValueError, 'slice step cannot be zero'):
unpack(slice(1, 1, 0))
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
unpack(slice('a', 7))
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
unpack(slice(1, 'a'))
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
unpack(slice(1, 7, 'a'))
with self.assertRaisesRegex(RuntimeError, 'bad index'):
unpack(slice(BadIndex(), 7))
with self.assertRaisesRegex(RuntimeError, 'bad index'):
unpack(slice(1, BadIndex()))
with self.assertRaisesRegex(RuntimeError, 'bad index'):
unpack(slice(1, 7, BadIndex()))

# CRASHES unpack(NULL)
# CRASHES unpack(object())

def test_adjustindices(self):
# Test PySlice_AdjustIndices()
adjust = _testlimitedcapi.slice_adjustindices
self.assertEqual(adjust(10, 1, 7, 1), (6, 1, 7))
self.assertEqual(adjust(10, 1, 7, 2), (3, 1, 7))
self.assertEqual(adjust(10, 7, 1, -1), (6, 7, 1))
self.assertEqual(adjust(10, 7, 1, -2), (3, 7, 1))
# An empty slice keeps the adjusted indices.
self.assertEqual(adjust(10, 7, 1, 1), (0, 7, 1))
self.assertEqual(adjust(10, 1, 7, -1), (0, 1, 7))

# Negative indices are added to the length.
self.assertEqual(adjust(10, -9, -3, 1), (6, 1, 7))
self.assertEqual(adjust(10, -3, -9, -1), (6, 7, 1))

# Out of bounds indices are clipped.
self.assertEqual(adjust(10, -100, 100, 1), (10, 0, 10))
self.assertEqual(adjust(10, 100, -100, -1), (10, 9, -1))
self.assertEqual(adjust(10, SSIZE_MIN, SSIZE_MAX, 1), (10, 0, 10))
self.assertEqual(adjust(10, SSIZE_MAX, SSIZE_MIN, -1), (10, 9, -1))
self.assertEqual(adjust(0, 1, 7, 1), (0, 0, 0))
self.assertEqual(adjust(0, 7, 1, -1), (0, -1, -1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add tests on negative length?


# The returned length is the length of the corresponding range.
for length in LENGTHS:
for start in VALUES[1:]:
for stop in VALUES[1:]:
for step in STEPS[1:]:
with self.subTest(length=length, start=start,
stop=stop, step=step):
slicelength, start2, stop2 = adjust(length, start,
stop, step)
self.assertEqual(slicelength,
len(range(start2, stop2, step)))

# Negative length is not supported, but does not fail.
self.assertEqual(adjust(-3, 1, 7, 1), (0, -3, -3))
self.assertEqual(adjust(-3, 7, 1, -1), (0, -4, -4))
self.assertEqual(adjust(-3, -10, -5, 1), (0, 0, 0))

# The step is asserted to be neither zero nor less than
# -PY_SSIZE_T_MAX.
# CRASHES adjust(10, 0, 10, 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that the function cannot fail. Can you update PySlice_AdjustIndices() document to mention that step must not be equal to zero and must not be equal to PY_SSIZE_T_MIN?

@serhiy-storchaka serhiy-storchaka Sep 25, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the docs if they already do not mentioned this.

# CRASHES adjust(10, 0, 10, SSIZE_MIN)


class GetIndicesExMacroTest(unittest.TestCase):
# PySlice_GetIndicesEx() is a macro using PySlice_Unpack() and
# PySlice_AdjustIndices(). It is also a deprecated function, exported
# for the stable ABI.
getindicesex = staticmethod(_testlimitedcapi.slice_getindicesex_macro)
getindicesex_seq = staticmethod(
_testlimitedcapi.slice_getindicesex_seq_macro)
# The macro evaluates the length after calling PySlice_Unpack(), so the
# size of the list after removing an item is used.
resized = (6, 8, 1, 2)

def test_getindicesex(self):
# Test PySlice_GetIndicesEx()
getindicesex = self.getindicesex
self.assertEqual(getindicesex(slice(1, 7, 2), 10), (1, 7, 2, 3))
self.assertEqual(getindicesex(slice(7, 1, -2), 10), (7, 1, -2, 3))
self.assertEqual(getindicesex(slice(Index(1), Index(7), Index(2)), 10),
(1, 7, 2, 3))

# The result agrees with slice.indices() and the slice length is
# the length of the corresponding range.
for length in LENGTHS:
for start in VALUES:
for stop in VALUES:
for step in STEPS:
s = slice(start, stop, step)
with self.subTest(slice=s, length=length):
indices = s.indices(length)
self.assertEqual(getindicesex(s, length),
indices + (len(range(*indices)),))

# Negative indices are added to the length.
self.assertEqual(getindicesex(slice(-9, -3), 10), (1, 7, 1, 6))
self.assertEqual(getindicesex(slice(-3, -9, -1), 10), (7, 1, -1, 6))

# Out of bounds indices are clipped.
self.assertEqual(getindicesex(slice(-100, 100), 10), (0, 10, 1, 10))
self.assertEqual(getindicesex(slice(100, -100, -1), 10),
(9, -1, -1, 10))
self.assertEqual(getindicesex(slice(None), 0), (0, 0, 1, 0))
self.assertEqual(getindicesex(slice(1, 7, 2), 0), (0, 0, 2, 0))
self.assertEqual(getindicesex(slice(None, None, -1), 0),
(-1, -1, -1, 0))

# Indices which do not fit in Py_ssize_t are clipped, not rejected.
# Note that slice.indices() does not clip the step.
self.assertEqual(getindicesex(slice(1, 2**1000), 10), (1, 10, 1, 9))
self.assertEqual(getindicesex(slice(2**1000, 7), 10), (10, 7, 1, 0))
self.assertEqual(getindicesex(slice(1, 7, 2**1000), 10),
(1, 7, SSIZE_MAX, 1))
# -PY_SSIZE_T_MAX-1 is replaced with -PY_SSIZE_T_MAX.
self.assertEqual(getindicesex(slice(7, 1, -2**1000), 10),
(7, 1, -SSIZE_MAX, 1))
self.assertEqual(getindicesex(slice(7, 1, SSIZE_MIN), 10),
(7, 1, -SSIZE_MAX, 1))

with self.assertRaisesRegex(ValueError, 'slice step cannot be zero'):
getindicesex(slice(1, 7, 0), 10)
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
getindicesex(slice('a', 7), 10)
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
getindicesex(slice(1, 'a'), 10)
with self.assertRaisesRegex(TypeError,
'slice indices must be integers'):
getindicesex(slice(1, 7, 'a'), 10)
with self.assertRaisesRegex(RuntimeError, 'bad index'):
getindicesex(slice(BadIndex(), 7), 10)
with self.assertRaisesRegex(RuntimeError, 'bad index'):
getindicesex(slice(1, BadIndex()), 10)
with self.assertRaisesRegex(RuntimeError, 'bad index'):
getindicesex(slice(1, 7, BadIndex()), 10)

# Negative length is not supported, but does not fail.
self.assertEqual(getindicesex(slice(None), -3), (-3, -3, 1, 0))
self.assertEqual(getindicesex(slice(1, 7, 2), -3), (-3, -3, 2, 0))
self.assertEqual(getindicesex(slice(7, 1, -2), -3), (-4, -4, -2, 0))

# CRASHES getindicesex(NULL, 10)
# CRASHES getindicesex(object(), 10)

def test_getindicesex_seq(self):
# The length is the size of a sequence.
getindicesex_seq = self.getindicesex_seq
seq = list(range(10))
self.assertEqual(getindicesex_seq(slice(-3, -1), seq), (7, 9, 1, 2))
self.assertEqual(getindicesex_seq(slice(-3, -1), []), (0, 0, 1, 0))

# gh-72054: __index__() can resize the sequence. Negative indices
# are adjusted by the length, so the result depends on when it is
# evaluated.
seq = list(range(10))
self.assertEqual(getindicesex_seq(slice(PopIndex(-3, seq), -1), seq),
self.resized)
self.assertEqual(len(seq), 9, seq)

seq = list(range(10))
self.assertEqual(getindicesex_seq(slice(-3, PopIndex(-1, seq)), seq),
self.resized)
self.assertEqual(len(seq), 9, seq)

seq = list(range(10))
self.assertEqual(
getindicesex_seq(slice(-3, -1, PopIndex(1, seq)), seq),
self.resized)
self.assertEqual(len(seq), 9, seq)

# CRASHES getindicesex_seq(slice(None), NULL)
# CRASHES getindicesex_seq(slice(None), object())


class GetIndicesExFuncTest(GetIndicesExMacroTest):
# The deprecated function is equivalent to the macro, except that the
# length is evaluated before the call.
getindicesex = staticmethod(_testlimitedcapi.slice_getindicesex_func)
getindicesex_seq = staticmethod(
_testlimitedcapi.slice_getindicesex_seq_func)
# The size of the list before removing an item is used.
resized = (7, 9, 1, 2)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@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
@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
@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
@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
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
3 changes: 3 additions & 0 deletions Modules/_testlimitedcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ module_exec(PyObject *mod)
if (_PyTestLimitedCAPI_Init_Set(mod) < 0) {
return -1;
}
if (_PyTestLimitedCAPI_Init_Slice(mod) < 0) {
return -1;
}
if (_PyTestLimitedCAPI_Init_Slots(mod) < 0) {
return -1;
}
Expand Down
1 change: 1 addition & 0 deletions Modules/_testlimitedcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ int _PyTestLimitedCAPI_Init_List(PyObject *module);
int _PyTestLimitedCAPI_Init_Long(PyObject *module);
int _PyTestLimitedCAPI_Init_PyOS(PyObject *module);
int _PyTestLimitedCAPI_Init_Set(PyObject *module);
int _PyTestLimitedCAPI_Init_Slice(PyObject *module);
int _PyTestLimitedCAPI_Init_Slots(PyObject *module);
int _PyTestLimitedCAPI_Init_Sys(PyObject *module);
int _PyTestLimitedCAPI_Init_ThreadState(PyObject *module);
Expand Down
Loading
Loading