From 9246cf3a88cbe921f42feaf50f42dff55e5797b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Fri, 18 Jun 2021 11:42:24 +0200 Subject: [PATCH 1/2] Ensure we don't pick up Poetry's virtualenv as the system env --- poetry/locations.py | 10 ++++++++++ poetry/utils/env.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/poetry/locations.py b/poetry/locations.py index 003950d500d..001e1a9ef66 100644 --- a/poetry/locations.py +++ b/poetry/locations.py @@ -1,9 +1,19 @@ +import os + from .utils._compat import Path from .utils.appdirs import user_cache_dir from .utils.appdirs import user_config_dir +from .utils.appdirs import user_data_dir CACHE_DIR = user_cache_dir("pypoetry") CONFIG_DIR = user_config_dir("pypoetry") REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories" + + +def data_dir(): # type: () -> Path + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME")).expanduser() + + return Path(user_data_dir("pypoetry", roaming=True)) diff --git a/poetry/utils/env.py b/poetry/utils/env.py index 681027ac3a4..d6875245eb7 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -472,7 +472,7 @@ def get(self, reload=False): # type: (bool) -> Env create_venv = self._poetry.config.get("virtualenvs.create", True) if not create_venv: - return SystemEnv(Path(sys.prefix)) + return self.get_system_env() venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: @@ -485,7 +485,7 @@ def get(self, reload=False): # type: (bool) -> Env venv = venv_path / name if not venv.exists(): - return SystemEnv(Path(sys.prefix)) + return self.get_system_env() return VirtualEnv(venv) @@ -790,7 +790,7 @@ def create_venv( p_venv = os.path.normcase(str(venv)) if any(p.startswith(p_venv) for p in paths): # Running properly in the virtualenv, don't need to do anything - return SystemEnv(Path(sys.prefix), self.get_base_prefix()) + return SystemEnv(Path(sys.prefix), Path(self.get_base_prefix())) return VirtualEnv(venv) @@ -833,7 +833,33 @@ def remove_venv(cls, path): # type: (Union[Path,str]) -> None elif file_path.is_dir(): shutil.rmtree(str(file_path)) - def get_base_prefix(self): # type: () -> Path + @classmethod + def get_system_env(cls, naive=False): # type: (bool) -> "SystemEnv" + """ + Retrieve the current Python environment. + This can be the base Python environment or an activated virtual environment. + This method also works around the issue that the virtual environment + used by Poetry internally (when installed via the custom installer) + is incorrectly detected as the system environment. Note that this workaround + happens only when `naive` is False since there are times where we actually + want to retrieve Poetry's custom virtual environment + (e.g. plugin installation or self update). + """ + prefix, base_prefix = Path(sys.prefix), Path(cls.get_base_prefix()) + if naive is False: + from poetry.locations import data_dir + + try: + prefix.relative_to(data_dir()) + except ValueError: + pass + else: + prefix = base_prefix + + return SystemEnv(prefix) + + @classmethod + def get_base_prefix(cls): # type: () -> str if hasattr(sys, "real_prefix"): return sys.real_prefix @@ -993,7 +1019,7 @@ def supported_tags(self): # type: () -> List[Tag] return self._supported_tags @classmethod - def get_base_prefix(cls): # type: () -> Path + def get_base_prefix(cls): # type: () -> str if hasattr(sys, "real_prefix"): return sys.real_prefix From a42bdafa2bcc449dd74f3b8278fe4bb8c36ba18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Fri, 18 Jun 2021 12:17:01 +0200 Subject: [PATCH 2/2] Ensure the self update command is compatible with the new installer --- poetry/console/commands/self/update.py | 147 ++++++++++++++++++++- poetry/packages/locker.py | 5 + tests/console/commands/self/test_update.py | 76 ++++++++++- 3 files changed, 221 insertions(+), 7 deletions(-) diff --git a/poetry/console/commands/self/update.py b/poetry/console/commands/self/update.py index 8660bea2f09..49bfbfa233d 100644 --- a/poetry/console/commands/self/update.py +++ b/poetry/console/commands/self/update.py @@ -4,6 +4,7 @@ import os import re import shutil +import site import stat import subprocess import sys @@ -16,6 +17,8 @@ from cleo import option from poetry.core.packages import Dependency +from poetry.utils._compat import PY2 +from poetry.utils._compat import Path from ..command import Command @@ -60,6 +63,10 @@ class SelfUpdateCommand(Command): REPOSITORY_URL = "https://github.com/python-poetry/poetry" BASE_URL = REPOSITORY_URL + "/releases/download" + _data_dir = None + _bin_dir = None + _pool = None + @property def home(self): from poetry.utils._compat import Path @@ -78,18 +85,75 @@ def lib(self): def lib_backup(self): return self.home / "lib-backup" + @property + def data_dir(self): # type: () -> Path + if self._data_dir is not None: + return self._data_dir + + from poetry.locations import data_dir + + self._data_dir = data_dir() + + return self._data_dir + + @property + def bin_dir(self): # type: () -> Path + if self._data_dir is not None: + return self._data_dir + + from poetry.utils._compat import WINDOWS + + if os.getenv("POETRY_HOME"): + return Path(os.getenv("POETRY_HOME"), "bin").expanduser() + + user_base = site.getuserbase() + + if WINDOWS: + bin_dir = os.path.join(user_base, "Scripts") + else: + bin_dir = os.path.join(user_base, "bin") + + self._bin_dir = Path(bin_dir) + + return self._bin_dir + + @property + def pool(self): + if self._pool is not None: + return self._pool + + from poetry.repositories.pool import Pool + from poetry.repositories.pypi_repository import PyPiRepository + + pool = Pool() + pool.add_repository(PyPiRepository(fallback=False)) + + self._pool = pool + + return self._pool + def handle(self): from poetry.__version__ import __version__ from poetry.core.semver import Version - from poetry.repositories.pypi_repository import PyPiRepository + from poetry.utils.env import EnvManager + + new_update_method = False + try: + self._check_recommended_installation() + except RuntimeError as e: + env = EnvManager.get_system_env(naive=True) + try: + env.path.relative_to(self.data_dir) + except ValueError: + raise e - self._check_recommended_installation() + new_update_method = True version = self.argument("version") if not version: version = ">=" + __version__ - repo = PyPiRepository(fallback=False) + repo = self.pool.repositories[0] packages = repo.find_packages( Dependency("poetry", version, allows_prereleases=self.option("preview")) ) @@ -127,6 +191,9 @@ def handle(self): self.line("You are using the latest version") return + if new_update_method: + return self.update_with_new_method(release.version) + self.update(release) def update(self, release): @@ -165,6 +232,18 @@ def update(self, release): ) ) + def update_with_new_method(self, version): + self.line("Updating Poetry to {}".format(version)) + self.line("") + + self._update_with_new_method(version) + self._make_bin() + + self.line("") + self.line( + "Poetry ({}) is installed now. Great!".format(version) + ) + def _update(self, version): from poetry.utils.helpers import temporary_directory @@ -235,6 +314,68 @@ def _update(self, version): finally: gz.close() + def _update_with_new_method(self, version): + from poetry.config.config import Config + from poetry.core.packages.dependency import Dependency + from poetry.core.packages.project_package import ProjectPackage + from poetry.installation.installer import Installer + from poetry.packages.locker import NullLocker + from poetry.repositories.installed_repository import InstalledRepository + from poetry.utils.env import EnvManager + + env = EnvManager.get_system_env() + installed = InstalledRepository.load(env) + + root = ProjectPackage("poetry-updater", "0.0.0") + root.python_versions = ".".join(str(c) for c in env.version_info[:3]) + root.add_dependency(Dependency("poetry", version.text)) + + installer = Installer( + self.io, + env, + root, + NullLocker(self.data_dir.joinpath("poetry.lock"), {}), + self.pool, + Config(), + installed=installed, + ) + installer.update(True) + installer.run() + + def _make_bin(self): + from poetry.utils._compat import WINDOWS + + self.line("") + self.line("Updating the poetry script") + + self.bin_dir.mkdir(parents=True, exist_ok=True) + + script = "poetry" + target_script = "venv/bin/poetry" + if WINDOWS: + script = "poetry.exe" + target_script = "venv/Scripts/poetry.exe" + + if self.bin_dir.joinpath(script).exists(): + self.bin_dir.joinpath(script).unlink() + + if not PY2 and not WINDOWS: + try: + self.bin_dir.joinpath(script).symlink_to( + self.data_dir.joinpath(target_script) + ) + except OSError: + # This can happen if the user + # does not have the correct permission on Windows + shutil.copy( + self.data_dir.joinpath(target_script), self.bin_dir.joinpath(script) + ) + else: + shutil.copy( + str(self.data_dir.joinpath(target_script)), + str(self.bin_dir.joinpath(script)), + ) + def process(self, *args): return subprocess.check_output(list(args), stderr=subprocess.STDOUT) diff --git a/poetry/packages/locker.py b/poetry/packages/locker.py index 56a77d2dbe6..145007a4e15 100644 --- a/poetry/packages/locker.py +++ b/poetry/packages/locker.py @@ -586,3 +586,8 @@ def _dump_package(self, package): # type: (Package) -> dict data["develop"] = package.develop return data + + +class NullLocker(Locker): + def set_lock_data(self, root, packages): # type: (Package, List[Package]) -> None + pass diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 6e094111ad0..5b86d446fb6 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -5,8 +5,13 @@ from poetry.__version__ import __version__ from poetry.core.packages.package import Package from poetry.core.semver.version import Version +from poetry.factory import Factory +from poetry.repositories.installed_repository import InstalledRepository +from poetry.repositories.pool import Pool +from poetry.repositories.repository import Repository from poetry.utils._compat import WINDOWS from poetry.utils._compat import Path +from poetry.utils.env import EnvManager FIXTURES = Path(__file__).parent.joinpath("fixtures") @@ -25,10 +30,13 @@ def test_self_update_should_install_all_necessary_elements( command = tester._command version = Version.parse(__version__).next_minor.text - mocker.patch( - "poetry.repositories.pypi_repository.PyPiRepository.find_packages", - return_value=[Package("poetry", version)], - ) + repository = Repository() + repository.add_package(Package("poetry", version)) + + pool = Pool() + pool.add_repository(repository) + + command._pool = pool mocker.patch.object(command, "_check_recommended_installation", return_value=None) mocker.patch.object( command, "_get_release_name", return_value="poetry-{}-darwin".format(version) @@ -89,3 +97,63 @@ def test_self_update_should_install_all_necessary_elements( assert lib.exists() assert lib.joinpath("poetry").exists() + + +def test_self_update_can_update_from_recommended_installation( + tester, http, mocker, environ, tmp_venv +): + mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) + target_script = tmp_venv.path.parent.joinpath("venv/bin/poetry") + if WINDOWS: + target_script = tmp_venv.path.parent.joinpath("venv/Scripts/poetry.exe") + + target_script.parent.mkdir(parents=True, exist_ok=True) + target_script.touch() + + command = tester._command + command._data_dir = tmp_venv.path.parent + + new_version = Version.parse(__version__).next_minor.text + + old_poetry = Package("poetry", __version__) + old_poetry.add_dependency(Factory.create_dependency("cleo", "^0.8.2")) + + new_poetry = Package("poetry", new_version) + new_poetry.add_dependency(Factory.create_dependency("cleo", "^1.0.0")) + + installed_repository = Repository() + installed_repository.add_package(old_poetry) + installed_repository.add_package(Package("cleo", "0.8.2")) + + repository = Repository() + repository.add_package(new_poetry) + repository.add_package(Package("cleo", "1.0.0")) + + pool = Pool() + pool.add_repository(repository) + + command._pool = pool + + mocker.patch.object(InstalledRepository, "load", return_value=installed_repository) + + tester.execute() + + expected_output = """\ +Updating Poetry to {} + +Updating dependencies +Resolving dependencies... + +Package operations: 0 installs, 2 updates, 0 removals + + - Updating cleo (0.8.2 -> 1.0.0) + - Updating poetry ({} -> {}) + +Updating the poetry script + +Poetry (1.2.0) is installed now. Great! +""".format( + new_version, __version__, new_version + ) + + assert tester.io.fetch_output() == expected_output