1
1
from __future__ import annotations
2
2
3
- import shutil
4
- import subprocess
5
3
import sys
6
4
7
5
from functools import cached_property
8
6
from pathlib import Path
9
7
from typing import TYPE_CHECKING
8
+ from typing import cast
9
+ from typing import overload
10
+
11
+ import findpython
12
+ import packaging .version
10
13
11
14
from cleo .io .null_io import NullIO
12
15
from cleo .io .outputs .output import Verbosity
13
16
from poetry .core .constraints .version import Version
14
- from poetry .core .constraints .version import parse_constraint
15
17
16
- from poetry .utils ._compat import decode
17
18
from poetry .utils .env .exceptions import NoCompatiblePythonVersionFoundError
18
- from poetry .utils .env .script_strings import GET_PYTHON_VERSION_ONELINER
19
19
20
20
21
21
if TYPE_CHECKING :
26
26
27
27
28
28
class Python :
29
- def __init__ (self , executable : str | Path , version : Version | None = None ) -> None :
30
- self .executable = Path (executable )
31
- self ._version = version
29
+ @overload
30
+ def __init__ (self , * , python : findpython .PythonVersion ) -> None : ...
31
+
32
+ @overload
33
+ def __init__ (
34
+ self , executable : str | Path , version : Version | None = None
35
+ ) -> None : ...
36
+
37
+ # we overload __init__ to ensure we do not break any downstream plugins
38
+ # that use the this
39
+ def __init__ (
40
+ self ,
41
+ executable : str | Path | None = None ,
42
+ version : Version | None = None ,
43
+ python : findpython .PythonVersion | None = None ,
44
+ ) -> None :
45
+ if python and (executable or version ):
46
+ raise ValueError (
47
+ "When python is provided, neither executable or version must be specified"
48
+ )
49
+
50
+ if python :
51
+ self ._python = python
52
+ elif executable :
53
+ self ._python = findpython .PythonVersion (
54
+ executable = Path (executable ),
55
+ _version = packaging .version .Version (str (version )) if version else None ,
56
+ )
57
+ else :
58
+ raise ValueError ("Either python or executable must be provided" )
32
59
33
60
@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 )
61
+ def python (self ) -> findpython .PythonVersion :
62
+ return self ._python
48
63
49
- return self ._version
64
+ @property
65
+ def executable (self ) -> Path :
66
+ return cast (Path , self ._python .executable )
67
+
68
+ @property
69
+ def version (self ) -> Version :
70
+ return Version .parse (str (self ._python .version ))
50
71
51
72
@cached_property
52
73
def patch_version (self ) -> Version :
@@ -60,66 +81,47 @@ def patch_version(self) -> Version:
60
81
def minor_version (self ) -> Version :
61
82
return Version .from_parts (major = self .version .major , minor = self .version .minor )
62
83
63
- @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
84
+ @classmethod
85
+ def get_active_python (cls ) -> Python | None :
86
+ if python := findpython .find ():
87
+ return cls (python = python )
88
+ return None
69
89
90
+ @classmethod
91
+ def from_executable (cls , path : Path | str ) -> Python :
70
92
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
93
+ return cls (python = findpython .PythonVersion (executable = Path (path )))
94
+ except (FileNotFoundError , NotADirectoryError , ValueError ):
95
+ raise ValueError (f"{ path } is not a valid Python executable" )
102
96
103
97
@classmethod
104
98
def get_system_python (cls ) -> Python :
105
- return cls (executable = sys .executable )
99
+ return cls (
100
+ python = findpython .PythonVersion (
101
+ executable = Path (sys .executable ),
102
+ _version = packaging .version .Version (
103
+ "." .join (str (v ) for v in sys .version_info [:3 ])
104
+ ),
105
+ )
106
+ )
106
107
107
108
@classmethod
108
109
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 )
110
+ if python := findpython .find (python_name ):
111
+ return cls (python = python )
112
+ return None
114
113
115
114
@classmethod
116
115
def get_preferred_python (cls , config : Config , io : IO | None = None ) -> Python :
117
116
io = io or NullIO ()
118
117
119
118
if not config .get ("virtualenvs.use-poetry-python" ) and (
120
- active_python := Python ._detect_active_python ( io )
119
+ active_python := Python .get_active_python ( )
121
120
):
122
- return cls (executable = active_python )
121
+ io .write_error_line (
122
+ f"Found: { active_python .executable } " , verbosity = Verbosity .VERBOSE
123
+ )
124
+ return active_python
123
125
124
126
return cls .get_system_python ()
125
127
@@ -129,39 +131,12 @@ def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python:
129
131
supported_python = poetry .package .python_constraint
130
132
python = None
131
133
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
134
+ for candidate in findpython .find_all ():
135
+ python = cls (python = candidate )
136
+ if python .version .allows_any (supported_python ):
159
137
io .write_error_line (
160
- f"Using <c1>{ python_name } </c1> ({ python .patch_version } )"
138
+ f"Using <c1>{ candidate . name } </c1> ({ python .patch_version } )"
161
139
)
162
- break
163
-
164
- if not python :
165
- raise NoCompatiblePythonVersionFoundError (poetry .package .python_versions )
140
+ return python
166
141
167
- return python
142
+ raise NoCompatiblePythonVersionFoundError ( poetry . package . python_versions )
0 commit comments