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
This commit is contained in:
Jeongseop Lim authored and GitHub committed 2026-10-07 23:32:59 +09:00
1 parent a39d6c9676
commit 95686c08e4
13 files changed
+132 -60

No files matched your search

-2
View File
@@ -897,7 +897,6 @@ class MmapTests(unittest.TestCase):
match = closed_mmap_repr_pat.match(repr(mm))
self.assertIsNotNone(match)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipUnless(hasattr(mmap.mmap, 'madvise'), 'needs madvise')
def test_madvise(self):
size = 2 * PAGESIZE
@@ -1039,7 +1038,6 @@ class MmapTests(unittest.TestCase):
self.assertEqual(m1[:data_length], data)
self.assertEqual(m2[:data_length], data)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mmap_closed_by_int_scenarios(self):
"""
gh-103987: Test that mmap objects raise ValueError
+7 -8
View File
@@ -8,9 +8,8 @@ mod fcntl {
use crate::vm::{
PyResult, VirtualMachine,
builtins::PyIntRef,
convert::ToPyException,
function::{ArgMemoryBuffer, ArgStrOrBytesLike, Either, OptionalArg},
function::{ArgIndex, ArgMemoryBuffer, ArgStrOrBytesLike, Either, OptionalArg},
stdlib::_io,
};
use rustpython_host_env::io::is_interrupted_error;
@@ -86,7 +85,7 @@ mod fcntl {
#[derive(FromArgs)]
struct FcntlArg {
#[pyarg(positional, default = 0)]
arg: Either<ArgStrOrBytesLike, PyIntRef>,
arg: Either<ArgStrOrBytesLike, ArgIndex>,
}
#[pyfunction]
@@ -114,7 +113,7 @@ mod fcntl {
)?;
return Ok(vm.ctx.new_bytes(buf[..arg_len].to_vec()).into());
}
Either::B(i) => i.as_u32_mask(),
Either::B(i) => i.as_ref().as_u32_mask(),
};
let ret = retry_on_eintr(
vm,
@@ -213,9 +212,9 @@ mod fcntl {
#[derive(FromArgs)]
struct LockfArgs {
#[pyarg(positional, default = 0)]
len: PyIntRef,
len: ArgIndex,
#[pyarg(positional, default = 0)]
start: PyIntRef,
start: ArgIndex,
#[pyarg(positional, default)]
whence: i32,
}
@@ -229,8 +228,8 @@ mod fcntl {
LockfArgs { len, start, whence }: LockfArgs,
vm: &VirtualMachine,
) -> PyResult {
let start = start.try_to_primitive(vm)?;
let len = len.try_to_primitive(vm)?;
let start = start.as_ref().try_to_primitive(vm)?;
let len = len.as_ref().try_to_primitive(vm)?;
// F_LOCK and F_TLOCK differ in exactly this: the first one waits.
let ret = retry_on_eintr(
vm,
+4 -3
View File
@@ -5,9 +5,10 @@ pub(crate) use grp::module_def;
mod grp {
use crate::vm::{
PyObjectRef, PyResult, VirtualMachine,
builtins::{PyIntRef, PyListRef, PyUtf8StrRef},
builtins::{PyListRef, PyUtf8StrRef},
convert::{IntoPyException, ToPyObject},
exceptions,
function::ArgIndex,
types::PyStructSequence,
};
use core::hint::cold_path;
@@ -44,12 +45,12 @@ mod grp {
#[derive(FromArgs)]
struct GetGrGidArgs {
#[pyarg(any)]
id: PyIntRef,
id: ArgIndex,
}
#[pyfunction]
fn getgrgid(id: GetGrGidArgs, vm: &VirtualMachine) -> PyResult<GroupData> {
let gid = id.id;
let gid = id.id.into_int_ref();
let gr_gid = gid.as_bigint();
let gid = host_grp::gid_t::try_from(gr_gid).ok();
let group = gid
+11 -9
View File
@@ -14,7 +14,7 @@ mod mmap {
builtins::{PyBytes, PyBytesRef, PyInt, PyIntRef, PyType, PyTypeRef},
byte::{bytes_from_object, value_from_object},
convert::ToPyException,
function::ArgBytesLike,
function::{ArgBytesLike, ArgIndex},
protocol::{
BufferDescriptor, BufferMethods, PyBuffer, PyMappingMethods, PySequenceMethods,
},
@@ -330,9 +330,9 @@ mod mmap {
option: core::ffi::c_int,
// None means 0.
#[pyarg(positional, default, py_default = "0")]
start: Option<PyIntRef>,
start: Option<ArgIndex>,
#[pyarg(positional, optional)]
length: Option<PyIntRef>,
length: Option<ArgIndex>,
}
#[cfg(all(unix, not(target_os = "redox")))]
@@ -345,7 +345,8 @@ mod mmap {
let start = self
.start
.map(|s| {
s.try_to_primitive::<usize>(vm)
s.as_ref()
.try_to_primitive::<usize>(vm)
.ok()
.filter(|s| *s < len)
.ok_or_else(|| vm.new_value_error("madvise start out of bounds"))
@@ -355,7 +356,8 @@ mod mmap {
let length = self
.length
.map(|s| {
s.try_to_primitive::<usize>(vm)
s.as_ref()
.try_to_primitive::<usize>(vm)
.map_err(|_| vm.new_value_error("madvise length invalid"))
})
.transpose()?
@@ -920,9 +922,9 @@ mod mmap {
#[pymethod(name = "move")]
fn move_(
zelf: &Py<Self>,
dest: PyIntRef,
src: PyIntRef,
count: PyIntRef,
dest: ArgIndex,
src: ArgIndex,
count: ArgIndex,
vm: &VirtualMachine,
) -> PyResult<()> {
fn args(
@@ -948,7 +950,7 @@ mod mmap {
}
let size = zelf.__len__();
let (dest, src, cnt) = args(&dest, &src, &count, size, vm)
let (dest, src, cnt) = args(dest.as_ref(), src.as_ref(), count.as_ref(), size, vm)
.ok_or_else(|| vm.new_value_error("source, destination, or count out of range"))?;
let dest_end = dest + cnt;
+6 -5
View File
@@ -6,8 +6,9 @@ pub(crate) use resource::module_def;
mod resource {
use crate::vm::{
Py, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, VirtualMachine,
builtins::{PyInt, PyIntRef},
builtins::PyInt,
convert::{ToPyException, ToPyObject},
function::ArgIndex,
types::PyStructSequence,
};
use num_traits::Signed;
@@ -185,8 +186,8 @@ mod resource {
}
#[pyfunction]
fn getrlimit(resource: PyIntRef, vm: &VirtualMachine) -> PyResult<Limits> {
let resource = py2rlim(&resource, vm)?;
fn getrlimit(resource: ArgIndex, vm: &VirtualMachine) -> PyResult<Limits> {
let resource = py2rlim(resource.as_ref(), vm)?;
if resource >= RLIM_NLIMITS as host_resource::rlim_t {
return Err(vm.new_value_error("invalid resource specified"));
@@ -197,8 +198,8 @@ mod resource {
}
#[pyfunction]
fn setrlimit(resource: PyIntRef, limits: Limits, vm: &VirtualMachine) -> PyResult<()> {
let resource = py2rlim(&resource, vm)?;
fn setrlimit(resource: ArgIndex, limits: Limits, vm: &VirtualMachine) -> PyResult<()> {
let resource = py2rlim(resource.as_ref(), vm)?;
if resource >= RLIM_NLIMITS as host_resource::rlim_t {
return Err(vm.new_value_error("invalid resource specified"));
+6 -6
View File
@@ -5,9 +5,9 @@ use rustpython_unicode::case;
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
builtins::{PyInt, PyIntRef, PyTuple},
builtins::{PyInt, PyTuple},
convert::TryFromBorrowedObject,
function::PySsize,
function::{ArgIndex, PySsize},
};
#[derive(FromArgs)]
@@ -41,17 +41,17 @@ pub(crate) struct StartsEndsWithArgs {
#[pyarg(positional)]
affix: PyObjectRef,
#[pyarg(positional, default)]
start: Option<PyIntRef>,
start: Option<ArgIndex>,
#[pyarg(positional, default)]
end: Option<PyIntRef>,
end: Option<ArgIndex>,
}
impl StartsEndsWithArgs {
pub(crate) fn get_value(self, len: usize) -> (PyObjectRef, Option<Range<usize>>) {
let range = if self.start.is_some() || self.end.is_some() {
Some(adjust_indices(
self.start.as_deref(),
self.end.as_deref(),
self.start.as_ref().map(AsRef::as_ref),
self.end.as_ref().map(AsRef::as_ref),
len,
))
} else {
+4 -2
View File
@@ -1,11 +1,12 @@
use super::{
IterStatus, PositionIterInternal, PyGenericAlias, PyIntRef, PyTupleRef, PyType, PyTypeRef,
IterStatus, PositionIterInternal, PyGenericAlias, PyTupleRef, PyType, PyTypeRef,
iter::builtins_reversed, locked_rev_next,
};
use crate::common::lock::{PyMutex, PyRwLock};
use crate::{
AsObject, Context, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
class::PyClassImpl,
function::ArgIndex,
protocol::{PyIter, PyIterReturn},
raise_if_stop,
types::{Constructor, IterNext, Iterable, SelfIter},
@@ -56,7 +57,7 @@ pub struct EnumerateArgs {
#[pyarg(any)]
iterable: PyIter,
#[pyarg(any, default = 0)]
start: PyIntRef,
start: ArgIndex,
}
impl Constructor for PyEnumerate {
@@ -67,6 +68,7 @@ impl Constructor for PyEnumerate {
Self::Args { iterable, start }: Self::Args,
_vm: &VirtualMachine,
) -> PyResult<Self> {
let start = start.into_int_ref();
let counter = match start.as_bigint().to_usize() {
Some(n) => Counter::Small(n),
None => Counter::Big(start.as_bigint().clone()),
+9 -5
View File
@@ -1,6 +1,6 @@
use super::{
PositionIterInternal, PyBytesRef, PyDict, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef,
int::{PyInt, PyIntRef},
int::PyInt,
iter::{IterStatus, builtins_iter},
};
use crate::{
@@ -17,7 +17,7 @@ use crate::{
},
convert::{ToPyException, ToPyObject, ToPyResult},
format::{format, format_map},
function::{ArgIterable, FuncArgs, OptionalArg, PyComparisonValue, PySsize},
function::{ArgIndex, ArgIterable, FuncArgs, OptionalArg, PyComparisonValue, PySsize},
intern::PyInterned,
object::{MaybeTraverse, Traverse, TraverseFn},
protocol::{
@@ -1871,14 +1871,18 @@ pub(crate) struct FindArgs {
#[pyarg(positional)]
sub: PyStrRef,
#[pyarg(positional, default)]
start: Option<PyIntRef>,
start: Option<ArgIndex>,
#[pyarg(positional, default)]
end: Option<PyIntRef>,
end: Option<ArgIndex>,
}
impl FindArgs {
fn get_value(self, len: usize) -> (PyStrRef, core::ops::Range<usize>) {
let range = adjust_indices(self.start.as_deref(), self.end.as_deref(), len);
let range = adjust_indices(
self.start.as_ref().map(AsRef::as_ref),
self.end.as_ref().map(AsRef::as_ref),
len,
);
(self.sub, range)
}
}
+8 -4
View File
@@ -10,7 +10,7 @@ use crate::{
cformat::cformat_bytes,
common::wtf8::is_py_ascii_whitespace,
common::{borrow::BorrowedValue, hash},
function::{ArgIterable, Either, OptionalArg, PyComparisonValue},
function::{ArgIndex, ArgIterable, Either, OptionalArg, PyComparisonValue},
literal::escape::Escape,
protocol::{BufferFlags, PyBuffer},
sequence::{SequenceExt, SequenceMutExt},
@@ -315,9 +315,9 @@ pub struct ByteInnerFindOptions {
#[pyarg(positional)]
sub: ByteInnerSub,
#[pyarg(positional, default)]
start: Option<PyIntRef>,
start: Option<ArgIndex>,
#[pyarg(positional, default)]
end: Option<PyIntRef>,
end: Option<ArgIndex>,
}
impl ByteInnerFindOptions {
@@ -327,7 +327,11 @@ impl ByteInnerFindOptions {
vm: &VirtualMachine,
) -> PyResult<(Vec<u8>, core::ops::Range<usize>)> {
let sub = self.sub.into_vec(vm)?;
let range = anystr::adjust_indices(self.start.as_deref(), self.end.as_deref(), len);
let range = anystr::adjust_indices(
self.start.as_ref().map(AsRef::as_ref),
self.end.as_ref().map(AsRef::as_ref),
len,
);
Ok((sub, range))
}
}
+4 -4
View File
@@ -27,8 +27,8 @@ pub(crate) mod _thread {
convert::ToPyException,
frame::FrameObjectRef,
function::{
ArgCallable, ArgumentError, DefaultRepr, FromArgs, FuncArgs, KwArgs, NameExcInfo,
OptionalArg, Param, ParamKind, PosArgs, PySetterValue, TimeoutSeconds,
ArgCallable, ArgIndex, ArgumentError, DefaultRepr, FromArgs, FuncArgs, KwArgs,
NameExcInfo, OptionalArg, Param, ParamKind, PosArgs, PySetterValue, TimeoutSeconds,
},
object::{Traverse, TraverseFn},
types::{Constructor, GetAttr, PyStructSequence, Representable, SetAttr},
@@ -847,7 +847,7 @@ pub(crate) mod _thread {
#[derive(FromArgs)]
struct StackSizeArgs {
#[pyarg(positional, default = 0)]
size: PyIntRef,
size: ArgIndex,
}
#[pyfunction]
@@ -855,7 +855,7 @@ pub(crate) mod _thread {
const MIN_SIZE: usize = PY_OS_MIN_STACK_SIZE + SYSTEM_PAGE_SIZE;
let StackSizeArgs { size } = args;
let Ok(size) = size.try_to_primitive(vm) else {
let Ok(size) = size.as_ref().try_to_primitive(vm) else {
return Err(vm.new_value_error(format!("size must be at least {MIN_SIZE} bytes")));
};
+7 -6
View File
@@ -5,13 +5,11 @@ mod decl {
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, PyWeakRef, TryFromObject,
VirtualMachine,
builtins::{
PyGenericAlias, PyInt, PyIntRef, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef, int,
},
builtins::{PyGenericAlias, PyInt, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef, int},
class::PyClassDef,
common::lock::{PyMutex, PyRwLock, PyRwLockWriteGuard},
convert::ToPyObject,
function::{FuncArgs, NameIterables, PosArgs},
function::{ArgIndex, FuncArgs, NameIterables, PosArgs},
protocol::{PyIter, PyIterReturn, PyNumber},
raise_if_stop,
stdlib::sys,
@@ -1329,7 +1327,7 @@ mod decl {
#[pyarg(any)]
iterable: PyObjectRef,
#[pyarg(any)]
r: PyIntRef,
r: ArgIndex,
}
impl Constructor for PyItertoolsCombinations {
@@ -1342,6 +1340,7 @@ mod decl {
) -> PyResult<Self> {
let pool: Vec<_> = iterable.try_to_value(vm)?;
let r = r.into_int_ref();
let r = r.as_bigint();
if r.is_negative() {
return Err(vm.new_value_error("r must be non-negative"));
@@ -1454,6 +1453,7 @@ mod decl {
vm: &VirtualMachine,
) -> PyResult<Self> {
let pool: Vec<_> = iterable.try_to_value(vm)?;
let r = r.into_int_ref();
let r = r.as_bigint();
if r.is_negative() {
return Err(vm.new_value_error("r must be non-negative"));
@@ -1800,7 +1800,7 @@ mod decl {
#[pyarg(any)]
iterable: PyObjectRef,
#[pyarg(any)]
n: PyIntRef,
n: ArgIndex,
#[pyarg(named, default)]
strict: bool,
}
@@ -1817,6 +1817,7 @@ mod decl {
}: Self::Args,
vm: &VirtualMachine,
) -> PyResult<Self> {
let n = n.into_int_ref();
let n = n.as_bigint();
if n.lt(&BigInt::one()) {
return Err(vm.new_value_error("n must be at least one"));
+4 -6
View File
@@ -5,11 +5,8 @@ pub(crate) use pwd::module_def;
#[pymodule]
mod pwd {
use crate::{
PyResult, VirtualMachine,
builtins::{PyIntRef, PyUtf8StrRef},
convert::IntoPyException,
exceptions,
types::PyStructSequence,
PyResult, VirtualMachine, builtins::PyUtf8StrRef, convert::IntoPyException, exceptions,
function::ArgIndex, types::PyStructSequence,
};
use core::hint::cold_path;
use rustpython_host_env::pwd as host_pwd;
@@ -68,7 +65,8 @@ mod pwd {
}
#[pyfunction]
fn getpwuid(uidobj: PyIntRef, vm: &VirtualMachine) -> PyResult<PasswdData> {
fn getpwuid(uidobj: ArgIndex, vm: &VirtualMachine) -> PyResult<PasswdData> {
let uidobj = uidobj.into_int_ref();
let uid_t = libc::uid_t::try_from(uidobj.as_bigint()).ok();
let user = uid_t
.map(host_pwd::getpwuid)
@@ -312,3 +312,65 @@ class HintIndex:
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))