diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 9f163df9ed2..def32ecc614 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -489,8 +489,118 @@ If the input is neither a string nor an integer, the error message will be: - the argument must be the name of the function as a string. - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. +### `IntoPyObject` +This trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, +as does a `#[pyclass]` which doesn't use `extends`. + +Occasionally you may choose to implement this for custom types which are mapped to Python types +_without_ having a unique python type. + +#### derive macro + +`IntoPyObject` can be implemented using our derive macro. Both `struct`s and `enum`s are supported. + +`struct`s will turn into a `PyDict` using the field names as keys, tuple `struct`s will turn convert +into `PyTuple` with the fields in declaration order. +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use std::collections::HashMap; +# use std::hash::Hash; + +// structs convert into `PyDict` with field names as keys +#[derive(IntoPyObject)] +struct Struct { + count: usize, + obj: Py, +} + +// tuple structs convert into `PyTuple` +// lifetimes and generics are supported, the impl will be bounded by +// `K: IntoPyObject, V: IntoPyObject` +#[derive(IntoPyObject)] +struct Tuple<'a, K: Hash + Eq, V>(&'a str, HashMap); +``` + +For structs with a single field (newtype pattern) the `#[pyo3(transparent)]` option can be used to +forward the implementation to the inner type. + + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; + +// newtype tuple structs are implicitly `transparent` +#[derive(IntoPyObject)] +struct TransparentTuple(PyObject); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentStruct<'py> { + inner: Bound<'py, PyAny>, // `'py` lifetime will be used as the Python lifetime +} +``` + +For `enum`s each variant is converted according to the rules for `struct`s above. + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use std::collections::HashMap; +# use std::hash::Hash; + +#[derive(IntoPyObject)] +enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using the same + TransparentTuple(PyObject), // rules on the variants as the structs above + #[pyo3(transparent)] + TransparentStruct { inner: Bound<'py, PyAny> }, + Tuple(&'a str, HashMap), + Struct { count: usize, obj: Py } +} +``` + +#### manual implementation + +If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as +demonstrated below. + +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +struct MyPyObjectWrapper(PyObject); + +impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` + type Error = std::convert::Infallible; // the conversion error type, has to be convertable to `PyErr` + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.into_bound(py)) + } +} + +// equivalent to former `ToPyObject` implementations +impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { + type Target = PyAny; + type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.bind_borrowed(py)) + } +} +``` + ### `IntoPy` +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type. +
+ + This trait defines the to-python conversion for a Rust type. It is usually implemented as `IntoPy`, which is the trait needed for returning a value from `#[pyfunction]` and `#[pymethods]`. @@ -514,6 +624,14 @@ impl IntoPy for MyPyObjectWrapper { ### The `ToPyObject` trait +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. To migrate +implement `IntoPyObject` on a referece of your type (`impl<'py> IntoPyObject<'py> for &Type { ... }`). +
+ [`ToPyObject`] is a conversion trait that allows various objects to be converted into [`PyObject`]. `IntoPy` serves the same purpose, except that it consumes `self`. diff --git a/guide/src/migration.md b/guide/src/migration.md index ac20408a522..6a5b44a6c00 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -153,11 +153,28 @@ Notable features of this new trait: - `()` (unit) is now only special handled in return position and otherwise converts into an empty `PyTuple` All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will -need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. +need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. In many cases +the new [`#[derive(IntoPyObject)]`](#intopyobject-derive-macro) macro can be used instead of +[manual implementations](#intopyobject-manual-implementation). Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` are deprecated and will be removed in a future PyO3 version. +#### `IntoPyObject` derive macro + +To migrate you may use the new `IntoPyObject` derive macro as below. + +```rust +# use pyo3::prelude::*; +#[derive(IntoPyObject)] +struct Struct { + count: usize, + obj: Py, +} +``` + + +#### `IntoPyObject` manual implementation Before: ```rust,ignore diff --git a/newsfragments/4495.added.md b/newsfragments/4495.added.md new file mode 100644 index 00000000000..2cbe2a85bbf --- /dev/null +++ b/newsfragments/4495.added.md @@ -0,0 +1 @@ +Added `IntoPyObject` derive macro \ No newline at end of file diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs new file mode 100644 index 00000000000..3b4b2d376bb --- /dev/null +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -0,0 +1,587 @@ +use crate::attributes::{self, get_pyo3_options, CrateAttribute}; +use crate::utils::Ctx; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned as _; +use syn::{ + parenthesized, parse_quote, Attribute, DataEnum, DeriveInput, Fields, Ident, Index, Result, + Token, +}; + +/// Attributes for deriving `IntoPyObject` scoped on containers. +enum ContainerPyO3Attribute { + /// Treat the Container as a Wrapper, directly convert its field into the output object. + Transparent(attributes::kw::transparent), + /// Change the path for the pyo3 crate + Crate(CrateAttribute), +} + +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(Token![crate]) { + input.parse().map(ContainerPyO3Attribute::Crate) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Default)] +struct ContainerOptions { + /// Treat the Container as a Wrapper, directly convert its field into the output object. + transparent: Option, + /// Change the path for the pyo3 crate + krate: Option, +} + +impl ContainerOptions { + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = ContainerOptions::default(); + + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { + pyo3_attrs + .into_iter() + .try_for_each(|opt| options.set_option(opt))?; + } + } + Ok(options) + } + + fn set_option(&mut self, option: ContainerPyO3Attribute) -> 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 { + ContainerPyO3Attribute::Transparent(transparent) => set_option!(transparent), + ContainerPyO3Attribute::Crate(krate) => set_option!(krate), + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct ItemOption { + field: Option, + span: Span, +} + +impl ItemOption { + fn span(&self) -> Span { + self.span + } +} + +enum FieldAttribute { + Item(ItemOption), +} + +impl Parse for FieldAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::attribute) { + let attr: attributes::kw::attribute = input.parse()?; + bail_spanned!(attr.span => "`attribute` is not supported by `IntoPyObject`"); + } else if lookahead.peek(attributes::kw::item) { + let attr: 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(FieldAttribute::Item(ItemOption { + field: Some(key), + span: attr.span, + })) + } else { + Ok(FieldAttribute::Item(ItemOption { + field: None, + span: attr.span, + })) + } + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, Debug, Default)] +struct FieldAttributes { + item: Option, +} + +impl FieldAttributes { + /// Extract the field attributes. + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = FieldAttributes::default(); + + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { + pyo3_attrs + .into_iter() + .try_for_each(|opt| options.set_option(opt))?; + } + } + Ok(options) + } + + fn set_option(&mut self, option: FieldAttribute) -> 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 { + FieldAttribute::Item(item) => set_option!(item), + } + Ok(()) + } +} + +struct IntoPyObjectImpl { + target: TokenStream, + output: TokenStream, + error: TokenStream, + body: TokenStream, +} + +struct NamedStructField<'a> { + ident: &'a syn::Ident, + field: &'a syn::Field, + item: Option, +} + +struct TupleStructField<'a> { + field: &'a syn::Field, +} + +/// Container Style +/// +/// Covers Structs, Tuplestructs and corresponding Newtypes. +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>), + /// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }` + /// + /// The field specified by the identifier is extracted directly from the object. + StructNewtype(&'a syn::Field), + /// Tuple struct, e.g. `struct Foo(String)`. + /// + /// Variant contains a list of conversion methods for each of the fields that are directly + /// extracted from the tuple. + Tuple(Vec>), + /// Tuple newtype, e.g. `#[transparent] struct Foo(String)` + /// + /// The wrapped field is directly extracted from the object. + TupleNewtype(&'a syn::Field), +} + +/// Data container +/// +/// Either describes a struct or an enum variant. +struct Container<'a> { + path: syn::Path, + receiver: Option, + ty: ContainerType<'a>, +} + +/// Construct a container based on fields, identifier and attributes. +impl<'a> Container<'a> { + /// + /// Fails if the variant has no fields or incompatible attributes. + fn new( + receiver: Option, + fields: &'a Fields, + path: syn::Path, + options: ContainerOptions, + ) -> Result { + let style = match fields { + Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => { + let mut tuple_fields = unnamed + .unnamed + .iter() + .map(|field| { + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + ensure_spanned!( + attrs.item.is_none(), + attrs.item.unwrap().span() => "`item` is not permitted on tuple struct elements." + ); + Ok(TupleStructField { field }) + }) + .collect::>>()?; + if tuple_fields.len() == 1 { + // Always treat a 1-length tuple struct as "transparent", even without the + // explicit annotation. + let TupleStructField { field } = tuple_fields.pop().unwrap(); + ContainerType::TupleNewtype(field) + } else if options.transparent.is_some() { + bail_spanned!( + fields.span() => "transparent structs and variants can only have 1 field" + ); + } else { + ContainerType::Tuple(tuple_fields) + } + } + Fields::Named(named) if !named.named.is_empty() => { + if options.transparent.is_some() { + ensure_spanned!( + named.named.iter().count() == 1, + fields.span() => "transparent structs and variants can only have 1 field" + ); + + let field = named.named.iter().next().unwrap(); + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + ensure_spanned!( + attrs.item.is_none(), + attrs.item.unwrap().span() => "`transparent` structs may not have `item` for the inner field" + ); + ContainerType::StructNewtype(field) + } else { + let struct_fields = named + .named + .iter() + .map(|field| { + let ident = field + .ident + .as_ref() + .expect("Named fields should have identifiers"); + + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + + Ok(NamedStructField { + ident, + field, + item: attrs.item, + }) + }) + .collect::>>()?; + ContainerType::Struct(struct_fields) + } + } + _ => bail_spanned!( + fields.span() => "cannot derive `IntoPyObject` for empty structs" + ), + }; + + let v = Container { + path, + receiver, + ty: style, + }; + Ok(v) + } + + fn match_pattern(&self) -> TokenStream { + let path = &self.path; + let pattern = match &self.ty { + ContainerType::Struct(fields) => fields + .iter() + .enumerate() + .map(|(i, f)| { + let ident = f.ident; + let new_ident = format_ident!("arg{i}"); + quote! {#ident: #new_ident,} + }) + .collect::(), + ContainerType::StructNewtype(field) => { + let ident = field.ident.as_ref().unwrap(); + quote!(#ident: arg0) + } + ContainerType::Tuple(fields) => { + let i = (0..fields.len()).map(Index::from); + let idents = (0..fields.len()).map(|i| format_ident!("arg{i}")); + quote! { #(#i: #idents,)* } + } + ContainerType::TupleNewtype(_) => quote!(0: arg0), + }; + + quote! { #path{ #pattern } } + } + + /// Build derivation body for a struct. + fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl { + match &self.ty { + ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { + self.build_newtype_struct(field, ctx) + } + ContainerType::Tuple(fields) => self.build_tuple_struct(fields, ctx), + ContainerType::Struct(fields) => self.build_struct(fields, ctx), + } + } + + fn build_newtype_struct(&self, field: &syn::Field, ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + let ty = &field.ty; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + IntoPyObjectImpl { + target: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Target}, + output: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Output}, + error: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Error}, + body: quote_spanned! { ty.span() => + #unpack + <#ty as #pyo3_path::conversion::IntoPyObject<'py>>::into_pyobject(arg0, py) + }, + } + } + + fn build_struct(&self, fields: &[NamedStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + let setter = fields + .iter() + .enumerate() + .map(|(i, f)| { + let key = f + .item + .as_ref() + .and_then(|item| item.field.as_ref()) + .map(|item| item.value()) + .unwrap_or_else(|| f.ident.unraw().to_string()); + let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); + quote! { + #pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?; + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyDict), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + #unpack + let dict = #pyo3_path::types::PyDict::new(py); + #setter + ::std::result::Result::Ok::<_, Self::Error>(dict) + }, + } + } + + fn build_tuple_struct(&self, fields: &[TupleStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + let setter = fields + .iter() + .enumerate() + .map(|(i, f)| { + let ty = &f.field.ty; + let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); + quote_spanned! { f.field.ty.span() => + <#ty as #pyo3_path::conversion::IntoPyObject>::into_pyobject(#value, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::into_bound)?, + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyTuple), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + #unpack + #pyo3_path::types::PyTuple::new(py, [#setter]) + }, + } + } +} + +/// Describes derivation input of an enum. +struct Enum<'a> { + variants: Vec>, +} + +impl<'a> Enum<'a> { + /// Construct a new enum representation. + /// + /// `data_enum` is the `syn` representation of the input enum, `ident` is the + /// `Identifier` of the enum. + fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result { + ensure_spanned!( + !data_enum.variants.is_empty(), + ident.span() => "cannot derive `IntoPyObject` for empty enum" + ); + let variants = data_enum + .variants + .iter() + .map(|variant| { + let attrs = ContainerOptions::from_attrs(&variant.attrs)?; + let var_ident = &variant.ident; + + ensure_spanned!( + !variant.fields.is_empty(), + variant.ident.span() => "cannot derive `IntoPyObject` for empty variants" + ); + + Container::new( + None, + &variant.fields, + parse_quote!(#ident::#var_ident), + attrs, + ) + }) + .collect::>>()?; + + Ok(Enum { variants }) + } + + /// Build derivation body for enums. + fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let variants = self + .variants + .iter() + .map(|v| { + let IntoPyObjectImpl { body, .. } = v.build(ctx); + let pattern = v.match_pattern(); + quote! { + #pattern => { + {#body} + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::into_bound) + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + } + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyAny), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + match self { + #variants + } + }, + } + } +} + +// if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime +fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimeParam> { + let mut lifetimes = generics.lifetimes(); + lifetimes.find(|l| l.lifetime.ident == "py") +} + +pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = &ctx; + + let (_, ty_generics, _) = tokens.generics.split_for_impl(); + let mut trait_generics = tokens.generics.clone(); + let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics) { + lt.clone() + } else { + trait_generics.params.push(parse_quote!('py)); + parse_quote!('py) + }; + let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); + + let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); + for param in trait_generics.type_params() { + let gen_ident = ¶m.ident; + where_clause + .predicates + .push(parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)) + } + + let IntoPyObjectImpl { + target, + output, + error, + body, + } = match &tokens.data { + syn::Data::Enum(en) => { + if options.transparent.is_some() { + bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums"); + } + let en = Enum::new(en, &tokens.ident)?; + en.build(ctx) + } + syn::Data::Struct(st) => { + let ident = &tokens.ident; + let st = Container::new( + Some(Ident::new("self", Span::call_site())), + &st.fields, + parse_quote!(#ident), + options, + )?; + st.build(ctx) + } + syn::Data::Union(_) => bail_spanned!( + tokens.span() => "#[derive(`IntoPyObject`)] is not supported for unions" + ), + }; + + let ident = &tokens.ident; + Ok(quote!( + #[automatically_derived] + impl #impl_generics #pyo3_path::conversion::IntoPyObject<#lt_param> for #ident #ty_generics #where_clause { + type Target = #target; + type Output = #output; + type Error = #error; + + fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result { + #body + } + } + )) +} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 5d7437a4295..d6c8f287332 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -11,6 +11,7 @@ mod utils; mod attributes; mod deprecations; mod frompyobject; +mod intopyobject; mod konst; mod method; mod module; @@ -23,6 +24,7 @@ mod pyversions; mod quotes; pub use frompyobject::build_derive_from_pyobject; +pub use intopyobject::build_derive_into_pyobject; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 08b2af3cd6f..7c43c55dcd7 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -5,9 +5,9 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ - build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + build_derive_from_pyobject, build_derive_into_pyobject, build_py_class, build_py_enum, + build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, + PyClassMethodsType, PyFunctionOptions, PyModuleOptions, }; use quote::quote; use syn::{parse_macro_input, Item}; @@ -153,6 +153,16 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(IntoPyObject, attributes(pyo3))] +pub fn derive_into_py_object(item: TokenStream) -> TokenStream { + let ast = parse_macro_input!(item as syn::DeriveInput); + let expanded = build_derive_into_pyobject(&ast).unwrap_or_compile_error(); + quote!( + #expanded + ) + .into() +} + #[proc_macro_derive(FromPyObject, attributes(pyo3))] pub fn derive_from_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); diff --git a/src/lib.rs b/src/lib.rs index 73a22e94103..d6bf1b374c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -446,7 +446,7 @@ mod version; pub use crate::conversions::*; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// diff --git a/src/prelude.rs b/src/prelude.rs index 97f3e35afa1..cc44a199611 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,7 +19,7 @@ pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; #[cfg(feature = "macros")] pub use crate::wrap_pyfunction; diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 3e1cd51422a..1790c65961d 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -56,3 +56,39 @@ macro_rules! macro_rules_hygiene { } macro_rules_hygiene!(MyClass1, MyClass2); + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject1(i32); // transparent newtype case + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate", transparent)] +struct IntoPyObject2<'a> { + inner: &'a str, // transparent newtype case +} + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject3<'py>(i32, crate::Bound<'py, crate::PyAny>); // tuple case + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject4<'a, 'py> { + callable: &'a crate::Bound<'py, crate::PyAny>, // struct case + num: usize, +} + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +enum IntoPyObject5<'a, 'py> { + TransparentTuple(i32), + #[pyo3(transparent)] + TransparentStruct { + f: crate::Py, + }, + Tuple(crate::Bound<'py, crate::types::PyString>, usize), + Struct { + f: i32, + g: &'a str, + }, +} // enum case diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 012d759a99d..b6cf5065371 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -25,6 +25,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); + t.compile_fail("tests/ui/invalid_intopy_derive.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs new file mode 100644 index 00000000000..6b3718693d7 --- /dev/null +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -0,0 +1,180 @@ +#![cfg(feature = "macros")] + +use pyo3::types::{PyDict, PyString}; +use pyo3::{prelude::*, IntoPyObject}; +use std::collections::HashMap; +use std::hash::Hash; + +#[macro_use] +#[path = "../src/tests/common.rs"] +mod common; + +#[derive(Debug, Clone, IntoPyObject, FromPyObject)] +pub struct A<'py> { + #[pyo3(item)] + s: String, + #[pyo3(item)] + t: Bound<'py, PyString>, + #[pyo3(item("foo"))] + p: Bound<'py, PyAny>, +} + +#[test] +fn test_named_fields_struct() { + Python::with_gil(|py| { + let a = A { + s: "Hello".into(), + t: PyString::new(py, "World"), + p: 42i32.into_pyobject(py).unwrap().into_any(), + }; + let pya = a.clone().into_pyobject(py).unwrap(); + let new_a = pya.extract::>().unwrap(); + + assert_eq!(a.s, new_a.s); + assert_eq!(a.t.to_cow().unwrap(), new_a.t.to_cow().unwrap()); + assert_eq!( + a.p.extract::().unwrap(), + new_a.p.extract::().unwrap() + ); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[pyo3(transparent)] +pub struct B { + test: String, +} + +#[test] +fn test_transparent_named_field_struct() { + Python::with_gil(|py| { + let b = B { + test: "test".into(), + }; + let pyb = b.clone().into_pyobject(py).unwrap(); + let new_b = pyb.extract::().unwrap(); + assert_eq!(b, new_b); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[pyo3(transparent)] +pub struct D { + test: T, +} + +#[test] +fn test_generic_transparent_named_field_struct() { + Python::with_gil(|py| { + let d = D { + test: String::from("test"), + }; + let pyd = d.clone().into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + + let d = D { test: 1usize }; + let pyd = d.clone().into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + }); +} + +#[derive(Debug, IntoPyObject, FromPyObject)] +pub struct GenericWithBound(HashMap); + +#[test] +fn test_generic_with_bound() { + Python::with_gil(|py| { + let mut hash_map = HashMap::::new(); + hash_map.insert("1".into(), 1); + hash_map.insert("2".into(), 2); + let map = GenericWithBound(hash_map).into_pyobject(py).unwrap(); + assert_eq!(map.len(), 2); + assert_eq!( + map.get_item("1") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert_eq!( + map.get_item("2") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + assert!(map.get_item("3").unwrap().is_none()); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub struct Tuple(String, usize); + +#[test] +fn test_tuple_struct() { + Python::with_gil(|py| { + let tup = Tuple(String::from("test"), 1); + let tuple = tup.clone().into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub struct TransparentTuple(String); + +#[test] +fn test_transparent_tuple_struct() { + Python::with_gil(|py| { + let tup = TransparentTuple(String::from("test")); + let tuple = tup.clone().into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub enum Foo { + TupleVar(usize, String), + StructVar { + #[pyo3(item)] + test: char, + }, + #[pyo3(transparent)] + TransparentTuple(usize), + #[pyo3(transparent)] + TransparentStructVar { + a: Option, + }, +} + +#[test] +fn test_enum() { + Python::with_gil(|py| { + let tuple_var = Foo::TupleVar(1, "test".into()); + let foo = tuple_var.clone().into_pyobject(py).unwrap(); + assert_eq!(tuple_var, foo.extract::().unwrap()); + + let struct_var = Foo::StructVar { test: 'b' }; + let foo = struct_var + .clone() + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + + assert_eq!(struct_var, foo.extract::().unwrap()); + + let transparent_tuple = Foo::TransparentTuple(1); + let foo = transparent_tuple.clone().into_pyobject(py).unwrap(); + assert_eq!(transparent_tuple, foo.extract::().unwrap()); + + let transparent_struct_var = Foo::TransparentStructVar { a: None }; + let foo = transparent_struct_var.clone().into_pyobject(py).unwrap(); + assert_eq!(transparent_struct_var, foo.extract::().unwrap()); + }); +} diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs new file mode 100644 index 00000000000..971663b05d7 --- /dev/null +++ b/tests/test_intopyobject.rs @@ -0,0 +1,201 @@ +#![cfg(feature = "macros")] + +use pyo3::types::{PyDict, PyString}; +use pyo3::{prelude::*, IntoPyObject}; +use std::collections::HashMap; +use std::hash::Hash; + +#[macro_use] +#[path = "../src/tests/common.rs"] +mod common; + +#[derive(Debug, IntoPyObject)] +pub struct A<'py> { + s: String, + t: Bound<'py, PyString>, + p: Bound<'py, PyAny>, +} + +#[test] +fn test_named_fields_struct() { + Python::with_gil(|py| { + let a = A { + s: "Hello".into(), + t: PyString::new(py, "World"), + p: 42i32.into_pyobject(py).unwrap().into_any(), + }; + let pya = a.into_pyobject(py).unwrap(); + assert_eq!( + pya.get_item("s") + .unwrap() + .unwrap() + .downcast::() + .unwrap(), + "Hello" + ); + assert_eq!( + pya.get_item("t") + .unwrap() + .unwrap() + .downcast::() + .unwrap(), + "World" + ); + assert_eq!( + pya.get_item("p") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 42 + ); + }); +} + +#[derive(Debug, IntoPyObject)] +#[pyo3(transparent)] +pub struct B<'a> { + test: &'a str, +} + +#[test] +fn test_transparent_named_field_struct() { + Python::with_gil(|py| { + let pyb = B { test: "test" }.into_pyobject(py).unwrap(); + let b = pyb.extract::().unwrap(); + assert_eq!(b, "test"); + }); +} + +#[derive(Debug, IntoPyObject)] +#[pyo3(transparent)] +pub struct D { + test: T, +} + +#[test] +fn test_generic_transparent_named_field_struct() { + Python::with_gil(|py| { + let pyd = D { + test: String::from("test"), + } + .into_pyobject(py) + .unwrap(); + let d = pyd.extract::().unwrap(); + assert_eq!(d, "test"); + + let pyd = D { test: 1usize }.into_pyobject(py).unwrap(); + let d = pyd.extract::().unwrap(); + assert_eq!(d, 1); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct GenericWithBound(HashMap); + +#[test] +fn test_generic_with_bound() { + Python::with_gil(|py| { + let mut hash_map = HashMap::::new(); + hash_map.insert("1".into(), 1); + hash_map.insert("2".into(), 2); + let map = GenericWithBound(hash_map).into_pyobject(py).unwrap(); + assert_eq!(map.len(), 2); + assert_eq!( + map.get_item("1") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert_eq!( + map.get_item("2") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + assert!(map.get_item("3").unwrap().is_none()); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct Tuple(String, usize); + +#[test] +fn test_tuple_struct() { + Python::with_gil(|py| { + let tup = Tuple(String::from("test"), 1).into_pyobject(py).unwrap(); + assert!(tup.extract::<(usize, String)>().is_err()); + let tup = tup.extract::<(String, usize)>().unwrap(); + assert_eq!(tup.0, "test"); + assert_eq!(tup.1, 1); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct TransparentTuple(String); + +#[test] +fn test_transparent_tuple_struct() { + Python::with_gil(|py| { + let tup = TransparentTuple(String::from("test")) + .into_pyobject(py) + .unwrap(); + assert!(tup.extract::<(String,)>().is_err()); + let tup = tup.extract::().unwrap(); + assert_eq!(tup, "test"); + }); +} + +#[derive(Debug, IntoPyObject)] +pub enum Foo<'py> { + TupleVar(usize, String), + StructVar { + test: Bound<'py, PyString>, + }, + #[pyo3(transparent)] + TransparentTuple(usize), + #[pyo3(transparent)] + TransparentStructVar { + a: Option, + }, +} + +#[test] +fn test_enum() { + Python::with_gil(|py| { + let foo = Foo::TupleVar(1, "test".into()).into_pyobject(py).unwrap(); + assert_eq!( + foo.extract::<(usize, String)>().unwrap(), + (1, String::from("test")) + ); + + let foo = Foo::StructVar { + test: PyString::new(py, "test"), + } + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + + assert_eq!( + foo.get_item("test") + .unwrap() + .unwrap() + .downcast_into::() + .unwrap(), + "test" + ); + + let foo = Foo::TransparentTuple(1).into_pyobject(py).unwrap(); + assert_eq!(foo.extract::().unwrap(), 1); + + let foo = Foo::TransparentStructVar { a: None } + .into_pyobject(py) + .unwrap(); + assert!(foo.is_none()); + }); +} diff --git a/tests/ui/invalid_intopy_derive.rs b/tests/ui/invalid_intopy_derive.rs new file mode 100644 index 00000000000..310309992d4 --- /dev/null +++ b/tests/ui/invalid_intopy_derive.rs @@ -0,0 +1,109 @@ +use pyo3::IntoPyObject; + +#[derive(IntoPyObject)] +struct Foo(); + +#[derive(IntoPyObject)] +struct Foo2 {} + +#[derive(IntoPyObject)] +enum EmptyEnum {} + +#[derive(IntoPyObject)] +enum EnumWithEmptyTupleVar { + EmptyTuple(), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithEmptyStructVar { + EmptyStruct {}, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct EmptyTransparentTup(); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct EmptyTransparentStruct {} + +#[derive(IntoPyObject)] +enum EnumWithTransparentEmptyTupleVar { + #[pyo3(transparent)] + EmptyTuple(), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentEmptyStructVar { + #[pyo3(transparent)] + EmptyStruct {}, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentTupTooManyFields(String, String); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentStructTooManyFields { + foo: String, + bar: String, +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentTupleTooMany { + #[pyo3(transparent)] + EmptyTuple(String, String), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentStructTooMany { + #[pyo3(transparent)] + EmptyStruct { + foo: String, + bar: String, + }, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(unknown = "should not work")] +struct UnknownContainerAttr { + a: String, +} + +#[derive(IntoPyObject)] +union Union { + a: usize, +} + +#[derive(IntoPyObject)] +enum UnitEnum { + Unit, +} + +#[derive(IntoPyObject)] +struct TupleAttribute(#[pyo3(attribute)] String, usize); + +#[derive(IntoPyObject)] +struct TupleItem(#[pyo3(item)] String, usize); + +#[derive(IntoPyObject)] +struct StructAttribute { + #[pyo3(attribute)] + foo: String, +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct StructTransparentItem { + #[pyo3(item)] + foo: String, +} + +fn main() {} diff --git a/tests/ui/invalid_intopy_derive.stderr b/tests/ui/invalid_intopy_derive.stderr new file mode 100644 index 00000000000..cf125d9c073 --- /dev/null +++ b/tests/ui/invalid_intopy_derive.stderr @@ -0,0 +1,127 @@ +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:4:11 + | +4 | struct Foo(); + | ^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:7:13 + | +7 | struct Foo2 {} + | ^^ + +error: cannot derive `IntoPyObject` for empty enum + --> tests/ui/invalid_intopy_derive.rs:10:6 + | +10 | enum EmptyEnum {} + | ^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:14:5 + | +14 | EmptyTuple(), + | ^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:20:5 + | +20 | EmptyStruct {}, + | ^^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:26:27 + | +26 | struct EmptyTransparentTup(); + | ^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:30:31 + | +30 | struct EmptyTransparentStruct {} + | ^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:35:5 + | +35 | EmptyTuple(), + | ^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:42:5 + | +42 | EmptyStruct {}, + | ^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:48:35 + | +48 | struct TransparentTupTooManyFields(String, String); + | ^^^^^^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:52:39 + | +52 | struct TransparentStructTooManyFields { + | _______________________________________^ +53 | | foo: String, +54 | | bar: String, +55 | | } + | |_^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:60:15 + | +60 | EmptyTuple(String, String), + | ^^^^^^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:67:17 + | +67 | EmptyStruct { + | _________________^ +68 | | foo: String, +69 | | bar: String, +70 | | }, + | |_____^ + +error: expected `transparent` or `crate` + --> tests/ui/invalid_intopy_derive.rs:75:8 + | +75 | #[pyo3(unknown = "should not work")] + | ^^^^^^^ + +error: #[derive(`IntoPyObject`)] is not supported for unions + --> tests/ui/invalid_intopy_derive.rs:81:1 + | +81 | union Union { + | ^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:87:5 + | +87 | Unit, + | ^^^^ + +error: `attribute` is not supported by `IntoPyObject` + --> tests/ui/invalid_intopy_derive.rs:91:30 + | +91 | struct TupleAttribute(#[pyo3(attribute)] String, usize); + | ^^^^^^^^^ + +error: `item` is not permitted on tuple struct elements. + --> tests/ui/invalid_intopy_derive.rs:94:25 + | +94 | struct TupleItem(#[pyo3(item)] String, usize); + | ^^^^ + +error: `attribute` is not supported by `IntoPyObject` + --> tests/ui/invalid_intopy_derive.rs:98:12 + | +98 | #[pyo3(attribute)] + | ^^^^^^^^^ + +error: `transparent` structs may not have `item` for the inner field + --> tests/ui/invalid_intopy_derive.rs:105:12 + | +105 | #[pyo3(item)] + | ^^^^