From dcb72df459cc43341df0f0e9e6d6ad46c15a18c3 Mon Sep 17 00:00:00 2001 From: "widehyo@gmail.com" Date: Sun, 4 Oct 2026 15:03:17 +0900 Subject: [PATCH 1/4] Reject unsupported native keywords before argument binding Add explicit keyword dispatch metadata independently of inferred Rust calling flags. Reject nonempty keywords before binding and conversion, while preserving descriptor self checks and keyword-capable calls. Annotate 31 builtins and list.append/list.clear, derive C-API policies from ml_flags, and group dynamic registration metadata in MethodDefSpec. Add regression coverage and remove the id doctest expected-failure marker. Validation: workspace and C-API tests/clippy, VM without doc, keyword regressions, and test_call/test_extcall/test_builtin/test_list passed. The full snippet suite had 475 passes and 7 failures: six CPython-side expectation mismatches and one missing optional SQLite module. Assisted-by: Codex:GPT-6 --- Lib/test/test_extcall.py | 2 +- crates/capi/src/methodobject.rs | 55 +++++++++++++--- crates/derive-impl/src/pyclass.rs | 12 +++- crates/derive-impl/src/pymodule.rs | 30 +++++++-- crates/vm/src/builtins/builtin_func.rs | 38 +++++++++++- crates/vm/src/builtins/descriptor.rs | 58 +++++++++++++++-- crates/vm/src/builtins/list.rs | 4 +- crates/vm/src/codecs.rs | 1 + crates/vm/src/function/method.rs | 45 ++++++++++++++ crates/vm/src/function/mod.rs | 2 +- crates/vm/src/macros.rs | 8 ++- crates/vm/src/stdlib/builtins.rs | 62 +++++++++---------- crates/vm/src/vm/context.rs | 11 +++- crates/vm/src/vm/vm_new.rs | 16 +++-- .../snippets/builtin_keyword_dispatch.py | 41 ++++++++++++ 15 files changed, 316 insertions(+), 69 deletions(-) create mode 100644 extra_tests/snippets/builtin_keyword_dispatch.py diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index 483d5ad5f2b..c816d0135a1 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -442,7 +442,7 @@ ... False True - >>> id(1, **{'foo': 1}) # TODO: RUSTPYTHON # doctest:+EXPECTED_FAILURE + >>> id(1, **{'foo': 1}) Traceback (most recent call last): ... TypeError: id() takes no keyword arguments diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 9868db36cac..1a2c0fa1f1f 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -5,7 +5,9 @@ use crate::pystate::with_vm; use crate::util::{CStrExt, FfiPtrExt}; use core::ffi::{c_char, c_int}; use core::fmt::Debug; -use rustpython_vm::function::{FuncArgs, HeapMethodDef, ItemDoc, PosArgs, PyMethodFlags}; +use rustpython_vm::function::{ + FuncArgs, HeapMethodDef, ItemDoc, KeywordDispatch, MethodDefSpec, PosArgs, PyMethodFlags, +}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type); @@ -80,6 +82,19 @@ pub(crate) fn build_method_def( let flags = PyMethodFlags::from_bits(ml.ml_flags as u32) .ok_or_else(|| vm.new_system_error("PyMethodDef contains unknown flags"))?; + let keyword_dispatch = if flags.contains(PyMethodFlags::KEYWORDS) { + KeywordDispatch::PassToBinder + } else if flags.contains(PyMethodFlags::VARARGS) { + KeywordDispatch::RejectNonemptyUnqualified + } else { + KeywordDispatch::RejectNonempty + }; + let spec = MethodDefSpec { + name, + flags, + doc, + keyword_dispatch, + }; let has_self = has_self && !flags.contains(PyMethodFlags::STATIC); let method = ml.ml_meth; @@ -102,38 +117,38 @@ pub(crate) fn build_method_def( let f = method.PyCFunction; f(zelf.as_raw().cast_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) } else { let callable = move |vm: &VirtualMachine| unsafe { let f = method.PyCFunction; f(core::ptr::null_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) } }, PyMethodFlags::VARARGS => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_function(vm, method, flags, Some(args)) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) }, PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { let callable = move | args: FuncArgs, vm: &VirtualMachine| unsafe { call_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) }, PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS => { let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { call_fast_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) }, PyMethodFlags::FASTCALL => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_fast_function(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) }, PyMethodFlags::O => { let f = unsafe { method.PyCFunction }; @@ -141,12 +156,12 @@ pub(crate) fn build_method_def( let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) } else { let callable = move |arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(core::ptr::null_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc)) + Ok(vm.ctx.new_method_def(callable, spec)) } }, _ => { @@ -385,4 +400,26 @@ mod tests { func.call0().unwrap(); }); } + + #[test] + fn no_keywords_before_arity() { + Python::attach(|py| { + unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { + unsafe { PyLong_FromLong(42) } + } + let f = PyCFunction::new(py, c_fn, c"no_keywords", c"", None).unwrap(); + let kwargs = pyo3::types::PyDict::new(py); + kwargs.set_item("unexpected", 1).unwrap(); + let err = f.call((1,), Some(&kwargs)).unwrap_err(); + assert_eq!( + err.value(py).str().unwrap().to_str().unwrap(), + "no_keywords() takes no keyword arguments" + ); + kwargs.clear(); + assert_eq!( + f.call((), Some(&kwargs)).unwrap().extract::().unwrap(), + 42 + ); + }); + } } diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index b935420591f..89d41118aab 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -1290,6 +1290,7 @@ where } let raw = item_meta.raw()?; + let no_keywords = item_meta.inner()._bool("no_keywords")?; let has_receiver = func .sig() .inputs @@ -1336,6 +1337,7 @@ where doc, doc_body_pending, raw, + no_keywords, coexist, attr_name: self.inner.attr_name, }); @@ -1530,6 +1532,7 @@ struct MethodNurseryItem { cfgs: Vec, ident: Ident, raw: bool, + no_keywords: bool, coexist: bool, doc: TokenStream, doc_body_pending: TokenStream, @@ -1561,6 +1564,11 @@ impl ToTokens for MethodNursery { let cfgs = &item.cfgs; let doc = &item.doc; let doc_body_pending = &item.doc_body_pending; + let keyword_dispatch = if item.no_keywords { + quote!(rustpython_vm::function::KeywordDispatch::RejectNonempty) + } else { + quote!(rustpython_vm::function::KeywordDispatch::PassToBinder) + }; let binding_flags = match &item.attr_name { AttrName::Method => { quote! { rustpython_vm::function::PyMethodFlags::METHOD } @@ -1603,7 +1611,7 @@ impl ToTokens for MethodNursery { Self::#ident, #flags, #doc, - ); + ).with_keyword_dispatch(#keyword_dispatch); ::rustpython_vm::__cfg_doc!({ def.doc_body_pending = #doc_body_pending; } else {}); @@ -1731,7 +1739,7 @@ impl ToTokens for GetSetNursery { struct MethodItemMeta(ItemMetaInner); impl ItemMeta for MethodItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist"]; + const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist", "no_keywords"]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 9a2ab6878e7..b58eaab6f1a 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -2,8 +2,8 @@ use crate::error::Diagnostic; use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, - ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, internal_doc_tokens, - iter_use_idents, pyclass_ident_and_attrs, + ErrorVec, ItemMeta, ItemMetaInner, ItemNursery, ModuleItemMeta, SimpleItemMeta, + internal_doc_tokens, iter_use_idents, pyclass_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; @@ -536,6 +536,7 @@ struct FunctionNurseryItem { ident: Ident, /// One internal doc per [`py_names`](Self::py_names) entry. docs: Vec, + no_keywords: bool, } impl FunctionNursery { @@ -565,6 +566,11 @@ impl ToTokens for ValidatedFunctionNursery { let ident = &item.ident; let cfgs = &item.cfgs; let cfgs = quote!(#(#cfgs)*); + let keyword_dispatch = if item.no_keywords { + quote!(rustpython_vm::function::KeywordDispatch::RejectNonempty) + } else { + quote!(rustpython_vm::function::KeywordDispatch::PassToBinder) + }; for (py_name, doc) in item.py_names.iter().zip(&item.docs) { inner_tokens.extend(quote![ #cfgs @@ -573,7 +579,7 @@ impl ToTokens for ValidatedFunctionNursery { #ident, rustpython_vm::function::PyMethodFlags::empty(), #doc, - ), + ).with_keyword_dispatch(#keyword_dispatch), ]); } } @@ -670,7 +676,8 @@ impl ModuleItem for FunctionItem { let ident = &func.sig().ident; let item_attr = args.attrs.remove(self.index()); - let item_meta = SimpleItemMeta::from_attr(ident.clone(), &item_attr)?; + let item_meta = FunctionItemMeta::from_attr(ident.clone(), &item_attr)?; + let no_keywords = item_meta.inner()._bool("no_keywords")?; let py_name = item_meta.simple_name()?; let mut py_names = vec![py_name]; @@ -715,6 +722,7 @@ impl ModuleItem for FunctionItem { py_names, cfgs: args.cfgs.to_vec(), docs, + no_keywords, }); Ok(()) } @@ -1082,3 +1090,17 @@ impl ModuleItem for AttributeItem { Ok(()) } } + +struct FunctionItemMeta(ItemMetaInner); + +impl ItemMeta for FunctionItemMeta { + const ALLOWED_NAMES: &'static [&'static str] = &["name", "no_keywords"]; + + fn from_inner(inner: ItemMetaInner) -> Self { + Self(inner) + } + + fn inner(&self) -> &ItemMetaInner { + &self.0 + } +} diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index 17e3e342485..b394d27df87 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -4,7 +4,10 @@ use crate::{ class::PyClassImpl, common::wtf8::Wtf8, convert::TryFromObject, - function::{Callee, FuncArgs, PyComparisonValue, PyMethodDef, PyMethodFlags, PyNativeFn}, + function::{ + Callee, FuncArgs, KeywordDispatch, PyComparisonValue, PyMethodDef, PyMethodFlags, + PyNativeFn, + }, types::{Callable, Comparable, PyComparisonOp, Representable}, }; use alloc::fmt; @@ -87,6 +90,9 @@ impl Callable for PyNativeFunction { type Args = FuncArgs; #[inline] fn call(zelf: &Py, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.kwargs.is_empty() && zelf.value.keyword_dispatch != KeywordDispatch::PassToBinder { + return Err(native_no_keywords_error(zelf, vm)?); + } let mut callee = Callee::named(zelf.value.name); if let Some(z) = &zelf.zelf { // STATIC methods store the class in zelf for qualname/repr purposes, @@ -272,6 +278,12 @@ fn vectorcall_native_function( ) -> PyResult { let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + if kwnames.is_some_and(|names| !names.is_empty()) + && zelf.value.keyword_dispatch != KeywordDispatch::PassToBinder + { + return Err(native_no_keywords_error(zelf, vm)?); + } + // Build FuncArgs with self already at position 0 (no insert(0) needed) let needs_self = zelf .zelf @@ -292,6 +304,30 @@ fn vectorcall_native_function( (zelf.value.func)(vm, func_args, callee) } +#[cold] +fn native_no_keywords_error( + zelf: &Py, + vm: &VirtualMachine, +) -> PyResult { + if zelf.value.keyword_dispatch == KeywordDispatch::RejectNonemptyUnqualified { + return Ok(vm.new_type_error(format!("{}() takes no keyword arguments", zelf.value.name))); + } + let qualname = PyNativeFunction::__qualname__(NativeFunctionOrMethod(zelf.to_owned()), vm)?; + let module = zelf + .module + .load_owned() + .filter(|module| !vm.is_none(module)); + let module = module.map(|module| module.str(vm)).transpose()?; + let name = if let Some(module) = module + && module.to_string() != "builtins" + { + format!("{module}.{qualname}") + } else { + qualname.to_string() + }; + Ok(vm.new_type_error(format!("{name}() takes no keyword arguments"))) +} + pub(crate) fn init(context: &'static Context) { PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type); context diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 301448654a6..ac10a21d355 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -6,7 +6,8 @@ use crate::{ common::hash::PyHash, convert::{ToPyObject, ToPyResult}, function::{ - Callee, FuncArgs, ItemDoc, PyMethodDef, PyMethodFlags, PySetterValue, PySsize, plain_doc, + Callee, FuncArgs, ItemDoc, KeywordDispatch, PyMethodDef, PyMethodFlags, PySetterValue, + PySsize, plain_doc, }, protocol::{PyNumberBinaryFunc, PyNumberTernaryFunc, PyNumberUnaryFunc}, types::{ @@ -112,8 +113,10 @@ impl Callable for PyMethodDescriptor { type Args = FuncArgs; #[inline] fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - if let Some(obj) = args.args.first() { - method_descr_typecheck(zelf, obj, vm)?; + method_descr_check_self(zelf, args.args.first().map(|obj| &**obj), vm)?; + if !args.kwargs.is_empty() && zelf.method.keyword_dispatch != KeywordDispatch::PassToBinder + { + return Err(method_descr_no_keywords_error(zelf, vm)); } (zelf.method.func)( vm, @@ -1556,6 +1559,40 @@ fn method_descr_typecheck( ))) } +#[inline] +fn method_descr_check_self( + descr: &PyMethodDescriptor, + obj: Option<&PyObject>, + vm: &VirtualMachine, +) -> PyResult<()> { + let obj = obj.ok_or_else(|| method_descr_missing_self_error(descr, vm))?; + method_descr_typecheck(descr, obj, vm) +} + +#[cold] +fn method_descr_missing_self_error( + descr: &PyMethodDescriptor, + vm: &VirtualMachine, +) -> crate::exceptions::types::PyBaseExceptionRef { + vm.new_type_error(format!( + "unbound method {}.{}() needs an argument", + descr.common.typ.name(), + descr.common.name + )) +} + +#[cold] +fn method_descr_no_keywords_error( + descr: &PyMethodDescriptor, + vm: &VirtualMachine, +) -> crate::exceptions::types::PyBaseExceptionRef { + vm.new_type_error(format!( + "{}.{}() takes no keyword arguments", + descr.common.typ.name(), + descr.common.name + )) +} + /// Vectorcall for method_descriptor: calls native method directly fn vectorcall_method_descriptor( zelf_obj: &PyObject, @@ -1565,10 +1602,19 @@ fn vectorcall_method_descriptor( vm: &VirtualMachine, ) -> PyResult { let zelf: &Py = zelf_obj.downcast_ref().unwrap(); - if nargs > 0 - && let Some(obj) = args.first() + method_descr_check_self( + zelf, + if nargs > 0 { + args.first().map(|obj| &**obj) + } else { + None + }, + vm, + )?; + if kwnames.is_some_and(|names| !names.is_empty()) + && zelf.method.keyword_dispatch != KeywordDispatch::PassToBinder { - method_descr_typecheck(zelf, obj, vm)?; + return Err(method_descr_no_keywords_error(zelf, vm)); } let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); (zelf.method.func)( diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index dbd40b69c76..143368c1c1e 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -420,7 +420,7 @@ impl PyList { flags(BASETYPE, SEQUENCE, _MATCH_SELF) )] impl Py { - #[pymethod] + #[pymethod(no_keywords)] pub(crate) fn append(&self, object: PyObjectRef) { self.payload.append(object) } @@ -497,7 +497,7 @@ impl Py { elements.insert(index, object); } - #[pymethod] + #[pymethod(no_keywords)] fn clear(&self) { let removed = core::mem::take(self.borrow_vec_mut().deref_mut()); removed.into_iter().rev().for_each(drop); diff --git a/crates/vm/src/codecs.rs b/crates/vm/src/codecs.rs index 587701d6528..807eaa7d822 100644 --- a/crates/vm/src/codecs.rs +++ b/crates/vm/src/codecs.rs @@ -177,6 +177,7 @@ impl CodecsRegistry { name: $name, func: crate::function::static_func($func), flags: crate::function::PyMethodFlags::O, + keyword_dispatch: crate::function::KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: DOC.offset, #[cfg(feature = "doc")] diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index b33da287710..886fee3b8a1 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -75,6 +75,7 @@ macro_rules! define_methods { name: $name, func: $crate::function::static_func($func), flags: $crate::function::PyMethodFlags::$flags, + keyword_dispatch: $crate::function::KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: 0, #[cfg(feature = "doc")] @@ -86,11 +87,44 @@ macro_rules! define_methods { }; } +/// Keyword acceptance is independent of the flags inferred from a Rust signature. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum KeywordDispatch { + #[default] + PassToBinder, + RejectNonempty, + /// CPython's legacy METH_VARARGS function reports only ml_name. + RejectNonemptyUnqualified, +} + +/// Registration metadata shared by dynamically created native methods. +#[derive(Clone, Copy, Debug)] +pub struct MethodDefSpec { + pub name: &'static str, + pub flags: PyMethodFlags, + pub doc: super::ItemDoc, + pub keyword_dispatch: KeywordDispatch, +} + +impl MethodDefSpec { + #[must_use] + pub const fn new(name: &'static str, flags: PyMethodFlags, doc: super::ItemDoc) -> Self { + Self { + name, + flags, + doc, + keyword_dispatch: KeywordDispatch::PassToBinder, + } + } +} + #[derive(Clone)] pub struct PyMethodDef { pub name: &'static str, // TODO: interned pub func: &'static dyn PyNativeFn, pub flags: PyMethodFlags, + pub keyword_dispatch: KeywordDispatch, /// Start of the database body. Read only when `doc_len != 0`. /// Absent when the `doc` feature is off, as are `doc_len` and `doc_body_pending`. #[cfg(feature = "doc")] @@ -115,6 +149,12 @@ pub struct PyMethodDef { } impl PyMethodDef { + #[must_use] + pub const fn with_keyword_dispatch(mut self, policy: KeywordDispatch) -> Self { + self.keyword_dispatch = policy; + self + } + #[must_use] pub fn item_doc(&self) -> super::ItemDoc { super::ItemDoc { @@ -141,6 +181,7 @@ impl PyMethodDef { name, func: super::static_func(func), flags: flags.with_call_convention(F::ARGS), + keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] @@ -162,6 +203,7 @@ impl PyMethodDef { name, func: super::static_raw_func(func), flags: flags.with_call_convention(&[SigArg::from_arg::("args")]), + keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] @@ -323,6 +365,7 @@ impl PyMethodDef { name: "", func: &|_, _, _| unreachable!(), flags: PyMethodFlags::empty(), + keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: 0, #[cfg(feature = "doc")] @@ -353,6 +396,7 @@ impl PyMethodDef { name: self.name, func: self.func, flags: self.flags, + keyword_dispatch: self.keyword_dispatch, #[cfg(feature = "doc")] doc_off: self.doc_off, #[cfg(feature = "doc")] @@ -375,6 +419,7 @@ impl core::fmt::Debug for PyMethodDef { }), ) .field("flags", &self.flags) + .field("keyword_dispatch", &self.keyword_dispatch) .field("doc", &self.doc) .finish() } diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index 707afed0966..0cff2ff17e2 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -33,7 +33,7 @@ pub use fspath::FsPath; pub(crate) use getargs::ArgSpec; pub use getset::PySetterValue; pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc}; -pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags}; +pub use method::{HeapMethodDef, KeywordDispatch, MethodDefSpec, PyMethodDef, PyMethodFlags}; pub use number::{ ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgStrictInt, PySize, PySsize, }; diff --git a/crates/vm/src/macros.rs b/crates/vm/src/macros.rs index 340e5461571..65d7921117c 100644 --- a/crates/vm/src/macros.rs +++ b/crates/vm/src/macros.rs @@ -245,10 +245,12 @@ macro_rules! named_function { let ctx: &$crate::Context = &$ctx; $crate::__exports::paste::expr! { ctx.new_method_def( - stringify!($func), [<$module _ $func>], - ::rustpython_vm::function::PyMethodFlags::empty(), - ::rustpython_vm::function::ItemDoc::NONE, + ::rustpython_vm::function::MethodDefSpec::new( + stringify!($func), + ::rustpython_vm::function::PyMethodFlags::empty(), + ::rustpython_vm::function::ItemDoc::NONE, + ), ) .to_function() .with_module(ctx.intern_str(stringify!($module)).into()) diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 65f64299443..61dd3529211 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -39,12 +39,12 @@ mod builtins { const CODEGEN_NOT_SUPPORTED: &str = "can't compile() to bytecode when the `codegen` feature of rustpython is disabled"; - #[pyfunction] + #[pyfunction(no_keywords)] fn abs(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm._abs(&x) } - #[pyfunction] + #[pyfunction(no_keywords)] fn all(iterable: ArgIterable, vm: &VirtualMachine) -> PyResult { for item in iterable.iter(vm)? { if !item?.into_bool() { @@ -54,7 +54,7 @@ mod builtins { Ok(true) } - #[pyfunction] + #[pyfunction(no_keywords)] fn any(iterable: ArgIterable, vm: &VirtualMachine) -> PyResult { for item in iterable.iter(vm)? { if item?.into_bool() { @@ -64,12 +64,12 @@ mod builtins { Ok(false) } - #[pyfunction] + #[pyfunction(no_keywords)] pub fn ascii(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.ascii(vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn bin(number: ArgIndex) -> String { let number = number.into_int_ref(); let x = number.as_bigint(); @@ -80,12 +80,12 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn callable(obj: PyObjectRef) -> bool { obj.is_callable() } - #[pyfunction] + #[pyfunction(no_keywords)] fn chr(i: ArgIndex, vm: &VirtualMachine) -> PyResult { let i = i.into_int_ref(); let value = i @@ -375,7 +375,7 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn delattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let attr = name.try_to_ref::(vm).map_err(|_e| { vm.new_type_error(format!( @@ -386,12 +386,12 @@ mod builtins { obj.del_attr(attr, vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn dir(obj: OptionalArg, vm: &VirtualMachine) -> PyResult { vm.dir(obj.into_option()) } - #[pyfunction] + #[pyfunction(no_keywords)] fn divmod(x: PyObjectRef, y: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm._divmod(&x, &y) } @@ -673,12 +673,12 @@ mod builtins { prompt: OptionalArg, } - #[pyfunction] + #[pyfunction(no_keywords)] fn format(args: FormatArgs, vm: &VirtualMachine) -> PyResult { vm.format(&args.value, args.format_spec) } - #[pyfunction] + #[pyfunction(no_keywords)] fn getattr( obj: PyObjectRef, attr: PyObjectRef, @@ -699,12 +699,12 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn globals(vm: &VirtualMachine) -> PyDictRef { vm.current_globals() } - #[pyfunction] + #[pyfunction(no_keywords)] fn hasattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult { let attr = name.try_to_ref::(vm).map_err(|_e| { vm.new_type_error(format!( @@ -715,7 +715,7 @@ mod builtins { obj.has_attr(attr, vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn hash(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.hash(vm) } @@ -739,19 +739,19 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn hex(number: ArgIndex) -> String { let number = number.into_int_ref(); let n = number.as_bigint(); format!("{n:#x}") } - #[pyfunction] + #[pyfunction(no_keywords)] fn id(obj: PyObjectRef) -> usize { obj.get_id() } - #[pyfunction] + #[pyfunction(no_keywords)] fn input(args: InputArgs, vm: &VirtualMachine) -> PyResult { let prompt = args.prompt; use std::io::IsTerminal; @@ -838,7 +838,7 @@ mod builtins { false } - #[pyfunction] + #[pyfunction(no_keywords)] fn isinstance( obj: PyObjectRef, class_or_tuple: PyObjectRef, @@ -847,7 +847,7 @@ mod builtins { obj.is_instance(&class_or_tuple, vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn issubclass( cls: PyObjectRef, class_or_tuple: PyObjectRef, @@ -856,7 +856,7 @@ mod builtins { cls.is_subclass(&class_or_tuple, vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn iter( iter_target: PyObjectRef, sentinel: OptionalArg, @@ -873,12 +873,12 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn aiter(async_iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult { async_iterable.get_aiter(vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn anext( aiter: PyObjectRef, default_value: OptionalArg, @@ -905,12 +905,12 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn len(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.length(vm) } - #[pyfunction] + #[pyfunction(no_keywords)] fn locals(vm: &VirtualMachine) -> PyResult { vm.current_locals() } @@ -985,7 +985,7 @@ mod builtins { min_or_max(args, vm, "min", PyComparisonOp::Lt) } - #[pyfunction] + #[pyfunction(no_keywords)] fn next( iterator: PyObjectRef, default_value: OptionalArg, @@ -1007,7 +1007,7 @@ mod builtins { }) } - #[pyfunction] + #[pyfunction(no_keywords)] fn oct(number: ArgIndex, vm: &VirtualMachine) -> PyObjectRef { let number = number.into_int_ref(); let n = number.as_bigint(); @@ -1020,7 +1020,7 @@ mod builtins { vm.ctx.new_str(s).into() } - #[pyfunction] + #[pyfunction(no_keywords)] // builtin_ord fn ord(character: PyObjectRef, vm: &VirtualMachine) -> PyResult { let bytes = if let Some(string) = character.downcast_ref::() { @@ -1124,7 +1124,7 @@ mod builtins { Ok(()) } - #[pyfunction] + #[pyfunction(no_keywords)] fn repr(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.repr(vm) } @@ -1173,7 +1173,7 @@ mod builtins { } } - #[pyfunction] + #[pyfunction(no_keywords)] fn setattr( obj: PyObjectRef, name: PyObjectRef, @@ -1266,7 +1266,7 @@ mod builtins { ) } - #[pyfunction] + #[pyfunction(no_keywords)] fn vars(obj: OptionalArg, vm: &VirtualMachine) -> PyResult { if let OptionalArg::Present(obj) = obj { obj.get_attr(identifier!(vm, __dict__), vm) diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 42da0551cae..a81e83edcaa 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -651,18 +651,23 @@ impl Context { pub fn new_method_def( &self, - name: &'static str, f: F, - flags: PyMethodFlags, - doc: ItemDoc, + spec: crate::function::MethodDefSpec, ) -> PyRef where F: IntoPyNativeFn, { + let crate::function::MethodDefSpec { + name, + flags, + doc, + keyword_dispatch, + } = spec; let def = PyMethodDef { name, func: Box::leak(Box::new(f.into_func())), flags: flags.with_call_convention(F::ARGS), + keyword_dispatch, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 1a1696b7106..29b1b2363e3 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -143,10 +143,12 @@ impl VirtualMachine { F: IntoPyNativeFn, { let def = self.ctx.new_method_def( - name, f, - PyMethodFlags::empty(), - crate::function::ItemDoc::NONE, + crate::function::MethodDefSpec::new( + name, + PyMethodFlags::empty(), + crate::function::ItemDoc::NONE, + ), ); def.build_function(self, None) } @@ -161,10 +163,12 @@ impl VirtualMachine { F: IntoPyNativeFn, { let def = self.ctx.new_method_def( - name, f, - PyMethodFlags::METHOD, - crate::function::ItemDoc::NONE, + crate::function::MethodDefSpec::new( + name, + PyMethodFlags::METHOD, + crate::function::ItemDoc::NONE, + ), ); def.build_method(class, self) } diff --git a/extra_tests/snippets/builtin_keyword_dispatch.py b/extra_tests/snippets/builtin_keyword_dispatch.py new file mode 100644 index 00000000000..9e244837950 --- /dev/null +++ b/extra_tests/snippets/builtin_keyword_dispatch.py @@ -0,0 +1,41 @@ +def error(call, expected): + try: + call() + except TypeError as exc: + assert str(exc) == expected, (str(exc), expected) + else: + raise AssertionError("expected TypeError") + + +error(lambda: id(obj=1), "id() takes no keyword arguments") +error(lambda: id(**{"obj": 1}), "id() takes no keyword arguments") +error(lambda: id(1, 2, obj=1), "id() takes no keyword arguments") +error(lambda: list.append(x=1), "unbound method list.append() needs an argument") +error(lambda: list.append(**{"x": 1}), "unbound method list.append() needs an argument") +error( + lambda: list.append(1, x=1), + "descriptor 'append' for 'list' objects doesn't apply to a 'int' object", +) +error(lambda: list.append([], x=1), "list.append() takes no keyword arguments") +error(lambda: list.append([], **{"x": 1}), "list.append() takes no keyword arguments") +error(lambda: [].append(x=1), "list.append() takes no keyword arguments") +error(lambda: [].clear(1, x=1), "list.clear() takes no keyword arguments") + + +class Index: + def __index__(self): + raise AssertionError("conversion ran before keyword check") + + +error(lambda: bin(Index(), x=1), "bin() takes no keyword arguments") +assert id(1, **{}) == id(1) +assert round(1.25, ndigits=1) == 1.2 +assert pow(2, exp=3) == 8 +assert sorted([2, 1], reverse=True) == [2, 1] +assert sum([1, 2], start=4) == 7 +values = [] +list.append(values, 1) +values.append(2) +assert values == [1, 2] +list.clear(values) +assert values == [] From 4575883f447fbfdb6314227d6fc96a2136391b59 Mon Sep 17 00:00:00 2001 From: "widehyo@gmail.com" Date: Sun, 4 Oct 2026 23:55:16 +0900 Subject: [PATCH 2/4] Infer native keyword dispatch from argument metadata Replace no_keywords annotations with registration-time inference from FromArgs::PARAMS, including nested parameter metadata. Forward arguments with keyword-capable parameters to the binder, preserving min/max body validation and descriptor self checks. Add regressions for raw arguments, keyword argument structs, and list.sort. Tested: C-API tests, workspace and C-API Clippy, VM without doc, both keyword regression snippets, and test_call/test_extcall/test_builtin/test_list. Workspace tests reported eight _opcode snapshot failures. The full snippet suite reported 477 passes and seven failures: six CPython-side failures and one RustPython failure due to the missing optional SQLite module. Assisted-by: Codex:GPT-6 --- crates/derive-impl/src/pyclass.rs | 21 ++++--- crates/derive-impl/src/pymodule.rs | 16 ++--- crates/derive-impl/src/util.rs | 16 +++++ crates/vm/src/builtins/list.rs | 4 +- crates/vm/src/function/mod.rs | 3 +- crates/vm/src/function/signature.rs | 32 ++++++++++ crates/vm/src/stdlib/builtins.rs | 62 +++++++++---------- .../snippets/builtin_keyword_inference.py | 38 ++++++++++++ 8 files changed, 138 insertions(+), 54 deletions(-) create mode 100644 extra_tests/snippets/builtin_keyword_inference.py diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index 89d41118aab..804a0e3adaf 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -2,7 +2,7 @@ use super::Diagnostic; use crate::util::{ ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta, ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, internal_doc_tokens, - pyclass_ident_and_attrs, pyexception_ident_and_attrs, + keyword_dispatch_tokens, pyclass_ident_and_attrs, pyexception_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree}; @@ -1290,7 +1290,6 @@ where } let raw = item_meta.raw()?; - let no_keywords = item_meta.inner()._bool("no_keywords")?; let has_receiver = func .sig() .inputs @@ -1308,6 +1307,12 @@ where _ => None, } }; + let keyword_dispatch = keyword_dispatch_tokens( + func.sig(), + implicit_self, + args.context.self_ty_subst.as_ref(), + ); + // Add #[allow(non_snake_case)] for setter methods like set___name__ let method_name = ident.to_string(); if method_name.starts_with("set_") && method_name.contains("__") { @@ -1337,7 +1342,7 @@ where doc, doc_body_pending, raw, - no_keywords, + keyword_dispatch, coexist, attr_name: self.inner.attr_name, }); @@ -1532,7 +1537,7 @@ struct MethodNurseryItem { cfgs: Vec, ident: Ident, raw: bool, - no_keywords: bool, + keyword_dispatch: TokenStream, coexist: bool, doc: TokenStream, doc_body_pending: TokenStream, @@ -1564,11 +1569,7 @@ impl ToTokens for MethodNursery { let cfgs = &item.cfgs; let doc = &item.doc; let doc_body_pending = &item.doc_body_pending; - let keyword_dispatch = if item.no_keywords { - quote!(rustpython_vm::function::KeywordDispatch::RejectNonempty) - } else { - quote!(rustpython_vm::function::KeywordDispatch::PassToBinder) - }; + let keyword_dispatch = &item.keyword_dispatch; let binding_flags = match &item.attr_name { AttrName::Method => { quote! { rustpython_vm::function::PyMethodFlags::METHOD } @@ -1739,7 +1740,7 @@ impl ToTokens for GetSetNursery { struct MethodItemMeta(ItemMetaInner); impl ItemMeta for MethodItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist", "no_keywords"]; + const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist"]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index b58eaab6f1a..8f60550f0ba 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -3,7 +3,7 @@ use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ItemMeta, ItemMetaInner, ItemNursery, ModuleItemMeta, SimpleItemMeta, - internal_doc_tokens, iter_use_idents, pyclass_ident_and_attrs, + internal_doc_tokens, iter_use_idents, keyword_dispatch_tokens, pyclass_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; @@ -536,7 +536,7 @@ struct FunctionNurseryItem { ident: Ident, /// One internal doc per [`py_names`](Self::py_names) entry. docs: Vec, - no_keywords: bool, + keyword_dispatch: TokenStream, } impl FunctionNursery { @@ -566,11 +566,7 @@ impl ToTokens for ValidatedFunctionNursery { let ident = &item.ident; let cfgs = &item.cfgs; let cfgs = quote!(#(#cfgs)*); - let keyword_dispatch = if item.no_keywords { - quote!(rustpython_vm::function::KeywordDispatch::RejectNonempty) - } else { - quote!(rustpython_vm::function::KeywordDispatch::PassToBinder) - }; + let keyword_dispatch = &item.keyword_dispatch; for (py_name, doc) in item.py_names.iter().zip(&item.docs) { inner_tokens.extend(quote![ #cfgs @@ -677,7 +673,6 @@ impl ModuleItem for FunctionItem { let item_attr = args.attrs.remove(self.index()); let item_meta = FunctionItemMeta::from_attr(ident.clone(), &item_attr)?; - let no_keywords = item_meta.inner()._bool("no_keywords")?; let py_name = item_meta.simple_name()?; let mut py_names = vec![py_name]; @@ -716,13 +711,14 @@ impl ModuleItem for FunctionItem { internal_doc_tokens(func.sig(), py_name, None, doc, None, Some("$module")) }) .collect(); + let keyword_dispatch = keyword_dispatch_tokens(func.sig(), None, None); args.context.function_items.add_item(FunctionNurseryItem { ident: ident.to_owned(), py_names, cfgs: args.cfgs.to_vec(), docs, - no_keywords, + keyword_dispatch, }); Ok(()) } @@ -1094,7 +1090,7 @@ impl ModuleItem for AttributeItem { struct FunctionItemMeta(ItemMetaInner); impl ItemMeta for FunctionItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["name", "no_keywords"]; + const ALLOWED_NAMES: &'static [&'static str] = &["name"]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 6a14dd11688..04755568e3d 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -936,3 +936,19 @@ pub(crate) fn internal_doc_tokens( } } } + +pub(crate) fn keyword_dispatch_tokens( + sig: &Signature, + implicit_self: Option<&str>, + self_ty: Option<&Type>, +) -> TokenStream { + let args = args_const(&sig_pieces(sig, implicit_self, self_ty, None)); + quote! { + { + #args + const POLICY: ::rustpython_vm::function::KeywordDispatch = + ::rustpython_vm::function::keyword_dispatch(ARGS); + POLICY + } + } +} diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 143368c1c1e..dbd40b69c76 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -420,7 +420,7 @@ impl PyList { flags(BASETYPE, SEQUENCE, _MATCH_SELF) )] impl Py { - #[pymethod(no_keywords)] + #[pymethod] pub(crate) fn append(&self, object: PyObjectRef) { self.payload.append(object) } @@ -497,7 +497,7 @@ impl Py { elements.insert(index, object); } - #[pymethod(no_keywords)] + #[pymethod] fn clear(&self) { let removed = core::mem::take(self.borrow_vec_mut().deref_mut()); removed.into_iter().rev().for_each(drop); diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index 0cff2ff17e2..dfaa4b4372e 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -40,7 +40,8 @@ pub use number::{ pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence}; pub use signature::{ DefaultRepr, Param, ParamKind, SigArg, choose_class_params, has_signature, internal_doc_bytes, - internal_doc_len, real_signature, signature_prefix_bytes, signature_prefix_len, + internal_doc_len, keyword_dispatch, real_signature, signature_prefix_bytes, + signature_prefix_len, }; pub use time::TimeoutSeconds; diff --git a/crates/vm/src/function/signature.rs b/crates/vm/src/function/signature.rs index 74cf058d007..ccbd3228597 100644 --- a/crates/vm/src/function/signature.rs +++ b/crates/vm/src/function/signature.rs @@ -210,6 +210,38 @@ const fn count_param( } } +/// Determine keyword dispatch from each argument's binding metadata. +/// +/// Inspect arguments separately: a tuple's `PARAMS` can erase leaf metadata. +#[must_use] +pub const fn keyword_dispatch(args: &[SigArg]) -> super::KeywordDispatch { + let mut i = 0; + while i < args.len() { + if let Some(params) = args[i].params + && params_accept_keywords(params) + { + return super::KeywordDispatch::PassToBinder; + } + i += 1; + } + super::KeywordDispatch::RejectNonempty +} + +const fn params_accept_keywords(params: &[Param]) -> bool { + let mut i = 0; + while i < params.len() { + match params[i].kind { + ParamKind::PositionalOrKeyword | ParamKind::KeywordOnly | ParamKind::VarKeyword => { + return true; + } + ParamKind::Flatten(Some(inner)) if params_accept_keywords(inner) => return true, + _ => {} + } + i += 1; + } + false +} + const fn name_eq(name: &str, bytes: &[u8]) -> bool { let got = name.as_bytes(); if got.len() != bytes.len() { diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 61dd3529211..65f64299443 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -39,12 +39,12 @@ mod builtins { const CODEGEN_NOT_SUPPORTED: &str = "can't compile() to bytecode when the `codegen` feature of rustpython is disabled"; - #[pyfunction(no_keywords)] + #[pyfunction] fn abs(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm._abs(&x) } - #[pyfunction(no_keywords)] + #[pyfunction] fn all(iterable: ArgIterable, vm: &VirtualMachine) -> PyResult { for item in iterable.iter(vm)? { if !item?.into_bool() { @@ -54,7 +54,7 @@ mod builtins { Ok(true) } - #[pyfunction(no_keywords)] + #[pyfunction] fn any(iterable: ArgIterable, vm: &VirtualMachine) -> PyResult { for item in iterable.iter(vm)? { if item?.into_bool() { @@ -64,12 +64,12 @@ mod builtins { Ok(false) } - #[pyfunction(no_keywords)] + #[pyfunction] pub fn ascii(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.ascii(vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn bin(number: ArgIndex) -> String { let number = number.into_int_ref(); let x = number.as_bigint(); @@ -80,12 +80,12 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn callable(obj: PyObjectRef) -> bool { obj.is_callable() } - #[pyfunction(no_keywords)] + #[pyfunction] fn chr(i: ArgIndex, vm: &VirtualMachine) -> PyResult { let i = i.into_int_ref(); let value = i @@ -375,7 +375,7 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn delattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let attr = name.try_to_ref::(vm).map_err(|_e| { vm.new_type_error(format!( @@ -386,12 +386,12 @@ mod builtins { obj.del_attr(attr, vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn dir(obj: OptionalArg, vm: &VirtualMachine) -> PyResult { vm.dir(obj.into_option()) } - #[pyfunction(no_keywords)] + #[pyfunction] fn divmod(x: PyObjectRef, y: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm._divmod(&x, &y) } @@ -673,12 +673,12 @@ mod builtins { prompt: OptionalArg, } - #[pyfunction(no_keywords)] + #[pyfunction] fn format(args: FormatArgs, vm: &VirtualMachine) -> PyResult { vm.format(&args.value, args.format_spec) } - #[pyfunction(no_keywords)] + #[pyfunction] fn getattr( obj: PyObjectRef, attr: PyObjectRef, @@ -699,12 +699,12 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn globals(vm: &VirtualMachine) -> PyDictRef { vm.current_globals() } - #[pyfunction(no_keywords)] + #[pyfunction] fn hasattr(obj: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine) -> PyResult { let attr = name.try_to_ref::(vm).map_err(|_e| { vm.new_type_error(format!( @@ -715,7 +715,7 @@ mod builtins { obj.has_attr(attr, vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn hash(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.hash(vm) } @@ -739,19 +739,19 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn hex(number: ArgIndex) -> String { let number = number.into_int_ref(); let n = number.as_bigint(); format!("{n:#x}") } - #[pyfunction(no_keywords)] + #[pyfunction] fn id(obj: PyObjectRef) -> usize { obj.get_id() } - #[pyfunction(no_keywords)] + #[pyfunction] fn input(args: InputArgs, vm: &VirtualMachine) -> PyResult { let prompt = args.prompt; use std::io::IsTerminal; @@ -838,7 +838,7 @@ mod builtins { false } - #[pyfunction(no_keywords)] + #[pyfunction] fn isinstance( obj: PyObjectRef, class_or_tuple: PyObjectRef, @@ -847,7 +847,7 @@ mod builtins { obj.is_instance(&class_or_tuple, vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn issubclass( cls: PyObjectRef, class_or_tuple: PyObjectRef, @@ -856,7 +856,7 @@ mod builtins { cls.is_subclass(&class_or_tuple, vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn iter( iter_target: PyObjectRef, sentinel: OptionalArg, @@ -873,12 +873,12 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn aiter(async_iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult { async_iterable.get_aiter(vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn anext( aiter: PyObjectRef, default_value: OptionalArg, @@ -905,12 +905,12 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn len(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.length(vm) } - #[pyfunction(no_keywords)] + #[pyfunction] fn locals(vm: &VirtualMachine) -> PyResult { vm.current_locals() } @@ -985,7 +985,7 @@ mod builtins { min_or_max(args, vm, "min", PyComparisonOp::Lt) } - #[pyfunction(no_keywords)] + #[pyfunction] fn next( iterator: PyObjectRef, default_value: OptionalArg, @@ -1007,7 +1007,7 @@ mod builtins { }) } - #[pyfunction(no_keywords)] + #[pyfunction] fn oct(number: ArgIndex, vm: &VirtualMachine) -> PyObjectRef { let number = number.into_int_ref(); let n = number.as_bigint(); @@ -1020,7 +1020,7 @@ mod builtins { vm.ctx.new_str(s).into() } - #[pyfunction(no_keywords)] + #[pyfunction] // builtin_ord fn ord(character: PyObjectRef, vm: &VirtualMachine) -> PyResult { let bytes = if let Some(string) = character.downcast_ref::() { @@ -1124,7 +1124,7 @@ mod builtins { Ok(()) } - #[pyfunction(no_keywords)] + #[pyfunction] fn repr(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { obj.repr(vm) } @@ -1173,7 +1173,7 @@ mod builtins { } } - #[pyfunction(no_keywords)] + #[pyfunction] fn setattr( obj: PyObjectRef, name: PyObjectRef, @@ -1266,7 +1266,7 @@ mod builtins { ) } - #[pyfunction(no_keywords)] + #[pyfunction] fn vars(obj: OptionalArg, vm: &VirtualMachine) -> PyResult { if let OptionalArg::Present(obj) = obj { obj.get_attr(identifier!(vm, __dict__), vm) diff --git a/extra_tests/snippets/builtin_keyword_inference.py b/extra_tests/snippets/builtin_keyword_inference.py new file mode 100644 index 00000000000..7b386395476 --- /dev/null +++ b/extra_tests/snippets/builtin_keyword_inference.py @@ -0,0 +1,38 @@ +# Keyword metadata must forward raw arguments without changing body validation. +for choose in (min, max): + assert choose([], default=42) == 42 + assert choose([1, 2], key=lambda value: -value) == (2 if choose is min else 1) + try: + choose(bogus=1) + except TypeError as exc: + assert str(exc) == f"{choose.__name__} expected at least 1 argument, got 0" + else: + raise AssertionError("missing positional argument was accepted") + try: + choose(1, 2, default=0) + except TypeError as exc: + assert str(exc) == ( + f"Cannot specify a default for {choose.__name__}() " + "with multiple positional arguments" + ) + else: + raise AssertionError("default with multiple arguments was accepted") + +# Named fields in argument structs must override positional-only arguments. +assert eval("value", globals={"value": 42}) == 42 +namespace = {} +exec("value = 42", globals=namespace) +assert namespace["value"] == 42 +assert compile(source="42", filename="", mode="eval") +assert __import__(name="sys").__name__ == "sys" + +from io import StringIO + +output = StringIO() +print(1, 2, sep=":", end="!", file=output) +assert output.getvalue() == "1:2!" + +# Methods using a keyword-only argument struct must keep accepting keywords. +values = [1, 2] +values.sort(reverse=True) +assert values == [2, 1] From f4cc07febb1b50a2237aef463448e711b1e77433 Mon Sep 17 00:00:00 2001 From: "widehyo@gmail.com" Date: Tue, 6 Oct 2026 20:44:27 +0900 Subject: [PATCH 3/4] Simplify native function registration metadata Reuse SimpleItemMeta for pyfunction attributes and remove FunctionItemMeta. Remove MethodDefSpec and restore the original new_method_def arguments with an additional keyword_dispatch parameter, updating its callers. Consolidate keyword dispatch regressions in syntax_function_args.py, preserving their assertions and placing them before builtin shadowing. Validation: workspace and C-API tests/clippy, VM without doc, the combined snippet, and test_extcall/test_builtin/test_list/test_call passed. The full snippet suite reported 477 passes and seven existing failures: six CPython-side failures and one missing optional SQLite module. Assisted-by: Codex:GPT-6 --- crates/capi/src/methodobject.rs | 24 +++--- crates/derive-impl/src/pymodule.rs | 20 +---- crates/vm/src/function/method.rs | 21 ----- crates/vm/src/function/mod.rs | 2 +- crates/vm/src/macros.rs | 9 +-- crates/vm/src/vm/context.rs | 11 +-- crates/vm/src/vm/vm_new.rs | 18 ++--- .../snippets/builtin_keyword_dispatch.py | 41 ---------- .../snippets/builtin_keyword_inference.py | 38 --------- extra_tests/snippets/syntax_function_args.py | 81 +++++++++++++++++++ 10 files changed, 110 insertions(+), 155 deletions(-) delete mode 100644 extra_tests/snippets/builtin_keyword_dispatch.py delete mode 100644 extra_tests/snippets/builtin_keyword_inference.py diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 1a2c0fa1f1f..718a1ef6119 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -6,7 +6,7 @@ use crate::util::{CStrExt, FfiPtrExt}; use core::ffi::{c_char, c_int}; use core::fmt::Debug; use rustpython_vm::function::{ - FuncArgs, HeapMethodDef, ItemDoc, KeywordDispatch, MethodDefSpec, PosArgs, PyMethodFlags, + FuncArgs, HeapMethodDef, ItemDoc, KeywordDispatch, PosArgs, PyMethodFlags, }; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; @@ -89,12 +89,6 @@ pub(crate) fn build_method_def( } else { KeywordDispatch::RejectNonempty }; - let spec = MethodDefSpec { - name, - flags, - doc, - keyword_dispatch, - }; let has_self = has_self && !flags.contains(PyMethodFlags::STATIC); let method = ml.ml_meth; @@ -117,38 +111,38 @@ pub(crate) fn build_method_def( let f = method.PyCFunction; f(zelf.as_raw().cast_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) } else { let callable = move |vm: &VirtualMachine| unsafe { let f = method.PyCFunction; f(core::ptr::null_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) } }, PyMethodFlags::VARARGS => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_function(vm, method, flags, Some(args)) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) }, PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { let callable = move | args: FuncArgs, vm: &VirtualMachine| unsafe { call_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) }, PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS => { let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { call_fast_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) }, PyMethodFlags::FASTCALL => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_fast_function(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) }, PyMethodFlags::O => { let f = unsafe { method.PyCFunction }; @@ -156,12 +150,12 @@ pub(crate) fn build_method_def( let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) } else { let callable = move |arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(core::ptr::null_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(callable, spec)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) } }, _ => { diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 8f60550f0ba..10384885a2d 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -2,8 +2,8 @@ use crate::error::Diagnostic; use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, - ErrorVec, ItemMeta, ItemMetaInner, ItemNursery, ModuleItemMeta, SimpleItemMeta, - internal_doc_tokens, iter_use_idents, keyword_dispatch_tokens, pyclass_ident_and_attrs, + ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, internal_doc_tokens, + iter_use_idents, keyword_dispatch_tokens, pyclass_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; @@ -672,7 +672,7 @@ impl ModuleItem for FunctionItem { let ident = &func.sig().ident; let item_attr = args.attrs.remove(self.index()); - let item_meta = FunctionItemMeta::from_attr(ident.clone(), &item_attr)?; + let item_meta = SimpleItemMeta::from_attr(ident.clone(), &item_attr)?; let py_name = item_meta.simple_name()?; let mut py_names = vec![py_name]; @@ -1086,17 +1086,3 @@ impl ModuleItem for AttributeItem { Ok(()) } } - -struct FunctionItemMeta(ItemMetaInner); - -impl ItemMeta for FunctionItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["name"]; - - fn from_inner(inner: ItemMetaInner) -> Self { - Self(inner) - } - - fn inner(&self) -> &ItemMetaInner { - &self.0 - } -} diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index 886fee3b8a1..a6dedf8dcc3 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -98,27 +98,6 @@ pub enum KeywordDispatch { RejectNonemptyUnqualified, } -/// Registration metadata shared by dynamically created native methods. -#[derive(Clone, Copy, Debug)] -pub struct MethodDefSpec { - pub name: &'static str, - pub flags: PyMethodFlags, - pub doc: super::ItemDoc, - pub keyword_dispatch: KeywordDispatch, -} - -impl MethodDefSpec { - #[must_use] - pub const fn new(name: &'static str, flags: PyMethodFlags, doc: super::ItemDoc) -> Self { - Self { - name, - flags, - doc, - keyword_dispatch: KeywordDispatch::PassToBinder, - } - } -} - #[derive(Clone)] pub struct PyMethodDef { pub name: &'static str, // TODO: interned diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index dfaa4b4372e..ae3768ae239 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -33,7 +33,7 @@ pub use fspath::FsPath; pub(crate) use getargs::ArgSpec; pub use getset::PySetterValue; pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc}; -pub use method::{HeapMethodDef, KeywordDispatch, MethodDefSpec, PyMethodDef, PyMethodFlags}; +pub use method::{HeapMethodDef, KeywordDispatch, PyMethodDef, PyMethodFlags}; pub use number::{ ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgStrictInt, PySize, PySsize, }; diff --git a/crates/vm/src/macros.rs b/crates/vm/src/macros.rs index 65d7921117c..2b13f0a5d75 100644 --- a/crates/vm/src/macros.rs +++ b/crates/vm/src/macros.rs @@ -245,12 +245,11 @@ macro_rules! named_function { let ctx: &$crate::Context = &$ctx; $crate::__exports::paste::expr! { ctx.new_method_def( + stringify!($func), [<$module _ $func>], - ::rustpython_vm::function::MethodDefSpec::new( - stringify!($func), - ::rustpython_vm::function::PyMethodFlags::empty(), - ::rustpython_vm::function::ItemDoc::NONE, - ), + ::rustpython_vm::function::PyMethodFlags::empty(), + ::rustpython_vm::function::ItemDoc::NONE, + ::rustpython_vm::function::KeywordDispatch::PassToBinder, ) .to_function() .with_module(ctx.intern_str(stringify!($module)).into()) diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index a81e83edcaa..9ea2456ad44 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -651,18 +651,15 @@ impl Context { pub fn new_method_def( &self, + name: &'static str, f: F, - spec: crate::function::MethodDefSpec, + flags: PyMethodFlags, + doc: ItemDoc, + keyword_dispatch: crate::function::KeywordDispatch, ) -> PyRef where F: IntoPyNativeFn, { - let crate::function::MethodDefSpec { - name, - flags, - doc, - keyword_dispatch, - } = spec; let def = PyMethodDef { name, func: Box::leak(Box::new(f.into_func())), diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 29b1b2363e3..8ad168d46e9 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -143,12 +143,11 @@ impl VirtualMachine { F: IntoPyNativeFn, { let def = self.ctx.new_method_def( + name, f, - crate::function::MethodDefSpec::new( - name, - PyMethodFlags::empty(), - crate::function::ItemDoc::NONE, - ), + PyMethodFlags::empty(), + crate::function::ItemDoc::NONE, + crate::function::KeywordDispatch::PassToBinder, ); def.build_function(self, None) } @@ -163,12 +162,11 @@ impl VirtualMachine { F: IntoPyNativeFn, { let def = self.ctx.new_method_def( + name, f, - crate::function::MethodDefSpec::new( - name, - PyMethodFlags::METHOD, - crate::function::ItemDoc::NONE, - ), + PyMethodFlags::METHOD, + crate::function::ItemDoc::NONE, + crate::function::KeywordDispatch::PassToBinder, ); def.build_method(class, self) } diff --git a/extra_tests/snippets/builtin_keyword_dispatch.py b/extra_tests/snippets/builtin_keyword_dispatch.py deleted file mode 100644 index 9e244837950..00000000000 --- a/extra_tests/snippets/builtin_keyword_dispatch.py +++ /dev/null @@ -1,41 +0,0 @@ -def error(call, expected): - try: - call() - except TypeError as exc: - assert str(exc) == expected, (str(exc), expected) - else: - raise AssertionError("expected TypeError") - - -error(lambda: id(obj=1), "id() takes no keyword arguments") -error(lambda: id(**{"obj": 1}), "id() takes no keyword arguments") -error(lambda: id(1, 2, obj=1), "id() takes no keyword arguments") -error(lambda: list.append(x=1), "unbound method list.append() needs an argument") -error(lambda: list.append(**{"x": 1}), "unbound method list.append() needs an argument") -error( - lambda: list.append(1, x=1), - "descriptor 'append' for 'list' objects doesn't apply to a 'int' object", -) -error(lambda: list.append([], x=1), "list.append() takes no keyword arguments") -error(lambda: list.append([], **{"x": 1}), "list.append() takes no keyword arguments") -error(lambda: [].append(x=1), "list.append() takes no keyword arguments") -error(lambda: [].clear(1, x=1), "list.clear() takes no keyword arguments") - - -class Index: - def __index__(self): - raise AssertionError("conversion ran before keyword check") - - -error(lambda: bin(Index(), x=1), "bin() takes no keyword arguments") -assert id(1, **{}) == id(1) -assert round(1.25, ndigits=1) == 1.2 -assert pow(2, exp=3) == 8 -assert sorted([2, 1], reverse=True) == [2, 1] -assert sum([1, 2], start=4) == 7 -values = [] -list.append(values, 1) -values.append(2) -assert values == [1, 2] -list.clear(values) -assert values == [] diff --git a/extra_tests/snippets/builtin_keyword_inference.py b/extra_tests/snippets/builtin_keyword_inference.py deleted file mode 100644 index 7b386395476..00000000000 --- a/extra_tests/snippets/builtin_keyword_inference.py +++ /dev/null @@ -1,38 +0,0 @@ -# Keyword metadata must forward raw arguments without changing body validation. -for choose in (min, max): - assert choose([], default=42) == 42 - assert choose([1, 2], key=lambda value: -value) == (2 if choose is min else 1) - try: - choose(bogus=1) - except TypeError as exc: - assert str(exc) == f"{choose.__name__} expected at least 1 argument, got 0" - else: - raise AssertionError("missing positional argument was accepted") - try: - choose(1, 2, default=0) - except TypeError as exc: - assert str(exc) == ( - f"Cannot specify a default for {choose.__name__}() " - "with multiple positional arguments" - ) - else: - raise AssertionError("default with multiple arguments was accepted") - -# Named fields in argument structs must override positional-only arguments. -assert eval("value", globals={"value": 42}) == 42 -namespace = {} -exec("value = 42", globals=namespace) -assert namespace["value"] == 42 -assert compile(source="42", filename="", mode="eval") -assert __import__(name="sys").__name__ == "sys" - -from io import StringIO - -output = StringIO() -print(1, 2, sep=":", end="!", file=output) -assert output.getvalue() == "1:2!" - -# Methods using a keyword-only argument struct must keep accepting keywords. -values = [1, 2] -values.sort(reverse=True) -assert values == [2, 1] diff --git a/extra_tests/snippets/syntax_function_args.py b/extra_tests/snippets/syntax_function_args.py index eb4f94b8a36..5c527ff3d98 100644 --- a/extra_tests/snippets/syntax_function_args.py +++ b/extra_tests/snippets/syntax_function_args.py @@ -1,6 +1,87 @@ from testutils import assert_raises +def error(call, expected): + try: + call() + except TypeError as exc: + assert str(exc) == expected, (str(exc), expected) + else: + raise AssertionError("expected TypeError") + + +error(lambda: id(obj=1), "id() takes no keyword arguments") +error(lambda: id(**{"obj": 1}), "id() takes no keyword arguments") +error(lambda: id(1, 2, obj=1), "id() takes no keyword arguments") +error(lambda: list.append(x=1), "unbound method list.append() needs an argument") +error(lambda: list.append(**{"x": 1}), "unbound method list.append() needs an argument") +error( + lambda: list.append(1, x=1), + "descriptor 'append' for 'list' objects doesn't apply to a 'int' object", +) +error(lambda: list.append([], x=1), "list.append() takes no keyword arguments") +error(lambda: list.append([], **{"x": 1}), "list.append() takes no keyword arguments") +error(lambda: [].append(x=1), "list.append() takes no keyword arguments") +error(lambda: [].clear(1, x=1), "list.clear() takes no keyword arguments") + + +class Index: + def __index__(self): + raise AssertionError("conversion ran before keyword check") + + +error(lambda: bin(Index(), x=1), "bin() takes no keyword arguments") +assert id(1, **{}) == id(1) +assert round(1.25, ndigits=1) == 1.2 +assert pow(2, exp=3) == 8 +assert sorted([2, 1], reverse=True) == [2, 1] +assert sum([1, 2], start=4) == 7 +values = [] +list.append(values, 1) +values.append(2) +assert values == [1, 2] +list.clear(values) +assert values == [] + +# Keyword metadata must forward raw arguments without changing body validation. +for choose in (min, max): + assert choose([], default=42) == 42 + assert choose([1, 2], key=lambda value: -value) == (2 if choose is min else 1) + try: + choose(bogus=1) + except TypeError as exc: + assert str(exc) == f"{choose.__name__} expected at least 1 argument, got 0" + else: + raise AssertionError("missing positional argument was accepted") + try: + choose(1, 2, default=0) + except TypeError as exc: + assert str(exc) == ( + f"Cannot specify a default for {choose.__name__}() " + "with multiple positional arguments" + ) + else: + raise AssertionError("default with multiple arguments was accepted") + +# Named fields in argument structs must override positional-only arguments. +assert eval("value", globals={"value": 42}) == 42 +namespace = {} +exec("value = 42", globals=namespace) +assert namespace["value"] == 42 +assert compile(source="42", filename="", mode="eval") +assert __import__(name="sys").__name__ == "sys" + +from io import StringIO + +output = StringIO() +print(1, 2, sep=":", end="!", file=output) +assert output.getvalue() == "1:2!" + +# Methods using a keyword-only argument struct must keep accepting keywords. +values = [1, 2] +values.sort(reverse=True) +assert values == [2, 1] + def sum(x, y): return x+y From d8469d3a574d1a7f168fc429927592499395853e Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 8 Oct 2026 11:59:14 +0900 Subject: [PATCH 4/4] Reject native keywords by calling convention flags Remove `KeywordDispatch`, the `keyword_dispatch` field of `PyMethodDef`, `PyMethodDef::with_keyword_dispatch`, `function::keyword_dispatch` and the derive macros' `keyword_dispatch_tokens`. Native functions and method descriptors reject nonempty keywords when `flags` lacks `KEYWORDS`, and report the unqualified name when `flags` contains `VARARGS`. `Context::new_method_def` takes `(name, f, flags, doc)` again. Add a snippet for `int.__new__` called with a keyword argument. Assisted-by: Claude Code:claude-opus-5-5 --- crates/capi/src/methodobject.rs | 27 ++++++----------- crates/derive-impl/src/pyclass.rs | 13 ++------ crates/derive-impl/src/pymodule.rs | 8 ++--- crates/derive-impl/src/util.rs | 16 ---------- crates/vm/src/builtins/builtin_func.rs | 11 +++---- crates/vm/src/builtins/descriptor.rs | 8 ++--- crates/vm/src/codecs.rs | 1 - crates/vm/src/function/method.rs | 24 --------------- crates/vm/src/function/mod.rs | 5 ++- crates/vm/src/function/signature.rs | 32 -------------------- crates/vm/src/macros.rs | 1 - crates/vm/src/vm/context.rs | 2 -- crates/vm/src/vm/vm_new.rs | 2 -- extra_tests/snippets/syntax_function_args.py | 3 ++ 14 files changed, 25 insertions(+), 128 deletions(-) diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 718a1ef6119..2e82c5e9daa 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -5,9 +5,7 @@ use crate::pystate::with_vm; use crate::util::{CStrExt, FfiPtrExt}; use core::ffi::{c_char, c_int}; use core::fmt::Debug; -use rustpython_vm::function::{ - FuncArgs, HeapMethodDef, ItemDoc, KeywordDispatch, PosArgs, PyMethodFlags, -}; +use rustpython_vm::function::{FuncArgs, HeapMethodDef, ItemDoc, PosArgs, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type); @@ -82,13 +80,6 @@ pub(crate) fn build_method_def( let flags = PyMethodFlags::from_bits(ml.ml_flags as u32) .ok_or_else(|| vm.new_system_error("PyMethodDef contains unknown flags"))?; - let keyword_dispatch = if flags.contains(PyMethodFlags::KEYWORDS) { - KeywordDispatch::PassToBinder - } else if flags.contains(PyMethodFlags::VARARGS) { - KeywordDispatch::RejectNonemptyUnqualified - } else { - KeywordDispatch::RejectNonempty - }; let has_self = has_self && !flags.contains(PyMethodFlags::STATIC); let method = ml.ml_meth; @@ -111,38 +102,38 @@ pub(crate) fn build_method_def( let f = method.PyCFunction; f(zelf.as_raw().cast_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) } else { let callable = move |vm: &VirtualMachine| unsafe { let f = method.PyCFunction; f(core::ptr::null_mut(), core::ptr::null_mut()).assume_owned_or_err(vm) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) } }, PyMethodFlags::VARARGS => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_function(vm, method, flags, Some(args)) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) }, PyMethodFlags::VARARGS | PyMethodFlags::KEYWORDS => { let callable = move | args: FuncArgs, vm: &VirtualMachine| unsafe { call_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) }, PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS => { let callable = move |args: FuncArgs, vm: &VirtualMachine| unsafe { call_fast_function_with_keywords(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) }, PyMethodFlags::FASTCALL => { let callable = move |args: PosArgs, vm: &VirtualMachine| unsafe { call_fast_function(vm, method, flags, args) }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) }, PyMethodFlags::O => { let f = unsafe { method.PyCFunction }; @@ -150,12 +141,12 @@ pub(crate) fn build_method_def( let callable = move |zelf: PyObjectRef, arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(zelf.as_raw().cast_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) } else { let callable = move |arg: PyObjectRef, vm: &VirtualMachine| -> PyResult { unsafe { f(core::ptr::null_mut(), arg.as_raw().cast_mut()).assume_owned_or_err(vm) } }; - Ok(vm.ctx.new_method_def(name, callable, flags, doc, keyword_dispatch)) + Ok(vm.ctx.new_method_def(name, callable, flags, doc)) } }, _ => { diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index 804a0e3adaf..b935420591f 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -2,7 +2,7 @@ use super::Diagnostic; use crate::util::{ ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta, ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, internal_doc_tokens, - keyword_dispatch_tokens, pyclass_ident_and_attrs, pyexception_ident_and_attrs, + pyclass_ident_and_attrs, pyexception_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree}; @@ -1307,12 +1307,6 @@ where _ => None, } }; - let keyword_dispatch = keyword_dispatch_tokens( - func.sig(), - implicit_self, - args.context.self_ty_subst.as_ref(), - ); - // Add #[allow(non_snake_case)] for setter methods like set___name__ let method_name = ident.to_string(); if method_name.starts_with("set_") && method_name.contains("__") { @@ -1342,7 +1336,6 @@ where doc, doc_body_pending, raw, - keyword_dispatch, coexist, attr_name: self.inner.attr_name, }); @@ -1537,7 +1530,6 @@ struct MethodNurseryItem { cfgs: Vec, ident: Ident, raw: bool, - keyword_dispatch: TokenStream, coexist: bool, doc: TokenStream, doc_body_pending: TokenStream, @@ -1569,7 +1561,6 @@ impl ToTokens for MethodNursery { let cfgs = &item.cfgs; let doc = &item.doc; let doc_body_pending = &item.doc_body_pending; - let keyword_dispatch = &item.keyword_dispatch; let binding_flags = match &item.attr_name { AttrName::Method => { quote! { rustpython_vm::function::PyMethodFlags::METHOD } @@ -1612,7 +1603,7 @@ impl ToTokens for MethodNursery { Self::#ident, #flags, #doc, - ).with_keyword_dispatch(#keyword_dispatch); + ); ::rustpython_vm::__cfg_doc!({ def.doc_body_pending = #doc_body_pending; } else {}); diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 10384885a2d..9a2ab6878e7 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -3,7 +3,7 @@ use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, internal_doc_tokens, - iter_use_idents, keyword_dispatch_tokens, pyclass_ident_and_attrs, + iter_use_idents, pyclass_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; @@ -536,7 +536,6 @@ struct FunctionNurseryItem { ident: Ident, /// One internal doc per [`py_names`](Self::py_names) entry. docs: Vec, - keyword_dispatch: TokenStream, } impl FunctionNursery { @@ -566,7 +565,6 @@ impl ToTokens for ValidatedFunctionNursery { let ident = &item.ident; let cfgs = &item.cfgs; let cfgs = quote!(#(#cfgs)*); - let keyword_dispatch = &item.keyword_dispatch; for (py_name, doc) in item.py_names.iter().zip(&item.docs) { inner_tokens.extend(quote![ #cfgs @@ -575,7 +573,7 @@ impl ToTokens for ValidatedFunctionNursery { #ident, rustpython_vm::function::PyMethodFlags::empty(), #doc, - ).with_keyword_dispatch(#keyword_dispatch), + ), ]); } } @@ -711,14 +709,12 @@ impl ModuleItem for FunctionItem { internal_doc_tokens(func.sig(), py_name, None, doc, None, Some("$module")) }) .collect(); - let keyword_dispatch = keyword_dispatch_tokens(func.sig(), None, None); args.context.function_items.add_item(FunctionNurseryItem { ident: ident.to_owned(), py_names, cfgs: args.cfgs.to_vec(), docs, - keyword_dispatch, }); Ok(()) } diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 04755568e3d..6a14dd11688 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -936,19 +936,3 @@ pub(crate) fn internal_doc_tokens( } } } - -pub(crate) fn keyword_dispatch_tokens( - sig: &Signature, - implicit_self: Option<&str>, - self_ty: Option<&Type>, -) -> TokenStream { - let args = args_const(&sig_pieces(sig, implicit_self, self_ty, None)); - quote! { - { - #args - const POLICY: ::rustpython_vm::function::KeywordDispatch = - ::rustpython_vm::function::keyword_dispatch(ARGS); - POLICY - } - } -} diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index b394d27df87..e0399987049 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -4,10 +4,7 @@ use crate::{ class::PyClassImpl, common::wtf8::Wtf8, convert::TryFromObject, - function::{ - Callee, FuncArgs, KeywordDispatch, PyComparisonValue, PyMethodDef, PyMethodFlags, - PyNativeFn, - }, + function::{Callee, FuncArgs, PyComparisonValue, PyMethodDef, PyMethodFlags, PyNativeFn}, types::{Callable, Comparable, PyComparisonOp, Representable}, }; use alloc::fmt; @@ -90,7 +87,7 @@ impl Callable for PyNativeFunction { type Args = FuncArgs; #[inline] fn call(zelf: &Py, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { - if !args.kwargs.is_empty() && zelf.value.keyword_dispatch != KeywordDispatch::PassToBinder { + if !args.kwargs.is_empty() && !zelf.value.flags.contains(PyMethodFlags::KEYWORDS) { return Err(native_no_keywords_error(zelf, vm)?); } let mut callee = Callee::named(zelf.value.name); @@ -279,7 +276,7 @@ fn vectorcall_native_function( let zelf: &Py = zelf_obj.downcast_ref().unwrap(); if kwnames.is_some_and(|names| !names.is_empty()) - && zelf.value.keyword_dispatch != KeywordDispatch::PassToBinder + && !zelf.value.flags.contains(PyMethodFlags::KEYWORDS) { return Err(native_no_keywords_error(zelf, vm)?); } @@ -309,7 +306,7 @@ fn native_no_keywords_error( zelf: &Py, vm: &VirtualMachine, ) -> PyResult { - if zelf.value.keyword_dispatch == KeywordDispatch::RejectNonemptyUnqualified { + if zelf.value.flags.contains(PyMethodFlags::VARARGS) { return Ok(vm.new_type_error(format!("{}() takes no keyword arguments", zelf.value.name))); } let qualname = PyNativeFunction::__qualname__(NativeFunctionOrMethod(zelf.to_owned()), vm)?; diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index ac10a21d355..55c2275f5bb 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -6,8 +6,7 @@ use crate::{ common::hash::PyHash, convert::{ToPyObject, ToPyResult}, function::{ - Callee, FuncArgs, ItemDoc, KeywordDispatch, PyMethodDef, PyMethodFlags, PySetterValue, - PySsize, plain_doc, + Callee, FuncArgs, ItemDoc, PyMethodDef, PyMethodFlags, PySetterValue, PySsize, plain_doc, }, protocol::{PyNumberBinaryFunc, PyNumberTernaryFunc, PyNumberUnaryFunc}, types::{ @@ -114,8 +113,7 @@ impl Callable for PyMethodDescriptor { #[inline] fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { method_descr_check_self(zelf, args.args.first().map(|obj| &**obj), vm)?; - if !args.kwargs.is_empty() && zelf.method.keyword_dispatch != KeywordDispatch::PassToBinder - { + if !args.kwargs.is_empty() && !zelf.method.flags.contains(PyMethodFlags::KEYWORDS) { return Err(method_descr_no_keywords_error(zelf, vm)); } (zelf.method.func)( @@ -1612,7 +1610,7 @@ fn vectorcall_method_descriptor( vm, )?; if kwnames.is_some_and(|names| !names.is_empty()) - && zelf.method.keyword_dispatch != KeywordDispatch::PassToBinder + && !zelf.method.flags.contains(PyMethodFlags::KEYWORDS) { return Err(method_descr_no_keywords_error(zelf, vm)); } diff --git a/crates/vm/src/codecs.rs b/crates/vm/src/codecs.rs index 807eaa7d822..587701d6528 100644 --- a/crates/vm/src/codecs.rs +++ b/crates/vm/src/codecs.rs @@ -177,7 +177,6 @@ impl CodecsRegistry { name: $name, func: crate::function::static_func($func), flags: crate::function::PyMethodFlags::O, - keyword_dispatch: crate::function::KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: DOC.offset, #[cfg(feature = "doc")] diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index a6dedf8dcc3..b33da287710 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -75,7 +75,6 @@ macro_rules! define_methods { name: $name, func: $crate::function::static_func($func), flags: $crate::function::PyMethodFlags::$flags, - keyword_dispatch: $crate::function::KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: 0, #[cfg(feature = "doc")] @@ -87,23 +86,11 @@ macro_rules! define_methods { }; } -/// Keyword acceptance is independent of the flags inferred from a Rust signature. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(u8)] -pub enum KeywordDispatch { - #[default] - PassToBinder, - RejectNonempty, - /// CPython's legacy METH_VARARGS function reports only ml_name. - RejectNonemptyUnqualified, -} - #[derive(Clone)] pub struct PyMethodDef { pub name: &'static str, // TODO: interned pub func: &'static dyn PyNativeFn, pub flags: PyMethodFlags, - pub keyword_dispatch: KeywordDispatch, /// Start of the database body. Read only when `doc_len != 0`. /// Absent when the `doc` feature is off, as are `doc_len` and `doc_body_pending`. #[cfg(feature = "doc")] @@ -128,12 +115,6 @@ pub struct PyMethodDef { } impl PyMethodDef { - #[must_use] - pub const fn with_keyword_dispatch(mut self, policy: KeywordDispatch) -> Self { - self.keyword_dispatch = policy; - self - } - #[must_use] pub fn item_doc(&self) -> super::ItemDoc { super::ItemDoc { @@ -160,7 +141,6 @@ impl PyMethodDef { name, func: super::static_func(func), flags: flags.with_call_convention(F::ARGS), - keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] @@ -182,7 +162,6 @@ impl PyMethodDef { name, func: super::static_raw_func(func), flags: flags.with_call_convention(&[SigArg::from_arg::("args")]), - keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] @@ -344,7 +323,6 @@ impl PyMethodDef { name: "", func: &|_, _, _| unreachable!(), flags: PyMethodFlags::empty(), - keyword_dispatch: KeywordDispatch::PassToBinder, #[cfg(feature = "doc")] doc_off: 0, #[cfg(feature = "doc")] @@ -375,7 +353,6 @@ impl PyMethodDef { name: self.name, func: self.func, flags: self.flags, - keyword_dispatch: self.keyword_dispatch, #[cfg(feature = "doc")] doc_off: self.doc_off, #[cfg(feature = "doc")] @@ -398,7 +375,6 @@ impl core::fmt::Debug for PyMethodDef { }), ) .field("flags", &self.flags) - .field("keyword_dispatch", &self.keyword_dispatch) .field("doc", &self.doc) .finish() } diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index ae3768ae239..707afed0966 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -33,15 +33,14 @@ pub use fspath::FsPath; pub(crate) use getargs::ArgSpec; pub use getset::PySetterValue; pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc}; -pub use method::{HeapMethodDef, KeywordDispatch, PyMethodDef, PyMethodFlags}; +pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags}; pub use number::{ ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgStrictInt, PySize, PySsize, }; pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence}; pub use signature::{ DefaultRepr, Param, ParamKind, SigArg, choose_class_params, has_signature, internal_doc_bytes, - internal_doc_len, keyword_dispatch, real_signature, signature_prefix_bytes, - signature_prefix_len, + internal_doc_len, real_signature, signature_prefix_bytes, signature_prefix_len, }; pub use time::TimeoutSeconds; diff --git a/crates/vm/src/function/signature.rs b/crates/vm/src/function/signature.rs index ccbd3228597..74cf058d007 100644 --- a/crates/vm/src/function/signature.rs +++ b/crates/vm/src/function/signature.rs @@ -210,38 +210,6 @@ const fn count_param( } } -/// Determine keyword dispatch from each argument's binding metadata. -/// -/// Inspect arguments separately: a tuple's `PARAMS` can erase leaf metadata. -#[must_use] -pub const fn keyword_dispatch(args: &[SigArg]) -> super::KeywordDispatch { - let mut i = 0; - while i < args.len() { - if let Some(params) = args[i].params - && params_accept_keywords(params) - { - return super::KeywordDispatch::PassToBinder; - } - i += 1; - } - super::KeywordDispatch::RejectNonempty -} - -const fn params_accept_keywords(params: &[Param]) -> bool { - let mut i = 0; - while i < params.len() { - match params[i].kind { - ParamKind::PositionalOrKeyword | ParamKind::KeywordOnly | ParamKind::VarKeyword => { - return true; - } - ParamKind::Flatten(Some(inner)) if params_accept_keywords(inner) => return true, - _ => {} - } - i += 1; - } - false -} - const fn name_eq(name: &str, bytes: &[u8]) -> bool { let got = name.as_bytes(); if got.len() != bytes.len() { diff --git a/crates/vm/src/macros.rs b/crates/vm/src/macros.rs index 2b13f0a5d75..340e5461571 100644 --- a/crates/vm/src/macros.rs +++ b/crates/vm/src/macros.rs @@ -249,7 +249,6 @@ macro_rules! named_function { [<$module _ $func>], ::rustpython_vm::function::PyMethodFlags::empty(), ::rustpython_vm::function::ItemDoc::NONE, - ::rustpython_vm::function::KeywordDispatch::PassToBinder, ) .to_function() .with_module(ctx.intern_str(stringify!($module)).into()) diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 9ea2456ad44..42da0551cae 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -655,7 +655,6 @@ impl Context { f: F, flags: PyMethodFlags, doc: ItemDoc, - keyword_dispatch: crate::function::KeywordDispatch, ) -> PyRef where F: IntoPyNativeFn, @@ -664,7 +663,6 @@ impl Context { name, func: Box::leak(Box::new(f.into_func())), flags: flags.with_call_convention(F::ARGS), - keyword_dispatch, #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 8ad168d46e9..1a1696b7106 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -147,7 +147,6 @@ impl VirtualMachine { f, PyMethodFlags::empty(), crate::function::ItemDoc::NONE, - crate::function::KeywordDispatch::PassToBinder, ); def.build_function(self, None) } @@ -166,7 +165,6 @@ impl VirtualMachine { f, PyMethodFlags::METHOD, crate::function::ItemDoc::NONE, - crate::function::KeywordDispatch::PassToBinder, ); def.build_method(class, self) } diff --git a/extra_tests/snippets/syntax_function_args.py b/extra_tests/snippets/syntax_function_args.py index 5c527ff3d98..96cbaf37272 100644 --- a/extra_tests/snippets/syntax_function_args.py +++ b/extra_tests/snippets/syntax_function_args.py @@ -82,6 +82,9 @@ def __index__(self): values.sort(reverse=True) assert values == [2, 1] +# Definitions built without the macros infer their calling convention too. +assert int.__new__(int, "11", base=2) == 3 + def sum(x, y): return x+y