From 980272afff3a1af9701c0d062b5563c9deb74d63 Mon Sep 17 00:00:00 2001 From: insign <1113045+insign@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:25:36 +0000 Subject: [PATCH] feat: Add support for Rye python project manager - Detect Rye projects via `[tool.rye]` in `pyproject.toml` - Validate scripts in `[tool.rye.scripts]` - Support Rye built-in commands (e.g., `sync`, `lint`, `fmt`) and `run` fallback - Priority 5 (same as UV) --- src/detectors/mod.rs | 48 +++++++++++++++++++++++++++ src/detectors/python.rs | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index c252dd5..b75f7e1 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -193,6 +193,28 @@ const PIP_BUILTINS: &[&str] = &[ "help", ]; +const RYE_BUILTINS: &[&str] = &[ + "add", + "remove", + "sync", + "pin", + "show", + "build", + "publish", + "fmt", + "lint", + "run", + "shell", + "init", + "install", + "uninstall", + "tools", + "self", + "config", + "version", + "help", +]; + /// Indicates if a command is supported by a runner #[derive(Debug, Clone, Copy, PartialEq)] pub enum CommandSupport { @@ -384,6 +406,13 @@ impl DetectedRunner { } // Python ecosystem + "rye" => { + if RYE_BUILTINS.contains(&task) { + vec!["rye".to_string(), task.to_string()] + } else { + 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()], @@ -630,6 +659,25 @@ mod tests { assert_eq!(cmd, vec!["python", "-m", "pytest"]); } + #[test] + fn test_build_command_rye() { + let runner = DetectedRunner::new("rye", "pyproject.toml", Ecosystem::Python, 5); + + // Built-in + let cmd = runner.build_command("sync", &[]); + assert_eq!(cmd, vec!["rye", "sync"]); + + let cmd = runner.build_command("add", &["requests".to_string()]); + assert_eq!(cmd, vec!["rye", "add", "requests"]); + + // Not built-in -> run + let cmd = runner.build_command("test", &[]); + assert_eq!(cmd, vec!["rye", "run", "test"]); + + let cmd = runner.build_command("serve", &[]); + assert_eq!(cmd, vec!["rye", "run", "serve"]); + } + #[test] fn test_build_command_cargo() { let runner = DetectedRunner::new("cargo", "Cargo.toml", Ecosystem::Rust, 9); diff --git a/src/detectors/python.rs b/src/detectors/python.rs index 95e041d..e20d3e0 100644 --- a/src/detectors/python.rs +++ b/src/detectors/python.rs @@ -56,6 +56,18 @@ impl CommandValidator for PythonValidator { } } + // Check [tool.rye.scripts] (Rye scripts) + if let Some(scripts) = toml_value + .get("tool") + .and_then(|t| t.get("rye")) + .and_then(|r| r.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 @@ -71,6 +83,28 @@ pub fn detect(dir: &Path) -> Vec { let has_pyproject = dir.join("pyproject.toml").exists(); let validator: Arc = Arc::new(PythonValidator); + // Check for Rye (priority 5 - same as UV) + if has_pyproject { + // Check content for [tool.rye] + if let Ok(content) = fs::read_to_string(dir.join("pyproject.toml")) { + if let Ok(toml_value) = toml::from_str::(&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 { @@ -337,4 +371,42 @@ 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 = "rye-project" +version = "0.1.0" + +[tool.rye] +managed = true + +[tool.rye.scripts] +server = "python manage.py runserver" +"# + ) + .unwrap(); + + // Rye also creates requirements.lock + File::create(dir.path().join("requirements.lock")).unwrap(); + + let runners = detect(dir.path()); + + let rye_runner = runners.iter().find(|r| r.name == "rye"); + assert!(rye_runner.is_some(), "Rye should be detected"); + assert_eq!(rye_runner.unwrap().priority, 5); + assert_eq!(rye_runner.unwrap().detected_file, "pyproject.toml"); + + // Test validator + let validator = PythonValidator; + assert_eq!( + validator.supports_command(dir.path(), "server"), + CommandSupport::Supported + ); + } }