Skip to content

Commit 7dec6fb

Browse files
committed
PyO3: Adds generate-stubs command
This builds the package in debug mode then tries to extract the generated stubs in a given target directory
1 parent d97bbd0 commit 7dec6fb

6 files changed

Lines changed: 144 additions & 11 deletions

File tree

src/commands/generate_stubs.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use anyhow::{Context, Result, ensure};
2+
use fs_err as fs;
3+
use maturin::{BuildOptions, BuildOrchestrator, CargoOptions, OutputOptions, PythonOptions};
4+
use std::io;
5+
use std::path::PathBuf;
6+
use tempfile::TempDir;
7+
use tracing::instrument;
8+
9+
#[instrument(skip_all)]
10+
pub fn generate_stubs(
11+
output: PathBuf,
12+
python_options: PythonOptions,
13+
cargo_options: CargoOptions,
14+
) -> Result<()> {
15+
let temporary_wheels_dir = TempDir::new()?;
16+
let build_context = BuildOptions {
17+
python: python_options,
18+
cargo: cargo_options,
19+
generate_stubs: true,
20+
output: OutputOptions {
21+
out: Some(temporary_wheels_dir.path().into()),
22+
..Default::default()
23+
},
24+
..Default::default()
25+
}
26+
.into_build_context()
27+
.build()?;
28+
29+
let orchestrator = BuildOrchestrator::new(&build_context);
30+
let wheels = orchestrator.build_wheels()?;
31+
let mut found_stubs = false;
32+
for wheel in wheels {
33+
let mut archive = zip::ZipArchive::new(fs::File::open(&wheel.0)?)
34+
.with_context(|| format!("Failed to open wheel {}", wheel.0.display()))?;
35+
for idx in 0..archive.len() {
36+
let mut entry = archive.by_index(idx)?;
37+
if entry.name().ends_with(".pyi") {
38+
let output_path = output.join(entry.name());
39+
if let Some(output_parent) = output_path.parent() {
40+
fs::create_dir_all(output_parent)?;
41+
}
42+
io::copy(&mut entry, &mut fs::File::create_new(&output_path)?).with_context(
43+
|| {
44+
format!(
45+
"Failed to copy {} from {} to {}",
46+
entry.name(),
47+
wheel.0.display(),
48+
output_path.display()
49+
)
50+
},
51+
)?;
52+
found_stubs = true;
53+
}
54+
}
55+
}
56+
ensure!(
57+
found_stubs,
58+
"No auto-generated stubs found for package {}",
59+
build_context.project.module_name
60+
);
61+
Ok(())
62+
}

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tracing::instrument;
1414

1515
pub(crate) mod build;
1616
pub(crate) mod develop;
17+
pub(crate) mod generate_stubs;
1718
pub(crate) mod pep517;
1819
#[cfg(feature = "upload")]
1920
pub(crate) mod publish;

src/main.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use clap::Parser;
1414
use maturin::GenerateJsonSchemaOptions;
1515
#[cfg(feature = "upload")]
1616
use maturin::PublishOpt;
17-
use maturin::{BuildOptions, DevelopOptions, TargetTriple};
17+
use maturin::{BuildOptions, CargoOptions, DevelopOptions, PythonOptions, TargetTriple};
1818
#[cfg(feature = "scaffolding")]
1919
use maturin::{GenerateProjectOptions, ci::GenerateCI};
2020
use std::env;
@@ -163,6 +163,19 @@ enum Command {
163163
#[arg(value_name = "FILE")]
164164
files: Vec<PathBuf>,
165165
},
166+
/// Autogenerate type stubs
167+
#[command(name = "generate-stubs")]
168+
GenerateStub {
169+
/// The directory to store the type stubs in
170+
#[arg(short, long)]
171+
out: PathBuf,
172+
/// Python and bindings options
173+
#[command(flatten)]
174+
python: PythonOptions,
175+
/// Cargo build options
176+
#[command(flatten)]
177+
cargo: CargoOptions,
178+
},
166179
/// Backend for the PEP 517 integration. Not for human consumption
167180
///
168181
/// The commands are meant to be called from the python PEP 517
@@ -254,6 +267,9 @@ fn run() -> Result<()> {
254267
Command::GenerateCI(generate_ci) => commands::generate_ci(generate_ci)?,
255268
#[cfg(feature = "upload")]
256269
Command::Upload { publish, files } => commands::upload(publish, files)?,
270+
Command::GenerateStub { out, python, cargo } => {
271+
commands::generate_stubs::generate_stubs(out, python, cargo)?
272+
}
257273
#[cfg(feature = "cli-completion")]
258274
Command::Completions { shell } => {
259275
commands::completions(shell, &mut Opt::command());

tests/cmd/maturin.stdout

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ packages
44
Usage: maturin[EXE] [OPTIONS] <COMMAND>
55

66
Commands:
7-
build Build the crate into python packages
8-
publish Build and publish the crate as python packages to pypi
9-
list-python Search and list the available python installations
10-
develop Install the crate as module in the current virtualenv
11-
sdist Build only a source distribution (sdist) without compiling
12-
init Create a new cargo project in an existing directory
13-
new Create a new cargo project
14-
generate-ci Generate CI configuration
15-
upload Upload python packages to pypi
16-
help Print this message or the help of the given subcommand(s)
7+
build Build the crate into python packages
8+
publish Build and publish the crate as python packages to pypi
9+
list-python Search and list the available python installations
10+
develop Install the crate as module in the current virtualenv
11+
sdist Build only a source distribution (sdist) without compiling
12+
init Create a new cargo project in an existing directory
13+
new Create a new cargo project
14+
generate-ci Generate CI configuration
15+
upload Upload python packages to pypi
16+
generate-stubs Autogenerate type stubs
17+
help Print this message or the help of the given subcommand(s)
1718

1819
Options:
1920
-v, --verbose...

tests/common/other.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use pretty_assertions::assert_eq;
1212
use std::collections::BTreeSet;
1313
use std::io::Read;
1414
use std::path::{Path, PathBuf};
15+
use std::process::Command;
1516
use tar::Archive;
1617
use time::PrimitiveDateTime;
18+
use walkdir::WalkDir;
1719
use zip::ZipArchive;
1820

1921
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
@@ -626,3 +628,42 @@ pub fn test_build_wheels_from_sdist(package: impl AsRef<Path>, unique_name: &str
626628

627629
Ok(())
628630
}
631+
632+
pub fn generate_stubs(
633+
package: impl AsRef<Path>,
634+
unique_name: &str,
635+
expected_files: &[&str],
636+
) -> Result<()> {
637+
let package = package.as_ref();
638+
let output_dir = tempfile::tempdir()?;
639+
assert!(
640+
Command::new(env!("CARGO_BIN_EXE_maturin"))
641+
.arg("generate-stubs")
642+
.arg("-m")
643+
.arg(package.join("Cargo.toml"))
644+
.arg("--target-dir")
645+
.arg(format!("test-crates/targets/{unique_name}"))
646+
.arg("--out")
647+
.arg(output_dir.path())
648+
.status()?
649+
.success()
650+
);
651+
let found_files = WalkDir::new(output_dir.path())
652+
.sort_by_file_name()
653+
.into_iter()
654+
.filter(|e| match e {
655+
Ok(e) => e.file_type().is_file(),
656+
Err(_) => true,
657+
})
658+
.map(|e| {
659+
Ok(e?
660+
.path()
661+
.strip_prefix(output_dir.path())?
662+
.to_str()
663+
.unwrap()
664+
.replace('\\', '/'))
665+
})
666+
.collect::<Result<Vec<_>>>()?;
667+
assert_eq!(found_files, expected_files);
668+
Ok(())
669+
}

tests/run/integration.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,15 @@ fn abi3_without_version() {
164164
fn abi3_python_interpreter_args() {
165165
handle_result(other::abi3_python_interpreter_args());
166166
}
167+
168+
#[test]
169+
fn abi3_generate_stubs() {
170+
handle_result(other::generate_stubs(
171+
"test-crates/pyo3-stub-generation",
172+
"integration-pyo3-stub-generation-generate-stubs",
173+
&[
174+
"pyo3_stub_generation/__init__.pyi",
175+
"pyo3_stub_generation/submodule.pyi",
176+
],
177+
));
178+
}

0 commit comments

Comments
 (0)