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
40enum ContainerType<'a> {
44 Struct(Vec<NamedStructField<'a>>),
48 StructNewtype(&'a syn::Field),
52 Tuple(Vec<TupleStructField<'a>>),
57 TupleNewtype(&'a syn::Field),
61}
62
63struct Container<'a, const REF: bool> {
67 path: syn::Path,
68 receiver: Option<Ident>,
69 ty: ContainerType<'a>,
70 rename_rule: Option<RenamingRule>,
71}
72
73impl<'a, const REF: bool> Container<'a, REF> {
75 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 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 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 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 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
413struct Enum<'a, const REF: bool> {
415 variants: Vec<Container<'a, REF>>,
416}
417
418impl<'a, const REF: bool> Enum<'a, REF> {
419 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 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
498fn 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 = ¶m.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 builder.push_str("_typeshed.Incomplete")
614 }
615 }
616 } else {
617 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}