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
48 changes: 48 additions & 0 deletions src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()],
Expand Down Expand Up @@ -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);
Expand Down
72 changes: 72 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] (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
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 - 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::<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,
Comment on lines +96 to +100
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent Rye from shadowing installed Python runners

This new rye insertion adds a second Python runner at priority 5, and in mixed projects (for example [tool.rye] + uv.lock or requirements.txt) it can cause command selection to prefer Rye even when Rye is not installed. check_conflicts can resolve to the installed tool, but main currently ignores that returned runner and re-runs select_runner across the full list, so Rye being added first can lead to rye is not installed instead of using the available runner.

Useful? React with 👍 / 👎.

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,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
);
}
}
Loading