use super::tools::ToolsConfig;
use std::path::Path;
pub const PYO3_EXTENSION_MODULE_FEATURE: &str = "extension-module";
pub const PYO3_EXTENSION_MODULE_PYO3_FEATURES: &[&str] = &["pyo3/extension-module", "pyo3/abi3-py310"];
pub fn extension_module_feature_line() -> String {
let values = PYO3_EXTENSION_MODULE_PYO3_FEATURES
.iter()
.map(|feature| format!("\"{feature}\""))
.collect::<Vec<_>>()
.join(", ");
format!("{PYO3_EXTENSION_MODULE_FEATURE} = [{values}]")
}
pub fn python_tool_runner(tools: &ToolsConfig) -> Option<&'static str> {
match tools.python_package_manager.as_deref()? {
"uv" => Some("uv run --frozen --only-dev"),
"poetry" => Some("poetry run"),
_ => None,
}
}
pub fn python_build_precondition_tool(tools: &ToolsConfig) -> &'static str {
match python_tool_runner(tools) {
Some(runner) => runner.split_whitespace().next().unwrap_or("maturin"),
None => "maturin",
}
}
pub fn run_through_python_package_manager(command: String, tools: &ToolsConfig) -> String {
match python_tool_runner(tools) {
Some(runner) => format!("{runner} {command}"),
None => command,
}
}
pub fn declared_extension_module_feature(manifest_path: &Path) -> Option<&'static str> {
let contents = std::fs::read_to_string(manifest_path).ok()?;
let manifest = toml::from_str::<toml::Value>(&contents).ok()?;
let declared = manifest
.get("features")
.and_then(toml::Value::as_table)
.is_some_and(|features| features.contains_key(PYO3_EXTENSION_MODULE_FEATURE));
declared.then_some(PYO3_EXTENSION_MODULE_FEATURE)
}
pub fn extension_module_feature_flag(manifest_path: &Path) -> String {
declared_extension_module_feature(manifest_path)
.map(|feature| format!(" --features {feature}"))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn tools_with(package_manager: Option<&str>) -> ToolsConfig {
ToolsConfig {
python_package_manager: package_manager.map(str::to_string),
..Default::default()
}
}
#[test]
fn unset_package_manager_leaves_the_command_untouched() {
let tools = tools_with(None);
assert_eq!(python_tool_runner(&tools), None);
assert_eq!(
run_through_python_package_manager("maturin develop".to_string(), &tools),
"maturin develop"
);
assert_eq!(python_build_precondition_tool(&tools), "maturin");
}
#[test]
fn configured_package_manager_runs_the_build_in_its_locked_environment() {
let tools = tools_with(Some("uv"));
assert_eq!(
run_through_python_package_manager("maturin develop".to_string(), &tools),
"uv run --frozen --only-dev maturin develop"
);
assert_eq!(python_build_precondition_tool(&tools), "uv");
}
#[test]
fn poetry_runs_the_build_through_poetry_run() {
let tools = tools_with(Some("poetry"));
assert_eq!(
run_through_python_package_manager("maturin develop".to_string(), &tools),
"poetry run maturin develop"
);
assert_eq!(python_build_precondition_tool(&tools), "poetry");
}
#[test]
fn pip_has_no_run_wrapper() {
let tools = tools_with(Some("pip"));
assert_eq!(python_tool_runner(&tools), None);
assert_eq!(
run_through_python_package_manager("maturin develop".to_string(), &tools),
"maturin develop"
);
assert_eq!(python_build_precondition_tool(&tools), "maturin");
}
#[test]
fn feature_line_matches_the_flag_the_build_passes() {
assert_eq!(
extension_module_feature_line(),
"extension-module = [\"pyo3/extension-module\", \"pyo3/abi3-py310\"]"
);
assert!(extension_module_feature_line().starts_with(PYO3_EXTENSION_MODULE_FEATURE));
}
#[test]
fn declared_feature_is_read_from_the_generated_manifest() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
format!(
"[package]\nname = \"sample-lib-py\"\nversion = \"0.1.0\"\n\n[features]\n{}\n",
extension_module_feature_line()
),
)
.unwrap();
assert_eq!(
declared_extension_module_feature(&manifest),
Some(PYO3_EXTENSION_MODULE_FEATURE)
);
assert_eq!(extension_module_feature_flag(&manifest), " --features extension-module");
}
#[test]
fn a_crate_without_the_feature_gets_no_flag() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(&manifest, "[package]\nname = \"sample-lib-py\"\nversion = \"0.1.0\"\n").unwrap();
assert_eq!(declared_extension_module_feature(&manifest), None);
assert_eq!(extension_module_feature_flag(&manifest), "");
}
#[test]
fn a_missing_manifest_gets_no_flag() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(extension_module_feature_flag(&dir.path().join("Cargo.toml")), "");
}
}