From b1a036ccf5ba744847f14f7cb630ed5993ed7d91 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 7 Aug 2025 15:10:38 -0500 Subject: [PATCH] Refactor `find_uv_bin` and add a better error message (#14182) Follows https://github.com/astral-sh/uv/pull/14181 Two goals here - Remove duplicated logic and make the search order clear - Resolve user confusion around the searched directories; we previously only displayed the last attempt, which we rarely expect to be relevant --- crates/uv/tests/it/common/mod.rs | 82 +++++++++++++++++++---------- crates/uv/tests/it/python_module.rs | 59 +++++++++++++++------ python/uv/_find_uv.py | 44 ++++++++-------- 3 files changed, 119 insertions(+), 66 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 6705716de..28cadfdfe 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -592,6 +592,59 @@ impl TestContext { filters.push((r"exit code: ".to_string(), "exit status: ".to_string())); } + for (version, executable) in &python_versions { + // Add filtering for the interpreter path + filters.extend( + Self::path_patterns(executable) + .into_iter() + .map(|pattern| (pattern.to_string(), format!("[PYTHON-{version}]"))), + ); + + // Add filtering for the bin directory of the base interpreter path + let bin_dir = if cfg!(windows) { + // On Windows, the Python executable is in the root, not the bin directory + executable + .canonicalize() + .unwrap() + .parent() + .unwrap() + .join("Scripts") + } else { + executable + .canonicalize() + .unwrap() + .parent() + .unwrap() + .to_path_buf() + }; + filters.extend( + Self::path_patterns(bin_dir) + .into_iter() + .map(|pattern| (pattern.to_string(), format!("[PYTHON-BIN-{version}]"))), + ); + + // And for the symlink we created in the test the Python path + filters.extend( + Self::path_patterns(python_dir.join(version.to_string())) + .into_iter() + .map(|pattern| { + ( + format!("{pattern}[a-zA-Z0-9]*"), + format!("[PYTHON-{version}]"), + ) + }), + ); + + // Add Python patch version filtering unless explicitly requested to ensure + // snapshots are patch version agnostic when it is not a part of the test. + if version.patch().is_none() { + filters.push(( + format!(r"({})\.\d+", regex::escape(version.to_string().as_str())), + "$1.[X]".to_string(), + )); + } + } + filters.extend( Self::path_patterns(&bin_dir) .into_iter() @@ -634,35 +687,6 @@ impl TestContext { )); filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string())); - for (version, executable) in &python_versions { - // Add filtering for the interpreter path - filters.extend( - Self::path_patterns(executable) - .into_iter() - .map(|pattern| (pattern.to_string(), format!("[PYTHON-{version}]"))), - ); - - // And for the symlink we created in the test the Python path - filters.extend( - Self::path_patterns(python_dir.join(version.to_string())) - .into_iter() - .map(|pattern| { - ( - format!("{pattern}[a-zA-Z0-9]*"), - format!("[PYTHON-{version}]"), - ) - }), - ); - - // Add Python patch version filtering unless explicitly requested to ensure - // snapshots are patch version agnostic when it is not a part of the test. - if version.patch().is_none() { - filters.push(( - format!(r"({})\.\d+", regex::escape(version.to_string().as_str())), - "$1.[X]".to_string(), - )); - } - } filters.extend( Self::path_patterns(&temp_dir) .into_iter() diff --git a/crates/uv/tests/it/python_module.rs b/crates/uv/tests/it/python_module.rs index 316dbe237..204a27aa3 100644 --- a/crates/uv/tests/it/python_module.rs +++ b/crates/uv/tests/it/python_module.rs @@ -25,7 +25,10 @@ fn find_uv_bin_venv() { .with_filtered_python_names() .with_filtered_virtualenv_bin() .with_filtered_exe_suffix() - .with_filter(user_scheme_bin_filter()); + .with_filter(user_scheme_bin_filter()) + // Target installs always use "bin" on all platforms. On Windows, + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Install in a virtual environment uv_snapshot!(context.filters(), context.pip_install() @@ -64,8 +67,8 @@ fn find_uv_bin_target() { .with_filtered_exe_suffix() .with_filter(user_scheme_bin_filter()) // Target installs always use "bin" on all platforms. On Windows, - // with_filtered_virtualenv_bin only filters "Scripts", not "bin" - .with_filter((r"[\\/]bin[\\/]".to_string(), "/[BIN]/".to_string())); + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Install in a target directory uv_snapshot!(context.filters(), context.pip_install() @@ -106,7 +109,10 @@ fn find_uv_bin_prefix() { .with_filtered_python_names() .with_filtered_virtualenv_bin() .with_filtered_exe_suffix() - .with_filter(user_scheme_bin_filter()); + .with_filter(user_scheme_bin_filter()) + // Target installs always use "bin" on all platforms. On Windows, + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Install in a prefix directory let prefix = context.temp_dir.child("prefix"); @@ -143,9 +149,13 @@ fn find_uv_bin_prefix() { ----- stderr ----- Traceback (most recent call last): File "", line 1, in - File "[TEMP_DIR]/prefix/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 37, in find_uv_bin - raise FileNotFoundError(path) - FileNotFoundError: [USER_SCHEME]/[BIN]/uv + File "[TEMP_DIR]/prefix/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 36, in find_uv_bin + raise UvNotFound( + uv._find_uv.UvNotFound: Could not find the uv binary in any of the following locations: + - [VENV]/[BIN] + - [PYTHON-BIN-3.12] + - [USER_SCHEME]/[BIN] + - [TEMP_DIR]/prefix/[PYTHON-LIB]/site-packages/[BIN] "# ); } @@ -156,7 +166,10 @@ fn find_uv_bin_base_prefix() { .with_filtered_python_names() .with_filtered_virtualenv_bin() .with_filtered_exe_suffix() - .with_filter(user_scheme_bin_filter()); + .with_filter(user_scheme_bin_filter()) + // Target installs always use "bin" on all platforms. On Windows, + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Test base prefix fallback by mutating sys.base_prefix // First, create a "base" environment with fake-uv installed @@ -204,7 +217,10 @@ fn find_uv_bin_in_ephemeral_environment() -> anyhow::Result<()> { .with_filtered_python_names() .with_filtered_virtualenv_bin() .with_filtered_exe_suffix() - .with_filter(user_scheme_bin_filter()); + .with_filter(user_scheme_bin_filter()) + // Target installs always use "bin" on all platforms. On Windows, + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Create a minimal pyproject.toml let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -237,9 +253,13 @@ fn find_uv_bin_in_ephemeral_environment() -> anyhow::Result<()> { + uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv) Traceback (most recent call last): File "", line 1, in - File "[CACHE_DIR]/archive-v0/[HASH]/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 37, in find_uv_bin - raise FileNotFoundError(path) - FileNotFoundError: [USER_SCHEME]/[BIN]/uv + File "[CACHE_DIR]/archive-v0/[HASH]/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 36, in find_uv_bin + raise UvNotFound( + uv._find_uv.UvNotFound: Could not find the uv binary in any of the following locations: + - [CACHE_DIR]/builds-v0/[TMP]/[BIN] + - [PYTHON-BIN-3.12] + - [USER_SCHEME]/[BIN] + - [CACHE_DIR]/archive-v0/[HASH]/[PYTHON-LIB]/site-packages/[BIN] "# ); @@ -252,7 +272,10 @@ fn find_uv_bin_in_parent_of_ephemeral_environment() -> anyhow::Result<()> { .with_filtered_python_names() .with_filtered_virtualenv_bin() .with_filtered_exe_suffix() - .with_filter(user_scheme_bin_filter()); + .with_filter(user_scheme_bin_filter()) + // Target installs always use "bin" on all platforms. On Windows, + // `with_filtered_virtualenv_bin` only filters "Scripts", not "bin" + .with_filter((r"[\\/]bin".to_string(), "/[BIN]".to_string())); // Add the fake-uv package as a dependency let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -295,9 +318,13 @@ fn find_uv_bin_in_parent_of_ephemeral_environment() -> anyhow::Result<()> { + sniffio==1.3.1 Traceback (most recent call last): File "", line 1, in - File "[SITE_PACKAGES]/uv/_find_uv.py", line 37, in find_uv_bin - raise FileNotFoundError(path) - FileNotFoundError: [USER_SCHEME]/[BIN]/uv + File "[SITE_PACKAGES]/uv/_find_uv.py", line 36, in find_uv_bin + raise UvNotFound( + uv._find_uv.UvNotFound: Could not find the uv binary in any of the following locations: + - [CACHE_DIR]/builds-v0/[TMP]/[BIN] + - [PYTHON-BIN-3.12] + - [USER_SCHEME]/[BIN] + - [SITE_PACKAGES]/[BIN] "# ); diff --git a/python/uv/_find_uv.py b/python/uv/_find_uv.py index 0c45aff1a..4da33d9e2 100644 --- a/python/uv/_find_uv.py +++ b/python/uv/_find_uv.py @@ -5,36 +5,38 @@ import sys import sysconfig +class UvNotFound(FileNotFoundError): ... + + def find_uv_bin() -> str: """Return the uv binary path.""" uv_exe = "uv" + sysconfig.get_config_var("EXE") - # Search in the scripts directory for the current prefix - path = os.path.join(sysconfig.get_path("scripts"), uv_exe) - if os.path.isfile(path): - return path + targets = [ + # The scripts directory for the current Python + sysconfig.get_path("scripts"), + # The scripts directory for the base prefix (if different) + sysconfig.get_path("scripts", vars={"base": sys.base_prefix}), + # The user scheme scripts directory, e.g., `~/.local/bin` + sysconfig.get_path("scripts", scheme=_user_scheme()), + # Adjacent to the package root, e.g. from, `pip install --target` + os.path.join(os.path.dirname(os.path.dirname(__file__)), "bin"), + ] - # If in a virtual environment, also search in the base prefix's scripts directory - if sys.prefix != sys.base_prefix: - path = os.path.join( - sysconfig.get_path("scripts", vars={"base": sys.base_prefix}), uv_exe - ) + seen = [] + for target in targets: + if target in seen: + continue + seen.append(target) + path = os.path.join(target, uv_exe) if os.path.isfile(path): return path - # Search in the user scheme scripts directory, e.g., `~/.local/bin` - path = os.path.join(sysconfig.get_path("scripts", scheme=_user_scheme()), uv_exe) - if os.path.isfile(path): - return path - - # Search in `bin` adjacent to package root (as created by `pip install --target`). - pkg_root = os.path.dirname(os.path.dirname(__file__)) - target_path = os.path.join(pkg_root, "bin", uv_exe) - if os.path.isfile(target_path): - return target_path - - raise FileNotFoundError(path) + raise UvNotFound( + f"Could not find the uv binary in any of the following locations:\n" + f"{os.linesep.join(f' - {target}' for target in seen)}\n" + ) def _user_scheme() -> str: