Skip to content

Commit

Permalink
feat: add coroutine __name__/__qualname__ and not-awaited warning
Browse files Browse the repository at this point in the history
  • Loading branch information
wyfo committed Nov 25, 2023
1 parent 9f66846 commit 871efcb
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 26 deletions.
1 change: 1 addition & 0 deletions newsfragments/3588.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `__name__`/`__qualname__` attributes to `Coroutine`, as well as a Python warning when the coroutine is dropped without having been awaited
24 changes: 17 additions & 7 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,13 +455,23 @@ impl<'a> FnSpec<'a> {
let func_name = &self.name;

let rust_call = |args: Vec<TokenStream>| {
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 {
Expand Down
37 changes: 32 additions & 5 deletions src/coroutine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -30,6 +30,8 @@ type FutureOutput = Result<PyResult<PyObject>, Box<dyn Any + Send>>;
/// Python coroutine wrapping a [`Future`].
#[pyclass(crate = "crate")]
pub struct Coroutine {
name: Option<Py<PyString>>,
qualname_prefix: Option<&'static str>,
future: Option<Pin<Box<dyn Future<Output = FutureOutput> + Send>>>,
waker: Option<Arc<AsyncioWaker>>,
}
Expand All @@ -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<F, T, E>(future: F) -> Self
pub(crate) fn new<F, T, E>(
name: Option<Py<PyString>>,
qualname_prefix: Option<&'static str>,
future: F,
) -> Self
where
F: Future<Output = Result<T, E>> + Send + 'static,
T: IntoPy<PyObject>,
PyErr: From<E>,
E: Into<PyErr>,
{
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,
}
Expand Down Expand Up @@ -113,6 +121,25 @@ pub(crate) fn iter_result(result: IterNextOutput<PyObject, PyObject>) -> PyResul

#[pymethods(crate = "crate")]
impl Coroutine {
#[getter]
fn __name__(&self, py: Python<'_>) -> PyResult<Py<PyString>> {
match &self.name {
Some(name) => Ok(name.clone_ref(py)),
None => Err(PyAttributeError::new_err("__name__")),
}
}

#[getter]
fn __qualname__(&self, py: Python<'_>) -> PyResult<Py<PyString>> {
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<PyObject> {
iter_result(self.poll(py, None)?)
}
Expand Down
2 changes: 2 additions & 0 deletions src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/impl_/coroutine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::future::Future;

use crate::{coroutine::Coroutine, types::PyString, IntoPy, PyErr, PyObject};

pub fn new_coroutine<F, T, E>(
name: &PyString,
qualname_prefix: Option<&'static str>,
future: F,
) -> Coroutine
where
F: Future<Output = Result<T, E>> + Send + 'static,
T: IntoPy<PyObject>,
E: Into<PyErr>,
{
Coroutine::new(Some(name.into()), qualname_prefix, future)
}
14 changes: 0 additions & 14 deletions src/impl_/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,6 @@ pub fn map_result_into_py<T: IntoPy<PyObject>>(
result.map(|err| err.into_py(py))
}

/// Used to wrap the result of async `#[pyfunction]` and `#[pymethods]`.
#[cfg(feature = "macros")]
pub fn wrap_future<F, R, T>(future: F) -> crate::coroutine::Coroutine
where
F: std::future::Future<Output = R> + Send + 'static,
R: OkWrap<T>,
T: IntoPy<PyObject>,
crate::PyErr: From<R::Error>,
{
crate::coroutine::Coroutine::from_future::<_, T, crate::PyErr>(async move {
OkWrap::wrap(future.await).map_err(Into::into)
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
40 changes: 40 additions & 0 deletions tests/test_coroutine.rs
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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<Self>) {}
#[classmethod]
async fn my_classmethod(_cls: Py<PyType>) {}
#[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::<MyClass>()),
]
.into_py_dict(gil);
py_run!(gil, *locals, &handle_windows(test));
})
}

#[test]
fn sleep_0_like_coroutine() {
#[pyfunction]
Expand Down

0 comments on commit 871efcb

Please sign in to comment.