3
3
import logging
4
4
import os
5
5
import sys
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING , Callable
6
8
7
9
from virtualenv .info import IS_WIN
8
10
9
11
from .discover import Discover
10
12
from .py_info import PythonInfo
11
13
from .py_spec import PythonSpec
12
14
15
+ if TYPE_CHECKING :
16
+ from argparse import ArgumentParser
17
+ from collections .abc import Generator , Iterable , Mapping , Sequence
18
+
19
+ from virtualenv .app_data .base import AppData
20
+
13
21
14
22
class Builtin (Discover ):
23
+ python_spec : Sequence [str ]
24
+ app_data : AppData
25
+ try_first_with : Sequence [str ]
26
+
15
27
def __init__ (self , options ) -> None :
16
28
super ().__init__ (options )
17
29
self .python_spec = options .python or [sys .executable ]
18
30
self .app_data = options .app_data
19
31
self .try_first_with = options .try_first_with
20
32
21
33
@classmethod
22
- def add_parser_arguments (cls , parser ) :
34
+ def add_parser_arguments (cls , parser : ArgumentParser ) -> None :
23
35
parser .add_argument (
24
36
"-p" ,
25
37
"--python" ,
@@ -41,7 +53,7 @@ def add_parser_arguments(cls, parser):
41
53
help = "try first these interpreters before starting the discovery" ,
42
54
)
43
55
44
- def run (self ):
56
+ def run (self ) -> PythonInfo | None :
45
57
for python_spec in self .python_spec :
46
58
result = get_interpreter (python_spec , self .try_first_with , self .app_data , self ._env )
47
59
if result is not None :
@@ -53,7 +65,9 @@ def __repr__(self) -> str:
53
65
return f"{ self .__class__ .__name__ } discover of python_spec={ spec !r} "
54
66
55
67
56
- def get_interpreter (key , try_first_with , app_data = None , env = None ):
68
+ def get_interpreter (
69
+ key , try_first_with : Iterable [str ], app_data : AppData | None = None , env : Mapping [str , str ] | None = None
70
+ ) -> PythonInfo | None :
57
71
spec = PythonSpec .from_string_spec (key )
58
72
logging .info ("find interpreter for spec %r" , spec )
59
73
proposed_paths = set ()
@@ -70,7 +84,12 @@ def get_interpreter(key, try_first_with, app_data=None, env=None):
70
84
return None
71
85
72
86
73
- def propose_interpreters (spec , try_first_with , app_data , env = None ): # noqa: C901, PLR0912
87
+ def propose_interpreters ( # noqa: C901, PLR0912
88
+ spec : PythonSpec ,
89
+ try_first_with : Iterable [str ],
90
+ app_data : AppData | None = None ,
91
+ env : Mapping [str , str ] | None = None ,
92
+ ) -> Generator [tuple [PythonInfo , bool ], None , None ]:
74
93
# 0. try with first
75
94
env = os .environ if env is None else env
76
95
for py_exe in try_first_with :
@@ -104,34 +123,35 @@ def propose_interpreters(spec, try_first_with, app_data, env=None): # noqa: C90
104
123
for interpreter in propose_interpreters (spec , app_data , env ):
105
124
yield interpreter , True
106
125
# finally just find on path, the path order matters (as the candidates are less easy to control by end user)
107
- paths = get_paths (env )
108
126
tested_exes = set ()
109
- for pos , path in enumerate (paths ):
110
- path_str = str (path )
111
- logging .debug (LazyPathDump (pos , path_str , env ))
112
- for candidate , match in possible_specs (spec ):
113
- found = check_path (candidate , path_str )
114
- if found is not None :
115
- exe = os .path .abspath (found )
116
- if exe not in tested_exes :
117
- tested_exes .add (exe )
118
- interpreter = PathPythonInfo .from_exe (exe , app_data , raise_on_error = False , env = env )
119
- if interpreter is not None :
120
- yield interpreter , match
121
-
122
-
123
- def get_paths (env ):
127
+ find_candidates = path_exe_finder (spec )
128
+ for pos , path in enumerate (get_paths (env )):
129
+ logging .debug (LazyPathDump (pos , path , env ))
130
+ for exe , impl_must_match in find_candidates (path ):
131
+ if exe in tested_exes :
132
+ continue
133
+ tested_exes .add (exe )
134
+ interpreter = PathPythonInfo .from_exe (str (exe ), app_data , raise_on_error = False , env = env )
135
+ if interpreter is not None :
136
+ yield interpreter , impl_must_match
137
+
138
+
139
+ def get_paths (env : Mapping [str , str ]) -> Generator [Path , None , None ]:
124
140
path = env .get ("PATH" , None )
125
141
if path is None :
126
142
try :
127
143
path = os .confstr ("CS_PATH" )
128
144
except (AttributeError , ValueError ):
129
145
path = os .defpath
130
- return [] if not path else [p for p in path .split (os .pathsep ) if os .path .exists (p )]
146
+ if not path :
147
+ return None
148
+ for p in map (Path , path .split (os .pathsep )):
149
+ if p .exists ():
150
+ yield p
131
151
132
152
133
153
class LazyPathDump :
134
- def __init__ (self , pos , path , env ) -> None :
154
+ def __init__ (self , pos : int , path : Path , env : Mapping [ str , str ] ) -> None :
135
155
self .pos = pos
136
156
self .path = path
137
157
self .env = env
@@ -140,35 +160,35 @@ def __repr__(self) -> str:
140
160
content = f"discover PATH[{ self .pos } ]={ self .path } "
141
161
if self .env .get ("_VIRTUALENV_DEBUG" ): # this is the over the board debug
142
162
content += " with =>"
143
- for file_name in os . listdir ( self .path ):
163
+ for file_path in self .path . iterdir ( ):
144
164
try :
145
- file_path = os .path .join (self .path , file_name )
146
- if os .path .isdir (file_path ) or not os .access (file_path , os .X_OK ):
165
+ if file_path .is_dir () or not (file_path .stat ().st_mode & os .X_OK ):
147
166
continue
148
167
except OSError :
149
168
pass
150
169
content += " "
151
- content += file_name
170
+ content += file_path . name
152
171
return content
153
172
154
173
155
- def check_path (candidate , path ):
156
- _ , ext = os .path .splitext (candidate )
157
- if sys .platform == "win32" and ext != ".exe" :
158
- candidate += ".exe"
159
- if os .path .isfile (candidate ):
160
- return candidate
161
- candidate = os .path .join (path , candidate )
162
- if os .path .isfile (candidate ):
163
- return candidate
164
- return None
165
-
166
-
167
- def possible_specs (spec ):
168
- # 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
169
- yield spec .str_spec , False
170
- # 5. or from the spec we can deduce a name on path that matches
171
- yield from spec .generate_names ()
174
+ def path_exe_finder (spec : PythonSpec ) -> Callable [[Path ], Generator [tuple [Path , bool ], None , None ]]:
175
+ """Given a spec, return a function that can be called on a path to find all matching files in it."""
176
+ pat = spec .generate_re (windows = sys .platform == "win32" )
177
+ direct = spec .str_spec
178
+ if sys .platform == "win32" :
179
+ direct = f"{ direct } .exe"
180
+
181
+ def path_exes (path : Path ) -> Generator [tuple [Path , bool ], None , None ]:
182
+ # 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
183
+ yield (path / direct ), False
184
+ # 5. or from the spec we can deduce if a name on path matches
185
+ for exe in path .iterdir ():
186
+ match = pat .fullmatch (exe .name )
187
+ if match :
188
+ # the implementation must match when we find “python[ver]”
189
+ yield exe .absolute (), match ["impl" ] == "python"
190
+
191
+ return path_exes
172
192
173
193
174
194
class PathPythonInfo (PythonInfo ):
0 commit comments