mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
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
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 "<string>", line 1, in <module>
|
||||
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 "<string>", line 1, in <module>
|
||||
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 "<string>", line 1, in <module>
|
||||
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]
|
||||
"#
|
||||
);
|
||||
|
||||
|
||||
+23
-21
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user