Skip to content
Closed
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
8 changes: 8 additions & 0 deletions src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ impl DetectedRunner {
"npm" => vec!["npm".to_string(), "run".to_string(), task.to_string()],

// Python ecosystem
"rye" => vec!["rye".to_string(), "run".to_string(), task.to_string()],
"uv" => vec!["uv".to_string(), "run".to_string(), task.to_string()],
"poetry" => vec!["poetry".to_string(), "run".to_string(), task.to_string()],
"pipenv" => vec!["pipenv".to_string(), "run".to_string(), task.to_string()],
Expand Down Expand Up @@ -369,6 +370,13 @@ mod tests {
assert_eq!(cmd, vec!["npm", "run", "test", "--coverage"]);
}

#[test]
fn test_build_command_rye() {
let runner = DetectedRunner::new("rye", "pyproject.toml", Ecosystem::Python, 5);
let cmd = runner.build_command("test", &[]);
assert_eq!(cmd, vec!["rye", "run", "test"]);
}

#[test]
fn test_build_command_cargo() {
let runner = DetectedRunner::new("cargo", "Cargo.toml", Ecosystem::Rust, 9);
Expand Down
90 changes: 90 additions & 0 deletions src/detectors/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ impl CommandValidator for PythonValidator {
}
}

// Check [tool.rye.scripts]
if let Some(scripts) = toml_value
.get("tool")
.and_then(|t| t.get("rye"))
.and_then(|p| p.get("scripts"))
.and_then(|s| s.as_table())
{
if scripts.contains_key(command) {
return CommandSupport::Supported;
}
}

// Python is extensible - uv run / poetry run can also execute
// commands from the virtual environment (pytest, mypy, etc.)
// So we return Unknown to allow fallback behavior
Expand All @@ -71,6 +83,28 @@ pub fn detect(dir: &Path) -> Vec<DetectedRunner> {
let has_pyproject = dir.join("pyproject.toml").exists();
let validator: Arc<dyn CommandValidator> = Arc::new(PythonValidator);

// Check for Rye (priority 5)
if has_pyproject {
// Check for [tool.rye] in pyproject.toml
if let Ok(content) = fs::read_to_string(dir.join("pyproject.toml")) {
if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
if toml_value
.get("tool")
.and_then(|t| t.get("rye"))
.is_some()
{
runners.push(DetectedRunner::with_validator(
"rye",
"pyproject.toml",
Ecosystem::Python,
5,
Arc::clone(&validator),
));
}
}
}
}

// Check for UV (priority 5)
let uv_lock = dir.join("uv.lock");
if uv_lock.exists() && has_pyproject {
Expand Down Expand Up @@ -337,4 +371,60 @@ myapp = "example:main"
CommandSupport::Unknown
);
}

#[test]
fn test_detect_rye() {
let dir = tempdir().unwrap();
let mut file = File::create(dir.path().join("pyproject.toml")).unwrap();
writeln!(
file,
r#"
[project]
name = "example"
version = "0.1.0"

[tool.rye]
managed = true
"#
)
.unwrap();

let runners = detect(dir.path());
assert_eq!(runners.len(), 1);
assert_eq!(runners[0].name, "rye");
assert_eq!(runners[0].detected_file, "pyproject.toml");
}

#[test]
fn test_python_validator_rye_scripts() {
let dir = tempdir().unwrap();
let mut file = File::create(dir.path().join("pyproject.toml")).unwrap();
writeln!(
file,
r#"
[project]
name = "example"
version = "0.1.0"

[tool.rye.scripts]
fmt = "ruff format"
lint = "ruff check"
"#
)
.unwrap();

let validator = PythonValidator;
assert_eq!(
validator.supports_command(dir.path(), "fmt"),
CommandSupport::Supported
);
assert_eq!(
validator.supports_command(dir.path(), "lint"),
CommandSupport::Supported
);
assert_eq!(
validator.supports_command(dir.path(), "unknown"),
CommandSupport::Unknown
);
}
}
Loading