Search code examples
pythonrustpackageffipyo3

How to structure a mixed Python Rust package with PyO3


I'm looking for Info on how to structure a Python package that wraps an extension module written in Rust, where both languages are mixed. I'm using pyO3 for FFI but can't seem to find an example on how to do this. To be specific: my rust library exposes a type that is later wrapped by a python class. Only the python class should be exposed for later users and the package should be structured, such that it can be pushed to PyPI.

For example:

On the rust side

#[pyclass]
pub struct Point {
    x: f64,
    y: f64 
}

#[pymethods]
impl Point {
    #[new]
    pub fn new(x: f64, y: f64) -> Self { Self{x, y} }
}

and on the python side

from ??? import Point

class Points:
    points: List[Point] 
    
    def __init__(self, points: List[Tuple[float, float]]):
        self.points = []
        for point in points:
            x, y = point
            self.points.append(Point(x, y))

I would be thankful for any Infos, Sources, Examples etc.!


Solution

  • I found a way to do this using Maturin. So, in case anyone else is trying to find out how to do this, here's one way.

    The project needs to have the following structure:

    my_project
    ├── Cargo.toml
    ├── my_project
    │   ├── __init__.py
    │   └── sum.py
    └── src
        └── lib.rs
    

    Cargo.toml can be:

    [package]
    name = "my_project"
    version = "0.1.0"
    edition = "2018"
    
    [lib]
    name = "my_project"
    crate-type = ["cdylib"]
    
    [dependencies.pyo3]
    version = "0.14.5"
    features = ["extension-module"]
    

    One example for lib.rs would be:

    use pyo3::prelude::*;
    
    #[pyfunction]
    fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
        Ok((a + b).to_string())
    }
    
    #[pymodule]
    fn my_project(_py: Python, m: &PyModule) -> PyResult<()> {
        m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
        Ok(())
    }
    

    Now in sum.py the function can be accessed (after using maturin develop during development, and when publishing automatically after maturin build):

    from .my_project import sum_as_string
    
    class Sum:
        sum: str
        
        def __init__(self, lhs: int, rhs: int):
            self.sum = sum_as_string(lhs, rhs)
    

    The _ init _.py file can for example only expose the Sum class:

    from .sum import Sum