Skip to content

Commit 2490163

Browse files
authored
add #[pyclass(from_py_object)] to opt-in to a FromPyObject impl (#5506)
1 parent 6da075d commit 2490163

File tree

9 files changed

+103
-3
lines changed

9 files changed

+103
-3
lines changed

guide/pyclass-parameters.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| `eq_int` | Implements `__eq__` using `__int__` for simple enums. |
1010
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
1111
| <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
12+
| `from_py_object` | Implement `FromPyObject` for this pyclass. Requires the pyclass to be `Clone`. |
1213
| <span style="white-space: pre">`frozen`</span> | Declares that your pyclass is immutable. It removes the borrow checker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. |
1314
| `generic` | Implements runtime parametrization for the class following [PEP 560](https://peps.python.org/pep-0560/). |
1415
| `get_all` | Generates getters for all fields of the pyclass. |

guide/src/conversions/traits.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ impl<'py> FromPyObject<'_, 'py> for Number {
558558
}
559559
```
560560

561+
As a second step the `from_py_object` option was introduced. This option also opts-out of the blanket implementation and instead generates a custom `FromPyObject` implementation for the pyclass which is functionally equivalent to the blanket.
562+
561563
### `IntoPyObject`
562564
The [`IntoPyObject`] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait,
563565
as does a `#[pyclass]` which doesn't use `extends`.

newsfragments/5506.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add `from_py_object` pyclass option, to opt-in to the extraction of pyclasses by value (requires `Clone`)

pyo3-macros-backend/src/attributes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub mod kw {
5353
syn::custom_keyword!(warn);
5454
syn::custom_keyword!(message);
5555
syn::custom_keyword!(category);
56+
syn::custom_keyword!(from_py_object);
5657
syn::custom_keyword!(skip_from_py_object);
5758
}
5859

pyo3-macros-backend/src/pyclass.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub struct PyClassPyO3Options {
9090
pub unsendable: Option<kw::unsendable>,
9191
pub weakref: Option<kw::weakref>,
9292
pub generic: Option<kw::generic>,
93+
pub from_py_object: Option<kw::from_py_object>,
9394
pub skip_from_py_object: Option<kw::skip_from_py_object>,
9495
}
9596

@@ -116,6 +117,7 @@ pub enum PyClassPyO3Option {
116117
Unsendable(kw::unsendable),
117118
Weakref(kw::weakref),
118119
Generic(kw::generic),
120+
FromPyObject(kw::from_py_object),
119121
SkipFromPyObject(kw::skip_from_py_object),
120122
}
121123

@@ -166,6 +168,8 @@ impl Parse for PyClassPyO3Option {
166168
input.parse().map(PyClassPyO3Option::Weakref)
167169
} else if lookahead.peek(attributes::kw::generic) {
168170
input.parse().map(PyClassPyO3Option::Generic)
171+
} else if lookahead.peek(attributes::kw::from_py_object) {
172+
input.parse().map(PyClassPyO3Option::FromPyObject)
169173
} else if lookahead.peek(attributes::kw::skip_from_py_object) {
170174
input.parse().map(PyClassPyO3Option::SkipFromPyObject)
171175
} else {
@@ -248,8 +252,19 @@ impl PyClassPyO3Options {
248252
}
249253
PyClassPyO3Option::Generic(generic) => set_option!(generic),
250254
PyClassPyO3Option::SkipFromPyObject(skip_from_py_object) => {
255+
ensure_spanned!(
256+
self.from_py_object.is_none(),
257+
skip_from_py_object.span() => "`skip_from_py_object` and `from_py_object` are mutually exclusive"
258+
);
251259
set_option!(skip_from_py_object)
252260
}
261+
PyClassPyO3Option::FromPyObject(from_py_object) => {
262+
ensure_spanned!(
263+
self.skip_from_py_object.is_none(),
264+
from_py_object.span() => "`skip_from_py_object` and `from_py_object` are mutually exclusive"
265+
);
266+
set_option!(from_py_object)
267+
}
253268
}
254269
Ok(())
255270
}
@@ -2504,7 +2519,29 @@ impl<'a> PyClassImplsBuilder<'a> {
25042519
quote! {}
25052520
};
25062521

2507-
let extract_pyclass_with_clone = if self.attr.options.skip_from_py_object.is_none() {
2522+
let extract_pyclass_with_clone = if let Some(from_py_object) =
2523+
self.attr.options.from_py_object
2524+
{
2525+
let input_ty = if cfg!(feature = "experimental-inspect") {
2526+
quote!(const INPUT_TYPE: &'static str = <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::TYPE_NAME;)
2527+
} else {
2528+
TokenStream::new()
2529+
};
2530+
quote_spanned! { from_py_object.span() =>
2531+
impl<'a, 'py> #pyo3_path::FromPyObject<'a, 'py> for #cls
2532+
where
2533+
Self: ::std::clone::Clone,
2534+
{
2535+
type Error = #pyo3_path::pyclass::PyClassGuardError<'a, 'py>;
2536+
2537+
#input_ty
2538+
2539+
fn extract(obj: #pyo3_path::Borrowed<'a, 'py, #pyo3_path::PyAny>) -> ::std::result::Result<Self, Self::Error> {
2540+
::std::result::Result::Ok(::std::clone::Clone::clone(&*obj.extract::<#pyo3_path::PyClassGuard<'_, #cls>>()?))
2541+
}
2542+
}
2543+
}
2544+
} else if self.attr.options.skip_from_py_object.is_none() {
25082545
quote!( impl #pyo3_path::impl_::pyclass::ExtractPyClassWithClone for #cls {} )
25092546
} else {
25102547
TokenStream::new()

src/conversion.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,25 @@ mod tests {
636636
})
637637
.unwrap();
638638
}
639+
640+
#[test]
641+
#[cfg(feature = "macros")]
642+
fn test_pyclass_from_py_object() {
643+
use crate::{types::PyAnyMethods, IntoPyObject, PyErr, Python};
644+
645+
#[crate::pyclass(crate = "crate", from_py_object)]
646+
#[derive(Clone)]
647+
struct Foo(i32);
648+
649+
Python::attach(|py| {
650+
let foo1 = 42i32.into_pyobject(py)?;
651+
assert!(foo1.extract::<Foo>().is_err());
652+
653+
let foo2 = Foo(0).into_pyobject(py)?;
654+
assert_eq!(foo2.extract::<Foo>()?.0, 0);
655+
656+
Ok::<_, PyErr>(())
657+
})
658+
.unwrap();
659+
}
639660
}

src/tests/hygiene/pyclass.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,9 @@ impl ::std::fmt::Display for Point {
151151
::std::write!(f, "({}, {}, {})", self.x, self.y, self.z)
152152
}
153153
}
154+
155+
#[crate::pyclass(crate = "crate", from_py_object)]
156+
#[derive(Clone)]
157+
pub struct Foo5 {
158+
a: i32,
159+
}

tests/ui/invalid_pyclass_args.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,16 @@ impl Display for MyEnumInvalidStrFmt {
181181
}
182182
}
183183

184+
#[pyclass(from_py_object, skip_from_py_object)]
185+
struct StructTooManyFromPyObject {
186+
a: String,
187+
b: String,
188+
}
189+
190+
#[pyclass(from_py_object)]
191+
struct StructFromPyObjectNoClone {
192+
a: String,
193+
b: String,
194+
}
195+
184196
fn main() {}

tests/ui/invalid_pyclass_args.stderr

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `skip_from_py_object`
1+
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
22
--> tests/ui/invalid_pyclass_args.rs:4:11
33
|
44
4 | #[pyclass(extend=pyo3::types::PyDict)]
@@ -46,7 +46,7 @@ error: expected string literal
4646
25 | #[pyclass(module = my_module)]
4747
| ^^^^^^^^^
4848

49-
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `skip_from_py_object`
49+
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
5050
--> tests/ui/invalid_pyclass_args.rs:28:11
5151
|
5252
28 | #[pyclass(weakrev)]
@@ -162,6 +162,25 @@ error: The format string syntax cannot be used with enums
162162
171 | #[pyclass(eq, str = "Stuff...")]
163163
| ^^^^^^^^^^
164164

165+
error: `skip_from_py_object` and `from_py_object` are mutually exclusive
166+
--> tests/ui/invalid_pyclass_args.rs:184:27
167+
|
168+
184 | #[pyclass(from_py_object, skip_from_py_object)]
169+
| ^^^^^^^^^^^^^^^^^^^
170+
171+
error[E0277]: the trait bound `StructFromPyObjectNoClone: Clone` is not satisfied
172+
--> tests/ui/invalid_pyclass_args.rs:190:11
173+
|
174+
190 | #[pyclass(from_py_object)]
175+
| ^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `StructFromPyObjectNoClone`
176+
|
177+
= help: see issue #48214
178+
help: consider annotating `StructFromPyObjectNoClone` with `#[derive(Clone)]`
179+
|
180+
191 + #[derive(Clone)]
181+
192 | struct StructFromPyObjectNoClone {
182+
|
183+
165184
error[E0592]: duplicate definitions with name `__pymethod___richcmp____`
166185
--> tests/ui/invalid_pyclass_args.rs:37:1
167186
|

0 commit comments

Comments
 (0)