Add support for --prefix and --with installations in find_uv_bin (#14184)

Follows #14182

Adds support for the case described at
https://github.com/astral-sh/uv/issues/10194#issuecomment-2993544346

This also happens to fix both `--with` requirement test cases, which
should close https://github.com/tox-dev/pre-commit-uv/issues/70
This commit is contained in:
Zanie Blue
2025-08-07 16:48:07 -05:00
committed by GitHub
parent 554f06c595
commit 8968d783de
2 changed files with 70 additions and 42 deletions

View File

@@ -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 `<prefix>/Lib/site-packages/uv`
_join(_matching_parents(_module_path(), "Lib/site-packages/uv"), "Scripts")
if sys.platform == "win32"
# On Unix, with module path `<prefix>/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 `<target>/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")