Skip to content

Commit 6b4937d

Browse files
author
Andrew J Westlake
committed
Added support for async-std runtime, removed spawn and spawn_blocking calls from API
1 parent b1a6c0d commit 6b4937d

11 files changed

+933
-374
lines changed

Cargo.toml

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,49 @@ edition = "2018"
77
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
88

99
[features]
10+
async-std-runtime = ["async-std"]
1011
testing = ["clap"]
12+
tokio-runtime = ["tokio"]
1113
default = []
1214

15+
1316
[[test]]
14-
name = "test_asyncio"
15-
path = "pytests/test_asyncio.rs"
17+
name = "test_async_std_asyncio"
18+
path = "pytests/test_async_std_asyncio.rs"
1619
harness = false
17-
required-features = ["testing"]
20+
required-features = ["async-std-runtime", "testing"]
1821

1922
[[test]]
20-
name = "test_run_forever"
21-
path = "pytests/test_run_forever.rs"
23+
name = "test_async_std_run_forever"
24+
path = "pytests/test_async_std_run_forever.rs"
2225
harness = false
23-
required-features = ["testing"]
26+
required-features = ["async-std-runtime", "testing"]
27+
28+
[[test]]
29+
name = "test_tokio_asyncio"
30+
path = "pytests/test_tokio_asyncio.rs"
31+
harness = false
32+
required-features = ["tokio-runtime", "testing"]
33+
34+
[[test]]
35+
name = "test_tokio_run_forever"
36+
path = "pytests/test_tokio_run_forever.rs"
37+
harness = false
38+
required-features = ["tokio-runtime", "testing"]
2439

2540
[dependencies]
2641
clap = { version = "2.33", optional = true }
2742
futures = "0.3"
2843
lazy_static = "1.4"
2944
once_cell = "1.5"
3045
pyo3 = "0.13"
31-
tokio = { version = "1.0", features = ["full"] }
46+
47+
[dependencies.async-std]
48+
version = "1.9"
49+
features = ["unstable"]
50+
optional = true
51+
52+
[dependencies.tokio]
53+
version = "1.0"
54+
features = ["full"]
55+
optional = true

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ lint: fmt clippy
1212
@true
1313

1414
test: lint
15-
cargo test --features testing
15+
cargo test --features async-std-runtime --features tokio-runtime --features testing
1616

1717
publish: test
1818
cargo publish

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313

1414
> PyO3 Asyncio is a _brand new_ part of the broader PyO3 ecosystem. Feel free to open any issues for feature requests or bugfixes for this crate.
1515
16+
## Known Problems
17+
18+
Currently, this library can give spurious failures during finalization. A solution should be released for PyO3 soon, but in the meantime you can add this patch to your `Cargo.toml` to use the master branch for PyO3
19+
20+
```toml
21+
[patch.crates-io]
22+
pyo3 = { git = "https://github.com/PyO3/pyo3" }
23+
```
24+
1625
## Quickstart
1726

1827
Here we initialize the runtime, import Python's `asyncio` library and run the given future to completion using Python's default `EventLoop` and Tokio. Inside the future, we convert `asyncio` sleep into a Rust future and await it.
@@ -29,7 +38,7 @@ fn main() {
2938
let asyncio: PyObject = py.import("asyncio")?.into();
3039

3140
// Run the event loop until the given future completes
32-
pyo3_asyncio::run_until_complete(py, async move {
41+
pyo3_asyncio::async_std::run_until_complete(py, async move {
3342
Python::with_gil(|py| {
3443
// convert asyncio.sleep into a Rust Future
3544
pyo3_asyncio::into_future(

pytests/test_asyncio.rs renamed to pytests/test_async_std_asyncio.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use std::{future::Future, thread, time::Duration};
22

3+
use async_std::task;
34
use pyo3::{prelude::*, wrap_pyfunction};
45

5-
use pyo3_asyncio::testing::{test_main, Test};
6+
use pyo3_asyncio::{
7+
async_std::testing::{new_sync_test, test_main},
8+
testing::Test,
9+
};
610

711
#[pyfunction]
812
fn sleep_for(py: Python, secs: &PyAny) -> PyResult<PyObject> {
913
let secs = secs.extract()?;
1014

11-
pyo3_asyncio::into_coroutine(py, async move {
12-
tokio::time::sleep(Duration::from_secs(secs)).await;
15+
pyo3_asyncio::async_std::into_coroutine(py, async move {
16+
task::sleep(Duration::from_secs(secs)).await;
1317
Python::with_gil(|py| Ok(py.None()))
1418
})
1519
}
@@ -74,7 +78,7 @@ fn test_async_sleep<'p>(
7478
let asyncio = PyObject::from(py.import("asyncio")?);
7579

7680
Ok(async move {
77-
tokio::time::sleep(Duration::from_secs(1)).await;
81+
task::sleep(Duration::from_secs(1)).await;
7882

7983
Python::with_gil(|py| {
8084
pyo3_asyncio::into_future(py, asyncio.as_ref(py).call_method1("sleep", (1.0,))?)
@@ -103,7 +107,7 @@ fn main() {
103107
.unwrap()
104108
}),
105109
),
106-
Test::new_sync("test_blocking_sleep".into(), || {
110+
new_sync_test("test_blocking_sleep".into(), || {
107111
test_blocking_sleep();
108112
Ok(())
109113
}),

pytests/test_run_forever.rs renamed to pytests/test_async_std_run_forever.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ fn dump_err(py: Python<'_>) -> impl FnOnce(PyErr) + '_ {
1313
fn main() {
1414
Python::with_gil(|py| {
1515
pyo3_asyncio::with_runtime(py, || {
16-
pyo3_asyncio::spawn(async move {
17-
tokio::time::sleep(Duration::from_secs(1)).await;
16+
async_std::task::spawn(async move {
17+
async_std::task::sleep(Duration::from_secs(1)).await;
1818

1919
Python::with_gil(|py| {
2020
let event_loop = pyo3_asyncio::get_event_loop(py);

pytests/test_tokio_asyncio.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use std::{
2+
future::{pending, Future},
3+
thread,
4+
time::Duration,
5+
};
6+
7+
use lazy_static::lazy_static;
8+
use pyo3::{prelude::*, wrap_pyfunction};
9+
use tokio::runtime::{Builder, Runtime};
10+
11+
use pyo3_asyncio::{
12+
testing::Test,
13+
tokio::testing::{new_sync_test, test_main},
14+
};
15+
16+
lazy_static! {
17+
static ref CURRENT_THREAD_RUNTIME: Runtime = {
18+
Builder::new_current_thread()
19+
.enable_all()
20+
.build()
21+
.expect("Couldn't build the runtime")
22+
};
23+
}
24+
25+
#[pyfunction]
26+
fn sleep_for(py: Python, secs: &PyAny) -> PyResult<PyObject> {
27+
let secs = secs.extract()?;
28+
29+
pyo3_asyncio::tokio::into_coroutine(py, &CURRENT_THREAD_RUNTIME, async move {
30+
tokio::time::sleep(Duration::from_secs(secs)).await;
31+
Python::with_gil(|py| Ok(py.None()))
32+
})
33+
}
34+
35+
const TEST_MOD: &'static str = r#"
36+
import asyncio
37+
38+
async def py_sleep(duration):
39+
await asyncio.sleep(duration)
40+
41+
async def sleep_for_1s(sleep_for):
42+
await sleep_for(1)
43+
"#;
44+
45+
fn test_into_coroutine(
46+
py: Python,
47+
) -> PyResult<impl Future<Output = PyResult<()>> + Send + 'static> {
48+
let sleeper_mod: Py<PyModule> = PyModule::new(py, "rust_sleeper")?.into();
49+
50+
sleeper_mod
51+
.as_ref(py)
52+
.add_wrapped(wrap_pyfunction!(sleep_for))?;
53+
54+
let test_mod: PyObject =
55+
PyModule::from_code(py, TEST_MOD, "test_rust_coroutine/test_mod.py", "test_mod")?.into();
56+
57+
Ok(async move {
58+
Python::with_gil(|py| {
59+
pyo3_asyncio::into_future(
60+
py,
61+
test_mod
62+
.call_method1(py, "sleep_for_1s", (sleeper_mod.getattr(py, "sleep_for")?,))?
63+
.as_ref(py),
64+
)
65+
})?
66+
.await?;
67+
Ok(())
68+
})
69+
}
70+
71+
fn test_into_future(py: Python) -> PyResult<impl Future<Output = PyResult<()>> + Send + 'static> {
72+
let test_mod: PyObject =
73+
PyModule::from_code(py, TEST_MOD, "test_rust_coroutine/test_mod.py", "test_mod")?.into();
74+
75+
Ok(async move {
76+
Python::with_gil(|py| {
77+
pyo3_asyncio::into_future(
78+
py,
79+
test_mod
80+
.call_method1(py, "py_sleep", (1.into_py(py),))?
81+
.as_ref(py),
82+
)
83+
})?
84+
.await?;
85+
Ok(())
86+
})
87+
}
88+
89+
fn test_async_sleep<'p>(
90+
py: Python<'p>,
91+
) -> PyResult<impl Future<Output = PyResult<()>> + Send + 'static> {
92+
let asyncio = PyObject::from(py.import("asyncio")?);
93+
94+
Ok(async move {
95+
tokio::time::sleep(Duration::from_secs(1)).await;
96+
97+
Python::with_gil(|py| {
98+
pyo3_asyncio::into_future(py, asyncio.as_ref(py).call_method1("sleep", (1.0,))?)
99+
})?
100+
.await?;
101+
102+
Ok(())
103+
})
104+
}
105+
106+
fn test_blocking_sleep() {
107+
thread::sleep(Duration::from_secs(1));
108+
}
109+
110+
fn main() {
111+
thread::spawn(|| {
112+
CURRENT_THREAD_RUNTIME.block_on(pending::<()>());
113+
});
114+
115+
test_main(
116+
"PyO3 Asyncio Test Suite",
117+
&CURRENT_THREAD_RUNTIME,
118+
vec![
119+
Test::new_async(
120+
"test_async_sleep".into(),
121+
Python::with_gil(|py| {
122+
test_async_sleep(py)
123+
.map_err(|e| {
124+
e.print_and_set_sys_last_vars(py);
125+
})
126+
.unwrap()
127+
}),
128+
),
129+
new_sync_test("test_blocking_sleep".into(), || {
130+
test_blocking_sleep();
131+
Ok(())
132+
}),
133+
Test::new_async(
134+
"test_into_coroutine".into(),
135+
Python::with_gil(|py| {
136+
test_into_coroutine(py)
137+
.map_err(|e| {
138+
e.print_and_set_sys_last_vars(py);
139+
})
140+
.unwrap()
141+
}),
142+
),
143+
Test::new_async(
144+
"test_into_future".into(),
145+
Python::with_gil(|py| {
146+
test_into_future(py)
147+
.map_err(|e| {
148+
e.print_and_set_sys_last_vars(py);
149+
})
150+
.unwrap()
151+
}),
152+
),
153+
],
154+
)
155+
}

pytests/test_tokio_run_forever.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use std::{future::pending, thread, time::Duration};
2+
3+
use lazy_static::lazy_static;
4+
use pyo3::prelude::*;
5+
use tokio::runtime::{Builder, Runtime};
6+
7+
lazy_static! {
8+
static ref CURRENT_THREAD_RUNTIME: Runtime = {
9+
Builder::new_current_thread()
10+
.enable_all()
11+
.build()
12+
.expect("Couldn't build the runtime")
13+
};
14+
}
15+
16+
fn dump_err(py: Python<'_>) -> impl FnOnce(PyErr) + '_ {
17+
move |e| {
18+
// We can't display Python exceptions via std::fmt::Display,
19+
// so print the error here manually.
20+
e.print_and_set_sys_last_vars(py);
21+
}
22+
}
23+
24+
fn main() {
25+
thread::spawn(|| {
26+
CURRENT_THREAD_RUNTIME.block_on(pending::<()>());
27+
});
28+
29+
Python::with_gil(|py| {
30+
pyo3_asyncio::with_runtime(py, || {
31+
CURRENT_THREAD_RUNTIME.spawn(async move {
32+
tokio::time::sleep(Duration::from_secs(1)).await;
33+
34+
Python::with_gil(|py| {
35+
let event_loop = pyo3_asyncio::get_event_loop(py);
36+
37+
event_loop
38+
.call_method1(
39+
"call_soon_threadsafe",
40+
(event_loop.getattr("stop").map_err(dump_err(py)).unwrap(),),
41+
)
42+
.map_err(dump_err(py))
43+
.unwrap();
44+
})
45+
});
46+
47+
pyo3_asyncio::run_forever(py)?;
48+
49+
println!("test test_run_forever ... ok");
50+
Ok(())
51+
})
52+
.map_err(dump_err(py))
53+
.unwrap();
54+
})
55+
}

0 commit comments

Comments
 (0)