6
6
from pathlib import Path
7
7
from typing import TYPE_CHECKING , Callable
8
8
9
- from virtualenv .info import IS_WIN
9
+ from virtualenv .info import IS_WIN , fs_path_id
10
10
11
11
from .discover import Discover
12
12
from .py_info import PythonInfo
@@ -84,22 +84,28 @@ def get_interpreter(
84
84
return None
85
85
86
86
87
- def propose_interpreters ( # noqa: C901, PLR0912
87
+ def propose_interpreters ( # noqa: C901, PLR0912, PLR0915
88
88
spec : PythonSpec ,
89
89
try_first_with : Iterable [str ],
90
90
app_data : AppData | None = None ,
91
91
env : Mapping [str , str ] | None = None ,
92
92
) -> Generator [tuple [PythonInfo , bool ], None , None ]:
93
93
# 0. try with first
94
94
env = os .environ if env is None else env
95
+ tested_exes : set [str ] = set ()
95
96
for py_exe in try_first_with :
96
97
path = os .path .abspath (py_exe )
97
98
try :
98
99
os .lstat (path ) # Windows Store Python does not work with os.path.exists, but does for os.lstat
99
100
except OSError :
100
101
pass
101
102
else :
102
- yield PythonInfo .from_exe (os .path .abspath (path ), app_data , env = env ), True
103
+ exe_raw = os .path .abspath (path )
104
+ exe_id = fs_path_id (exe_raw )
105
+ if exe_id in tested_exes :
106
+ continue
107
+ tested_exes .add (exe_id )
108
+ yield PythonInfo .from_exe (exe_raw , app_data , env = env ), True
103
109
104
110
# 1. if it's a path and exists
105
111
if spec .path is not None :
@@ -109,29 +115,44 @@ def propose_interpreters( # noqa: C901, PLR0912
109
115
if spec .is_abs :
110
116
raise
111
117
else :
112
- yield PythonInfo .from_exe (os .path .abspath (spec .path ), app_data , env = env ), True
118
+ exe_raw = os .path .abspath (spec .path )
119
+ exe_id = fs_path_id (exe_raw )
120
+ if exe_id not in tested_exes :
121
+ tested_exes .add (exe_id )
122
+ yield PythonInfo .from_exe (exe_raw , app_data , env = env ), True
113
123
if spec .is_abs :
114
124
return
115
125
else :
116
126
# 2. otherwise try with the current
117
- yield PythonInfo .current_system (app_data ), True
127
+ current_python = PythonInfo .current_system (app_data )
128
+ exe_raw = str (current_python .executable )
129
+ exe_id = fs_path_id (exe_raw )
130
+ if exe_id not in tested_exes :
131
+ tested_exes .add (exe_id )
132
+ yield current_python , True
118
133
119
134
# 3. otherwise fallback to platform default logic
120
135
if IS_WIN :
121
136
from .windows import propose_interpreters # noqa: PLC0415
122
137
123
138
for interpreter in propose_interpreters (spec , app_data , env ):
139
+ exe_raw = str (interpreter .executable )
140
+ exe_id = fs_path_id (exe_raw )
141
+ if exe_id in tested_exes :
142
+ continue
143
+ tested_exes .add (exe_id )
124
144
yield interpreter , True
125
145
# finally just find on path, the path order matters (as the candidates are less easy to control by end user)
126
- tested_exes = set ()
127
146
find_candidates = path_exe_finder (spec )
128
147
for pos , path in enumerate (get_paths (env )):
129
148
logging .debug (LazyPathDump (pos , path , env ))
130
149
for exe , impl_must_match in find_candidates (path ):
131
- if exe in tested_exes :
150
+ exe_raw = str (exe )
151
+ exe_id = fs_path_id (exe_raw )
152
+ if exe_id in tested_exes :
132
153
continue
133
- tested_exes .add (exe )
134
- interpreter = PathPythonInfo .from_exe (str ( exe ) , app_data , raise_on_error = False , env = env )
154
+ tested_exes .add (exe_id )
155
+ interpreter = PathPythonInfo .from_exe (exe_raw , app_data , raise_on_error = False , env = env )
135
156
if interpreter is not None :
136
157
yield interpreter , impl_must_match
137
158
@@ -180,7 +201,10 @@ def path_exe_finder(spec: PythonSpec) -> Callable[[Path], Generator[tuple[Path,
180
201
181
202
def path_exes (path : Path ) -> Generator [tuple [Path , bool ], None , None ]:
182
203
# 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
183
- yield (path / direct ), False
204
+ direct_path = path / direct
205
+ if direct_path .exists ():
206
+ yield direct_path , False
207
+
184
208
# 5. or from the spec we can deduce if a name on path matches
185
209
for exe in path .iterdir ():
186
210
match = pat .fullmatch (exe .name )
0 commit comments