diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e0566076e35..7c7bab657d3 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,78 +1,108 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, + spanned::Spanned, token::Comma, - Attribute, ExprPath, Ident, LitStr, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token, }; pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); + syn::custom_keyword!(dict); + syn::custom_keyword!(extends); + syn::custom_keyword!(freelist); syn::custom_keyword!(from_py_with); + syn::custom_keyword!(gc); syn::custom_keyword!(get); syn::custom_keyword!(item); - syn::custom_keyword!(pass_module); + syn::custom_keyword!(module); syn::custom_keyword!(name); + syn::custom_keyword!(pass_module); syn::custom_keyword!(set); syn::custom_keyword!(signature); + syn::custom_keyword!(subclass); syn::custom_keyword!(text_signature); syn::custom_keyword!(transparent); + syn::custom_keyword!(unsendable); + syn::custom_keyword!(weakref); +} + +#[derive(Clone, Debug)] +pub struct KeywordAttribute { + pub kw: K, + pub value: V, } +/// A helper type which parses the inner type via a literal string +/// e.g. LitStrValue -> parses "some::path" in quotes. #[derive(Clone, Debug, PartialEq)] -pub struct FromPyWithAttribute(pub ExprPath); +pub struct LitStrValue(pub T); -impl Parse for FromPyWithAttribute { +impl Parse for LitStrValue { fn parse(input: ParseStream) -> Result { - let _: kw::from_py_with = input.parse()?; - let _: Token![=] = input.parse()?; - let string_literal: LitStr = input.parse()?; - string_literal.parse().map(FromPyWithAttribute) + let lit_str: LitStr = input.parse()?; + lit_str.parse().map(LitStrValue) } } -#[derive(Clone, Debug, PartialEq)] -pub struct NameAttribute(pub Ident); - -impl Parse for NameAttribute { - fn parse(input: ParseStream) -> Result { - let _: kw::name = input.parse()?; - let _: Token![=] = input.parse()?; - let string_literal: LitStr = input.parse()?; - string_literal.parse().map(NameAttribute) +impl ToTokens for LitStrValue { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) } } -/// For specifying the path to the pyo3 crate. +/// A helper type which parses a name via a literal string #[derive(Clone, Debug, PartialEq)] -pub struct CrateAttribute(pub Path); +pub struct NameLitStr(pub Ident); -impl Parse for CrateAttribute { +impl Parse for NameLitStr { fn parse(input: ParseStream) -> Result { - let _: Token![crate] = input.parse()?; - let _: Token![=] = input.parse()?; let string_literal: LitStr = input.parse()?; - string_literal.parse().map(CrateAttribute) + if let Ok(ident) = string_literal.parse() { + Ok(NameLitStr(ident)) + } else { + bail_spanned!(string_literal.span() => "expected a single identifier in double quotes") + } } } -#[derive(Clone, Debug, PartialEq)] -pub struct TextSignatureAttribute { - pub kw: kw::text_signature, - pub eq_token: Token![=], - pub lit: LitStr, +impl ToTokens for NameLitStr { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } } -impl Parse for TextSignatureAttribute { +pub type ExtendsAttribute = KeywordAttribute; +pub type FreelistAttribute = KeywordAttribute; +pub type ModuleAttribute = KeywordAttribute; +pub type NameAttribute = KeywordAttribute; +pub type TextSignatureAttribute = KeywordAttribute; + +impl Parse for KeywordAttribute { fn parse(input: ParseStream) -> Result { - Ok(TextSignatureAttribute { - kw: input.parse()?, - eq_token: input.parse()?, - lit: input.parse()?, - }) + 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); + } +} + +pub type FromPyWithAttribute = KeywordAttribute>; + +/// For specifying the path to the pyo3 crate. +pub type CrateAttribute = KeywordAttribute>; + pub fn get_pyo3_options(attr: &syn::Attribute) -> Result>> { if is_attribute_ident(attr, "pyo3") { attr.parse_args_with(Punctuated::parse_terminated).map(Some) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index f262df3ecbd..9f7dd40a286 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -252,7 +252,9 @@ impl<'a> Container<'a> { None => quote!( obj.get_item(#index)?.extract() ), - Some(FromPyWithAttribute(expr_path)) => quote! ( + Some(FromPyWithAttribute { + value: expr_path, .. + }) => quote! ( #expr_path(obj.get_item(#index)?) ), }; @@ -308,7 +310,9 @@ impl<'a> Container<'a> { new_err.set_cause(py, ::std::option::Option::Some(inner)); new_err })?), - Some(FromPyWithAttribute(expr_path)) => quote! ( + Some(FromPyWithAttribute { + value: expr_path, .. + }) => quote! ( #expr_path(#get_field).map_err(|inner| { let py = _pyo3::PyNativeType::py(obj); let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); @@ -388,7 +392,7 @@ impl ContainerOptions { ContainerPyO3Attribute::Crate(path) => { ensure_spanned!( options.krate.is_none(), - path.0.span() => "`crate` may only be provided once" + path.span() => "`crate` may only be provided once" ); options.krate = Some(path); } diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index c11c828d2e3..61d1ff14f6d 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -21,7 +21,7 @@ pub struct ConstSpec { impl ConstSpec { pub fn python_name(&self) -> Cow { if let Some(name) = &self.attributes.name { - Cow::Borrowed(&name.0) + Cow::Borrowed(&name.value.0) } else { Cow::Owned(self.rust_ident.unraw()) } @@ -89,7 +89,7 @@ impl ConstAttributes { fn set_name(&mut self, name: NameAttribute) -> Result<()> { ensure_spanned!( self.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); self.name = Some(name); Ok(()) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index bde4b079572..a8d46efb37b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -14,7 +14,7 @@ use syn::ext::IdentExt; use syn::spanned::Spanned; use syn::Result; -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct FnArg<'a> { pub name: &'a syn::Ident, pub by_ref: &'a Option, @@ -273,7 +273,7 @@ impl<'a> FnSpec<'a> { ty: fn_type_attr, args: fn_attrs, mut python_name, - } = parse_method_attributes(meth_attrs, name.map(|name| name.0), &mut deprecations)?; + } = parse_method_attributes(meth_attrs, name.map(|name| name.value.0), &mut deprecations)?; let (fn_type, skip_first_arg, fixed_convention) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 738b69fb901..aacc9bef8b2 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -31,7 +31,7 @@ impl PyModuleOptions { for option in take_pyo3_options(attrs)? { match option { - PyModulePyO3Option::Name(name) => options.set_name(name.0)?, + PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, PyModulePyO3Option::Crate(path) => options.set_crate(path)?, } } @@ -52,7 +52,7 @@ impl PyModuleOptions { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 01fb63253b2..219c1e1919b 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -231,7 +231,9 @@ fn impl_arg_param( let arg_value = quote_arg_span!(#args_array[#option_pos]); *option_pos += 1; - let arg_value_or_default = if let Some(FromPyWithAttribute(expr_path)) = &arg.attrs.from_py_with + let arg_value_or_default = if let Some(FromPyWithAttribute { + value: expr_path, .. + }) = &arg.attrs.from_py_with { match (spec.default_value(name), arg.optional.is_some()) { (Some(default), true) if default.to_string() != "None" => { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index e827f8a00b1..4950844c0e1 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,19 +1,20 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::attributes::{ - self, take_pyo3_options, CrateAttribute, NameAttribute, TextSignatureAttribute, + self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, + ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, }; use crate::deprecations::{Deprecation, Deprecations}; use crate::konst::{ConstAttributes, ConstSpec}; use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; -use crate::utils::{self, get_pyo3_crate, unwrap_group, PythonDoc}; +use crate::utils::{self, get_pyo3_crate, PythonDoc}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; //unraw +use syn::{parse_quote, spanned::Spanned, Result, Token}; /// If the class is derived from a Rust `struct` or `enum`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -24,27 +25,18 @@ pub enum PyClassKind { /// The parsed arguments of the pyclass macro pub struct PyClassArgs { - pub freelist: Option, - pub name: Option, - pub base: syn::TypePath, - pub has_dict: bool, - pub has_weaklist: bool, - pub is_basetype: bool, - pub has_extends: bool, - pub has_unsendable: bool, - pub module: Option, pub class_kind: PyClassKind, + pub options: PyClassPyO3Options, pub deprecations: Deprecations, } impl PyClassArgs { fn parse(input: ParseStream, kind: PyClassKind) -> Result { - let mut slf = PyClassArgs::new(kind); - let vars = Punctuated::::parse_terminated(input)?; - for expr in vars { - slf.add_expr(&expr)?; - } - Ok(slf) + Ok(PyClassArgs { + class_kind: kind, + options: PyClassPyO3Options::parse(input)?, + deprecations: Deprecations::new(), + }) } pub fn parse_stuct_args(input: ParseStream) -> syn::Result { @@ -54,155 +46,64 @@ impl PyClassArgs { pub fn parse_enum_args(input: ParseStream) -> syn::Result { Self::parse(input, PyClassKind::Enum) } - - fn new(class_kind: PyClassKind) -> Self { - PyClassArgs { - freelist: None, - name: None, - module: None, - base: parse_quote! { _pyo3::PyAny }, - has_dict: false, - has_weaklist: false, - is_basetype: false, - has_extends: false, - has_unsendable: false, - class_kind, - deprecations: Deprecations::new(), - } - } - - /// Add a single expression from the comma separated list in the attribute, which is - /// either a single word or an assignment expression - fn add_expr(&mut self, expr: &Expr) -> Result<()> { - match expr { - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => self.add_path(exp), - syn::Expr::Assign(assign) => self.add_assign(assign), - _ => bail_spanned!(expr.span() => "failed to parse arguments"), - } - } - - /// Match a key/value flag - fn add_assign(&mut self, assign: &syn::ExprAssign) -> syn::Result<()> { - let syn::ExprAssign { left, right, .. } = assign; - let key = match &**left { - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => { - exp.path.segments.first().unwrap().ident.to_string() - } - _ => bail_spanned!(assign.span() => "failed to parse arguments"), - }; - - macro_rules! expected { - ($expected: literal) => { - expected!($expected, right.span()) - }; - ($expected: literal, $span: expr) => { - bail_spanned!($span => concat!("expected ", $expected)) - }; - } - - match key.as_str() { - "freelist" => { - // We allow arbitrary expressions here so you can e.g. use `8*64` - self.freelist = Some(syn::Expr::clone(right)); - } - "name" => match unwrap_group(&**right) { - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) => { - self.name = Some(lit.parse().map_err(|_| { - err_spanned!( - lit.span() => "expected a single identifier in double-quotes") - })?); - } - syn::Expr::Path(exp) if exp.path.segments.len() == 1 => { - bail_spanned!( - exp.span() => format!( - "since PyO3 0.13 a pyclass name should be in double-quotes, \ - e.g. \"{}\"", - exp.path.get_ident().expect("path has 1 segment") - ) - ); - } - _ => expected!("type name (e.g. \"Name\")"), - }, - "extends" => match unwrap_group(&**right) { - syn::Expr::Path(exp) => { - if self.class_kind == PyClassKind::Enum { - bail_spanned!( assign.span() => "enums cannot extend from other classes" ); - } - self.base = syn::TypePath { - path: exp.path.clone(), - qself: None, - }; - self.has_extends = true; - } - _ => expected!("type path (e.g., my_mod::BaseClass)"), - }, - "module" => match unwrap_group(&**right) { - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) => { - self.module = Some(lit.clone()); - } - _ => expected!(r#"string literal (e.g., "my_mod")"#), - }, - _ => expected!("one of freelist/name/extends/module", left.span()), - }; - - Ok(()) - } - - /// Match a single flag - fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> { - let flag = exp.path.segments.first().unwrap().ident.to_string(); - match flag.as_str() { - "gc" => self - .deprecations - .push(Deprecation::PyClassGcOption, exp.span()), - "weakref" => { - self.has_weaklist = true; - } - "subclass" => { - if self.class_kind == PyClassKind::Enum { - bail_spanned!(exp.span() => "enums can't be inherited by other classes"); - } - self.is_basetype = true; - } - "dict" => { - self.has_dict = true; - } - "unsendable" => { - self.has_unsendable = true; - } - _ => bail_spanned!( - exp.path.span() => "expected one of gc/weakref/subclass/dict/unsendable" - ), - }; - Ok(()) - } } #[derive(Default)] pub struct PyClassPyO3Options { + pub krate: Option, + pub dict: Option, + pub extends: Option, + pub freelist: Option, + pub module: Option, + pub name: Option, + pub subclass: Option, pub text_signature: Option, + pub unsendable: Option, + pub weakref: Option, + pub deprecations: Deprecations, - pub krate: Option, } enum PyClassPyO3Option { - TextSignature(TextSignatureAttribute), Crate(CrateAttribute), + Dict(kw::dict), + Extends(ExtendsAttribute), + Freelist(FreelistAttribute), + Module(ModuleAttribute), + Name(NameAttribute), + Subclass(kw::subclass), + TextSignature(TextSignatureAttribute), + Unsendable(kw::unsendable), + Weakref(kw::weakref), + + DeprecatedGC(kw::gc), } impl Parse for PyClassPyO3Option { fn parse(input: ParseStream) -> Result { let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::text_signature) { - input.parse().map(PyClassPyO3Option::TextSignature) - } else if lookahead.peek(Token![crate]) { + if lookahead.peek(Token![crate]) { input.parse().map(PyClassPyO3Option::Crate) + } else if lookahead.peek(kw::dict) { + input.parse().map(PyClassPyO3Option::Dict) + } else if lookahead.peek(kw::extends) { + input.parse().map(PyClassPyO3Option::Extends) + } else if lookahead.peek(attributes::kw::freelist) { + input.parse().map(PyClassPyO3Option::Freelist) + } else if lookahead.peek(attributes::kw::module) { + input.parse().map(PyClassPyO3Option::Module) + } else if lookahead.peek(kw::name) { + input.parse().map(PyClassPyO3Option::Name) + } else if lookahead.peek(attributes::kw::subclass) { + input.parse().map(PyClassPyO3Option::Subclass) + } else if lookahead.peek(attributes::kw::text_signature) { + input.parse().map(PyClassPyO3Option::TextSignature) + } else if lookahead.peek(attributes::kw::unsendable) { + input.parse().map(PyClassPyO3Option::Unsendable) + } else if lookahead.peek(attributes::kw::weakref) { + input.parse().map(PyClassPyO3Option::Weakref) + } else if lookahead.peek(attributes::kw::gc) { + input.parse().map(PyClassPyO3Option::DeprecatedGC) } else { Err(lookahead.error()) } @@ -210,57 +111,69 @@ impl Parse for PyClassPyO3Option { } impl PyClassPyO3Options { - pub fn take_pyo3_options(attrs: &mut Vec) -> syn::Result { + fn parse(input: ParseStream) -> syn::Result { let mut options: PyClassPyO3Options = Default::default(); - for option in take_pyo3_options(attrs)? { - match option { - PyClassPyO3Option::TextSignature(text_signature) => { - options.set_text_signature(text_signature)?; - } - PyClassPyO3Option::Crate(path) => { - options.set_crate(path)?; - } - } + + for option in Punctuated::::parse_terminated(input)? { + options.set_option(option)?; } + Ok(options) } - pub fn set_text_signature( - &mut self, - text_signature: TextSignatureAttribute, - ) -> syn::Result<()> { - ensure_spanned!( - self.text_signature.is_none(), - text_signature.kw.span() => "`text_signature` may only be specified once" - ); - self.text_signature = Some(text_signature); - Ok(()) + pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { + take_pyo3_options(attrs)? + .into_iter() + .try_for_each(|option| self.set_option(option)) } - pub fn set_crate(&mut self, path: CrateAttribute) -> syn::Result<()> { - ensure_spanned!( - self.krate.is_none(), - path.0.span() => "`text_signature` may only be specified once" - ); - self.krate = Some(path); + fn set_option(&mut self, option: PyClassPyO3Option) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") + ); + self.$key = Some($key); + } + }; + } + + match option { + PyClassPyO3Option::Crate(krate) => set_option!(krate), + PyClassPyO3Option::Dict(dict) => set_option!(dict), + PyClassPyO3Option::Extends(extends) => set_option!(extends), + PyClassPyO3Option::Freelist(freelist) => set_option!(freelist), + PyClassPyO3Option::Module(module) => set_option!(module), + PyClassPyO3Option::Name(name) => set_option!(name), + PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), + PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature), + PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), + PyClassPyO3Option::Weakref(weakref) => set_option!(weakref), + + PyClassPyO3Option::DeprecatedGC(gc) => self + .deprecations + .push(Deprecation::PyClassGcOption, gc.span()), + } Ok(()) } } pub fn build_py_class( class: &mut syn::ItemStruct, - args: &PyClassArgs, + mut args: PyClassArgs, methods_type: PyClassMethodsType, ) -> syn::Result { - let options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; + args.options.take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc( &class.attrs, - options + args.options .text_signature .as_ref() - .map(|attr| (get_class_python_name(&class.ident, args), attr)), + .map(|attr| (get_class_python_name(&class.ident, &args), attr)), ); - let krate = get_pyo3_crate(&options.krate); + let krate = get_pyo3_crate(&args.options.krate); ensure_spanned!( class.generics.params.is_empty(), @@ -290,15 +203,7 @@ pub fn build_py_class( } }; - impl_class( - &class.ident, - args, - doc, - field_options, - methods_type, - options.deprecations, - krate, - ) + impl_class(&class.ident, &args, doc, field_options, methods_type, krate) } /// `#[pyo3()]` options for pyclass fields @@ -356,7 +261,7 @@ impl FieldPyO3Options { FieldPyO3Option::Name(name) => { ensure_spanned!( options.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); options.name = Some(name); } @@ -367,24 +272,27 @@ impl FieldPyO3Options { } } -fn get_class_python_name<'a>(cls: &'a syn::Ident, attr: &'a PyClassArgs) -> &'a syn::Ident { - attr.name.as_ref().unwrap_or(cls) +fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> &'a syn::Ident { + args.options + .name + .as_ref() + .map(|name_attr| &name_attr.value.0) + .unwrap_or(cls) } fn impl_class( cls: &syn::Ident, - attr: &PyClassArgs, + args: &PyClassArgs, doc: PythonDoc, field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, - deprecations: Deprecations, krate: syn::Path, ) -> syn::Result { - let pytypeinfo_impl = impl_pytypeinfo(cls, attr, Some(&deprecations)); + let pytypeinfo_impl = impl_pytypeinfo(cls, args, Some(&args.options.deprecations)); let py_class_impl = PyClassImplsBuilder::new( cls, - attr, + args, methods_type, descriptors_to_items(cls, field_options)?, vec![], @@ -458,23 +366,28 @@ impl<'a> PyClassEnum<'a> { pub fn build_py_enum( enum_: &mut syn::ItemEnum, - args: &PyClassArgs, + mut args: PyClassArgs, method_type: PyClassMethodsType, ) -> syn::Result { - let options = PyClassPyO3Options::take_pyo3_options(&mut enum_.attrs)?; - - if enum_.variants.is_empty() { - bail_spanned!(enum_.brace_token.span => "Empty enums can't be #[pyclass]."); + args.options.take_pyo3_options(&mut enum_.attrs)?; + + if let Some(extends) = &args.options.extends { + bail_spanned!(extends.span() => "enums can't extend from other classes"); + } else if let Some(subclass) = &args.options.subclass { + bail_spanned!(subclass.span() => "enums can't be inherited by other classes"); + } else if enum_.variants.is_empty() { + bail_spanned!(enum_.brace_token.span => "#[pyclass] can't be used on enums without any variants"); } + let doc = utils::get_doc( &enum_.attrs, - options + args.options .text_signature .as_ref() - .map(|attr| (get_class_python_name(&enum_.ident, args), attr)), + .map(|attr| (get_class_python_name(&enum_.ident, &args), attr)), ); let enum_ = PyClassEnum::new(enum_)?; - Ok(impl_enum(enum_, args, doc, method_type, options)) + Ok(impl_enum(enum_, &args, doc, method_type)) } fn impl_enum( @@ -482,9 +395,8 @@ fn impl_enum( args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, - options: PyClassPyO3Options, ) -> TokenStream { - let krate = get_pyo3_crate(&options.krate); + let krate = get_pyo3_crate(&args.options.krate); impl_enum_class(enum_, args, doc, methods_type, krate) } @@ -613,7 +525,10 @@ fn enum_default_methods<'a>( rust_ident: ident.clone(), attributes: ConstAttributes { is_class_attr: true, - name: Some(NameAttribute(ident.clone())), + name: Some(NameAttribute { + kw: syn::parse_quote! { name }, + value: NameLitStr(ident.clone()), + }), deprecations: Default::default(), }, }; @@ -649,7 +564,7 @@ fn descriptors_to_items( .enumerate() .flat_map(|(field_index, (field, options))| { let name_err = if options.name.is_some() && !options.get && !options.set { - Some(Err(err_spanned!(options.name.as_ref().unwrap().0.span() => "`name` is useless without `get` or `set`"))) + Some(Err(err_spanned!(options.name.as_ref().unwrap().span() => "`name` is useless without `get` or `set`"))) } else { None }; @@ -686,8 +601,8 @@ fn impl_pytypeinfo( ) -> TokenStream { let cls_name = get_class_python_name(cls, attr).to_string(); - let module = if let Some(m) = &attr.module { - quote! { ::core::option::Option::Some(#m) } + let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module { + quote! { ::core::option::Option::Some(#value) } } else { quote! { ::core::option::Option::None } }; @@ -765,20 +680,20 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclass(&self) -> TokenStream { let cls = self.cls; let attr = self.attr; - let dict = if attr.has_dict { + let dict = if attr.options.dict.is_some() { quote! { _pyo3::impl_::pyclass::PyClassDictSlot } } else { quote! { _pyo3::impl_::pyclass::PyClassDummySlot } }; // insert space for weak ref - let weakref = if attr.has_weaklist { + let weakref = if attr.options.weakref.is_some() { quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot } } else { quote! { _pyo3::impl_::pyclass::PyClassDummySlot } }; - let base_nativetype = if attr.has_extends { + let base_nativetype = if attr.options.extends.is_some() { quote! { ::BaseNativeType } } else { quote! { _pyo3::PyAny } @@ -810,7 +725,7 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion - if !attr.has_extends { + if attr.options.extends.is_none() { quote! { impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { @@ -825,11 +740,17 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclassimpl(&self) -> TokenStream { let cls = self.cls; let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); - let is_basetype = self.attr.is_basetype; - let base = &self.attr.base; - let is_subclass = self.attr.has_extends; + let is_basetype = self.attr.options.subclass.is_some(); + let base = self + .attr + .options + .extends + .as_ref() + .map(|extends_attr| extends_attr.value.clone()) + .unwrap_or_else(|| parse_quote! { _pyo3::PyAny }); + let is_subclass = self.attr.options.extends.is_some(); - let dict_offset = if self.attr.has_dict { + let dict_offset = if self.attr.options.dict.is_some() { quote! { fn dict_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { ::std::option::Option::Some(_pyo3::impl_::pyclass::dict_offset::()) @@ -840,7 +761,7 @@ impl<'a> PyClassImplsBuilder<'a> { }; // insert space for weak ref - let weaklist_offset = if self.attr.has_weaklist { + let weaklist_offset = if self.attr.options.weakref.is_some() { quote! { fn weaklist_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { ::std::option::Option::Some(_pyo3::impl_::pyclass::weaklist_offset::()) @@ -850,9 +771,9 @@ impl<'a> PyClassImplsBuilder<'a> { TokenStream::new() }; - let thread_checker = if self.attr.has_unsendable { + let thread_checker = if self.attr.options.unsendable.is_some() { quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl<#cls> } - } else if self.attr.has_extends { + } else if self.attr.options.extends.is_some() { quote! { _pyo3::impl_::pyclass::ThreadCheckerInherited<#cls, <#cls as _pyo3::impl_::pyclass::PyClassImpl>::BaseType> } @@ -940,7 +861,8 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_freelist(&self) -> TokenStream { let cls = self.cls; - self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { + self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { + let freelist = &freelist.value; quote! { impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] @@ -962,7 +884,7 @@ impl<'a> PyClassImplsBuilder<'a> { fn freelist_slots(&self) -> Vec { let cls = self.cls; - if self.attr.freelist.is_some() { + if self.attr.options.freelist.is_some() { vec![ quote! { _pyo3::ffi::PyType_Slot { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a45c6a9a591..3da0044e2bb 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -40,7 +40,7 @@ pub struct PyFunctionSignature { has_kwargs: bool, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct PyFunctionArgPyO3Attributes { pub from_py_with: Option, } @@ -71,7 +71,7 @@ impl PyFunctionArgPyO3Attributes { PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => { ensure_spanned!( attributes.from_py_with.is_none(), - from_py_with.0.span() => "`from_py_with` may only be specified once per argument" + from_py_with.span() => "`from_py_with` may only be specified once per argument" ); attributes.from_py_with = Some(from_py_with); } @@ -339,7 +339,7 @@ impl PyFunctionOptions { PyFunctionOption::Crate(path) => { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); } @@ -351,7 +351,7 @@ impl PyFunctionOptions { pub fn set_name(&mut self, name: NameAttribute) -> Result<()> { ensure_spanned!( self.name.is_none(), - name.0.span() => "`name` may only be specified once" + name.span() => "`name` may only be specified once" ); self.name = Some(name); Ok(()) @@ -377,7 +377,7 @@ pub fn impl_wrap_pyfunction( let python_name = options .name - .map_or_else(|| func.sig.ident.unraw(), |name| name.0); + .map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); let signature = options.signature.unwrap_or_default(); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index e8f2381977b..ebc05a45396 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -61,7 +61,7 @@ impl PyImplOptions { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { ensure_spanned!( self.krate.is_none(), - path.0.span() => "`crate` may only be specified once" + path.span() => "`crate` may only be specified once" ); self.krate = Some(path); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index ef43419e4dc..5fd66c9db54 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -544,7 +544,7 @@ impl PropertyType<'_> { field, python_name, .. } => { let name = match (python_name, &field.ident) { - (Some(name), _) => name.0.to_string(), + (Some(name), _) => name.value.0.to_string(), (None, Some(field_name)) => format!("{}\0", field_name.unraw()), (None, None) => { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 9c75239e7cb..39475d9b6c8 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -77,7 +77,8 @@ pub fn get_doc( syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { if let Some((python_name, text_signature)) = text_signature { // create special doc string lines to set `__text_signature__` - let signature_lines = format!("{}{}\n--\n\n", python_name, text_signature.lit.value()); + let signature_lines = + format!("{}{}\n--\n\n", python_name, text_signature.value.value()); signature_lines.to_tokens(tokens); comma.to_tokens(tokens); } @@ -154,13 +155,6 @@ pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> { Ok(()) } -pub fn unwrap_group(mut expr: &syn::Expr) -> &syn::Expr { - while let syn::Expr::Group(g) = expr { - expr = &*g.expr; - } - expr -} - pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { while let syn::Type::Group(g) = ty { ty = &*g.elem; @@ -193,6 +187,6 @@ pub(crate) fn replace_self(ty: &mut syn::Type, cls: &syn::Type) { /// Extract the path to the pyo3 crate, or use the default (`::pyo3`). pub(crate) fn get_pyo3_crate(attr: &Option) -> syn::Path { attr.as_ref() - .map(|p| p.0.clone()) + .map(|p| p.value.0.clone()) .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 0673805345f..367b37fa598 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -81,17 +81,34 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// -/// `#[pyclass]` accepts the following [parameters][2]: +/// `#[pyclass]` can be used the following [parameters][2]: /// /// | Parameter | Description | /// | :- | :- | -/// | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | -/// | `freelist = N` | Implements a [free list][9] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | -/// | `weakref` | Allows this class to be [weakly referenceable][6]. | +/// | `crate = "some::path"` | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. | +/// | `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. | /// | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][4] | +/// | `freelist = N` | Implements a [free list][9] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | +/// | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | +/// | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | +/// | `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | /// | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | /// | `unsendable` | Required if your struct is not [`Send`][3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][7] with [`Arc`][8]. By using `unsendable`, your class will panic when accessed by another thread.| -/// | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | +/// | `weakref` | Allows this class to be [weakly referenceable][6]. | +/// +/// All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, or as one or +/// more accompanying `#[pyo3(...)]` annotations, e.g.: +/// +/// ```rust,ignore +/// // Argument supplied directly to the `#[pyclass]` annotation. +/// #[pyclass(name = "SomeName", subclass)] +/// struct MyClass { } +/// +/// // Argument supplied as a separate annotation. +/// #[pyclass] +/// #[pyo3(name = "SomeName", subclass)] +/// struct MyClass { } +/// ``` /// /// For more on creating Python classes, /// see the [class section of the guide][1]. @@ -230,7 +247,7 @@ fn pyclass_impl( methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); - let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_compile_error(); + let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -245,7 +262,7 @@ fn pyclass_enum_impl( methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args); - let expanded = build_py_enum(&mut ast, &args, methods_type).unwrap_or_compile_error(); + let expanded = build_py_enum(&mut ast, args, methods_type).unwrap_or_compile_error(); quote!( #ast diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 2147682cc90..a13e40f1042 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -35,13 +35,13 @@ error: `set` may only be specified once | ^^^ error: `name` may only be specified once - --> tests/ui/invalid_property_args.rs:37:49 + --> tests/ui/invalid_property_args.rs:37:42 | 37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); - | ^^^^^ + | ^^^^ error: `name` is useless without `get` or `set` - --> tests/ui/invalid_property_args.rs:40:40 + --> tests/ui/invalid_property_args.rs:40:33 | 40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32); - | ^^^^^^^ + | ^^^^ diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index d166cc81996..fd64e06e755 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,40 +1,40 @@ -error: expected one of freelist/name/extends/module +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] | ^^^^^^ -error: expected type path (e.g., my_mod::BaseClass) +error: expected identifier --> tests/ui/invalid_pyclass_args.rs:6:21 | 6 | #[pyclass(extends = "PyDict")] | ^^^^^^^^ -error: expected type name (e.g. "Name") +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:9:18 | 9 | #[pyclass(name = m::MyClass)] | ^ -error: expected a single identifier in double-quotes +error: expected a single identifier in double quotes --> tests/ui/invalid_pyclass_args.rs:12:18 | 12 | #[pyclass(name = "Custom Name")] | ^^^^^^^^^^^^^ -error: since PyO3 0.13 a pyclass name should be in double-quotes, e.g. "CustomName" +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:15:18 | 15 | #[pyclass(name = CustomName)] | ^^^^^^^^^^ -error: expected string literal (e.g., "my_mod") +error: expected string literal --> tests/ui/invalid_pyclass_args.rs:18:20 | 18 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of gc/weakref/subclass/dict/unsendable +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc` --> tests/ui/invalid_pyclass_args.rs:21:11 | 21 | #[pyclass(weakrev)] diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index a12accbf4ba..4bc53238a2c 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -2,14 +2,14 @@ use pyo3::prelude::*; #[pyclass(subclass)] enum NotBaseClass { - x, - y, + X, + Y, } #[pyclass(extends = PyList)] enum NotDrivedClass { - x, - y, + X, + Y, } #[pyclass] diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index eea36e5c5b9..8f340a762bb 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -4,13 +4,13 @@ error: enums can't be inherited by other classes 3 | #[pyclass(subclass)] | ^^^^^^^^ -error: enums cannot extend from other classes +error: enums can't extend from other classes --> tests/ui/invalid_pyclass_enum.rs:9:11 | 9 | #[pyclass(extends = PyList)] | ^^^^^^^ -error: Empty enums can't be #[pyclass]. +error: #[pyclass] can't be used on enums without any variants --> tests/ui/invalid_pyclass_enum.rs:16:18 | 16 | enum NoEmptyEnum {} diff --git a/tests/ui/invalid_pymethod_names.stderr b/tests/ui/invalid_pymethod_names.stderr index 8aed1d41c52..c99c692c459 100644 --- a/tests/ui/invalid_pymethod_names.stderr +++ b/tests/ui/invalid_pymethod_names.stderr @@ -5,10 +5,10 @@ error: `name` may only be specified once | ^^^^^ error: `name` may only be specified once - --> tests/ui/invalid_pymethod_names.rs:18:19 + --> tests/ui/invalid_pymethod_names.rs:18:12 | 18 | #[pyo3(name = "bar")] - | ^^^^^ + | ^^^^ error: `name` not allowed with `#[new]` --> tests/ui/invalid_pymethod_names.rs:24:19