Skip to content

Commit

Permalink
Support enum variant named fields and cover failures
Browse files Browse the repository at this point in the history
  • Loading branch information
Tpt committed Jan 8, 2025
1 parent 1df9a53 commit 4e6f1f7
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 9 deletions.
10 changes: 9 additions & 1 deletion guide/src/conversions/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand All @@ -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;
Expand All @@ -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::<RustyStruct>()?;
# 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::<RustyStruct>()?;
# assert_eq!(result.len, 0);
# assert_eq!(result.other, 1);
# Ok(())
# })
# }
Expand Down
10 changes: 9 additions & 1 deletion pyo3-macros-backend/src/frompyobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -200,7 +204,11 @@ impl<'a> Container<'a> {
})
})
.collect::<Result<Vec<_>>>()?;
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"
Expand Down
61 changes: 54 additions & 7 deletions tests/test_frompyobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,38 +690,49 @@ fn test_with_keyword_item() {
#[derive(Debug, FromPyObject, PartialEq, Eq)]
pub struct WithDefaultItem {
#[pyo3(item, default)]
value: Option<usize>,
opt: Option<usize>,
#[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::<WithDefaultItem>().unwrap();
let expected = WithDefaultItem { value: None };
let expected = WithDefaultItem {
value: 3,
opt: None,
};
assert_eq!(result, expected);
});
}

#[derive(Debug, FromPyObject, PartialEq, Eq)]
pub struct WithExplicitDefaultItem {
#[pyo3(item, default = 1)]
opt: usize,
#[pyo3(item)]
value: usize,
}

#[test]
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::<WithExplicitDefaultItem>().unwrap();
let expected = WithExplicitDefaultItem { value: 1 };
let expected = WithExplicitDefaultItem { value: 3, opt: 1 };
assert_eq!(result, expected);
});
}

#[derive(Debug, FromPyObject, PartialEq, Eq)]
pub struct WithDefaultItemAndConversionFunction {
#[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")]
opt: usize,
#[pyo3(item)]
value: usize,
}

Expand All @@ -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::<WithDefaultItemAndConversionFunction>()
.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::<WithDefaultItemAndConversionFunction>()
.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::<WithDefaultItemAndConversionFunction>()
.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::<WithDefaultItemEnum>().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::<WithDefaultItemEnum>().unwrap();
let expected = WithDefaultItemEnum::Foo { a: 1, b: 0 };
assert_eq!(result, expected);
});
}
17 changes: 17 additions & 0 deletions tests/ui/invalid_frompy_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
26 changes: 26 additions & 0 deletions tests/ui/invalid_frompy_derive.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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);
| ^

0 comments on commit 4e6f1f7

Please sign in to comment.