-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
builtin.py
226 lines (194 loc) · 7.77 KB
/
builtin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
from __future__ import annotations
import logging
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Callable
from virtualenv.info import IS_WIN, fs_path_id
from .discover import Discover
from .py_info import PythonInfo
from .py_spec import PythonSpec
if TYPE_CHECKING:
from argparse import ArgumentParser
from collections.abc import Generator, Iterable, Mapping, Sequence
from virtualenv.app_data.base import AppData
class Builtin(Discover):
python_spec: Sequence[str]
app_data: AppData
try_first_with: Sequence[str]
def __init__(self, options) -> None:
super().__init__(options)
self.python_spec = options.python or [sys.executable]
self.app_data = options.app_data
self.try_first_with = options.try_first_with
@classmethod
def add_parser_arguments(cls, parser: ArgumentParser) -> None:
parser.add_argument(
"-p",
"--python",
dest="python",
metavar="py",
type=str,
action="append",
default=[],
help="interpreter based on what to create environment (path/identifier) "
"- by default use the interpreter where the tool is installed - first found wins",
)
parser.add_argument(
"--try-first-with",
dest="try_first_with",
metavar="py_exe",
type=str,
action="append",
default=[],
help="try first these interpreters before starting the discovery",
)
def run(self) -> PythonInfo | None:
for python_spec in self.python_spec:
result = get_interpreter(python_spec, self.try_first_with, self.app_data, self._env)
if result is not None:
return result
return None
def __repr__(self) -> str:
spec = self.python_spec[0] if len(self.python_spec) == 1 else self.python_spec
return f"{self.__class__.__name__} discover of python_spec={spec!r}"
def get_interpreter(
key, try_first_with: Iterable[str], app_data: AppData | None = None, env: Mapping[str, str] | None = None
) -> PythonInfo | None:
spec = PythonSpec.from_string_spec(key)
logging.info("find interpreter for spec %r", spec)
proposed_paths = set()
env = os.environ if env is None else env
for interpreter, impl_must_match in propose_interpreters(spec, try_first_with, app_data, env):
key = interpreter.system_executable, impl_must_match
if key in proposed_paths:
continue
logging.info("proposed %s", interpreter)
if interpreter.satisfies(spec, impl_must_match):
logging.debug("accepted %s", interpreter)
return interpreter
proposed_paths.add(key)
return None
def propose_interpreters( # noqa: C901, PLR0912, PLR0915
spec: PythonSpec,
try_first_with: Iterable[str],
app_data: AppData | None = None,
env: Mapping[str, str] | None = None,
) -> Generator[tuple[PythonInfo, bool], None, None]:
# 0. try with first
env = os.environ if env is None else env
tested_exes: set[str] = set()
for py_exe in try_first_with:
path = os.path.abspath(py_exe)
try:
os.lstat(path) # Windows Store Python does not work with os.path.exists, but does for os.lstat
except OSError:
pass
else:
exe_raw = os.path.abspath(path)
exe_id = fs_path_id(exe_raw)
if exe_id in tested_exes:
continue
tested_exes.add(exe_id)
yield PythonInfo.from_exe(exe_raw, app_data, env=env), True
# 1. if it's a path and exists
if spec.path is not None:
try:
os.lstat(spec.path) # Windows Store Python does not work with os.path.exists, but does for os.lstat
except OSError:
if spec.is_abs:
raise
else:
exe_raw = os.path.abspath(spec.path)
exe_id = fs_path_id(exe_raw)
if exe_id not in tested_exes:
tested_exes.add(exe_id)
yield PythonInfo.from_exe(exe_raw, app_data, env=env), True
if spec.is_abs:
return
else:
# 2. otherwise try with the current
current_python = PythonInfo.current_system(app_data)
exe_raw = str(current_python.executable)
exe_id = fs_path_id(exe_raw)
if exe_id not in tested_exes:
tested_exes.add(exe_id)
yield current_python, True
# 3. otherwise fallback to platform default logic
if IS_WIN:
from .windows import propose_interpreters # noqa: PLC0415
for interpreter in propose_interpreters(spec, app_data, env):
exe_raw = str(interpreter.executable)
exe_id = fs_path_id(exe_raw)
if exe_id in tested_exes:
continue
tested_exes.add(exe_id)
yield interpreter, True
# finally just find on path, the path order matters (as the candidates are less easy to control by end user)
find_candidates = path_exe_finder(spec)
for pos, path in enumerate(get_paths(env)):
logging.debug(LazyPathDump(pos, path, env))
for exe, impl_must_match in find_candidates(path):
exe_raw = str(exe)
exe_id = fs_path_id(exe_raw)
if exe_id in tested_exes:
continue
tested_exes.add(exe_id)
interpreter = PathPythonInfo.from_exe(exe_raw, app_data, raise_on_error=False, env=env)
if interpreter is not None:
yield interpreter, impl_must_match
def get_paths(env: Mapping[str, str]) -> Generator[Path, None, None]:
path = env.get("PATH", None)
if path is None:
try:
path = os.confstr("CS_PATH")
except (AttributeError, ValueError):
path = os.defpath
if not path:
return None
for p in map(Path, path.split(os.pathsep)):
if p.exists():
yield p
class LazyPathDump:
def __init__(self, pos: int, path: Path, env: Mapping[str, str]) -> None:
self.pos = pos
self.path = path
self.env = env
def __repr__(self) -> str:
content = f"discover PATH[{self.pos}]={self.path}"
if self.env.get("_VIRTUALENV_DEBUG"): # this is the over the board debug
content += " with =>"
for file_path in self.path.iterdir():
try:
if file_path.is_dir() or not (file_path.stat().st_mode & os.X_OK):
continue
except OSError:
pass
content += " "
content += file_path.name
return content
def path_exe_finder(spec: PythonSpec) -> Callable[[Path], Generator[tuple[Path, bool], None, None]]:
"""Given a spec, return a function that can be called on a path to find all matching files in it."""
pat = spec.generate_re(windows=sys.platform == "win32")
direct = spec.str_spec
if sys.platform == "win32":
direct = f"{direct}.exe"
def path_exes(path: Path) -> Generator[tuple[Path, bool], None, None]:
# 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
direct_path = path / direct
if direct_path.exists():
yield direct_path, False
# 5. or from the spec we can deduce if a name on path matches
for exe in path.iterdir():
match = pat.fullmatch(exe.name)
if match:
# the implementation must match when we find “python[ver]”
yield exe.absolute(), match["impl"] == "python"
return path_exes
class PathPythonInfo(PythonInfo):
"""python info from path."""
__all__ = [
"Builtin",
"PathPythonInfo",
"get_interpreter",
]