use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{Data, DataStruct, DeriveInput, Expr, Fields, Ident, LitStr, Token, Type, spanned::Spanned};
pub(crate) fn expand(input: &DeriveInput) -> syn::Result<TokenStream> {
let signature = Signature::parse(input)?;
Ok(signature.render())
}
struct Signature {
struct_ident: Ident,
func_name: String,
style: Style,
fields: Vec<Field>,
varargs_idx: Option<usize>,
varkwargs_idx: Option<usize>,
at_most_total: bool,
vectorcall: bool,
kwarg_error_name: Option<String>,
bad_arg: Option<BadArgStyle>,
kwargs_not_supported_yet: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Style {
Def,
Clinic,
C,
CNamed,
Unpack,
ParseTuple,
}
#[derive(Clone, Copy)]
enum BadArgStyle {
Positional,
Named,
}
struct Field {
ident: Ident,
ty: Type,
kind: FieldKind,
default: Option<DefaultExpr>,
static_string: Option<Ident>,
pos_index: Option<usize>,
slot_index: Option<usize>,
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum FieldKind {
#[default]
PosOrKeyword,
PosOnly,
KwOnly,
Varargs,
Varkwargs,
}
pub(crate) enum DefaultExpr {
DefaultTrait,
Explicit(Box<Expr>),
}
impl Signature {
fn parse(input: &DeriveInput) -> syn::Result<Self> {
let DeriveInput {
ident: struct_ident,
data,
attrs,
..
} = input;
let Data::Struct(DataStruct {
fields: Fields::Named(named),
..
}) = data
else {
return Err(syn::Error::new(
input.span(),
"FromArgs can only be derived for structs with named fields",
));
};
let StructAttrs {
name: func_name,
style,
at_most_total,
vectorcall,
kwarg_error_name,
bad_arg,
kwargs_not_supported_yet,
} = parse_struct_attrs(attrs)?;
let mut fields = Vec::with_capacity(named.named.len());
for field in &named.named {
let opts = parse_field_attrs(&field.attrs)?;
let ident = field.ident.clone().expect("named field");
fields.push(Field {
ident,
ty: field.ty.clone(),
kind: opts.kind,
default: opts.default,
static_string: opts.static_string,
pos_index: None,
slot_index: None,
});
}
let (varargs_idx, varkwargs_idx) = resolve_field_roles(&mut fields)?;
let signature = Self {
struct_ident: struct_ident.clone(),
func_name,
style,
fields,
varargs_idx,
varkwargs_idx,
at_most_total,
vectorcall,
kwarg_error_name,
bad_arg,
kwargs_not_supported_yet,
};
signature.validate()?;
Ok(signature)
}
fn validate(&self) -> syn::Result<()> {
let err = |msg: &str| Err(syn::Error::new(self.struct_ident.span(), msg));
match self.style {
Style::Def => {
if self.bad_arg.is_some() {
return err("`bad_arg`/`bad_arg_named` cannot be combined with `style = def` \
— CPython `def` binding never type-checks while binding; declare \
fields as raw `Value` and coerce in the function body");
}
}
Style::Unpack | Style::ParseTuple => {
let style = if self.style == Style::Unpack {
"unpack"
} else {
"parse_tuple"
};
if self.fields.iter().any(|f| matches!(f.kind, FieldKind::PosOrKeyword)) {
return err(&format!(
"`style = {style}` models a positional-only signature \
— every positional field must be `pos_only`"
));
}
if self.varargs_idx.is_some() || self.varkwargs_idx.is_some() {
return err(&format!(
"`style = {style}` cannot be combined with `varargs` or `varkwargs` \
— it models a fixed positional min..max range"
));
}
}
Style::Clinic | Style::C | Style::CNamed => {}
}
if self.at_most_total {
if matches!(self.style, Style::Def | Style::Unpack | Style::ParseTuple) {
return err(
"`at_most_total` cannot be combined with `style = def`, `style = unpack` or \
`style = parse_tuple` — the total pre-count models \
`PyArg_ParseTupleAndKeywords`-family C parsers",
);
}
if self.varargs_idx.is_some() || self.varkwargs_idx.is_some() {
return err("`at_most_total` cannot be combined with `varargs` or `varkwargs` \
— the up-front total-count check is only meaningful for \
signatures with a fixed maximum");
}
}
if self.vectorcall && !(self.at_most_total && self.style == Style::Clinic) {
return err("`vectorcall` requires the default `clinic` style plus `at_most_total` \
— it models a `tp_vectorcall` fast path in front of a clinic parser");
}
if self.kwarg_error_name.is_some()
&& !matches!(
self.style,
Style::Def | Style::Clinic | Style::Unpack | Style::ParseTuple
)
{
return err("`kwarg_error_name` is only meaningful with `style = def`, the default \
`clinic` style, `style = unpack` or `style = parse_tuple` (where it names the \
function in the `takes no keyword arguments` error) — the C families defer \
unknown-kwarg errors past binding");
}
if self.kwargs_not_supported_yet {
if self.varkwargs_idx.is_some() {
return err("`kwargs_not_supported_yet` cannot be combined with `varkwargs` \
— the flag rejects every kwarg up front, so there's nothing to collect");
}
if self.fields.iter().any(|f| matches!(f.kind, FieldKind::KwOnly)) {
return err("`kwargs_not_supported_yet` cannot be combined with `kw_only` fields \
— the flag rejects every kwarg up front, so kw_only slots are unreachable");
}
if self.kwarg_error_name.is_some() {
return err("`kwargs_not_supported_yet` cannot be combined with `kwarg_error_name` \
— the override only applies to the unknown-kwarg dispatch path, which is skipped");
}
}
let mut seen_positional_default = false;
for field in &self.fields {
if !matches!(field.kind, FieldKind::PosOnly | FieldKind::PosOrKeyword) {
continue;
}
if field.default.is_some() {
seen_positional_default = true;
} else if seen_positional_default {
return Err(syn::Error::new(
field.ident.span(),
"required positional fields must come before positional fields with \
defaults — matching Python signatures, and relied on by the runtime \
binder's fast path",
));
}
}
if let Some(idx) = self.varargs_idx
&& !is_vec_of_value(&self.fields[idx].ty)
{
return Err(syn::Error::new(
self.fields[idx].ident.span(),
"`varargs` fields must be `Vec<Value>` — coerce elements in the function body",
));
}
if let Some(field) = self
.fields
.iter()
.find(|f| matches!(f.kind, FieldKind::KwOnly) && f.default.is_none())
{
return Err(syn::Error::new(
field.ident.span(),
"keyword-only fields must have a `default` — the runtime binder's fast \
paths skip the aggregated missing-keyword check, so a required \
keyword-only parameter would report the wrong error; extend the binder \
before allowing this",
));
}
for idx in [self.varargs_idx, self.varkwargs_idx].into_iter().flatten() {
let field = &self.fields[idx];
if field.default.is_some() || field.static_string.is_some() {
return Err(syn::Error::new(
field.ident.span(),
"`default` and `static_string` cannot be applied to `varargs` / \
`varkwargs` fields — they configure a named parameter slot, which \
collector fields don't own",
));
}
}
Ok(())
}
fn render(&self) -> TokenStream {
let struct_ident = &self.struct_ident;
let slots_struct_ident = format_ident!("__{}Slots", struct_ident);
let named: Vec<&Field> = self.named_fields().collect();
let n_slots = named.len();
let slot_idents: Vec<&Ident> = named.iter().map(|f| &f.ident).collect();
let slot_tys: Vec<&Type> = named.iter().map(|f| &f.ty).collect();
let spec = self.render_spec(&named);
let conversions = named.iter().map(|f| self.render_conversion(f));
let build_fields = self.fields.iter().map(render_build_field);
quote! {
struct #slots_struct_ident {
raw: crate::args::Bound<#n_slots>,
#(#slot_idents: ::std::option::Option<#slot_tys>,)*
}
#[automatically_derived]
impl<__C: crate::heap::ContainsHeap> crate::heap::DropWithContext<__C> for #slots_struct_ident {
fn drop_with(self, heap: &mut __C) {
crate::heap::DropWithContext::drop_with(self.raw, heap);
#(
if let ::std::option::Option::Some(__v) = self.#slot_idents {
<#slot_tys as crate::args::FromValue>::drop_extracted(__v, heap);
}
)*
}
}
#[automatically_derived]
impl #struct_ident {
pub(crate) fn from_args(
args: crate::args::ArgValues,
vm: &mut crate::bytecode::VM<'_>,
) -> crate::exception_private::RunResult<Self> {
#spec
let mut __guard = crate::heap::DropGuard::new(
#slots_struct_ident {
raw: crate::args::Bound::new(&__SPEC),
#(#slot_idents: ::std::option::Option::None,)*
},
vm,
);
let (__slots, vm) = __guard.as_parts_mut();
crate::args::bind::<#n_slots>(&__SPEC, &mut __slots.raw, args, vm)?;
#(#conversions)*
__slots.raw.finish()?;
::std::result::Result::Ok(Self {
#(#build_fields)*
})
}
}
}
}
fn render_spec(&self, named: &[&Field]) -> TokenStream {
let func_name = self.func_name.as_str();
let params = named.iter().map(|f| f.render_param(self.kwargs_not_supported_yet));
let family = self.render_family(named);
let n_positional = named
.iter()
.filter(|f| matches!(f.kind, FieldKind::PosOnly | FieldKind::PosOrKeyword))
.count();
let n_required_positional = named
.iter()
.filter(|f| matches!(f.kind, FieldKind::PosOnly | FieldKind::PosOrKeyword) && f.default.is_none())
.count();
let n_required_pos_only = named
.iter()
.filter(|f| matches!(f.kind, FieldKind::PosOnly) && f.default.is_none())
.count();
let varargs = self.varargs_idx.is_some();
let varkwargs = self.varkwargs_idx.is_some();
let at_most_total = self.at_most_total;
let vectorcall = self.vectorcall;
let kwargs_not_supported_yet = self.kwargs_not_supported_yet;
let kwarg_error_name = if let Some(name) = &self.kwarg_error_name {
quote! { ::std::option::Option::Some(#name) }
} else {
quote! { ::std::option::Option::None }
};
quote! {
static __SPEC: crate::args::ParamSpec = crate::args::ParamSpec {
func_name: #func_name,
family: #family,
params: &[#(#params),*],
n_positional: #n_positional,
n_required_positional: #n_required_positional,
n_required_pos_only: #n_required_pos_only,
varargs: #varargs,
varkwargs: #varkwargs,
at_most_total: #at_most_total,
vectorcall: #vectorcall,
kwargs_not_supported_yet: #kwargs_not_supported_yet,
kwarg_error_name: #kwarg_error_name,
};
}
}
fn render_family(&self, named: &[&Field]) -> TokenStream {
let pivot = named.iter().any(|f| matches!(f.kind, FieldKind::KwOnly));
match self.style {
Style::Def => quote! { crate::args::ErrorFamily::Def },
Style::Clinic => quote! { crate::args::ErrorFamily::Clinic },
Style::C => quote! { crate::args::ErrorFamily::C { positional_pivot: #pivot } },
Style::CNamed => quote! { crate::args::ErrorFamily::CNamed { positional_pivot: #pivot } },
Style::Unpack => quote! { crate::args::ErrorFamily::Unpack },
Style::ParseTuple => quote! { crate::args::ErrorFamily::ParseTuple },
}
}
fn render_conversion(&self, field: &Field) -> TokenStream {
let ident = &field.ident;
let ty = &field.ty;
let slot_index = field.slot_index.expect("named field has a slot");
let ctx = self.render_err_ctx(field);
if field.default.is_none() {
quote! {
let __v = __slots.raw.require(#slot_index)?;
<#ty as crate::args::FromValue>::extract_into(__v, &mut __slots.#ident, vm, #ctx)?;
}
} else {
quote! {
if let ::std::option::Option::Some(__v) = __slots.raw.take(#slot_index) {
<#ty as crate::args::FromValue>::extract_into(__v, &mut __slots.#ident, vm, #ctx)?;
}
}
}
}
fn render_err_ctx(&self, field: &Field) -> TokenStream {
let func_name = self.func_name.as_str();
match self.bad_arg {
None => quote! { crate::args::ArgErrCtx::Plain },
Some(BadArgStyle::Positional) => {
let pos = field.pos_index.unwrap_or(0);
quote! { crate::args::ArgErrCtx::BadArgPos { func_name: #func_name, pos: #pos } }
}
Some(BadArgStyle::Named) => {
let arg_name = field.ident.to_string();
quote! { crate::args::ArgErrCtx::BadArgNamed { func_name: #func_name, arg_name: #arg_name } }
}
}
}
fn named_fields(&self) -> impl Iterator<Item = &Field> {
self.fields
.iter()
.filter(|f| !matches!(f.kind, FieldKind::Varargs | FieldKind::Varkwargs))
}
}
fn resolve_field_roles(fields: &mut [Field]) -> syn::Result<(Option<usize>, Option<usize>)> {
let mut varargs_idx = None;
let mut varkwargs_idx = None;
let mut seen_varargs = false;
let mut seen_varkwargs = false;
let mut seen_pos_or_kw = false;
let mut seen_kw_only = false;
let mut pos_counter: usize = 0;
let mut slot_counter: usize = 0;
for (idx, field) in fields.iter_mut().enumerate() {
if seen_varkwargs {
return Err(syn::Error::new(
field.ident.span(),
"no fields may appear after a `#[from_args(varkwargs)]` field",
));
}
match field.kind {
FieldKind::PosOnly => {
if seen_pos_or_kw || seen_kw_only || seen_varargs {
return Err(syn::Error::new(
field.ident.span(),
"positional-only fields must come before positional-or-keyword, varargs, and keyword-only fields",
));
}
}
FieldKind::PosOrKeyword => {
if seen_varargs {
field.kind = FieldKind::KwOnly;
seen_kw_only = true;
} else if seen_kw_only {
return Err(syn::Error::new(
field.ident.span(),
"positional-or-keyword fields cannot appear after keyword-only fields",
));
} else {
seen_pos_or_kw = true;
}
}
FieldKind::KwOnly => {
seen_kw_only = true;
}
FieldKind::Varargs => {
if seen_varargs {
return Err(syn::Error::new(
field.ident.span(),
"only one `#[from_args(varargs)]` field is allowed",
));
}
if seen_kw_only {
return Err(syn::Error::new(
field.ident.span(),
"`varargs` cannot appear after keyword-only fields — Python has no \
signature form with `*args` following keyword-only parameters",
));
}
seen_varargs = true;
varargs_idx = Some(idx);
}
FieldKind::Varkwargs => {
if seen_varkwargs {
return Err(syn::Error::new(
field.ident.span(),
"only one `#[from_args(varkwargs)]` field is allowed",
));
}
seen_varkwargs = true;
varkwargs_idx = Some(idx);
}
}
if matches!(field.kind, FieldKind::PosOnly | FieldKind::PosOrKeyword) {
pos_counter += 1;
field.pos_index = Some(pos_counter);
}
if !matches!(field.kind, FieldKind::Varargs | FieldKind::Varkwargs) {
field.slot_index = Some(slot_counter);
slot_counter += 1;
}
}
Ok((varargs_idx, varkwargs_idx))
}
fn render_build_field(field: &Field) -> TokenStream {
let ident = &field.ident;
match field.kind {
FieldKind::Varargs => quote! { #ident: __slots.raw.take_varargs(), },
FieldKind::Varkwargs => quote! { #ident: __slots.raw.take_varkwargs(), },
_ => match &field.default {
None => quote! {
#ident: __slots.#ident.take().expect("required FromArgs slot checked before build"),
},
Some(DefaultExpr::DefaultTrait) => quote! {
#ident: __slots.#ident.take().unwrap_or_default(),
},
Some(DefaultExpr::Explicit(expr)) => quote! {
#ident: __slots.#ident.take().unwrap_or_else(|| { #expr }),
},
},
}
}
impl Field {
fn render_param(&self, never_matchable: bool) -> TokenStream {
let name = self.ident.to_string();
let keyword_name = self.keyword_name_expr(never_matchable);
let kind = match self.kind {
FieldKind::PosOnly => quote! { crate::args::ParamKind::PosOnly },
FieldKind::PosOrKeyword => quote! { crate::args::ParamKind::PosOrKeyword },
FieldKind::KwOnly => quote! { crate::args::ParamKind::KwOnly },
FieldKind::Varargs | FieldKind::Varkwargs => unreachable!("varargs/varkwargs own no param slot"),
};
let required = self.default.is_none();
quote! {
crate::args::Param {
name: #name,
keyword_name: #keyword_name,
kind: #kind,
required: #required,
}
}
}
fn keyword_name_expr(&self, never_matchable: bool) -> TokenStream {
if never_matchable || matches!(self.kind, FieldKind::PosOnly) && self.static_string.is_none() {
quote! { ::std::option::Option::None }
} else {
let variant = self.static_string_variant();
quote! { ::std::option::Option::Some(crate::intern::StaticStrings::#variant) }
}
}
fn static_string_variant(&self) -> Ident {
if let Some(explicit) = &self.static_string {
explicit.clone()
} else {
let name = self.ident.to_string();
let variant = match name.as_bytes() {
[byte @ b'a'..=b'z'] => format!("AsciiLower{}", char::from(byte.to_ascii_uppercase())),
[b'A'..=b'Z'] => format!("Ascii{name}"),
_ => snake_to_pascal(&name),
};
Ident::new(&variant, self.ident.span())
}
}
}
fn snake_to_pascal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper = true;
for c in s.chars() {
if c == '_' {
upper = true;
} else if upper {
out.extend(c.to_uppercase());
upper = false;
} else {
out.push(c);
}
}
out
}
fn is_vec_of_value(ty: &Type) -> bool {
fn last_segment(ty: &Type) -> Option<&syn::PathSegment> {
match ty {
Type::Path(p) => p.path.segments.last(),
_ => None,
}
}
last_segment(ty).is_some_and(|seg| {
seg.ident == "Vec"
&& match &seg.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| match arg {
syn::GenericArgument::Type(t) => last_segment(t).is_some_and(|s| s.ident == "Value"),
_ => false,
}),
_ => false,
}
})
}
struct StructAttrs {
name: String,
style: Style,
at_most_total: bool,
vectorcall: bool,
kwarg_error_name: Option<String>,
bad_arg: Option<BadArgStyle>,
kwargs_not_supported_yet: bool,
}
fn parse_struct_attrs(attrs: &[syn::Attribute]) -> syn::Result<StructAttrs> {
let mut name: Option<String> = None;
let mut style: Option<Style> = None;
let mut at_most_total = false;
let mut vectorcall = false;
let mut kwarg_error_name: Option<String> = None;
let mut bad_arg: Option<BadArgStyle> = None;
let mut kwargs_not_supported_yet = false;
for attr in attrs {
if !attr.path().is_ident("from_args") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value: LitStr = meta.value()?.parse()?;
name = Some(value.value());
Ok(())
} else if meta.path.is_ident("style") {
if style.is_some() {
return Err(meta.error("duplicate `style` attribute"));
}
let value: Ident = meta.value()?.parse()?;
style = Some(match value.to_string().as_str() {
"def" => Style::Def,
"clinic" => Style::Clinic,
"c" => Style::C,
"c_named" => Style::CNamed,
"unpack" => Style::Unpack,
"parse_tuple" => Style::ParseTuple,
other => {
return Err(syn::Error::new(
value.span(),
format!(
"unknown style `{other}`; expected `def`, `clinic`, `c`, `c_named`, `unpack`, or `parse_tuple`"
),
));
}
});
Ok(())
} else if meta.path.is_ident("at_most_total") {
at_most_total = true;
Ok(())
} else if meta.path.is_ident("vectorcall") {
vectorcall = true;
Ok(())
} else if meta.path.is_ident("kwarg_error_name") {
let value: LitStr = meta.value()?.parse()?;
kwarg_error_name = Some(value.value());
Ok(())
} else if meta.path.is_ident("bad_arg") {
if bad_arg.is_some() {
return Err(meta.error("`bad_arg` and `bad_arg_named` are mutually exclusive"));
}
bad_arg = Some(BadArgStyle::Positional);
Ok(())
} else if meta.path.is_ident("bad_arg_named") {
if bad_arg.is_some() {
return Err(meta.error("`bad_arg` and `bad_arg_named` are mutually exclusive"));
}
bad_arg = Some(BadArgStyle::Named);
Ok(())
} else if meta.path.is_ident("kwargs_not_supported_yet") {
kwargs_not_supported_yet = true;
Ok(())
} else {
Err(meta.error(
"unknown struct attribute; expected `name = \"...\"`, `style = def|clinic|c|c_named|unpack|parse_tuple`, \
`at_most_total`, `vectorcall`, `kwarg_error_name = \"...\"`, `bad_arg`, `bad_arg_named`, \
or `kwargs_not_supported_yet`",
))
}
})?;
}
let name = name.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"missing `#[from_args(name = \"...\")]` on the struct",
)
})?;
Ok(StructAttrs {
name,
style: style.unwrap_or(Style::Clinic),
at_most_total,
vectorcall,
kwarg_error_name,
bad_arg,
kwargs_not_supported_yet,
})
}
#[derive(Default)]
pub(crate) struct FieldAttrs {
pub(crate) kind: FieldKind,
pub(crate) default: Option<DefaultExpr>,
pub(crate) static_string: Option<Ident>,
}
pub(crate) fn parse_field_attrs(attrs: &[syn::Attribute]) -> syn::Result<FieldAttrs> {
let mut out = FieldAttrs::default();
let mut seen_role = false;
for attr in attrs {
if !attr.path().is_ident("from_args") {
continue;
}
attr.parse_nested_meta(|meta| {
let set_role = |out: &mut FieldAttrs, kind: FieldKind, seen: &mut bool| {
if *seen {
return Err(meta.error("only one of `pos_only`, `kw_only`, `varargs`, `varkwargs` may be set"));
}
out.kind = kind;
*seen = true;
Ok(())
};
if meta.path.is_ident("pos_only") {
set_role(&mut out, FieldKind::PosOnly, &mut seen_role)
} else if meta.path.is_ident("kw_only") {
set_role(&mut out, FieldKind::KwOnly, &mut seen_role)
} else if meta.path.is_ident("varargs") {
set_role(&mut out, FieldKind::Varargs, &mut seen_role)
} else if meta.path.is_ident("varkwargs") {
set_role(&mut out, FieldKind::Varkwargs, &mut seen_role)
} else if meta.path.is_ident("default") {
if out.default.is_some() {
return Err(meta.error("duplicate `default` attribute"));
}
if meta.input.peek(Token![=]) {
let _: Token![=] = meta.input.parse()?;
let expr: Expr = meta.input.parse()?;
out.default = Some(DefaultExpr::Explicit(Box::new(expr)));
} else {
out.default = Some(DefaultExpr::DefaultTrait);
}
Ok(())
} else if meta.path.is_ident("static_string") {
let value: LitStr = meta.value()?.parse()?;
out.static_string = Some(Ident::new(&value.value(), value.span()));
Ok(())
} else {
Err(meta.error("unknown field attribute"))
}
})?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use syn::parse_quote;
use super::expand;
#[track_caller]
fn expand_err(input: &syn::DeriveInput) -> String {
expand(input).expect_err("expected a validation error").to_string()
}
#[track_caller]
fn expand_ok(input: &syn::DeriveInput) {
if let Err(err) = expand(input) {
panic!("expected expansion to succeed, got: {err}");
}
}
#[test]
fn valid_one_struct_per_style() {
expand_ok(&parse_quote! {
#[from_args(name = "search", style = def)]
struct Def {
pattern: Value,
#[from_args(default)]
flags: Value,
#[from_args(kw_only, default)]
extra: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "replace")]
struct Clinic {
#[from_args(pos_only)]
old: Value,
#[from_args(pos_only)]
new: Value,
#[from_args(default = Value::Int(-1))]
count: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "date", style = c, at_most_total)]
struct C {
year: i32,
month: i32,
#[from_args(kw_only, default)]
fold: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "timezone", style = c_named, at_most_total)]
struct CNamed {
offset: Value,
#[from_args(default)]
name: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "unicodedata.name", style = unpack)]
struct Unpack {
#[from_args(pos_only)]
chr: Value,
#[from_args(pos_only, default)]
default: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "print")]
struct Varargs {
#[from_args(varargs)]
objects: Vec<Value>,
#[from_args(default)]
sep: Value,
}
});
expand_ok(&parse_quote! {
#[from_args(name = "dict")]
struct Varkwargs {
#[from_args(pos_only, default)]
iterable: Value,
#[from_args(varkwargs)]
kwargs: KwargsValues,
}
});
}
#[test]
fn unknown_style() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", style = fancy)]
struct S { a: Value }
});
assert_snapshot!(err, @"unknown style `fancy`; expected `def`, `clinic`, `c`, `c_named`, `unpack`, or `parse_tuple`");
}
#[test]
fn def_rejects_bad_arg() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", style = def, bad_arg)]
struct S { a: Value }
});
assert_snapshot!(err, @"`bad_arg`/`bad_arg_named` cannot be combined with `style = def` — CPython `def` binding never type-checks while binding; declare fields as raw `Value` and coerce in the function body");
}
#[test]
fn unpack_rejects_pos_or_keyword() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", style = unpack)]
struct S { a: Value }
});
assert_snapshot!(err, @"`style = unpack` models a positional-only signature — every positional field must be `pos_only`");
}
#[test]
fn at_most_total_rejects_def_and_unpack() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", style = def, at_most_total)]
struct S { a: Value }
});
assert_snapshot!(err, @"`at_most_total` cannot be combined with `style = def`, `style = unpack` or `style = parse_tuple` — the total pre-count models `PyArg_ParseTupleAndKeywords`-family C parsers");
}
#[test]
fn at_most_total_rejects_varargs() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", at_most_total)]
struct S {
#[from_args(varargs)]
args: Vec<Value>,
}
});
assert_snapshot!(err, @"`at_most_total` cannot be combined with `varargs` or `varkwargs` — the up-front total-count check is only meaningful for signatures with a fixed maximum");
}
#[test]
fn vectorcall_requires_clinic_and_at_most_total() {
let err = expand_err(&parse_quote! {
#[from_args(name = "int", vectorcall)]
struct S {
#[from_args(pos_only, default)]
x: Value,
}
});
assert_snapshot!(err, @"`vectorcall` requires the default `clinic` style plus `at_most_total` — it models a `tp_vectorcall` fast path in front of a clinic parser");
expand_ok(&parse_quote! {
#[from_args(name = "int", at_most_total, vectorcall)]
struct S {
#[from_args(pos_only, default)]
x: Value,
#[from_args(default)]
base: Value,
}
});
}
#[test]
fn kwarg_error_name_requires_def_or_clinic() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", style = c_named, kwarg_error_name = "g")]
struct S { a: Value }
});
assert_snapshot!(err, @"`kwarg_error_name` is only meaningful with `style = def`, the default `clinic` style, `style = unpack` or `style = parse_tuple` (where it names the function in the `takes no keyword arguments` error) — the C families defer unknown-kwarg errors past binding");
}
#[test]
fn kwargs_not_supported_yet_rejects_kw_only() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f", kwargs_not_supported_yet)]
struct S {
#[from_args(kw_only, default)]
a: Value,
}
});
assert_snapshot!(err, @"`kwargs_not_supported_yet` cannot be combined with `kw_only` fields — the flag rejects every kwarg up front, so kw_only slots are unreachable");
}
#[test]
fn varargs_must_be_vec_of_value() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(varargs)]
args: Vec<i64>,
}
});
assert_snapshot!(err, @"`varargs` fields must be `Vec<Value>` — coerce elements in the function body");
}
#[test]
fn required_positional_after_default_rejected() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(default)]
a: Value,
b: Value,
}
});
assert_snapshot!(err, @"required positional fields must come before positional fields with defaults — matching Python signatures, and relied on by the runtime binder's fast path");
}
#[test]
fn field_ordering_enforced() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
a: Value,
#[from_args(pos_only)]
b: Value,
}
});
assert_snapshot!(err, @"positional-only fields must come before positional-or-keyword, varargs, and keyword-only fields");
}
#[test]
fn varargs_after_kw_only_rejected() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(kw_only, default)]
a: Value,
#[from_args(varargs)]
rest: Vec<Value>,
}
});
assert_snapshot!(err, @"`varargs` cannot appear after keyword-only fields — Python has no signature form with `*args` following keyword-only parameters");
}
#[test]
fn required_kw_only_rejected() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(kw_only)]
a: Value,
}
});
assert_snapshot!(err, @"keyword-only fields must have a `default` — the runtime binder's fast paths skip the aggregated missing-keyword check, so a required keyword-only parameter would report the wrong error; extend the binder before allowing this");
}
#[test]
fn default_on_varargs_rejected() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(varargs, default)]
rest: Vec<Value>,
}
});
assert_snapshot!(err, @"`default` and `static_string` cannot be applied to `varargs` / `varkwargs` fields — they configure a named parameter slot, which collector fields don't own");
}
#[test]
fn static_string_on_varkwargs_rejected() {
let err = expand_err(&parse_quote! {
#[from_args(name = "f")]
struct S {
#[from_args(varkwargs, static_string = "PatternAttr")]
kwargs: KwargsValues,
}
});
assert_snapshot!(err, @"`default` and `static_string` cannot be applied to `varargs` / `varkwargs` fields — they configure a named parameter slot, which collector fields don't own");
}
#[test]
fn old_flag_spellings_are_rejected() {
for old_flag in [
"py_def",
"c_error",
"c_error_named",
"expected_exact",
"unpack_tuple",
"at_most_positional",
] {
let flag = syn::Ident::new(old_flag, proc_macro2::Span::call_site());
let err = expand_err(&parse_quote! {
#[from_args(name = "f", #flag)]
struct S { a: Value }
});
assert!(err.starts_with("unknown struct attribute"), "{old_flag}: {err}");
}
}
}