Skip to content

Commit

Permalink
Merge #1565
Browse files Browse the repository at this point in the history
1565: Document binary and library in a single package by entrypoint workaround r=messense a=konstin

Inspired by #368 (comment)

Preview: https://deploy-preview-1565--maturin-guide.netlify.app/bindings.html#both-binary-and-library

CC `@nanthony007`

Co-authored-by: konstin <[email protected]>
  • Loading branch information
bors[bot] and konstin authored Apr 16, 2023
2 parents 277dbcd + cfcc8e2 commit 86ef4ad
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 1 deletion.
36 changes: 36 additions & 0 deletions guide/src/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,42 @@ directory of a virtual environment) once installed.
> **Note**: Maturin _does not_ automatically detect `bin` bindings. You _must_
> specify them via either command line with `-b bin` or in `pyproject.toml`.
### Both binary and library?

Shipping both a binary and library would double the size of your wheel. Consider instead exposing a CLI function in the library and using a Python entrypoint:

```rust
#[pyfunction]
fn print_cli_args(py: Python) -> PyResult<()> {
// This one includes python and the name of the wrapper script itself, e.g.
// `["/home/ferris/.venv/bin/python", "/home/ferris/.venv/bin/print_cli_args", "a", "b", "c"]`
println!("{:?}", env::args().collect::<Vec<_>>());
// This one includes only the name of the wrapper script itself, e.g.
// `["/home/ferris/.venv/bin/print_cli_args", "a", "b", "c"])`
println!(
"{:?}",
py.import("sys")?
.getattr("argv")?
.extract::<Vec<String>>()?
);
Ok(())
}

#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(print_cli_args))?;

Ok(())
}
```

In pyproject.toml:

```toml
[project.scripts]
print_cli_args = "my_module:print_cli_args"
```

## `uniffi`

uniffi bindings use [uniffi-rs](https://mozilla.github.io/uniffi-rs/) to generate Python `ctypes` bindings
Expand Down
36 changes: 36 additions & 0 deletions test-crates/pyo3-mixed/check_installed/check_installed.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
#!/usr/bin/env python3
import json
import platform
import sys
from pathlib import Path
from subprocess import check_output

from boltons.strutils import slugify

import pyo3_mixed

assert pyo3_mixed.get_42() == 42
assert slugify("First post! Hi!!!!~1 ") == "first_post_hi_1"

script_name = "print_cli_args"
args = ["a", "b", "c"]
[rust_args, python_args] = check_output([script_name, *args], text=True).splitlines()
# The rust vec debug format is also valid json
rust_args = json.loads(rust_args)
python_args = json.loads(python_args)

# On linux we get sys.executable, windows resolve the path and mac os gives us a third
# path (
# {prefix}/Python.framework/Versions/3.10/Resources/Python.app/Contents/MacOS/Python
# vs
# {prefix}/Python.framework/Versions/3.10/bin/python3.10
# on cirrus ci)
# We also skip windows because cpython resolves while pypy doesn't
if platform.system() == "Linux":
assert rust_args[0] == sys.executable, (rust_args, sys.executable)

# Windows can't decide if it's with or without .exe, FreeBSB just doesn't work for some reason
if platform.system() in ["Darwin", "Linux"]:
# Unix venv layout (and hopefully also on more exotic platforms)
print_cli_args = str(Path(sys.prefix).joinpath("bin").joinpath(script_name))
assert rust_args[1] == print_cli_args, (rust_args, print_cli_args)
assert python_args[0] == print_cli_args, (python_args, print_cli_args)

# FreeBSB just doesn't work for some reason
if platform.system() in ["Darwin", "Linux", "Windows"]:
# Rust contains the python executable as first argument but python does not
assert rust_args[2:] == args, rust_args
assert python_args[1:] == args, python_args

print("SUCCESS")
2 changes: 1 addition & 1 deletion test-crates/pyo3-mixed/pyo3_mixed/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .pyo3_mixed import get_21, print_cli_args # noqa: F401
from .python_module.double import double
from .pyo3_mixed import get_21


def get_42() -> int:
Expand Down
1 change: 1 addition & 0 deletions test-crates/pyo3-mixed/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dependencies = ["boltons"]

[project.scripts]
get_42 = "pyo3_mixed:get_42"
print_cli_args = "pyo3_mixed:print_cli_args"
19 changes: 19 additions & 0 deletions test-crates/pyo3-mixed/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
use pyo3::prelude::*;
use std::env;

#[pyfunction]
fn get_21() -> usize {
21
}

/// Prints the CLI arguments, once from Rust's point of view and once from Python's point of view.
#[pyfunction]
fn print_cli_args(py: Python) -> PyResult<()> {
// This one includes Python and the name of the wrapper script itself, e.g.
// `["/home/ferris/.venv/bin/python", "/home/ferris/.venv/bin/print_cli_args", "a", "b", "c"]`
println!("{:?}", env::args().collect::<Vec<_>>());
// This one includes only the name of the wrapper script itself, e.g.
// `["/home/ferris/.venv/bin/print_cli_args", "a", "b", "c"])`
println!(
"{:?}",
py.import("sys")?
.getattr("argv")?
.extract::<Vec<String>>()?
);
Ok(())
}

#[pymodule]
fn pyo3_mixed(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(get_21))?;
m.add_wrapped(wrap_pyfunction!(print_cli_args))?;

Ok(())
}

0 comments on commit 86ef4ad

Please sign in to comment.