use super::*;
use crate::core::backend::{BuildConfig, BuildDependency};
use crate::core::config::python_build::PYO3_EXTENSION_MODULE_FEATURE;
use crate::core::config::{LangContext, ToolsConfig, build_defaults};
fn maturin_build_config() -> BuildConfig {
BuildConfig {
tool: "maturin",
crate_suffix: "-py",
build_dep: BuildDependency::None,
post_build: Vec::new(),
}
}
fn python_config(output_path: &str, package_manager: Option<&str>) -> crate::core::config::ResolvedCrateConfig {
let tools_section = package_manager
.map(|pm| format!("\n[workspace.tools]\npython_package_manager = \"{pm}\"\n"))
.unwrap_or_default();
let alef_cfg: crate::core::config::NewAlefConfig = toml::from_str(&format!(
r#"
[workspace]
languages = ["python"]
{tools_section}
[[crates]]
name = "sample-lib"
sources = ["src/lib.rs"]
[crates.output]
python = '{output_path}'
"#
))
.unwrap();
alef_cfg.resolve().unwrap().remove(0)
}
fn scaffolded_py_crate(dir: &std::path::Path) -> String {
let crate_dir = dir.join("crates").join("sample-lib-py");
std::fs::create_dir_all(&crate_dir).unwrap();
std::fs::write(
crate_dir.join("Cargo.toml"),
format!(
"[package]\nname = \"sample-lib-py\"\nversion = \"0.1.0\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n\
[features]\n{}\n",
crate::core::config::python_build::extension_module_feature_line()
),
)
.unwrap();
crate_dir.to_string_lossy().into_owned()
}
#[test]
fn python_build_command_runs_maturin_through_the_configured_package_manager() {
let dir = tempfile::tempdir().unwrap();
let crate_dir = scaffolded_py_crate(dir.path());
let config = python_config(&crate_dir, Some("uv"));
let command = build_command_for(Language::Python, &maturin_build_config(), &config, false);
assert!(
command.starts_with("uv run --frozen --only-dev maturin develop"),
"a configured python_package_manager must run maturin from its locked environment, \
not off PATH: {command}"
);
}
#[test]
fn python_build_command_is_unchanged_when_no_package_manager_is_configured() {
let dir = tempfile::tempdir().unwrap();
let crate_dir = dir.path().join("crates").join("sample-lib-py");
std::fs::create_dir_all(&crate_dir).unwrap();
let crate_dir = crate_dir.to_string_lossy().into_owned();
let config = python_config(&crate_dir, None);
let command = build_command_for(Language::Python, &maturin_build_config(), &config, false);
assert_eq!(
command,
format!("maturin develop --manifest-path {crate_dir}/Cargo.toml")
);
}
#[test]
fn python_build_command_carries_the_feature_the_generated_manifest_declares() {
let dir = tempfile::tempdir().unwrap();
let crate_dir = scaffolded_py_crate(dir.path());
let config = python_config(&crate_dir, None);
let command = build_command_for(Language::Python, &maturin_build_config(), &config, false);
let release = build_command_for(Language::Python, &maturin_build_config(), &config, true);
let expected_flag = format!("--features {PYO3_EXTENSION_MODULE_FEATURE}");
assert!(
command.contains(&expected_flag),
"the build must activate the feature the generated manifest defines: {command}"
);
assert!(
release.contains(&expected_flag),
"the release build shares the arm and must carry the same feature: {release}"
);
assert!(release.contains("--release"), "{release}");
}
#[test]
fn python_build_command_omits_features_for_a_crate_that_declares_none() {
let dir = tempfile::tempdir().unwrap();
let crate_dir = dir.path().join("crates").join("hand-written-py");
std::fs::create_dir_all(&crate_dir).unwrap();
std::fs::write(
crate_dir.join("Cargo.toml"),
"[package]\nname = \"hand-written-py\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let config = python_config(&crate_dir.to_string_lossy(), None);
let command = build_command_for(Language::Python, &maturin_build_config(), &config, false);
assert!(
!command.contains("--features"),
"a crate with no extension-module feature must not be handed one: {command}"
);
}
#[test]
fn python_build_command_resolves_the_binding_crate_without_an_explicit_output_path() {
let alef_cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "sample-lib"
sources = ["src/lib.rs"]
"#,
)
.unwrap();
let config = alef_cfg.resolve().unwrap().remove(0);
let command = build_command_for(Language::Python, &maturin_build_config(), &config, false);
assert!(
command.contains("--manifest-path crates/sample-lib-py/Cargo.toml"),
"the maturin arm must name the binding crate scaffold writes, not a root-relative \
`/Cargo.toml`: {command}"
);
}
#[test]
fn python_default_build_commands_honour_the_package_manager_and_the_feature() {
let tools = ToolsConfig {
python_package_manager: Some("uv".to_string()),
..Default::default()
};
let ctx = LangContext::default(&tools);
let config = build_defaults::default_build_config(Language::Python, "packages/python", "sample-lib", &ctx);
let build = config
.build
.expect("python has a default build command")
.commands()
.join(" ");
let release = config
.build_release
.expect("python has a default build_release command")
.commands()
.join(" ");
for command in [&build, &release] {
assert!(
command.starts_with("uv run --frozen --only-dev maturin develop"),
"the default python build must run through the configured package manager: {command}"
);
assert!(
command.contains(&format!("--features {PYO3_EXTENSION_MODULE_FEATURE}")),
"the default python build must activate the extension-module feature: {command}"
);
}
assert!(release.contains("--release"), "{release}");
assert_eq!(
config.precondition.as_deref(),
Some("command -v uv >/dev/null 2>&1"),
"readiness must check the tool that actually runs the build -- maturin lives in the \
locked environment and need not be on PATH at all"
);
}
#[test]
fn python_default_build_commands_stay_on_path_without_a_configured_package_manager() {
let tools = ToolsConfig::default();
let ctx = LangContext::default(&tools);
let config = build_defaults::default_build_config(Language::Python, "packages/python", "sample-lib", &ctx);
let build = config
.build
.expect("python has a default build command")
.commands()
.join(" ");
let expected = format!(
"maturin develop --manifest-path crates/sample-lib-py/Cargo.toml \
--features {PYO3_EXTENSION_MODULE_FEATURE}"
);
assert_eq!(build, expected);
assert_eq!(
config.precondition.as_deref(),
Some("command -v maturin >/dev/null 2>&1")
);
}