diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index c252dd5..697cb71 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -384,6 +384,7 @@ impl DetectedRunner { } // 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()], @@ -618,6 +619,13 @@ mod tests { assert_eq!(cmd, vec!["bun", "run", "foo"]); } + #[test] + fn test_build_command_rye() { + let runner = DetectedRunner::new("rye", "pyproject.toml", Ecosystem::Python, 5); + let cmd = runner.build_command("start", &[]); + assert_eq!(cmd, vec!["rye", "run", "start"]); + } + #[test] fn test_build_command_pip() { let runner = DetectedRunner::new("pip", "requirements.txt", Ecosystem::Python, 8); diff --git a/src/detectors/python.rs b/src/detectors/python.rs index 95e041d..cf09b14 100644 --- a/src/detectors/python.rs +++ b/src/detectors/python.rs @@ -56,6 +56,18 @@ impl CommandValidator for PythonValidator { } } + // Check [tool.rye.scripts] (Rye) + 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 @@ -64,13 +76,35 @@ impl CommandValidator for PythonValidator { } /// Detect Python package managers -/// Priority: UV (5) > Poetry (6) > Pipenv (7) > Pip (8) +/// Priority: UV (5) > Rye (5) > Poetry (6) > Pipenv (7) > Pip (8) pub fn detect(dir: &Path) -> Vec { let mut runners = Vec::new(); - let has_pyproject = dir.join("pyproject.toml").exists(); + let pyproject_path = dir.join("pyproject.toml"); + let has_pyproject = pyproject_path.exists(); let validator: Arc = Arc::new(PythonValidator); + // Determine if it's a Rye project by inspecting pyproject.toml + let mut is_rye = false; + if has_pyproject { + if let Ok(content) = fs::read_to_string(&pyproject_path) { + if let Ok(toml_value) = toml::from_str::(&content) { + is_rye = toml_value.get("tool").and_then(|t| t.get("rye")).is_some(); + } + } + } + + // Check for Rye (priority 5) + if is_rye { + 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 { @@ -150,6 +184,27 @@ mod tests { assert_eq!(runners[0].name, "uv"); } + #[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" + +[tool.rye] +managed = true +"# + ) + .unwrap(); + + let runners = detect(dir.path()); + assert_eq!(runners.len(), 1); + assert_eq!(runners[0].name, "rye"); + } + #[test] fn test_detect_poetry() { let dir = tempdir().unwrap(); @@ -239,6 +294,38 @@ serve = "example.server:run" ); } + #[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#" +[tool.rye] +managed = true + +[tool.rye.scripts] +build = "python build.py" +dev = "python -m http.server" +"# + ) + .unwrap(); + + let validator = PythonValidator; + assert_eq!( + validator.supports_command(dir.path(), "build"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir.path(), "dev"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir.path(), "unknown"), + CommandSupport::Unknown + ); + } + #[test] fn test_python_validator_poetry_scripts() { let dir = tempdir().unwrap();