How can I test rust functions wrapped by pyo3 in rust, before building and installing in python?
I have functionality written in rust, which I am exposing to python via pyo3. I would like to test that the python functions are correctly exposed and handle python types correctly.
I already have tests in place to validate the actual functional implementation (in rust) and the end-to-end integration (in python).
How can I test the pyo3 python functions in rust?
Answer
Testing PyO3 Python functions from within Rust can be achieved by creating Rust unit tests that directly interact with Python through the PyO3 framework. This allows you to test the Python API you have exposed using PyO3, ensuring that Python types are correctly handled and the integration works as expected.
Here’s how you can approach testing Python functions exposed via PyO3 directly in Rust:
1. Set Up a #[cfg(test)]
Module in Rust
You can use PyO3's pyo3::types::Py
and pyo3::Python
to interact with Python objects and execute Python code from within Rust tests.
Example: Testing Python Functions Exposed by PyO3
Rust Side (PyO3 code)
Let’s assume you have a simple function exposed to Python that you want to test from Rust. Here’s a basic example of exposing a function to Python using PyO3:
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
/// A simple function that we want to expose to Python.
#[pyfunction]
fn add(a: usize, b: usize) -> usize {
a + b
}
/// Create a Python module exposing the `add` function.
#[pymodule]
fn mymodule(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add, py)?)?;
Ok(())
}
Unit Test in Rust with PyO3
In Rust, you can test the Python API by calling the Python functions exposed through PyO3 in a test module:
#[cfg(test)]
mod tests {
use super::*;
use pyo3::types::PyString;
use pyo3::types::PyDict;
use pyo3::Python;
/// A test to validate the `add` function exposed to Python.
#[test]
fn test_add_function() {
// Initialize Python interpreter
let gil = Python::acquire_gil();
let py = gil.python();
// Import the Python module
let mymodule = PyModule::new(py, "mymodule").unwrap();
// Add the function to the module
mymodule.add_function(wrap_pyfunction!(add, py).unwrap()).unwrap();
// Call the Python function directly using the module
let result: usize = mymodule.call1("add", (5, 3)).unwrap().extract(py).unwrap();
// Assert that the result is correct
assert_eq!(result, 8);
}
/// Another test case with Python objects (testing string handling).
#[test]
fn test_python_string() {
let gil = Python::acquire_gil();
let py = gil.python();
let result: String = PyString::new(py, "Hello, PyO3!")
.to_object(py)
.extract(py)
.unwrap();
assert_eq!(result, "Hello, PyO3!");
}
}
Key Points in the Test Code:
-
Acquire Python GIL: You need to acquire the Python Global Interpreter Lock (GIL) when interacting with Python objects. This is done using
Python::acquire_gil()
. -
Create Python Module and Functions: In the
test_add_function()
example, we usePyModule::new(py, "mymodule")
to create a module in Python and then add functions to that module usingmymodule.add_function(wrap_pyfunction!(add, py)?)
. -
Calling Python Functions: After the function is added to the module, you can call it using
call1()
(passing the arguments as a tuple) and then extract the result back into Rust usingextract(py)
. -
Testing Python Objects: The
test_python_string()
shows how to create a Pythonstr
and extract it back into a Rust string. -
Assertions: After the function is called and the result is extracted, use standard Rust assertions (
assert_eq!
) to verify the correctness of the result.
2. Running the Tests
To run the tests, you can use cargo test
as usual. PyO3 will set up the Python interpreter when tests are run, and the tests will interact with the Python code directly.
cargo test
3. Why Test Python Functions in Rust?
Testing the Python functions in Rust serves several purposes:
- Unit Test the Python Interface: Ensure that the Python interface behaves as expected when invoked from Rust.
- Catch Type Mismatches: Ensure that types like
str
,int
,list
, etc., are correctly handled and mapped between Rust and Python. - Check for Proper Error Handling: Verify that Python exceptions are correctly handled by your Rust code (i.e.,
PyResult
is used properly).
4. Considerations
- Testing Environment: Ensure that Python is available and correctly configured on your system. PyO3 relies on the Python interpreter being available.
- PyO3 and Python Version Compatibility: Ensure that the version of PyO3 you're using is compatible with the Python version you're testing against.
- Complex Types: For more complex objects (e.g., custom classes), ensure that you also test how those objects are serialized/deserialized correctly between Rust and Python.
By integrating these tests into your CI/CD pipeline, you can ensure that your Python functions behave as expected and that they work seamlessly with your Rust code.