diff --git a/crates/uv/tests/it/python_module.rs b/crates/uv/tests/it/python_module.rs index 204a27aa3..16efc5deb 100644 --- a/crates/uv/tests/it/python_module.rs +++ b/crates/uv/tests/it/python_module.rs @@ -141,22 +141,14 @@ fn find_uv_bin_prefix() { .env( EnvVars::PYTHONPATH, site_packages_path(&context.temp_dir.join("prefix"), "python3.12"), - ), @r#" - success: false - exit_code: 1 + ), @r" + success: true + exit_code: 0 ----- stdout ----- + [TEMP_DIR]/prefix/[BIN]/uv ----- stderr ----- - Traceback (most recent call last): - File "", line 1, in - 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] - "# + " ); } @@ -239,10 +231,11 @@ fn find_uv_bin_in_ephemeral_environment() -> anyhow::Result<()> { .arg(context.workspace_root.join("scripts/packages/fake-uv")) .arg("python") .arg("-c") - .arg("import uv; print(uv.find_uv_bin())"), @r#" - success: false - exit_code: 1 + .arg("import uv; print(uv.find_uv_bin())"), @r" + success: true + exit_code: 0 ----- stdout ----- + [CACHE_DIR]/archive-v0/[HASH]/[BIN]/uv ----- stderr ----- Resolved 1 package in [TIME] @@ -251,16 +244,7 @@ fn find_uv_bin_in_ephemeral_environment() -> anyhow::Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + 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 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] - "# + " ); Ok(()) @@ -300,10 +284,11 @@ fn find_uv_bin_in_parent_of_ephemeral_environment() -> anyhow::Result<()> { .arg("python") .arg("-c") .arg("import uv; print(uv.find_uv_bin())"), - @r#" - success: false - exit_code: 1 + @r" + success: true + exit_code: 0 ----- stdout ----- + [VENV]/[BIN]/uv ----- stderr ----- Resolved 2 packages in [TIME] @@ -316,16 +301,7 @@ fn find_uv_bin_in_parent_of_ephemeral_environment() -> anyhow::Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - Traceback (most recent call last): - File "", line 1, in - 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] - "# + " ); Ok(()) diff --git a/python/uv/_find_uv.py b/python/uv/_find_uv.py index 4da33d9e2..e8b812863 100644 --- a/python/uv/_find_uv.py +++ b/python/uv/_find_uv.py @@ -16,16 +16,29 @@ def find_uv_bin() -> str: targets = [ # The scripts directory for the current Python sysconfig.get_path("scripts"), - # The scripts directory for the base prefix (if different) + # The scripts directory for the base prefix 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"), + # Above the package root, e.g., from `pip install --prefix` or `uv run --with` + ( + # On Windows, with module path `/Lib/site-packages/uv` + _join(_matching_parents(_module_path(), "Lib/site-packages/uv"), "Scripts") + if sys.platform == "win32" + # On Unix, with module path `/lib/python3.13/site-packages/uv` + else _join( + _matching_parents(_module_path(), "lib/python*/site-packages/uv"), "bin" + ) + ), + # Adjacent to the package root, e.g., from `pip install --target` + # with module path `/uv` + _join(_matching_parents(_module_path(), "uv"), "bin"), ] seen = [] for target in targets: + if not target: + continue if target in seen: continue seen.append(target) @@ -39,6 +52,45 @@ def find_uv_bin() -> str: ) +def _module_path() -> str | None: + path = os.path.dirname(__file__) + return path + + +def _matching_parents(path: str | None, match: str) -> str | None: + """ + Return the parent directory of `path` after trimming a `match` from the end. + The match is expected to contain `/` as a path separator, while the `path` + is expected to use the platform's path separator (e.g., `os.sep`). The path + components are compared case-insensitively and a `*` wildcard can be used + in the `match`. + """ + from fnmatch import fnmatch + + if not path: + return None + parts = path.split(os.sep) + match_parts = match.split("/") + if len(parts) < len(match_parts): + return None + + if not all( + fnmatch(part, match_part) + for part, match_part in zip( + reversed(parts), reversed(match_parts), strict=False + ) + ): + return None + + return os.sep.join(parts[: -len(match_parts)]) + + +def _join(path: str | None, *parts: str) -> str | None: + if not path: + return None + return os.path.join(path, *parts) + + def _user_scheme() -> str: if sys.version_info >= (3, 10): user_scheme = sysconfig.get_preferred_scheme("user")