use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::parse::Parser; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Expr, ExprPath, Ident, Index, LitBool, LitStr, Member, Path, Result, Token, }; use crate::combine_errors::CombineErrors; pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); syn::custom_keyword!(cancel_handle); syn::custom_keyword!(constructor); syn::custom_keyword!(dict); syn::custom_keyword!(eq); syn::custom_keyword!(eq_int); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); syn::custom_keyword!(from_py_with); syn::custom_keyword!(frozen); syn::custom_keyword!(get); syn::custom_keyword!(get_all); syn::custom_keyword!(hash); syn::custom_keyword!(into_py_with); syn::custom_keyword!(item); syn::custom_keyword!(immutable_type); syn::custom_keyword!(from_item_all); syn::custom_keyword!(mapping); syn::custom_keyword!(module); syn::custom_keyword!(name); syn::custom_keyword!(ord); syn::custom_keyword!(pass_module); syn::custom_keyword!(rename_all); syn::custom_keyword!(sequence); syn::custom_keyword!(set); syn::custom_keyword!(set_all); syn::custom_keyword!(new); syn::custom_keyword!(signature); syn::custom_keyword!(str); syn::custom_keyword!(subclass); syn::custom_keyword!(submodule); syn::custom_keyword!(text_signature); syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); syn::custom_keyword!(generic); syn::custom_keyword!(gil_used); syn::custom_keyword!(warn); syn::custom_keyword!(message); syn::custom_keyword!(category); syn::custom_keyword!(from_py_object); syn::custom_keyword!(skip_from_py_object); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { let mut int = String::new(); for (i, ch) in read.char_indices() { match ch { '0'..='9' => { *tracker += 1; int.push(ch) } _ => { *read = &read[i..]; break; } } } int } fn take_ident(read: &mut &str, tracker: &mut usize) -> Ident { let mut ident = String::new(); if read.starts_with("r#") { ident.push_str("r#"); *tracker += 2; *read = &read[2..]; } for (i, ch) in read.char_indices() { match ch { 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { *tracker += 1; ident.push(ch) } _ => { *read = &read[i..]; break; } } } Ident::parse_any.parse_str(&ident).unwrap() } // shorthand parsing logic inspiration taken from https://github.com/dtolnay/thiserror/blob/master/impl/src/fmt.rs fn parse_shorthand_format(fmt: LitStr) -> Result<(LitStr, Vec)> { let span = fmt.span(); let token = fmt.token(); let value = fmt.value(); let mut read = value.as_str(); let mut out = String::new(); let mut members = Vec::new(); let mut tracker = 1; while let Some(brace) = read.find('{') { tracker += brace; out += &read[..brace + 1]; read = &read[brace + 1..]; if read.starts_with('{') { out.push('{'); read = &read[1..]; tracker += 2; continue; } let next = match read.chars().next() { Some(next) => next, None => break, }; tracker += 1; let member = match next { '0'..='9' => { let start = tracker; let index = take_int(&mut read, &mut tracker).parse::().unwrap(); let end = tracker; let subspan = token.subspan(start..end).unwrap_or(span); let idx = Index { index, span: subspan, }; Member::Unnamed(idx) } 'a'..='z' | 'A'..='Z' | '_' => { let start = tracker; let mut ident = take_ident(&mut read, &mut tracker); let end = tracker; let subspan = token.subspan(start..end).unwrap_or(span); ident.set_span(subspan); Member::Named(ident) } '}' | ':' => { let start = tracker; tracker += 1; let end = tracker; let subspan = token.subspan(start..end).unwrap_or(span); // we found a closing bracket or formatting ':' without finding a member, we assume the user wants the instance formatted here bail_spanned!(subspan.span() => "No member found, you must provide a named or positionally specified member.") } _ => continue, }; members.push(member); } out += read; Ok((LitStr::new(&out, span), members)) } #[derive(Clone, Debug)] pub struct StringFormatter { pub fmt: LitStr, pub args: Vec, } impl Parse for crate::attributes::StringFormatter { fn parse(input: ParseStream<'_>) -> Result { let (fmt, args) = parse_shorthand_format(input.parse()?)?; Ok(Self { fmt, args }) } } impl ToTokens for crate::attributes::StringFormatter { fn to_tokens(&self, tokens: &mut TokenStream) { self.fmt.to_tokens(tokens); tokens.extend(quote! {self.args}) } } #[derive(Clone, Debug)] pub struct KeywordAttribute { pub kw: K, pub value: V, } #[derive(Clone, Debug)] pub struct OptionalKeywordAttribute { pub kw: K, pub value: Option, } /// A helper type which parses the inner type via a literal string /// e.g. `LitStrValue` -> parses "some::path" in quotes. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LitStrValue(pub T); impl Parse for LitStrValue { fn parse(input: ParseStream<'_>) -> Result { let lit_str: LitStr = input.parse()?; lit_str.parse().map(LitStrValue) } } impl ToTokens for LitStrValue { fn to_tokens(&self, tokens: &mut TokenStream) { self.0.to_tokens(tokens) } } /// A helper type which parses a name via a literal string #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameLitStr(pub Ident); impl Parse for NameLitStr { fn parse(input: ParseStream<'_>) -> Result { let string_literal: LitStr = input.parse()?; if let Ok(ident) = string_literal.parse_with(Ident::parse_any) { Ok(NameLitStr(ident)) } else { bail_spanned!(string_literal.span() => "expected a single identifier in double quotes") } } } impl ToTokens for NameLitStr { fn to_tokens(&self, tokens: &mut TokenStream) { self.0.to_tokens(tokens) } } /// Available renaming rules #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RenamingRule { CamelCase, KebabCase, Lowercase, PascalCase, ScreamingKebabCase, ScreamingSnakeCase, SnakeCase, Uppercase, } /// A helper type which parses a renaming rule via a literal string #[derive(Clone, Debug, PartialEq, Eq)] pub struct RenamingRuleLitStr { pub lit: LitStr, pub rule: RenamingRule, } impl Parse for RenamingRuleLitStr { fn parse(input: ParseStream<'_>) -> Result { let string_literal: LitStr = input.parse()?; let rule = match string_literal.value().as_ref() { "camelCase" => RenamingRule::CamelCase, "kebab-case" => RenamingRule::KebabCase, "lowercase" => RenamingRule::Lowercase, "PascalCase" => RenamingRule::PascalCase, "SCREAMING-KEBAB-CASE" => RenamingRule::ScreamingKebabCase, "SCREAMING_SNAKE_CASE" => RenamingRule::ScreamingSnakeCase, "snake_case" => RenamingRule::SnakeCase, "UPPERCASE" => RenamingRule::Uppercase, _ => { bail_spanned!(string_literal.span() => "expected a valid renaming rule, possible values are: \"camelCase\", \"kebab-case\", \"lowercase\", \"PascalCase\", \"SCREAMING-KEBAB-CASE\", \"SCREAMING_SNAKE_CASE\", \"snake_case\", \"UPPERCASE\"") } }; Ok(Self { lit: string_literal, rule, }) } } impl ToTokens for RenamingRuleLitStr { fn to_tokens(&self, tokens: &mut TokenStream) { self.lit.to_tokens(tokens) } } /// Text signature can be either a literal string or opt-in/out #[derive(Clone, Debug, PartialEq, Eq)] pub enum TextSignatureAttributeValue { Str(LitStr), // `None` ident to disable automatic text signature generation Disabled(Ident), } impl Parse for TextSignatureAttributeValue { fn parse(input: ParseStream<'_>) -> Result { if let Ok(lit_str) = input.parse::() { return Ok(TextSignatureAttributeValue::Str(lit_str)); } let err_span = match input.parse::() { Ok(ident) if ident == "None" => { return Ok(TextSignatureAttributeValue::Disabled(ident)); } Ok(other_ident) => other_ident.span(), Err(e) => e.span(), }; Err(err_spanned!(err_span => "expected a string literal or `None`")) } } impl ToTokens for TextSignatureAttributeValue { fn to_tokens(&self, tokens: &mut TokenStream) { match self { TextSignatureAttributeValue::Str(s) => s.to_tokens(tokens), TextSignatureAttributeValue::Disabled(b) => b.to_tokens(tokens), } } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum NewImplTypeAttributeValue { FromFields, // Future variant for 'default' should go here } impl Parse for NewImplTypeAttributeValue { fn parse(input: ParseStream<'_>) -> Result { let string_literal: LitStr = input.parse()?; if string_literal.value().as_str() == "from_fields" { Ok(NewImplTypeAttributeValue::FromFields) } else { bail_spanned!(string_literal.span() => "expected \"from_fields\"") } } } impl ToTokens for NewImplTypeAttributeValue { fn to_tokens(&self, tokens: &mut TokenStream) { match self { NewImplTypeAttributeValue::FromFields => { tokens.extend(quote! { "from_fields" }); } } } } pub type ExtendsAttribute = KeywordAttribute; pub type FreelistAttribute = KeywordAttribute>; pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; pub type RenameAllAttribute = KeywordAttribute; pub type StrFormatterAttribute = OptionalKeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type NewImplTypeAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; pub type GILUsedAttribute = KeywordAttribute; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { let kw: K = input.parse()?; let _: Token![=] = input.parse()?; let value = input.parse()?; Ok(KeywordAttribute { kw, value }) } } impl ToTokens for KeywordAttribute { fn to_tokens(&self, tokens: &mut TokenStream) { self.kw.to_tokens(tokens); Token![=](self.kw.span()).to_tokens(tokens); self.value.to_tokens(tokens); } } impl Parse for OptionalKeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { let kw: K = input.parse()?; let value = match input.parse::() { Ok(_) => Some(input.parse()?), Err(_) => None, }; Ok(OptionalKeywordAttribute { kw, value }) } } impl ToTokens for OptionalKeywordAttribute { fn to_tokens(&self, tokens: &mut TokenStream) { self.kw.to_tokens(tokens); if self.value.is_some() { Token![=](self.kw.span()).to_tokens(tokens); self.value.to_tokens(tokens); } } } pub type FromPyWithAttribute = KeywordAttribute; pub type IntoPyWithAttribute = KeywordAttribute; pub type DefaultAttribute = OptionalKeywordAttribute; /// For specifying the path to the pyo3 crate. pub type CrateAttribute = KeywordAttribute>; pub fn get_pyo3_options(attr: &syn::Attribute) -> Result>> { if attr.path().is_ident("pyo3") { attr.parse_args_with(Punctuated::parse_terminated).map(Some) } else { Ok(None) } } /// Takes attributes from an attribute vector. /// /// For each attribute in `attrs`, `extractor` is called. If `extractor` returns `Ok(true)`, then /// the attribute will be removed from the vector. /// /// This is similar to `Vec::retain` except the closure is fallible and the condition is reversed. /// (In `retain`, returning `true` keeps the element, here it removes it.) pub fn take_attributes( attrs: &mut Vec, mut extractor: impl FnMut(&Attribute) -> Result, ) -> Result<()> { *attrs = attrs .drain(..) .filter_map(|attr| { extractor(&attr) .map(move |attribute_handled| if attribute_handled { None } else { Some(attr) }) .transpose() }) .collect::>()?; Ok(()) } pub fn take_pyo3_options(attrs: &mut Vec) -> Result> { let mut out = Vec::new(); take_attributes(attrs, |attr| match get_pyo3_options(attr) { Ok(result) => { if let Some(options) = result { out.extend(options.into_iter().map(|a| Ok(a))); Ok(true) } else { Ok(false) } } Err(err) => { out.push(Err(err)); Ok(true) } })?; let out: Vec = out.into_iter().try_combine_syn_errors()?; Ok(out) }