Skip to content

Commit 37772f8

Browse files
authored
refactor(env_manager): split out python detection (#9050)
1 parent 7443d0f commit 37772f8

File tree

8 files changed

+334
-197
lines changed

8 files changed

+334
-197
lines changed

src/poetry/console/commands/init.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from poetry.console.commands.command import Command
1717
from poetry.console.commands.env_command import EnvCommand
1818
from poetry.utils.dependency_specification import RequirementsParser
19+
from poetry.utils.env.python_manager import Python
1920

2021

2122
if TYPE_CHECKING:
@@ -96,7 +97,6 @@ def _init_pyproject(
9697
from poetry.config.config import Config
9798
from poetry.layouts import layout
9899
from poetry.pyproject.toml import PyProjectTOML
99-
from poetry.utils.env import EnvManager
100100

101101
is_interactive = self.io.is_interactive() and allow_interactive
102102

@@ -174,11 +174,7 @@ def _init_pyproject(
174174
config = Config.create()
175175
python = (
176176
">="
177-
+ EnvManager.get_python_version(
178-
precision=2,
179-
prefer_active_python=config.get("virtualenvs.prefer-active-python"),
180-
io=self.io,
181-
).to_string()
177+
+ Python.get_preferred_python(config, self.io).minor_version.to_string()
182178
)
183179

184180
if is_interactive:

src/poetry/utils/env/env_manager.py

Lines changed: 31 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import plistlib
77
import re
8-
import shutil
98
import subprocess
109
import sys
1110

@@ -18,9 +17,7 @@
1817
import virtualenv
1918

2019
from cleo.io.null_io import NullIO
21-
from cleo.io.outputs.output import Verbosity
2220
from poetry.core.constraints.version import Version
23-
from poetry.core.constraints.version import parse_constraint
2421

2522
from poetry.toml.file import TOMLFile
2623
from poetry.utils._compat import WINDOWS
@@ -31,6 +28,7 @@
3128
from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound
3229
from poetry.utils.env.exceptions import PythonVersionNotFound
3330
from poetry.utils.env.generic_env import GenericEnv
31+
from poetry.utils.env.python_manager import Python
3432
from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER
3533
from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER
3634
from poetry.utils.env.system_env import SystemEnv
@@ -97,70 +95,6 @@ def __init__(self, poetry: Poetry, io: None | IO = None) -> None:
9795
self._poetry = poetry
9896
self._io = io or NullIO()
9997

100-
@staticmethod
101-
def _full_python_path(python: str) -> Path | None:
102-
# eg first find pythonXY.bat on windows.
103-
path_python = shutil.which(python)
104-
if path_python is None:
105-
return None
106-
107-
try:
108-
encoding = "locale" if sys.version_info >= (3, 10) else None
109-
executable = subprocess.check_output(
110-
[path_python, "-c", "import sys; print(sys.executable)"],
111-
text=True,
112-
encoding=encoding,
113-
).strip()
114-
return Path(executable)
115-
116-
except CalledProcessError:
117-
return None
118-
119-
@staticmethod
120-
def _detect_active_python(io: None | IO = None) -> Path | None:
121-
io = io or NullIO()
122-
io.write_error_line(
123-
"Trying to detect current active python executable as specified in"
124-
" the config.",
125-
verbosity=Verbosity.VERBOSE,
126-
)
127-
128-
executable = EnvManager._full_python_path("python")
129-
130-
if executable is not None:
131-
io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE)
132-
else:
133-
io.write_error_line(
134-
"Unable to detect the current active python executable. Falling"
135-
" back to default.",
136-
verbosity=Verbosity.VERBOSE,
137-
)
138-
139-
return executable
140-
141-
@staticmethod
142-
def get_python_version(
143-
precision: int = 3,
144-
prefer_active_python: bool = False,
145-
io: None | IO = None,
146-
) -> Version:
147-
version = ".".join(str(v) for v in sys.version_info[:precision])
148-
149-
if prefer_active_python:
150-
executable = EnvManager._detect_active_python(io)
151-
152-
if executable:
153-
encoding = "locale" if sys.version_info >= (3, 10) else None
154-
python_patch = subprocess.check_output(
155-
[executable, "-c", GET_PYTHON_VERSION_ONELINER],
156-
text=True,
157-
encoding=encoding,
158-
).strip()
159-
160-
version = ".".join(str(v) for v in python_patch.split(".")[:precision])
161-
162-
return Version.parse(version)
163-
16498
@property
16599
def in_project_venv(self) -> Path:
166100
venv: Path = self._poetry.file.path.parent / ".venv"
@@ -189,24 +123,10 @@ def activate(self, python: str) -> Env:
189123
# Executable in PATH or full executable path
190124
pass
191125

192-
python_path = self._full_python_path(python)
193-
if python_path is None:
126+
python_ = Python.get_by_name(python)
127+
if python_ is None:
194128
raise PythonVersionNotFound(python)
195129

196-
try:
197-
encoding = "locale" if sys.version_info >= (3, 10) else None
198-
python_version_string = subprocess.check_output(
199-
[python_path, "-c", GET_PYTHON_VERSION_ONELINER],
200-
text=True,
201-
encoding=encoding,
202-
)
203-
except CalledProcessError as e:
204-
raise EnvCommandError(e)
205-
206-
python_version = Version.parse(python_version_string.strip())
207-
minor = f"{python_version.major}.{python_version.minor}"
208-
patch = python_version.text
209-
210130
create = False
211131
# If we are required to create the virtual environment in the project directory,
212132
# create or recreate it if needed
@@ -218,10 +138,10 @@ def activate(self, python: str) -> Env:
218138
_venv = VirtualEnv(venv)
219139
current_patch = ".".join(str(v) for v in _venv.version_info[:3])
220140

221-
if patch != current_patch:
141+
if python_.patch_version.to_string() != current_patch:
222142
create = True
223143

224-
self.create_venv(executable=python_path, force=create)
144+
self.create_venv(executable=python_.executable, force=create)
225145

226146
return self.get(reload=True)
227147

@@ -233,11 +153,14 @@ def activate(self, python: str) -> Env:
233153
current_minor = current_env["minor"]
234154
current_patch = current_env["patch"]
235155

236-
if current_minor == minor and current_patch != patch:
156+
if (
157+
current_minor == python_.minor_version.to_string()
158+
and current_patch != python_.patch_version.to_string()
159+
):
237160
# We need to recreate
238161
create = True
239162

240-
name = f"{self.base_env_name}-py{minor}"
163+
name = f"{self.base_env_name}-py{python_.minor_version.to_string()}"
241164
venv = venv_path / name
242165

243166
# Create if needed
@@ -251,13 +174,16 @@ def activate(self, python: str) -> Env:
251174
_venv = VirtualEnv(venv)
252175
current_patch = ".".join(str(v) for v in _venv.version_info[:3])
253176

254-
if patch != current_patch:
177+
if python_.patch_version.to_string() != current_patch:
255178
create = True
256179

257-
self.create_venv(executable=python_path, force=create)
180+
self.create_venv(executable=python_.executable, force=create)
258181

259182
# Activate
260-
envs[self.base_env_name] = {"minor": minor, "patch": patch}
183+
envs[self.base_env_name] = {
184+
"minor": python_.minor_version.to_string(),
185+
"patch": python_.patch_version.to_string(),
186+
}
261187
self.envs_file.write(envs)
262188

263189
return self.get(reload=True)
@@ -277,12 +203,8 @@ def get(self, reload: bool = False) -> Env:
277203
if self._env is not None and not reload:
278204
return self._env
279205

280-
prefer_active_python = self._poetry.config.get(
281-
"virtualenvs.prefer-active-python"
282-
)
283-
python_minor = self.get_python_version(
284-
precision=2, prefer_active_python=prefer_active_python, io=self._io
285-
).to_string()
206+
python = Python.get_preferred_python(config=self._poetry.config, io=self._io)
207+
python_minor = python.minor_version.to_string()
286208

287209
env = None
288210
envs = None
@@ -480,8 +402,11 @@ def create_venv(
480402
)
481403
venv_prompt = self._poetry.config.get("virtualenvs.prompt")
482404

483-
if not executable and prefer_active_python:
484-
executable = self._detect_active_python()
405+
python = (
406+
Python(executable)
407+
if executable
408+
else Python.get_preferred_python(config=self._poetry.config, io=self._io)
409+
)
485410

486411
venv_path = (
487412
self.in_project_venv
@@ -491,19 +416,8 @@ def create_venv(
491416
if not name:
492417
name = self._poetry.package.name
493418

494-
python_patch = ".".join([str(v) for v in sys.version_info[:3]])
495-
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
496-
if executable:
497-
encoding = "locale" if sys.version_info >= (3, 10) else None
498-
python_patch = subprocess.check_output(
499-
[executable, "-c", GET_PYTHON_VERSION_ONELINER],
500-
text=True,
501-
encoding=encoding,
502-
).strip()
503-
python_minor = ".".join(python_patch.split(".")[:2])
504-
505419
supported_python = self._poetry.package.python_constraint
506-
if not supported_python.allows(Version.parse(python_patch)):
420+
if not supported_python.allows(python.patch_version):
507421
# The currently activated or chosen Python version
508422
# is not compatible with the Python constraint specified
509423
# for the project.
@@ -512,71 +426,29 @@ def create_venv(
512426
# Otherwise, we try to find a compatible Python version.
513427
if executable and not prefer_active_python:
514428
raise NoCompatiblePythonVersionFound(
515-
self._poetry.package.python_versions, python_patch
429+
self._poetry.package.python_versions,
430+
python.patch_version.to_string(),
516431
)
517432

518433
self._io.write_error_line(
519-
f"<warning>The currently activated Python version {python_patch} is not"
434+
f"<warning>The currently activated Python version {python.patch_version.to_string()} is not"
520435
f" supported by the project ({self._poetry.package.python_versions}).\n"
521436
"Trying to find and use a compatible version.</warning> "
522437
)
523438

524-
for suffix in sorted(
525-
self._poetry.package.AVAILABLE_PYTHONS,
526-
key=lambda v: (v.startswith("3"), -len(v), v),
527-
reverse=True,
528-
):
529-
if len(suffix) == 1:
530-
if not parse_constraint(f"^{suffix}.0").allows_any(
531-
supported_python
532-
):
533-
continue
534-
elif not supported_python.allows_any(parse_constraint(suffix + ".*")):
535-
continue
536-
537-
python_name = f"python{suffix}"
538-
if self._io.is_debug():
539-
self._io.write_error_line(f"<debug>Trying {python_name}</debug>")
540-
541-
python = self._full_python_path(python_name)
542-
if python is None:
543-
continue
544-
545-
try:
546-
encoding = "locale" if sys.version_info >= (3, 10) else None
547-
python_patch = subprocess.check_output(
548-
[python, "-c", GET_PYTHON_VERSION_ONELINER],
549-
stderr=subprocess.STDOUT,
550-
text=True,
551-
encoding=encoding,
552-
).strip()
553-
except CalledProcessError:
554-
continue
555-
556-
if supported_python.allows(Version.parse(python_patch)):
557-
self._io.write_error_line(
558-
f"Using <c1>{python_name}</c1> ({python_patch})"
559-
)
560-
executable = python
561-
python_minor = ".".join(python_patch.split(".")[:2])
562-
break
563-
564-
if not executable:
565-
raise NoCompatiblePythonVersionFound(
566-
self._poetry.package.python_versions
567-
)
439+
python = Python.get_compatible_python(poetry=self._poetry, io=self._io)
568440

569441
if in_project_venv:
570442
venv = venv_path
571443
else:
572444
name = self.generate_env_name(name, str(cwd))
573-
name = f"{name}-py{python_minor.strip()}"
445+
name = f"{name}-py{python.minor_version.to_string()}"
574446
venv = venv_path / name
575447

576448
if venv_prompt is not None:
577449
venv_prompt = venv_prompt.format(
578450
project_name=self._poetry.package.name or "virtualenv",
579-
python_version=python_minor,
451+
python_version=python.minor_version.to_string(),
580452
)
581453

582454
if not venv.exists():
@@ -613,7 +485,7 @@ def create_venv(
613485
if create_venv:
614486
self.build_venv(
615487
venv,
616-
executable=executable,
488+
executable=python.executable,
617489
flags=self._poetry.config.get("virtualenvs.options"),
618490
prompt=venv_prompt,
619491
)

0 commit comments

Comments
 (0)