Skip to content
Merged
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 @@ -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()],
Expand Down Expand Up @@ -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);
Expand Down
91 changes: 89 additions & 2 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)
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;
Comment on lines +59 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict Rye script validation to the Rye runner

This validator is shared by all Python runners (uv, poetry, pipenv, pip, and rye), so adding [tool.rye.scripts] as universally Supported makes non-Rye runners claim support for Rye-only task definitions. select_runner prefers supported runners by priority, so a higher-priority non-Rye runner can be selected and then execute a command form it does not actually implement (e.g., uv run <rye-script>), producing runtime failures.

Useful? React with 👍 / 👎.

}
}

// 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 @@ -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<DetectedRunner> {
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<dyn CommandValidator> = 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::<toml::Value>(&content) {
is_rye = toml_value.get("tool").and_then(|t| t.get("rye")).is_some();
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 Narrow Rye detection to avoid uv migration conflicts

Detecting Rye whenever any [tool.rye] table exists causes mixed uv/Rye projects (common during migration) to be classified as both rye and uv, because uv.lock is still detected below. In that case check_conflicts treats Python tools as a lockfile conflict and can error when both binaries are installed, which is a regression from previously selecting uv-only projects. Please gate Rye detection on a stronger Rye-specific signal (for example Rye lockfiles or explicit managed flag semantics) so uv projects with leftover metadata do not hard-fail.

Useful? React with 👍 / 👎.

}
}
}

// 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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading