From 781b9e3983903a41a8bd385c1f978b7aafd8d56b Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Mon, 20 Nov 2023 10:39:05 +0100 Subject: [PATCH] feat: add coroutine `__name__`/`__qualname__` and not-awaited warning --- newsfragments/3588.added.md | 1 + pyo3-macros-backend/src/method.rs | 24 +++++++++++++------ src/coroutine.rs | 37 ++++++++++++++++++++++++---- src/impl_.rs | 2 ++ src/impl_/coroutine.rs | 16 +++++++++++++ src/impl_/wrap.rs | 14 ----------- tests/test_coroutine.rs | 40 +++++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 newsfragments/3588.added.md create mode 100644 src/impl_/coroutine.rs diff --git a/newsfragments/3588.added.md b/newsfragments/3588.added.md new file mode 100644 index 00000000..acddf296 --- /dev/null +++ b/newsfragments/3588.added.md @@ -0,0 +1 @@ +Add `__name__`/`__qualname__` attributes to `Coroutine` \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index cacd07c3..d8d901bb 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -455,13 +455,23 @@ impl<'a> FnSpec<'a> { let func_name = &self.name; let rust_call = |args: Vec| { - let call = quote! { function(#self_arg #(#args),*) }; - let wrapped_call = if self.asyncness.is_some() { - quote! { _pyo3::PyResult::Ok(_pyo3::impl_::wrap::wrap_future(#call)) } - } else { - quotes::ok_wrap(call) - }; - quotes::map_result_into_ptr(wrapped_call) + let mut call = quote! { function(#self_arg #(#args),*) }; + if self.asyncness.is_some() { + let python_name = &self.python_name; + let qualname_prefix = match cls { + Some(cls) => quote!(Some(<#cls as _pyo3::PyTypeInfo>::NAME)), + None => quote!(None), + }; + call = quote! {{ + let future = #call; + _pyo3::impl_::coroutine::new_coroutine( + _pyo3::intern!(py, stringify!(#python_name)), + #qualname_prefix, + async move { _pyo3::impl_::wrap::OkWrap::wrap(future.await) } + ) + }}; + } + quotes::map_result_into_ptr(quotes::ok_wrap(call)) }; let rust_name = if let Some(cls) = cls { diff --git a/src/coroutine.rs b/src/coroutine.rs index 564262f3..c1a73938 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -14,10 +14,10 @@ use pyo3_macros::{pyclass, pymethods}; use crate::{ coroutine::waker::AsyncioWaker, - exceptions::{PyRuntimeError, PyStopIteration}, + exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration}, panic::PanicException, pyclass::IterNextOutput, - types::PyIterator, + types::{PyIterator, PyString}, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; @@ -30,6 +30,8 @@ type FutureOutput = Result, Box>; /// Python coroutine wrapping a [`Future`]. #[pyclass(crate = "crate")] pub struct Coroutine { + name: Option>, + qualname_prefix: Option<&'static str>, future: Option + Send>>>, waker: Option>, } @@ -41,18 +43,24 @@ impl Coroutine { /// (should always be `None` anyway). /// /// `Coroutine `throw` drop the wrapped future and reraise the exception passed - pub(crate) fn from_future(future: F) -> Self + pub(crate) fn new( + name: Option>, + qualname_prefix: Option<&'static str>, + future: F, + ) -> Self where F: Future> + Send + 'static, T: IntoPy, - PyErr: From, + E: Into, { let wrap = async move { - let obj = future.await?; + let obj = future.await.map_err(Into::into)?; // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) Ok(obj.into_py(unsafe { Python::assume_gil_acquired() })) }; Self { + name, + qualname_prefix, future: Some(Box::pin(panic::AssertUnwindSafe(wrap).catch_unwind())), waker: None, } @@ -113,6 +121,25 @@ pub(crate) fn iter_result(result: IterNextOutput) -> PyResul #[pymethods(crate = "crate")] impl Coroutine { + #[getter] + fn __name__(&self, py: Python<'_>) -> PyResult> { + match &self.name { + Some(name) => Ok(name.clone_ref(py)), + None => Err(PyAttributeError::new_err("__name__")), + } + } + + #[getter] + fn __qualname__(&self, py: Python<'_>) -> PyResult> { + match (&self.name, &self.qualname_prefix) { + (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.as_ref(py).to_str()?) + .as_str() + .into_py(py)), + (Some(name), None) => Ok(name.clone_ref(py)), + (None, _) => Err(PyAttributeError::new_err("__qualname__")), + } + } + fn send(&mut self, py: Python<'_>, _value: &PyAny) -> PyResult { iter_result(self.poll(py, None)?) } diff --git a/src/impl_.rs b/src/impl_.rs index 118d62d9..77f9ff4e 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 00000000..6f66cc48 --- /dev/null +++ b/src/impl_/coroutine.rs @@ -0,0 +1,16 @@ +use std::future::Future; + +use crate::{coroutine::Coroutine, types::PyString, IntoPy, PyErr, PyObject}; + +pub fn new_coroutine( + name: &PyString, + qualname_prefix: Option<&'static str>, + future: F, +) -> Coroutine +where + F: Future> + Send + 'static, + T: IntoPy, + E: Into, +{ + Coroutine::new(Some(name.into()), qualname_prefix, future) +} diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index b41055b2..2110d841 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -67,20 +67,6 @@ pub fn map_result_into_py>( result.map(|err| err.into_py(py)) } -/// Used to wrap the result of async `#[pyfunction]` and `#[pymethods]`. -#[cfg(feature = "macros")] -pub fn wrap_future(future: F) -> crate::coroutine::Coroutine -where - F: std::future::Future + Send + 'static, - R: OkWrap, - T: IntoPy, - crate::PyErr: From, -{ - crate::coroutine::Coroutine::from_future::<_, T, crate::PyErr>(async move { - OkWrap::wrap(future.await).map_err(Into::into) - }) -} - #[cfg(test)] mod tests { use super::*; diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 7c195e63..7420a093 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,8 +1,10 @@ #![cfg(feature = "macros")] #![cfg(not(target_arch = "wasm32"))] +use std::ops::Deref; use std::{task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn}; +use pyo3::types::{IntoPyDict, PyType}; use pyo3::{prelude::*, py_run}; #[path = "../src/tests/common.rs"] @@ -30,6 +32,44 @@ fn noop_coroutine() { }) } +#[test] +fn test_coroutine_qualname() { + #[pyfunction] + async fn my_fn() {} + #[pyclass] + struct MyClass; + #[pymethods] + impl MyClass { + #[new] + fn new() -> Self { + Self + } + // TODO use &self when possible + async fn my_method(_self: Py) {} + #[classmethod] + async fn my_classmethod(_cls: Py) {} + #[staticmethod] + async fn my_staticmethod() {} + } + Python::with_gil(|gil| { + let test = r#" + for coro, name, qualname in [ + (my_fn(), "my_fn", "my_fn"), + (MyClass().my_method(), "my_method", "MyClass.my_method"), + #(MyClass().my_classmethod(), "my_classmethod", "MyClass.my_classmethod"), + (MyClass.my_staticmethod(), "my_staticmethod", "MyClass.my_staticmethod"), + ]: + assert coro.__name__ == name and coro.__qualname__ == qualname + "#; + let locals = [ + ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().deref()), + ("MyClass", gil.get_type::()), + ] + .into_py_dict(gil); + py_run!(gil, *locals, &handle_windows(test)); + }) +} + #[test] fn sleep_0_like_coroutine() { #[pyfunction]