Skip to content

Commit 70f0bd1

Browse files
committed
Introspection: add a CLI to generate stubs
Useful for setuptools-rust Use it also for the integration tests
1 parent e8566a9 commit 70f0bd1

File tree

5 files changed

+72
-138
lines changed

5 files changed

+72
-138
lines changed

newsfragments/5904.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`pyo3-introspection`: add a small CLI to generate stubs

noxfile.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,35 +1413,53 @@ def update_ui_tests(session: nox.Session):
14131413

14141414
@nox.session(name="test-introspection")
14151415
def test_introspection(session: nox.Session):
1416-
session.install("maturin")
1417-
session.install("ruff")
1418-
options = []
1419-
target = os.environ.get("CARGO_BUILD_TARGET")
1420-
if target is not None:
1421-
options += ("--target", target)
1422-
profile = os.environ.get("CARGO_BUILD_PROFILE")
1423-
if profile == "release":
1424-
options.append("--release")
1425-
session.run_always(
1426-
"maturin",
1427-
"develop",
1428-
"-m",
1429-
"./pytests/Cargo.toml",
1430-
"--features",
1431-
"experimental-async,experimental-inspect",
1432-
*options,
1433-
)
1434-
lib_file = session.run(
1435-
"python",
1436-
"-c",
1437-
"import pyo3_pytests; print(pyo3_pytests.pyo3_pytests.__file__)",
1438-
silent=True,
1439-
).strip()
1440-
_run_cargo_test(
1441-
session,
1442-
package="pyo3-introspection",
1443-
env={"PYO3_PYTEST_LIB_PATH": lib_file},
1444-
)
1416+
with tempfile.TemporaryDirectory() as stub_dir:
1417+
session.install("maturin")
1418+
session.install("ruff")
1419+
options = []
1420+
target = os.environ.get("CARGO_BUILD_TARGET")
1421+
if target is not None:
1422+
options += ("--target", target)
1423+
profile = os.environ.get("CARGO_BUILD_PROFILE")
1424+
if profile == "release":
1425+
options.append("--release")
1426+
_run(
1427+
session,
1428+
"maturin",
1429+
"develop",
1430+
"-m",
1431+
"./pytests/Cargo.toml",
1432+
"--features",
1433+
"experimental-async,experimental-inspect",
1434+
*options,
1435+
)
1436+
lib_file = session.run(
1437+
"python",
1438+
"-c",
1439+
"import pyo3_pytests; print(pyo3_pytests.pyo3_pytests.__file__)",
1440+
silent=True,
1441+
).strip()
1442+
_run_cargo(session, "run", "-p", "pyo3-introspection", "--", lib_file, stub_dir)
1443+
_run(session, "ruff", "format", stub_dir)
1444+
_ensure_directory_equals(Path(stub_dir), Path("pytests/stubs"))
1445+
1446+
1447+
def _ensure_directory_equals(expected_dir: Path, actual_dir: Path):
1448+
# Assert all expected files are in actual and are equals
1449+
for expected_file_path in expected_dir.rglob("*"):
1450+
file_path = expected_file_path.relative_to(expected_dir)
1451+
actual_file_path = actual_dir / file_path
1452+
assert actual_file_path.exists(), f"File {file_path} does not exist"
1453+
assert expected_file_path.read_text() == actual_file_path.read_text(), (
1454+
f"Content is different in {file_path}"
1455+
)
1456+
# Assert all actual files are expected
1457+
for actual_file_path in actual_dir.rglob("*"):
1458+
file_path = actual_file_path.relative_to(actual_dir)
1459+
expected_file_path = expected_dir / file_path
1460+
assert expected_file_path.exists(), (
1461+
f"File {file_path} exist even if not existed"
1462+
)
14451463

14461464

14471465
def _build_docs_for_ffi_check(session: nox.Session) -> None:

pyo3-introspection/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,5 @@ goblin = ">=0.9, <0.11"
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
1616

17-
[dev-dependencies]
18-
tempfile = "3.12.0"
19-
2017
[lints]
2118
workspace = true

pyo3-introspection/src/main.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Small CLI entry point to introspect a Python cdylib built using PyO3 and generate [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html).
2+
3+
use anyhow::{anyhow, Context, Result};
4+
use pyo3_introspection::{introspect_cdylib, module_stub_files};
5+
use std::path::Path;
6+
use std::{env, fs};
7+
8+
fn main() -> Result<()> {
9+
let [_, binary_path, output_path] = env::args().collect::<Vec<_>>().try_into().map_err(|_| anyhow!("pyo3-introspection takes two arguments, the path of the binary to introspect and the path of the directory to write the stub to"))?;
10+
let module = introspect_cdylib(&binary_path, "pyo3_pytests")
11+
.with_context(|| format!("Failed to introspect module {binary_path}"))?;
12+
let actual_stubs = module_stub_files(&module);
13+
for (path, module) in actual_stubs {
14+
let file_path = Path::new(&output_path).join(path);
15+
if let Some(parent) = file_path.parent() {
16+
fs::create_dir_all(parent).with_context(|| {
17+
format!("Failed to create output directory {}", file_path.display())
18+
})?;
19+
}
20+
fs::write(&file_path, module)
21+
.with_context(|| format!("Failed to write module {}", file_path.display()))?;
22+
}
23+
Ok(())
24+
}

pyo3-introspection/tests/test.rs

Lines changed: 0 additions & 106 deletions
This file was deleted.

0 commit comments

Comments
 (0)