Skip to content

Commit a45c3c8

Browse files
author
Andrew J Westlake
committed
Added support for generic runtimes
1 parent 2f6b054 commit a45c3c8

File tree

5 files changed

+559
-356
lines changed

5 files changed

+559
-356
lines changed

pytests/test_tokio_asyncio.rs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,11 @@ use pyo3_asyncio::{
1515
tokio::testing::{new_sync_test, test_main},
1616
};
1717

18-
lazy_static! {
19-
static ref CURRENT_THREAD_RUNTIME: Runtime = {
20-
Builder::new_current_thread()
21-
.enable_all()
22-
.build()
23-
.expect("Couldn't build the runtime")
24-
};
25-
}
26-
2718
#[pyfunction]
2819
fn sleep_for(py: Python, secs: &PyAny) -> PyResult<PyObject> {
2920
let secs = secs.extract()?;
3021

31-
pyo3_asyncio::tokio::into_coroutine(py, &CURRENT_THREAD_RUNTIME, async move {
22+
pyo3_asyncio::tokio::into_coroutine(py, async move {
3223
tokio::time::sleep(Duration::from_secs(secs)).await;
3324
Python::with_gil(|py| Ok(py.None()))
3425
})
@@ -83,13 +74,8 @@ fn test_async_sleep<'p>(
8374
}
8475

8576
fn main() {
86-
thread::spawn(|| {
87-
CURRENT_THREAD_RUNTIME.block_on(pending::<()>());
88-
});
89-
9077
test_main(
9178
"PyO3 Asyncio Test Suite",
92-
&CURRENT_THREAD_RUNTIME,
9379
vec![
9480
Test::new_async(
9581
"test_async_sleep".into(),

src/async_std.rs

Lines changed: 115 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,35 @@
1-
//! # PyO3 Asyncio Testing Utilities
2-
//!
3-
//! This module provides some utilities for parsing test arguments as well as running and filtering
4-
//! a sequence of tests.
5-
//!
6-
//! As mentioned [here](crate#pythons-event-loop), PyO3 Asyncio tests cannot use the default test
7-
//! harness since it doesn't allow Python to gain control over the main thread. Instead, we have to
8-
//! provide our own test harness in order to create integration tests.
9-
//!
10-
//! ## Creating A PyO3 Asyncio Integration Test
11-
//!
12-
//! ### Main Test File
13-
//! First, we need to create the test's main file. Although these tests are considered integration
14-
//! tests, we cannot put them in the `tests` directory since that is a special directory owned by
15-
//! Cargo. Instead, we put our tests in a `pytests` directory, although the name `pytests` is just
16-
//! a convention.
17-
//!
18-
//! `pytests/test_example.rs`
19-
//! ```no_run
20-
//! fn main() {
21-
//!
22-
//! }
23-
//! ```
24-
//!
25-
//! ### Test Manifest Entry
26-
//! Next, we need to add our test file to the Cargo manifest. Add the following section to your
27-
//! `Cargo.toml`
28-
//!
29-
//! ```toml
30-
//! [[test]]
31-
//! name = "test_example"
32-
//! path = "pytests/test_example.rs"
33-
//! harness = false
34-
//! ```
35-
//!
36-
//! At this point you should be able to run the test via `cargo test`
37-
//!
38-
//! ### Using the PyO3 Asyncio Test Harness
39-
//! Now that we've got our test registered with `cargo test`, we can start using the PyO3 Asyncio
40-
//! test harness.
41-
//!
42-
//! In your `Cargo.toml` add the testing feature to `pyo3-asyncio`:
43-
//! ```toml
44-
//! pyo3-asyncio = { version = "0.13", features = ["testing", "async-std-runtime"] }
45-
//! ```
46-
//!
47-
//! Now, in your test's main file, call [`async_std::testing::test_main`]:
48-
//!
49-
//! ```no_run
50-
//! fn main() {
51-
//! pyo3_asyncio::async_std::testing::test_main("Example Test Suite", vec![]);
52-
//! }
53-
//! ```
54-
//!
55-
//! ### Adding Tests to the PyO3 Asyncio Test Harness
56-
//!
57-
//! ```no_run
58-
//! use std::{time::Duration, thread};
59-
//!
60-
//! use pyo3_asyncio::testing::Test;
61-
//!
62-
//! fn main() {
63-
//! pyo3_asyncio::async_std::testing::test_main(
64-
//! "Example Test Suite",
65-
//! vec![
66-
//! Test::new_async(
67-
//! "test_async_sleep".into(),
68-
//! async move {
69-
//! async_std::task::sleep(Duration::from_secs(1)).await;
70-
//! Ok(())
71-
//! }
72-
//! ),
73-
//! pyo3_asyncio::async_std::testing::new_sync_test(
74-
//! "test_sync_sleep".into(),
75-
//! || {
76-
//! thread::sleep(Duration::from_secs(1));
77-
//! Ok(())
78-
//! }
79-
//! )
80-
//! ]
81-
//! );
82-
//! }
83-
//! ```
84-
851
use std::future::Future;
862

873
use async_std::task;
884
use futures::channel::oneshot;
895
use pyo3::prelude::*;
906

91-
use crate::{get_event_loop, CALL_SOON, CREATE_FUTURE, EXPECT_INIT};
7+
use crate::generic::{self, JoinError, Runtime};
8+
9+
struct AsyncStdJoinError;
10+
11+
impl JoinError for AsyncStdJoinError {
12+
fn is_panic(&self) -> bool {
13+
todo!()
14+
}
15+
}
16+
17+
struct AsyncStdRuntime;
18+
19+
impl Runtime for AsyncStdRuntime {
20+
type JoinError = AsyncStdJoinError;
21+
type JoinHandle = task::JoinHandle<Result<(), AsyncStdJoinError>>;
22+
23+
fn spawn<F>(fut: F) -> Self::JoinHandle
24+
where
25+
F: Future<Output = ()> + Send + 'static,
26+
{
27+
task::spawn(async move {
28+
fut.await;
29+
Ok(())
30+
})
31+
}
32+
}
9233

9334
/// Run the event loop until the given Future completes
9435
///
@@ -107,17 +48,11 @@ use crate::{get_event_loop, CALL_SOON, CREATE_FUTURE, EXPECT_INIT};
10748
/// # use std::time::Duration;
10849
/// #
10950
/// # use pyo3::prelude::*;
110-
/// # use tokio::runtime::{Builder, Runtime};
111-
/// #
112-
/// # let runtime = Builder::new_current_thread()
113-
/// # .enable_all()
114-
/// # .build()
115-
/// # .expect("Couldn't build the runtime");
11651
/// #
11752
/// # Python::with_gil(|py| {
11853
/// # pyo3_asyncio::with_runtime(py, || {
119-
/// pyo3_asyncio::tokio::run_until_complete(py, &runtime, async move {
120-
/// tokio::time::sleep(Duration::from_secs(1)).await;
54+
/// pyo3_asyncio::async_std::run_until_complete(py, async move {
55+
/// async_std::task::sleep(Duration::from_secs(1)).await;
12156
/// Ok(())
12257
/// })?;
12358
/// # Ok(())
@@ -132,14 +67,7 @@ pub fn run_until_complete<F>(py: Python, fut: F) -> PyResult<()>
13267
where
13368
F: Future<Output = PyResult<()>> + Send + 'static,
13469
{
135-
let coro = into_coroutine(py, async move {
136-
fut.await?;
137-
Ok(Python::with_gil(|py| py.None()))
138-
})?;
139-
140-
get_event_loop(py).call_method1("run_until_complete", (coro,))?;
141-
142-
Ok(())
70+
generic::run_until_complete::<AsyncStdRuntime, _>(py, fut)
14371
}
14472

14573
#[pyclass]
@@ -173,27 +101,6 @@ impl PyTaskCompleter {
173101
}
174102
}
175103

176-
fn set_result(py: Python, future: &PyAny, result: PyResult<PyObject>) -> PyResult<()> {
177-
match result {
178-
Ok(val) => {
179-
let set_result = future.getattr("set_result")?;
180-
CALL_SOON
181-
.get()
182-
.expect(EXPECT_INIT)
183-
.call1(py, (set_result, val))?;
184-
}
185-
Err(err) => {
186-
let set_exception = future.getattr("set_exception")?;
187-
CALL_SOON
188-
.get()
189-
.expect(EXPECT_INIT)
190-
.call1(py, (set_exception, err))?;
191-
}
192-
}
193-
194-
Ok(())
195-
}
196-
197104
fn dump_err(py: Python<'_>) -> impl FnOnce(PyErr) + '_ {
198105
move |e| {
199106
// We can't display Python exceptions via std::fmt::Display,
@@ -213,25 +120,14 @@ fn dump_err(py: Python<'_>) -> impl FnOnce(PyErr) + '_ {
213120
/// ```no_run
214121
/// use std::time::Duration;
215122
///
216-
/// use lazy_static::lazy_static;
217123
/// use pyo3::prelude::*;
218-
/// use tokio::runtime::{Builder, Runtime};
219-
///
220-
/// lazy_static! {
221-
/// static ref CURRENT_THREAD_RUNTIME: Runtime = {
222-
/// Builder::new_current_thread()
223-
/// .enable_all()
224-
/// .build()
225-
/// .expect("Couldn't build the runtime")
226-
/// };
227-
/// }
228124
///
229125
/// /// Awaitable sleep function
230126
/// #[pyfunction]
231127
/// fn sleep_for(py: Python, secs: &PyAny) -> PyResult<PyObject> {
232128
/// let secs = secs.extract()?;
233129
///
234-
/// pyo3_asyncio::tokio::into_coroutine(py, &CURRENT_THREAD_RUNTIME, async move {
130+
/// pyo3_asyncio::async_std::into_coroutine(py, async move {
235131
/// tokio::time::sleep(Duration::from_secs(secs)).await;
236132
/// Python::with_gil(|py| Ok(py.None()))
237133
/// })
@@ -241,32 +137,96 @@ pub fn into_coroutine<F>(py: Python, fut: F) -> PyResult<PyObject>
241137
where
242138
F: Future<Output = PyResult<PyObject>> + Send + 'static,
243139
{
244-
let future_rx = CREATE_FUTURE.get().expect(EXPECT_INIT).call0(py)?;
245-
let future_tx1 = future_rx.clone();
246-
247-
task::spawn(async move {
248-
task::spawn(async move {
249-
let result = fut.await;
250-
251-
Python::with_gil(move |py| {
252-
if set_result(py, future_tx1.as_ref(py), result)
253-
.map_err(dump_err(py))
254-
.is_err()
255-
{
256-
257-
// Cancelled
258-
}
259-
});
260-
})
261-
.await;
262-
});
263-
264-
Ok(future_rx)
140+
generic::into_coroutine::<AsyncStdRuntime, _>(py, fut)
265141
}
266142

267143
/// <span class="module-item stab portability" style="display: inline; border-radius: 3px; padding: 2px; font-size: 80%; line-height: 1.2;"><code>testing</code></span> Testing Utilities for the async-std runtime.
268144
#[cfg(feature = "testing")]
269145
pub mod testing {
146+
//! # PyO3 Asyncio Testing Utilities
147+
//!
148+
//! This module provides some utilities for parsing test arguments as well as running and filtering
149+
//! a sequence of tests.
150+
//!
151+
//! As mentioned [here](crate#pythons-event-loop), PyO3 Asyncio tests cannot use the default test
152+
//! harness since it doesn't allow Python to gain control over the main thread. Instead, we have to
153+
//! provide our own test harness in order to create integration tests.
154+
//!
155+
//! ## Creating A PyO3 Asyncio Integration Test
156+
//!
157+
//! ### Main Test File
158+
//! First, we need to create the test's main file. Although these tests are considered integration
159+
//! tests, we cannot put them in the `tests` directory since that is a special directory owned by
160+
//! Cargo. Instead, we put our tests in a `pytests` directory, although the name `pytests` is just
161+
//! a convention.
162+
//!
163+
//! `pytests/test_example.rs`
164+
//! ```no_run
165+
//! fn main() {
166+
//!
167+
//! }
168+
//! ```
169+
//!
170+
//! ### Test Manifest Entry
171+
//! Next, we need to add our test file to the Cargo manifest. Add the following section to your
172+
//! `Cargo.toml`
173+
//!
174+
//! ```toml
175+
//! [[test]]
176+
//! name = "test_example"
177+
//! path = "pytests/test_example.rs"
178+
//! harness = false
179+
//! ```
180+
//!
181+
//! At this point you should be able to run the test via `cargo test`
182+
//!
183+
//! ### Using the PyO3 Asyncio Test Harness
184+
//! Now that we've got our test registered with `cargo test`, we can start using the PyO3 Asyncio
185+
//! test harness.
186+
//!
187+
//! In your `Cargo.toml` add the testing feature to `pyo3-asyncio`:
188+
//! ```toml
189+
//! pyo3-asyncio = { version = "0.13", features = ["testing", "async-std-runtime"] }
190+
//! ```
191+
//!
192+
//! Now, in your test's main file, call [`async_std::testing::test_main`]:
193+
//!
194+
//! ```no_run
195+
//! fn main() {
196+
//! pyo3_asyncio::async_std::testing::test_main("Example Test Suite", vec![]);
197+
//! }
198+
//! ```
199+
//!
200+
//! ### Adding Tests to the PyO3 Asyncio Test Harness
201+
//!
202+
//! ```no_run
203+
//! use std::{time::Duration, thread};
204+
//!
205+
//! use pyo3_asyncio::testing::Test;
206+
//!
207+
//! fn main() {
208+
//! pyo3_asyncio::async_std::testing::test_main(
209+
//! "Example Test Suite",
210+
//! vec![
211+
//! Test::new_async(
212+
//! "test_async_sleep".into(),
213+
//! async move {
214+
//! async_std::task::sleep(Duration::from_secs(1)).await;
215+
//! Ok(())
216+
//! }
217+
//! ),
218+
//! pyo3_asyncio::async_std::testing::new_sync_test(
219+
//! "test_sync_sleep".into(),
220+
//! || {
221+
//! thread::sleep(Duration::from_secs(1));
222+
//! Ok(())
223+
//! }
224+
//! )
225+
//! ]
226+
//! );
227+
//! }
228+
//! ```
229+
270230
use async_std::task;
271231
use pyo3::prelude::*;
272232

0 commit comments

Comments
 (0)