Visitar URL original
Reject unsupported native keywords before argument binding by widehyo1 · Pull Request #8957 · RustPython/RustPython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
  • Loading branch information
widehyo1 authored and youknowone committed Oct 8, 2026
commit 4575883f447fbfdb6314227d6fc96a2136391b59
21 changes: 11 additions & 10 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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("__") {
Expand Down Expand Up @@ -1337,7 +1342,7 @@ where
doc,
doc_body_pending,
raw,
no_keywords,
keyword_dispatch,
coexist,
attr_name: self.inner.attr_name,
});
Expand Down Expand Up @@ -1532,7 +1537,7 @@ struct MethodNurseryItem {
cfgs: Vec<Attribute>,
ident: Ident,
raw: bool,
no_keywords: bool,
keyword_dispatch: TokenStream,
coexist: bool,
doc: TokenStream,
doc_body_pending: TokenStream,
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 6 additions & 10 deletions crates/derive-impl/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -536,7 +536,7 @@ struct FunctionNurseryItem {
ident: Ident,
/// One internal doc per [`py_names`](Self::py_names) entry.
docs: Vec<TokenStream>,
no_keywords: bool,
keyword_dispatch: TokenStream,
}

impl FunctionNursery {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -1094,7 +1090,7 @@ impl ModuleItem for AttributeItem {
struct FunctionItemMeta(ItemMetaInner);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you still need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FunctionItemMeta was introduced to support the no_keywords marker. Now that the marker is gone, it is equivalent to SimpleItemMeta, so I’ll remove it.


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)
Expand Down
16 changes: 16 additions & 0 deletions crates/derive-impl/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
4 changes: 2 additions & 2 deletions crates/vm/src/builtins/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ impl PyList {
flags(BASETYPE, SEQUENCE, _MATCH_SELF)
)]
impl Py<PyList> {
#[pymethod(no_keywords)]
#[pymethod]
pub(crate) fn append(&self, object: PyObjectRef) {
self.payload.append(object)
}
Expand Down Expand Up @@ -497,7 +497,7 @@ impl Py<PyList> {
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);
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
32 changes: 32 additions & 0 deletions crates/vm/src/function/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading