feat: add coroutine __name__/__qualname__ and not-awaited warning

This commit is contained in:
Joseph Perez 2023-11-20 10:39:05 +01:00
parent 9f66846238
commit 781b9e3983
No known key found for this signature in database
GPG key ID: FE77882EF19365C5
7 changed files with 108 additions and 26 deletions

View file

@ -0,0 +1 @@
Add `__name__`/`__qualname__` attributes to `Coroutine`

View file

@ -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 {

View file

@ -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<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>>,
}
@ -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,
}
@ -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)?)
}

View file

@ -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;

16
src/impl_/coroutine.rs Normal file
View file

@ -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)
}

View file

@ -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::*;

View file

@ -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<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]