diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 7ea65899850..8dcbd3118b6 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -449,7 +449,23 @@ impl<'a> FnSpec<'a> { let rust_call = |args: Vec| { let mut call = quote! { function(#self_arg #(#args),*) }; if self.asyncness.is_some() { - call = quote! { _pyo3::impl_::coroutine::wrap_future(#call) }; + let python_name = &self.python_name; + let qualname = match cls { + Some(cls) => quote! { + Some(_pyo3::impl_::coroutine::method_coroutine_qualname::<#cls>(py, stringify!(#python_name))) + }, + None => quote! { + _pyo3::impl_::coroutine::coroutine_qualname(py, py.from_borrowed_ptr_or_opt::<_pyo3::types::PyModule>(_slf), stringify!(#python_name)) + }, + }; + call = quote! {{ + let future = #call; + _pyo3::impl_::coroutine::new_coroutine( + Some(_pyo3::impl_::coroutine::coroutine_name(py, stringify!(#python_name))), + #qualname, + async move { _pyo3::impl_::wrap::OkWrap::wrap_no_gil(future.await) } + ) + }}; } quotes::map_result_into_ptr(quotes::ok_wrap(call)) }; diff --git a/src/coroutine.rs b/src/coroutine.rs index a530184a1c3..6705bfd56a5 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, PyRuntimeWarning, 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: Option>, future: Option + Send>>>, waker: Option>, } @@ -41,7 +43,11 @@ 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>, + mut qualname: Option>, + future: F, + ) -> Self where F: Future> + Send + 'static, T: IntoPy, @@ -52,7 +58,10 @@ impl Coroutine { // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) Ok(obj.into_py(unsafe { Python::assume_gil_acquired() })) }; + qualname = qualname.or_else(|| name.clone()); Self { + name, + qualname, future: Some(Box::pin(panic::AssertUnwindSafe(wrap).catch_unwind())), waker: None, } @@ -113,6 +122,20 @@ pub(crate) fn iter_result(result: IterNextOutput) -> PyResul #[pymethods(crate = "crate")] impl Coroutine { + #[getter] + fn __name__(&self) -> PyResult> { + self.name + .clone() + .ok_or_else(|| PyAttributeError::new_err("__name__")) + } + + #[getter] + fn __qualname__(&self) -> PyResult> { + self.qualname + .clone() + .ok_or_else(|| PyAttributeError::new_err("__qualname__")) + } + fn send(&mut self, py: Python<'_>, _value: &PyAny) -> PyResult { iter_result(self.poll(py, None)?) } @@ -135,3 +158,21 @@ impl Coroutine { self.poll(py, None) } } + +impl Drop for Coroutine { + fn drop(&mut self) { + if self.future.is_some() { + Python::with_gil(|gil| { + let qualname = self + .qualname + .as_ref() + .map_or(Ok(""), |n| n.as_ref(gil).to_str()) + .unwrap(); + let message = format!("coroutine {qualname} was never awaited"); + PyErr::warn(gil, gil.get_type::(), &message, 2) + .expect("warning error"); + self.poll(gil, None).expect("coroutine close error"); + }) + } + } +} diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 843c42f169a..fe23e7c41bd 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -1,19 +1,41 @@ -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 +use crate::{ + coroutine::Coroutine, + types::{PyModule, PyString}, + IntoPy, Py, PyClass, PyErr, PyObject, Python, +}; + +pub fn new_coroutine( + name: Option>, + qualname: Option>, + future: F, +) -> Coroutine where - F: Future + Send + 'static, - R: OkWrap, + F: Future> + Send + 'static, T: IntoPy, - PyErr: From, + 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::new(name, qualname, future) +} + +pub fn coroutine_name(py: Python<'_>, name: &str) -> Py { + PyString::new(py, name).into() +} + +pub unsafe fn coroutine_qualname( + py: Python<'_>, + module: Option<&PyModule>, + name: &str, +) -> Option> { + Some(PyString::new(py, &format!("{}.{name}", module?.name().ok()?)).into()) +} + +pub fn method_coroutine_qualname(py: Python<'_>, name: &str) -> Py { + let class = T::NAME; + let qualname = match T::MODULE { + Some(module) => format!("{module}.{class}.{name}"), + None => format!("{class}.{name}"), }; - Coroutine::from_future(future) + PyString::new(py, &qualname).into() } diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index a73e3597302..08fab47a7ba 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -20,6 +20,7 @@ impl SomeWrap> for Option { /// Used to wrap the result of `#[pyfunction]` and `#[pymethods]`. pub trait OkWrap { type Error; + fn wrap_no_gil(self) -> Result; fn wrap(self, py: Python<'_>) -> Result, Self::Error>; } @@ -30,6 +31,9 @@ where T: IntoPy, { type Error = PyErr; + fn wrap_no_gil(self) -> Result { + Ok(self) + } fn wrap(self, py: Python<'_>) -> PyResult> { Ok(self.into_py(py)) } @@ -40,6 +44,9 @@ where T: IntoPy, { type Error = E; + fn wrap_no_gil(self) -> Result { + self + } fn wrap(self, py: Python<'_>) -> Result, Self::Error> { self.map(|o| o.into_py(py)) } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index a24d6bbd142..5ae1f58145a 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,7 +1,9 @@ #![cfg(feature = "macros")] +use std::ops::Deref; use std::{task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn}; +use pyo3::types::IntoPyDict; use pyo3::{prelude::*, py_run}; #[path = "../src/tests/common.rs"] @@ -29,6 +31,71 @@ 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) {} + // TODO uncomment when https://github.com/PyO3/pyo3/pull/3587 is merged + // #[classmethod] + // async fn my_classmethod(_cls: Py) {} + #[staticmethod] + async fn my_staticmethod() {} + } + #[pyclass(module = "my_module")] + struct MyClassWithModule; + #[pymethods] + impl MyClassWithModule { + #[new] + fn new() -> Self { + Self + } + // TODO use &self when possible + async fn my_method(_self: Py) {} + // TODO uncomment when https://github.com/PyO3/pyo3/pull/3587 is merged + // #[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"), + (my_fn_with_module(), "my_fn", "my_module.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"), + (MyClassWithModule().my_method(), "my_method", "my_module.MyClassWithModule.my_method"), + #(MyClassWithModule().my_classmethod(), "my_classmethod", "my_module.MyClassWithModule.my_classmethod"), + (MyClassWithModule.my_staticmethod(), "my_staticmethod", "my_module.MyClassWithModule.my_staticmethod"), + ]: + assert coro.__name__ == name and coro.__qualname__ == qualname + "#; + let my_module = PyModule::new(gil, "my_module").unwrap(); + let locals = [ + ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().deref()), + ( + "my_fn_with_module", + wrap_pyfunction!(my_fn, my_module).unwrap(), + ), + ("MyClass", gil.get_type::()), + ("MyClassWithModule", gil.get_type::()), + ] + .into_py_dict(gil); + py_run!(gil, *locals, &handle_windows(test)); + }) +} + #[test] fn sleep_0_like_coroutine() { #[pyfunction]