Skip to content

Commit 2ca9f59

Browse files
committed
refactor: drop futures_util dependency
1 parent 8a674c2 commit 2ca9f59

File tree

4 files changed

+65
-32
lines changed

4 files changed

+65
-32
lines changed

Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ unindent = { version = "0.2.1", optional = true }
3131
# support crate for multiple-pymethods feature
3232
inventory = { version = "0.3.0", optional = true }
3333

34-
# coroutine implementation
35-
futures-util = "0.3"
36-
3734
# crate integrations that can be added using the eponymous features
3835
anyhow = { version = "1.0", optional = true }
3936
chrono = { version = "0.4.25", default-features = false, optional = true }

src/coroutine.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
//! Python coroutine implementation, used notably when wrapping `async fn`
22
//! with `#[pyfunction]`/`#[pymethods]`.
33
use std::{
4-
any::Any,
54
future::Future,
65
panic,
76
pin::Pin,
87
sync::Arc,
9-
task::{Context, Poll},
8+
task::{Context, Poll, Waker},
109
};
1110

12-
use futures_util::FutureExt;
1311
use pyo3_macros::{pyclass, pymethods};
1412

1513
use crate::{
16-
coroutine::waker::AsyncioWaker,
14+
coroutine::{cancel::ThrowCallback, waker::AsyncioWaker},
1715
exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration},
1816
panic::PanicException,
1917
pyclass::IterNextOutput,
@@ -24,20 +22,17 @@ use crate::{
2422
pub(crate) mod cancel;
2523
mod waker;
2624

27-
use crate::coroutine::cancel::ThrowCallback;
2825
pub use cancel::CancelHandle;
2926

3027
const COROUTINE_REUSED_ERROR: &str = "cannot reuse already awaited coroutine";
3128

32-
type FutureOutput = Result<PyResult<PyObject>, Box<dyn Any + Send>>;
33-
3429
/// Python coroutine wrapping a [`Future`].
3530
#[pyclass(crate = "crate")]
3631
pub struct Coroutine {
3732
name: Option<Py<PyString>>,
3833
qualname_prefix: Option<&'static str>,
3934
throw_callback: Option<ThrowCallback>,
40-
future: Option<Pin<Box<dyn Future<Output = FutureOutput> + Send>>>,
35+
future: Option<Pin<Box<dyn Future<Output = PyResult<PyObject>> + Send>>>,
4136
waker: Option<Arc<AsyncioWaker>>,
4237
}
4338

@@ -68,7 +63,7 @@ impl Coroutine {
6863
name,
6964
qualname_prefix,
7065
throw_callback,
71-
future: Some(Box::pin(panic::AssertUnwindSafe(wrap).catch_unwind())),
66+
future: Some(Box::pin(wrap)),
7267
waker: None,
7368
}
7469
}
@@ -98,22 +93,28 @@ impl Coroutine {
9893
} else {
9994
self.waker = Some(Arc::new(AsyncioWaker::new()));
10095
}
101-
let waker = futures_util::task::waker(self.waker.clone().unwrap());
96+
let waker = Waker::from(self.waker.clone().unwrap());
10297
// poll the Rust future and forward its results if ready
103-
if let Poll::Ready(res) = future_rs.as_mut().poll(&mut Context::from_waker(&waker)) {
104-
self.close();
105-
return match res {
106-
Ok(res) => Ok(IterNextOutput::Return(res?)),
107-
Err(err) => Err(PanicException::from_panic_payload(err)),
108-
};
98+
// polling is UnwindSafe because the future is dropped in case of panic
99+
let poll = || future_rs.as_mut().poll(&mut Context::from_waker(&waker));
100+
match panic::catch_unwind(panic::AssertUnwindSafe(poll)) {
101+
Ok(Poll::Ready(res)) => {
102+
self.close();
103+
return Ok(IterNextOutput::Return(res?));
104+
}
105+
Err(err) => {
106+
self.close();
107+
return Err(PanicException::from_panic_payload(err));
108+
}
109+
_ => {}
109110
}
110111
// otherwise, initialize the waker `asyncio.Future`
111112
if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? {
112113
// `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__`
113114
// and will yield itself if its result has not been set in polling above
114115
if let Some(future) = PyIterator::from_object(future).unwrap().next() {
115116
// future has not been leaked into Python for now, and Rust code can only call
116-
// `set_result(None)` in `ArcWake` implementation, so it's safe to unwrap
117+
// `set_result(None)` in `Wake` implementation, so it's safe to unwrap
117118
return Ok(IterNextOutput::Yield(future.unwrap().into()));
118119
}
119120
}

src/coroutine/waker.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::sync::GILOnceCell;
22
use crate::types::PyCFunction;
33
use crate::{intern, wrap_pyfunction, Py, PyAny, PyObject, PyResult, Python};
4-
use futures_util::task::ArcWake;
54
use pyo3_macros::pyfunction;
65
use std::sync::Arc;
6+
use std::task::Wake;
77

8-
/// Lazy `asyncio.Future` wrapper, implementing [`ArcWake`] by calling `Future.set_result`.
8+
/// Lazy `asyncio.Future` wrapper, implementing [`Wake`] by calling `Future.set_result`.
99
///
1010
/// asyncio future is let uninitialized until [`initialize_future`][1] is called.
1111
/// If [`wake`][2] is called before future initialization (during Rust future polling),
@@ -31,10 +31,14 @@ impl AsyncioWaker {
3131
}
3232
}
3333

34-
impl ArcWake for AsyncioWaker {
35-
fn wake_by_ref(arc_self: &Arc<Self>) {
34+
impl Wake for AsyncioWaker {
35+
fn wake(self: Arc<Self>) {
36+
self.wake_by_ref()
37+
}
38+
39+
fn wake_by_ref(self: &Arc<Self>) {
3640
Python::with_gil(|gil| {
37-
if let Some(loop_and_future) = arc_self.0.get_or_init(gil, || None) {
41+
if let Some(loop_and_future) = self.0.get_or_init(gil, || None) {
3842
loop_and_future
3943
.set_result(gil)
4044
.expect("unexpected error in coroutine waker");

tests/test_coroutine.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#![cfg(feature = "macros")]
22
#![cfg(not(target_arch = "wasm32"))]
3-
use std::ops::Deref;
4-
use std::{task::Poll, thread, time::Duration};
3+
use std::{ops::Deref, task::Poll, thread, time::Duration};
54

65
use futures::{channel::oneshot, future::poll_fn, FutureExt};
7-
use pyo3::coroutine::CancelHandle;
8-
use pyo3::types::{IntoPyDict, PyType};
9-
use pyo3::{prelude::*, py_run};
6+
use pyo3::{
7+
coroutine::CancelHandle,
8+
prelude::*,
9+
py_run,
10+
types::{IntoPyDict, PyType},
11+
};
1012

1113
#[path = "../src/tests/common.rs"]
1214
mod common;
@@ -119,7 +121,7 @@ fn cancelled_coroutine() {
119121
let test = r#"
120122
import asyncio
121123
async def main():
122-
task = asyncio.create_task(sleep(1))
124+
task = asyncio.create_task(sleep(999))
123125
await asyncio.sleep(0)
124126
task.cancel()
125127
await task
@@ -155,7 +157,7 @@ fn coroutine_cancel_handle() {
155157
let test = r#"
156158
import asyncio;
157159
async def main():
158-
task = asyncio.create_task(cancellable_sleep(1))
160+
task = asyncio.create_task(cancellable_sleep(999))
159161
await asyncio.sleep(0)
160162
task.cancel()
161163
return await task
@@ -203,3 +205,32 @@ fn coroutine_is_cancelled() {
203205
.unwrap();
204206
})
205207
}
208+
209+
#[test]
210+
fn coroutine_panic() {
211+
#[pyfunction]
212+
async fn panic() {
213+
panic!("test panic");
214+
}
215+
Python::with_gil(|gil| {
216+
let panic = wrap_pyfunction!(panic, gil).unwrap();
217+
let test = r#"
218+
import asyncio
219+
coro = panic()
220+
try:
221+
asyncio.run(coro)
222+
except BaseException as err:
223+
assert type(err).__name__ == "PanicException"
224+
assert str(err) == "test panic"
225+
else:
226+
assert False
227+
try:
228+
coro.send(None)
229+
except RuntimeError as err:
230+
assert str(err) == "cannot reuse already awaited coroutine"
231+
else:
232+
assert False;
233+
"#;
234+
py_run!(gil, panic, &handle_windows(test));
235+
})
236+
}

0 commit comments

Comments
 (0)