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
30 changes: 30 additions & 0 deletions src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"toolchain",
"uninstall",
"version",
];

/// Indicates if a command is supported by a runner
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CommandSupport {
Expand Down Expand Up @@ -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()],
Expand Down
90 changes: 88 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 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;
}
}

// 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,36 @@ 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<DetectedRunner> {
let mut runners = Vec::new();

let has_pyproject = dir.join("pyproject.toml").exists();
let pyproject = dir.join("pyproject.toml");
let has_pyproject = pyproject.exists();
let validator: Arc<dyn CommandValidator> = Arc::new(PythonValidator);

// Check for Rye (priority 5)
if has_pyproject {
if let Ok(content) = fs::read_to_string(&pyproject) {
if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
if toml_value.get("tool").and_then(|t| t.get("rye")).is_some() {
let lockfile = if dir.join("requirements.lock").exists() {
"requirements.lock"
} else {
"pyproject.toml"
};
runners.push(DetectedRunner::with_validator(
"rye",
lockfile,
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 @@ -139,6 +174,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].detected_file, "pyproject.toml");
}

#[test]
fn test_detect_uv() {
let dir = tempdir().unwrap();
Expand Down Expand Up @@ -239,6 +293,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]
fmt = "rye run black ."
serve = "python -m http.server"
"#
)
.unwrap();

let validator = PythonValidator;
assert_eq!(
validator.supports_command(dir.path(), "fmt"),
CommandSupport::Supported
);
assert_eq!(
validator.supports_command(dir.path(), "serve"),
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
14 changes: 6 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,7 @@ fn main() {
};

// Search for runners
let search_result = search_runners(
&current_dir,
max_levels,
&ignore_list,
verbose,
);
let search_result = search_runners(&current_dir, max_levels, &ignore_list, verbose);

// Prepare to inject custom commands
// Filter empty commands
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Loading