mirror of
https://github.com/python/cpython
synced 2026-11-06 09:02:40 +03:00
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()
(cherry picked from commit 8c056c33aa)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
307 lines
13 KiB
Python
307 lines
13 KiB
Python
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))
|
|
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))
|
|
|
|
# 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))
|
|
|
|
# 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)
|
|
# 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()
|