diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 7c60f99836d..2dd422573fa 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -295,8 +295,8 @@ def configure_env(self, event: Event, event_name: str, _: EventDispatcher) -> No io = event.io poetry = command.poetry - env_manager = EnvManager(poetry) - env = env_manager.create_venv(io) + env_manager = EnvManager(poetry, io=io) + env = env_manager.create_venv() if env.is_venv() and io.is_verbose(): io.write_line(f"Using virtualenv: {env.path}") diff --git a/src/poetry/console/commands/env/use.py b/src/poetry/console/commands/env/use.py index cdfc8cbe554..c48312d9e4c 100644 --- a/src/poetry/console/commands/env/use.py +++ b/src/poetry/console/commands/env/use.py @@ -14,14 +14,14 @@ class EnvUseCommand(Command): def handle(self) -> int: from poetry.utils.env import EnvManager - manager = EnvManager(self.poetry) + manager = EnvManager(self.poetry, io=self.io) if self.argument("python") == "system": - manager.deactivate(self.io) + manager.deactivate() return 0 - env = manager.activate(self.argument("python"), self.io) + env = manager.activate(self.argument("python")) self.line(f"Using virtualenv: {env.path}") diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 4c60cf9e46c..e521ea0c526 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -20,11 +20,13 @@ from subprocess import CalledProcessError from typing import TYPE_CHECKING from typing import Any +from typing import cast import packaging.tags import tomlkit import virtualenv +from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from packaging.tags import Tag from packaging.tags import interpreter_name @@ -515,8 +517,9 @@ class EnvManager: ENVS_FILE = "envs.toml" - def __init__(self, poetry: Poetry) -> None: + def __init__(self, poetry: Poetry, io: None | IO = None) -> None: self._poetry = poetry + self._io = io or NullIO() def _full_python_path(self, python: str) -> str: try: @@ -533,26 +536,48 @@ def _full_python_path(self, python: str) -> str: return executable - def _detect_active_python(self, io: IO) -> str | None: + def _detect_active_python(self) -> str | None: executable = None try: - io.write_error_line( + self._io.write_error_line( "Trying to detect current active python executable as specified in the" " config.", verbosity=Verbosity.VERBOSE, ) executable = self._full_python_path("python") - io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE) + self._io.write_error_line( + f"Found: {executable}", verbosity=Verbosity.VERBOSE + ) except CalledProcessError: - io.write_error_line( + self._io.write_error_line( "Unable to detect the current active python executable. Falling back to" " default.", verbosity=Verbosity.VERBOSE, ) return executable - def activate(self, python: str, io: IO) -> Env: + def _get_python_version(self) -> tuple[int, int, int]: + version_info = tuple(sys.version_info[:3]) + + if self._poetry.config.get("virtualenvs.prefer-active-python"): + executable = self._detect_active_python() + + if executable: + python_patch = decode( + subprocess.check_output( + list_to_shell_command( + [executable, "-c", GET_PYTHON_VERSION_ONELINER] + ), + shell=True, + ).strip() + ) + + version_info = tuple(int(v) for v in python_patch.split(".")[:3]) + + return cast("tuple[int, int, int]", version_info) + + def activate(self, python: str) -> Env: venv_path = self._poetry.config.virtualenvs_path cwd = self._poetry.file.parent @@ -598,7 +623,7 @@ def activate(self, python: str, io: IO) -> Env: if patch != current_patch: create = True - self.create_venv(io, executable=python, force=create) + self.create_venv(executable=python, force=create) return self.get(reload=True) @@ -632,7 +657,7 @@ def activate(self, python: str, io: IO) -> Env: if patch != current_patch: create = True - self.create_venv(io, executable=python, force=create) + self.create_venv(executable=python, force=create) # Activate envs[base_env_name] = {"minor": minor, "patch": patch} @@ -640,7 +665,7 @@ def activate(self, python: str, io: IO) -> Env: return self.get(reload=True) - def deactivate(self, io: IO) -> None: + def deactivate(self) -> None: venv_path = self._poetry.config.virtualenvs_path name = self.generate_env_name( self._poetry.package.name, str(self._poetry.file.parent) @@ -652,7 +677,7 @@ def deactivate(self, io: IO) -> None: env = envs.get(name) if env is not None: venv = venv_path / f"{name}-py{env['minor']}" - io.write_error_line( + self._io.write_error_line( f"Deactivating virtualenv: {venv}" ) del envs[name] @@ -663,7 +688,7 @@ def get(self, reload: bool = False) -> Env: if self._env is not None and not reload: return self._env - python_minor = ".".join([str(v) for v in sys.version_info[:2]]) + python_minor = ".".join([str(v) for v in self._get_python_version()[:2]]) venv_path = self._poetry.config.virtualenvs_path @@ -855,7 +880,6 @@ def remove(self, python: str) -> Env: def create_venv( self, - io: IO, name: str | None = None, executable: str | None = None, force: bool = False, @@ -888,7 +912,7 @@ def create_venv( venv_prompt = self._poetry.config.get("virtualenvs.prompt") if not executable and prefer_active_python: - executable = self._detect_active_python(io) + executable = self._detect_active_python() venv_path = cwd / ".venv" if root_venv else self._poetry.config.virtualenvs_path if not name: @@ -921,7 +945,7 @@ def create_venv( self._poetry.package.python_versions, python_patch ) - io.write_error_line( + self._io.write_error_line( f"The currently activated Python version {python_patch} is not" f" supported by the project ({self._poetry.package.python_versions}).\n" "Trying to find and use a compatible version. " @@ -944,8 +968,8 @@ def create_venv( python = "python" + python_to_try - if io.is_debug(): - io.write_error_line(f"Trying {python}") + if self._io.is_debug(): + self._io.write_error_line(f"Trying {python}") try: python_patch = decode( @@ -964,7 +988,9 @@ def create_venv( continue if supported_python.allows(Version.parse(python_patch)): - io.write_error_line(f"Using {python} ({python_patch})") + self._io.write_error_line( + f"Using {python} ({python_patch})" + ) executable = python python_minor = ".".join(python_patch.split(".")[:2]) break @@ -989,7 +1015,7 @@ def create_venv( if not venv.exists(): if create_venv is False: - io.write_error_line( + self._io.write_error_line( "" "Skipping virtualenv creation, " "as specified in config file." @@ -998,7 +1024,7 @@ def create_venv( return self.get_system_env() - io.write_error_line( + self._io.write_error_line( f"Creating virtualenv {name} in" f" {venv_path if not WINDOWS else get_real_windows_path(venv_path)!s}" ) @@ -1006,15 +1032,17 @@ def create_venv( create_venv = False if force: if not env.is_sane(): - io.write_error_line( + self._io.write_error_line( f"The virtual environment found in {env.path} seems to" " be broken." ) - io.write_error_line(f"Recreating virtualenv {name} in {venv!s}") + self._io.write_error_line( + f"Recreating virtualenv {name} in {venv!s}" + ) self.remove_venv(venv) create_venv = True - elif io.is_very_verbose(): - io.write_error_line(f"Virtualenv {name} already exists.") + elif self._io.is_very_verbose(): + self._io.write_error_line(f"Virtualenv {name} already exists.") if create_venv: self.build_venv( diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index dcd379302e8..8df2abb175f 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -11,7 +11,6 @@ import pytest import tomlkit -from cleo.io.null_io import NullIO from poetry.core.constraints.version import Version from poetry.core.toml.file import TOMLFile @@ -226,7 +225,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_called_with( Path(tmp_dir) / f"{venv_name}-py3.7", @@ -275,7 +274,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_not_called() @@ -319,7 +318,7 @@ def test_activate_activates_same_virtualenv_with_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.create_venv") - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") m.assert_not_called() @@ -362,7 +361,7 @@ def test_activate_activates_different_virtualenv_with_envs_file( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv) - env = manager.activate("python3.6", NullIO()) + env = manager.activate("python3.6") m.assert_called_with( Path(tmp_dir) / f"{venv_name}-py3.6", @@ -426,7 +425,7 @@ def test_activate_activates_recreates_for_different_patch( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) - env = manager.activate("python3.7", NullIO()) + env = manager.activate("python3.7") build_venv_m.assert_called_with( Path(tmp_dir) / f"{venv_name}-py3.7", @@ -487,7 +486,7 @@ def test_activate_does_not_recreate_when_switching_minor( "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv ) - env = manager.activate("python3.6", NullIO()) + env = manager.activate("python3.6") build_venv_m.assert_not_called() remove_venv_m.assert_not_called() @@ -523,7 +522,7 @@ def test_deactivate_non_activated_but_existing( side_effect=check_output_wrapper(), ) - manager.deactivate(NullIO()) + manager.deactivate() env = manager.get() assert env.path == Path(tmp_dir) / f"{venv_name}-py{python}" @@ -563,7 +562,7 @@ def test_deactivate_activated( side_effect=check_output_wrapper(), ) - manager.deactivate(NullIO()) + manager.deactivate() env = manager.get() assert env.path == Path(tmp_dir) / f"{venv_name}-py{version.major}.{version.minor}" @@ -999,7 +998,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7", @@ -1033,7 +1032,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.9", @@ -1062,7 +1061,7 @@ def test_create_venv_fails_if_no_compatible_python_version_could_be_found( ) with pytest.raises(NoCompatiblePythonVersionFound) as e: - manager.create_venv(NullIO()) + manager.create_venv() expected_message = ( "Poetry was unable to find a compatible version. " @@ -1088,7 +1087,7 @@ def test_create_venv_does_not_try_to_find_compatible_versions_with_executable( ) with pytest.raises(NoCompatiblePythonVersionFound) as e: - manager.create_venv(NullIO(), executable="3.8") + manager.create_venv(executable="3.8") expected_message = ( "The specified Python version (3.8.0) is not supported by the project (^4.8).\n" @@ -1125,7 +1124,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() assert not check_output.called m.assert_called_with( @@ -1165,9 +1164,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv( - NullIO(), executable=f"python{version.major}.{version.minor - 1}" - ) + manager.create_venv(executable=f"python{version.major}.{version.minor - 1}") assert check_output.called m.assert_called_with( @@ -1189,7 +1186,7 @@ def test_create_venv_fails_if_current_python_version_is_not_supported( if "VIRTUAL_ENV" in os.environ: del os.environ["VIRTUAL_ENV"] - manager.create_venv(NullIO()) + manager.create_venv() current_version = Version.parse(".".join(str(c) for c in sys.version_info[:3])) next_version = ".".join( @@ -1199,7 +1196,7 @@ def test_create_venv_fails_if_current_python_version_is_not_supported( poetry.package.python_versions = package_version with pytest.raises(InvalidCurrentPythonVersionError) as e: - manager.create_venv(NullIO()) + manager.create_venv() expected_message = ( f"Current Python version ({current_version}) is not allowed by the project" @@ -1239,7 +1236,7 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( ) m = mocker.patch("poetry.utils.env.EnvManager.build_venv") - manager.activate("python3.7", NullIO()) + manager.activate("python3.7") m.assert_called_with( poetry.file.parent / ".venv", @@ -1472,7 +1469,7 @@ def test_create_venv_accepts_fallback_version_w_nonzero_patchlevel( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() assert check_output.called m.assert_called_with( @@ -1581,7 +1578,7 @@ def test_create_venv_project_name_empty_sets_correct_prompt( "poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: "" ) - manager.create_venv(NullIO()) + manager.create_venv() m.assert_called_with( config_virtualenvs_path / f"{venv_name}-py3.7",