Skip to content

Commit f906491

Browse files
authored
Update the Python module (notably find_ty_bin) for parity with uv (#2852)
Closes astral-sh/ruff#18153 Copy of astral-sh/ruff#23406
1 parent e7676a8 commit f906491

3 files changed

Lines changed: 120 additions & 72 deletions

File tree

python/ty/__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_ty import find_ty_bin
4+
5+
__all__ = ["find_ty_bin"]

python/ty/__main__.py

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

33
import os
44
import sys
5-
import sysconfig
65

6+
from ty import find_ty_bin
77

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

11-
ty_exe = "ty" + sysconfig.get_config_var("EXE")
9+
def _run() -> None:
10+
ty = find_ty_bin()
1211

13-
scripts_path = os.path.join(sysconfig.get_path("scripts"), ty_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(sysconfig.get_path("scripts", scheme=user_scheme), ty_exe)
27-
if os.path.isfile(user_path):
28-
return user_path
29-
30-
# Search in `bin` adjacent to package root (as created by `pip install --target`).
31-
pkg_root = os.path.dirname(os.path.dirname(__file__))
32-
target_path = os.path.join(pkg_root, "bin", ty_exe)
33-
if os.path.isfile(target_path):
34-
return target_path
35-
36-
# Search for pip-specific build environments.
37-
#
38-
# Expect to find ty in <prefix>/pip-build-env-<rand>/overlay/bin/ty
39-
# Expect to find a "normal" folder at <prefix>/pip-build-env-<rand>/normal
40-
#
41-
# See: https://github.com/pypa/pip/blob/102d8187a1f5a4cd5de7a549fd8a9af34e89a54f/src/pip/_internal/build_env.py#L87
42-
paths = os.environ.get("PATH", "").split(os.pathsep)
43-
if len(paths) >= 2:
44-
45-
def get_last_three_path_parts(path: str) -> list[str]:
46-
"""Return a list of up to the last three parts of a path."""
47-
parts = []
48-
49-
while len(parts) < 3:
50-
head, tail = os.path.split(path)
51-
if tail or head != path:
52-
parts.append(tail)
53-
path = head
54-
else:
55-
parts.append(path)
56-
break
57-
58-
return parts
59-
60-
maybe_overlay = get_last_three_path_parts(paths[0])
61-
maybe_normal = get_last_three_path_parts(paths[1])
62-
if (
63-
len(maybe_normal) >= 3
64-
and maybe_normal[-1].startswith("pip-build-env-")
65-
and maybe_normal[-2] == "normal"
66-
and len(maybe_overlay) >= 3
67-
and maybe_overlay[-1].startswith("pip-build-env-")
68-
and maybe_overlay[-2] == "overlay"
69-
):
70-
# The overlay must contain the ty binary.
71-
candidate = os.path.join(paths[0], ty_exe)
72-
if os.path.isfile(candidate):
73-
return candidate
74-
75-
raise FileNotFoundError(scripts_path)
76-
77-
78-
if __name__ == "__main__":
79-
ty = os.fsdecode(find_ty_bin())
8012
if sys.platform == "win32":
8113
import subprocess
8214

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

python/ty/_find_ty.py

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

0 commit comments

Comments
 (0)