55import sys
66from contextlib import suppress
77from pathlib import Path
8- from typing import TYPE_CHECKING
8+ from typing import TYPE_CHECKING , Final
99
1010from platformdirs import user_data_path
1111
12- from ._compat import IS_WIN , fs_path_id
12+ from ._compat import fs_path_id
1313from ._py_info import PythonInfo
1414from ._py_spec import PythonSpec
1515
1818
1919 from ._cache import PyInfoCache
2020
21- LOGGER = logging .getLogger (__name__ )
21+ _LOGGER : Final [logging .Logger ] = logging .getLogger (__name__ )
22+ IS_WIN : Final [bool ] = sys .platform == "win32"
2223
2324
2425def get_interpreter (
@@ -41,7 +42,7 @@ def _find_interpreter(
4142 env : Mapping [str , str ] | None = None ,
4243) -> PythonInfo | None :
4344 spec = PythonSpec .from_string_spec (key )
44- LOGGER .info ("find interpreter for spec %r" , spec )
45+ _LOGGER .info ("find interpreter for spec %r" , spec )
4546 proposed_paths : set [tuple [str , bool ]] = set ()
4647 env = os .environ if env is None else env
4748 for interpreter , impl_must_match in propose_interpreters (spec , try_first_with , cache , env ):
@@ -50,14 +51,37 @@ def _find_interpreter(
5051 proposed_key = interpreter .system_executable , impl_must_match
5152 if proposed_key in proposed_paths :
5253 continue
53- LOGGER .info ("proposed %s" , interpreter )
54+ _LOGGER .info ("proposed %s" , interpreter )
5455 if interpreter .satisfies (spec , impl_must_match ):
55- LOGGER .debug ("accepted %s" , interpreter )
56+ _LOGGER .debug ("accepted %s" , interpreter )
5657 return interpreter
5758 proposed_paths .add (proposed_key )
5859 return None
5960
6061
62+ def _check_exe (path : str , tested_exes : set [str ]) -> str | None :
63+ """Resolve *path* to an absolute path and return it if not yet tested, otherwise ``None``."""
64+ try :
65+ os .lstat (path )
66+ except OSError :
67+ return None
68+ exe_raw = os .path .abspath (path )
69+ exe_id = fs_path_id (exe_raw )
70+ if exe_id in tested_exes :
71+ return None
72+ tested_exes .add (exe_id )
73+ return exe_raw
74+
75+
76+ def _is_new_exe (exe_raw : str , tested_exes : set [str ]) -> bool :
77+ """Return ``True`` and register *exe_raw* if it hasn't been tested yet."""
78+ exe_id = fs_path_id (exe_raw )
79+ if exe_id in tested_exes :
80+ return False
81+ tested_exes .add (exe_id )
82+ return True
83+
84+
6185def propose_interpreters (
6286 spec : PythonSpec ,
6387 try_first_with : Iterable [str ],
@@ -67,76 +91,41 @@ def propose_interpreters(
6791 env = os .environ if env is None else env
6892 tested_exes : set [str ] = set ()
6993 if spec .is_abs and spec .path is not None :
70- try :
71- os .lstat (spec .path )
72- except OSError :
73- pass
74- else :
75- exe_raw = os .path .abspath (spec .path )
76- exe_id = fs_path_id (exe_raw )
77- if exe_id not in tested_exes : # pragma: no branch # first exe always new
78- tested_exes .add (exe_id )
79- yield PythonInfo .from_exe (exe_raw , cache , env = env ), True
94+ if exe_raw := _check_exe (spec .path , tested_exes ): # pragma: no branch # first exe always new
95+ yield PythonInfo .from_exe (exe_raw , cache , env = env ), True
8096 return
8197
8298 for py_exe in try_first_with :
83- path = os .path .abspath (py_exe )
84- try :
85- os .lstat (path )
86- except OSError :
87- pass
88- else :
89- exe_raw = os .path .abspath (path )
90- exe_id = fs_path_id (exe_raw )
91- if exe_id in tested_exes :
92- continue
93- tested_exes .add (exe_id )
99+ if exe_raw := _check_exe (os .path .abspath (py_exe ), tested_exes ):
94100 yield PythonInfo .from_exe (exe_raw , cache , env = env ), True
95101
96102 if spec .path is not None :
97- try :
98- os .lstat (spec .path )
99- except OSError :
100- pass
101- else :
102- exe_raw = os .path .abspath (spec .path )
103- exe_id = fs_path_id (exe_raw )
104- if exe_id not in tested_exes : # pragma: no branch
105- tested_exes .add (exe_id )
106- yield PythonInfo .from_exe (exe_raw , cache , env = env ), True
103+ if exe_raw := _check_exe (spec .path , tested_exes ): # pragma: no branch
104+ yield PythonInfo .from_exe (exe_raw , cache , env = env ), True
107105 if spec .is_abs : # pragma: no cover # relative spec.path is never abs
108106 return
109107 else :
110108 current_python = PythonInfo .current_system (cache )
111- exe_raw = str (current_python .executable )
112- exe_id = fs_path_id (exe_raw )
113- if exe_id not in tested_exes :
114- tested_exes .add (exe_id )
109+ if _is_new_exe (str (current_python .executable ), tested_exes ):
115110 yield current_python , True
116111
117112 if IS_WIN : # pragma: win32 cover
118113 from ._windows import propose_interpreters
119114
120115 for interpreter in propose_interpreters (spec , cache , env ):
121- exe_raw = str (interpreter .executable )
122- exe_id = fs_path_id (exe_raw )
123- if exe_id in tested_exes :
124- continue
125- tested_exes .add (exe_id )
126- yield interpreter , True
116+ if _is_new_exe (str (interpreter .executable ), tested_exes ):
117+ yield interpreter , True
127118
128119 find_candidates = path_exe_finder (spec )
129120 for pos , path in enumerate (get_paths (env )):
130- LOGGER .debug (LazyPathDump (pos , path , env ))
121+ _LOGGER .debug (LazyPathDump (pos , path , env ))
131122 for exe , impl_must_match in find_candidates (path ):
132123 exe_raw = str (exe )
133124 if resolved := _resolve_shim (exe_raw , env ):
134- LOGGER .debug ("resolved shim %s to %s" , exe_raw , resolved )
125+ _LOGGER .debug ("resolved shim %s to %s" , exe_raw , resolved )
135126 exe_raw = resolved
136- exe_id = fs_path_id (exe_raw )
137- if exe_id in tested_exes :
127+ if not _is_new_exe (exe_raw , tested_exes ):
138128 continue
139- tested_exes .add (exe_id )
140129 interpreter = PathPythonInfo .from_exe (exe_raw , cache , raise_on_error = False , env = env )
141130 if interpreter is not None :
142131 yield interpreter , impl_must_match
0 commit comments