pyo3-async-runtimes-macros-0.27.0/.cargo_vcs_info.json0000644000000001700000000000100162530ustar { "git": { "sha1": "22e0ec107b3f6b0741ab6a0b4321d0e247a4a981" }, "path_in_vcs": "pyo3-async-runtimes-macros" }pyo3-async-runtimes-macros-0.27.0/Cargo.lock0000644000000021550000000000100142330ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3-async-runtimes-macros" version = "0.27.0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "syn" version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" pyo3-async-runtimes-macros-0.27.0/Cargo.toml0000644000000026170000000000100142610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "pyo3-async-runtimes-macros" version = "0.27.0" authors = [ "Andrew J Westlake ", "David Hewitt ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Proc Macro Attributes for `pyo3-async-runtimes`" homepage = "https://github.com/PyO3/pyo3-async-runtimes" documentation = "https://docs.rs/crate/pyo3-async-runtimes/" readme = "README.md" keywords = [ "pyo3", "python", "ffi", "async", "asyncio", ] categories = [ "api-bindings", "development-tools::ffi", ] license = "Apache-2.0" repository = "https://github.com/PyO3/pyo3-async-runtimes" resolver = "2" [lib] name = "pyo3_async_runtimes_macros" path = "src/lib.rs" proc-macro = true [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1" [dependencies.syn] version = "2" features = ["full"] pyo3-async-runtimes-macros-0.27.0/Cargo.toml.orig000064400000000000000000000013031046102023000177310ustar 00000000000000[package] name = "pyo3-async-runtimes-macros" description = "Proc Macro Attributes for `pyo3-async-runtimes`" version = "0.27.0" authors = [ "Andrew J Westlake ", "David Hewitt ", ] readme = "../README.md" keywords = ["pyo3", "python", "ffi", "async", "asyncio"] homepage = "https://github.com/PyO3/pyo3-async-runtimes" repository = "https://github.com/PyO3/pyo3-async-runtimes" documentation = "https://docs.rs/crate/pyo3-async-runtimes/" categories = ["api-bindings", "development-tools::ffi"] license = "Apache-2.0" edition = "2018" [lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1" syn = { version = "2", features = ["full"] } pyo3-async-runtimes-macros-0.27.0/LICENSE000064400000000000000000000250351046102023000160570ustar 00000000000000 Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. pyo3-async-runtimes-macros-0.27.0/README.md000064400000000000000000000431551046102023000163340ustar 00000000000000# Async Runtime Integrations for PyO3 [![Actions Status](https://github.com/PyO3/pyo3-async-runtimes/workflows/CI/badge.svg)](https://github.com/PyO3/pyo3-async-runtimes) [![codecov](https://codecov.io/gh/davidhewitt/pyo3-async-runtimes/branch/main/graph/badge.svg)](https://codecov.io/gh/PyO3/pyo3-async-runtimes) [![crates.io](https://img.shields.io/crates/v/pyo3-async-runtimes)](https://crates.io/crates/pyo3-async-runtimes) [![minimum rustc 1.63](https://img.shields.io/badge/rustc-1.63+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) ***Forked from [`pyo3-asyncio`](https://github.com/awestlake87/pyo3-asyncio/) to deliver compatibility for PyO3 0.21+.*** [Rust](http://www.rust-lang.org/) bindings for [Python](https://www.python.org/)'s [Asyncio Library](https://docs.python.org/3/library/asyncio.html). This crate facilitates interactions between Rust Futures and Python Coroutines and manages the lifecycle of their corresponding event loops. - PyO3 Project: [Homepage](https://pyo3.rs/) | [GitHub](https://github.com/PyO3/pyo3) - `pyo3-async-runtimes` API Documentation: [stable](https://docs.rs/pyo3-async-runtimes/) - Guide for Async / Await [stable](https://pyo3.rs/latest/ecosystem/async-await.html) | [main](https://pyo3.rs/main/ecosystem/async-await.html) - Contributing Notes: [github](https://github.com/PyO3/pyo3-async-runtimes/blob/main/Contributing.md) ## Usage `pyo3-async-runtimes` supports the following software versions: - Python 3.9 and up (CPython and PyPy) - Rust 1.63 and up ## `pyo3-async-runtimes` Primer If you are working with a Python library that makes use of async functions or wish to provide Python bindings for an async Rust library, [`pyo3-async-runtimes`](https://github.com/PyO3/pyo3-async-runtimes) likely has the tools you need. It provides conversions between async functions in both Python and Rust and was designed with first-class support for popular Rust runtimes such as [`tokio`](https://tokio.rs/) and [`async-std`](https://async.rs/). In addition, all async Python code runs on the default `asyncio` event loop, so `pyo3-async-runtimes` should work just fine with existing Python libraries. In the following sections, we'll give a general overview of `pyo3-async-runtimes` explaining how to call async Python functions with PyO3, how to call async Rust functions from Python, and how to configure your codebase to manage the runtimes of both. ### Quickstart Here are some examples to get you started right away! A more detailed breakdown of the concepts in these examples can be found in the following sections. #### Rust Applications Here we initialize the runtime, import Python's `asyncio` library and run the given future to completion using Python's default `EventLoop` and `async-std`. Inside the future, we convert `asyncio` sleep into a Rust future and await it. ```toml # Cargo.toml dependencies [dependencies] pyo3 = { version = "0.27" } pyo3-async-runtimes = { version = "0.27", features = ["attributes", "async-std-runtime"] } async-std = "1.13" ``` ```rust //! main.rs use pyo3::prelude::*; #[pyo3_async_runtimes::async_std::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::async_std::into_future(asyncio.call_method1("sleep", (1,))?) })?; fut.await?; Ok(()) } ``` The same application can be written to use `tokio` instead using the `#[pyo3_async_runtimes::tokio::main]` attribute. ```toml # Cargo.toml dependencies [dependencies] pyo3 = { version = "0.27" } pyo3-async-runtimes = { version = "0.27", features = ["attributes", "tokio-runtime"] } tokio = "1.40" ``` ```rust //! main.rs use pyo3::prelude::*; #[pyo3_async_runtimes::tokio::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::tokio::into_future(asyncio.call_method1("sleep", (1,))?) })?; fut.await?; Ok(()) } ``` More details on the usage of this library can be found in the API docs and the primer below. #### PyO3 Native Rust Modules `pyo3-async-runtimes` can also be used to write native modules with async functions. Add the `[lib]` section to `Cargo.toml` to make your library a `cdylib` that Python can import. ```toml [lib] name = "my_async_module" crate-type = ["cdylib"] ``` Make your project depend on `pyo3` with the `extension-module` feature enabled and select your `pyo3-async-runtimes` runtime: For `async-std`: ```toml [dependencies] pyo3 = { version = "0.27", features = ["extension-module"] } pyo3-async-runtimes = { version = "0.27", features = ["async-std-runtime"] } async-std = "1.13" ``` For `tokio`: ```toml [dependencies] pyo3 = { version = "0.27", features = ["extension-module"] } pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] } tokio = "1.40" ``` Export an async function that makes use of `async-std`: ```rust //! lib.rs use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] fn rust_sleep(py: Python) -> PyResult> { pyo3_async_runtimes::async_std::future_into_py(py, async { async_std::task::sleep(std::time::Duration::from_secs(1)).await; Ok(()) }) } #[pymodule] fn my_async_module(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } ``` If you want to use `tokio` instead, here's what your module should look like: ```rust //! lib.rs use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] fn rust_sleep(py: Python) -> PyResult> { pyo3_async_runtimes::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(()) }) } #[pymodule] fn my_async_module(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } ``` You can build your module with maturin (see the [Using Rust in Python](https://pyo3.rs/main/#using-rust-from-python) section in the PyO3 guide for setup instructions). After that you should be able to run the Python REPL to try it out. ```bash maturin develop && python3 🔗 Found pyo3 bindings 🐍 Found CPython 3.8 at python3 Finished dev [unoptimized + debuginfo] target(s) in 0.04s Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio >>> >>> from my_async_module import rust_sleep >>> >>> async def main(): >>> await rust_sleep() >>> >>> # should sleep for 1s >>> asyncio.run(main()) >>> ``` ### Awaiting an Async Python Function in Rust Let's take a look at a dead simple async Python function: ```python # Sleep for 1 second async def py_sleep(): await asyncio.sleep(1) ``` **Async functions in Python are simply functions that return a `coroutine` object**. For our purposes, we really don't need to know much about these `coroutine` objects. The key factor here is that calling an `async` function is _just like calling a regular function_, the only difference is that we have to do something special with the object that it returns. Normally in Python, that something special is the `await` keyword, but in order to await this coroutine in Rust, we first need to convert it into Rust's version of a `coroutine`: a `Future`. That's where `pyo3-async-runtimes` comes in. [`pyo3_async_runtimes::into_future`](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/fn.into_future.html) performs this conversion for us: ```rust no_run use pyo3::prelude::*; #[pyo3_async_runtimes::tokio::main] async fn main() -> PyResult<()> { let future = Python::attach(|py| -> PyResult<_> { // import the module containing the py_sleep function let example = py.import("example")?; // calling the py_sleep method like a normal function // returns a coroutine let coroutine = example.call_method0("py_sleep")?; // convert the coroutine into a Rust future using the // tokio runtime pyo3_async_runtimes::tokio::into_future(coroutine) })?; // await the future future.await?; Ok(()) } ``` > If you're interested in learning more about `coroutines` and `awaitables` in general, check out the > [Python 3 `asyncio` docs](https://docs.python.org/3/library/asyncio-task.html) for more information. ### Awaiting a Rust Future in Python Here we have the same async function as before written in Rust using the [`async-std`](https://async.rs/) runtime: ```rust /// Sleep for 1 second async fn rust_sleep() { async_std::task::sleep(std::time::Duration::from_secs(1)).await; } ``` Similar to Python, Rust's async functions also return a special object called a `Future`: ```rust compile_fail let future = rust_sleep(); ``` We can convert this `Future` object into Python to make it `awaitable`. This tells Python that you can use the `await` keyword with it. In order to do this, we'll call [`pyo3_async_runtimes::async_std::future_into_py`](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/async_std/fn.future_into_py.html): ```rust use pyo3::prelude::*; async fn rust_sleep() { async_std::task::sleep(std::time::Duration::from_secs(1)).await; } #[pyfunction] fn call_rust_sleep(py: Python) -> PyResult> { pyo3_async_runtimes::async_std::future_into_py(py, async move { rust_sleep().await; Ok(()) }) } ``` In Python, we can call this pyo3 function just like any other async function: ```python from example import call_rust_sleep async def rust_sleep(): await call_rust_sleep() ``` ## Managing Event Loops Python's event loop requires some special treatment, especially regarding the main thread. Some of Python's `asyncio` features, like proper signal handling, require control over the main thread, which doesn't always play well with Rust. Luckily, Rust's event loops are pretty flexible and don't _need_ control over the main thread, so in `pyo3-async-runtimes`, we decided the best way to handle Rust/Python interop was to just surrender the main thread to Python and run Rust's event loops in the background. Unfortunately, since most event loop implementations _prefer_ control over the main thread, this can still make some things awkward. ### `pyo3-async-runtimes` Initialization Because Python needs to control the main thread, we can't use the convenient proc macros from Rust runtimes to handle the `main` function or `#[test]` functions. Instead, the initialization for PyO3 has to be done from the `main` function and the main thread must block on [`pyo3_async_runtimes::async_std::run_until_complete`](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/async_std/fn.run_until_complete.html). Because we have to block on one of those functions, we can't use [`#[async_std::main]`](https://docs.rs/async-std/latest/async_std/attr.main.html) or [`#[tokio::main]`](https://docs.rs/tokio/1.1.0/tokio/attr.main.html) since it's not a good idea to make long blocking calls during an async function. > Internally, these `#[main]` proc macros are expanded to something like this: > > ```rust compile_fail > fn main() { > // your async main fn > async fn _main_impl() { /* ... */ } > Runtime::new().block_on(_main_impl()); > } > ``` > > Making a long blocking call inside the `Future` that's being driven by `block_on` prevents that > thread from doing anything else and can spell trouble for some runtimes (also this will actually > deadlock a single-threaded runtime!). Many runtimes have some sort of `spawn_blocking` mechanism > that can avoid this problem, but again that's not something we can use here since we need it to > block on the _main_ thread. For this reason, `pyo3-async-runtimes` provides its own set of proc macros to provide you with this initialization. These macros are intended to mirror the initialization of `async-std` and `tokio` while also satisfying the Python runtime's needs. Here's a full example of PyO3 initialization with the `async-std` runtime: ```rust no_run use pyo3::prelude::*; #[pyo3_async_runtimes::async_std::main] async fn main() -> PyResult<()> { // PyO3 is initialized - Ready to go let fut = Python::attach(|py| -> PyResult<_> { let asyncio = py.import("asyncio")?; // convert asyncio.sleep into a Rust Future pyo3_async_runtimes::async_std::into_future( asyncio.call_method1("sleep", (1,))? ) })?; fut.await?; Ok(()) } ``` #### A Note About `asyncio.run` In Python 3.7+, the recommended way to run a top-level coroutine with `asyncio` is with `asyncio.run`. In `v0.13` we recommended against using this function due to initialization issues, but in `v0.14` it's perfectly valid to use this function... with a caveat. Since our Rust <--> Python conversions require a reference to the Python event loop, this poses a problem. Imagine we have a `pyo3-async-runtimes` module that defines a `rust_sleep` function like in previous examples. You might rightfully assume that you can call pass this directly into `asyncio.run` like this: ```python import asyncio from my_async_module import rust_sleep asyncio.run(rust_sleep()) ``` You might be surprised to find out that this throws an error: ```bash Traceback (most recent call last): File "example.py", line 5, in asyncio.run(rust_sleep()) RuntimeError: no running event loop ``` What's happening here is that we are calling `rust_sleep` _before_ the future is actually running on the event loop created by `asyncio.run`. This is counter-intuitive, but expected behaviour, and unfortunately there doesn't seem to be a good way of solving this problem within `pyo3-async-runtimes` itself. However, we can make this example work with a simple workaround: ```python import asyncio from my_async_module import rust_sleep # Calling main will just construct the coroutine that later calls rust_sleep. # - This ensures that rust_sleep will be called when the event loop is running, # not before. async def main(): await rust_sleep() # Run the main() coroutine at the top-level instead asyncio.run(main()) ``` #### Non-standard Python Event Loops Python allows you to use alternatives to the default `asyncio` event loop. One popular alternative is `uvloop`. In `v0.13` using non-standard event loops was a bit of an ordeal, but in `v0.14` it's trivial. #### Using `uvloop` in a PyO3 Native Extensions ```toml # Cargo.toml [lib] name = "my_async_module" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.27", features = ["extension-module"] } pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] } async-std = "1.13" tokio = "1.40" ``` ```rust //! lib.rs use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] fn rust_sleep(py: Python) -> PyResult> { pyo3_async_runtimes::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(()) }) } #[pymodule] fn my_async_module(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } ``` ```bash $ maturin develop && python3 🔗 Found pyo3 bindings 🐍 Found CPython 3.8 at python3 Finished dev [unoptimized + debuginfo] target(s) in 0.04s Python 3.8.8 (default, Apr 13 2021, 19:58:27) [GCC 7.3.0] :: Anaconda, Inc. on linux Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio >>> import uvloop >>> >>> import my_async_module >>> >>> uvloop.install() >>> >>> async def main(): ... await my_async_module.rust_sleep() ... >>> asyncio.run(main()) >>> ``` #### Using `uvloop` in Rust Applications Using `uvloop` in Rust applications is a bit trickier, but it's still possible with relatively few modifications. Unfortunately, we can't make use of the `#[pyo3_async_runtimes::::main]` attribute with non-standard event loops. This is because the `#[pyo3_async_runtimes::::main]` proc macro has to interact with the Python event loop before we can install the `uvloop` policy. ```toml [dependencies] async-std = "1.13" pyo3 = "0.27" pyo3-async-runtimes = { version = "0.27", features = ["async-std-runtime"] } ``` ```rust no_run //! main.rs use pyo3::{prelude::*, types::PyType}; fn main() -> PyResult<()> { Python::initialize(); Python::attach(|py| { let uvloop = py.import("uvloop")?; uvloop.call_method0("install")?; // store a reference for the assertion let uvloop: Py = uvloop.into(); pyo3_async_runtimes::async_std::run(py, async move { // verify that we are on a uvloop.Loop Python::attach(|py| -> PyResult<()> { assert!(uvloop .bind(py) .getattr("Loop")? .cast::() .unwrap() .is_instance(&pyo3_async_runtimes::async_std::get_current_loop(py)?)?); Ok(()) })?; async_std::task::sleep(std::time::Duration::from_secs(1)).await; Ok(()) }) }) } ``` ### Additional Information - Managing event loop references can be tricky with `pyo3-async-runtimes`. See [Event Loop References and ContextVars](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/#event-loop-references-and-contextvars) in the API docs to get a better intuition for how event loop references are managed in this library. - Testing `pyo3-async-runtimes` libraries and applications requires a custom test harness since Python requires control over the main thread. You can find a testing guide in the [API docs for the `testing` module](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/testing/index.html) pyo3-async-runtimes-macros-0.27.0/src/lib.rs000064400000000000000000000244411046102023000167550ustar 00000000000000#![forbid(unsafe_code, future_incompatible, rust_2018_idioms)] #![deny(missing_debug_implementations, nonstandard_style)] #![recursion_limit = "512"] mod tokio; use proc_macro::TokenStream; use quote::{quote, quote_spanned}; use syn::spanned::Spanned; /// Enables an async main function that uses the async-std runtime. /// /// # Examples /// /// ```ignore /// #[pyo3_async_runtimes::async_std::main] /// async fn main() -> PyResult<()> { /// Ok(()) /// } /// ``` #[cfg(not(test))] // NOTE: exporting main breaks tests, we should file an issue. #[proc_macro_attribute] pub fn async_std_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::async_std::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 /// * `flavor` - selects the type of tokio runtime ["multi_thread", "current_thread"] /// * `worker_threads` - number of worker threads, defaults to the number of CPUs on the system /// /// # Examples /// /// Default configuration: /// ```ignore /// #[pyo3_async_runtimes::tokio::main] /// async fn main() -> PyResult<()> { /// Ok(()) /// } /// ``` /// /// Current-thread scheduler: /// ```ignore /// #[pyo3_async_runtimes::tokio::main(flavor = "current_thread")] /// async fn main() -> PyResult<()> { /// Ok(()) /// } /// ``` /// /// Multi-thread scheduler with custom worker thread count: /// ```ignore /// #[pyo3_async_runtimes::tokio::main(flavor = "multi_thread", worker_threads = 10)] /// async fn main() -> PyResult<()> { /// Ok(()) /// } /// ``` #[cfg(not(test))] // NOTE: exporting main breaks tests, we should file an issue. #[proc_macro_attribute] pub fn tokio_main(args: TokenStream, item: TokenStream) -> TokenStream { tokio::main(args, item, true) } /// 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 `#[async_std::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::async_std::test] /// async fn test_async_sleep() -> PyResult<()> { /// async_std::task::sleep(Duration::from_secs(1)).await; /// Ok(()) /// } /// /// // blocking test function /// #[pyo3_async_runtimes::async_std::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::async_std::test] /// fn test_blocking_sleep_with_event_loop(event_loop: Py) -> 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 async_std_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::async_std::re_exports::spawn_blocking(move || { #name() })) } } else { quote! { let event_loop = pyo3::Python::attach(|py| { pyo3_async_runtimes::async_std::get_current_loop(py).unwrap().into() }); Box::pin(pyo3_async_runtimes::async_std::re_exports::spawn_blocking(move || { #name(event_loop) })) } }; quote! { #vis fn #name() -> std::pin::Pin> + Send>> { #sig { #body } #task } } } else { quote! { #vis fn #name() -> std::pin::Pin> + 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 /// testing within an integration test. Like the `#[tokio::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::tokio::test] /// async fn test_async_sleep() -> PyResult<()> { /// tokio::time::sleep(Duration::from_secs(1)).await; /// Ok(()) /// } /// /// // blocking test function /// #[pyo3_async_runtimes::tokio::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::tokio::test] /// fn test_blocking_sleep_with_event_loop(event_loop: Py) -> 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 tokio_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(async move { match pyo3_async_runtimes::tokio::get_runtime().spawn_blocking(move || #name()).await { Ok(result) => result, Err(e) => { assert!(e.is_panic()); let panic = e.into_panic(); let panic_message = if let Some(s) = panic.downcast_ref::<&str>() { s.to_string() } else if let Some(s) = panic.downcast_ref::() { s.clone() } else { "unknown error".into() }; Err(pyo3_async_runtimes::err::RustPanic::new_err(format!("rust future panicked: {}", panic_message))) } } }) } } else { quote! { let event_loop = pyo3::Python::attach(|py| { pyo3_async_runtimes::tokio::get_current_loop(py).unwrap().into() }); Box::pin(async move { match pyo3_async_runtimes::tokio::get_runtime().spawn_blocking(move || #name(event_loop)).await { Ok(result) => result, Err(e) => { assert!(e.is_panic()); let panic = e.into_panic(); let panic_message = if let Some(s) = panic.downcast_ref::<&str>() { s.to_string() } else if let Some(s) = panic.downcast_ref::() { s.clone() } else { "unknown error".into() }; Err(pyo3_async_runtimes::err::RustPanic::new_err(format!("rust future panicked: {}", panic_message))) } } }) } }; quote! { #vis fn #name() -> std::pin::Pin> + Send>> { #sig { #body } #task } } } else { quote! { #vis fn #name() -> std::pin::Pin> + 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() } pyo3-async-runtimes-macros-0.27.0/src/tokio.rs000064400000000000000000000255611046102023000173400ustar 00000000000000use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; use syn::spanned::Spanned; #[derive(Clone, Copy, PartialEq)] enum RuntimeFlavor { CurrentThread, Threaded, } impl RuntimeFlavor { fn from_str(s: &str) -> Result { match s { "current_thread" => Ok(RuntimeFlavor::CurrentThread), "multi_thread" => Ok(RuntimeFlavor::Threaded), "single_thread" => Err("The single threaded runtime flavor is called `current_thread`.".to_string()), "basic_scheduler" => Err("The `basic_scheduler` runtime flavor has been renamed to `current_thread`.".to_string()), "threaded_scheduler" => Err("The `threaded_scheduler` runtime flavor has been renamed to `multi_thread`.".to_string()), _ => Err(format!("No such runtime flavor `{}`. The runtime flavors are `current_thread` and `multi_thread`.", s)), } } } struct FinalConfig { flavor: RuntimeFlavor, worker_threads: Option, } struct Configuration { rt_multi_thread_available: bool, default_flavor: RuntimeFlavor, flavor: Option, worker_threads: Option<(usize, Span)>, } impl Configuration { fn new(is_test: bool, rt_multi_thread: bool) -> Self { Configuration { rt_multi_thread_available: rt_multi_thread, default_flavor: match is_test { true => RuntimeFlavor::CurrentThread, false => RuntimeFlavor::Threaded, }, flavor: None, worker_threads: None, } } fn set_flavor(&mut self, runtime: syn::Lit, span: Span) -> Result<(), syn::Error> { if self.flavor.is_some() { return Err(syn::Error::new(span, "`flavor` set multiple times.")); } let runtime_str = parse_string(runtime, span, "flavor")?; let runtime = RuntimeFlavor::from_str(&runtime_str).map_err(|err| syn::Error::new(span, err))?; self.flavor = Some(runtime); Ok(()) } fn set_worker_threads( &mut self, worker_threads: syn::Lit, span: Span, ) -> Result<(), syn::Error> { if self.worker_threads.is_some() { return Err(syn::Error::new( span, "`worker_threads` set multiple times.", )); } let worker_threads = parse_int(worker_threads, span, "worker_threads")?; if worker_threads == 0 { return Err(syn::Error::new(span, "`worker_threads` may not be 0.")); } self.worker_threads = Some((worker_threads, span)); Ok(()) } fn build(&self) -> Result { let flavor = self.flavor.unwrap_or(self.default_flavor); use RuntimeFlavor::*; match (flavor, self.worker_threads) { (CurrentThread, Some((_, worker_threads_span))) => Err(syn::Error::new( worker_threads_span, "The `worker_threads` option requires the `multi_thread` runtime flavor.", )), (CurrentThread, None) => Ok(FinalConfig { flavor, worker_threads: None, }), (Threaded, worker_threads) if self.rt_multi_thread_available => Ok(FinalConfig { flavor, worker_threads: worker_threads.map(|(val, _span)| val), }), (Threaded, _) => { let msg = if self.flavor.is_none() { "The default runtime flavor is `multi_thread`, but the `rt-multi-thread` feature is disabled." } else { "The runtime flavor `multi_thread` requires the `rt-multi-thread` feature." }; Err(syn::Error::new(Span::call_site(), msg)) } } } } fn parse_int(int: syn::Lit, span: Span, field: &str) -> Result { match int { syn::Lit::Int(lit) => match lit.base10_parse::() { Ok(value) => Ok(value), Err(e) => Err(syn::Error::new( span, format!("Failed to parse {} as integer: {}", field, e), )), }, _ => Err(syn::Error::new( span, format!("Failed to parse {} as integer.", field), )), } } fn parse_string(int: syn::Lit, span: Span, field: &str) -> Result { match int { syn::Lit::Str(s) => Ok(s.value()), syn::Lit::Verbatim(s) => Ok(s.to_string()), _ => Err(syn::Error::new( span, format!("Failed to parse {} as string.", field), )), } } fn parse_knobs( input: syn::ItemFn, args: Vec, is_test: bool, rt_multi_thread: bool, ) -> Result { let sig = &input.sig; let ret = &input.sig.output; let body = &input.block; let attrs = &input.attrs; let vis = input.vis; if sig.asyncness.is_none() { let msg = "the async keyword is missing from the function declaration"; return Err(syn::Error::new_spanned(sig.fn_token, msg)); } let macro_name = "pyo3_async_runtimes::tokio::main"; let mut config = Configuration::new(is_test, rt_multi_thread); for arg in args { match arg { syn::Meta::NameValue(namevalue) => { let ident = namevalue.path.get_ident(); if ident.is_none() { let msg = "Must have specified ident"; return Err(syn::Error::new_spanned(namevalue, msg)); } match ident.unwrap().to_string().to_lowercase().as_str() { "worker_threads" => { if let syn::Expr::Lit(expr_lit) = &namevalue.value { config.set_worker_threads(expr_lit.lit.clone(), namevalue.span())?; } else { return Err(syn::Error::new_spanned( &namevalue.value, "Expected a literal value", )); } } "flavor" => { if let syn::Expr::Lit(expr_lit) = &namevalue.value { config.set_flavor(expr_lit.lit.clone(), namevalue.span())?; } else { return Err(syn::Error::new_spanned( &namevalue.value, "Expected a literal value", )); } } "core_threads" => { let msg = "Attribute `core_threads` is renamed to `worker_threads`"; return Err(syn::Error::new_spanned(namevalue, msg)); } name => { let msg = format!("Unknown attribute {} is specified; expected one of: `flavor`, `worker_threads`", name); return Err(syn::Error::new_spanned(namevalue, msg)); } } } syn::Meta::Path(path) => { let ident = path.get_ident(); if ident.is_none() { let msg = "Must have specified ident"; return Err(syn::Error::new_spanned(path, msg)); } let name = ident.unwrap().to_string().to_lowercase(); let msg = match name.as_str() { "threaded_scheduler" | "multi_thread" => { format!( "Set the runtime flavor with #[{}(flavor = \"multi_thread\")].", macro_name ) } "basic_scheduler" | "current_thread" | "single_threaded" => { format!( "Set the runtime flavor with #[{}(flavor = \"current_thread\")].", macro_name ) } "flavor" | "worker_threads" => { format!("The `{}` attribute requires an argument.", name) } name => { format!("Unknown attribute {} is specified; expected one of: `flavor`, `worker_threads`", name) } }; return Err(syn::Error::new_spanned(path, msg)); } other => { return Err(syn::Error::new_spanned( other, "Unknown attribute inside the macro", )); } } } let config = config.build()?; let builder = match config.flavor { RuntimeFlavor::CurrentThread => quote! { pyo3_async_runtimes::tokio::re_exports::runtime::Builder::new_current_thread() }, RuntimeFlavor::Threaded => quote! { pyo3_async_runtimes::tokio::re_exports::runtime::Builder::new_multi_thread() }, }; let mut builder_init = quote! { builder.enable_all(); }; if let Some(v) = config.worker_threads { builder_init = quote! { builder.worker_threads(#v); #builder_init; }; } let rt_init = match config.flavor { RuntimeFlavor::CurrentThread => quote! { std::thread::spawn(|| pyo3_async_runtimes::tokio::get_runtime().block_on( pyo3_async_runtimes::tokio::re_exports::pending::<()>() )); }, _ => quote! {}, }; let result = quote! { #(#attrs)* #vis fn main() { async fn main() #ret { #body } pyo3::Python::initialize(); let mut builder = #builder; #builder_init; pyo3_async_runtimes::tokio::init(builder); #rt_init pyo3::Python::attach(|py| { pyo3_async_runtimes::tokio::run(py, main()) .map_err(|e| { e.print_and_set_sys_last_vars(py); }) .unwrap(); }); } }; Ok(result.into()) } #[cfg(not(test))] // Work around for rust-lang/rust#62127 pub(crate) fn main(args: TokenStream, item: TokenStream, rt_multi_thread: bool) -> TokenStream { let input = syn::parse_macro_input!(item as syn::ItemFn); let args = syn::parse_macro_input!(args with syn::punctuated::Punctuated::::parse_terminated); let args: Vec = args.into_iter().collect(); if input.sig.ident == "main" && !input.sig.inputs.is_empty() { let msg = "the main function cannot accept arguments"; return syn::Error::new_spanned(&input.sig.ident, msg) .to_compile_error() .into(); } parse_knobs(input, args, false, rt_multi_thread).unwrap_or_else(|e| e.to_compile_error().into()) }