Skip to content

Commit eaf6d73

Browse files
committed
Drive text signatures from FromArgs, not type names
Keyword-only fragments live on the FromArgs impl. The derive reads those consts instead of matching DirFd or FollowSymlinks by name. Assisted-by: Grok:grok-4.6
1 parent 75fdcf0 commit eaf6d73

15 files changed

Lines changed: 323 additions & 99 deletions

File tree

‎crates/derive-impl/src/from_args.rs‎

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
22
use quote::{ToTokens, quote};
33
use syn::ext::IdentExt;
44
use syn::meta::ParseNestedMeta;
5-
use syn::{Attribute, Data, DeriveInput, Expr, Field, Ident, Result, Token, parse_quote};
5+
use syn::{Attribute, Data, DeriveInput, Expr, Field, Ident, Lit, Result, Token, parse_quote};
66

77
/// The kind of the python parameter, this corresponds to the value of Parameter.kind
88
/// (https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind)
@@ -229,28 +229,104 @@ fn compute_arity_bounds(field_attrs: &[ArgAttribute]) -> (usize, usize) {
229229
(min_arity, max_arity)
230230
}
231231

232+
fn python_default_repr(default: Option<&DefaultValue>) -> Option<String> {
233+
match default {
234+
None => None,
235+
Some(None) => Some("None".to_owned()),
236+
Some(Some(expr)) => match expr {
237+
Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
238+
Lit::Bool(b) => Some(if b.value {
239+
"True".to_owned()
240+
} else {
241+
"False".to_owned()
242+
}),
243+
Lit::Int(i) => Some(i.base10_digits().to_owned()),
244+
Lit::Float(f) => Some(f.base10_digits().to_owned()),
245+
Lit::Str(s) => Some(format!("'{}'", s.value())),
246+
_ => Some("None".to_owned()),
247+
},
248+
_ => Some("None".to_owned()),
249+
},
250+
}
251+
}
252+
232253
pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
233-
let (fields, field_attrs) = match input.data {
234-
Data::Struct(syn::DataStruct { fields, .. }) => (
235-
fields
236-
.iter()
237-
.enumerate()
238-
.map(generate_field)
239-
.collect::<Result<TokenStream>>()?,
240-
fields
241-
.iter()
242-
.filter_map(|field| field.try_into().ok())
243-
.collect::<Vec<ArgAttribute>>(),
244-
),
254+
let struct_fields = match input.data {
255+
Data::Struct(syn::DataStruct { fields, .. }) => fields,
245256
_ => bail_span!(input, "FromArgs input must be a struct"),
246257
};
247258

259+
let generated_fields = struct_fields
260+
.iter()
261+
.enumerate()
262+
.map(generate_field)
263+
.collect::<Result<TokenStream>>()?;
264+
let field_attrs = struct_fields
265+
.iter()
266+
.filter_map(|field| field.try_into().ok())
267+
.collect::<Vec<ArgAttribute>>();
268+
248269
let (min_arity, max_arity) = compute_arity_bounds(&field_attrs);
249270

271+
let takes_keywords_named = field_attrs.iter().any(|attr| {
272+
matches!(
273+
attr.kind,
274+
ParameterKind::KeywordOnly | ParameterKind::PositionalOrKeyword
275+
)
276+
});
277+
let flatten_tys = struct_fields
278+
.iter()
279+
.zip(&field_attrs)
280+
.filter(|(_, attr)| attr.kind == ParameterKind::Flatten)
281+
.map(|(field, _)| field.ty.clone())
282+
.collect::<Vec<_>>();
283+
let takes_keywords = quote! {
284+
#takes_keywords_named #(|| <#flatten_tys as ::rustpython_vm::function::FromArgs>::TAKES_KEYWORDS)*
285+
};
286+
let variable_arity = {
287+
let from_bounds = min_arity != max_arity;
288+
quote! {
289+
#from_bounds #(|| <#flatten_tys as ::rustpython_vm::function::FromArgs>::VARIABLE_ARITY)*
290+
}
291+
};
292+
293+
let all_keyword_only = !field_attrs.is_empty()
294+
&& field_attrs
295+
.iter()
296+
.all(|attr| attr.kind == ParameterKind::KeywordOnly);
297+
let keyword_only_signature = if all_keyword_only {
298+
let mut parts = Vec::new();
299+
for (field, attr) in struct_fields.iter().zip(&field_attrs) {
300+
let Some(name) = attr
301+
.name
302+
.clone()
303+
.or_else(|| field.ident.as_ref().map(|ident| ident.unraw().to_string()))
304+
else {
305+
continue;
306+
};
307+
if let Some(default) = python_default_repr(attr.default.as_ref()) {
308+
parts.push(format!("{name}={default}"));
309+
} else {
310+
parts.push(name);
311+
}
312+
}
313+
let joined = parts.join(", ");
314+
quote!(Some(#joined))
315+
} else {
316+
quote!(None)
317+
};
318+
319+
let max_positional = if all_keyword_only { 0 } else { max_arity };
320+
250321
let name = input.ident;
251322
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
252323
let output = quote! {
253324
impl #impl_generics ::rustpython_vm::function::FromArgs for #name #ty_generics #where_clause {
325+
const TAKES_KEYWORDS: bool = #takes_keywords;
326+
const VARIABLE_ARITY: bool = #variable_arity;
327+
const MAX_POSITIONAL: usize = #max_positional;
328+
const KEYWORD_ONLY_SIGNATURE: Option<&'static str> = #keyword_only_signature;
329+
254330
fn arity() -> ::std::ops::RangeInclusive<usize> {
255331
#min_arity..=#max_arity
256332
}
@@ -259,7 +335,7 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
259335
vm: &::rustpython_vm::VirtualMachine,
260336
args: &mut ::rustpython_vm::function::FuncArgs
261337
) -> ::core::result::Result<Self, ::rustpython_vm::function::ArgumentError> {
262-
Ok(Self { #fields })
338+
Ok(Self { #generated_fields })
263339
}
264340
}
265341
};

‎crates/derive-impl/src/pyclass.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::Diagnostic;
22
use crate::util::{
33
ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta,
44
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, format_doc, infer_native_call_flags,
5-
pyclass_ident_and_attrs, pyexception_ident_and_attrs, text_signature,
5+
pyclass_ident_and_attrs, pyexception_ident_and_attrs, sig_parts_tokens, text_signature,
66
};
77
use core::str::FromStr;
88
use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
@@ -1125,6 +1125,12 @@ where
11251125
let drop_first_typed = usize::from(implicit_self.is_some());
11261126
let sig_doc = text_signature(func.sig(), &py_name, implicit_self);
11271127
let call_flags = infer_native_call_flags(func.sig(), drop_first_typed);
1128+
let sig_parts = sig_parts_tokens(func.sig(), implicit_self).unwrap_or_else(|| {
1129+
quote! {{
1130+
const __SIG_PARTS: &'static [rustpython_vm::function::SigPart] = &[];
1131+
__SIG_PARTS
1132+
}}
1133+
});
11281134

11291135
// Add #[allow(non_snake_case)] for setter methods like set___name__
11301136
let method_name = ident.to_string();
@@ -1147,6 +1153,7 @@ where
11471153
coexist,
11481154
attr_name: self.inner.attr_name,
11491155
call_flags,
1156+
sig_parts,
11501157
});
11511158
Ok(())
11521159
}
@@ -1376,6 +1383,7 @@ struct MethodNurseryItem {
13761383
doc: Option<String>,
13771384
attr_name: AttrName,
13781385
call_flags: TokenStream,
1386+
sig_parts: TokenStream,
13791387
}
13801388

13811389
impl MethodNursery {
@@ -1441,13 +1449,15 @@ impl ToTokens for MethodNursery {
14411449
} else {
14421450
quote!(new_const)
14431451
};
1452+
let sig_parts = &item.sig_parts;
14441453
inner_tokens.extend(quote! [
14451454
#(#cfgs)*
14461455
rustpython_vm::function::PyMethodDef::#method_new(
14471456
#py_name,
14481457
Self::#ident,
14491458
#flags,
14501459
#doc,
1460+
#sig_parts,
14511461
),
14521462
]);
14531463
}

‎crates/derive-impl/src/pymodule.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use crate::pystructseq::PyStructSequenceMeta;
33
use crate::util::{
44
ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner,
55
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, format_doc,
6-
infer_native_call_flags, iter_use_idents, pyclass_ident_and_attrs, text_signature,
6+
infer_native_call_flags, iter_use_idents, pyclass_ident_and_attrs, sig_parts_tokens,
7+
text_signature,
78
};
89
use core::str::FromStr;
910
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
@@ -526,6 +527,7 @@ struct FunctionNurseryItem {
526527
ident: Ident,
527528
doc: Option<String>,
528529
call_flags: TokenStream,
530+
sig_parts: TokenStream,
529531
}
530532

531533
impl FunctionNursery {
@@ -561,6 +563,7 @@ impl ToTokens for ValidatedFunctionNursery {
561563
None => quote!(None),
562564
};
563565
let flags = &item.call_flags;
566+
let sig_parts = &item.sig_parts;
564567

565568
inner_tokens.extend(quote![
566569
#(
@@ -570,6 +573,7 @@ impl ToTokens for ValidatedFunctionNursery {
570573
#ident,
571574
#flags,
572575
#doc,
576+
#sig_parts,
573577
),
574578
)*
575579
]);
@@ -665,6 +669,12 @@ impl ModuleItem for FunctionItem {
665669

666670
let py_name = item_meta.simple_name()?;
667671
let sig_doc = text_signature(func.sig(), &py_name, None);
672+
let sig_parts = sig_parts_tokens(func.sig(), None).unwrap_or_else(|| {
673+
quote! {{
674+
const __SIG_PARTS: &'static [rustpython_vm::function::SigPart] = &[];
675+
__SIG_PARTS
676+
}}
677+
});
668678

669679
let module = args.module_name();
670680
// TODO: doc must exist at least one of code or CPython
@@ -716,6 +726,7 @@ impl ModuleItem for FunctionItem {
716726
cfgs: args.cfgs.to_vec(),
717727
doc,
718728
call_flags,
729+
sig_parts,
719730
});
720731
Ok(())
721732
}

0 commit comments

Comments
 (0)