Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package.rust-version = "1.83"

[features]
async-std-runtime = ["async-std"]
smol-runtime = ["smol", "task-local", "async-executor"]
attributes = ["pyo3-async-runtimes-macros"]
testing = ["clap", "inventory"]
tokio-runtime = ["tokio"]
Expand All @@ -32,16 +33,21 @@ unstable-streams = [
"futures-util/sink",
"futures-channel/sink",
]
default = []
default = ["smol-runtime"]

[package.metadata.docs.rs]
features = ["attributes", "testing", "async-std-runtime", "tokio-runtime"]
features = ["attributes", "testing", "async-std-runtime", "tokio-runtime", "smol-runtime"]

[[example]]
name = "async_std"
path = "examples/async_std.rs"
required-features = ["attributes", "async-std-runtime"]

[[example]]
name = "smol"
path = "examples/smol.rs"
required-features = ["attributes", "smol-runtime"]

[[example]]
name = "tokio"
path = "examples/tokio.rs"
Expand Down Expand Up @@ -70,6 +76,18 @@ path = "pytests/test_async_std_run_forever.rs"
harness = false
required-features = ["async-std-runtime", "testing"]

[[test]]
name = "test_smol_asyncio"
path = "pytests/test_smol_asyncio.rs"
harness = false
required-features = ["smol-runtime", "testing", "attributes"]

[[test]]
name = "test_smol_run_forever"
path = "pytests/test_smol_run_forever.rs"
harness = false
required-features = ["async-std-runtime", "testing"]

[[test]]
name = "test_tokio_current_thread_asyncio"
path = "pytests/test_tokio_current_thread_asyncio.rs"
Expand Down Expand Up @@ -100,6 +118,12 @@ path = "pytests/test_async_std_uvloop.rs"
harness = false
required-features = ["async-std-runtime", "testing"]

[[test]]
name = "test_smol_uvloop"
path = "pytests/test_smol_uvloop.rs"
harness = false
required-features = ["smol-runtime", "testing"]

[[test]]
name = "test_tokio_current_thread_uvloop"
path = "pytests/test_tokio_current_thread_uvloop.rs"
Expand All @@ -121,6 +145,7 @@ required-features = ["async-std-runtime", "testing"]

[dependencies]
async-channel = { version = "2.3", optional = true }
async-executor = { version = "1.14.0", optional = true }
clap = { version = "4.5", optional = true }
futures-channel = "0.3"
futures-util = "0.3"
Expand All @@ -129,6 +154,7 @@ once_cell = "1.14"
pin-project-lite = "0.2"
pyo3 = "0.28"
pyo3-async-runtimes-macros = { path = "pyo3-async-runtimes-macros", version = "=0.28.0", optional = true }
task-local = { version = "0.1.0", optional = true }

[dev-dependencies]
pyo3 = { version = "0.28", features = ["macros"] }
Expand All @@ -138,6 +164,10 @@ version = "1.12"
features = ["unstable"]
optional = true

[dependencies.smol]
version = "2.0.2"
optional = true

[dependencies.tokio]
version = "1.13"
features = ["rt", "rt-multi-thread", "time"]
Expand Down
17 changes: 17 additions & 0 deletions examples/smol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use pyo3::prelude::*;

#[pyo3_async_runtimes::smol::main]
async fn main() -> PyResult<()> {
let fut = Python::attach(|py| {
let asyncio = py.import("asyncio")?;

// convert asyncio.sleep into a Rust Future
pyo3_async_runtimes::smol::into_future(asyncio.call_method1("sleep", (1,))?)
})?;

println!("sleeping for 1s");
fut.await?;
println!("done");

Ok(())
}
153 changes: 153 additions & 0 deletions pyo3-async-runtimes-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,62 @@ pub fn async_std_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
result.into()
}

/// Enables an async main function that uses the async-std runtime.
///
/// # Examples
///
/// ```ignore
/// #[pyo3_async_runtimes::smol::main]
/// async fn main() -> PyResult<()> {
/// Ok(())
/// }
/// ```
#[cfg(not(test))] // NOTE: exporting main breaks tests, we should file an issue.
#[proc_macro_attribute]
pub fn smol_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);

let ret = &input.sig.output;
let inputs = &input.sig.inputs;
let name = &input.sig.ident;
let body = &input.block;
let attrs = &input.attrs;
let vis = &input.vis;

if name != "main" {
return TokenStream::from(quote_spanned! { name.span() =>
compile_error!("only the main function can be tagged with #[async_std::main]"),
});
}

if input.sig.asyncness.is_none() {
return TokenStream::from(quote_spanned! { input.span() =>
compile_error!("the async keyword is missing from the function declaration"),
});
}

let result = quote! {
#vis fn main() {
#(#attrs)*
async fn main(#inputs) #ret {
#body
}

pyo3::Python::initialize();

pyo3::Python::attach(|py| {
pyo3_async_runtimes::smol::run(py, main())
.map_err(|e| {
e.print_and_set_sys_last_vars(py);
})
.unwrap();
});
}
};

result.into()
}

/// Enables an async main function that uses the tokio runtime.
///
/// # Arguments
Expand Down Expand Up @@ -198,6 +254,103 @@ pub fn async_std_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
result.into()
}

/// Registers an `async-std` test with the `pyo3-asyncio` test harness.
///
/// This attribute is meant to mirror the `#[test]` attribute and allow you to mark a function for
/// testing within an integration test. Like the `#[smol::test]` attribute, it will accept
/// `async` test functions, but it will also accept blocking functions as well.
///
/// # Examples
/// ```ignore
/// use std::{time::Duration, thread};
///
/// use pyo3::prelude::*;
///
/// // async test function
/// #[pyo3_async_runtimes::smol::test]
/// async fn test_async_sleep() -> PyResult<()> {
/// smol::Timer::after(Duration::from_secs(1)).await;
/// Ok(())
/// }
///
/// // blocking test function
/// #[pyo3_async_runtimes::smol::test]
/// fn test_blocking_sleep() -> PyResult<()> {
/// thread::sleep(Duration::from_secs(1));
/// Ok(())
/// }
///
/// // blocking test functions can optionally accept an event_loop parameter
/// #[pyo3_async_runtimes::smol::test]
/// fn test_blocking_sleep_with_event_loop(event_loop: Py<PyAny>) -> PyResult<()> {
/// thread::sleep(Duration::from_secs(1));
/// Ok(())
/// }
/// ```
#[cfg(not(test))] // NOTE: exporting main breaks tests, we should file an issue.
#[proc_macro_attribute]
pub fn smol_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);

let sig = &input.sig;
let name = &input.sig.ident;
let body = &input.block;
let vis = &input.vis;

let fn_impl = if input.sig.asyncness.is_none() {
// Optionally pass an event_loop parameter to blocking tasks
let task = if sig.inputs.is_empty() {
quote! {
Box::pin(pyo3_async_runtimes::smol::re_exports::spawn_blocking(move || {
#name()
}))
}
} else {
quote! {
let event_loop = pyo3::Python::attach(|py| {
pyo3_async_runtimes::smol::get_current_loop(py).unwrap().into()
});
Box::pin(pyo3_async_runtimes::smol::re_exports::spawn_blocking(move || {
#name(event_loop)
}))
}
};

quote! {
#vis fn #name() -> std::pin::Pin<Box<dyn std::future::Future<Output = pyo3::PyResult<()>> + Send>> {
#sig {
#body
}

#task
}
}
} else {
quote! {
#vis fn #name() -> std::pin::Pin<Box<dyn std::future::Future<Output = pyo3::PyResult<()>> + Send>> {
#sig {
#body
}

Box::pin(#name())
}
}
};

let result = quote! {
#fn_impl

pyo3_async_runtimes::inventory::submit! {
pyo3_async_runtimes::testing::Test {
name: concat!(std::module_path!(), "::", stringify!(#name)),
test_fn: &#name
}
}
};

result.into()
}

/// Registers a `tokio` test with the `pyo3-asyncio` test harness.
///
/// This attribute is meant to mirror the `#[test]` attribute and allow you to mark a function for
Expand Down
65 changes: 65 additions & 0 deletions pytests/common copy/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::ffi::CString;
use std::{thread, time::Duration};

use pyo3::prelude::*;
use pyo3_async_runtimes::TaskLocals;

pub(super) const TEST_MOD: &str = r#"
import asyncio

async def py_sleep(duration):
await asyncio.sleep(duration)

async def sleep_for_1s(sleep_for):
await sleep_for(1)
"#;

pub(super) async fn test_into_future(event_loop: Py<PyAny>) -> PyResult<()> {
let fut = Python::attach(|py| {
let test_mod = PyModule::from_code(
py,
&CString::new(TEST_MOD).unwrap(),
&CString::new("test_rust_coroutine/test_mod.py").unwrap(),
&CString::new("test_mod").unwrap(),
)?;

pyo3_async_runtimes::into_future_with_locals(
&TaskLocals::new(event_loop.into_bound(py)),
test_mod.call_method1("py_sleep", (1,))?,
)
})?;

fut.await?;

Ok(())
}

pub(super) fn test_blocking_sleep() -> PyResult<()> {
thread::sleep(Duration::from_secs(1));
Ok(())
}

pub(super) async fn test_other_awaitables(event_loop: Py<PyAny>) -> PyResult<()> {
let fut = Python::attach(|py| {
let functools = py.import("functools")?;
let time = py.import("time")?;

// spawn a blocking sleep in the threadpool executor - returns a task, not a coroutine
let task = event_loop.bind(py).call_method1(
"run_in_executor",
(
py.None(),
functools.call_method1("partial", (time.getattr("sleep")?, 1))?,
),
)?;

pyo3_async_runtimes::into_future_with_locals(
&TaskLocals::new(event_loop.into_bound(py)),
task,
)
})?;

fut.await?;

Ok(())
}
Loading
Loading