1
1
from __future__ import annotations
2
2
3
- import shutil
4
- import subprocess
5
- import sys
6
-
7
3
from functools import cached_property
8
4
from pathlib import Path
9
5
from typing import TYPE_CHECKING
6
+ from typing import cast
7
+
8
+ import findpython
10
9
11
10
from cleo .io .null_io import NullIO
12
11
from cleo .io .outputs .output import Verbosity
13
12
from poetry .core .constraints .version import Version
14
- from poetry .core .constraints .version import parse_constraint
15
13
16
- from poetry .utils ._compat import decode
17
14
from poetry .utils .env .exceptions import NoCompatiblePythonVersionFoundError
18
- from poetry .utils .env .script_strings import GET_PYTHON_VERSION_ONELINER
19
15
20
16
21
17
if TYPE_CHECKING :
26
22
27
23
28
24
class Python :
29
- def __init__ (self , executable : str | Path , version : Version | None = None ) -> None :
30
- self .executable = Path (executable )
31
- self ._version = version
25
+ def __init__ (self , python : findpython .PythonVersion ) -> None :
26
+ self ._python = python
32
27
33
28
@property
34
- def version (self ) -> Version :
35
- if not self ._version :
36
- if self .executable == Path (sys .executable ):
37
- python_version = "." .join (str (v ) for v in sys .version_info [:3 ])
38
- else :
39
- encoding = "locale" if sys .version_info >= (3 , 10 ) else None
40
- python_version = decode (
41
- subprocess .check_output (
42
- [str (self .executable ), "-c" , GET_PYTHON_VERSION_ONELINER ],
43
- text = True ,
44
- encoding = encoding ,
45
- ).strip ()
46
- )
47
- self ._version = Version .parse (python_version )
29
+ def executable (self ) -> Path :
30
+ return cast (Path , self ._python .executable )
48
31
49
- return self ._version
32
+ @property
33
+ def version (self ) -> Version :
34
+ return Version .parse (str (self ._python .version ))
50
35
51
36
@cached_property
52
37
def patch_version (self ) -> Version :
@@ -61,65 +46,37 @@ def minor_version(self) -> Version:
61
46
return Version .from_parts (major = self .version .major , minor = self .version .minor )
62
47
63
48
@staticmethod
64
- def _full_python_path (python : str ) -> Path | None :
65
- # eg first find pythonXY.bat on windows.
66
- path_python = shutil .which (python )
67
- if path_python is None :
68
- return None
49
+ def get_active_python () -> findpython .PythonVersion | None :
50
+ return findpython .find ("python" )
69
51
52
+ @classmethod
53
+ def from_executable (cls , path : Path | str ) -> Python :
70
54
try :
71
- encoding = "locale" if sys .version_info >= (3 , 10 ) else None
72
- executable = subprocess .check_output (
73
- [path_python , "-c" , "import sys; print(sys.executable)" ],
74
- text = True ,
75
- encoding = encoding ,
76
- ).strip ()
77
- return Path (executable )
78
-
79
- except subprocess .CalledProcessError :
80
- return None
81
-
82
- @staticmethod
83
- def _detect_active_python (io : IO ) -> Path | None :
84
- io .write_error_line (
85
- "Trying to detect current active python executable as specified in"
86
- " the config." ,
87
- verbosity = Verbosity .VERBOSE ,
88
- )
89
-
90
- executable = Python ._full_python_path ("python" )
91
-
92
- if executable is not None :
93
- io .write_error_line (f"Found: { executable } " , verbosity = Verbosity .VERBOSE )
94
- else :
95
- io .write_error_line (
96
- "Unable to detect the current active python executable. Falling"
97
- " back to default." ,
98
- verbosity = Verbosity .VERBOSE ,
99
- )
100
-
101
- return executable
55
+ return cls (findpython .PythonVersion (executable = Path (path )))
56
+ except (FileNotFoundError , NotADirectoryError , ValueError ):
57
+ raise ValueError (f"{ path } is not a valid Python executable" )
102
58
103
59
@classmethod
104
60
def get_system_python (cls ) -> Python :
105
- return cls (executable = sys . executable )
61
+ return cls (findpython . find () )
106
62
107
63
@classmethod
108
64
def get_by_name (cls , python_name : str ) -> Python | None :
109
- executable = cls ._full_python_path (python_name )
110
- if not executable :
111
- return None
112
-
113
- return cls (executable = executable )
65
+ if python := findpython .find (python_name ):
66
+ return cls (python )
67
+ return None
114
68
115
69
@classmethod
116
70
def get_preferred_python (cls , config : Config , io : IO | None = None ) -> Python :
117
71
io = io or NullIO ()
118
72
119
73
if not config .get ("virtualenvs.use-poetry-python" ) and (
120
- active_python := Python ._detect_active_python ( io )
74
+ active_python := Python .get_active_python ( )
121
75
):
122
- return cls (executable = active_python )
76
+ io .write_error_line (
77
+ f"Found: { active_python .executable } " , verbosity = Verbosity .VERBOSE
78
+ )
79
+ return cls (active_python )
123
80
124
81
return cls .get_system_python ()
125
82
@@ -129,39 +86,12 @@ def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python:
129
86
supported_python = poetry .package .python_constraint
130
87
python = None
131
88
132
- for suffix in [
133
- * sorted (
134
- poetry .package .AVAILABLE_PYTHONS ,
135
- key = lambda v : (v .startswith ("3" ), - len (v ), v ),
136
- reverse = True ,
137
- ),
138
- "" ,
139
- ]:
140
- if len (suffix ) == 1 :
141
- if not parse_constraint (f"^{ suffix } .0" ).allows_any (supported_python ):
142
- continue
143
- elif suffix and not supported_python .allows_any (
144
- parse_constraint (suffix + ".*" )
145
- ):
146
- continue
147
-
148
- python_name = f"python{ suffix } "
149
- if io .is_debug ():
150
- io .write_error_line (f"<debug>Trying { python_name } </debug>" )
151
-
152
- executable = cls ._full_python_path (python_name )
153
- if executable is None :
154
- continue
155
-
156
- candidate = cls (executable )
157
- if supported_python .allows (candidate .patch_version ):
158
- python = candidate
89
+ for candidate in findpython .find_all ():
90
+ python = cls (candidate )
91
+ if python .version .allows_any (supported_python ):
159
92
io .write_error_line (
160
- f"Using <c1>{ python_name } </c1> ({ python .patch_version } )"
93
+ f"Using <c1>{ candidate . name } </c1> ({ python .patch_version } )"
161
94
)
162
- break
163
-
164
- if not python :
165
- raise NoCompatiblePythonVersionFoundError (poetry .package .python_versions )
95
+ return python
166
96
167
- return python
97
+ raise NoCompatiblePythonVersionFoundError ( poetry . package . python_versions )
0 commit comments