Skip to content

Commit d537f03

Browse files
authored
Update the Python module (notably find_ruff_bin) for parity with uv (astral-sh#23406)
Closes astral-sh/uv#14874 Closes astral-sh#23402 uv has fairly extensive test coverage for this functionality but it seems challenging to copy it over My smoke test strategy was to ask an LLM to build the wheel and test all of the cases ``` $ uv build --wheel Building wheel... Successfully built dist/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl $ WHEEL=dist/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl $ uv venv -q .smoke-venv && uv pip install -q --python .smoke-venv $WHEEL $ .smoke-venv/bin/python -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-venv/bin/ruff $ .smoke-venv/bin/python -m ruff version ruff 0.15.1+81 (1e42d4f 2026-02-18) $ uv run --no-project --with $WHEEL -- python -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/.cache/uv/archive-v0/zf7_vNji2jmEGEDox-9Vj/bin/ruff $ uv run --no-project --with $WHEEL -- python -m ruff version ruff 0.15.1+81 (1e42d4f 2026-02-18) $ uv pip install --target .smoke-target $WHEEL $ PYTHONPATH=.smoke-target python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-target/bin/ruff $ uv pip install --prefix .smoke-prefix $WHEEL $ PYTHONPATH=.smoke-prefix/lib/python3.14/site-packages python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-prefix/bin/ruff $ python3 -m pip install --user --break-system-packages $WHEEL $ python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/Library/Python/3.13/bin/ruff $ python3 -m ruff version ruff 0.15.1+81 (1e42d4f 2026-02-18) ```
1 parent 333ad91 commit d537f03

3 files changed

Lines changed: 122 additions & 74 deletions

File tree

python/ruff/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import annotations
2+
3+
from ._find_ruff import find_ruff_bin
4+
5+
__all__ = ["find_ruff_bin"]

python/ruff/__main__.py

Lines changed: 13 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,26 @@
22

33
import os
44
import sys
5-
import sysconfig
65

6+
from ruff import find_ruff_bin
77

8-
def find_ruff_bin() -> str:
9-
"""Return the ruff binary path."""
108

11-
ruff_exe = "ruff" + sysconfig.get_config_var("EXE")
12-
13-
scripts_path = os.path.join(sysconfig.get_path("scripts"), ruff_exe)
14-
if os.path.isfile(scripts_path):
15-
return scripts_path
16-
17-
if sys.version_info >= (3, 10):
18-
user_scheme = sysconfig.get_preferred_scheme("user")
19-
elif os.name == "nt":
20-
user_scheme = "nt_user"
21-
elif sys.platform == "darwin" and sys._framework:
22-
user_scheme = "osx_framework_user"
23-
else:
24-
user_scheme = "posix_user"
25-
26-
user_path = os.path.join(
27-
sysconfig.get_path("scripts", scheme=user_scheme), ruff_exe
28-
)
29-
if os.path.isfile(user_path):
30-
return user_path
31-
32-
# Search in `bin` adjacent to package root (as created by `pip install --target`).
33-
pkg_root = os.path.dirname(os.path.dirname(__file__))
34-
target_path = os.path.join(pkg_root, "bin", ruff_exe)
35-
if os.path.isfile(target_path):
36-
return target_path
37-
38-
# Search for pip-specific build environments.
39-
#
40-
# Expect to find ruff in <prefix>/pip-build-env-<rand>/overlay/bin/ruff
41-
# Expect to find a "normal" folder at <prefix>/pip-build-env-<rand>/normal
42-
#
43-
# See: https://github.com/pypa/pip/blob/102d8187a1f5a4cd5de7a549fd8a9af34e89a54f/src/pip/_internal/build_env.py#L87
44-
paths = os.environ.get("PATH", "").split(os.pathsep)
45-
if len(paths) >= 2:
46-
47-
def get_last_three_path_parts(path: str) -> list[str]:
48-
"""Return a list of up to the last three parts of a path."""
49-
parts = []
50-
51-
while len(parts) < 3:
52-
head, tail = os.path.split(path)
53-
if tail or head != path:
54-
parts.append(tail)
55-
path = head
56-
else:
57-
parts.append(path)
58-
break
59-
60-
return parts
61-
62-
maybe_overlay = get_last_three_path_parts(paths[0])
63-
maybe_normal = get_last_three_path_parts(paths[1])
64-
if (
65-
len(maybe_normal) >= 3
66-
and maybe_normal[-1].startswith("pip-build-env-")
67-
and maybe_normal[-2] == "normal"
68-
and len(maybe_overlay) >= 3
69-
and maybe_overlay[-1].startswith("pip-build-env-")
70-
and maybe_overlay[-2] == "overlay"
71-
):
72-
# The overlay must contain the ruff binary.
73-
candidate = os.path.join(paths[0], ruff_exe)
74-
if os.path.isfile(candidate):
75-
return candidate
76-
77-
raise FileNotFoundError(scripts_path)
78-
79-
80-
if __name__ == "__main__":
9+
def _run() -> None:
8110
ruff = find_ruff_bin()
11+
8212
if sys.platform == "win32":
8313
import subprocess
8414

85-
completed_process = subprocess.run([ruff, *sys.argv[1:]])
15+
# Avoid emitting a traceback on interrupt
16+
try:
17+
completed_process = subprocess.run([ruff, *sys.argv[1:]])
18+
except KeyboardInterrupt:
19+
sys.exit(2)
20+
8621
sys.exit(completed_process.returncode)
8722
else:
8823
os.execvp(ruff, [ruff, *sys.argv[1:]])
24+
25+
26+
if __name__ == "__main__":
27+
_run()

python/ruff/_find_ruff.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import sys
5+
import sysconfig
6+
7+
8+
class RuffNotFound(FileNotFoundError): ...
9+
10+
11+
def find_ruff_bin() -> str:
12+
"""Return the ruff binary path."""
13+
14+
ruff_exe = "ruff" + sysconfig.get_config_var("EXE")
15+
16+
targets = [
17+
# The scripts directory for the current Python
18+
sysconfig.get_path("scripts"),
19+
# The scripts directory for the base prefix
20+
sysconfig.get_path("scripts", vars={"base": sys.base_prefix}),
21+
# Above the package root, e.g., from `pip install --prefix` or `uv run --with`
22+
(
23+
# On Windows, with module path `<prefix>/Lib/site-packages/ruff`
24+
_join(
25+
_matching_parents(_module_path(), "Lib/site-packages/ruff"), "Scripts"
26+
)
27+
if sys.platform == "win32"
28+
# On Unix, with module path `<prefix>/lib/python3.13/site-packages/ruff`
29+
else _join(
30+
_matching_parents(_module_path(), "lib/python*/site-packages/ruff"),
31+
"bin",
32+
)
33+
),
34+
# Adjacent to the package root, e.g., from `pip install --target`
35+
# with module path `<target>/ruff`
36+
_join(_matching_parents(_module_path(), "ruff"), "bin"),
37+
# The user scheme scripts directory, e.g., `~/.local/bin`
38+
sysconfig.get_path("scripts", scheme=_user_scheme()),
39+
]
40+
41+
seen = []
42+
for target in targets:
43+
if not target:
44+
continue
45+
if target in seen:
46+
continue
47+
seen.append(target)
48+
path = os.path.join(target, ruff_exe)
49+
if os.path.isfile(path):
50+
return path
51+
52+
locations = "\n".join(f" - {target}" for target in seen)
53+
raise RuffNotFound(
54+
f"Could not find the ruff binary in any of the following locations:\n{locations}\n"
55+
)
56+
57+
58+
def _module_path() -> str | None:
59+
path = os.path.dirname(__file__)
60+
return path
61+
62+
63+
def _matching_parents(path: str | None, match: str) -> str | None:
64+
"""
65+
Return the parent directory of `path` after trimming a `match` from the end.
66+
The match is expected to contain `/` as a path separator, while the `path`
67+
is expected to use the platform's path separator (e.g., `os.sep`). The path
68+
components are compared case-insensitively and a `*` wildcard can be used
69+
in the `match`.
70+
"""
71+
from fnmatch import fnmatch
72+
73+
if not path:
74+
return None
75+
parts = path.split(os.sep)
76+
match_parts = match.split("/")
77+
if len(parts) < len(match_parts):
78+
return None
79+
80+
if not all(
81+
fnmatch(part, match_part)
82+
for part, match_part in zip(reversed(parts), reversed(match_parts))
83+
):
84+
return None
85+
86+
return os.sep.join(parts[: -len(match_parts)])
87+
88+
89+
def _join(path: str | None, *parts: str) -> str | None:
90+
if not path:
91+
return None
92+
return os.path.join(path, *parts)
93+
94+
95+
def _user_scheme() -> str:
96+
if sys.version_info >= (3, 10):
97+
user_scheme = sysconfig.get_preferred_scheme("user")
98+
elif os.name == "nt":
99+
user_scheme = "nt_user"
100+
elif sys.platform == "darwin" and sys._framework: # ty: ignore[unresolved-attribute]
101+
user_scheme = "osx_framework_user"
102+
else:
103+
user_scheme = "posix_user"
104+
return user_scheme

0 commit comments

Comments
 (0)