Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document binary and library in a single package by entrypoint workaround #1565

Merged
merged 5 commits into from
Apr 26, 2023
Merged
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
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
46 changes: 46 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,55 @@
#!/usr/bin/env python3
import json
import os.path
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 alpine/musl, rust_args is empty so we skip all tests on musl
if len(rust_args) > 0:
# 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)
# On windows, cpython resolves while pypy doesn't.
# The script for cpython is actually a distinct file from the system interpreter for
# windows and mac
if platform.system() == "Linux":
assert os.path.samefile(rust_args[0], sys.executable), (
rust_args,
sys.executable,
os.path.realpath(rust_args[0]),
os.path.realpath(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(())
}