feat: add coroutine __name__
/__qualname__
and not-awaited warning
This commit is contained in:
parent
9f66846238
commit
781b9e3983
1
newsfragments/3588.added.md
Normal file
1
newsfragments/3588.added.md
Normal file
|
@ -0,0 +1 @@
|
|||
Add `__name__`/`__qualname__` attributes to `Coroutine`
|
|
@ -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 {
|
||||
|
|
|
@ -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)?)
|
||||
}
|
||||
|
|
|
@ -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
16
src/impl_/coroutine.rs
Normal 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)
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue