diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 18ea4d89324..1aa445cce41 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -492,7 +492,9 @@ If the input is neither a string nor an integer, the error message will be: - 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. + - note that the default value is only used if the field is not set. + If the field is set and the conversion function from Python to Rust fails, an exception is raised and the default value is not used. + - this attribute is only supported on named 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): @@ -503,6 +505,8 @@ use pyo3::prelude::*; struct RustyStruct { #[pyo3(item("value"), default, from_py_with = "Bound::<'_, PyAny>::len")] len: usize, + #[pyo3(item)] + other: usize, } # # use pyo3::types::PyDict; @@ -511,13 +515,17 @@ struct RustyStruct { # // Filled case # let dict = PyDict::new(py); # dict.set_item("value", (1,)).unwrap(); +# dict.set_item("other", 1).unwrap(); # let result = dict.extract::()?; # assert_eq!(result.len, 1); +# assert_eq!(result.other, 1); # # // Empty case # let dict = PyDict::new(py); +# dict.set_item("other", 1).unwrap(); # let result = dict.extract::()?; # assert_eq!(result.len, 0); +# assert_eq!(result.other, 1); # Ok(()) # }) # } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index b66a1dcefda..b353e2dc16d 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -147,6 +147,10 @@ impl<'a> Container<'a> { attrs.getter.is_none(), field.span() => "`getter` is not permitted on tuple struct elements." ); + ensure_spanned!( + attrs.default.is_none(), + field.span() => "`default` is not permitted on tuple struct elements." + ); Ok(TupleStructField { from_py_with: attrs.from_py_with, }) @@ -200,7 +204,11 @@ impl<'a> Container<'a> { }) }) .collect::>>()?; - if options.transparent { + if struct_fields.iter().all(|field| field.default.is_some()) { + bail_spanned!( + fields.span() => "cannot derive FromPyObject for structs and variants with only default values" + ) + } else if options.transparent { ensure_spanned!( struct_fields.len() == 1, fields.span() => "transparent structs and variants can only have 1 field" diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 26437906b96..d72a215814c 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -690,15 +690,21 @@ fn test_with_keyword_item() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub struct WithDefaultItem { #[pyo3(item, default)] - value: Option, + opt: Option, + #[pyo3(item)] + value: usize, } #[test] fn test_with_default_item() { Python::with_gil(|py| { let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); let result = dict.extract::().unwrap(); - let expected = WithDefaultItem { value: None }; + let expected = WithDefaultItem { + value: 3, + opt: None, + }; assert_eq!(result, expected); }); } @@ -706,6 +712,8 @@ fn test_with_default_item() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub struct WithExplicitDefaultItem { #[pyo3(item, default = 1)] + opt: usize, + #[pyo3(item)] value: usize, } @@ -713,8 +721,9 @@ pub struct WithExplicitDefaultItem { fn test_with_explicit_default_item() { Python::with_gil(|py| { let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); let result = dict.extract::().unwrap(); - let expected = WithExplicitDefaultItem { value: 1 }; + let expected = WithExplicitDefaultItem { value: 3, opt: 1 }; assert_eq!(result, expected); }); } @@ -722,6 +731,8 @@ fn test_with_explicit_default_item() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub struct WithDefaultItemAndConversionFunction { #[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")] + opt: usize, + #[pyo3(item)] value: usize, } @@ -730,26 +741,62 @@ 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(); + dict.set_item("opt", (1,)).unwrap(); + dict.set_item("value", 3).unwrap(); let result = dict .extract::() .unwrap(); - let expected = WithDefaultItemAndConversionFunction { value: 1 }; + let expected = WithDefaultItemAndConversionFunction { opt: 1, value: 3 }; assert_eq!(result, expected); // Empty case let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); let result = dict .extract::() .unwrap(); - let expected = WithDefaultItemAndConversionFunction { value: 0 }; + let expected = WithDefaultItemAndConversionFunction { opt: 0, value: 3 }; assert_eq!(result, expected); // Error case let dict = PyDict::new(py); - dict.set_item("value", 1).unwrap(); + dict.set_item("value", 3).unwrap(); + dict.set_item("opt", 1).unwrap(); assert!(dict .extract::() .is_err()); }); } + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub enum WithDefaultItemEnum { + #[pyo3(from_item_all)] + Foo { + a: usize, + #[pyo3(default)] + b: usize, + }, + NeverUsedA { + a: usize, + }, +} + +#[test] +fn test_with_default_item_enum() { + Python::with_gil(|py| { + // A and B filled + let dict = PyDict::new(py); + dict.set_item("a", 1).unwrap(); + dict.set_item("b", 2).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithDefaultItemEnum::Foo { a: 1, b: 2 }; + assert_eq!(result, expected); + + // A filled + let dict = PyDict::new(py); + dict.set_item("a", 1).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithDefaultItemEnum::Foo { a: 1, b: 0 }; + assert_eq!(result, expected); + }); +} diff --git a/tests/ui/invalid_frompy_derive.rs b/tests/ui/invalid_frompy_derive.rs index f123b149fb8..d3a778e686b 100644 --- a/tests/ui/invalid_frompy_derive.rs +++ b/tests/ui/invalid_frompy_derive.rs @@ -213,4 +213,21 @@ struct FromItemAllConflictAttrWithArgs { field: String, } +#[derive(FromPyObject)] +struct StructWithOnlyDefaultValues { + #[pyo3(default)] + field: String, +} + +#[derive(FromPyObject)] +enum EnumVariantWithOnlyDefaultValues { + Foo { + #[pyo3(default)] + field: String, + }, +} + +#[derive(FromPyObject)] +struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); + fn main() {} diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index e48176b45c5..5b8c1fc718b 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -223,3 +223,29 @@ error: The struct is already annotated with `from_item_all`, `attribute` is not | 210 | #[pyo3(from_item_all)] | ^^^^^^^^^^^^^ + +error: cannot derive FromPyObject for structs and variants with only default values + --> tests/ui/invalid_frompy_derive.rs:217:36 + | +217 | struct StructWithOnlyDefaultValues { + | ____________________________________^ +218 | | #[pyo3(default)] +219 | | field: String, +220 | | } + | |_^ + +error: cannot derive FromPyObject for structs and variants with only default values + --> tests/ui/invalid_frompy_derive.rs:224:9 + | +224 | Foo { + | _________^ +225 | | #[pyo3(default)] +226 | | field: String, +227 | | }, + | |_____^ + +error: `default` is not permitted on tuple struct elements. + --> tests/ui/invalid_frompy_derive.rs:231:37 + | +231 | struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); + | ^