diff --git a/poetry/console/commands/self/update.py b/poetry/console/commands/self/update.py index 8660bea2f09..4283418456f 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,7 @@ from cleo import option from poetry.core.packages import Dependency +from poetry.utils._compat import Path from ..command import Command @@ -60,6 +62,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 +84,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 - self._check_recommended_installation() + 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 + + 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 +190,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 +231,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 +313,62 @@ 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() + + 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) + ) + 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..92453668122 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/bin/poetry") + + 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