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
13enum ContainerPyO3Attribute {
15 Transparent(attributes::kw::transparent),
17 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 transparent: Option<attributes::kw::transparent>,
39 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 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
198enum ContainerType<'a> {
202 Struct(Vec<NamedStructField<'a>>),
206 StructNewtype(&'a syn::Field),
210 Tuple(Vec<TupleStructField<'a>>),
215 TupleNewtype(&'a syn::Field),
219}
220
221struct Container<'a, const REF: bool> {
225 path: syn::Path,
226 receiver: Option<Ident>,
227 ty: ContainerType<'a>,
228}
229
230impl<'a, const REF: bool> Container<'a, REF> {
232 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 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 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
503struct Enum<'a, const REF: bool> {
505 variants: Vec<Container<'a, REF>>,
506}
507
508impl<'a, const REF: bool> Enum<'a, REF> {
509 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 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
578fn 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 = ¶m.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}