pyo3_macros_backend/
intopyobject.rs

1use crate::attributes::{IntoPyWithAttribute, RenamingRule};
2use crate::derive_attributes::{ContainerAttributes, FieldAttributes};
3#[cfg(feature = "experimental-inspect")]
4use crate::introspection::{elide_lifetimes, ConcatenationBuilder};
5use crate::utils::{self, Ctx};
6use proc_macro2::{Span, TokenStream};
7use quote::{format_ident, quote, quote_spanned, ToTokens};
8use syn::ext::IdentExt;
9use syn::spanned::Spanned as _;
10use syn::{parse_quote, DataEnum, DeriveInput, Fields, Ident, Index, Result};
11
12struct ItemOption(Option<syn::Lit>);
13
14enum IntoPyObjectTypes {
15    Transparent(syn::Type),
16    Opaque {
17        target: TokenStream,
18        output: TokenStream,
19        error: TokenStream,
20    },
21}
22
23struct IntoPyObjectImpl {
24    types: IntoPyObjectTypes,
25    body: TokenStream,
26}
27
28struct NamedStructField<'a> {
29    ident: &'a syn::Ident,
30    field: &'a syn::Field,
31    item: Option<ItemOption>,
32    into_py_with: Option<IntoPyWithAttribute>,
33}
34
35struct TupleStructField<'a> {
36    field: &'a syn::Field,
37    into_py_with: Option<IntoPyWithAttribute>,
38}
39
40/// Container Style
41///
42/// Covers Structs, Tuplestructs and corresponding Newtypes.
43enum ContainerType<'a> {
44    /// Struct Container, e.g. `struct Foo { a: String }`
45    ///
46    /// Variant contains the list of field identifiers and the corresponding extraction call.
47    Struct(Vec<NamedStructField<'a>>),
48    /// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }`
49    ///
50    /// The field specified by the identifier is extracted directly from the object.
51    StructNewtype(&'a syn::Field),
52    /// Tuple struct, e.g. `struct Foo(String)`.
53    ///
54    /// Variant contains a list of conversion methods for each of the fields that are directly
55    ///  extracted from the tuple.
56    Tuple(Vec<TupleStructField<'a>>),
57    /// Tuple newtype, e.g. `#[transparent] struct Foo(String)`
58    ///
59    /// The wrapped field is directly extracted from the object.
60    TupleNewtype(&'a syn::Field),
61}
62
63/// Data container
64///
65/// Either describes a struct or an enum variant.
66struct Container<'a, const REF: bool> {
67    path: syn::Path,
68    receiver: Option<Ident>,
69    ty: ContainerType<'a>,
70    rename_rule: Option<RenamingRule>,
71}
72
73/// Construct a container based on fields, identifier and attributes.
74impl<'a, const REF: bool> Container<'a, REF> {
75    ///
76    /// Fails if the variant has no fields or incompatible attributes.
77    fn new(
78        receiver: Option<Ident>,
79        fields: &'a Fields,
80        path: syn::Path,
81        options: ContainerAttributes,
82    ) -> Result<Self> {
83        let style = match fields {
84            Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => {
85                ensure_spanned!(
86                    options.rename_all.is_none(),
87                    options.rename_all.span() => "`rename_all` is useless on tuple structs and variants."
88                );
89                let mut tuple_fields = unnamed
90                    .unnamed
91                    .iter()
92                    .map(|field| {
93                        let attrs = FieldAttributes::from_attrs(&field.attrs)?;
94                        ensure_spanned!(
95                            attrs.getter.is_none(),
96                            attrs.getter.unwrap().span() => "`item` and `attribute` are not permitted on tuple struct elements."
97                        );
98                        Ok(TupleStructField {
99                            field,
100                            into_py_with: attrs.into_py_with,
101                        })
102                    })
103                    .collect::<Result<Vec<_>>>()?;
104                if tuple_fields.len() == 1 {
105                    // Always treat a 1-length tuple struct as "transparent", even without the
106                    // explicit annotation.
107                    let TupleStructField {
108                        field,
109                        into_py_with,
110                    } = tuple_fields.pop().unwrap();
111                    ensure_spanned!(
112                        into_py_with.is_none(),
113                        into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs"
114                    );
115                    ContainerType::TupleNewtype(field)
116                } else if options.transparent.is_some() {
117                    bail_spanned!(
118                        fields.span() => "transparent structs and variants can only have 1 field"
119                    );
120                } else {
121                    ContainerType::Tuple(tuple_fields)
122                }
123            }
124            Fields::Named(named) if !named.named.is_empty() => {
125                if options.transparent.is_some() {
126                    ensure_spanned!(
127                        named.named.iter().count() == 1,
128                        fields.span() => "transparent structs and variants can only have 1 field"
129                    );
130
131                    let field = named.named.iter().next().unwrap();
132                    let attrs = FieldAttributes::from_attrs(&field.attrs)?;
133                    ensure_spanned!(
134                        attrs.getter.is_none(),
135                        attrs.getter.unwrap().span() => "`transparent` structs may not have `item` nor `attribute` for the inner field"
136                    );
137                    ensure_spanned!(
138                        options.rename_all.is_none(),
139                        options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants"
140                    );
141                    ensure_spanned!(
142                        attrs.into_py_with.is_none(),
143                        attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants"
144                    );
145                    ContainerType::StructNewtype(field)
146                } else {
147                    let struct_fields = named
148                        .named
149                        .iter()
150                        .map(|field| {
151                            let ident = field
152                                .ident
153                                .as_ref()
154                                .expect("Named fields should have identifiers");
155
156                            let attrs = FieldAttributes::from_attrs(&field.attrs)?;
157
158                            Ok(NamedStructField {
159                                ident,
160                                field,
161                                item: attrs.getter.and_then(|getter| match getter {
162                                    crate::derive_attributes::FieldGetter::GetItem(_, lit) => {
163                                        Some(ItemOption(lit))
164                                    }
165                                    crate::derive_attributes::FieldGetter::GetAttr(_, _) => None,
166                                }),
167                                into_py_with: attrs.into_py_with,
168                            })
169                        })
170                        .collect::<Result<Vec<_>>>()?;
171                    ContainerType::Struct(struct_fields)
172                }
173            }
174            _ => bail_spanned!(
175                fields.span() => "cannot derive `IntoPyObject` for empty structs"
176            ),
177        };
178
179        let v = Container {
180            path,
181            receiver,
182            ty: style,
183            rename_rule: options.rename_all.map(|v| v.value.rule),
184        };
185        Ok(v)
186    }
187
188    fn match_pattern(&self) -> TokenStream {
189        let path = &self.path;
190        let pattern = match &self.ty {
191            ContainerType::Struct(fields) => fields
192                .iter()
193                .enumerate()
194                .map(|(i, f)| {
195                    let ident = f.ident;
196                    let new_ident = format_ident!("arg{i}");
197                    quote! {#ident: #new_ident,}
198                })
199                .collect::<TokenStream>(),
200            ContainerType::StructNewtype(field) => {
201                let ident = field.ident.as_ref().unwrap();
202                quote!(#ident: arg0)
203            }
204            ContainerType::Tuple(fields) => {
205                let i = (0..fields.len()).map(Index::from);
206                let idents = (0..fields.len()).map(|i| format_ident!("arg{i}"));
207                quote! { #(#i: #idents,)* }
208            }
209            ContainerType::TupleNewtype(_) => quote!(0: arg0),
210        };
211
212        quote! { #path{ #pattern } }
213    }
214
215    /// Build derivation body for a struct.
216    fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl {
217        match &self.ty {
218            ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => {
219                self.build_newtype_struct(field, ctx)
220            }
221            ContainerType::Tuple(fields) => self.build_tuple_struct(fields, ctx),
222            ContainerType::Struct(fields) => self.build_struct(fields, ctx),
223        }
224    }
225
226    fn build_newtype_struct(&self, field: &syn::Field, ctx: &Ctx) -> IntoPyObjectImpl {
227        let Ctx { pyo3_path, .. } = ctx;
228        let ty = &field.ty;
229
230        let unpack = self
231            .receiver
232            .as_ref()
233            .map(|i| {
234                let pattern = self.match_pattern();
235                quote! { let #pattern = #i;}
236            })
237            .unwrap_or_default();
238
239        IntoPyObjectImpl {
240            types: IntoPyObjectTypes::Transparent(ty.clone()),
241            body: quote_spanned! { ty.span() =>
242                #unpack
243                #pyo3_path::conversion::IntoPyObject::into_pyobject(arg0, py)
244            },
245        }
246    }
247
248    fn build_struct(&self, fields: &[NamedStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl {
249        let Ctx { pyo3_path, .. } = ctx;
250
251        let unpack = self
252            .receiver
253            .as_ref()
254            .map(|i| {
255                let pattern = self.match_pattern();
256                quote! { let #pattern = #i;}
257            })
258            .unwrap_or_default();
259
260        let setter = fields
261            .iter()
262            .enumerate()
263            .map(|(i, f)| {
264                let key = f
265                    .item
266                    .as_ref()
267                    .and_then(|item| item.0.as_ref())
268                    .map(|item| item.into_token_stream())
269                    .unwrap_or_else(|| {
270                        let name = f.ident.unraw().to_string();
271                        self.rename_rule.map(|rule| utils::apply_renaming_rule(rule, &name)).unwrap_or(name).into_token_stream()
272                    });
273                let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
274
275                if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
276                    let cow = if REF {
277                        quote!(::std::borrow::Cow::Borrowed(#value))
278                    } else {
279                        quote!(::std::borrow::Cow::Owned(#value))
280                    };
281                    quote! {
282                        let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
283                        #pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#cow, py)?)?;
284                    }
285                } else {
286                    quote! {
287                        #pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
288                    }
289                }
290            })
291            .collect::<TokenStream>();
292
293        IntoPyObjectImpl {
294            types: IntoPyObjectTypes::Opaque {
295                target: quote!(#pyo3_path::types::PyDict),
296                output: quote!(#pyo3_path::Bound<'py, Self::Target>),
297                error: quote!(#pyo3_path::PyErr),
298            },
299            body: quote! {
300                #unpack
301                let dict = #pyo3_path::types::PyDict::new(py);
302                #setter
303                ::std::result::Result::Ok::<_, Self::Error>(dict)
304            },
305        }
306    }
307
308    fn build_tuple_struct(&self, fields: &[TupleStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl {
309        let Ctx { pyo3_path, .. } = ctx;
310
311        let unpack = self
312            .receiver
313            .as_ref()
314            .map(|i| {
315                let pattern = self.match_pattern();
316                quote! { let #pattern = #i;}
317            })
318            .unwrap_or_default();
319
320        let setter = fields
321            .iter()
322            .enumerate()
323            .map(|(i, f)| {
324                let ty = &f.field.ty;
325                let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
326
327                if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
328                    let cow = if REF {
329                        quote!(::std::borrow::Cow::Borrowed(#value))
330                    } else {
331                        quote!(::std::borrow::Cow::Owned(#value))
332                    };
333                    quote_spanned! { ty.span() =>
334                        {
335                            let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
336                            into_py_with(#cow, py)?
337                        },
338                    }
339                } else {
340                    quote_spanned! { ty.span() =>
341                        #pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py)
342                            .map(#pyo3_path::BoundObject::into_any)
343                            .map(#pyo3_path::BoundObject::into_bound)?,
344                    }
345                }
346            })
347            .collect::<TokenStream>();
348
349        IntoPyObjectImpl {
350            types: IntoPyObjectTypes::Opaque {
351                target: quote!(#pyo3_path::types::PyTuple),
352                output: quote!(#pyo3_path::Bound<'py, Self::Target>),
353                error: quote!(#pyo3_path::PyErr),
354            },
355            body: quote! {
356                #unpack
357                #pyo3_path::types::PyTuple::new(py, [#setter])
358            },
359        }
360    }
361
362    #[cfg(feature = "experimental-inspect")]
363    fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) {
364        match &self.ty {
365            ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => {
366                Self::write_field_output_type(&None, &field.ty, builder, ctx);
367            }
368            ContainerType::Tuple(tups) => {
369                builder.push_str("tuple[");
370                for (
371                    i,
372                    TupleStructField {
373                        into_py_with,
374                        field,
375                    },
376                ) in tups.iter().enumerate()
377                {
378                    if i > 0 {
379                        builder.push_str(", ");
380                    }
381                    Self::write_field_output_type(into_py_with, &field.ty, builder, ctx);
382                }
383                builder.push_str("]");
384            }
385            ContainerType::Struct(_) => {
386                // TODO: implement using a Protocol?
387                builder.push_str("_typeshed.Incomplete")
388            }
389        }
390    }
391
392    #[cfg(feature = "experimental-inspect")]
393    fn write_field_output_type(
394        into_py_with: &Option<IntoPyWithAttribute>,
395        ty: &syn::Type,
396        builder: &mut ConcatenationBuilder,
397        ctx: &Ctx,
398    ) {
399        if into_py_with.is_some() {
400            // We don't know what into_py_with is doing
401            builder.push_str("_typeshed.Incomplete")
402        } else {
403            let mut ty = ty.clone();
404            elide_lifetimes(&mut ty);
405            let pyo3_crate_path = &ctx.pyo3_path;
406            builder.push_tokens(
407                quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE.as_bytes() },
408            )
409        }
410    }
411}
412
413/// Describes derivation input of an enum.
414struct Enum<'a, const REF: bool> {
415    variants: Vec<Container<'a, REF>>,
416}
417
418impl<'a, const REF: bool> Enum<'a, REF> {
419    /// Construct a new enum representation.
420    ///
421    /// `data_enum` is the `syn` representation of the input enum, `ident` is the
422    /// `Identifier` of the enum.
423    fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result<Self> {
424        ensure_spanned!(
425            !data_enum.variants.is_empty(),
426            ident.span() => "cannot derive `IntoPyObject` for empty enum"
427        );
428        let variants = data_enum
429            .variants
430            .iter()
431            .map(|variant| {
432                let attrs = ContainerAttributes::from_attrs(&variant.attrs)?;
433                let var_ident = &variant.ident;
434
435                ensure_spanned!(
436                    !variant.fields.is_empty(),
437                    variant.ident.span() => "cannot derive `IntoPyObject` for empty variants"
438                );
439
440                Container::new(
441                    None,
442                    &variant.fields,
443                    parse_quote!(#ident::#var_ident),
444                    attrs,
445                )
446            })
447            .collect::<Result<Vec<_>>>()?;
448
449        Ok(Enum { variants })
450    }
451
452    /// Build derivation body for enums.
453    fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl {
454        let Ctx { pyo3_path, .. } = ctx;
455
456        let variants = self
457            .variants
458            .iter()
459            .map(|v| {
460                let IntoPyObjectImpl { body, .. } = v.build(ctx);
461                let pattern = v.match_pattern();
462                quote! {
463                    #pattern => {
464                        {#body}
465                            .map(#pyo3_path::BoundObject::into_any)
466                            .map(#pyo3_path::BoundObject::into_bound)
467                            .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into)
468                    }
469                }
470            })
471            .collect::<TokenStream>();
472
473        IntoPyObjectImpl {
474            types: IntoPyObjectTypes::Opaque {
475                target: quote!(#pyo3_path::types::PyAny),
476                output: quote!(#pyo3_path::Bound<'py, <Self as #pyo3_path::conversion::IntoPyObject<'py>>::Target>),
477                error: quote!(#pyo3_path::PyErr),
478            },
479            body: quote! {
480                match self {
481                    #variants
482                }
483            },
484        }
485    }
486
487    #[cfg(feature = "experimental-inspect")]
488    fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) {
489        for (i, var) in self.variants.iter().enumerate() {
490            if i > 0 {
491                builder.push_str(" | ");
492            }
493            var.write_output_type(builder, ctx);
494        }
495    }
496}
497
498// if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime
499fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimeParam> {
500    let mut lifetimes = generics.lifetimes();
501    lifetimes.find(|l| l.lifetime.ident == "py")
502}
503
504pub fn build_derive_into_pyobject<const REF: bool>(tokens: &DeriveInput) -> Result<TokenStream> {
505    let options = ContainerAttributes::from_attrs(&tokens.attrs)?;
506    let ctx = &Ctx::new(&options.krate, None);
507    let Ctx { pyo3_path, .. } = &ctx;
508
509    let (_, ty_generics, _) = tokens.generics.split_for_impl();
510    let mut trait_generics = tokens.generics.clone();
511    if REF {
512        trait_generics.params.push(parse_quote!('_a));
513    }
514    let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics) {
515        lt.clone()
516    } else {
517        trait_generics.params.push(parse_quote!('py));
518        parse_quote!('py)
519    };
520    let (impl_generics, _, where_clause) = trait_generics.split_for_impl();
521
522    let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where));
523    for param in trait_generics.type_params() {
524        let gen_ident = &param.ident;
525        where_clause.predicates.push(if REF {
526            parse_quote!(&'_a #gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
527        } else {
528            parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
529        })
530    }
531
532    let IntoPyObjectImpl { types, body } = match &tokens.data {
533        syn::Data::Enum(en) => {
534            if options.transparent.is_some() {
535                bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums");
536            }
537            if let Some(rename_all) = options.rename_all {
538                bail_spanned!(rename_all.span() => "`rename_all` is not supported at top level for enums");
539            }
540            let en = Enum::<REF>::new(en, &tokens.ident)?;
541            en.build(ctx)
542        }
543        syn::Data::Struct(st) => {
544            let ident = &tokens.ident;
545            let st = Container::<REF>::new(
546                Some(Ident::new("self", Span::call_site())),
547                &st.fields,
548                parse_quote!(#ident),
549                options.clone(),
550            )?;
551            st.build(ctx)
552        }
553        syn::Data::Union(_) => bail_spanned!(
554            tokens.span() => "#[derive(`IntoPyObject`)] is not supported for unions"
555        ),
556    };
557
558    let (target, output, error) = match types {
559        IntoPyObjectTypes::Transparent(ty) => {
560            if REF {
561                (
562                    quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Target },
563                    quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Output },
564                    quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Error },
565                )
566            } else {
567                (
568                    quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Target },
569                    quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Output },
570                    quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Error },
571                )
572            }
573        }
574        IntoPyObjectTypes::Opaque {
575            target,
576            output,
577            error,
578        } => (target, output, error),
579    };
580
581    let ident = &tokens.ident;
582    let ident = if REF {
583        quote! { &'_a #ident}
584    } else {
585        quote! { #ident }
586    };
587
588    #[cfg(feature = "experimental-inspect")]
589    let output_type = {
590        let mut builder = ConcatenationBuilder::default();
591        if tokens
592            .generics
593            .params
594            .iter()
595            .all(|p| matches!(p, syn::GenericParam::Lifetime(_)))
596        {
597            match &tokens.data {
598                syn::Data::Enum(en) => {
599                    Enum::<REF>::new(en, &tokens.ident)?.write_output_type(&mut builder, ctx)
600                }
601                syn::Data::Struct(st) => {
602                    let ident = &tokens.ident;
603                    Container::<REF>::new(
604                        Some(Ident::new("self", Span::call_site())),
605                        &st.fields,
606                        parse_quote!(#ident),
607                        options,
608                    )?
609                    .write_output_type(&mut builder, ctx)
610                }
611                syn::Data::Union(_) => {
612                    // Not supported at this point
613                    builder.push_str("_typeshed.Incomplete")
614                }
615            }
616        } else {
617            // We don't know how to deal with generic parameters
618            // Blocked by https://github.com/rust-lang/rust/issues/76560
619            builder.push_str("_typeshed.Incomplete")
620        };
621        let output_type = builder.into_token_stream(&ctx.pyo3_path);
622        quote! { const OUTPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#output_type) }; }
623    };
624    #[cfg(not(feature = "experimental-inspect"))]
625    let output_type = quote! {};
626
627    Ok(quote!(
628        #[automatically_derived]
629        impl #impl_generics #pyo3_path::conversion::IntoPyObject<#lt_param> for #ident #ty_generics #where_clause {
630            type Target = #target;
631            type Output = #output;
632            type Error = #error;
633            #output_type
634
635            fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result<
636                <Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Output,
637                <Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Error,
638            > {
639                #body
640            }
641        }
642    ))
643}