From 8e37625005acbf7683ccf63bd5d93b422be72b96 Mon Sep 17 00:00:00 2001 From: Paul Moore Date: Tue, 16 Apr 2024 19:39:37 +0100 Subject: [PATCH] Allow passing a venv to `uv pip --python` (#3064) Fixes https://github.com/astral-sh/uv/issues/3060 ## Summary Allows passing a virtual environment (the path to the directory, rather than the path to the Python interpreter within the directory) to the `--python` option of the `uv pip` command. ## Test Plan Tested manually to confirm that the expected new functionality works. The test suite still passes after this change. I don't know how to add tests for a new feature like this. I would be happy to do so if someone can give me some pointers on how to do it. --- crates/uv-interpreter/src/find_python.rs | 70 +++++++++++++++--------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/crates/uv-interpreter/src/find_python.rs b/crates/uv-interpreter/src/find_python.rs index 0acc5b557..c24b58b7b 100644 --- a/crates/uv-interpreter/src/find_python.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -43,16 +43,35 @@ pub fn find_requested_python(request: &str, cache: &Cache) -> Result unreachable!(), }; find_python(selector, cache) - } else if !request.contains(std::path::MAIN_SEPARATOR) { - // `-p python3.10`; Generally not used on windows because all Python are `python.exe`. - let Some(executable) = find_executable(request)? else { - return Ok(None); - }; - Interpreter::query(executable, cache).map(Some) } else { - // `-p /home/ferris/.local/bin/python3.10` - let executable = uv_fs::absolutize_path(request.as_ref())?; - Interpreter::query(executable, cache).map(Some) + match fs_err::metadata(request) { + Ok(metadata) => { + // Map from user-provided path to an executable. + let path = uv_fs::absolutize_path(request.as_ref())?; + let executable = if metadata.is_dir() { + // If the user provided a directory, assume it's a virtual environment. + // `-p /home/ferris/.venv` + if cfg!(windows) { + Cow::Owned(path.join("Scripts/python.exe")) + } else { + Cow::Owned(path.join("bin/python")) + } + } else { + // Otherwise, assume it's a Python executable. + // `-p /home/ferris/.local/bin/python3.10` + path + }; + Interpreter::query(executable, cache).map(Some) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // `-p python3.10`; Generally not used on windows because all Python are `python.exe`. + let Some(executable) = find_executable(request)? else { + return Ok(None); + }; + Interpreter::query(executable, cache).map(Some) + } + Err(err) => return Err(err.into()), + } } } @@ -713,19 +732,15 @@ mod windows { #[cfg_attr(not(windows), ignore)] fn no_such_python_path() { let result = - find_requested_python(r"C:\does\not\exists\python3.12", &Cache::temp().unwrap()); - insta::with_settings!({ - filters => vec![ - // The exact message is host language dependent - (r"Caused by: .* \(os error 3\)", "Caused by: The system cannot find the path specified. (os error 3)") - ] - }, { - assert_snapshot!( - format_err(result), @r###" - failed to canonicalize path `C:\does\not\exists\python3.12` - Caused by: The system cannot find the path specified. (os error 3) - "###); - }); + find_requested_python(r"C:\does\not\exists\python3.12", &Cache::temp().unwrap()) + .unwrap() + .ok_or(Error::RequestedPythonNotFound( + r"C:\does\not\exists\python3.12".to_string(), + )); + assert_snapshot!( + format_err(result), + @"Failed to locate Python interpreter at `C:\\does\\not\\exists\\python3.12`" + ); } } } @@ -775,11 +790,12 @@ mod tests { #[test] #[cfg_attr(not(unix), ignore)] fn no_such_python_path() { - let result = find_requested_python("/does/not/exists/python3.12", &Cache::temp().unwrap()); + let result = find_requested_python("/does/not/exists/python3.12", &Cache::temp().unwrap()) + .unwrap() + .ok_or(Error::RequestedPythonNotFound( + "/does/not/exists/python3.12".to_string(), + )); assert_snapshot!( - format_err(result), @r###" - failed to canonicalize path `/does/not/exists/python3.12` - Caused by: No such file or directory (os error 2) - "###); + format_err(result), @"Failed to locate Python interpreter at `/does/not/exists/python3.12`"); } }