From 8a674c2bd3ed5d3df42d912310cc4de969b9dccc Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Sat, 25 Nov 2023 07:30:41 +0100 Subject: [PATCH] feat: add `coroutine::CancelHandle` --- guide/src/async-await.md | 21 ++++- newsfragments/3599.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/method.rs | 53 +++++++++-- pyo3-macros-backend/src/params.rs | 4 + pyo3-macros-backend/src/pyfunction.rs | 22 ++++- .../src/pyfunction/signature.rs | 14 ++- src/coroutine.rs | 17 +++- src/coroutine/cancel.rs | 78 ++++++++++++++++ src/impl_/coroutine.rs | 4 +- src/instance.rs | 2 +- tests/test_coroutine.rs | 69 ++++++++++++++- tests/ui/invalid_argument_attributes.rs | 28 ++++++ tests/ui/invalid_argument_attributes.stderr | 88 ++++++++++++++++++- 14 files changed, 381 insertions(+), 21 deletions(-) create mode 100644 newsfragments/3599.added.md create mode 100644 src/coroutine/cancel.rs diff --git a/guide/src/async-await.md b/guide/src/async-await.md index f54a8aaa..3649a9a0 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -69,10 +69,27 @@ where ## Cancellation -*To be implemented* +Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]. + +```rust +# #![allow(dead_code)] +use futures::FutureExt; +use pyo3::prelude::*; +use pyo3::coroutine::CancelHandle; + +#[pyfunction] +async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) { + futures::select! { + /* _ = ... => println!("done"), */ + _ = 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). 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)*. +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; *The type does not yet have a public constructor until the design is finalized.* \ No newline at end of file diff --git a/newsfragments/3599.added.md b/newsfragments/3599.added.md new file mode 100644 index 00000000..36078fbc --- /dev/null +++ b/newsfragments/3599.added.md @@ -0,0 +1 @@ +Add `coroutine::CancelHandle` to catch coroutine cancellation \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 31708e55..e91b3b8d 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -11,6 +11,7 @@ use syn::{ pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); + syn::custom_keyword!(cancel_handle); syn::custom_keyword!(dict); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 9298dab4..1cbb9304 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -24,6 +24,7 @@ pub struct FnArg<'a> { pub attrs: PyFunctionArgPyO3Attributes, pub is_varargs: bool, pub is_kwargs: bool, + pub is_cancel_handle: bool, } impl<'a> FnArg<'a> { @@ -44,6 +45,8 @@ impl<'a> FnArg<'a> { other => return Err(handle_argument_error(other)), }; + let is_cancel_handle = arg_attrs.cancel_handle.is_some(); + Ok(FnArg { name: ident, ty: &cap.ty, @@ -53,6 +56,7 @@ impl<'a> FnArg<'a> { attrs: arg_attrs, is_varargs: false, is_kwargs: false, + is_cancel_handle, }) } } @@ -455,9 +459,27 @@ impl<'a> FnSpec<'a> { let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise); let func_name = &self.name; + let mut cancel_handle_iter = self + .signature + .arguments + .iter() + .filter(|arg| arg.is_cancel_handle); + let cancel_handle = cancel_handle_iter.next(); + if let Some(arg) = cancel_handle { + ensure_spanned!(self.asyncness.is_some(), arg.name.span() => "`cancel_handle` attribute can only be used with `async fn`"); + if let Some(arg2) = cancel_handle_iter.next() { + bail_spanned!(arg2.name.span() => "`cancel_handle` may only be specified once"); + } + } + let rust_call = |args: Vec| { let mut call = quote! { function(#self_arg #(#args),*) }; if self.asyncness.is_some() { + let throw_callback = if cancel_handle.is_some() { + quote! { Some(__throw_callback) } + } else { + quote! { None } + }; let python_name = &self.python_name; let qualname_prefix = match cls { Some(cls) => quote!(Some(<#cls as _pyo3::PyTypeInfo>::NAME)), @@ -468,9 +490,17 @@ impl<'a> FnSpec<'a> { _pyo3::impl_::coroutine::new_coroutine( _pyo3::intern!(py, stringify!(#python_name)), #qualname_prefix, - async move { _pyo3::impl_::wrap::OkWrap::wrap(future.await) } + #throw_callback, + async move { _pyo3::impl_::wrap::OkWrap::wrap(future.await) }, ) }}; + if cancel_handle.is_some() { + call = quote! {{ + let __cancel_handle = _pyo3::coroutine::CancelHandle::new(); + let __throw_callback = __cancel_handle.throw_callback(); + #call + }}; + } } quotes::map_result_into_ptr(quotes::ok_wrap(call)) }; @@ -483,12 +513,21 @@ impl<'a> FnSpec<'a> { Ok(match self.convention { CallingConvention::Noargs => { - let call = if !self.signature.arguments.is_empty() { - // Only `py` arg can be here - rust_call(vec![quote!(py)]) - } else { - rust_call(vec![]) - }; + let args = self + .signature + .arguments + .iter() + .map(|arg| { + if arg.py { + quote!(py) + } else if arg.is_cancel_handle { + quote!(__cancel_handle) + } else { + unreachable!() + } + }) + .collect(); + let call = rust_call(args); quote! { unsafe fn #ident<'py>( diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index e511ca75..5e7a86ff 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -155,6 +155,10 @@ fn impl_arg_param( return Ok(quote! { py }); } + if arg.is_cancel_handle { + return Ok(quote! { __cancel_handle }); + } + let name = arg.name; let name_str = name.to_string(); diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 6f606516..b265a34d 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -23,16 +23,20 @@ pub use self::signature::{FunctionSignature, SignatureAttribute}; #[derive(Clone, Debug)] pub struct PyFunctionArgPyO3Attributes { pub from_py_with: Option, + pub cancel_handle: Option, } enum PyFunctionArgPyO3Attribute { FromPyWith(FromPyWithAttribute), + CancelHandle(attributes::kw::cancel_handle), } impl Parse for PyFunctionArgPyO3Attribute { fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::from_py_with) { + if lookahead.peek(attributes::kw::cancel_handle) { + input.parse().map(PyFunctionArgPyO3Attribute::CancelHandle) + } else if lookahead.peek(attributes::kw::from_py_with) { input.parse().map(PyFunctionArgPyO3Attribute::FromPyWith) } else { Err(lookahead.error()) @@ -43,7 +47,10 @@ impl Parse for PyFunctionArgPyO3Attribute { impl PyFunctionArgPyO3Attributes { /// Parses #[pyo3(from_python_with = "func")] pub fn from_attrs(attrs: &mut Vec) -> syn::Result { - let mut attributes = PyFunctionArgPyO3Attributes { from_py_with: None }; + let mut attributes = PyFunctionArgPyO3Attributes { + from_py_with: None, + cancel_handle: None, + }; take_attributes(attrs, |attr| { if let Some(pyo3_attrs) = get_pyo3_options(attr)? { for attr in pyo3_attrs { @@ -55,7 +62,18 @@ impl PyFunctionArgPyO3Attributes { ); attributes.from_py_with = Some(from_py_with); } + PyFunctionArgPyO3Attribute::CancelHandle(cancel_handle) => { + ensure_spanned!( + attributes.cancel_handle.is_none(), + cancel_handle.span() => "`cancel_handle` may only be specified once per argument" + ); + attributes.cancel_handle = Some(cancel_handle); + } } + ensure_spanned!( + attributes.from_py_with.is_none() || attributes.cancel_handle.is_none(), + attributes.cancel_handle.unwrap().span() => "`from_py_with` and `cancel_handle` cannot be specified together" + ); } Ok(true) } else { diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index ed3256ad..baf01285 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -361,6 +361,16 @@ impl<'a> FunctionSignature<'a> { // Otherwise try next argument. continue; } + if fn_arg.is_cancel_handle { + // If the user incorrectly tried to include cancel: CoroutineCancel in the + // signature, give a useful error as a hint. + ensure_spanned!( + name != fn_arg.name, + name.span() => "`cancel_handle` argument must not be part of the signature" + ); + // Otherwise try next argument. + continue; + } ensure_spanned!( name == fn_arg.name, @@ -411,7 +421,7 @@ impl<'a> FunctionSignature<'a> { } // Ensure no non-py arguments remain - if let Some(arg) = args_iter.find(|arg| !arg.py) { + if let Some(arg) = args_iter.find(|arg| !arg.py && !arg.is_cancel_handle) { bail_spanned!( attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name) ); @@ -429,7 +439,7 @@ impl<'a> FunctionSignature<'a> { let mut python_signature = PythonSignature::default(); for arg in &arguments { // Python<'_> arguments don't show in Python signature - if arg.py { + if arg.py || arg.is_cancel_handle { continue; } diff --git a/src/coroutine.rs b/src/coroutine.rs index c1a73938..c4c7bbf2 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -21,8 +21,12 @@ use crate::{ IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; +pub(crate) mod cancel; mod waker; +use crate::coroutine::cancel::ThrowCallback; +pub use cancel::CancelHandle; + const COROUTINE_REUSED_ERROR: &str = "cannot reuse already awaited coroutine"; type FutureOutput = Result, Box>; @@ -32,6 +36,7 @@ type FutureOutput = Result, Box>; pub struct Coroutine { name: Option>, qualname_prefix: Option<&'static str>, + throw_callback: Option, future: Option + Send>>>, waker: Option>, } @@ -46,6 +51,7 @@ impl Coroutine { pub(crate) fn new( name: Option>, qualname_prefix: Option<&'static str>, + throw_callback: Option, future: F, ) -> Self where @@ -61,6 +67,7 @@ impl Coroutine { Self { name, qualname_prefix, + throw_callback, future: Some(Box::pin(panic::AssertUnwindSafe(wrap).catch_unwind())), waker: None, } @@ -77,9 +84,13 @@ impl Coroutine { 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))); + match (throw, &self.throw_callback) { + (Some(exc), Some(cb)) => cb.throw(exc.as_ref(py)), + (Some(exc), None) => { + self.close(); + return Err(PyErr::from_value(exc.as_ref(py))); + } + (None, _) => {} } // create a new waker, or try to reset it in place if let Some(waker) = self.waker.as_mut().and_then(Arc::get_mut) { diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs new file mode 100644 index 00000000..7828986c --- /dev/null +++ b/src/coroutine/cancel.rs @@ -0,0 +1,78 @@ +use crate::{PyAny, PyObject}; +use parking_lot::Mutex; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +#[derive(Debug, Default)] +struct Inner { + exception: Option, + waker: Option, +} + +/// Helper used to wait and retrieve exception thrown in [`Coroutine`](super::Coroutine). +/// +/// Only the last exception thrown can be retrieved. +#[derive(Debug, Default)] +pub struct CancelHandle(Arc>); + +impl CancelHandle { + /// Create a new `CoroutineCancel`. + pub fn new() -> Self { + Default::default() + } + + /// Returns whether the associated coroutine has been cancelled. + pub fn is_cancelled(&self) -> bool { + self.0.lock().exception.is_some() + } + + /// Poll to retrieve the exception thrown in the associated coroutine. + pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll { + let mut inner = self.0.lock(); + if let Some(exc) = inner.exception.take() { + return Poll::Ready(exc); + } + if let Some(ref waker) = inner.waker { + if cx.waker().will_wake(waker) { + return Poll::Pending; + } + } + inner.waker = Some(cx.waker().clone()); + Poll::Pending + } + + /// Retrieve the exception thrown in the associated coroutine. + pub async fn cancelled(&mut self) -> PyObject { + Cancelled(self).await + } + + #[doc(hidden)] + pub fn throw_callback(&self) -> ThrowCallback { + ThrowCallback(self.0.clone()) + } +} + +// Because `poll_fn` is not available in MSRV +struct Cancelled<'a>(&'a mut CancelHandle); + +impl Future for Cancelled<'_> { + type Output = PyObject; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.0.poll_cancelled(cx) + } +} + +#[doc(hidden)] +pub struct ThrowCallback(Arc>); + +impl ThrowCallback { + pub(super) fn throw(&self, exc: &PyAny) { + let mut inner = self.0.lock(); + inner.exception = Some(exc.into()); + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } +} diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 6f66cc48..c8b2cdcc 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -1,10 +1,12 @@ use std::future::Future; +use crate::coroutine::cancel::ThrowCallback; use crate::{coroutine::Coroutine, types::PyString, IntoPy, PyErr, PyObject}; pub fn new_coroutine( name: &PyString, qualname_prefix: Option<&'static str>, + throw_callback: Option, future: F, ) -> Coroutine where @@ -12,5 +14,5 @@ where T: IntoPy, E: Into, { - Coroutine::new(Some(name.into()), qualname_prefix, future) + Coroutine::new(Some(name.into()), qualname_prefix, throw_callback, future) } diff --git a/src/instance.rs b/src/instance.rs index 07a6f872..4c43ef33 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1038,7 +1038,7 @@ impl Py { /// # Safety /// `ptr` must point to a Python object of type T. #[inline] - unsafe fn from_non_null(ptr: NonNull) -> Self { + pub(crate) unsafe fn from_non_null(ptr: NonNull) -> Self { Self(ptr, PhantomData) } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 7420a093..5d4f04d6 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -3,7 +3,8 @@ use std::ops::Deref; use std::{task::Poll, thread, time::Duration}; -use futures::{channel::oneshot, future::poll_fn}; +use futures::{channel::oneshot, future::poll_fn, FutureExt}; +use pyo3::coroutine::CancelHandle; use pyo3::types::{IntoPyDict, PyType}; use pyo3::{prelude::*, py_run}; @@ -136,3 +137,69 @@ fn cancelled_coroutine() { assert_eq!(err.value(gil).get_type().name().unwrap(), "CancelledError"); }) } + +#[test] +fn coroutine_cancel_handle() { + #[pyfunction] + async fn cancellable_sleep( + seconds: f64, + #[pyo3(cancel_handle)] mut cancel: CancelHandle, + ) -> usize { + futures::select! { + _ = sleep(seconds).fuse() => 42, + _ = cancel.cancelled().fuse() => 0, + } + } + Python::with_gil(|gil| { + let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, gil).unwrap(); + let test = r#" + import asyncio; + async def main(): + task = asyncio.create_task(cancellable_sleep(1)) + await asyncio.sleep(0) + task.cancel() + return await task + assert asyncio.run(main()) == 0 + "#; + let globals = gil.import("__main__").unwrap().dict(); + globals + .set_item("cancellable_sleep", cancellable_sleep) + .unwrap(); + gil.run( + &pyo3::unindent::unindent(&handle_windows(test)), + Some(globals), + None, + ) + .unwrap(); + }) +} + +#[test] +fn coroutine_is_cancelled() { + #[pyfunction] + async fn sleep_loop(#[pyo3(cancel_handle)] cancel: CancelHandle) { + while !cancel.is_cancelled() { + sleep(0.001).await; + } + } + Python::with_gil(|gil| { + let sleep_loop = wrap_pyfunction!(sleep_loop, gil).unwrap(); + let test = r#" + import asyncio; + async def main(): + task = asyncio.create_task(sleep_loop()) + await asyncio.sleep(0) + task.cancel() + await task + asyncio.run(main()) + "#; + let globals = gil.import("__main__").unwrap().dict(); + globals.set_item("sleep_loop", sleep_loop).unwrap(); + gil.run( + &pyo3::unindent::unindent(&handle_windows(test)), + Some(globals), + None, + ) + .unwrap(); + }) +} diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index 798d428a..ed9d6ce6 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -12,4 +12,32 @@ fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} #[pyfunction] 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 ed6479d5..4903fbc5 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -1,4 +1,4 @@ -error: expected `from_py_with` +error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:4:29 | 4 | fn invalid_attribute(#[pyo3(get)] param: String) {} @@ -10,7 +10,7 @@ error: expected `=` 7 | fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} | ^ -error: expected `from_py_with` +error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:10:31 | 10 | fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} @@ -21,3 +21,87 @@ error: expected string literal | 13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} | ^^^^ + +error: `from_py_with` may only be specified once per argument + --> tests/ui/invalid_argument_attributes.rs:16:56 + | +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 `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 Coroutine + &'a mut 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`