From f394d717bf6c85bc4cb4bfcaef4fcc70393c15f8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:10:20 +0000 Subject: [PATCH 1/2] feat(python): add support for rye project detection and execution Added support for `rye`, a modern Python project management tool. The `run` CLI can now properly detect Rye projects (priority 5) via the `[tool.rye]` section in `pyproject.toml`. It correctly executes custom scripts defined in `[tool.rye.scripts]` via `rye run ` and runs native Rye built-in commands directly. Help texts and README.md were updated accordingly to reflect the addition. Co-authored-by: insign <1113045+insign@users.noreply.github.com> --- README.md | 2 +- src/cli.rs | 2 +- src/detectors/mod.rs | 42 ++++++++++++++++++++ src/detectors/python.rs | 87 ++++++++++++++++++++++++++++++++++++++++- src/main.rs | 14 +++---- 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b17177b..6883f88 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ cd src/components && run test # Finds package.json in parent dirs | **Monorepo** | nx → turbo → lerna | | **Node.js** | bun → pnpm → yarn → npm | | **Deno** | deno | -| **Python** | uv → poetry → pipenv → pip | +| **Python** | rye → uv → poetry → pipenv → pip | | **Rust** | cargo | | **PHP** | composer | | **Go** | task → go | diff --git a/src/cli.rs b/src/cli.rs index 7681ac3..498bdab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,7 @@ use clap::{Parser, Subcommand}; #[command(about = "Universal task runner for modern development", long_about = None)] #[command(after_help = "SUPPORTED RUNNERS: Node.js: bun, pnpm, yarn, npm - Python: uv, poetry, pipenv, pip + Python: rye, uv, poetry, pipenv, pip Rust: cargo PHP: composer Go: task, go diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index c252dd5..6fdf903 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -193,6 +193,29 @@ const PIP_BUILTINS: &[&str] = &[ "help", ]; +const RYE_BUILTINS: &[&str] = &[ + "add", + "build", + "config", + "fetch", + "fmt", + "init", + "install", + "lint", + "lock", + "make-req", + "pin", + "publish", + "remove", + "run", + "show", + "sync", + "test", + "tools", + "uninstall", + "version", +]; + /// Indicates if a command is supported by a runner #[derive(Debug, Clone, Copy, PartialEq)] pub enum CommandSupport { @@ -384,6 +407,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()], @@ -618,6 +648,18 @@ 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); + // Built-in + let cmd = runner.build_command("sync", &[]); + assert_eq!(cmd, vec!["rye", "sync"]); + + // Custom script + let cmd = runner.build_command("dev", &[]); + assert_eq!(cmd, vec!["rye", "run", "dev"]); + } + #[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..c12c5ec 100644 --- a/src/detectors/python.rs +++ b/src/detectors/python.rs @@ -44,6 +44,18 @@ impl CommandValidator for PythonValidator { } } + // Check [tool.rye.scripts] (Rye custom 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; + } + } + // Check [tool.poetry.scripts] (Poetry legacy style) if let Some(scripts) = toml_value .get("tool") @@ -64,13 +76,33 @@ impl CommandValidator for PythonValidator { } /// Detect Python package managers -/// Priority: UV (5) > Poetry (6) > Pipenv (7) > Pip (8) +/// Priority: Rye (5) > UV (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); + // Check for Rye (priority 5) + if has_pyproject { + if let Ok(content) = fs::read_to_string(&pyproject_path) { + if let Ok(toml_value) = toml::from_str::(&content) { + let is_rye = toml_value.get("tool").and_then(|t| t.get("rye")).is_some(); + + 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 { @@ -139,6 +171,25 @@ mod tests { use std::io::Write; use tempfile::tempdir; + #[test] + fn test_detect_rye() { + let dir = tempdir().unwrap(); + let mut file = File::create(dir.path().join("pyproject.toml")).unwrap(); + writeln!( + file, + r#" +[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].priority, 5); + } + #[test] fn test_detect_uv() { let dir = tempdir().unwrap(); @@ -205,6 +256,38 @@ mod tests { // Validator tests + #[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] +dev = "python main.py" +build = "echo 'building'" +"# + ) + .unwrap(); + + let validator = PythonValidator; + assert_eq!( + validator.supports_command(dir.path(), "dev"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir.path(), "build"), + CommandSupport::Supported + ); + assert_eq!( + validator.supports_command(dir.path(), "unknown"), + CommandSupport::Unknown + ); + } + #[test] fn test_python_validator_pep621_scripts() { let dir = tempdir().unwrap(); diff --git a/src/main.rs b/src/main.rs index ee23013..842f4a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,12 +96,7 @@ fn main() { }; // Search for runners - let search_result = search_runners( - ¤t_dir, - max_levels, - &ignore_list, - verbose, - ); + let search_result = search_runners(¤t_dir, max_levels, &ignore_list, verbose); // Prepare to inject custom commands // Filter empty commands @@ -115,7 +110,7 @@ fn main() { let has_valid_commands = valid_config_commands .as_ref() - .map_or(false, |c| !c.is_empty()); + .is_some_and(|c| !c.is_empty()); let (mut runners, working_dir) = match search_result { Ok(result) => result, @@ -135,7 +130,10 @@ fn main() { if let Some(valid_config_commands) = valid_config_commands { if !valid_config_commands.is_empty() { // Check if we already have a custom runner - if let Some(idx) = runners.iter().position(|r| r.ecosystem == Ecosystem::Custom) { + if let Some(idx) = runners + .iter() + .position(|r| r.ecosystem == Ecosystem::Custom) + { // Merge config commands into existing runner (local overrides global) let mut merged_commands = valid_config_commands.clone(); if let Some(existing_cmds) = &runners[idx].custom_commands { From 100c1837825c17458eb3d95ab1f54e25f87a4850 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:48:08 +0000 Subject: [PATCH 2/2] Close PR due to upstream sunset Closing the PR without merging per feedback: Rye is sunset upstream in favor of uv (which is already supported). Adding new first-class support for a deprecated tool is not the right direction. Co-authored-by: insign <1113045+insign@users.noreply.github.com> --- README.md | 2 +- src/cli.rs | 2 +- src/detectors/mod.rs | 42 -------------------- src/detectors/python.rs | 87 +---------------------------------------- src/main.rs | 14 ++++--- 5 files changed, 12 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 6883f88..b17177b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ cd src/components && run test # Finds package.json in parent dirs | **Monorepo** | nx → turbo → lerna | | **Node.js** | bun → pnpm → yarn → npm | | **Deno** | deno | -| **Python** | rye → uv → poetry → pipenv → pip | +| **Python** | uv → poetry → pipenv → pip | | **Rust** | cargo | | **PHP** | composer | | **Go** | task → go | diff --git a/src/cli.rs b/src/cli.rs index 498bdab..7681ac3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,7 @@ use clap::{Parser, Subcommand}; #[command(about = "Universal task runner for modern development", long_about = None)] #[command(after_help = "SUPPORTED RUNNERS: Node.js: bun, pnpm, yarn, npm - Python: rye, uv, poetry, pipenv, pip + Python: uv, poetry, pipenv, pip Rust: cargo PHP: composer Go: task, go diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index 6fdf903..c252dd5 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -193,29 +193,6 @@ const PIP_BUILTINS: &[&str] = &[ "help", ]; -const RYE_BUILTINS: &[&str] = &[ - "add", - "build", - "config", - "fetch", - "fmt", - "init", - "install", - "lint", - "lock", - "make-req", - "pin", - "publish", - "remove", - "run", - "show", - "sync", - "test", - "tools", - "uninstall", - "version", -]; - /// Indicates if a command is supported by a runner #[derive(Debug, Clone, Copy, PartialEq)] pub enum CommandSupport { @@ -407,13 +384,6 @@ 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()], @@ -648,18 +618,6 @@ 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); - // Built-in - let cmd = runner.build_command("sync", &[]); - assert_eq!(cmd, vec!["rye", "sync"]); - - // Custom script - let cmd = runner.build_command("dev", &[]); - assert_eq!(cmd, vec!["rye", "run", "dev"]); - } - #[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 c12c5ec..95e041d 100644 --- a/src/detectors/python.rs +++ b/src/detectors/python.rs @@ -44,18 +44,6 @@ impl CommandValidator for PythonValidator { } } - // Check [tool.rye.scripts] (Rye custom 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; - } - } - // Check [tool.poetry.scripts] (Poetry legacy style) if let Some(scripts) = toml_value .get("tool") @@ -76,33 +64,13 @@ impl CommandValidator for PythonValidator { } /// Detect Python package managers -/// Priority: Rye (5) > UV (5) > Poetry (6) > Pipenv (7) > Pip (8) +/// Priority: UV (5) > Poetry (6) > Pipenv (7) > Pip (8) pub fn detect(dir: &Path) -> Vec { let mut runners = Vec::new(); - let pyproject_path = dir.join("pyproject.toml"); - let has_pyproject = pyproject_path.exists(); + let has_pyproject = dir.join("pyproject.toml").exists(); let validator: Arc = Arc::new(PythonValidator); - // Check for Rye (priority 5) - if has_pyproject { - if let Ok(content) = fs::read_to_string(&pyproject_path) { - if let Ok(toml_value) = toml::from_str::(&content) { - let is_rye = toml_value.get("tool").and_then(|t| t.get("rye")).is_some(); - - 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 { @@ -171,25 +139,6 @@ mod tests { use std::io::Write; use tempfile::tempdir; - #[test] - fn test_detect_rye() { - let dir = tempdir().unwrap(); - let mut file = File::create(dir.path().join("pyproject.toml")).unwrap(); - writeln!( - file, - r#" -[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].priority, 5); - } - #[test] fn test_detect_uv() { let dir = tempdir().unwrap(); @@ -256,38 +205,6 @@ managed = true // Validator tests - #[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] -dev = "python main.py" -build = "echo 'building'" -"# - ) - .unwrap(); - - let validator = PythonValidator; - assert_eq!( - validator.supports_command(dir.path(), "dev"), - CommandSupport::Supported - ); - assert_eq!( - validator.supports_command(dir.path(), "build"), - CommandSupport::Supported - ); - assert_eq!( - validator.supports_command(dir.path(), "unknown"), - CommandSupport::Unknown - ); - } - #[test] fn test_python_validator_pep621_scripts() { let dir = tempdir().unwrap(); diff --git a/src/main.rs b/src/main.rs index 842f4a5..ee23013 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,7 +96,12 @@ fn main() { }; // Search for runners - let search_result = search_runners(¤t_dir, max_levels, &ignore_list, verbose); + let search_result = search_runners( + ¤t_dir, + max_levels, + &ignore_list, + verbose, + ); // Prepare to inject custom commands // Filter empty commands @@ -110,7 +115,7 @@ fn main() { let has_valid_commands = valid_config_commands .as_ref() - .is_some_and(|c| !c.is_empty()); + .map_or(false, |c| !c.is_empty()); let (mut runners, working_dir) = match search_result { Ok(result) => result, @@ -130,10 +135,7 @@ fn main() { if let Some(valid_config_commands) = valid_config_commands { if !valid_config_commands.is_empty() { // Check if we already have a custom runner - if let Some(idx) = runners - .iter() - .position(|r| r.ecosystem == Ecosystem::Custom) - { + if let Some(idx) = runners.iter().position(|r| r.ecosystem == Ecosystem::Custom) { // Merge config commands into existing runner (local overrides global) let mut merged_commands = valid_config_commands.clone(); if let Some(existing_cmds) = &runners[idx].custom_commands {