From 398cbefa637a012c6ad583d86a45899e6958dcda Mon Sep 17 00:00:00 2001 From: finswimmer Date: Mon, 30 Oct 2023 08:12:00 +0100 Subject: [PATCH 1/8] feat: introduce new Python class --- src/poetry/utils/env/python_manager.py | 48 ++++++++++++++++++++++++++ tests/utils/test_python_manager.py | 24 +++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/poetry/utils/env/python_manager.py create mode 100644 tests/utils/test_python_manager.py diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py new file mode 100644 index 00000000000..adb2fe52e29 --- /dev/null +++ b/src/poetry/utils/env/python_manager.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import subprocess +import sys + +from functools import cached_property +from pathlib import Path + +from poetry.core.constraints.version import Version + +from poetry.utils._compat import decode +from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER + + +class Python: + def __init__(self, executable: str | Path, version: Version | None = None) -> None: + self.executable = Path(executable) + self._version = version + + @property + def version(self) -> Version: + if not self._version: + if self.executable == Path(sys.executable): + python_version = ".".join(str(v) for v in sys.version_info[:3]) + else: + encoding = "locale" if sys.version_info >= (3, 10) else None + python_version = decode( + subprocess.check_output( + [str(self.executable), "-c", GET_PYTHON_VERSION_ONELINER], + text=True, + encoding=encoding, + ).strip() + ) + self._version = Version.parse(python_version) + + return self._version + + @cached_property + def patch_version(self) -> Version: + return Version.from_parts( + major=self.version.major, + minor=self.version.minor, + patch=self.version.patch, + ) + + @cached_property + def minor_version(self) -> Version: + return Version.from_parts(major=self.version.major, minor=self.version.minor) diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py new file mode 100644 index 00000000000..4d9c4f4755a --- /dev/null +++ b/tests/utils/test_python_manager.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import sys + +from pathlib import Path + +from poetry.core.constraints.version import Version + +from poetry.utils.env.python_manager import Python + + +def test_python_get_version_on_the_fly() -> None: + python = Python(executable=sys.executable) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.patch_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:3]]) + ) + assert python.minor_version == Version.parse( + ".".join([str(s) for s in sys.version_info[:2]]) + ) From 5e116517c455a585d102a11ff00b6d57b61f1dcb Mon Sep 17 00:00:00 2001 From: finswimmer Date: Mon, 30 Oct 2023 08:14:05 +0100 Subject: [PATCH 2/8] feat: add Python.get_system_python --- src/poetry/utils/env/python_manager.py | 4 ++++ tests/utils/test_python_manager.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py index adb2fe52e29..f56ec53be13 100644 --- a/src/poetry/utils/env/python_manager.py +++ b/src/poetry/utils/env/python_manager.py @@ -46,3 +46,7 @@ def patch_version(self) -> Version: @cached_property def minor_version(self) -> Version: return Version.from_parts(major=self.version.major, minor=self.version.minor) + + @classmethod + def get_system_python(cls) -> Python: + return cls(executable=sys.executable) diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py index 4d9c4f4755a..e6206f1cc3c 100644 --- a/tests/utils/test_python_manager.py +++ b/tests/utils/test_python_manager.py @@ -22,3 +22,12 @@ def test_python_get_version_on_the_fly() -> None: assert python.minor_version == Version.parse( ".".join([str(s) for s in sys.version_info[:2]]) ) + + +def test_python_get_system_python() -> None: + python = Python.get_system_python() + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) From 6f49aad110d6d244c021603484a59cb88340a223 Mon Sep 17 00:00:00 2001 From: finswimmer Date: Fri, 15 Dec 2023 13:51:18 +0100 Subject: [PATCH 3/8] feat: add Python.get_preferred_python --- src/poetry/utils/env/python_manager.py | 61 +++++++++++++++++++++ tests/utils/env/test_env.py | 29 ---------- tests/utils/test_python_manager.py | 74 ++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 29 deletions(-) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py index f56ec53be13..aa327232d20 100644 --- a/src/poetry/utils/env/python_manager.py +++ b/src/poetry/utils/env/python_manager.py @@ -1,17 +1,27 @@ from __future__ import annotations +import shutil import subprocess import sys from functools import cached_property from pathlib import Path +from typing import TYPE_CHECKING +from cleo.io.null_io import NullIO +from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version from poetry.utils._compat import decode from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER +if TYPE_CHECKING: + from cleo.io.io import IO + + from poetry.config.config import Config + + class Python: def __init__(self, executable: str | Path, version: Version | None = None) -> None: self.executable = Path(executable) @@ -47,6 +57,57 @@ def patch_version(self) -> Version: def minor_version(self) -> Version: return Version.from_parts(major=self.version.major, minor=self.version.minor) + @staticmethod + def _full_python_path(python: str) -> Path | None: + # eg first find pythonXY.bat on windows. + path_python = shutil.which(python) + if path_python is None: + return None + + try: + encoding = "locale" if sys.version_info >= (3, 10) else None + executable = subprocess.check_output( + [path_python, "-c", "import sys; print(sys.executable)"], + text=True, + encoding=encoding, + ).strip() + return Path(executable) + + except subprocess.CalledProcessError: + return None + + @staticmethod + def _detect_active_python(io: IO) -> Path | None: + io.write_error_line( + "Trying to detect current active python executable as specified in" + " the config.", + verbosity=Verbosity.VERBOSE, + ) + + executable = Python._full_python_path("python") + + if executable is not None: + io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) + else: + io.write_error_line( + "Unable to detect the current active python executable. Falling" + " back to default.", + verbosity=Verbosity.VERBOSE, + ) + + return executable + @classmethod def get_system_python(cls) -> Python: return cls(executable=sys.executable) + + @classmethod + def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: + io = io or NullIO() + + if config.get("virtualenvs.prefer-active-python") and ( + active_python := Python._detect_active_python(io) + ): + return cls(executable=active_python) + + return cls.get_system_python() diff --git a/tests/utils/env/test_env.py b/tests/utils/env/test_env.py index 745b33a091d..15aaf4cfac8 100644 --- a/tests/utils/env/test_env.py +++ b/tests/utils/env/test_env.py @@ -492,35 +492,6 @@ def test_build_environment_not_called_without_build_script_specified( assert not env.executed # type: ignore[attr-defined] -def test_fallback_on_detect_active_python( - poetry: Poetry, mocker: MockerFixture -) -> None: - m = mocker.patch( - "subprocess.check_output", - side_effect=subprocess.CalledProcessError(1, "some command"), - ) - env_manager = EnvManager(poetry) - active_python = env_manager._detect_active_python() - - assert active_python is None - assert m.call_count == 1 - - -@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") -def test_detect_active_python_with_bat(poetry: Poetry, tmp_path: Path) -> None: - """On Windows pyenv uses batch files for python management.""" - python_wrapper = tmp_path / "python.bat" - wrapped_python = Path(r"C:\SpecialPython\python.exe") - encoding = "locale" if sys.version_info >= (3, 10) else None - with python_wrapper.open("w", encoding=encoding) as f: - f.write(f"@echo {wrapped_python}") - os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] - - active_python = EnvManager(poetry)._detect_active_python() - - assert active_python == wrapped_python - - def test_command_from_bin_preserves_relative_path(manager: EnvManager) -> None: # https://github.com/python-poetry/poetry/issues/7959 env = manager.get() diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py index e6206f1cc3c..12fe3112b30 100644 --- a/tests/utils/test_python_manager.py +++ b/tests/utils/test_python_manager.py @@ -1,12 +1,25 @@ from __future__ import annotations +import os +import subprocess import sys from pathlib import Path +from typing import TYPE_CHECKING +import pytest + +from cleo.io.null_io import NullIO from poetry.core.constraints.version import Version from poetry.utils.env.python_manager import Python +from tests.utils.env.test_env_manager import check_output_wrapper + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from poetry.config.config import Config def test_python_get_version_on_the_fly() -> None: @@ -31,3 +44,64 @@ def test_python_get_system_python() -> None: assert python.version == Version.parse( ".".join(str(v) for v in sys.version_info[:3]) ) + + +def test_python_get_preferred_default(config: Config) -> None: + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + assert python.version == Version.parse( + ".".join(str(v) for v in sys.version_info[:3]) + ) + + +def test_python_get_preferred_activated(config: Config, mocker: MockerFixture) -> None: + mocker.patch( + "subprocess.check_output", + side_effect=check_output_wrapper(Version.parse("3.7.1")), + ) + config.config["virtualenvs"]["prefer-active-python"] = True + python = Python.get_preferred_python(config) + + assert python.executable.as_posix().startswith("/usr/bin/python") + assert python.version == Version.parse("3.7.1") + + +def test_python_get_preferred_activated_fallback( + config: Config, mocker: MockerFixture +) -> None: + config.config["virtualenvs"]["prefer-active-python"] = True + with mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ): + python = Python.get_preferred_python(config) + + assert python.executable == Path(sys.executable) + + +def test_fallback_on_detect_active_python(mocker: MockerFixture) -> None: + m = mocker.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "some command"), + ) + + active_python = Python._detect_active_python(NullIO()) + + assert active_python is None + assert m.call_count == 1 + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") +def test_detect_active_python_with_bat(tmp_path: Path) -> None: + """On Windows pyenv uses batch files for python management.""" + python_wrapper = tmp_path / "python.bat" + wrapped_python = Path(r"C:\SpecialPython\python.exe") + encoding = "locale" if sys.version_info >= (3, 10) else None + with python_wrapper.open("w", encoding=encoding) as f: + f.write(f"@echo {wrapped_python}") + os.environ["PATH"] = str(python_wrapper.parent) + os.pathsep + os.environ["PATH"] + + active_python = Python._detect_active_python(NullIO()) + + assert active_python == wrapped_python From 06ec4fbefd8f974a371a79425223fe1466cd0d6a Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sun, 3 Mar 2024 18:27:44 +0100 Subject: [PATCH 4/8] feat: add Python.get_by_name --- src/poetry/utils/env/python_manager.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py index aa327232d20..89e2ee60480 100644 --- a/src/poetry/utils/env/python_manager.py +++ b/src/poetry/utils/env/python_manager.py @@ -101,6 +101,14 @@ def _detect_active_python(io: IO) -> Path | None: def get_system_python(cls) -> Python: return cls(executable=sys.executable) + @classmethod + def get_by_name(cls, python_name: str) -> Python | None: + executable = cls._full_python_path(python_name) + if not executable: + return None + + return cls(executable=executable) + @classmethod def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: io = io or NullIO() From 2fa851363517cbb77f9d60b97ce533d410f340cb Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sat, 24 Feb 2024 17:44:37 +0100 Subject: [PATCH 5/8] feat: add get_compatible_python --- src/poetry/utils/env/python_manager.py | 46 ++++++++++++++++++++++++++ tests/utils/env/test_env_manager.py | 5 ++- tests/utils/test_python_manager.py | 14 ++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/poetry/utils/env/python_manager.py b/src/poetry/utils/env/python_manager.py index 89e2ee60480..b0c52ccea30 100644 --- a/src/poetry/utils/env/python_manager.py +++ b/src/poetry/utils/env/python_manager.py @@ -11,8 +11,10 @@ from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version +from poetry.core.constraints.version import parse_constraint from poetry.utils._compat import decode +from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER @@ -20,6 +22,7 @@ from cleo.io.io import IO from poetry.config.config import Config + from poetry.poetry import Poetry class Python: @@ -119,3 +122,46 @@ def get_preferred_python(cls, config: Config, io: IO | None = None) -> Python: return cls(executable=active_python) return cls.get_system_python() + + @classmethod + def get_compatible_python(cls, poetry: Poetry, io: IO | None = None) -> Python: + io = io or NullIO() + supported_python = poetry.package.python_constraint + python = None + + for suffix in [ + *sorted( + poetry.package.AVAILABLE_PYTHONS, + key=lambda v: (v.startswith("3"), -len(v), v), + reverse=True, + ), + "", + ]: + if len(suffix) == 1: + if not parse_constraint(f"^{suffix}.0").allows_any(supported_python): + continue + elif suffix and not supported_python.allows_any( + parse_constraint(suffix + ".*") + ): + continue + + python_name = f"python{suffix}" + if io.is_debug(): + io.write_error_line(f"Trying {python_name}") + + executable = cls._full_python_path(python_name) + if executable is None: + continue + + candidate = cls(executable) + if supported_python.allows(candidate.patch_version): + python = candidate + io.write_error_line( + f"Using {python_name} ({python.patch_version})" + ) + break + + if not python: + raise NoCompatiblePythonVersionFound(poetry.package.python_versions) + + return python diff --git a/tests/utils/env/test_env_manager.py b/tests/utils/env/test_env_manager.py index 7724e891118..bf51f8d7dbd 100644 --- a/tests/utils/env/test_env_manager.py +++ b/tests/utils/env/test_env_manager.py @@ -992,7 +992,10 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( poetry.package.python_versions = "^4.8" - mocker.patch("subprocess.check_output", side_effect=[sys.base_prefix]) + mocker.patch( + "subprocess.check_output", + side_effect=[sys.base_prefix, "/usr/bin/python", "3.9.0"], + ) m = mocker.patch( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) diff --git a/tests/utils/test_python_manager.py b/tests/utils/test_python_manager.py index 12fe3112b30..263cd4d572c 100644 --- a/tests/utils/test_python_manager.py +++ b/tests/utils/test_python_manager.py @@ -20,6 +20,7 @@ from pytest_mock import MockerFixture from poetry.config.config import Config + from tests.types import ProjectFactory def test_python_get_version_on_the_fly() -> None: @@ -105,3 +106,16 @@ def test_detect_active_python_with_bat(tmp_path: Path) -> None: active_python = Python._detect_active_python(NullIO()) assert active_python == wrapped_python + + +def test_python_find_compatible(project_factory: ProjectFactory) -> None: + # Note: This test may fail on Windows systems using Python from the Microsoft Store, + # as the executable is named `py.exe`, which is not currently recognized by + # Python.get_compatible_python. This issue will be resolved in #2117. + # However, this does not cause problems in our case because Poetry's own + # Python interpreter is used before attempting to find another compatible version. + fixture = Path(__file__).parent.parent / "fixtures" / "simple_project" + poetry = project_factory("simple-project", source=fixture) + python = Python.get_compatible_python(poetry) + + assert Version.from_parts(3, 4) <= python.version <= Version.from_parts(4, 0) From b0cd621073d4e29eb768ee0c5f241a61712db5bc Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sun, 25 Feb 2024 10:36:54 +0100 Subject: [PATCH 6/8] feat: make use of Python during create_env --- src/poetry/utils/env/env_manager.py | 86 ++++++----------------------- tests/utils/env/test_env_manager.py | 2 +- 2 files changed, 17 insertions(+), 71 deletions(-) diff --git a/src/poetry/utils/env/env_manager.py b/src/poetry/utils/env/env_manager.py index 2e28e68fb9d..8906e49cf12 100644 --- a/src/poetry/utils/env/env_manager.py +++ b/src/poetry/utils/env/env_manager.py @@ -20,7 +20,6 @@ from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version -from poetry.core.constraints.version import parse_constraint from poetry.toml.file import TOMLFile from poetry.utils._compat import WINDOWS @@ -31,6 +30,7 @@ from poetry.utils.env.exceptions import NoCompatiblePythonVersionFound from poetry.utils.env.exceptions import PythonVersionNotFound from poetry.utils.env.generic_env import GenericEnv +from poetry.utils.env.python_manager import Python from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER from poetry.utils.env.system_env import SystemEnv @@ -277,12 +277,8 @@ def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env - prefer_active_python = self._poetry.config.get( - "virtualenvs.prefer-active-python" - ) - python_minor = self.get_python_version( - precision=2, prefer_active_python=prefer_active_python, io=self._io - ).to_string() + python = Python.get_preferred_python(config=self._poetry.config, io=self._io) + python_minor = python.minor_version.to_string() env = None envs = None @@ -480,8 +476,11 @@ def create_venv( ) venv_prompt = self._poetry.config.get("virtualenvs.prompt") - if not executable and prefer_active_python: - executable = self._detect_active_python() + python = ( + Python(executable) + if executable + else Python.get_preferred_python(config=self._poetry.config, io=self._io) + ) venv_path = ( self.in_project_venv @@ -491,19 +490,8 @@ def create_venv( if not name: name = self._poetry.package.name - python_patch = ".".join([str(v) for v in sys.version_info[:3]]) - python_minor = ".".join([str(v) for v in sys.version_info[:2]]) - if executable: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - python_minor = ".".join(python_patch.split(".")[:2]) - supported_python = self._poetry.package.python_constraint - if not supported_python.allows(Version.parse(python_patch)): + if not supported_python.allows(python.patch_version): # The currently activated or chosen Python version # is not compatible with the Python constraint specified # for the project. @@ -512,71 +500,29 @@ def create_venv( # Otherwise, we try to find a compatible Python version. if executable and not prefer_active_python: raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions, python_patch + self._poetry.package.python_versions, + python.patch_version.to_string(), ) self._io.write_error_line( - f"The currently activated Python version {python_patch} is not" + f"The currently activated Python version {python.patch_version.to_string()} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " ) - for suffix in sorted( - self._poetry.package.AVAILABLE_PYTHONS, - key=lambda v: (v.startswith("3"), -len(v), v), - reverse=True, - ): - if len(suffix) == 1: - if not parse_constraint(f"^{suffix}.0").allows_any( - supported_python - ): - continue - elif not supported_python.allows_any(parse_constraint(suffix + ".*")): - continue - - python_name = f"python{suffix}" - if self._io.is_debug(): - self._io.write_error_line(f"Trying {python_name}") - - python = self._full_python_path(python_name) - if python is None: - continue - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [python, "-c", GET_PYTHON_VERSION_ONELINER], - stderr=subprocess.STDOUT, - text=True, - encoding=encoding, - ).strip() - except CalledProcessError: - continue - - if supported_python.allows(Version.parse(python_patch)): - self._io.write_error_line( - f"Using {python_name} ({python_patch})" - ) - executable = python - python_minor = ".".join(python_patch.split(".")[:2]) - break - - if not executable: - raise NoCompatiblePythonVersionFound( - self._poetry.package.python_versions - ) + python = Python.get_compatible_python(poetry=self._poetry, io=self._io) if in_project_venv: venv = venv_path else: name = self.generate_env_name(name, str(cwd)) - name = f"{name}-py{python_minor.strip()}" + name = f"{name}-py{python.minor_version.to_string()}" venv = venv_path / name if venv_prompt is not None: venv_prompt = venv_prompt.format( project_name=self._poetry.package.name or "virtualenv", - python_version=python_minor, + python_version=python.minor_version.to_string(), ) if not venv.exists(): @@ -613,7 +559,7 @@ def create_venv( if create_venv: self.build_venv( venv, - executable=executable, + executable=python.executable, flags=self._poetry.config.get("virtualenvs.options"), prompt=venv_prompt, ) diff --git a/tests/utils/env/test_env_manager.py b/tests/utils/env/test_env_manager.py index bf51f8d7dbd..ef49d367797 100644 --- a/tests/utils/env/test_env_manager.py +++ b/tests/utils/env/test_env_manager.py @@ -1070,7 +1070,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py{version.major}.{version.minor}", - executable=None, + executable=Path(sys.executable), flags=venv_flags_default, prompt=f"simple-project-py{version.major}.{version.minor}", ) From d20dd4ffd2d9a1402ddc764282245f6dba27e9ed Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sun, 3 Mar 2024 18:31:00 +0100 Subject: [PATCH 7/8] feat: make use of Python in EnvManager.activate() --- src/poetry/utils/env/env_manager.py | 104 ++++------------------------ 1 file changed, 15 insertions(+), 89 deletions(-) diff --git a/src/poetry/utils/env/env_manager.py b/src/poetry/utils/env/env_manager.py index 8906e49cf12..d33f963bb87 100644 --- a/src/poetry/utils/env/env_manager.py +++ b/src/poetry/utils/env/env_manager.py @@ -5,7 +5,6 @@ import os import plistlib import re -import shutil import subprocess import sys @@ -18,7 +17,6 @@ import virtualenv from cleo.io.null_io import NullIO -from cleo.io.outputs.output import Verbosity from poetry.core.constraints.version import Version from poetry.toml.file import TOMLFile @@ -97,70 +95,6 @@ def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._poetry = poetry self._io = io or NullIO() - @staticmethod - def _full_python_path(python: str) -> Path | None: - # eg first find pythonXY.bat on windows. - path_python = shutil.which(python) - if path_python is None: - return None - - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - executable = subprocess.check_output( - [path_python, "-c", "import sys; print(sys.executable)"], - text=True, - encoding=encoding, - ).strip() - return Path(executable) - - except CalledProcessError: - return None - - @staticmethod - def _detect_active_python(io: None | IO = None) -> Path | None: - io = io or NullIO() - io.write_error_line( - "Trying to detect current active python executable as specified in" - " the config.", - verbosity=Verbosity.VERBOSE, - ) - - executable = EnvManager._full_python_path("python") - - if executable is not None: - io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) - else: - io.write_error_line( - "Unable to detect the current active python executable. Falling" - " back to default.", - verbosity=Verbosity.VERBOSE, - ) - - return executable - - @staticmethod - def get_python_version( - precision: int = 3, - prefer_active_python: bool = False, - io: None | IO = None, - ) -> Version: - version = ".".join(str(v) for v in sys.version_info[:precision]) - - if prefer_active_python: - executable = EnvManager._detect_active_python(io) - - if executable: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_patch = subprocess.check_output( - [executable, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ).strip() - - version = ".".join(str(v) for v in python_patch.split(".")[:precision]) - - return Version.parse(version) - @property def in_project_venv(self) -> Path: venv: Path = self._poetry.file.path.parent / ".venv" @@ -189,24 +123,10 @@ def activate(self, python: str) -> Env: # Executable in PATH or full executable path pass - python_path = self._full_python_path(python) - if python_path is None: + python_ = Python.get_by_name(python) + if python_ is None: raise PythonVersionNotFound(python) - try: - encoding = "locale" if sys.version_info >= (3, 10) else None - python_version_string = subprocess.check_output( - [python_path, "-c", GET_PYTHON_VERSION_ONELINER], - text=True, - encoding=encoding, - ) - except CalledProcessError as e: - raise EnvCommandError(e) - - python_version = Version.parse(python_version_string.strip()) - minor = f"{python_version.major}.{python_version.minor}" - patch = python_version.text - create = False # If we are required to create the virtual environment in the project directory, # create or recreate it if needed @@ -218,10 +138,10 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) return self.get(reload=True) @@ -233,11 +153,14 @@ def activate(self, python: str) -> Env: current_minor = current_env["minor"] current_patch = current_env["patch"] - if current_minor == minor and current_patch != patch: + if ( + current_minor == python_.minor_version.to_string() + and current_patch != python_.patch_version.to_string() + ): # We need to recreate create = True - name = f"{self.base_env_name}-py{minor}" + name = f"{self.base_env_name}-py{python_.minor_version.to_string()}" venv = venv_path / name # Create if needed @@ -251,13 +174,16 @@ def activate(self, python: str) -> Env: _venv = VirtualEnv(venv) current_patch = ".".join(str(v) for v in _venv.version_info[:3]) - if patch != current_patch: + if python_.patch_version.to_string() != current_patch: create = True - self.create_venv(executable=python_path, force=create) + self.create_venv(executable=python_.executable, force=create) # Activate - envs[self.base_env_name] = {"minor": minor, "patch": patch} + envs[self.base_env_name] = { + "minor": python_.minor_version.to_string(), + "patch": python_.patch_version.to_string(), + } self.envs_file.write(envs) return self.get(reload=True) From 5630770fb8eb665a5f714be5ada8f209f47581ff Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sun, 3 Mar 2024 18:31:42 +0100 Subject: [PATCH 8/8] feat: make use of Python in init and new command --- src/poetry/console/commands/init.py | 8 ++------ tests/console/commands/test_init.py | 5 ++++- tests/console/commands/test_new.py | 4 ++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 595b53fc232..9cc330e6d67 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -16,6 +16,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand from poetry.utils.dependency_specification import RequirementsParser +from poetry.utils.env.python_manager import Python if TYPE_CHECKING: @@ -96,7 +97,6 @@ def _init_pyproject( from poetry.config.config import Config from poetry.layouts import layout from poetry.pyproject.toml import PyProjectTOML - from poetry.utils.env import EnvManager is_interactive = self.io.is_interactive() and allow_interactive @@ -174,11 +174,7 @@ def _init_pyproject( config = Config.create() python = ( ">=" - + EnvManager.get_python_version( - precision=2, - prefer_active_python=config.get("virtualenvs.prefer-active-python"), - io=self.io, - ).to_string() + + Python.get_preferred_python(config, self.io).minor_version.to_string() ) if is_interactive: diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index ec85d321f59..a13e07c15ec 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -1113,7 +1113,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return result mocker.patch("subprocess.check_output", side_effect=mock_check_output) - + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active pyproject_file = source_dir / "pyproject.toml" diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index 72cb5654a66..b9b4579ba7e 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -215,6 +215,10 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str: return output mocker.patch("subprocess.check_output", side_effect=mock_check_output) + mocker.patch( + "poetry.utils.env.python_manager.Python._full_python_path", + return_value=Path(f"/usr/bin/python{python}"), + ) config.config["virtualenvs"]["prefer-active-python"] = prefer_active