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
33 changes: 23 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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);
Expand Down
31 changes: 31 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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");
Expand Down
Loading