2424
2525try :
2626 from _collections_abc import Iterable
27- except ImportError :
27+ except ImportError : # pragma: nocover
2828 from collections .abc import Iterable
2929
3030import os
@@ -76,20 +76,58 @@ class PythonVEnv(Prefab):
7676 executable : str
7777 version : tuple [int , int , int , str , int ]
7878 parent_path : str
79+ _implementation : str | None = attribute (default = None , repr = False )
7980 _parent_executable : str | None = attribute (default = None , repr = False )
8081
8182 @property
8283 def version_str (self ) -> str :
8384 return version_tuple_to_str (self .version )
8485
86+ @property
87+ def implementation (self ) -> str | None :
88+ if not self ._implementation :
89+ try :
90+ pyout = _laz .run (
91+ [
92+ self .executable ,
93+ "-c" ,
94+ "import sys; sys.stdout.write(sys.implementation.name)"
95+ ],
96+ capture_output = True ,
97+ text = True ,
98+ check = True ,
99+ )
100+ except (_laz .subprocess .CalledProcessError , FileNotFoundError ):
101+ pass
102+ else :
103+ if out_implementation := pyout .stdout :
104+ self ._implementation = out_implementation .lower ().strip ()
105+
106+ return self ._implementation
107+
85108 @property
86109 def parent_executable (self ) -> str | None :
87110 if self ._parent_executable is None :
88- # Guess the parent executable file
89- parent_exe = None
90- if sys .platform == "win32" :
91- parent_exe = os .path .join (self .parent_path , "python.exe" )
92- else :
111+
112+ parent_exe : None | str = None
113+
114+ implementation_bins = {
115+ "cpython" : "python" ,
116+ "pypy" : "pypy" ,
117+ "graalpy" : "graalpy" ,
118+ }
119+
120+ venv_exe_path = _laz .Path (self .executable )
121+
122+ if venv_exe_path .is_symlink ():
123+ parent_path = venv_exe_path .resolve ()
124+ if parent_path .exists ():
125+ parent_exe = str (venv_exe_path .resolve ())
126+
127+ elif self .implementation and self .implementation in implementation_bins :
128+
129+ bin_name = implementation_bins [self .implementation ]
130+
93131 # try with additional numbers in order eg: python3.13, python313, python3, python
94132 suffixes = [
95133 f"{ self .version [0 ]} .{ self .version [1 ]} " ,
@@ -98,28 +136,42 @@ def parent_executable(self) -> str | None:
98136 ""
99137 ]
100138
101- for suffix in suffixes :
102- parent_exe = os .path .join (self .parent_path , f"python{ suffix } " )
139+ # Guess the parent executable file
140+ if sys .platform == "win32" :
141+ names = [
142+ f"{ bin_name } { suffix } .exe" for suffix in suffixes
143+ ]
144+ else :
145+ names = [
146+ f"{ bin_name } { suffix } " for suffix in suffixes
147+ ]
148+
149+ for candidate in names :
150+ parent_exe = os .path .join (self .parent_path , candidate )
103151 if os .path .exists (parent_exe ):
104152 break
105-
106- if not (parent_exe and os .path .exists (parent_exe )):
107- try :
108- pyout = _laz .run (
109- [
110- self .executable ,
111- "-c" ,
112- "import sys; sys.stdout.write(getattr(sys, '_base_executable', ''))" ,
113- ],
114- capture_output = True ,
115- text = True ,
116- check = True ,
117- )
118- except (_laz .subprocess .CalledProcessError , FileNotFoundError ):
119- pass
120153 else :
121- if out_exe := pyout .stdout :
122- parent_exe = os .path .join (self .parent_path , os .path .basename (out_exe ))
154+ # Exhausted options and none exist
155+ parent_exe = None
156+
157+ # base_executable should point to the correct path from 3.11+, except on PyPy
158+ if not parent_exe and self .version >= (3 , 11 ) and self .implementation != "pypy" :
159+ try :
160+ pyout = _laz .run (
161+ [
162+ self .executable ,
163+ "-c" ,
164+ "import sys; sys.stdout.write(getattr(sys, '_base_executable', ''))" ,
165+ ],
166+ capture_output = True ,
167+ text = True ,
168+ check = True ,
169+ )
170+ except (_laz .subprocess .CalledProcessError , FileNotFoundError ):
171+ pass
172+ else :
173+ if out_exe := pyout .stdout :
174+ parent_exe = out_exe
123175
124176 self ._parent_executable = parent_exe
125177
@@ -202,8 +254,20 @@ def from_cfg(cls, cfg_path: str | os.PathLike) -> PythonVEnv:
202254
203255 parent_path = conf .get ("home" )
204256 version_str = conf .get ("version" , conf .get ("version_info" ))
257+
258+ # Included in venv and virtualenv generated venvs
205259 parent_exe = conf .get ("executable" , conf .get ("base-executable" ))
206260
261+ # Included in virtualenv and uv generated venvs
262+ implementation = conf .get ("implementation" )
263+
264+ if implementation :
265+ implementation = implementation .lower ()
266+ # More graalpy special casing
267+ # For whatever reason in pyvenv the listing is graalvm not graalpy
268+ if implementation == "graalvm" :
269+ implementation = "graalpy"
270+
207271 if parent_path is None or version_str is None :
208272 # Not a valid venv
209273 raise InvalidVEnvError (f"Path or version not defined in { cfg_path } " )
@@ -238,6 +302,7 @@ def from_cfg(cls, cfg_path: str | os.PathLike) -> PythonVEnv:
238302 version = version_tuple ,
239303 parent_path = parent_path ,
240304 _parent_executable = parent_exe ,
305+ _implementation = implementation ,
241306 )
242307
243308
0 commit comments