From 1df9a531330010bad8183a05da3efe5f0733b7e3 Mon Sep 17 00:00:00 2001 From: "thomas.pellissier-tanon" Date: Mon, 6 Jan 2025 12:51:16 +0100 Subject: [PATCH] Documentation, testing and hygiene --- guide/src/conversions/traits.md | 34 ++++++++++++++++++++++++ newsfragments/4829.added.md | 2 +- pyo3-macros-backend/src/attributes.rs | 3 +-- pyo3-macros-backend/src/frompyobject.rs | 6 ++--- src/tests/hygiene/misc.rs | 2 ++ tests/test_frompyobject.rs | 35 +++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index c4e8f14866c..18ea4d89324 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -488,6 +488,40 @@ If the input is neither a string nor an integer, the error message will be: - apply a custom function to convert the field from Python the desired Rust type. - the argument must be the name of the function as a string. - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. +- `pyo3(default)`, `pyo3(default = ...)` + - if the argument is set, uses the given default value. + - in this case, the argument must be a Rust expression returning a value of the desired Rust type. + - if the argument is not set, [`Default::default`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default) is used. + - this attribute is only supported on named struct fields. + +For example, the code below applies the given conversion function on the `"value"` dict item to compute its length or fall back to the type default value (0): + +```rust +use pyo3::prelude::*; + +#[derive(FromPyObject)] +struct RustyStruct { + #[pyo3(item("value"), default, from_py_with = "Bound::<'_, PyAny>::len")] + len: usize, +} +# +# use pyo3::types::PyDict; +# fn main() -> PyResult<()> { +# Python::with_gil(|py| -> PyResult<()> { +# // Filled case +# let dict = PyDict::new(py); +# dict.set_item("value", (1,)).unwrap(); +# let result = dict.extract::()?; +# assert_eq!(result.len, 1); +# +# // Empty case +# let dict = PyDict::new(py); +# let result = dict.extract::()?; +# assert_eq!(result.len, 0); +# Ok(()) +# }) +# } +``` ### `IntoPyObject` The ['IntoPyObject'] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, diff --git a/newsfragments/4829.added.md b/newsfragments/4829.added.md index 849aaaf39a6..9400501a799 100644 --- a/newsfragments/4829.added.md +++ b/newsfragments/4829.added.md @@ -1 +1 @@ -`derive(FromPyObject)` allow a `default` attribute to set a default value for extracted fields. The default value is either provided explicitly or fetched via `Default::default()`. \ No newline at end of file +`derive(FromPyObject)` allow a `default` attribute to set a default value for extracted fields of named structs. The default value is either provided explicitly or fetched via `Default::default()`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index c5a81e47ec1..bd5da377121 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -45,7 +45,6 @@ pub mod kw { syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); syn::custom_keyword!(gil_used); - syn::custom_keyword!(default); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { @@ -352,7 +351,7 @@ impl ToTokens for OptionalKeywordAttribute { pub type FromPyWithAttribute = KeywordAttribute>; -pub type DefaultAttribute = OptionalKeywordAttribute; +pub type DefaultAttribute = OptionalKeywordAttribute; /// For specifying the path to the pyo3 crate. pub type CrateAttribute = KeywordAttribute>; diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index a2799f002c8..b66a1dcefda 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -362,9 +362,9 @@ impl<'a> Container<'a> { let default_expr = if let Some(default_expr) = &default.value { default_expr.to_token_stream() } else { - quote!(Default::default()) + quote!(::std::default::Default::default()) }; - quote!(if let Ok(value) = #getter { + quote!(if let ::std::result::Result::Ok(value) = #getter { #extractor } else { #default_expr @@ -533,7 +533,7 @@ impl Parse for FieldPyO3Attribute { } } else if lookahead.peek(attributes::kw::from_py_with) { input.parse().map(FieldPyO3Attribute::FromPyWith) - } else if lookahead.peek(attributes::kw::default) { + } else if lookahead.peek(Token![default]) { input.parse().map(FieldPyO3Attribute::Default) } else { Err(lookahead.error()) diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 6e00167ddb6..a953cea4a24 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -12,6 +12,8 @@ struct Derive3 { f: i32, #[pyo3(item(42))] g: i32, + #[pyo3(default)] + h: i32, } // struct case #[derive(crate::FromPyObject)] diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 75252032842..26437906b96 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -718,3 +718,38 @@ fn test_with_explicit_default_item() { assert_eq!(result, expected); }); } + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithDefaultItemAndConversionFunction { + #[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")] + value: usize, +} + +#[test] +fn test_with_default_item_and_conversion_function() { + Python::with_gil(|py| { + // Filled case + let dict = PyDict::new(py); + dict.set_item("value", (1,)).unwrap(); + let result = dict + .extract::() + .unwrap(); + let expected = WithDefaultItemAndConversionFunction { value: 1 }; + assert_eq!(result, expected); + + // Empty case + let dict = PyDict::new(py); + let result = dict + .extract::() + .unwrap(); + let expected = WithDefaultItemAndConversionFunction { value: 0 }; + assert_eq!(result, expected); + + // Error case + let dict = PyDict::new(py); + dict.set_item("value", 1).unwrap(); + assert!(dict + .extract::() + .is_err()); + }); +}