Skip to content

Commit

Permalink
introduce into_py_with/into_py_with_ref for `#[derive(IntoPyObjec…
Browse files Browse the repository at this point in the history
…t, IntoPyObjectRef)]`
  • Loading branch information
Icxolu committed Jan 10, 2025
1 parent 21132a8 commit c280ff0
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 22 deletions.
28 changes: 28 additions & 0 deletions guide/src/conversions/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,34 @@ enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using t
Additionally `IntoPyObject` can be derived for a reference to a struct or enum using the
`IntoPyObjectRef` derive macro. All the same rules from above apply as well.

##### `#[derive(IntoPyObject)]`/`#[derive(IntoPyObjectRef)]` Field Attributes
- `pyo3(into_py_with = ...)`/`pyo3(into_py_with_ref = ...)`
- apply a custom function to convert the field from Rust into Python.
- the argument must be the function indentifier
- the function signature must be `fn(T, Python<'_>) -> PyResult<Bound<'_, PyAny>>`/`fn<'py>(&T, Python<'py>) -> PyResult<Bound<'py, PyAny>>` where `T` is the Rust type of the argument.

```rust
# use pyo3::prelude::*;
# use pyo3::IntoPyObjectExt;
struct NotIntoPy(usize);

#[derive(IntoPyObject, IntoPyObjectRef)]
struct MyStruct {
#[pyo3(into_py_with = convert, into_py_with_ref = convert_ref)]
not_into_py: NotIntoPy,
}

/// Convert `NotIntoPy` into Python by value
fn convert(NotIntoPy(i): NotIntoPy, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
i.into_bound_py_any(py)
}

/// Convert `NotIntoPy` into Python by refrence
fn convert_ref<'py>(&NotIntoPy(i): &NotIntoPy, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
i.into_bound_py_any(py)
}
```

#### manual implementation

If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as
Expand Down
4 changes: 4 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub mod kw {
syn::custom_keyword!(get);
syn::custom_keyword!(get_all);
syn::custom_keyword!(hash);
syn::custom_keyword!(into_py_with);
syn::custom_keyword!(into_py_with_ref);
syn::custom_keyword!(item);
syn::custom_keyword!(from_item_all);
syn::custom_keyword!(mapping);
Expand Down Expand Up @@ -350,6 +352,8 @@ impl<K: ToTokens, V: ToTokens> ToTokens for OptionalKeywordAttribute<K, V> {
}

pub type FromPyWithAttribute = KeywordAttribute<kw::from_py_with, LitStrValue<ExprPath>>;
pub type IntoPyWithAttribute = KeywordAttribute<kw::into_py_with, ExprPath>;
pub type IntoPyWithRefAttribute = KeywordAttribute<kw::into_py_with_ref, ExprPath>;

pub type DefaultAttribute = OptionalKeywordAttribute<Token![default], Expr>;

Expand Down
102 changes: 86 additions & 16 deletions pyo3-macros-backend/src/intopyobject.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::attributes::{self, get_pyo3_options, CrateAttribute};
use crate::attributes::{
self, get_pyo3_options, CrateAttribute, IntoPyWithAttribute, IntoPyWithRefAttribute,
};
use crate::utils::Ctx;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
Expand Down Expand Up @@ -89,6 +91,8 @@ impl ItemOption {

enum FieldAttribute {
Item(ItemOption),
IntoPyWith(IntoPyWithAttribute),
IntoPyWithRef(IntoPyWithRefAttribute),
}

impl Parse for FieldAttribute {
Expand Down Expand Up @@ -118,6 +122,10 @@ impl Parse for FieldAttribute {
span: attr.span,
}))
}
} else if lookahead.peek(attributes::kw::into_py_with) {
input.parse().map(FieldAttribute::IntoPyWith)
} else if lookahead.peek(attributes::kw::into_py_with_ref) {
input.parse().map(FieldAttribute::IntoPyWithRef)
} else {
Err(lookahead.error())
}
Expand All @@ -127,6 +135,8 @@ impl Parse for FieldAttribute {
#[derive(Clone, Debug, Default)]
struct FieldAttributes {
item: Option<ItemOption>,
into_py_with: Option<IntoPyWithAttribute>,
into_py_with_ref: Option<IntoPyWithRefAttribute>,
}

impl FieldAttributes {
Expand Down Expand Up @@ -159,6 +169,8 @@ impl FieldAttributes {

match option {
FieldAttribute::Item(item) => set_option!(item),
FieldAttribute::IntoPyWith(into_py_with) => set_option!(into_py_with),
FieldAttribute::IntoPyWithRef(into_py_with_ref) => set_option!(into_py_with_ref),
}
Ok(())
}
Expand All @@ -182,10 +194,14 @@ struct NamedStructField<'a> {
ident: &'a syn::Ident,
field: &'a syn::Field,
item: Option<ItemOption>,
into_py_with: Option<IntoPyWithAttribute>,
into_py_with_ref: Option<IntoPyWithRefAttribute>,
}

struct TupleStructField<'a> {
field: &'a syn::Field,
into_py_with: Option<IntoPyWithAttribute>,
into_py_with_ref: Option<IntoPyWithRefAttribute>,
}

/// Container Style
Expand Down Expand Up @@ -214,14 +230,14 @@ enum ContainerType<'a> {
/// Data container
///
/// Either describes a struct or an enum variant.
struct Container<'a> {
struct Container<'a, const REF: bool> {
path: syn::Path,
receiver: Option<Ident>,
ty: ContainerType<'a>,
}

/// Construct a container based on fields, identifier and attributes.
impl<'a> Container<'a> {
impl<'a, const REF: bool> Container<'a, REF> {
///
/// Fails if the variant has no fields or incompatible attributes.
fn new(
Expand All @@ -241,13 +257,29 @@ impl<'a> Container<'a> {
attrs.item.is_none(),
attrs.item.unwrap().span() => "`item` is not permitted on tuple struct elements."
);
Ok(TupleStructField { field })
Ok(TupleStructField {
field,
into_py_with: attrs.into_py_with,
into_py_with_ref: attrs.into_py_with_ref
})
})
.collect::<Result<Vec<_>>>()?;
if tuple_fields.len() == 1 {
// Always treat a 1-length tuple struct as "transparent", even without the
// explicit annotation.
let TupleStructField { field } = tuple_fields.pop().unwrap();
let TupleStructField {
field,
into_py_with,
into_py_with_ref,
} = tuple_fields.pop().unwrap();
ensure_spanned!(
into_py_with.is_none(),
into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs"
);
ensure_spanned!(
into_py_with_ref.is_none(),
into_py_with_ref.span() => "`into_py_with_ref` is not permitted on `transparent` structs"
);
ContainerType::TupleNewtype(field)
} else if options.transparent.is_some() {
bail_spanned!(
Expand All @@ -270,6 +302,14 @@ impl<'a> Container<'a> {
attrs.item.is_none(),
attrs.item.unwrap().span() => "`transparent` structs may not have `item` for the inner field"
);
ensure_spanned!(
attrs.into_py_with.is_none(),
attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants"
);
ensure_spanned!(
attrs.into_py_with_ref.is_none(),
attrs.into_py_with_ref.span() => "`into_py_with_ref` is not permitted on `transparent` structs or variants"
);
ContainerType::StructNewtype(field)
} else {
let struct_fields = named
Expand All @@ -287,6 +327,8 @@ impl<'a> Container<'a> {
ident,
field,
item: attrs.item,
into_py_with: attrs.into_py_with,
into_py_with_ref: attrs.into_py_with_ref,
})
})
.collect::<Result<Vec<_>>>()?;
Expand Down Expand Up @@ -389,8 +431,21 @@ impl<'a> Container<'a> {
.map(|item| item.value())
.unwrap_or_else(|| f.ident.unraw().to_string());
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
quote! {
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
let expr_path = if REF {
f.into_py_with_ref.as_ref().map(|i|&i.value)
} else {
f.into_py_with.as_ref().map(|i|&i.value)
};

if let Some(expr_path) = expr_path {
quote! {
let into_py_with: fn(_, #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'_, #pyo3_path::PyAny>> = #expr_path;
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#value, py)?)?;
}
} else {
quote! {
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
}
}
})
.collect::<TokenStream>();
Expand Down Expand Up @@ -426,11 +481,26 @@ impl<'a> Container<'a> {
.iter()
.enumerate()
.map(|(i, f)| {
let ty = &f.field.ty;
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
quote_spanned! { f.field.ty.span() =>
#pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py)
.map(#pyo3_path::BoundObject::into_any)
.map(#pyo3_path::BoundObject::into_bound)?,
let expr_path = if REF {
f.into_py_with_ref.as_ref().map(|i|&i.value)
} else {
f.into_py_with.as_ref().map(|i|&i.value)
};
if let Some(expr_path) = expr_path {
quote_spanned! { ty.span() =>
{
let into_py_with: fn(_, #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'_, #pyo3_path::PyAny>> = #expr_path;
into_py_with(#value, 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>();
Expand All @@ -450,11 +520,11 @@ impl<'a> Container<'a> {
}

/// Describes derivation input of an enum.
struct Enum<'a> {
variants: Vec<Container<'a>>,
struct Enum<'a, const REF: bool> {
variants: Vec<Container<'a, REF>>,
}

impl<'a> Enum<'a> {
impl<'a, const REF: bool> Enum<'a, REF> {
/// Construct a new enum representation.
///
/// `data_enum` is the `syn` representation of the input enum, `ident` is the
Expand Down Expand Up @@ -563,12 +633,12 @@ pub fn build_derive_into_pyobject<const REF: bool>(tokens: &DeriveInput) -> Resu
if options.transparent.is_some() {
bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums");
}
let en = Enum::new(en, &tokens.ident)?;
let en = Enum::<REF>::new(en, &tokens.ident)?;
en.build(ctx)
}
syn::Data::Struct(st) => {
let ident = &tokens.ident;
let st = Container::new(
let st = Container::<REF>::new(
Some(Ident::new("self", Span::call_site())),
&st.fields,
parse_quote!(#ident),
Expand Down
66 changes: 60 additions & 6 deletions tests/test_intopyobject.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg(feature = "macros")]

use pyo3::types::{PyDict, PyString};
use pyo3::{prelude::*, IntoPyObject};
use pyo3::types::{PyDict, PyList, PyString};
use pyo3::{prelude::*, py_run, IntoPyObject, IntoPyObjectExt};
use std::collections::HashMap;
use std::hash::Hash;

Expand Down Expand Up @@ -150,9 +150,20 @@ fn test_transparent_tuple_struct() {
});
}

fn phantom_into_py<T>(
_: std::marker::PhantomData<T>,
py: Python<'_>,
) -> PyResult<Bound<'_, PyAny>> {
std::any::type_name::<T>().into_bound_py_any(py)
}

#[derive(Debug, IntoPyObject)]
pub enum Foo<'py> {
TupleVar(usize, String),
TupleVar(
usize,
String,
#[pyo3(into_py_with = phantom_into_py::<()>)] std::marker::PhantomData<()>,
),
StructVar {
test: Bound<'py, PyString>,
},
Expand All @@ -167,10 +178,12 @@ pub enum Foo<'py> {
#[test]
fn test_enum() {
Python::with_gil(|py| {
let foo = Foo::TupleVar(1, "test".into()).into_pyobject(py).unwrap();
let foo = Foo::TupleVar(1, "test".into(), std::marker::PhantomData)
.into_pyobject(py)
.unwrap();
assert_eq!(
foo.extract::<(usize, String)>().unwrap(),
(1, String::from("test"))
foo.extract::<(usize, String, String)>().unwrap(),
(1, String::from("test"), String::from("()"))
);

let foo = Foo::StructVar {
Expand Down Expand Up @@ -199,3 +212,44 @@ fn test_enum() {
assert!(foo.is_none());
});
}

#[derive(Debug, IntoPyObject, IntoPyObjectRef)]
pub struct Zap {
#[pyo3(item)]
name: String,

#[pyo3(into_py_with = zap_into_py, into_py_with_ref = zap_into_py_ref, item("my_object"))]
some_object_length: usize,
}

fn zap_into_py(len: usize, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
Ok(PyList::new(py, 1..len + 1)?.into_any())
}

fn zap_into_py_ref<'py>(&len: &usize, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
Ok(PyList::new(py, 1..len + 1)?.into_any())
}

#[test]
fn test_into_py_with() {
Python::with_gil(|py| {
let zap = Zap {
name: "whatever".into(),
some_object_length: 3,
};

let py_zap_ref = (&zap).into_pyobject(py).unwrap();
let py_zap = zap.into_pyobject(py).unwrap();

py_run!(
py,
py_zap_ref,
"assert py_zap_ref == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap_ref}'"
);
py_run!(
py,
py_zap,
"assert py_zap == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap}'"
);
});
}
Loading

0 comments on commit c280ff0

Please sign in to comment.