Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce into_py_with/into_py_with_ref for #[derive(IntoPyObject, IntoPyObjectRef)] #4850

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions guide/src/conversions/traits.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,31 @@ 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 = ...)`
- 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(Cow<'_, T>, Python<'py>) -> PyResult<Bound<'py, PyAny>>` where `T` is the Rust type of the argument.

```rust
# use pyo3::prelude::*;
# use pyo3::IntoPyObjectExt;
# use std::borrow::Cow;
#[derive(Clone)]
struct NotIntoPy(usize);

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

/// Convert `NotIntoPy` into Python
fn convert<'py>(not_into_py: Cow<'_, NotIntoPy>, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
not_into_py.0.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
1 change: 1 addition & 0 deletions newsfragments/4850.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
introduce `into_py_with`/`into_py_with_ref` for `#[derive(IntoPyObject, IntoPyObjectRef)]`
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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!(item);
syn::custom_keyword!(from_item_all);
syn::custom_keyword!(mapping);
Expand Down Expand Up @@ -350,6 +351,7 @@ 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 DefaultAttribute = OptionalKeywordAttribute<Token![default], Expr>;

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

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

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

impl FieldAttributes {
Expand Down Expand Up @@ -159,6 +163,7 @@ impl FieldAttributes {

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

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

/// Container Style
Expand Down Expand Up @@ -214,14 +221,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 +248,23 @@ 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,
})
})
.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,
} = tuple_fields.pop().unwrap();
ensure_spanned!(
into_py_with.is_none(),
into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs"
);
ContainerType::TupleNewtype(field)
} else if options.transparent.is_some() {
bail_spanned!(
Expand All @@ -270,6 +287,10 @@ 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"
);
ContainerType::StructNewtype(field)
} else {
let struct_fields = named
Expand All @@ -287,6 +308,7 @@ impl<'a> Container<'a> {
ident,
field,
item: attrs.item,
into_py_with: attrs.into_py_with,
})
})
.collect::<Result<Vec<_>>>()?;
Expand Down Expand Up @@ -389,8 +411,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)?;

if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote! {
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#cow, py)?)?;
}
} else {
quote! {
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
}
}
})
.collect::<TokenStream>();
Expand Down Expand Up @@ -426,11 +461,27 @@ 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)?,

if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote_spanned! { ty.span() =>
{
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
into_py_with(#cow, 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 +501,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 +614,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
2 changes: 2 additions & 0 deletions tests/test_compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ fn test_compile_errors() {
t.compile_fail("tests/ui/pyclass_send.rs");
t.compile_fail("tests/ui/invalid_argument_attributes.rs");
t.compile_fail("tests/ui/invalid_intopy_derive.rs");
#[cfg(not(windows))]
t.compile_fail("tests/ui/invalid_intopy_with.rs");
t.compile_fail("tests/ui/invalid_frompy_derive.rs");
t.compile_fail("tests/ui/static_ref.rs");
t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs");
Expand Down
67 changes: 60 additions & 7 deletions tests/test_intopyobject.rs
100644 → 100755
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() {
});
}

#[derive(Debug, IntoPyObject)]
fn phantom_into_py<'py, T>(
_: std::borrow::Cow<'_, std::marker::PhantomData<T>>,
py: Python<'py>,
) -> PyResult<Bound<'py, PyAny>> {
std::any::type_name::<T>().into_bound_py_any(py)
}

#[derive(Debug, IntoPyObject, IntoPyObjectRef)]
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,43 @@ 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, item("my_object"))]
some_object_length: usize,
}

fn zap_into_py<'py>(
len: std::borrow::Cow<'_, 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}'"
);
});
}
32 changes: 32 additions & 0 deletions tests/ui/invalid_intopy_derive.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,36 @@ struct StructTransparentItem {
foo: String,
}

#[derive(IntoPyObject)]
#[pyo3(transparent)]
struct StructTransparentIntoPyWith {
#[pyo3(into_py_with = into)]
foo: String,
}

#[derive(IntoPyObjectRef)]
#[pyo3(transparent)]
struct StructTransparentIntoPyWithRef {
#[pyo3(into_py_with = into_ref)]
foo: String,
}

#[derive(IntoPyObject)]
#[pyo3(transparent)]
struct TupleTransparentIntoPyWith(#[pyo3(into_py_with = into)] String);

#[derive(IntoPyObject)]
enum EnumTupleIntoPyWith {
TransparentTuple(#[pyo3(into_py_with = into)] usize),
}

#[derive(IntoPyObject)]
enum EnumStructIntoPyWith {
#[pyo3(transparent)]
TransparentStruct {
#[pyo3(into_py_with = into)]
a: usize,
},
}

fn main() {}
Loading
Loading