From 627841f1e23650ab00caac7b7090bb5335ac3a91 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Mon, 23 Oct 2023 14:43:12 +0200 Subject: [PATCH] feat: support `async fn` in macros with coroutine implementation --- Cargo.toml | 4 + guide/src/SUMMARY.md | 1 + guide/src/async-await.md | 78 +++++++++++ guide/src/ecosystem/async-await.md | 2 + newsfragments/3540.added.md | 1 + pyo3-macros-backend/src/method.rs | 8 +- pyo3-macros-backend/src/pyfunction.rs | 5 +- pyo3-macros-backend/src/pymethod.rs | 3 +- pyo3-macros-backend/src/utils.rs | 13 +- src/coroutine.rs | 137 ++++++++++++++++++++ src/coroutine/waker.rs | 97 ++++++++++++++ src/impl_.rs | 2 + src/impl_/coroutine.rs | 19 +++ src/lib.rs | 3 + tests/test_coroutine.rs | 98 ++++++++++++++ tests/ui/abi3_nativetype_inheritance.stderr | 4 +- tests/ui/invalid_pyfunctions.rs | 3 - tests/ui/invalid_pyfunctions.stderr | 20 +-- tests/ui/invalid_pymethods.rs | 5 - tests/ui/invalid_pymethods.stderr | 32 ++--- 20 files changed, 474 insertions(+), 61 deletions(-) create mode 100644 guide/src/async-await.md create mode 100644 newsfragments/3540.added.md create mode 100644 src/coroutine.rs create mode 100644 src/coroutine/waker.rs create mode 100644 src/impl_/coroutine.rs create mode 100644 tests/test_coroutine.rs diff --git a/Cargo.toml b/Cargo.toml index f27dd90ee91..862ef6d0d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ unindent = { version = "0.2.1", optional = true } # support crate for multiple-pymethods feature inventory = { version = "0.3.0", optional = true } +# coroutine implementation +futures-util = "0.3" + # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } @@ -54,6 +57,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" rayon = "1.6.1" widestring = "0.5.1" +futures = "0.3.28" [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "0.21.0-dev", features = ["resolve-config"] } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 730f998530d..9e738a79946 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -19,6 +19,7 @@ - [Conversion traits](conversions/traits.md)] - [Python exceptions](exception.md) - [Calling Python from Rust](python_from_rust.md) +- [Using `async` and `await`](async-await.md) - [GIL, mutability and object types](types.md) - [Parallelism](parallelism.md) - [Debugging](debugging.md) diff --git a/guide/src/async-await.md b/guide/src/async-await.md new file mode 100644 index 00000000000..f54a8aaa4f1 --- /dev/null +++ b/guide/src/async-await.md @@ -0,0 +1,78 @@ +# Using `async` and `await` + +*This feature is still in active development. See [the related issue](https://github.com/PyO3/pyo3/issues/1632).* + +`#[pyfunction]` and `#[pymethods]` attributes also support `async fn`. + +```rust +# #![allow(dead_code)] +use std::{thread, time::Duration}; +use futures::channel::oneshot; +use pyo3::prelude::*; + +#[pyfunction] +async fn sleep(seconds: f64, result: Option) -> Option { + let (tx, rx) = oneshot::channel(); + thread::spawn(move || { + thread::sleep(Duration::from_secs_f64(seconds)); + tx.send(()).unwrap(); + }); + 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.* + +## `Send + 'static` constraint + +Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object. + +As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile(arg: &PyAny, py: Python<'_>) -> &PyAny`. + +It also means that methods cannot use `&self`/`&mut self`, *but this restriction should be dropped in the future.* + + +## Implicit GIL holding + +Even if it is not possible to pass a `py: Python<'_>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'_>`/`&PyAny` parameter, yet the GIL is held. + +It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. + +## Release the GIL across `.await` + +There is currently no simple way to release the GIL when awaiting a future, *but solutions are currently in development*. + +Here is the advised workaround for now: + +```rust,ignore +use std::{future::Future, pin::{Pin, pin}, task::{Context, Poll}}; +use pyo3::prelude::*; + +struct AllowThreads(F); + +impl Future for AllowThreads +where + F: Future + Unpin + Send, + F::Output: Send, +{ + type Output = F::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let waker = cx.waker(); + Python::with_gil(|gil| { + gil.allow_threads(|| pin!(&mut self.0).poll(&mut Context::from_waker(waker))) + }) + } +} +``` + +## Cancellation + +*To be implemented* + +## 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). Each `coroutine.send` call is translated to `Future::poll` call, while `coroutine.throw` call reraise the exception *(this behavior will be configurable with cancellation support)*. + +*The type does not yet have a public constructor until the design is finalized.* \ No newline at end of file diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 719f7dbe683..f537ab90df1 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -1,5 +1,7 @@ # Using `async` and `await` +*`async`/`await` support is currently being integrated in PyO3. See the [dedicated documentation](../async-await.md)* + If you are working with a Python library that makes use of async functions or wish to provide Python bindings for an async Rust library, [`pyo3-asyncio`](https://github.com/awestlake87/pyo3-asyncio) likely has the tools you need. It provides conversions between async functions in both Python and diff --git a/newsfragments/3540.added.md b/newsfragments/3540.added.md new file mode 100644 index 00000000000..2b113193bef --- /dev/null +++ b/newsfragments/3540.added.md @@ -0,0 +1 @@ +Support `async fn` in macros with coroutine implementation \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 428efc950a1..7ea65899850 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -228,6 +228,7 @@ pub struct FnSpec<'a> { pub output: syn::Type, pub convention: CallingConvention, pub text_signature: Option, + pub asyncness: Option, pub unsafety: Option, pub deprecations: Deprecations, } @@ -317,6 +318,7 @@ impl<'a> FnSpec<'a> { signature, output: ty, text_signature, + asyncness: sig.asyncness, unsafety: sig.unsafety, deprecations, }) @@ -445,7 +447,11 @@ impl<'a> FnSpec<'a> { let func_name = &self.name; let rust_call = |args: Vec| { - quotes::map_result_into_ptr(quotes::ok_wrap(quote! { function(#self_arg #(#args),*) })) + let mut call = quote! { function(#self_arg #(#args),*) }; + if self.asyncness.is_some() { + call = quote! { _pyo3::impl_::coroutine::wrap_future(#call) }; + } + quotes::map_result_into_ptr(quotes::ok_wrap(call)) }; let rust_name = if let Some(cls) = cls { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b1a2bcc7a35..f0a1c18422a 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -6,7 +6,7 @@ use crate::{ deprecations::Deprecations, method::{self, CallingConvention, FnArg}, pymethod::check_generic, - utils::{ensure_not_async_fn, get_pyo3_crate}, + utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -179,8 +179,6 @@ pub fn impl_wrap_pyfunction( options: PyFunctionOptions, ) -> syn::Result { check_generic(&func.sig)?; - ensure_not_async_fn(&func.sig)?; - let PyFunctionOptions { pass_module, name, @@ -230,6 +228,7 @@ pub fn impl_wrap_pyfunction( signature, output: ty, text_signature, + asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, deprecations: Deprecations::new(), }; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index eaf050b8daa..a8fd3b41a18 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode}; -use crate::utils::{ensure_not_async_fn, PythonDoc}; +use crate::utils::PythonDoc; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, pyfunction::PyFunctionOptions, @@ -188,7 +188,6 @@ pub fn gen_py_method( options: PyFunctionOptions, ) -> Result { check_generic(sig)?; - ensure_not_async_fn(sig)?; ensure_function_options_valid(&options)?; let method = PyMethod::parse(sig, meth_attrs, options)?; let spec = &method.spec; diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 65da11f28e1..360b1ec2341 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,6 +1,6 @@ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; -use syn::{punctuated::Punctuated, spanned::Spanned, Token}; +use syn::{punctuated::Punctuated, Token}; use crate::attributes::{CrateAttribute, RenamingRule}; @@ -137,17 +137,6 @@ impl quote::ToTokens for PythonDoc { } } -pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> { - if let Some(asyncness) = &sig.asyncness { - bail_spanned!( - asyncness.span() => "`async fn` is not yet supported for Python functions.\n\n\ - Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and \ - Python. For more information, see https://github.com/PyO3/pyo3/issues/1632" - ); - }; - Ok(()) -} - pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { while let syn::Type::Group(g) = ty { ty = &*g.elem; diff --git a/src/coroutine.rs b/src/coroutine.rs new file mode 100644 index 00000000000..564262f3bf4 --- /dev/null +++ b/src/coroutine.rs @@ -0,0 +1,137 @@ +//! Python coroutine implementation, used notably when wrapping `async fn` +//! with `#[pyfunction]`/`#[pymethods]`. +use std::{ + any::Any, + future::Future, + panic, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use futures_util::FutureExt; +use pyo3_macros::{pyclass, pymethods}; + +use crate::{ + coroutine::waker::AsyncioWaker, + exceptions::{PyRuntimeError, PyStopIteration}, + panic::PanicException, + pyclass::IterNextOutput, + types::PyIterator, + IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, +}; + +mod waker; + +const COROUTINE_REUSED_ERROR: &str = "cannot reuse already awaited coroutine"; + +type FutureOutput = Result, Box>; + +/// Python coroutine wrapping a [`Future`]. +#[pyclass(crate = "crate")] +pub struct Coroutine { + future: Option + Send>>>, + waker: Option>, +} + +impl Coroutine { + /// Wrap a future into a Python coroutine. + /// + /// Coroutine `send` polls the wrapped future, ignoring the value passed + /// (should always be `None` anyway). + /// + /// `Coroutine `throw` drop the wrapped future and reraise the exception passed + pub(crate) fn from_future(future: F) -> Self + where + F: Future> + Send + 'static, + T: IntoPy, + PyErr: From, + { + let wrap = async move { + let obj = future.await?; + // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) + Ok(obj.into_py(unsafe { Python::assume_gil_acquired() })) + }; + Self { + future: Some(Box::pin(panic::AssertUnwindSafe(wrap).catch_unwind())), + waker: None, + } + } + + fn poll( + &mut self, + py: Python<'_>, + throw: Option, + ) -> PyResult> { + // raise if the coroutine has already been run to completion + let future_rs = match self.future { + Some(ref mut fut) => fut, + None => return Err(PyRuntimeError::new_err(COROUTINE_REUSED_ERROR)), + }; + // reraise thrown exception it + if let Some(exc) = throw { + self.close(); + return Err(PyErr::from_value(exc.as_ref(py))); + } + // create a new waker, or try to reset it in place + if let Some(waker) = self.waker.as_mut().and_then(Arc::get_mut) { + waker.reset(); + } else { + self.waker = Some(Arc::new(AsyncioWaker::new())); + } + let waker = futures_util::task::waker(self.waker.clone().unwrap()); + // poll the Rust future and forward its results if ready + if let Poll::Ready(res) = future_rs.as_mut().poll(&mut Context::from_waker(&waker)) { + self.close(); + return match res { + Ok(res) => Ok(IterNextOutput::Return(res?)), + Err(err) => Err(PanicException::from_panic_payload(err)), + }; + } + // otherwise, initialize the waker `asyncio.Future` + if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? { + // `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__` + // and will yield itself if its result has not been set in polling above + if let Some(future) = PyIterator::from_object(future).unwrap().next() { + // future has not been leaked into Python for now, and Rust code can only call + // `set_result(None)` in `ArcWake` implementation, so it's safe to unwrap + return Ok(IterNextOutput::Yield(future.unwrap().into())); + } + } + // if waker has been waken during future polling, this is roughly equivalent to + // `await asyncio.sleep(0)`, so just yield `None`. + Ok(IterNextOutput::Yield(py.None().into())) + } +} + +pub(crate) fn iter_result(result: IterNextOutput) -> PyResult { + match result { + IterNextOutput::Yield(ob) => Ok(ob), + IterNextOutput::Return(ob) => Err(PyStopIteration::new_err(ob)), + } +} + +#[pymethods(crate = "crate")] +impl Coroutine { + fn send(&mut self, py: Python<'_>, _value: &PyAny) -> PyResult { + iter_result(self.poll(py, None)?) + } + + fn throw(&mut self, py: Python<'_>, exc: PyObject) -> PyResult { + iter_result(self.poll(py, Some(exc))?) + } + + fn close(&mut self) { + // the Rust future is dropped, and the field set to `None` + // to indicate the coroutine has been run to completion + drop(self.future.take()); + } + + fn __await__(self_: Py) -> Py { + self_ + } + + fn __next__(&mut self, py: Python<'_>) -> PyResult> { + self.poll(py, None) + } +} diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs new file mode 100644 index 00000000000..7ed4103fbb7 --- /dev/null +++ b/src/coroutine/waker.rs @@ -0,0 +1,97 @@ +use crate::sync::GILOnceCell; +use crate::types::PyCFunction; +use crate::{intern, wrap_pyfunction, Py, PyAny, PyObject, PyResult, Python}; +use futures_util::task::ArcWake; +use pyo3_macros::pyfunction; +use std::sync::Arc; + +/// Lazy `asyncio.Future` wrapper, implementing [`ArcWake`] by calling `Future.set_result`. +/// +/// asyncio future is let uninitialized until [`initialize_future`][1] is called. +/// If [`wake`][2] is called before future initialization (during Rust future polling), +/// [`initialize_future`][1] will return `None` (it is roughly equivalent to `asyncio.sleep(0)`) +/// +/// [1]: AsyncioWaker::initialize_future +/// [2]: AsyncioWaker::wake +pub struct AsyncioWaker(GILOnceCell>); + +impl AsyncioWaker { + pub(super) fn new() -> Self { + Self(GILOnceCell::new()) + } + + pub(super) fn reset(&mut self) { + self.0.take(); + } + + pub(super) fn initialize_future<'a>(&'a self, py: Python<'a>) -> PyResult> { + let init = || LoopAndFuture::new(py).map(Some); + let loop_and_future = self.0.get_or_try_init(py, init)?.as_ref(); + Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.as_ref(py))) + } +} + +impl ArcWake for AsyncioWaker { + fn wake_by_ref(arc_self: &Arc) { + Python::with_gil(|gil| { + if let Some(loop_and_future) = arc_self.0.get_or_init(gil, || None) { + loop_and_future + .set_result(gil) + .expect("unexpected error in coroutine waker"); + } + }); + } +} + +struct LoopAndFuture { + event_loop: PyObject, + future: PyObject, +} + +impl LoopAndFuture { + fn new(py: Python<'_>) -> PyResult { + static GET_RUNNING_LOOP: GILOnceCell = GILOnceCell::new(); + let import = || -> PyResult<_> { + let module = py.import("asyncio")?; + Ok(module.getattr("get_running_loop")?.into()) + }; + let event_loop = GET_RUNNING_LOOP.get_or_try_init(py, import)?.call0(py)?; + let future = event_loop.call_method0(py, "create_future")?; + Ok(Self { event_loop, future }) + } + + fn set_result(&self, py: Python<'_>) -> PyResult<()> { + static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); + let release_waiter = RELEASE_WAITER + .get_or_try_init(py, || wrap_pyfunction!(release_waiter, py).map(Into::into))?; + // `Future.set_result` must be called in event loop thread, + // so it requires `call_soon_threadsafe` + let call_soon_threadsafe = self.event_loop.call_method1( + py, + intern!(py, "call_soon_threadsafe"), + (release_waiter, self.future.as_ref(py)), + ); + if let Err(err) = call_soon_threadsafe { + // `call_soon_threadsafe` will raise if the event loop is closed; + // instead of catching an unspecific `RuntimeError`, check directly if it's closed. + let is_closed = self.event_loop.call_method0(py, "is_closed")?; + if !is_closed.extract(py)? { + return Err(err); + } + } + Ok(()) + } +} + +/// Call `future.set_result` if the future is not done. +/// +/// Future can be cancelled by the event loop before being waken. +/// See +#[pyfunction(crate = "crate")] +fn release_waiter(future: &PyAny) -> PyResult<()> { + let done = future.call_method0(intern!(future.py(), "done"))?; + if !done.extract::()? { + future.call_method1(intern!(future.py(), "set_result"), (future.py().None(),))?; + } + Ok(()) +} diff --git a/src/impl_.rs b/src/impl_.rs index 118d62d9dbc..77f9ff4ea1f 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -6,6 +6,8 @@ //! APIs may may change at any time without documentation in the CHANGELOG and without //! breaking semver guarantees. +#[cfg(feature = "macros")] +pub mod coroutine; pub mod deprecations; pub mod extract_argument; pub mod freelist; diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs new file mode 100644 index 00000000000..843c42f169a --- /dev/null +++ b/src/impl_/coroutine.rs @@ -0,0 +1,19 @@ +use crate::coroutine::Coroutine; +use crate::impl_::wrap::OkWrap; +use crate::{IntoPy, PyErr, PyObject, Python}; +use std::future::Future; + +/// Used to wrap the result of async `#[pyfunction]` and `#[pymethods]`. +pub fn wrap_future(future: F) -> Coroutine +where + F: Future + Send + 'static, + R: OkWrap, + T: IntoPy, + PyErr: From, +{ + let future = async move { + // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) + future.await.wrap(unsafe { Python::assume_gil_acquired() }) + }; + Coroutine::from_future(future) +} diff --git a/src/lib.rs b/src/lib.rs index 3c422dd28e8..e308ebc676b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -397,6 +397,8 @@ pub mod buffer; pub mod callback; pub mod conversion; mod conversions; +#[cfg(feature = "macros")] +pub mod coroutine; #[macro_use] #[doc(hidden)] pub mod derive_utils; @@ -469,6 +471,7 @@ pub mod doc_test { doctests! { "README.md" => readme_md, "guide/src/advanced.md" => guide_advanced_md, + "guide/src/async-await.md" => guide_async_await_md, "guide/src/building_and_distribution.md" => guide_building_and_distribution_md, "guide/src/building_and_distribution/multiple_python_versions.md" => guide_bnd_multiple_python_versions_md, "guide/src/class.md" => guide_class_md, diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs new file mode 100644 index 00000000000..7c195e63733 --- /dev/null +++ b/tests/test_coroutine.rs @@ -0,0 +1,98 @@ +#![cfg(feature = "macros")] +#![cfg(not(target_arch = "wasm32"))] +use std::{task::Poll, thread, time::Duration}; + +use futures::{channel::oneshot, future::poll_fn}; +use pyo3::{prelude::*, py_run}; + +#[path = "../src/tests/common.rs"] +mod common; + +fn handle_windows(test: &str) -> String { + let set_event_loop_policy = r#" + import asyncio, sys + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + "#; + pyo3::unindent::unindent(set_event_loop_policy) + &pyo3::unindent::unindent(test) +} + +#[test] +fn noop_coroutine() { + #[pyfunction] + async fn noop() -> usize { + 42 + } + Python::with_gil(|gil| { + let noop = wrap_pyfunction!(noop, gil).unwrap(); + let test = "import asyncio; assert asyncio.run(noop()) == 42"; + py_run!(gil, noop, &handle_windows(test)); + }) +} + +#[test] +fn sleep_0_like_coroutine() { + #[pyfunction] + async fn sleep_0() -> usize { + let mut waken = false; + poll_fn(|cx| { + if !waken { + cx.waker().wake_by_ref(); + waken = true; + return Poll::Pending; + } + Poll::Ready(42) + }) + .await + } + Python::with_gil(|gil| { + let sleep_0 = wrap_pyfunction!(sleep_0, gil).unwrap(); + let test = "import asyncio; assert asyncio.run(sleep_0()) == 42"; + py_run!(gil, sleep_0, &handle_windows(test)); + }) +} + +#[pyfunction] +async fn sleep(seconds: f64) -> usize { + let (tx, rx) = oneshot::channel(); + thread::spawn(move || { + thread::sleep(Duration::from_secs_f64(seconds)); + tx.send(42).unwrap(); + }); + rx.await.unwrap() +} + +#[test] +fn sleep_coroutine() { + Python::with_gil(|gil| { + let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#; + py_run!(gil, sleep, &handle_windows(test)); + }) +} + +#[test] +fn cancelled_coroutine() { + Python::with_gil(|gil| { + let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let test = r#" + import asyncio + async def main(): + task = asyncio.create_task(sleep(1)) + await asyncio.sleep(0) + task.cancel() + await task + asyncio.run(main()) + "#; + let globals = gil.import("__main__").unwrap().dict(); + globals.set_item("sleep", sleep).unwrap(); + let err = gil + .run( + &pyo3::unindent::unindent(&handle_windows(test)), + Some(globals), + None, + ) + .unwrap_err(); + assert_eq!(err.value(gil).get_type().name().unwrap(), "CancelledError"); + }) +} diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index eec14202fd5..08fef60654d 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -4,6 +4,8 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied 5 | #[pyclass(extends=PyDict)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict` | - = help: the trait `PyClass` is implemented for `TestClass` + = help: the following other types implement trait `PyClass`: + TestClass + Coroutine = note: required for `PyDict` to implement `PyClassBaseType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 2a30a0d16fa..81491cb42ba 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -6,9 +6,6 @@ fn generic_function(value: T) {} #[pyfunction] fn impl_trait_function(impl_trait: impl AsRef) {} -#[pyfunction] -async fn async_function() {} - #[pyfunction] fn wildcard_argument(_: i32) {} diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 9f1409260b6..ec7e54cc120 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -10,29 +10,21 @@ error: Python functions cannot have `impl Trait` arguments 7 | fn impl_trait_function(impl_trait: impl AsRef) {} | ^^^^ -error: `async fn` is not yet supported for Python functions. - - Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632 - --> tests/ui/invalid_pyfunctions.rs:10:1 - | -10 | async fn async_function() {} - | ^^^^^ - error: wildcard argument names are not supported - --> tests/ui/invalid_pyfunctions.rs:13:22 + --> tests/ui/invalid_pyfunctions.rs:10:22 | -13 | fn wildcard_argument(_: i32) {} +10 | fn wildcard_argument(_: i32) {} | ^ error: destructuring in arguments is not supported - --> tests/ui/invalid_pyfunctions.rs:16:26 + --> tests/ui/invalid_pyfunctions.rs:13:26 | -16 | fn destructured_argument((a, b): (i32, i32)) {} +13 | fn destructured_argument((a, b): (i32, i32)) {} | ^^^^^^ error: required arguments after an `Option<_>` argument are ambiguous = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters - --> tests/ui/invalid_pyfunctions.rs:19:63 + --> tests/ui/invalid_pyfunctions.rs:16:63 | -19 | fn function_with_required_after_option(_opt: Option, _x: i32) {} +16 | fn function_with_required_after_option(_opt: Option, _x: i32) {} | ^^^ diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 8622d02bcab..2f8bb841eeb 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -161,11 +161,6 @@ impl MyClass { fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} } -#[pymethods] -impl MyClass { - async fn async_method(&self) {} -} - #[pymethods] impl MyClass { #[pyo3(pass_module)] diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 24fb52428f2..82abe1d1f59 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -153,38 +153,30 @@ error: Python functions cannot have `impl Trait` arguments 161 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} | ^^^^ -error: `async fn` is not yet supported for Python functions. - - Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632 - --> tests/ui/invalid_pymethods.rs:166:5 - | -166 | async fn async_method(&self) {} - | ^^^^^ - error: `pass_module` cannot be used on Python methods - --> tests/ui/invalid_pymethods.rs:171:12 + --> tests/ui/invalid_pymethods.rs:166:12 | -171 | #[pyo3(pass_module)] +166 | #[pyo3(pass_module)] | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. - --> tests/ui/invalid_pymethods.rs:177:29 + --> tests/ui/invalid_pymethods.rs:172:29 | -177 | fn method_self_by_value(self) {} +172 | fn method_self_by_value(self) {} | ^^^^ error: macros cannot be used as items in `#[pymethods]` impl blocks = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:212:5 + --> tests/ui/invalid_pymethods.rs:207:5 | -212 | macro_invocation!(); +207 | macro_invocation!(); | ^^^^^^^^^^^^^^^^ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` - --> tests/ui/invalid_pymethods.rs:182:1 + --> tests/ui/invalid_pymethods.rs:177:1 | -182 | #[pymethods] +177 | #[pymethods] | ^^^^^^^^^^^^ | | | first implementation here @@ -193,9 +185,9 @@ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClas = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod___new____` - --> tests/ui/invalid_pymethods.rs:182:1 + --> tests/ui/invalid_pymethods.rs:177:1 | -182 | #[pymethods] +177 | #[pymethods] | ^^^^^^^^^^^^ | | | duplicate definitions for `__pymethod___new____` @@ -204,9 +196,9 @@ error[E0592]: duplicate definitions with name `__pymethod___new____` = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod_func__` - --> tests/ui/invalid_pymethods.rs:197:1 + --> tests/ui/invalid_pymethods.rs:192:1 | -197 | #[pymethods] +192 | #[pymethods] | ^^^^^^^^^^^^ | | | duplicate definitions for `__pymethod_func__`