pyo3_macros_backend/
intopyobject.rs

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