diff --git a/README.md b/README.md index 7aee5b1..0915358 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ to have it output logging details of how it is performing its search. ### `py` (any version) 1. Use `${VIRTUAL_ENV}/bin/python` immediately if available -1. Use `.venv/bin/python` immediately if available +1. Use `.venv/bin/python` if available in the current working directory or any + of its parent directories 1. If the first argument is a file path ... 1. Check for a shebang 1. If shebang path starts with `/usr/bin/python`, `/usr/local/bin/python`, diff --git a/src/cli.rs b/src/cli.rs index 65b04ad..28d1a4d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -124,17 +124,23 @@ fn list_executables(executables: &HashMap) -> crate::Resu Ok(table.to_string() + "\n") } +fn relative_venv_path(add_default: bool) -> PathBuf { + let mut path = PathBuf::new(); + if add_default { + path.push(DEFAULT_VENV_DIR); + } + path.push("bin"); + path.push("python"); + path +} + /// Returns the path to the activated virtual environment's executable. /// /// A virtual environment is determined to be activated based on the /// existence of the `VIRTUAL_ENV` environment variable. fn venv_executable_path(venv_root: &str) -> PathBuf { - let mut path = PathBuf::new(); - path.push(venv_root); - path.push("bin"); - path.push("python"); + PathBuf::from(venv_root).join(relative_venv_path(false)) // XXX: Do a is_file() check first? - path } fn activated_venv() -> Option { @@ -145,20 +151,22 @@ fn activated_venv() -> Option { }) } -fn venv_in_dir() -> Option { - log::info!("Checking for a venv in {:?}", DEFAULT_VENV_DIR); - let venv_path = venv_executable_path(DEFAULT_VENV_DIR); - venv_path.exists().then(|| { - log::debug!( - "Virtual environment executable found in {}", - venv_path.display() - ); - venv_path +fn venv_path_search() -> Option { + let cwd = env::current_dir().unwrap(); + log::info!( + "Searching for a venv in {} and parent directories", + cwd.display() + ); + cwd.ancestors().find_map(|path| { + let venv_path = path.join(relative_venv_path(true)); + log::info!("Checking {}", venv_path.display()); + // bool::then_some() makes more sense, but still experimental. + venv_path.is_file().then(|| venv_path) }) } fn venv_executable() -> Option { - activated_venv().or_else(venv_in_dir) + activated_venv().or_else(venv_path_search) } // https://en.m.wikipedia.org/wiki/Shebang_(Unix) diff --git a/tests/cli_system_tests.rs b/tests/cli_system_tests.rs index a90d48b..9de2d18 100644 --- a/tests/cli_system_tests.rs +++ b/tests/cli_system_tests.rs @@ -1,5 +1,6 @@ mod common; +use std::env; use std::fs; use std::fs::File; use std::io::Write; @@ -156,7 +157,7 @@ fn from_main_activated_virtual_env() { #[test] #[serial] -fn from_main_default_venv_path() { +fn from_main_default_cwd_venv_path() { let _working_dir = common::CurrentDir::new(); let env_state = common::EnvState::new(); let mut expected = PathBuf::new(); @@ -168,7 +169,42 @@ fn from_main_default_venv_path() { match Action::from_main(&["/path/to/py".to_string()]) { Ok(Action::Execute { executable, .. }) => { - assert_eq!(executable, expected); + assert_eq!(executable, expected.canonicalize().unwrap()); + } + _ => panic!("No executable found in default virtual environment case"), + } + + // VIRTUAL_ENV gets ignored if any specific version is requested. + match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) { + Ok(Action::Execute { executable, .. }) => { + assert_eq!(executable, env_state.python37); + } + _ => panic!("No executable found in default virtual environment case"), + } +} + +#[test] +#[serial] +fn from_main_default_parent_venv_path() { + let working_dir = common::CurrentDir::new(); + let temp_dir = working_dir.dir.path().to_path_buf(); + let env_state = common::EnvState::new(); + let mut expected = temp_dir.clone(); + expected.push(cli::DEFAULT_VENV_DIR); + expected.push("bin"); + fs::create_dir_all(&expected).unwrap(); + expected.push("python"); + common::touch_file(expected.clone()); + + let subdir = temp_dir.join("subdir"); + fs::create_dir(&subdir).unwrap(); + env::set_current_dir(&subdir).unwrap(); + + // XXX Change working dir + + match Action::from_main(&["/path/to/py".to_string()]) { + Ok(Action::Execute { executable, .. }) => { + assert_eq!(executable, expected.canonicalize().unwrap()); } _ => panic!("No executable found in default virtual environment case"), }