From 6f98bbe916586afca54b0c979b7ae24947f7c3b1 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Fri, 27 Mar 2026 20:13:36 +0100 Subject: [PATCH] feat(venv): add upward directory traversal for .venv discovery This pull request enhances the anchor command to search for virtual environments by traversing up the directory tree, rather than only checking the current directory. The main changes improve usability when working in project subdirectories. **Virtual environment discovery enhancement:** * Added a new `find_venv_upward` function in `src/main.rs` that walks from the current directory up to the filesystem root looking for `.venv/pyvenv.cfg`, enabling anchor to work from any subdirectory within a project. * Refactored the `anchor` function to use `find_venv_upward` instead of only checking the current directory, with improved error messaging that indicates the traversal behavior. **Testing improvements:** * Added a new integration test `anchor_finds_venv_in_parent_directory` in `tests/integration.rs` that verifies the anchor command correctly finds `.venv` when executed from nested subdirectories and sets VIRTUAL_ENV to the correct root path. * Updated existing test assertions to verify the new error message mentions "or any parent directory" when no .venv is found. * Added cleanup of temporary directories in existing tests to prevent resource leaks. --- src/main.rs | 33 +++++++++++++++++++++++---------- tests/integration.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 220d7f5..cc0d196 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,17 +193,30 @@ fn activate_venv(venv_path: &PathBuf) { } } -fn anchor(shell_override: Option<&str>) { - let venv_path = get_venv_path(); - let cfg_path = venv_path.join("pyvenv.cfg"); - - if !venv_path.is_dir() || !cfg_path.exists() { - eprintln!( - "uv-shell anchor: no .venv found in {}", - venv_path.parent().unwrap_or(&venv_path).display() - ); - return; +/// Walk from `start` up to the filesystem root looking for `.venv/pyvenv.cfg`. +fn find_venv_upward(start: &std::path::Path) -> Option { + let mut dir = start; + loop { + let candidate = dir.join(".venv"); + if candidate.is_dir() && candidate.join("pyvenv.cfg").exists() { + return Some(candidate); + } + match dir.parent() { + Some(parent) => dir = parent, + None => return None, + } } +} + +fn anchor(shell_override: Option<&str>) { + let cwd = env::current_dir().expect("failed to get current directory"); + let venv_path = match find_venv_upward(&cwd) { + Some(p) => p, + None => { + eprintln!("uv-shell anchor: no .venv found in {} or any parent directory", cwd.display()); + return; + } + }; let bin_dir = get_bin_dir(); let venv_bin = venv_path.join(bin_dir); diff --git a/tests/integration.rs b/tests/integration.rs index 762bde2..a5cbdfc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -76,6 +76,10 @@ fn anchor_no_venv_reports_on_stderr() { stderr.contains("no .venv found"), "anchor should explain why it produced no output, got: {stderr}" ); + assert!( + stderr.contains("or any parent directory"), + "anchor should mention traversal in message, got: {stderr}" + ); let _ = fs::remove_dir_all(&dir); } @@ -97,6 +101,33 @@ fn anchor_prints_exports_bash() { let _ = fs::remove_dir_all(&dir); } +#[test] +fn anchor_finds_venv_in_parent_directory() { + // project root has .venv; anchor is run from a subdirectory + let dir = tmpdir_with_venv("anchor-traversal"); + let subdir = dir.join("src/utils"); + fs::create_dir_all(&subdir).unwrap(); + + let out = Command::new(bin()) + .args(["anchor", "--shell", "bash"]) + .current_dir(&subdir) + .output() + .unwrap(); + assert!(out.status.success()); + + let stdout = String::from_utf8_lossy(&out.stdout); + assert!( + stdout.contains("export VIRTUAL_ENV=\""), + "anchor should find .venv two levels up, got:\n{stdout}" + ); + // The VIRTUAL_ENV path should point to the root .venv, not a subdir .venv + assert!( + stdout.contains(dir.join(".venv").to_str().unwrap()), + "VIRTUAL_ENV should point to root .venv" + ); + let _ = fs::remove_dir_all(&dir); +} + #[test] fn anchor_creates_activated_marker() { let dir = tmpdir_with_venv("anchor-marker");