Files
RustPython/extra_tests/snippets/protocol_index_argument.py
Jeongseop Lim 95686c08e4 Accept __index__ for the remaining int-bound arguments (#8999)
#8828 made bin(), chr() and several other arguments that were bound
as PyIntRef go through __index__, but these still downcast to int and
reject an object that only defines __index__: the start and end of the
str/bytes/bytearray find family, enumerate(start=),
itertools.combinations/combinations_with_replacement/batched,
_thread.stack_size, resource.getrlimit/setrlimit, pwd.getpwuid,
grp.getgrgid, fcntl.fcntl/lockf and mmap.move/madvise. CPython converts
all of them with _PyNumber_Index or _PyEval_SliceIndex.

Bind them as ArgIndex, as #8828 did for the others.

test_mmap.test_madvise and test_mmap_closed_by_int_scenarios now pass,
so drop their expectedFailure markers.

Assisted-by: Claude Code:claude-opus-5-5
2026-10-07 23:32:59 +09:00

377 lines
9.9 KiB
Python

"""An object with `__index__` is accepted where a method takes an index."""
import sys
from testutils import assert_raises
class Index:
def __init__(self, value=1):
self.value = value
def __index__(self):
return self.value
l = [1, 2]
l.insert(Index(), 9)
assert l == [1, 9, 2]
assert "ab".rjust(Index(3)) == " ab"
assert "ab".ljust(Index(3)) == "ab "
assert "ab".center(Index(4)) == " ab "
assert "ab".zfill(Index(3)) == "0ab"
assert "a\tb".expandtabs(Index(2)) == "a b"
assert "a,b,c".split(",", Index()) == ["a", "b,c"]
assert b"a\tb".expandtabs(Index(2)) == b"a b"
assert bytearray(b"a\tb").expandtabs(Index(2)) == bytearray(b"a b")
assert (1).to_bytes(Index(2), "big") == b"\x00\x01"
# A subclass of int still works.
class MyInt(int):
pass
assert "ab".rjust(MyInt(3)) == " ab"
# Anything without `__index__` is still rejected, with CPython's message.
def message(f):
try:
f()
except TypeError as e:
return str(e)
raise AssertionError("expected a TypeError")
assert message(lambda: [].insert("a", 1)) == (
"'str' object cannot be interpreted as an integer"
)
assert message(lambda: "ab".rjust(1.0)) == (
"'float' object cannot be interpreted as an integer"
)
# An `__index__` that does not return an int is an error, not a silent cast.
class NotAnInt:
def __index__(self):
return "1"
with assert_raises(TypeError):
"ab".rjust(NotAnInt())
class Raises:
def __index__(self):
raise ValueError("boom")
with assert_raises(ValueError):
"ab".rjust(Raises())
# The conversion is per-argument, so a pointer or handle argument is untouched:
# ctypes dereferences the value, and CPython does not consult `__index__` there
# either. The message differs from CPython's; the refusal is the point.
try:
import ctypes
except ImportError:
ctypes = None
if ctypes is not None:
class Address:
def __index__(self):
raise AssertionError("__index__ must not be called for an address")
with assert_raises(TypeError):
ctypes.c_int.from_address(Address())
# The conversion happens while the argument is bound, so an out-of-range
# __index__ is reported before a later argument is looked at. CPython raises
# OverflowError for each of these, not the error the later argument would give.
class Big:
def __index__(self):
return 2**200
class NotBool:
def __bool__(self):
raise AssertionError("signed was converted before length")
assert_raises(OverflowError, lambda: "a b".split("", Big()))
assert_raises(OverflowError, lambda: (5).to_bytes(Big(), "bogus"))
assert_raises(OverflowError, lambda: (5).to_bytes(Big(), "big", signed=NotBool()))
assert_raises(OverflowError, lambda: "a".center(Big(), 5))
# A length past isize::MAX is an OverflowError, as CPython's Py_ssize_t
# converter gives, rather than a failed allocation further in.
assert_raises(OverflowError, lambda: (5).to_bytes(Index(2**63), "big"))
assert_raises(ValueError, lambda: (5).to_bytes(Index(-1), "big"))
# An omitted optional index argument takes its default, but an explicit None
# is not an index and stays a TypeError.
assert "a b c".split(" ") == ["a", "b", "c"]
assert "a\tb".expandtabs() == "a b"
assert (5).to_bytes() == b"\x05"
assert_raises(TypeError, lambda: "a b c".split(" ", None))
assert_raises(TypeError, lambda: "a\tb".expandtabs(None))
assert_raises(TypeError, lambda: b"a\tb".expandtabs(None))
assert_raises(TypeError, lambda: (5).to_bytes(None, "big"))
# `sys.maxsize` is a legitimate maxsplit; widening before the +1 keeps a debug
# build from overflowing on it.
assert "a b c".split(" ", sys.maxsize) == ["a", "b", "c"]
assert "a b c".rsplit(" ", sys.maxsize) == ["a", "b", "c"]
assert "a b c".split(" ", Index(sys.maxsize)) == ["a", "b", "c"]
assert b"a b".split(b" ", sys.maxsize) == [b"a", b"b"]
# Primitive integer arguments accept `__index__`.
assert chr(Index(65)) == chr(65)
assert bin(Index(3)) == bin(3)
assert [1, 2, 3].pop(Index(0)) == 1
assert b"ab".center(Index(4)) == b"ab".center(4)
assert (1, 2, 3).index(2, Index(1)) == 1
import math
assert math.factorial(Index(5)) == math.factorial(5)
import array
arr = array.array("L", [1, 2, 3])
assert arr.pop(Index(0)) == 1
arr = array.array("L")
arr.append(Index(4))
assert list(arr) == [4]
import os
assert os.strerror(Index(0)) == os.strerror(0)
if hasattr(os, "major"):
assert os.major(Index(0x3000000)) == os.major(0x3000000)
try:
os.read(Index(-1), Index(1))
except OSError:
pass
else:
raise AssertionError(
"os.read should fail on a bad descriptor, not accept it silently"
)
import time
assert time.localtime(Index(0)).tm_year == time.localtime(0).tm_year
import socket
assert socket.htons(Index(1)) == socket.htons(1)
import zlib
assert zlib.crc32(b"a", Index(0)) == zlib.crc32(b"a", 0)
import io
bio = io.BytesIO(b"abcdef")
assert bio.seek(Index(2)) == bio.seek(2)
old_limit = sys.getrecursionlimit()
try:
sys.setrecursionlimit(Index(old_limit))
assert sys.getrecursionlimit() == old_limit
finally:
sys.setrecursionlimit(old_limit)
import signal
if hasattr(signal, "alarm"):
signal.alarm(Index(0))
import select
# Windows select() refuses three empty lists.
if sys.platform != "win32":
assert select.select([], [], [], Index(0)) == ([], [], [])
class BoomIndex:
def __index__(self):
raise KeyError("boom")
if hasattr(select, "poll"):
poller = select.poll()
poller.register(0, Index(select.POLLIN))
poller.unregister(0)
assert message(lambda: poller.register(0, 1.0)) == (
"'float' object cannot be interpreted as an integer"
)
assert message(lambda: poller.register(0, "x")) == (
"'str' object cannot be interpreted as an integer"
)
with assert_raises(KeyError):
poller.register(0, BoomIndex())
# An int subclass is used directly; its `__index__` is not called.
class OverrideIndex(int):
def __index__(self):
raise AssertionError("__index__ must not be called on an int subclass")
assert chr(OverrideIndex(66)) == "B"
assert bin(OverrideIndex(4)) == "0b100"
assert [9].pop(OverrideIndex(0)) == 9
assert math.factorial(OverrideIndex(3)) == 6
# A float is still not an integer.
assert message(lambda: chr(1.0)) == "'float' object cannot be interpreted as an integer"
assert (
message(lambda: [1].pop(1.0))
== "'float' object cannot be interpreted as an integer"
)
# Sites that check for an int themselves keep refusing `__index__`.
assert message(lambda: math.ldexp(1.0, Index(2))) == (
"Expected an int as second argument to ldexp."
)
import collections
with assert_raises(TypeError):
collections.deque(maxlen=Index(2))
class HashIndex:
def __hash__(self):
return Index(1)
assert message(lambda: hash(HashIndex())) == "__hash__ method should return an integer"
import _thread
assert (
message(lambda: _thread._make_thread_handle(Index(1))) == "ident must be an integer"
)
if ctypes is not None:
import _ctypes
with assert_raises(TypeError):
ctypes.Structure.from_address(Index(0))
with assert_raises(TypeError):
ctypes.Union.from_address(Index(0))
for name in (
"PyObj_FromPtr",
"dlsym",
"dlclose",
"call_function",
"call_cdeclfunction",
"FreeLibrary",
):
fn = getattr(_ctypes, name, None)
# Windows handle arguments are left out of this change.
if fn is None or (name == "FreeLibrary" and sys.platform == "win32"):
continue
with assert_raises(TypeError):
if name == "dlsym":
fn(Index(1), "x")
elif name in ("call_function", "call_cdeclfunction"):
fn(Index(1), ())
else:
fn(Index(1))
class LenIndex:
def __len__(self):
return Index(2)
assert len(LenIndex()) == 2
import operator
class HintIndex:
def __iter__(self):
return iter(())
def __length_hint__(self):
return Index(1)
with assert_raises(TypeError):
operator.length_hint(HintIndex())
# Arguments bound as `int` objects accept `__index__` as well.
assert "abcabc".find("a", Index(1)) == 3
assert "abcabc".rfind("a", 0, Index(3)) == 0
assert "abcabc".index("c", Index(3)) == 5
assert "abcabc".rindex("a", Index(1), Index(4)) == 3
assert "aaaaaa".count("a", Index(2), Index(-1)) == 3
assert "abcdef".startswith("d", Index(3))
assert "abcdef".endswith("c", 0, Index(3))
assert b"abcabc".find(b"a", Index(1)) == 3
assert b"abcabc".count(b"b", Index(2)) == 1
assert b"abcdef".startswith(b"d", Index(3))
assert bytearray(b"abcabc").rindex(b"a", 0, Index(3)) == 0
assert bytearray(b"abcdef").endswith(b"c", 0, Index(3))
assert list(enumerate("ab", Index(3))) == [(3, "a"), (4, "b")]
assert list(enumerate("ab", start=Index(3))) == [(3, "a"), (4, "b")]
import itertools
assert list(itertools.combinations("abc", Index(2))) == [
("a", "b"),
("a", "c"),
("b", "c"),
]
assert list(itertools.combinations_with_replacement("ab", Index(2))) == [
("a", "a"),
("a", "b"),
("b", "b"),
]
assert list(itertools.batched("abcde", Index(2))) == [("a", "b"), ("c", "d"), ("e",)]
assert _thread.stack_size(Index(0)) == 0
import mmap
mapped = mmap.mmap(-1, 6)
mapped.write(b"abcdef")
mapped.move(Index(0), Index(3), Index(3))
assert mapped[:] == b"defdef"
if hasattr(mapped, "madvise"):
mapped.madvise(mmap.MADV_NORMAL, Index(0), Index(6))
if sys.platform != "win32":
import fcntl
import grp
import pwd
import resource
import tempfile
assert pwd.getpwuid(Index(0)).pw_uid == 0
assert grp.getgrgid(Index(0)).gr_gid == 0
nofile = resource.getrlimit(resource.RLIMIT_NOFILE)
assert resource.getrlimit(Index(resource.RLIMIT_NOFILE)) == nofile
resource.setrlimit(Index(resource.RLIMIT_NOFILE), nofile)
with tempfile.TemporaryFile() as f:
assert fcntl.fcntl(f, fcntl.F_SETFD, Index(fcntl.FD_CLOEXEC)) == 0
assert fcntl.fcntl(f, fcntl.F_GETFD) == fcntl.FD_CLOEXEC
fcntl.lockf(f, fcntl.LOCK_SH | fcntl.LOCK_NB, Index(0), Index(0))