Accept __index__-conforming objects for compile() flags / optimize (#7728)

CPython's compile() (Python/Python-ast.c) accepts any object with
__index__ for the flags and optimize arguments. RustPython's CompileArgs
typed both fields as OptionalArg<PyIntRef>, so a class with only
__index__ raised 'TypeError: Expected type int but X found' during arg
binding.

bpo-36907's regression test (test_call.test_fastcall_clearing_dict)
exercises exactly this: an IntWithDict.__index__ that mutates
self.kwargs mid-call. CPython parses both args via __index__ and doesn't
crash even when the kwargs dict is cleared between binding and use.

Switch flags and optimize to OptionalArg<ArgPrimitiveIndex<i32>>, the
same helper already used by range, slice, bytes.__mul__, hex, oct, etc.
ArgPrimitiveIndex calls try_index (= __index__ protocol) and converts
to the requested primitive in one step, so the three call sites in
compile() simplify from .map_or(Ok(d), |v| v.try_to_primitive(vm))? to
.map_or(d, |v| v.value).

Unmasks test_call.FastCallTests.test_fastcall_clearing_dict.
This commit is contained in:
Changjoon
2026-04-29 18:26:49 +09:00
committed by GitHub
parent c8ddbd2326
commit ba2b619c0c
2 changed files with 10 additions and 8 deletions

View File

@@ -585,7 +585,6 @@ class FastCallTests(unittest.TestCase):
result = _testcapi.pyobject_vectorcall(func, args, kwnames)
self.check_result(result, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'int' but 'IntWithDict' found.
def test_fastcall_clearing_dict(self):
# Test bpo-36907: the point of the test is just checking that this
# does not crash.

View File

@@ -21,8 +21,8 @@ mod builtins {
common::hash::PyHash,
function::{
ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping,
ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg, OptionalOption,
PosArgs,
ArgPrimitiveIndex, ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg,
OptionalOption, PosArgs,
},
protocol::{PyIter, PyIterReturn},
py_io,
@@ -99,12 +99,15 @@ mod builtins {
source: PyObjectRef,
filename: FsPath,
mode: PyUtf8StrRef,
// CPython parity: flags / optimize accept any object with __index__,
// not just exact int. Matches the behavior of `int(x)` arg conversion
// used by Python/Python-ast.c::compile.
#[pyarg(any, optional)]
flags: OptionalArg<PyIntRef>,
flags: OptionalArg<ArgPrimitiveIndex<i32>>,
#[pyarg(any, optional)]
dont_inherit: OptionalArg<bool>,
#[pyarg(any, optional)]
optimize: OptionalArg<PyIntRef>,
optimize: OptionalArg<ArgPrimitiveIndex<i32>>,
#[pyarg(any, optional)]
_feature_version: OptionalArg<i32>,
}
@@ -264,7 +267,7 @@ mod builtins {
let mode_str = args.mode.as_str();
let optimize: i32 = args.optimize.map_or(Ok(-1), |v| v.try_to_primitive(vm))?;
let optimize: i32 = args.optimize.map_or(-1, |v| v.value);
let optimize: u8 = if optimize == -1 {
vm.state.config.settings.optimize
} else {
@@ -277,7 +280,7 @@ mod builtins {
.source
.fast_isinstance(&_ast::NodeAst::make_static_type())
{
let flags: i32 = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?;
let flags: i32 = args.flags.map_or(0, |v| v.value);
let is_ast_only = !(flags & _ast::PY_CF_ONLY_AST).is_zero();
// func_type mode requires PyCF_ONLY_AST
@@ -342,7 +345,7 @@ mod builtins {
let source = decode_source_bytes(&source, &args.filename.to_string_lossy(), vm)?;
let source = source.as_str();
let flags = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?;
let flags: i32 = args.flags.map_or(0, |v| v.value);
if !(flags & !_ast::PY_COMPILE_FLAGS_MASK).is_zero() {
return Err(vm.new_value_error("compile() unrecognized flags"));