From 37b6412f9098ee71d475ed454cc2b1cfa6dcf778 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 4 Mar 2024 21:18:42 +0000 Subject: [PATCH 1/2] add `experimental-async` feature --- Cargo.toml | 6 +- guide/src/features.md | 6 ++ newsfragments/3931.added.md | 1 + pyo3-macros-backend/Cargo.toml | 3 + pyo3-macros-backend/src/method.rs | 7 ++ pyo3-macros/Cargo.toml | 1 + src/impl_.rs | 2 +- src/lib.rs | 2 +- tests/test_compile_error.rs | 2 + tests/test_coroutine.rs | 2 +- tests/ui/invalid_argument_attributes.rs | 25 ------- tests/ui/invalid_argument_attributes.stderr | 79 --------------------- tests/ui/invalid_cancel_handle.rs | 22 ++++++ tests/ui/invalid_cancel_handle.stderr | 72 +++++++++++++++++++ 14 files changed, 122 insertions(+), 108 deletions(-) create mode 100644 newsfragments/3931.added.md create mode 100644 tests/ui/invalid_cancel_handle.rs create mode 100644 tests/ui/invalid_cancel_handle.stderr diff --git a/Cargo.toml b/Cargo.toml index e7364a7c9f5..fad4bfca98c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,9 @@ pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.0-dev", featu [features] default = ["macros"] +# Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`. +experimental-async = ["macros", "pyo3-macros/experimental-async"] + # Enables pyo3::inspect module and additional type information on FromPyObject # and IntoPy traits experimental-inspect = [] @@ -116,8 +119,9 @@ full = [ "chrono", "chrono-tz", "either", - "experimental-inspect", + "experimental-async", "experimental-declarative-modules", + "experimental-inspect", "eyre", "hashbrown", "indexmap", diff --git a/guide/src/features.md b/guide/src/features.md index 43124e0076e..118284959d6 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -51,6 +51,12 @@ If you do not enable this feature, you should call `pyo3::prepare_freethreaded_p ## Advanced Features +### `experimental-async` + +This feature adds support for `async fn` in `#[pyfunction]` and `#[pymethods]`. + +The feature has some unfinished refinements and performance improvements. To help finish this off, see [issue #1632](https://github.com/PyO3/pyo3/issues/1632) and its associated draft PRs. + ### `experimental-inspect` This feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. diff --git a/newsfragments/3931.added.md b/newsfragments/3931.added.md new file mode 100644 index 00000000000..b532adeeae5 --- /dev/null +++ b/newsfragments/3931.added.md @@ -0,0 +1 @@ +Add `experimental-async` feature. diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 0e83cc29fd4..665c8c3510d 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -26,3 +26,6 @@ features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-trait [lints] workspace = true + +[features] +experimental-async = [] diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 1d2d22b236b..9a8cb828b22 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -512,6 +512,13 @@ impl<'a> FnSpec<'a> { } } + if self.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + self.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } + let rust_call = |args: Vec, holders: &mut Vec| { let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a0368a5f364..97d2de07cba 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] multiple-pymethods = [] +experimental-async = ["pyo3-macros-backend/experimental-async"] experimental-declarative-modules = [] [dependencies] diff --git a/src/impl_.rs b/src/impl_.rs index 77f9ff4ea1f..ea71b257c0e 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -6,7 +6,7 @@ //! APIs may may change at any time without documentation in the CHANGELOG and without //! breaking semver guarantees. -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; pub mod deprecations; pub mod extract_argument; diff --git a/src/lib.rs b/src/lib.rs index 26d2ec55da1..c2c7d96a40a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -418,7 +418,7 @@ pub mod buffer; pub mod callback; pub mod conversion; mod conversions; -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; #[macro_use] #[doc(hidden)] diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 5f2d25db92f..a43d662018e 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -48,4 +48,6 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); + #[cfg(feature = "experimental-async")] + t.compile_fail("tests/ui/invalid_cancel_handle.rs"); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index db79c72a233..17539fa113e 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "macros")] +#![cfg(feature = "experimental-async")] #![cfg(not(target_arch = "wasm32"))] use std::{task::Poll, thread, time::Duration}; diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index ed9d6ce6971..311c6c03e0e 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -15,29 +15,4 @@ fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) { #[pyfunction] fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} -#[pyfunction] -async fn from_py_with_value_and_cancel_handle( - #[pyo3(from_py_with = "func", cancel_handle)] _param: String, -) { -} - -#[pyfunction] -async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_repeated2( - #[pyo3(cancel_handle)] _param: String, - #[pyo3(cancel_handle)] _param2: String, -) { -} - -#[pyfunction] -fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - fn main() {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index c122dd25f8c..d27c25fd179 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -27,82 +27,3 @@ error: `from_py_with` may only be specified once per argument | 16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} | ^^^^^^^^^^^^ - -error: `from_py_with` and `cancel_handle` cannot be specified together - --> tests/ui/invalid_argument_attributes.rs:20:35 - | -20 | #[pyo3(from_py_with = "func", cancel_handle)] _param: String, - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once per argument - --> tests/ui/invalid_argument_attributes.rs:25:55 - | -25 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once - --> tests/ui/invalid_argument_attributes.rs:30:28 - | -30 | #[pyo3(cancel_handle)] _param2: String, - | ^^^^^^^ - -error: `cancel_handle` attribute can only be used with `async fn` - --> tests/ui/invalid_argument_attributes.rs:35:53 - | -35 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^ - -error[E0308]: mismatched types - --> tests/ui/invalid_argument_attributes.rs:37:1 - | -37 | #[pyfunction] - | ^^^^^^^^^^^^^ - | | - | expected `String`, found `CancelHandle` - | arguments to this function are incorrect - | -note: function defined here - --> tests/ui/invalid_argument_attributes.rs:38:10 - | -38 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- - = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `PyClass` is not implemented for `CancelHandle` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` - -error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `Clone` is not implemented for `CancelHandle` - | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - &'a pyo3::Bound<'py, T> - &'a pyo3::coroutine::Coroutine - &'a mut pyo3::coroutine::Coroutine - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_cancel_handle.rs b/tests/ui/invalid_cancel_handle.rs new file mode 100644 index 00000000000..59076b14418 --- /dev/null +++ b/tests/ui/invalid_cancel_handle.rs @@ -0,0 +1,22 @@ +use pyo3::prelude::*; + +#[pyfunction] +async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_repeated2( + #[pyo3(cancel_handle)] _param: String, + #[pyo3(cancel_handle)] _param2: String, +) { +} + +#[pyfunction] +fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + +fn main() {} diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr new file mode 100644 index 00000000000..6dc3ff3ccab --- /dev/null +++ b/tests/ui/invalid_cancel_handle.stderr @@ -0,0 +1,72 @@ +error: `cancel_handle` may only be specified once per argument + --> tests/ui/invalid_cancel_handle.rs:4:55 + | +4 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^ + +error: `cancel_handle` may only be specified once + --> tests/ui/invalid_cancel_handle.rs:9:28 + | +9 | #[pyo3(cancel_handle)] _param2: String, + | ^^^^^^^ + +error: `cancel_handle` attribute can only be used with `async fn` + --> tests/ui/invalid_cancel_handle.rs:14:53 + | +14 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^ + +error[E0308]: mismatched types + --> tests/ui/invalid_cancel_handle.rs:16:1 + | +16 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | expected `String`, found `CancelHandle` + | arguments to this function are incorrect + | +note: function defined here + --> tests/ui/invalid_cancel_handle.rs:17:10 + | +17 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `PyClass` is not implemented for `CancelHandle` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `Clone` is not implemented for `CancelHandle` + | + = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: + &'a pyo3::Bound<'py, T> + &'a pyo3::coroutine::Coroutine + &'a mut pyo3::coroutine::Coroutine + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` From fcdcbc72143d6992f6c8b33b40d5d28ef9d433ab Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 5 Mar 2024 23:56:14 +0000 Subject: [PATCH 2/2] gate async doctests on feature --- guide/src/async-await.md | 6 +++++- tests/test_compile_error.rs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/guide/src/async-await.md b/guide/src/async-await.md index c14b5d93d84..688f0a65bc4 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -6,6 +6,7 @@ ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use std::{thread, time::Duration}; use futures::channel::oneshot; use pyo3::prelude::*; @@ -20,6 +21,7 @@ async fn sleep(seconds: f64, result: Option) -> Option { rx.await.unwrap(); result } +# } ``` *Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.* @@ -72,6 +74,7 @@ Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOC ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use futures::FutureExt; use pyo3::prelude::*; use pyo3::coroutine::CancelHandle; @@ -83,11 +86,12 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) { _ = cancel.cancelled().fuse() => println!("cancelled"), } } +# } ``` ## The `Coroutine` type -To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). +To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). Each `coroutine.send` call is translated to a `Future::poll` call. If a [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) parameter is declared, the exception passed to `coroutine.throw` call is stored in it and can be retrieved with [`CancelHandle::cancelled`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html#method.cancelled); otherwise, it cancels the Rust future, and the exception is reraised; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index a43d662018e..f5fbec7a533 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -27,7 +27,8 @@ fn test_compile_errors() { t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); - #[cfg(Py_LIMITED_API)] + // output changes with async feature + #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs");