use crate::attributes::{IntoPyWithAttribute, RenamingRule};
use crate::derive_attributes::{ContainerAttributes, FieldAttributes};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::utils::{self, Ctx};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::spanned::Spanned as _;
use syn::{parse_quote, DataEnum, DeriveInput, Fields, Ident, Index, Result};
struct ItemOption(Option<syn::Lit>);
enum IntoPyObjectTypes {
Transparent(syn::Type),
Opaque {
target: TokenStream,
output: TokenStream,
error: TokenStream,
},
}
struct IntoPyObjectImpl {
types: IntoPyObjectTypes,
body: TokenStream,
}
struct NamedStructField<'a> {
ident: &'a syn::Ident,
field: &'a syn::Field,
item: Option<ItemOption>,
into_py_with: Option<IntoPyWithAttribute>,
}
struct TupleStructField<'a> {
field: &'a syn::Field,
into_py_with: Option<IntoPyWithAttribute>,
}
enum ContainerType<'a> {
Struct(Vec<NamedStructField<'a>>),
StructNewtype(&'a syn::Field),
Tuple(Vec<TupleStructField<'a>>),
TupleNewtype(&'a syn::Field),
}
struct Container<'a, const REF: bool> {
path: syn::Path,
receiver: Option<Ident>,
ty: ContainerType<'a>,
rename_rule: Option<RenamingRule>,
}
impl<'a, const REF: bool> Container<'a, REF> {
fn new(
receiver: Option<Ident>,
fields: &'a Fields,
path: syn::Path,
options: ContainerAttributes,
) -> Result<Self> {
let style = match fields {
Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => {
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is useless on tuple structs and variants."
);
let mut tuple_fields = unnamed
.unnamed
.iter()
.map(|field| {
let attrs = FieldAttributes::from_attrs(&field.attrs)?;
ensure_spanned!(
attrs.getter.is_none(),
attrs.getter.unwrap().span() => "`item` and `attribute` are not permitted on tuple struct elements."
);
Ok(TupleStructField {
field,
into_py_with: attrs.into_py_with,
})
})
.collect::<Result<Vec<_>>>()?;
if tuple_fields.len() == 1 {
let TupleStructField {
field,
into_py_with,
} = tuple_fields.pop().unwrap();
ensure_spanned!(
into_py_with.is_none(),
into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs"
);
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.getter.is_none(),
attrs.getter.unwrap().span() => "`transparent` structs may not have `item` nor `attribute` for the inner field"
);
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants"
);
ensure_spanned!(
attrs.into_py_with.is_none(),
attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants"
);
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.getter.and_then(|getter| match getter {
crate::derive_attributes::FieldGetter::GetItem(_, lit) => {
Some(ItemOption(lit))
}
crate::derive_attributes::FieldGetter::GetAttr(_, _) => None,
}),
into_py_with: attrs.into_py_with,
})
})
.collect::<Result<Vec<_>>>()?;
ContainerType::Struct(struct_fields)
}
}
_ => bail_spanned!(
fields.span() => "cannot derive `IntoPyObject` for empty structs"
),
};
let v = Container {
path,
receiver,
ty: style,
rename_rule: options.rename_all.map(|v| v.value.rule),
};
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::<TokenStream>(),
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 } }
}
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 {
types: IntoPyObjectTypes::Transparent(ty.clone()),
body: quote_spanned! { ty.span() =>
#unpack
#pyo3_path::conversion::IntoPyObject::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.0.as_ref())
.map(|item| item.into_token_stream())
.unwrap_or_else(|| {
let name = f.ident.unraw().to_string();
self.rename_rule.map(|rule| utils::apply_renaming_rule(rule, &name)).unwrap_or(name).into_token_stream()
});
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote! {
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#cow, py)?)?;
}
} else {
quote! {
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
}
}
})
.collect::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
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());
if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote_spanned! { ty.span() =>
{
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
into_py_with(#cow, py)?
},
}
} else {
quote_spanned! { ty.span() =>
#pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py)
.map(#pyo3_path::BoundObject::into_any)
.map(#pyo3_path::BoundObject::into_bound)?,
}
}
})
.collect::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
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])
},
}
}
#[cfg(feature = "experimental-inspect")]
fn output_type(&self) -> PyExpr {
match &self.ty {
ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => {
Self::field_output_type(&None, &field.ty)
}
ContainerType::Tuple(tups) => PyExpr::subscript(
PyExpr::builtin("tuple"),
PyExpr::tuple(tups.iter().map(
|TupleStructField {
into_py_with,
field,
}| { Self::field_output_type(into_py_with, &field.ty) },
)),
),
ContainerType::Struct(_) => {
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
}
#[cfg(feature = "experimental-inspect")]
fn field_output_type(into_py_with: &Option<IntoPyWithAttribute>, ty: &syn::Type) -> PyExpr {
if into_py_with.is_some() {
PyExpr::module_attr("_typeshed", "Incomplete")
} else {
PyExpr::from_into_py_object(ty.clone(), None)
}
}
}
struct Enum<'a, const REF: bool> {
variants: Vec<Container<'a, REF>>,
}
impl<'a, const REF: bool> Enum<'a, REF> {
fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result<Self> {
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 = ContainerAttributes::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::<Result<Vec<_>>>()?;
Ok(Enum { variants })
}
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::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
target: quote!(#pyo3_path::types::PyAny),
output: quote!(#pyo3_path::Bound<'py, <Self as #pyo3_path::conversion::IntoPyObject<'py>>::Target>),
error: quote!(#pyo3_path::PyErr),
},
body: quote! {
match self {
#variants
}
},
}
}
#[cfg(feature = "experimental-inspect")]
fn output_type(&self) -> PyExpr {
self.variants
.iter()
.map(|var| var.output_type())
.reduce(PyExpr::union)
.expect("Empty enum")
}
}
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<const REF: bool>(tokens: &DeriveInput) -> Result<TokenStream> {
let options = ContainerAttributes::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();
if REF {
trait_generics.params.push(parse_quote!('_a));
}
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(if REF {
parse_quote!(&'_a #gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
} else {
parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
})
}
let IntoPyObjectImpl { types, 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");
}
if let Some(rename_all) = options.rename_all {
bail_spanned!(rename_all.span() => "`rename_all` is not supported at top level for enums");
}
let en = Enum::<REF>::new(en, &tokens.ident)?;
en.build(ctx)
}
syn::Data::Struct(st) => {
let ident = &tokens.ident;
let st = Container::<REF>::new(
Some(Ident::new("self", Span::call_site())),
&st.fields,
parse_quote!(#ident),
options.clone(),
)?;
st.build(ctx)
}
syn::Data::Union(_) => bail_spanned!(
tokens.span() => "#[derive(`IntoPyObject`)] is not supported for unions"
),
};
let (target, output, error) = match types {
IntoPyObjectTypes::Transparent(ty) => {
if REF {
(
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Target },
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Output },
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Error },
)
} else {
(
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Target },
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Output },
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Error },
)
}
}
IntoPyObjectTypes::Opaque {
target,
output,
error,
} => (target, output, error),
};
let ident = &tokens.ident;
let ident = if REF {
quote! { &'_a #ident}
} else {
quote! { #ident }
};
#[cfg(feature = "experimental-inspect")]
let output_type = {
let pyo3_crate_path = &ctx.pyo3_path;
let output_type = if tokens
.generics
.params
.iter()
.all(|p| matches!(p, syn::GenericParam::Lifetime(_)))
{
match &tokens.data {
syn::Data::Enum(en) => Enum::<REF>::new(en, &tokens.ident)?.output_type(),
syn::Data::Struct(st) => {
let ident = &tokens.ident;
Container::<REF>::new(
Some(Ident::new("self", Span::call_site())),
&st.fields,
parse_quote!(#ident),
options,
)?
.output_type()
}
syn::Data::Union(_) => {
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
} else {
PyExpr::module_attr("_typeshed", "Incomplete")
}
.to_introspection_token_stream(pyo3_crate_path);
quote! { const OUTPUT_TYPE: #pyo3_path::inspect::PyStaticExpr = #output_type; }
};
#[cfg(not(feature = "experimental-inspect"))]
let output_type = quote! {};
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;
#output_type
fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result<
<Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Output,
<Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Error,
> {
#body
}
}
))
}