From 4d46abde73f632461fbd1943141d43c8a4fa91fc Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sat, 17 Apr 2021 22:22:06 +0100 Subject: [PATCH] macros: support `#[pyo3(name = "...")]` --- CHANGELOG.md | 2 + guide/src/trait_bounds.md | 2 +- pyo3-macros-backend/src/attributes.rs | 96 +++++ pyo3-macros-backend/src/attrs.rs | 22 -- pyo3-macros-backend/src/from_pyobject.rs | 328 ++++++++-------- pyo3-macros-backend/src/konst.rs | 117 ++++-- pyo3-macros-backend/src/lib.rs | 6 +- pyo3-macros-backend/src/method.rs | 85 ++--- pyo3-macros-backend/src/module.rs | 243 +++--------- pyo3-macros-backend/src/pyclass.rs | 28 +- pyo3-macros-backend/src/pyfunction.rs | 394 ++++++++++++++++---- pyo3-macros-backend/src/pyimpl.rs | 17 +- pyo3-macros-backend/src/pymethod.rs | 111 +++--- pyo3-macros-backend/src/pyproto.rs | 4 +- pyo3-macros-backend/src/utils.rs | 14 +- pyo3-macros/src/lib.rs | 6 +- src/impl_/mod.rs | 13 + src/lib.rs | 1 + tests/test_class_attributes.rs | 10 +- tests/test_class_basics.rs | 6 +- tests/test_compile_error.rs | 1 + tests/test_module.rs | 2 +- tests/ui/deprecations.rs | 33 ++ tests/ui/deprecations.stderr | 29 ++ tests/ui/invalid_argument_attributes.stderr | 12 +- tests/ui/invalid_frompy_derive.stderr | 50 +-- tests/ui/invalid_property_args.stderr | 2 +- tests/ui/invalid_pymethod_names.rs | 8 +- tests/ui/invalid_pymethod_names.stderr | 24 +- tests/ui/invalid_pymethods.rs | 6 + tests/ui/invalid_pymethods.stderr | 62 +-- 31 files changed, 1032 insertions(+), 702 deletions(-) create mode 100644 pyo3-macros-backend/src/attributes.rs delete mode 100644 pyo3-macros-backend/src/attrs.rs create mode 100644 src/impl_/mod.rs create mode 100644 tests/ui/deprecations.rs create mode 100644 tests/ui/deprecations.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2d1056..ed34b7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add FFI definition `_Py_InitializeMain`. [#1473](https://github.com/PyO3/pyo3/pull/1473) - Add FFI definitions from `cpython/import.h`.[#1475](https://github.com/PyO3/pyo3/pull/1475) - Add tuple and unit struct support for `#[pyclass]` macro. [#1504](https://github.com/PyO3/pyo3/pull/1504) +- Add `#[pyo3(name = "...")]` syntax for setting Python names. [#1567](https://github.com/PyO3/pyo3/pull/1567) - Add FFI definition `PyDateTime_TimeZone_UTC`. [#1572](https://github.com/PyO3/pyo3/pull/1572) - Add support for `#[pyclass(extends=Exception)]`. [#1591](https://github.com/PyO3/pyo3/pull/1591) @@ -40,6 +41,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `PyMappingProtocol::__reversed__` - `PyNumberProtocol::__complex__` and `PyNumberProtocol::__round__` - `PyAsyncProtocol::__aenter__` and `PyAsyncProtocol::__aexit__` +- Deprecate `#[name = "..."]` attributes in favor of `#[pyo3(name = "...")]`. [#1567](https://github.com/PyO3/pyo3/pull/1567) ### Removed - Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426) diff --git a/guide/src/trait_bounds.md b/guide/src/trait_bounds.md index 06942b3e..10e9ab72 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait_bounds.md @@ -475,7 +475,7 @@ pub fn solve(model: &mut T) { } #[pyfunction] -#[name = "solve"] +#[pyo3(name = "solve")] pub fn solve_wrapper(model: &mut UserModel) { solve(model); } diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs new file mode 100644 index 00000000..35efc7f9 --- /dev/null +++ b/pyo3-macros-backend/src/attributes.rs @@ -0,0 +1,96 @@ +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + token::Comma, + Attribute, ExprPath, Ident, LitStr, Result, Token, +}; + +pub mod kw { + syn::custom_keyword!(annotation); + syn::custom_keyword!(attribute); + syn::custom_keyword!(from_py_with); + syn::custom_keyword!(item); + syn::custom_keyword!(pass_module); + syn::custom_keyword!(name); + syn::custom_keyword!(signature); + syn::custom_keyword!(transparent); +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FromPyWithAttribute(pub ExprPath); + +impl Parse for FromPyWithAttribute { + 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) + } +} + +#[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) + } +} + +pub fn get_pyo3_attribute(attr: &syn::Attribute) -> Result>> { + if attribute_ident_is(attr, "pyo3") { + attr.parse_args_with(Punctuated::parse_terminated).map(Some) + } else { + Ok(None) + } +} + +pub fn attribute_ident_is(attr: &syn::Attribute, name: &str) -> bool { + if let Some(path_segment) = attr.path.segments.last() { + attr.path.segments.len() == 1 && path_segment.ident == name + } else { + false + } +} + +/// 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 get_deprecated_name_attribute(attr: &syn::Attribute) -> syn::Result> { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + path, + lit: syn::Lit::Str(s), + .. + })) if path.is_ident("name") => { + let mut ident: syn::Ident = s.parse()?; + // This span is the whole attribute span, which is nicer for reporting errors. + ident.set_span(attr.span()); + Ok(Some(NameAttribute(ident))) + } + _ => Ok(None), + } +} diff --git a/pyo3-macros-backend/src/attrs.rs b/pyo3-macros-backend/src/attrs.rs deleted file mode 100644 index 486eff39..00000000 --- a/pyo3-macros-backend/src/attrs.rs +++ /dev/null @@ -1,22 +0,0 @@ -use syn::spanned::Spanned; -use syn::{ExprPath, Lit, Meta, MetaNameValue, Result}; - -#[derive(Clone, Debug, PartialEq)] -pub struct FromPyWithAttribute(pub ExprPath); - -impl FromPyWithAttribute { - pub fn from_meta(meta: Meta) -> Result { - let string_literal = match meta { - Meta::NameValue(MetaNameValue { - lit: Lit::Str(string_literal), - .. - }) => string_literal, - meta => { - bail_spanned!(meta.span() => "expected a name-value: `pyo3(from_py_with = \"func\")`") - } - }; - - let expr_path = string_literal.parse::()?; - Ok(FromPyWithAttribute(expr_path)) - } -} diff --git a/pyo3-macros-backend/src/from_pyobject.rs b/pyo3-macros-backend/src/from_pyobject.rs index c143ca93..c7e4fd42 100644 --- a/pyo3-macros-backend/src/from_pyobject.rs +++ b/pyo3-macros-backend/src/from_pyobject.rs @@ -1,9 +1,14 @@ -use crate::attrs::FromPyWithAttribute; +use crate::attributes::{self, get_pyo3_attribute, FromPyWithAttribute}; use proc_macro2::TokenStream; use quote::quote; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{parse_quote, Attribute, DataEnum, DeriveInput, Fields, Ident, Meta, MetaList, Result}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + parse_quote, + punctuated::Punctuated, + spanned::Spanned, + Attribute, DataEnum, DeriveInput, Fields, Ident, LitStr, Result, Token, +}; /// Describes derivation input of an enum. #[derive(Debug)] @@ -26,7 +31,7 @@ impl<'a> Enum<'a> { .variants .iter() .map(|variant| { - let attrs = ContainerAttribute::parse_attrs(&variant.attrs)?; + let attrs = ContainerOptions::from_attrs(&variant.attrs)?; let var_ident = &variant.ident; Container::new( &variant.fields, @@ -86,7 +91,7 @@ enum ContainerType<'a> { /// Struct Container, e.g. `struct Foo { a: String }` /// /// Variant contains the list of field identifiers and the corresponding extraction call. - Struct(Vec<(&'a Ident, FieldAttributes)>), + Struct(Vec<(&'a Ident, FieldPyO3Attributes)>), /// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }` /// /// The field specified by the identifier is extracted directly from the object. @@ -119,20 +124,20 @@ impl<'a> Container<'a> { fn new( fields: &'a Fields, path: syn::Path, - attrs: Vec, + options: ContainerOptions, is_enum_variant: bool, ) -> Result { ensure_spanned!( !fields.is_empty(), fields.span() => "cannot derive FromPyObject for empty structs and variants" ); - let transparent = attrs - .iter() - .any(|attr| *attr == ContainerAttribute::Transparent); - if transparent { - Self::check_transparent_len(fields)?; + if options.transparent { + ensure_spanned!( + fields.len() == 1, + fields.span() => "transparent structs and variants can only have 1 field" + ); } - let style = match (fields, transparent) { + let style = match (fields, options.transparent) { (Fields::Unnamed(_), true) => ContainerType::TupleNewtype, (Fields::Unnamed(unnamed), false) => match unnamed.unnamed.len() { 1 => ContainerType::TupleNewtype, @@ -157,17 +162,17 @@ impl<'a> Container<'a> { .ident .as_ref() .expect("Named fields should have identifiers"); - let attrs = FieldAttributes::parse_attrs(&field.attrs)?; + let attrs = FieldPyO3Attributes::from_attrs(&field.attrs)?; fields.push((ident, attrs)) } ContainerType::Struct(fields) } (Fields::Unit, _) => unreachable!(), // covered by length check above }; - let err_name = attrs - .iter() - .find_map(|a| a.annotation().map(syn::LitStr::value)) - .unwrap_or_else(|| path.segments.last().unwrap().ident.to_string()); + let err_name = options.annotation.map_or_else( + || path.segments.last().unwrap().ident.to_string(), + |lit_str| lit_str.value(), + ); let v = Container { path, @@ -178,18 +183,6 @@ impl<'a> Container<'a> { Ok(v) } - fn verify_struct_container_attrs(attrs: &'a [ContainerAttribute]) -> Result<()> { - for attr in attrs { - match attr { - ContainerAttribute::Transparent => {} - ContainerAttribute::ErrorAnnotation(annotation) => bail_spanned!( - annotation.span() => "annotation is not supported for structs" - ), - } - } - Ok(()) - } - /// Build derivation body for a struct. fn build(&self) -> TokenStream { match &self.ty { @@ -235,7 +228,7 @@ impl<'a> Container<'a> { ) } - fn build_struct(&self, tups: &[(&Ident, FieldAttributes)]) -> TokenStream { + fn build_struct(&self, tups: &[(&Ident, FieldPyO3Attributes)]) -> TokenStream { let self_ty = &self.path; let mut fields: Punctuated = Punctuated::new(); for (ident, attrs) in tups { @@ -256,67 +249,73 @@ impl<'a> Container<'a> { } quote!(Ok(#self_ty{#fields})) } +} - fn check_transparent_len(fields: &Fields) -> Result<()> { - ensure_spanned!( - fields.len() == 1, - fields.span() => "transparent structs and variants can only have 1 field" - ); - Ok(()) - } +struct ContainerOptions { + transparent: bool, + annotation: Option, } /// Attributes for deriving FromPyObject scoped on containers. #[derive(Clone, Debug, PartialEq)] -enum ContainerAttribute { +enum ContainerPyO3Attribute { /// Treat the Container as a Wrapper, directly extract its fields from the input object. - Transparent, + Transparent(attributes::kw::transparent), /// Change the name of an enum variant in the generated error message. - ErrorAnnotation(syn::LitStr), + ErrorAnnotation(LitStr), } -impl ContainerAttribute { - /// Convenience method to access `ErrorAnnotation`. - fn annotation(&self) -> Option<&syn::LitStr> { - match self { - ContainerAttribute::ErrorAnnotation(s) => Some(s), - _ => None, +impl Parse for ContainerPyO3Attribute { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::transparent) { + let kw: attributes::kw::transparent = input.parse()?; + Ok(ContainerPyO3Attribute::Transparent(kw)) + } else if lookahead.peek(attributes::kw::annotation) { + let _: attributes::kw::annotation = input.parse()?; + let _: Token![=] = input.parse()?; + input.parse().map(ContainerPyO3Attribute::ErrorAnnotation) + } else { + Err(lookahead.error()) } } +} - /// Parse valid container arguments - /// - /// Fails if any are invalid. - fn parse_attrs(value: &[Attribute]) -> Result> { - get_pyo3_meta_list(value)? - .nested - .into_iter() - .map(|meta| { - if let syn::NestedMeta::Meta(metaitem) = &meta { - match metaitem { - Meta::Path(p) if p.is_ident("transparent") => { - return Ok(ContainerAttribute::Transparent); +impl ContainerOptions { + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = ContainerOptions { + transparent: false, + annotation: None, + }; + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_attribute(attr)? { + for pyo3_attr in pyo3_attrs { + match pyo3_attr { + ContainerPyO3Attribute::Transparent(kw) => { + ensure_spanned!( + !options.transparent, + kw.span() => "`transparent` may only be provided once" + ); + options.transparent = true; } - Meta::NameValue(nv) if nv.path.is_ident("annotation") => { - if let syn::Lit::Str(s) = &nv.lit { - return Ok(ContainerAttribute::ErrorAnnotation(s.clone())); - } else { - bail_spanned!(nv.lit.span() => "expected string literal for annotation"); - } + ContainerPyO3Attribute::ErrorAnnotation(lit_str) => { + ensure_spanned!( + options.annotation.is_none(), + lit_str.span() => "`annotation` may only be provided once" + ); + options.annotation = Some(lit_str); } - _ => {} // return Err below } } - - bail_spanned!(meta.span() => "unknown `pyo3` container attribute"); - }) - .collect() + } + } + Ok(options) } } /// Attributes for deriving FromPyObject scoped on fields. #[derive(Clone, Debug)] -struct FieldAttributes { +struct FieldPyO3Attributes { getter: FieldGetter, from_py_with: Option, } @@ -324,121 +323,96 @@ struct FieldAttributes { #[derive(Clone, Debug)] enum FieldGetter { GetItem(Option), - GetAttr(Option), + GetAttr(Option), } -impl FieldAttributes { +enum FieldPyO3Attribute { + Getter(FieldGetter), + FromPyWith(FromPyWithAttribute), +} + +impl Parse for FieldPyO3Attribute { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::attribute) { + let _: attributes::kw::attribute = input.parse()?; + if input.peek(syn::token::Paren) { + let content; + let _ = parenthesized!(content in input); + let attr_name: LitStr = content.parse()?; + if !content.is_empty() { + return Err(content.error( + "expected at most one argument: `attribute` or `attribute(\"name\")`", + )); + } + ensure_spanned!( + !attr_name.value().is_empty(), + attr_name.span() => "attribute name cannot be empty" + ); + Ok(FieldPyO3Attribute::Getter(FieldGetter::GetAttr(Some( + attr_name, + )))) + } else { + Ok(FieldPyO3Attribute::Getter(FieldGetter::GetAttr(None))) + } + } else if lookahead.peek(attributes::kw::item) { + let _: attributes::kw::item = input.parse()?; + if input.peek(syn::token::Paren) { + let content; + let _ = parenthesized!(content in input); + let key = content.parse()?; + if !content.is_empty() { + return Err( + content.error("expected at most one argument: `item` or `item(key)`") + ); + } + Ok(FieldPyO3Attribute::Getter(FieldGetter::GetItem(Some(key)))) + } else { + Ok(FieldPyO3Attribute::Getter(FieldGetter::GetItem(None))) + } + } else if lookahead.peek(attributes::kw::from_py_with) { + input.parse().map(FieldPyO3Attribute::FromPyWith) + } else { + Err(lookahead.error()) + } + } +} + +impl FieldPyO3Attributes { /// Extract the field attributes. /// - fn parse_attrs(attrs: &[Attribute]) -> Result { + fn from_attrs(attrs: &[Attribute]) -> Result { let mut getter = None; let mut from_py_with = None; - let list = get_pyo3_meta_list(attrs)?; - - for meta_item in list.nested { - let meta = match meta_item { - syn::NestedMeta::Meta(meta) => meta, - syn::NestedMeta::Lit(lit) => bail_spanned!( - lit.span() => - "expected `attribute`, `item` or `from_py_with`, got a literal" - ), - }; - let path = meta.path(); - - if path.is_ident("attribute") { - ensure_spanned!( - getter.is_none(), - meta.span() => "only one of `attribute` or `item` can be provided" - ); - getter = Some(FieldGetter::GetAttr(Self::attribute_arg(meta)?)) - } else if path.is_ident("item") { - ensure_spanned!( - getter.is_none(), - meta.span() => "only one of `attribute` or `item` can be provided" - ); - getter = Some(FieldGetter::GetItem(Self::item_arg(meta)?)) - } else if path.is_ident("from_py_with") { - from_py_with = Some(Self::from_py_with_arg(meta)?) - } else { - bail_spanned!(meta.span() => "expected `attribute`, `item` or `from_py_with`") - }; + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_attribute(attr)? { + for pyo3_attr in pyo3_attrs { + match pyo3_attr { + FieldPyO3Attribute::Getter(field_getter) => { + ensure_spanned!( + getter.is_none(), + attr.span() => "only one of `attribute` or `item` can be provided" + ); + getter = Some(field_getter) + } + FieldPyO3Attribute::FromPyWith(from_py_with_attr) => { + ensure_spanned!( + from_py_with.is_none(), + attr.span() => "`from_py_with` may only be provided once" + ); + from_py_with = Some(from_py_with_attr); + } + } + } + } } - Ok(FieldAttributes { + Ok(FieldPyO3Attributes { getter: getter.unwrap_or(FieldGetter::GetAttr(None)), from_py_with, }) } - - fn attribute_arg(meta: Meta) -> syn::Result> { - let mut arg_list = match meta { - Meta::List(list) => list, - Meta::Path(_) => return Ok(None), - Meta::NameValue(nv) => bail_spanned!( - nv.span() => - "expected a string literal or no argument: `pyo3(attribute(\"name\")` or \ - `pyo3(attribute)`" - ), - }; - - if arg_list.nested.len() == 1 { - let arg = arg_list.nested.pop().unwrap().into_value(); - - if let syn::NestedMeta::Lit(syn::Lit::Str(litstr)) = arg { - ensure_spanned!( - !litstr.value().is_empty(), - litstr.span() => "attribute name cannot be empty" - ); - return Ok(Some(litstr)); - } - } - - bail_spanned!(arg_list.span() => "expected a single string literal argument"); - } - - fn item_arg(meta: Meta) -> syn::Result> { - let mut arg_list = match meta { - Meta::List(list) => list, - Meta::Path(_) => return Ok(None), - Meta::NameValue(nv) => bail_spanned!( - nv.span() => "expected a literal or no argument: `pyo3(item(key)` or `pyo3(item)`" - ), - }; - - if arg_list.nested.len() == 1 { - let arg = arg_list.nested.pop().unwrap().into_value(); - if let syn::NestedMeta::Lit(lit) = arg { - return Ok(Some(lit)); - } - } - - bail_spanned!(arg_list.span() => "expected a single literal argument"); - } - - fn from_py_with_arg(meta: Meta) -> syn::Result { - FromPyWithAttribute::from_meta(meta) - } -} - -/// Extract pyo3 metalist, flattens multiple lists into a single one. -fn get_pyo3_meta_list(attrs: &[Attribute]) -> Result { - let mut list: Punctuated = Punctuated::new(); - for value in attrs { - match value.parse_meta()? { - Meta::List(ml) if value.path.is_ident("pyo3") => { - for meta in ml.nested { - list.push(meta); - } - } - _ => continue, - } - } - Ok(MetaList { - path: parse_quote!(pyo3), - paren_token: syn::token::Paren::default(), - nested: list, - }) } fn verify_and_get_lifetime(generics: &syn::Generics) -> Result> { @@ -481,10 +455,12 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { en.build() } syn::Data::Struct(st) => { - let attrs = ContainerAttribute::parse_attrs(&tokens.attrs)?; - Container::verify_struct_container_attrs(&attrs)?; + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + if let Some(lit_str) = &options.annotation { + bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs"); + } let ident = &tokens.ident; - let st = Container::new(&st.fields, parse_quote!(#ident), attrs, false)?; + let st = Container::new(&st.fields, parse_quote!(#ident), options, false)?; st.build() } syn::Data::Union(_) => bail_spanned!( diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 608339a7..44ebc562 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,34 +1,99 @@ -use crate::pyfunction::parse_name_attribute; -use syn::ext::IdentExt; +use crate::attributes::{ + self, attribute_ident_is, get_deprecated_name_attribute, get_pyo3_attribute, take_attributes, + NameAttribute, +}; +use crate::utils; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseStream}, + spanned::Spanned, + Result, +}; -#[derive(Clone, PartialEq, Debug)] pub struct ConstSpec { - pub is_class_attr: bool, - pub python_name: syn::Ident, + pub rust_ident: syn::Ident, + pub attributes: ConstAttributes, } impl ConstSpec { - // For now, the only valid attribute is `#[classattr]`. - pub fn parse(name: &syn::Ident, attrs: &mut Vec) -> syn::Result { - let mut new_attrs = Vec::new(); - let mut is_class_attr = false; - - for attr in attrs.iter() { - if let syn::Meta::Path(name) = attr.parse_meta()? { - if name.is_ident("classattr") { - is_class_attr = true; - continue; - } - } - new_attrs.push(attr.clone()); + /// Null-terminated Python name + pub fn python_name_with_deprecation(&self) -> TokenStream { + if let Some(name) = &self.attributes.name { + let deprecation = + utils::name_deprecation_token(name.0.span(), self.attributes.name_is_deprecated); + let name = format!("{}\0", name.0); + quote!({#deprecation #name}) + } else { + let name = format!("{}\0", self.rust_ident.unraw().to_string()); + quote!(#name) } - - attrs.clear(); - attrs.extend(new_attrs); - - Ok(ConstSpec { - is_class_attr, - python_name: parse_name_attribute(attrs)?.unwrap_or_else(|| name.unraw()), - }) + } +} + +pub struct ConstAttributes { + pub is_class_attr: bool, + pub name: Option, + pub name_is_deprecated: bool, +} + +pub enum PyO3ConstAttribute { + Name(NameAttribute), +} + +impl Parse for PyO3ConstAttribute { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::name) { + input.parse().map(PyO3ConstAttribute::Name) + } else { + Err(lookahead.error()) + } + } +} + +impl ConstAttributes { + pub fn from_attrs(attrs: &mut Vec) -> syn::Result { + let mut attributes = ConstAttributes { + is_class_attr: false, + name: None, + name_is_deprecated: false, + }; + + take_attributes(attrs, |attr| { + if attribute_ident_is(attr, "classattr") { + ensure_spanned!( + attr.tokens.is_empty(), + attr.span() => "`#[classattr]` does not take any arguments" + ); + attributes.is_class_attr = true; + Ok(true) + } else if let Some(pyo3_attributes) = get_pyo3_attribute(attr)? { + for pyo3_attr in pyo3_attributes { + match pyo3_attr { + PyO3ConstAttribute::Name(name) => attributes.set_name(name)?, + } + } + Ok(true) + } else if let Some(name) = get_deprecated_name_attribute(attr)? { + attributes.set_name(name)?; + attributes.name_is_deprecated = true; + Ok(true) + } else { + Ok(false) + } + })?; + + Ok(attributes) + } + + fn set_name(&mut self, name: NameAttribute) -> Result<()> { + ensure_spanned!( + self.name.is_none(), + name.0.span() => "`name` may only be specified once" + ); + self.name = Some(name); + Ok(()) } } diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 9c990d7f..5985b396 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -8,7 +8,7 @@ #[macro_use] mod utils; -mod attrs; +mod attributes; mod defs; mod from_pyobject; mod konst; @@ -22,9 +22,9 @@ mod pymethod; mod pyproto; pub use from_pyobject::build_derive_from_pyobject; -pub use module::{add_fn_to_module, process_functions_in_module, py_init}; +pub use module::{process_functions_in_module, py_init}; pub use pyclass::{build_py_class, PyClassArgs}; -pub use pyfunction::{build_py_function, PyFunctionAttr}; +pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use pyproto::build_py_proto; pub use utils::get_doc; diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 45f3bca3..b51212e6 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,7 +1,8 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::pyfunction::Argument; -use crate::pyfunction::{parse_name_attribute, PyFunctionArgAttrs, PyFunctionAttr}; +use crate::pyfunction::PyFunctionOptions; +use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature}; use crate::utils; use proc_macro2::TokenStream; use quote::ToTokens; @@ -17,7 +18,7 @@ pub struct FnArg<'a> { pub ty: &'a syn::Type, pub optional: Option<&'a syn::Type>, pub py: bool, - pub attrs: PyFunctionArgAttrs, + pub attrs: PyFunctionArgPyO3Attributes, } impl<'a> FnArg<'a> { @@ -32,7 +33,7 @@ impl<'a> FnArg<'a> { bail_spanned!(cap.ty.span() => IMPL_TRAIT_ERR); } - let arg_attrs = PyFunctionArgAttrs::from_attrs(&mut cap.attrs)?; + let arg_attrs = PyFunctionArgPyO3Attributes::from_attrs(&mut cap.attrs)?; let (ident, by_ref, mutability) = match *cap.pat { syn::Pat::Ident(syn::PatIdent { ref ident, @@ -133,6 +134,7 @@ pub struct FnSpec<'a> { pub args: Vec>, pub output: syn::Type, pub doc: syn::LitStr, + pub name_is_deprecated: bool, } pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { @@ -161,13 +163,29 @@ impl<'a> FnSpec<'a> { pub fn parse( sig: &'a mut syn::Signature, meth_attrs: &mut Vec, - allow_custom_name: bool, + options: PyFunctionOptions, ) -> syn::Result> { let MethodAttributes { ty: fn_type_attr, args: fn_attrs, mut python_name, - } = parse_method_attributes(meth_attrs, allow_custom_name)?; + } = parse_method_attributes(meth_attrs, options.name.map(|name| name.0))?; + + match fn_type_attr { + Some(MethodTypeAttribute::New) => { + if let Some(name) = &python_name { + bail_spanned!(name.span() => "`name` not allowed with `#[new]`"); + } + python_name = Some(syn::Ident::new("__new__", proc_macro2::Span::call_site())) + } + Some(MethodTypeAttribute::Call) => { + if let Some(name) = &python_name { + bail_spanned!(name.span() => "`name` not allowed with `#[call]`"); + } + python_name = Some(syn::Ident::new("__call__", proc_macro2::Span::call_site())) + } + _ => {} + } let (fn_type, skip_first_arg) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; @@ -199,9 +217,17 @@ impl<'a> FnSpec<'a> { args: arguments, output: ty, doc, + name_is_deprecated: options.name_is_deprecated, }) } + pub fn python_name_with_deprecation(&self) -> TokenStream { + let deprecation = + utils::name_deprecation_token(self.python_name.span(), self.name_is_deprecated); + let name = format!("{}\0", self.python_name); + quote!({#deprecation #name}) + } + fn parse_text_signature( meth_attrs: &mut Vec, fn_type: &FnType, @@ -362,12 +388,11 @@ struct MethodAttributes { fn parse_method_attributes( attrs: &mut Vec, - allow_custom_name: bool, + mut python_name: Option, ) -> syn::Result { let mut new_attrs = Vec::new(); let mut args = Vec::new(); let mut ty: Option = None; - let mut property_name = None; macro_rules! set_ty { ($new_ty:expr, $ident:expr) => { @@ -434,7 +459,12 @@ fn parse_method_attributes( set_ty!(MethodTypeAttribute::Getter, path); }; - property_name = match nested.pop().unwrap().into_value() { + ensure_spanned!( + python_name.is_none(), + python_name.span() => "`name` may only be specified once" + ); + + python_name = match nested.pop().unwrap().into_value() { syn::NestedMeta::Meta(syn::Meta::Path(w)) if w.segments.len() == 1 => { Some(w.segments[0].ident.clone()) } @@ -455,7 +485,7 @@ fn parse_method_attributes( } }; } else if path.is_ident("args") { - let attrs = PyFunctionAttr::from_meta(&nested)?; + let attrs = PyFunctionSignature::from_meta(&nested)?; args.extend(attrs.arguments) } else { new_attrs.push(attr) @@ -467,21 +497,6 @@ fn parse_method_attributes( *attrs = new_attrs; - let python_name = if allow_custom_name { - match parse_method_name_attribute(ty.as_ref(), attrs)? { - Some(python_name) if property_name.is_some() => { - return Err(syn::Error::new_spanned( - python_name, - "name cannot be specified twice", - )); - } - Some(python_name) => Some(python_name), - None => property_name, - } - } else { - property_name - }; - Ok(MethodAttributes { ty, args, @@ -489,26 +504,4 @@ fn parse_method_attributes( }) } -fn parse_method_name_attribute( - ty: Option<&MethodTypeAttribute>, - attrs: &mut Vec, -) -> syn::Result> { - use MethodTypeAttribute::*; - let name = parse_name_attribute(attrs)?; - - // Reject some invalid combinations - if let (Some(name), Some(ty)) = (&name, ty) { - if let New | Call = ty { - bail_spanned!(name.span() => "name not allowed with this method type"); - } - } - - // Thanks to check above we can be sure that this generates the right python name - Ok(match ty { - Some(New) => Some(syn::Ident::new("__new__", proc_macro2::Span::call_site())), - Some(Call) => Some(syn::Ident::new("__call__", proc_macro2::Span::call_site())), - _ => name, - }) -} - const IMPL_TRAIT_ERR: &str = "Python functions cannot have `impl Trait` arguments"; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index b33439ce..918ff28c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,13 +1,11 @@ // Copyright (c) 2017-present PyO3 Project and Contributors //! Code generation for the function that initializes a python module and adds classes and function. -use crate::method::{self, FnArg}; -use crate::pyfunction::PyFunctionAttr; -use crate::pymethod::{check_generic, get_arg_names, impl_arg_params}; -use crate::utils; +use crate::attributes::{attribute_ident_is, take_attributes, NameAttribute}; +use crate::pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{spanned::Spanned, Ident, Result}; +use quote::quote; +use syn::{parse::Parse, spanned::Spanned, token::Comma, Ident, Path}; /// Generates the function that is called by the python interpreter to initialize the native /// module @@ -35,15 +33,13 @@ pub fn process_functions_in_module(func: &mut syn::ItemFn) -> syn::Result<()> { for stmt in func.block.stmts.iter_mut() { if let syn::Stmt::Item(syn::Item::Fn(func)) = stmt { - if let Some((module_name, python_name, pyfn_attrs)) = - extract_pyfn_attrs(&mut func.attrs)? - { - let function_to_python = add_fn_to_module(func, python_name, pyfn_attrs)?; - let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); + if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { + let module_name = pyfn_args.modname; + let (ident, wrapped_function) = impl_wrap_pyfunction(func, pyfn_args.options)?; let item: syn::ItemFn = syn::parse_quote! { fn block_wrapper() { - #function_to_python - #module_name.add_function(#function_wrapper_ident(#module_name)?)?; + #wrapped_function + #module_name.add_function(#ident(#module_name)?)?; } }; stmts.extend(item.block.stmts.into_iter()); @@ -56,190 +52,49 @@ pub fn process_functions_in_module(func: &mut syn::ItemFn) -> syn::Result<()> { Ok(()) } +pub struct PyFnArgs { + modname: Path, + options: PyFunctionOptions, +} + +impl Parse for PyFnArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let modname = input.parse()?; + let _: Comma = input.parse()?; + let fnname_literal: syn::LitStr = input.parse()?; + let fnname = fnname_literal.parse()?; + if input.is_empty() { + let mut options = PyFunctionOptions::default(); + options.set_name(NameAttribute(fnname))?; + return Ok(Self { modname, options }); + } + let _: Comma = input.parse()?; + let mut options: PyFunctionOptions = input.parse()?; + options.set_name(NameAttribute(fnname))?; + Ok(Self { modname, options }) + } +} + /// Extracts the data from the #[pyfn(...)] attribute of a function -fn extract_pyfn_attrs( - attrs: &mut Vec, -) -> syn::Result> { - let mut new_attrs = Vec::new(); - let mut fnname = None; - let mut modname = None; - let mut fn_attrs = PyFunctionAttr::default(); +fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result> { + let mut pyfn_args: Option = None; - for attr in attrs.drain(..) { - match attr.parse_meta() { - Ok(syn::Meta::List(list)) if list.path.is_ident("pyfn") => { - let meta: Vec<_> = list.nested.iter().cloned().collect(); - if meta.len() >= 2 { - // read module name - match &meta[0] { - syn::NestedMeta::Meta(syn::Meta::Path(path)) => { - modname = Some(path.clone()) - } - _ => bail_spanned!( - meta[0].span() => "the first parameter of pyfn must be a MetaItem" - ), - } - // read Python function name - match &meta[1] { - syn::NestedMeta::Lit(syn::Lit::Str(lits)) => { - fnname = Some(syn::Ident::new(&lits.value(), lits.span())); - } - _ => bail_spanned!( - meta[1].span() => "the second parameter of pyfn must be a Literal" - ), - } - // Read additional arguments - if list.nested.len() >= 3 { - fn_attrs = PyFunctionAttr::from_meta(&meta[2..meta.len()])?; - } - } else { - bail_spanned!( - attr.span() => format!("can not parse 'pyfn' params {:?}", attr) - ); - } - } - _ => new_attrs.push(attr), + take_attributes(attrs, |attr| { + if attribute_ident_is(attr, "pyfn") { + ensure_spanned!( + pyfn_args.is_none(), + attr.span() => "`#[pyfn] may only be specified once" + ); + pyfn_args = Some(attr.parse_args()?); + Ok(true) + } else { + Ok(false) } + })?; + + if let Some(pyfn_args) = &mut pyfn_args { + pyfn_args.options.take_pyo3_attributes(attrs)?; } - *attrs = new_attrs; - match (modname, fnname) { - (Some(modname), Some(fnname)) => Ok(Some((modname, fnname, fn_attrs))), - _ => Ok(None), - } -} - -/// Coordinates the naming of a the add-function-to-python-module function -fn function_wrapper_ident(name: &Ident) -> Ident { - // Make sure this ident matches the one of wrap_pyfunction - format_ident!("__pyo3_get_function_{}", name) -} - -/// Generates python wrapper over a function that allows adding it to a python module as a python -/// function -pub fn add_fn_to_module( - func: &mut syn::ItemFn, - python_name: Ident, - pyfn_attrs: PyFunctionAttr, -) -> syn::Result { - check_generic(&func.sig)?; - - let mut arguments = func - .sig - .inputs - .iter_mut() - .map(FnArg::parse) - .collect::>>()?; - - if pyfn_attrs.pass_module { - const PASS_MODULE_ERR: &str = "expected &PyModule as first argument with `pass_module`"; - ensure_spanned!( - !arguments.is_empty(), - func.span() => PASS_MODULE_ERR - ); - let arg = arguments.remove(0); - ensure_spanned!( - type_is_pymodule(arg.ty), - arg.ty.span() => PASS_MODULE_ERR - ); - } - - let ty = method::get_return_info(&func.sig.output); - - let text_signature = utils::parse_text_signature_attrs(&mut func.attrs, &python_name)?; - let doc = utils::get_doc(&func.attrs, text_signature, true)?; - - let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); - - let spec = method::FnSpec { - tp: method::FnType::FnStatic, - name: &function_wrapper_ident, - python_name, - attrs: pyfn_attrs.arguments, - args: arguments, - output: ty, - doc, - }; - - let doc = &spec.doc; - let python_name = &spec.python_name; - - let name = &func.sig.ident; - let wrapper_ident = format_ident!("__pyo3_raw_{}", name); - let wrapper = function_c_wrapper(name, &wrapper_ident, &spec, pyfn_attrs.pass_module)?; - Ok(quote! { - #wrapper - pub(crate) fn #function_wrapper_ident<'a>( - args: impl Into> - ) -> pyo3::PyResult<&'a pyo3::types::PyCFunction> { - let name = concat!(stringify!(#python_name), "\0"); - pyo3::types::PyCFunction::internal_new( - pyo3::class::methods::PyMethodDef::cfunction_with_keywords( - name, - pyo3::class::methods::PyCFunctionWithKeywords(#wrapper_ident), - #doc, - ), - args.into(), - ) - } - }) -} - -fn type_is_pymodule(ty: &syn::Type) -> bool { - if let syn::Type::Reference(tyref) = ty { - if let syn::Type::Path(typath) = tyref.elem.as_ref() { - if typath - .path - .segments - .last() - .map(|seg| seg.ident == "PyModule") - .unwrap_or(false) - { - return true; - } - } - } - false -} - -/// Generate static function wrapper (PyCFunction, PyCFunctionWithKeywords) -fn function_c_wrapper( - name: &Ident, - wrapper_ident: &Ident, - spec: &method::FnSpec<'_>, - pass_module: bool, -) -> Result { - let names: Vec = get_arg_names(&spec); - let cb; - let slf_module; - if pass_module { - cb = quote! { - pyo3::callback::convert(_py, #name(_slf, #(#names),*)) - }; - slf_module = Some(quote! { - let _slf = _py.from_borrowed_ptr::(_slf); - }); - } else { - cb = quote! { - pyo3::callback::convert(_py, #name(#(#names),*)) - }; - slf_module = None; - }; - let py = syn::Ident::new("_py", Span::call_site()); - let body = impl_arg_params(spec, None, cb, &py)?; - Ok(quote! { - unsafe extern "C" fn #wrapper_ident( - _slf: *mut pyo3::ffi::PyObject, - _args: *mut pyo3::ffi::PyObject, - _kwargs: *mut pyo3::ffi::PyObject) -> *mut pyo3::ffi::PyObject - { - pyo3::callback::handle_panic(|#py| { - #slf_module - let _args = #py.from_borrowed_ptr::(_args); - let _kwargs: Option<&pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); - - #body - }) - } - }) + Ok(pyfn_args) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 1dd8db90..4d568d79 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -499,21 +499,21 @@ fn impl_descriptors( .flat_map(|(field, fns)| { fns.iter() .map(|desc| { - if let Some(name) = field.ident.as_ref().map(|ident| ident.unraw()) { - let doc = utils::get_doc(&field.attrs, None, true) - .unwrap_or_else(|_| syn::LitStr::new(&name.to_string(), name.span())); - let property_type = PropertyType::Descriptor(&field); - match desc { - FnType::Getter(self_ty) => { - impl_py_getter_def(cls, property_type, self_ty, &name, &doc) - } - FnType::Setter(self_ty) => { - impl_py_setter_def(cls, property_type, self_ty, &name, &doc) - } - _ => unreachable!(), + let doc = utils::get_doc(&field.attrs, None, true) + .unwrap_or_else(|_| syn::LitStr::new("", Span::call_site())); + let property_type = PropertyType::Descriptor( + field.ident.as_ref().ok_or_else( + || err_spanned!(field.span() => "`#[pyo3(get, set)]` is not supported on tuple struct fields") + )? + ); + match desc { + FnType::Getter(self_ty) => { + impl_py_getter_def(cls, property_type, self_ty, &doc) } - } else { - bail_spanned!(field.span() => "get/set are not supported on tuple struct field"); + FnType::Setter(self_ty) => { + impl_py_setter_def(cls, property_type, self_ty, &doc) + } + _ => unreachable!(), } }) .collect::>>() diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 15aeedd3..433e47a7 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,13 +1,22 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::attrs::FromPyWithAttribute; -use crate::module::add_fn_to_module; -use proc_macro2::TokenStream; -use syn::ext::IdentExt; -use syn::parse::ParseBuffer; +use crate::{ + attributes::{ + self, get_deprecated_name_attribute, get_pyo3_attribute, take_attributes, + FromPyWithAttribute, NameAttribute, + }, + method::{self, FnArg, FnSpec}, + pymethod::{check_generic, get_arg_names, impl_arg_params}, + utils, +}; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{NestedMeta, Path}; +use syn::{ext::IdentExt, spanned::Spanned, Ident, NestedMeta, Path, Result}; +use syn::{ + parse::{Parse, ParseBuffer, ParseStream}, + token::Comma, +}; #[derive(Debug, Clone, PartialEq)] pub enum Argument { @@ -20,29 +29,69 @@ pub enum Argument { /// The attributes of the pyfunction macro #[derive(Default)] -pub struct PyFunctionAttr { +pub struct PyFunctionSignature { pub arguments: Vec, has_kw: bool, has_varargs: bool, has_kwargs: bool, - pub pass_module: bool, } #[derive(Clone, PartialEq, Debug)] -pub struct PyFunctionArgAttrs { +pub struct PyFunctionArgPyO3Attributes { pub from_py_with: Option, } -impl syn::parse::Parse for PyFunctionAttr { +enum PyFunctionArgPyO3Attribute { + FromPyWith(FromPyWithAttribute), +} + +impl Parse for PyFunctionArgPyO3Attribute { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::from_py_with) { + input.parse().map(PyFunctionArgPyO3Attribute::FromPyWith) + } else { + Err(lookahead.error()) + } + } +} + +impl PyFunctionArgPyO3Attributes { + /// Parses #[pyo3(from_python_with = "func")] + pub fn from_attrs(attrs: &mut Vec) -> syn::Result { + let mut attributes = PyFunctionArgPyO3Attributes { from_py_with: None }; + take_attributes(attrs, |attr| { + if let Some(pyo3_attrs) = get_pyo3_attribute(attr)? { + for attr in pyo3_attrs { + match attr { + 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" + ); + attributes.from_py_with = Some(from_py_with); + } + } + } + Ok(true) + } else { + Ok(false) + } + })?; + Ok(attributes) + } +} + +impl syn::parse::Parse for PyFunctionSignature { fn parse(input: &ParseBuffer) -> syn::Result { let attr = Punctuated::::parse_terminated(input)?; Self::from_meta(&attr) } } -impl PyFunctionAttr { +impl PyFunctionSignature { pub fn from_meta<'a>(iter: impl IntoIterator) -> syn::Result { - let mut slf = PyFunctionAttr::default(); + let mut slf = PyFunctionSignature::default(); for item in iter { slf.add_item(item)? @@ -52,9 +101,6 @@ impl PyFunctionAttr { pub fn add_item(&mut self, item: &NestedMeta) -> syn::Result<()> { match item { - NestedMeta::Meta(syn::Meta::Path(ident)) if ident.is_ident("pass_module") => { - self.pass_module = true; - } NestedMeta::Meta(syn::Meta::Path(ident)) => self.add_work(item, ident)?, NestedMeta::Meta(syn::Meta::NameValue(nv)) => { self.add_name_value(item, nv)?; @@ -159,92 +205,286 @@ impl PyFunctionAttr { } } -pub fn parse_name_attribute(attrs: &mut Vec) -> syn::Result> { - let mut name_attrs = Vec::new(); - - // Using retain will extract all name attributes from the attribute list - attrs.retain(|attr| match attr.parse_meta() { - Ok(syn::Meta::NameValue(nv)) if nv.path.is_ident("name") => { - name_attrs.push((nv.lit, attr.span())); - false - } - _ => true, - }); - - match name_attrs.as_slice() { - [] => Ok(None), - [(syn::Lit::Str(s), span)] => { - let mut ident: syn::Ident = s.parse()?; - // This span is the whole attribute span, which is nicer for reporting errors. - ident.set_span(*span); - Ok(Some(ident)) - } - [(_, span)] => bail_spanned!(*span => "expected string literal for #[name] argument"), - slice => bail_spanned!( - slice[1].1 => "#[name] can not be specified multiple times" - ), - } +#[derive(Default)] +pub struct PyFunctionOptions { + pub pass_module: bool, + pub name: Option, + pub name_is_deprecated: bool, + pub signature: Option, } -pub fn build_py_function(ast: &mut syn::ItemFn, args: PyFunctionAttr) -> syn::Result { - let python_name = - parse_name_attribute(&mut ast.attrs)?.unwrap_or_else(|| ast.sig.ident.unraw()); - add_fn_to_module(ast, python_name, args) -} +impl Parse for PyFunctionOptions { + fn parse(input: ParseStream) -> Result { + let mut options = PyFunctionOptions { + pass_module: false, + name: None, + name_is_deprecated: false, + signature: None, + }; -fn extract_pyo3_metas(attrs: &mut Vec) -> syn::Result> { - let mut new_attrs = Vec::new(); - let mut metas = Vec::new(); - - for attr in attrs.drain(..) { - if let syn::Meta::List(meta_list) = attr.parse_meta()? { - if meta_list.path.is_ident("pyo3") { - for meta in meta_list.nested { - metas.push(meta); + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::name) + || lookahead.peek(attributes::kw::pass_module) + || lookahead.peek(attributes::kw::signature) + { + options.add_attributes(std::iter::once(input.parse()?))?; + if !input.is_empty() { + let _: Comma = input.parse()?; } } else { - new_attrs.push(attr) + // If not recognised attribute, this is "legacy" pyfunction syntax #[pyfunction(a, b)] + // + // TODO deprecate in favour of #[pyfunction(signature = (a, b), name = "foo")] + options.signature = Some(input.parse()?); + break; } } - } - *attrs = new_attrs; - Ok(metas) + Ok(options) + } } -impl PyFunctionArgAttrs { - /// Parses #[pyo3(from_python_with = "func")] +pub enum PyFunctionOption { + Name(NameAttribute), + PassModule(attributes::kw::pass_module), + Signature(PyFunctionSignature), +} + +impl Parse for PyFunctionOption { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::name) { + input.parse().map(PyFunctionOption::Name) + } else if lookahead.peek(attributes::kw::pass_module) { + input.parse().map(PyFunctionOption::PassModule) + } else if lookahead.peek(attributes::kw::signature) { + input.parse().map(PyFunctionOption::Signature) + } else { + Err(lookahead.error()) + } + } +} + +impl PyFunctionOptions { pub fn from_attrs(attrs: &mut Vec) -> syn::Result { - let mut from_py_with = None; + let mut options = PyFunctionOptions::default(); + options.take_pyo3_attributes(attrs)?; + Ok(options) + } - for meta in extract_pyo3_metas(attrs)? { - let meta = match meta { - NestedMeta::Meta(meta) => meta, - NestedMeta::Lit(lit) => { - bail_spanned!(lit.span() => "expected `from_py_with`, got a literal") - } - }; - - if meta.path().is_ident("from_py_with") { - from_py_with = Some(FromPyWithAttribute::from_meta(meta)?); + pub fn take_pyo3_attributes(&mut self, attrs: &mut Vec) -> syn::Result<()> { + take_attributes(attrs, |attr| { + if let Some(pyo3_attributes) = get_pyo3_attribute(attr)? { + self.add_attributes(pyo3_attributes)?; + Ok(true) + } else if let Some(name) = get_deprecated_name_attribute(attr)? { + self.set_name(name)?; + self.name_is_deprecated = true; + Ok(true) } else { - bail_spanned!(meta.span() => "only `from_py_with` is supported") + Ok(false) + } + })?; + + Ok(()) + } + + pub fn add_attributes( + &mut self, + attrs: impl IntoIterator, + ) -> Result<()> { + for attr in attrs { + match attr { + PyFunctionOption::Name(name) => self.set_name(name)?, + PyFunctionOption::PassModule(kw) => { + ensure_spanned!( + !self.pass_module, + kw.span() => "`pass_module` may only be specified once" + ); + self.pass_module = true; + } + PyFunctionOption::Signature(signature) => { + ensure_spanned!( + self.signature.is_none(), + // FIXME: improve the span of this error message + Span::call_site() => "`signature` may only be specified once" + ); + self.signature = Some(signature); + } } } - - Ok(PyFunctionArgAttrs { from_py_with }) + Ok(()) } + + pub fn set_name(&mut self, name: NameAttribute) -> Result<()> { + ensure_spanned!( + self.name.is_none(), + name.0.span() => "`name` may only be specified once" + ); + self.name = Some(name); + Ok(()) + } +} + +pub fn build_py_function( + ast: &mut syn::ItemFn, + mut options: PyFunctionOptions, +) -> syn::Result { + options.take_pyo3_attributes(&mut ast.attrs)?; + Ok(impl_wrap_pyfunction(ast, options)?.1) +} + +/// Coordinates the naming of a the add-function-to-python-module function +fn function_wrapper_ident(name: &Ident) -> Ident { + // Make sure this ident matches the one of wrap_pyfunction + format_ident!("__pyo3_get_function_{}", name) +} + +/// Generates python wrapper over a function that allows adding it to a python module as a python +/// function +pub fn impl_wrap_pyfunction( + func: &mut syn::ItemFn, + options: PyFunctionOptions, +) -> syn::Result<(Ident, TokenStream)> { + check_generic(&func.sig)?; + + let python_name = options + .name + .map_or_else(|| func.sig.ident.unraw(), |name| name.0); + + let signature = options.signature.unwrap_or_default(); + + let mut arguments = func + .sig + .inputs + .iter_mut() + .map(FnArg::parse) + .collect::>>()?; + + if options.pass_module { + const PASS_MODULE_ERR: &str = "expected &PyModule as first argument with `pass_module`"; + ensure_spanned!( + !arguments.is_empty(), + func.span() => PASS_MODULE_ERR + ); + let arg = arguments.remove(0); + ensure_spanned!( + type_is_pymodule(arg.ty), + arg.ty.span() => PASS_MODULE_ERR + ); + } + + let ty = method::get_return_info(&func.sig.output); + + let text_signature = utils::parse_text_signature_attrs(&mut func.attrs, &python_name)?; + let doc = utils::get_doc(&func.attrs, text_signature, true)?; + + let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); + + let spec = method::FnSpec { + tp: method::FnType::FnStatic, + name: &function_wrapper_ident, + python_name, + attrs: signature.arguments, + args: arguments, + output: ty, + doc, + name_is_deprecated: options.name_is_deprecated, + }; + + let doc = &spec.doc; + let python_name = spec.python_name_with_deprecation(); + + let name = &func.sig.ident; + let wrapper_ident = format_ident!("__pyo3_raw_{}", name); + let wrapper = function_c_wrapper(name, &wrapper_ident, &spec, options.pass_module)?; + let wrapped_pyfunction = quote! { + #wrapper + pub(crate) fn #function_wrapper_ident<'a>( + args: impl Into> + ) -> pyo3::PyResult<&'a pyo3::types::PyCFunction> { + pyo3::types::PyCFunction::internal_new( + pyo3::class::methods::PyMethodDef::cfunction_with_keywords( + #python_name, + pyo3::class::methods::PyCFunctionWithKeywords(#wrapper_ident), + #doc, + ), + args.into(), + ) + } + }; + Ok((function_wrapper_ident, wrapped_pyfunction)) +} + +/// Generate static function wrapper (PyCFunction, PyCFunctionWithKeywords) +fn function_c_wrapper( + name: &Ident, + wrapper_ident: &Ident, + spec: &FnSpec<'_>, + pass_module: bool, +) -> Result { + let names: Vec = get_arg_names(&spec); + let cb; + let slf_module; + if pass_module { + cb = quote! { + pyo3::callback::convert(_py, #name(_slf, #(#names),*)) + }; + slf_module = Some(quote! { + let _slf = _py.from_borrowed_ptr::(_slf); + }); + } else { + cb = quote! { + pyo3::callback::convert(_py, #name(#(#names),*)) + }; + slf_module = None; + }; + let py = syn::Ident::new("_py", Span::call_site()); + let body = impl_arg_params(spec, None, cb, &py)?; + Ok(quote! { + unsafe extern "C" fn #wrapper_ident( + _slf: *mut pyo3::ffi::PyObject, + _args: *mut pyo3::ffi::PyObject, + _kwargs: *mut pyo3::ffi::PyObject) -> *mut pyo3::ffi::PyObject + { + pyo3::callback::handle_panic(|#py| { + #slf_module + let _args = #py.from_borrowed_ptr::(_args); + let _kwargs: Option<&pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs); + + #body + }) + } + }) +} + +fn type_is_pymodule(ty: &syn::Type) -> bool { + if let syn::Type::Reference(tyref) = ty { + if let syn::Type::Path(typath) = tyref.elem.as_ref() { + if typath + .path + .segments + .last() + .map(|seg| seg.ident == "PyModule") + .unwrap_or(false) + { + return true; + } + } + } + false } #[cfg(test)] mod test { - use super::{Argument, PyFunctionAttr}; + use super::{Argument, PyFunctionSignature}; use proc_macro2::TokenStream; use quote::quote; use syn::parse_quote; fn items(input: TokenStream) -> syn::Result> { - let py_fn_attr: PyFunctionAttr = syn::parse2(input)?; + let py_fn_attr: PyFunctionSignature = syn::parse2(input)?; Ok(py_fn_attr.arguments) } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 27ad7933..33b966dc 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,6 +1,10 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::pymethod; +use crate::{ + konst::{ConstAttributes, ConstSpec}, + pyfunction::PyFunctionOptions, + pymethod, +}; use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; use quote::quote; @@ -39,7 +43,8 @@ pub fn impl_methods( for iimpl in impls.iter_mut() { match iimpl { syn::ImplItem::Method(meth) => { - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs)? { + let options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; + match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, options)? { GeneratedPyMethod::Method(token_stream) => { let attrs = get_cfg_attributes(&meth.attrs); methods.push(quote!(#(#attrs)* #token_stream)); @@ -55,8 +60,14 @@ pub fn impl_methods( } } syn::ImplItem::Const(konst) => { - if let Some(meth) = pymethod::gen_py_const(ty, &konst.ident, &mut konst.attrs)? { + let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; + if attributes.is_class_attr { + let spec = ConstSpec { + rust_ident: konst.ident.clone(), + attributes, + }; let attrs = get_cfg_attributes(&konst.attrs); + let meth = pymethod::gen_py_const(ty, &spec); methods.push(quote!(#(#attrs)* #meth)); } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 4e0fc3fd..afe8d897 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,14 +1,17 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::attrs::FromPyWithAttribute; -use crate::konst::ConstSpec; -use crate::method::{FnArg, FnSpec, FnType, SelfType}; use crate::utils; +use crate::{attributes::FromPyWithAttribute, konst::ConstSpec}; +use crate::{ + method::{FnArg, FnSpec, FnType, SelfType}, + pyfunction::PyFunctionOptions, +}; use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::{ext::IdentExt, spanned::Spanned, Result}; +#[derive(Clone, Copy)] pub enum PropertyType<'a> { - Descriptor(&'a syn::Field), + Descriptor(&'a syn::Ident), Function(&'a FnSpec<'a>), } @@ -22,9 +25,10 @@ pub fn gen_py_method( cls: &syn::Type, sig: &mut syn::Signature, meth_attrs: &mut Vec, + options: PyFunctionOptions, ) -> Result { check_generic(sig)?; - let spec = FnSpec::parse(sig, &mut *meth_attrs, true)?; + let spec = FnSpec::parse(sig, &mut *meth_attrs, options)?; Ok(match &spec.tp { FnType::Fn(self_ty) => { @@ -43,14 +47,12 @@ pub fn gen_py_method( cls, PropertyType::Function(&spec), self_ty, - &spec.python_name, &spec.doc, )?), FnType::Setter(self_ty) => GeneratedPyMethod::Method(impl_py_setter_def( cls, PropertyType::Function(&spec), self_ty, - &spec.python_name, &spec.doc, )?), }) @@ -68,22 +70,15 @@ pub(crate) fn check_generic(sig: &syn::Signature) -> syn::Result<()> { Ok(()) } -pub fn gen_py_const( - cls: &syn::Type, - name: &syn::Ident, - attrs: &mut Vec, -) -> syn::Result> { - let spec = ConstSpec::parse(name, attrs)?; - if spec.is_class_attr { - let wrapper = quote! {{ - fn __wrap(py: pyo3::Python<'_>) -> pyo3::PyObject { - pyo3::IntoPy::into_py(#cls::#name, py) - } - __wrap - }}; - return Ok(Some(impl_py_const_class_attribute(&spec, &wrapper))); - } - Ok(None) +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { + let member = &spec.rust_ident; + let wrapper = quote! {{ + fn __wrap(py: pyo3::Python<'_>) -> pyo3::PyObject { + pyo3::IntoPy::into_py(#cls::#member, py) + } + __wrap + }}; + impl_py_const_class_attribute(&spec, &wrapper) } /// Generate function wrapper for PyCFunctionWithKeywords @@ -255,12 +250,9 @@ pub(crate) fn impl_wrap_getter( property_type: PropertyType, self_ty: &SelfType, ) -> syn::Result { - let getter_impl = match property_type { - PropertyType::Descriptor(field) => { - let name = field.ident.as_ref().unwrap(); - quote!({ - _slf.#name.clone() - }) + let getter_impl = match &property_type { + PropertyType::Descriptor(ident) => { + quote!(_slf.#ident.clone()) } PropertyType::Function(spec) => impl_call_getter(cls, spec)?, }; @@ -307,10 +299,9 @@ pub(crate) fn impl_wrap_setter( property_type: PropertyType, self_ty: &SelfType, ) -> syn::Result { - let setter_impl = match property_type { - PropertyType::Descriptor(field) => { - let name = field.ident.as_ref().unwrap(); - quote!({ _slf.#name = _val; }) + let setter_impl = match &property_type { + PropertyType::Descriptor(ident) => { + quote!({ _slf.#ident = _val; }) } PropertyType::Function(spec) => impl_call_setter(cls, spec)?, }; @@ -584,14 +575,14 @@ pub fn impl_py_method_def( flags: Option, ) -> Result { let add_flags = flags.map(|flags| quote!(.flags(#flags))); - let python_name = &spec.python_name; + let python_name = spec.python_name_with_deprecation(); let doc = &spec.doc; if spec.args.is_empty() { let wrapper = impl_wrap_noargs(cls, spec, self_ty); Ok(quote! { pyo3::class::PyMethodDefType::Method({ pyo3::class::PyMethodDef::noargs( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyCFunction(#wrapper), #doc ) @@ -604,7 +595,7 @@ pub fn impl_py_method_def( Ok(quote! { pyo3::class::PyMethodDefType::Method({ pyo3::class::PyMethodDef::cfunction_with_keywords( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), #doc ) @@ -627,12 +618,12 @@ pub fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result Result { let wrapper = impl_wrap_class(cls, &spec)?; - let python_name = &spec.python_name; + let python_name = spec.python_name_with_deprecation(); let doc = &spec.doc; Ok(quote! { pyo3::class::PyMethodDefType::Class({ pyo3::class::PyMethodDef::cfunction_with_keywords( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), #doc ).flags(pyo3::ffi::METH_CLASS) @@ -642,12 +633,12 @@ pub fn impl_py_method_def_class(cls: &syn::Type, spec: &FnSpec) -> Result Result { let wrapper = impl_wrap_static(cls, &spec)?; - let python_name = &spec.python_name; + let python_name = spec.python_name_with_deprecation(); let doc = &spec.doc; Ok(quote! { pyo3::class::PyMethodDefType::Static({ pyo3::class::PyMethodDef::cfunction_with_keywords( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), #doc ).flags(pyo3::ffi::METH_STATIC) @@ -657,11 +648,11 @@ pub fn impl_py_method_def_static(cls: &syn::Type, spec: &FnSpec) -> Result TokenStream { let wrapper = impl_wrap_class_attribute(cls, &spec); - let python_name = &spec.python_name; + let python_name = spec.python_name_with_deprecation(); quote! { pyo3::class::PyMethodDefType::ClassAttribute({ pyo3::class::PyClassAttributeDef::new( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyClassAttributeFactory(#wrapper) ) }) @@ -669,14 +660,16 @@ pub fn impl_py_method_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenSt } pub fn impl_py_const_class_attribute(spec: &ConstSpec, wrapper: &TokenStream) -> TokenStream { - let python_name = &spec.python_name; + let python_name = &spec.python_name_with_deprecation(); quote! { - pyo3::class::PyMethodDefType::ClassAttribute({ - pyo3::class::PyClassAttributeDef::new( - concat!(stringify!(#python_name), "\0"), - pyo3::class::methods::PyClassAttributeFactory(#wrapper) - ) - }) + { + pyo3::class::PyMethodDefType::ClassAttribute({ + pyo3::class::PyClassAttributeDef::new( + #python_name, + pyo3::class::methods::PyClassAttributeFactory(#wrapper) + ) + }) + } } } @@ -699,14 +692,20 @@ pub(crate) fn impl_py_setter_def( cls: &syn::Type, property_type: PropertyType, self_ty: &SelfType, - python_name: &syn::Ident, doc: &syn::LitStr, ) -> Result { + let python_name = match property_type { + PropertyType::Descriptor(ident) => { + let formatted_name = format!("{}\0", ident.unraw()); + quote!(#formatted_name) + } + PropertyType::Function(spec) => spec.python_name_with_deprecation(), + }; let wrapper = impl_wrap_setter(cls, property_type, self_ty)?; Ok(quote! { pyo3::class::PyMethodDefType::Setter({ pyo3::class::PySetterDef::new( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PySetter(#wrapper), #doc ) @@ -718,14 +717,20 @@ pub(crate) fn impl_py_getter_def( cls: &syn::Type, property_type: PropertyType, self_ty: &SelfType, - python_name: &syn::Ident, doc: &syn::LitStr, ) -> Result { + let python_name = match property_type { + PropertyType::Descriptor(ident) => { + let formatted_name = format!("{}\0", ident.unraw()); + quote!(#formatted_name) + } + PropertyType::Function(spec) => spec.python_name_with_deprecation(), + }; let wrapper = impl_wrap_getter(cls, property_type, self_ty)?; Ok(quote! { pyo3::class::PyMethodDefType::Getter({ pyo3::class::PyGetterDef::new( - concat!(stringify!(#python_name), "\0"), + #python_name, pyo3::class::methods::PyGetter(#wrapper), #doc ) diff --git a/pyo3-macros-backend/src/pyproto.rs b/pyo3-macros-backend/src/pyproto.rs index a1b0c3c3..de69f201 100644 --- a/pyo3-macros-backend/src/pyproto.rs +++ b/pyo3-macros-backend/src/pyproto.rs @@ -3,6 +3,7 @@ use crate::defs; use crate::method::{FnSpec, FnType}; use crate::proto_method::impl_method_proto; +use crate::pyfunction::PyFunctionOptions; use crate::pymethod; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -62,7 +63,8 @@ fn impl_proto_impl( } // Add non-slot methods to inventory like `#[pymethods]` if let Some(m) = proto.get_method(&met.sig.ident) { - let fn_spec = FnSpec::parse(&mut met.sig, &mut met.attrs, false)?; + let fn_spec = + FnSpec::parse(&mut met.sig, &mut met.attrs, PyFunctionOptions::default())?; let flags = if m.can_coexist { // We need METH_COEXIST here to prevent __add__ from overriding __radd__ diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 991361c9..db2e96a6 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,5 +1,6 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; +use quote::quote_spanned; use syn::spanned::Spanned; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. @@ -164,3 +165,14 @@ pub fn get_doc( Ok(syn::LitStr::new(&doc, span)) } + +pub fn name_deprecation_token(span: Span, name_is_deprecated: bool) -> Option { + if name_is_deprecated { + Some(quote_spanned!( + span => + let _ = pyo3::impl_::deprecations::NAME_ATTRIBUTE; + )) + } else { + None + } +} diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 152df18b..faa09db3 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -9,7 +9,7 @@ use proc_macro::TokenStream; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods, build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType, - PyFunctionAttr, + PyFunctionOptions, }; use quote::quote; use syn::parse_macro_input; @@ -217,9 +217,9 @@ pub fn pymethods_with_inventory(_: TokenStream, input: TokenStream) -> TokenStre #[proc_macro_attribute] pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemFn); - let args = parse_macro_input!(attr as PyFunctionAttr); + let options = parse_macro_input!(attr as PyFunctionOptions); - let expanded = build_py_function(&mut ast, args).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_py_function(&mut ast, options).unwrap_or_else(|e| e.to_compile_error()); quote!( #ast diff --git a/src/impl_/mod.rs b/src/impl_/mod.rs new file mode 100644 index 00000000..5cb35475 --- /dev/null +++ b/src/impl_/mod.rs @@ -0,0 +1,13 @@ +//! Internals of PyO3 which are accessed by code expanded from PyO3's procedural macros. Usage of +//! any of these APIs in downstream code is implicitly acknowledging that these APIs may change at +//! any time without documentation in the CHANGELOG and without breaking semver guarantees. + +/// Symbols to represent deprecated uses of PyO3's macros. +pub mod deprecations { + #[doc(hidden)] + #[deprecated( + since = "0.14.0", + note = "use `#[pyo3(name = \"...\")]` instead of `#[name = \"...\"]`" + )] + pub const NAME_ATTRIBUTE: () = (); +} diff --git a/src/lib.rs b/src/lib.rs index 92537de5..d87d45dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,6 +177,7 @@ pub mod exceptions; pub mod ffi; pub mod freelist; mod gil; +pub mod impl_; mod instance; #[cfg(not(Py_LIMITED_API))] diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index e4893d63..91212754 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -19,13 +19,17 @@ impl Foo { #[classattr] const MY_CONST: &'static str = "foobar"; + #[classattr] + #[pyo3(name = "RENAMED_CONST")] + const MY_CONST_2: &'static str = "foobar_2"; + #[classattr] fn a() -> i32 { 5 } #[classattr] - #[name = "B"] + #[pyo3(name = "B")] fn b() -> String { "bar".to_string() } @@ -46,9 +50,11 @@ fn class_attributes() { let gil = Python::acquire_gil(); let py = gil.python(); let foo_obj = py.get_type::(); + py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); + py_assert!(py, foo_obj, "foo_obj.RENAMED_CONST == 'foobar_2'"); py_assert!(py, foo_obj, "foo_obj.a == 5"); py_assert!(py, foo_obj, "foo_obj.B == 'bar'"); - py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); + py_assert!(py, foo_obj, "foo_obj.foo.x == 1"); } // Ignored because heap types are not immutable: diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index b97db234..a44ee8b7 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -86,15 +86,15 @@ struct EmptyClass2 {} #[pymethods] impl EmptyClass2 { - #[name = "custom_fn"] + #[pyo3(name = "custom_fn")] fn bar(&self) {} #[staticmethod] - #[name = "custom_static"] + #[pyo3(name = "custom_static")] fn bar_static() {} #[getter] - #[name = "custom_getter"] + #[pyo3(name = "custom_getter")] fn foo(&self) -> i32 { 5 } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index df616dea..18bf31c8 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -34,6 +34,7 @@ fn test_compile_errors() { #[rustversion::since(1.49)] fn tests_rust_1_49(t: &trybuild::TestCases) { + t.compile_fail("tests/ui/deprecations.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); diff --git a/tests/test_module.rs b/tests/test_module.rs index 303306c1..9267ebb6 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -192,7 +192,7 @@ fn test_raw_idents() { } #[pyfunction] -#[name = "foobar"] +#[pyo3(name = "foobar")] fn custom_named_fn() -> usize { 42 } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs new file mode 100644 index 00000000..5617838e --- /dev/null +++ b/tests/ui/deprecations.rs @@ -0,0 +1,33 @@ +#![deny(deprecated)] + +use pyo3::prelude::*; + +#[pyclass] +struct TestClass { + num: u32, +} + +#[pymethods] +impl TestClass { + #[classattr] + #[name = "num"] + const DEPRECATED_NAME_CONSTANT: i32 = 0; + + #[name = "num"] + fn deprecated_name_pymethod(&self) { } + + #[staticmethod] + #[name = "custom_static"] + fn deprecated_name_staticmethod() {} +} + +#[pyfunction] +#[name = "foo"] +fn deprecated_name_pyfunction() { } + +fn main() { + +} + + +// TODO: ensure name deprecated on #[pyfunction] and #[pymodule] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr new file mode 100644 index 00000000..18e50823 --- /dev/null +++ b/tests/ui/deprecations.stderr @@ -0,0 +1,29 @@ +error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` + --> $DIR/deprecations.rs:13:5 + | +13 | #[name = "num"] + | ^ + | +note: the lint level is defined here + --> $DIR/deprecations.rs:1:9 + | +1 | #![deny(deprecated)] + | ^^^^^^^^^^ + +error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` + --> $DIR/deprecations.rs:16:5 + | +16 | #[name = "num"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` + --> $DIR/deprecations.rs:20:5 + | +20 | #[name = "custom_static"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` + --> $DIR/deprecations.rs:25:1 + | +25 | #[name = "foo"] + | ^ diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index f1e37518..e81eefbe 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -1,22 +1,22 @@ -error: only `from_py_with` is supported +error: expected `from_py_with` --> $DIR/invalid_argument_attributes.rs:4:29 | 4 | fn invalid_attribute(#[pyo3(get)] param: String) {} | ^^^ -error: expected a name-value: `pyo3(from_py_with = "func")` - --> $DIR/invalid_argument_attributes.rs:7:33 +error: expected `=` + --> $DIR/invalid_argument_attributes.rs:7:32 | 7 | fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} - | ^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^ -error: expected `from_py_with`, got a literal +error: expected `from_py_with` --> $DIR/invalid_argument_attributes.rs:10:31 | 10 | fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} | ^^^^^^^^^^^^^^ -error: expected literal +error: expected string literal --> $DIR/invalid_argument_attributes.rs:13:58 | 13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index ea8a96df..3a0b128f 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -84,23 +84,23 @@ error: transparent structs and variants can only have 1 field 70 | | }, | |_____^ -error: expected `attribute`, `item` or `from_py_with` +error: expected one of: `attribute`, `item`, `from_py_with` --> $DIR/invalid_frompy_derive.rs:76:12 | 76 | #[pyo3(attr)] | ^^^^ -error: expected a single string literal argument - --> $DIR/invalid_frompy_derive.rs:82:12 +error: expected string literal + --> $DIR/invalid_frompy_derive.rs:82:22 | 82 | #[pyo3(attribute(1))] - | ^^^^^^^^^ + | ^ -error: expected a single string literal argument - --> $DIR/invalid_frompy_derive.rs:88:12 +error: expected at most one argument: `attribute` or `attribute("name")` + --> $DIR/invalid_frompy_derive.rs:88:25 | 88 | #[pyo3(attribute("a", "b"))] - | ^^^^^^^^^ + | ^ error: attribute name cannot be empty --> $DIR/invalid_frompy_derive.rs:94:22 @@ -108,43 +108,43 @@ error: attribute name cannot be empty 94 | #[pyo3(attribute(""))] | ^^ -error: expected a single string literal argument - --> $DIR/invalid_frompy_derive.rs:100:12 +error: unexpected end of input, expected string literal + --> $DIR/invalid_frompy_derive.rs:100:21 | 100 | #[pyo3(attribute())] - | ^^^^^^^^^ + | ^^ -error: expected a single literal argument - --> $DIR/invalid_frompy_derive.rs:106:12 +error: expected at most one argument: `item` or `item(key)` + --> $DIR/invalid_frompy_derive.rs:106:20 | 106 | #[pyo3(item("a", "b"))] - | ^^^^ + | ^ -error: expected a single literal argument - --> $DIR/invalid_frompy_derive.rs:112:12 +error: unexpected end of input, expected literal + --> $DIR/invalid_frompy_derive.rs:112:16 | 112 | #[pyo3(item())] - | ^^^^ + | ^^ error: only one of `attribute` or `item` can be provided - --> $DIR/invalid_frompy_derive.rs:118:18 + --> $DIR/invalid_frompy_derive.rs:118:5 | 118 | #[pyo3(item, attribute)] - | ^^^^^^^^^ + | ^ -error: unknown `pyo3` container attribute +error: expected `transparent` or `annotation` --> $DIR/invalid_frompy_derive.rs:123:8 | 123 | #[pyo3(unknown = "should not work")] | ^^^^^^^ -error: annotation is not supported for structs +error: `annotation` is unsupported for structs --> $DIR/invalid_frompy_derive.rs:129:21 | 129 | #[pyo3(annotation = "should not work")] | ^^^^^^^^^^^^^^^^^ -error: expected string literal for annotation +error: expected string literal --> $DIR/invalid_frompy_derive.rs:136:25 | 136 | #[pyo3(annotation = 1)] @@ -170,13 +170,13 @@ error: cannot derive FromPyObject for empty structs and variants | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected a name-value: `pyo3(from_py_with = "func")` - --> $DIR/invalid_frompy_derive.rs:158:12 +error: expected `=` + --> $DIR/invalid_frompy_derive.rs:158:11 | 158 | #[pyo3(from_py_with)] - | ^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^ -error: expected literal +error: expected string literal --> $DIR/invalid_frompy_derive.rs:164:27 | 164 | #[pyo3(from_py_with = func)] diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 8e61fd28..15ad11e8 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -16,7 +16,7 @@ error: setter function can have at most two arguments ([pyo3::Python,] and value 24 | fn setter_with_too_many_args(&mut self, py: Python, foo: u32, bar: u32) {} | ^^^ -error: get/set are not supported on tuple struct field +error: `#[pyo3(get, set)]` is not supported on tuple struct fields --> $DIR/invalid_property_args.rs:28:44 | 28 | struct TupleGetterSetter(#[pyo3(get, set)] i32); diff --git a/tests/ui/invalid_pymethod_names.rs b/tests/ui/invalid_pymethod_names.rs index 41834fa1..337cb2a3 100644 --- a/tests/ui/invalid_pymethod_names.rs +++ b/tests/ui/invalid_pymethod_names.rs @@ -7,21 +7,21 @@ struct TestClass { #[pymethods] impl TestClass { - #[name = "num"] + #[pyo3(name = "num")] #[getter(number)] fn get_num(&self) -> u32 { self.num } } #[pymethods] impl TestClass { - #[name = "foo"] - #[name = "bar"] + #[pyo3(name = "foo")] + #[pyo3(name = "bar")] fn qux(&self) -> u32 { self.num } } #[pymethods] impl TestClass { - #[name = "makenew"] + #[pyo3(name = "makenew")] #[new] fn new(&self) -> Self { Self { num: 0 } } } diff --git a/tests/ui/invalid_pymethod_names.stderr b/tests/ui/invalid_pymethod_names.stderr index fe4d0750..8261aef7 100644 --- a/tests/ui/invalid_pymethod_names.stderr +++ b/tests/ui/invalid_pymethod_names.stderr @@ -1,17 +1,17 @@ -error: name cannot be specified twice - --> $DIR/invalid_pymethod_names.rs:10:5 +error: `name` may only be specified once + --> $DIR/invalid_pymethod_names.rs:10:19 | -10 | #[name = "num"] - | ^ +10 | #[pyo3(name = "num")] + | ^^^^^ -error: #[name] can not be specified multiple times - --> $DIR/invalid_pymethod_names.rs:18:5 +error: `name` may only be specified once + --> $DIR/invalid_pymethod_names.rs:18:19 | -18 | #[name = "bar"] - | ^ +18 | #[pyo3(name = "bar")] + | ^^^^^ -error: name not allowed with this method type - --> $DIR/invalid_pymethod_names.rs:24:5 +error: `name` not allowed with `#[new]` + --> $DIR/invalid_pymethod_names.rs:24:19 | -24 | #[name = "makenew"] - | ^ +24 | #[pyo3(name = "makenew")] + | ^^^^^^^^^ diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 78b2044d..a082ccc0 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -9,6 +9,12 @@ impl MyClass { fn class_attr_with_args(foo: i32) {} } +#[pymethods] +impl MyClass { + #[classattr(foobar)] + const CLASS_ATTR_WITH_ATTRIBUTE_ARG: i32 = 3; +} + #[pymethods] impl MyClass { fn staticmethod_without_attribute() {} diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index b4539af2..04c8a05e 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -4,80 +4,86 @@ error: class attribute methods cannot take arguments 9 | fn class_attr_with_args(foo: i32) {} | ^^^ -error: static method needs #[staticmethod] attribute +error: `#[classattr]` does not take any arguments --> $DIR/invalid_pymethods.rs:14:5 | -14 | fn staticmethod_without_attribute() {} +14 | #[classattr(foobar)] + | ^ + +error: static method needs #[staticmethod] attribute + --> $DIR/invalid_pymethods.rs:20:5 + | +20 | fn staticmethod_without_attribute() {} | ^^ error: unexpected receiver - --> $DIR/invalid_pymethods.rs:20:35 + --> $DIR/invalid_pymethods.rs:26:35 | -20 | fn staticmethod_with_receiver(&self) {} +26 | fn staticmethod_with_receiver(&self) {} | ^ error: expected receiver for #[getter] - --> $DIR/invalid_pymethods.rs:33:5 + --> $DIR/invalid_pymethods.rs:39:5 | -33 | fn getter_without_receiver() {} +39 | fn getter_without_receiver() {} | ^^ error: expected receiver for #[setter] - --> $DIR/invalid_pymethods.rs:39:5 + --> $DIR/invalid_pymethods.rs:45:5 | -39 | fn setter_without_receiver() {} +45 | fn setter_without_receiver() {} | ^^ error: text_signature not allowed on __new__; if you want to add a signature on __new__, put it on the struct definition instead - --> $DIR/invalid_pymethods.rs:45:24 + --> $DIR/invalid_pymethods.rs:51:24 | -45 | #[text_signature = "()"] +51 | #[text_signature = "()"] | ^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:52:24 + --> $DIR/invalid_pymethods.rs:58:24 | -52 | #[text_signature = "()"] +58 | #[text_signature = "()"] | ^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:59:24 + --> $DIR/invalid_pymethods.rs:65:24 | -59 | #[text_signature = "()"] +65 | #[text_signature = "()"] | ^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:66:24 + --> $DIR/invalid_pymethods.rs:72:24 | -66 | #[text_signature = "()"] +72 | #[text_signature = "()"] | ^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:73:24 + --> $DIR/invalid_pymethods.rs:79:24 | -73 | #[text_signature = "()"] +79 | #[text_signature = "()"] | ^^^^ error: cannot specify a second method type - --> $DIR/invalid_pymethods.rs:80:7 + --> $DIR/invalid_pymethods.rs:86:7 | -80 | #[staticmethod] +86 | #[staticmethod] | ^^^^^^^^^^^^ error: Python functions cannot have generic type parameters - --> $DIR/invalid_pymethods.rs:86:23 + --> $DIR/invalid_pymethods.rs:92:23 | -86 | fn generic_method(value: T) {} +92 | fn generic_method(value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> $DIR/invalid_pymethods.rs:92:48 + --> $DIR/invalid_pymethods.rs:98:48 | -92 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} +98 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> $DIR/invalid_pymethods.rs:97:56 - | -97 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} - | ^^^^ + --> $DIR/invalid_pymethods.rs:103:56 + | +103 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} + | ^^^^