From 4fa7226ef1486fa2c3184efa09163c8898f1bc8c Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 14 Apr 2022 22:34:52 +0200 Subject: [PATCH 01/12] cli: introduce self add/remove/install commands --- src/poetry/console/application.py | 3 + src/poetry/console/commands/add.py | 11 +- src/poetry/console/commands/remove.py | 6 +- src/poetry/console/commands/self/add.py | 23 ++ src/poetry/console/commands/self/install.py | 32 +++ src/poetry/console/commands/self/remove.py | 17 ++ .../console/commands/self/self_command.py | 127 +++++++++ src/poetry/console/commands/self/update.py | 242 +++--------------- src/poetry/utils/helpers.py | 12 + tests/console/commands/self/test_update.py | 103 +++----- 10 files changed, 296 insertions(+), 280 deletions(-) create mode 100644 src/poetry/console/commands/self/add.py create mode 100644 src/poetry/console/commands/self/install.py create mode 100644 src/poetry/console/commands/self/remove.py create mode 100644 src/poetry/console/commands/self/self_command.py diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index c37cd5a677e..04b03f6c42a 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -83,6 +83,9 @@ def _load() -> type[Command]: "plugin remove", "plugin show", # Self commands + "self add", + "self install", + "self remove", "self update", # Source commands "source add", diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 8b88fc810ff..1d00402742a 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -72,10 +72,7 @@ class AddCommand(InstallerCommand, InitCommand): ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] - help = """\ -The add command adds required packages to your pyproject.toml and installs\ - them. - + examples = """\ If you do not specify a version constraint, poetry will choose a suitable one based on\ the available package versions. @@ -91,6 +88,12 @@ class AddCommand(InstallerCommand, InitCommand): - A file path (../my-package/my-package.whl) - A directory (../my-package/) - A url (https://example.com/packages/my-package-0.1.0.tar.gz) +""" + help = f"""\ +The add command adds required packages to your pyproject.toml and installs\ + them. + +{examples} """ loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] diff --git a/src/poetry/console/commands/remove.py b/src/poetry/console/commands/remove.py index c91610d79e9..9a3b6025b4c 100644 --- a/src/poetry/console/commands/remove.py +++ b/src/poetry/console/commands/remove.py @@ -104,11 +104,7 @@ def handle(self) -> int: ) self._installer.set_locker(self.poetry.locker) - # Update packages - self._installer.use_executor( - self.poetry.config.get("experimental.new-installer", False) - ) - + self._installer.set_package(self.poetry.package) self._installer.dry_run(self.option("dry-run", False)) self._installer.verbose(self._io.is_verbose()) self._installer.update(True) diff --git a/src/poetry/console/commands/self/add.py b/src/poetry/console/commands/self/add.py new file mode 100644 index 00000000000..09e5c5b92fa --- /dev/null +++ b/src/poetry/console/commands/self/add.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from poetry.console.commands.add import AddCommand +from poetry.console.commands.self.self_command import SelfCommand + + +class SelfAddCommand(SelfCommand, AddCommand): + name = "self add" + description = "Add additional packages to Poetry's runtime environment." + options = [ + o + for o in AddCommand.options + if o.name in {"editable", "extras", "source", "dry-run", "allow-prereleases"} + ] + help = f"""\ +The self add command installs additional package's to Poetry's runtime \ +environment. + +This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ +file. + +{AddCommand.examples} +""" diff --git a/src/poetry/console/commands/self/install.py b/src/poetry/console/commands/self/install.py new file mode 100644 index 00000000000..64ea0587734 --- /dev/null +++ b/src/poetry/console/commands/self/install.py @@ -0,0 +1,32 @@ +from __future__ import annotations + + +try: + from poetry.core.packages.dependency_group import MAIN_GROUP +except ImportError: + MAIN_GROUP = "default" + +from poetry.console.commands.install import InstallCommand +from poetry.console.commands.self.self_command import SelfCommand + + +class SelfInstallCommand(SelfCommand, InstallCommand): + name = "self install" + description = ( + "Install locked packages (incl. addons) required by this Poetry installation." + ) + options = [o for o in InstallCommand.options if o.name in {"sync", "dry-run"}] + help = f"""\ +The self install command ensures all additional packages specified are \ +installed in the current runtime environment. + +This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ +file. + +You can add more packages using the self add command and remove them using \ +the self remove command. +""" + + @property + def activated_groups(self) -> set[str]: + return {MAIN_GROUP, self.default_group} diff --git a/src/poetry/console/commands/self/remove.py b/src/poetry/console/commands/self/remove.py new file mode 100644 index 00000000000..59f1d2fa614 --- /dev/null +++ b/src/poetry/console/commands/self/remove.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from poetry.console.commands.remove import RemoveCommand +from poetry.console.commands.self.self_command import SelfCommand + + +class SelfRemoveCommand(SelfCommand, RemoveCommand): + name = "self remove" + description = "Remove additional packages from Poetry's runtime environment." + options = [o for o in RemoveCommand.options if o.name in {"dry-run"}] + help = f"""\ +The self remove command removes additional package's to Poetry's runtime \ +environment. + +This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ +file. +""" diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py new file mode 100644 index 00000000000..877981ecf7f --- /dev/null +++ b/src/poetry/console/commands/self/self_command.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING +from typing import cast + +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.project_package import ProjectPackage +from poetry.core.pyproject.toml import PyProjectTOML + +from poetry.__version__ import __version__ +from poetry.console.commands.installer_command import InstallerCommand +from poetry.factory import Factory +from poetry.poetry import Poetry +from poetry.utils.env import EnvManager +from poetry.utils.env import SystemEnv +from poetry.utils.helpers import directory + + +if TYPE_CHECKING: + from poetry.utils.env import Env + + +class SelfCommand(InstallerCommand): + ADDITIONAL_PACKAGE_GROUP = "additional" + + @staticmethod + def get_default_system_pyproject_file() -> Path: + # We separate this out to avoid unwanted side effect during testing while + # maintaining dynamic use in help text. + # + # This is not ideal, but is the simplest solution for now. + from poetry.locations import CONFIG_DIR + + return Path(CONFIG_DIR).joinpath("pyproject.toml") + + @property + def system_pyproject(self) -> Path: + file = self.get_default_system_pyproject_file() + file.parent.mkdir(parents=True, exist_ok=True) + return file + + def reset_env(self) -> None: + self._env = EnvManager.get_system_env(naive=True) + + @property + def env(self) -> Env: + if self._env is None or not isinstance(self._env, SystemEnv): + self.reset_env() + return self._env + + @property + def default_group(self) -> str: + return self.ADDITIONAL_PACKAGE_GROUP + + @property + def activated_groups(self) -> set[str]: + return {self.default_group} + + def generate_system_pyproject(self) -> None: + preserved = {} + + if self.system_pyproject.exists(): + content = PyProjectTOML(self.system_pyproject).poetry_config + + for key in {"group", "source"}: + if key in content: + preserved[key] = content[key] + + package = ProjectPackage(name="poetry-instance", version=__version__) + package.add_dependency(Dependency(name="poetry", constraint=f"^{__version__}")) + + package.python_versions = ".".join(str(v) for v in self.env.version_info[:3]) + + content = Factory.create_pyproject_from_package(package=package) + + for key in preserved: + content[key] = preserved[key] + + self.system_pyproject.write_text(content.as_string(), encoding="utf-8") + + def reset_poetry(self) -> None: + with directory(self.system_pyproject.parent): + self.generate_system_pyproject() + self._poetry = Factory().create_poetry( + self.system_pyproject.parent, io=self._io, disable_plugins=True + ) + + @property + def poetry(self) -> Poetry: + if self._poetry is None: + self.reset_poetry() + return cast(Poetry, self._poetry) + + def _system_project_handle(self) -> int: + """ + This is a helper method that by default calls the handle method implemented in + the child class's next MRO sibling. Override this if you want special handling + either before calling the handle() from the super class or have custom logic + to handle the command. + + The default implementations handles cases where a `self` command delegates + handling to an existing command. Eg: `SelfAddCommand(SelfCommand, AddCommand)`. + """ + return super().handle() + + def reset(self) -> None: + """ + Reset current command instance's environment and poetry instances to ensure + use of the system specific ones. + """ + self.reset_env() + self.reset_poetry() + + def handle(self) -> int: + # We override the base class's handle() method to ensure that poetry and env + # are reset to work within the system project instead of current context. + # Further, during execution, the working directory is temporarily changed + # to parent directory of Poetry system pyproject.toml file. + # + # This method **should not** be overridden in child classes as it may have + # unexpected consequences. + + self.reset() + + with directory(self.system_pyproject.parent): + return self._system_project_handle() diff --git a/src/poetry/console/commands/self/update.py b/src/poetry/console/commands/self/update.py index 4f362db3d85..55ad1b75f35 100644 --- a/src/poetry/console/commands/self/update.py +++ b/src/poetry/console/commands/self/update.py @@ -1,32 +1,26 @@ from __future__ import annotations -import os -import shutil -import site - -from functools import cmp_to_key -from pathlib import Path -from typing import TYPE_CHECKING +from typing import cast from cleo.helpers import argument from cleo.helpers import option +from cleo.io.inputs.string_input import StringInput +from cleo.io.io import IO -from poetry.console.commands.command import Command - - -if TYPE_CHECKING: - from poetry.core.packages.package import Package - from poetry.core.semver.version import Version - - from poetry.repositories.pool import Pool +from poetry.console.application import Application +from poetry.console.commands.add import AddCommand +from poetry.console.commands.self.self_command import SelfCommand -class SelfUpdateCommand(Command): - +class SelfUpdateCommand(SelfCommand): name = "self update" description = "Updates Poetry to the latest version." - arguments = [argument("version", "The version to update to.", optional=True)] + arguments = [ + argument( + "version", "The version to update to.", optional=True, default="latest" + ) + ] options = [ option("preview", None, "Allow the installation of pre-release versions."), option( @@ -36,192 +30,30 @@ class SelfUpdateCommand(Command): "(implicitly enables --verbose).", ), ] - - _data_dir = None - _bin_dir = None - _pool = None - - @property - def data_dir(self) -> 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) -> Path: - if self._data_dir is not None: - return self._data_dir - - from poetry.utils._compat import WINDOWS - - home = os.getenv("POETRY_HOME") - if home: - return Path(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) -> Pool: - 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()) - - return pool - - def handle(self) -> int: - from poetry.core.packages.dependency import Dependency - from poetry.core.semver.version import Version - - from poetry.__version__ import __version__ - - version = self.argument("version") - if not version: - version = ">=" + __version__ - - repo = self.pool.repositories[0] - packages = repo.find_packages( - Dependency("poetry", version, allows_prereleases=self.option("preview")) - ) - if not packages: - self.line("No release found for the specified version") - return 1 - - def cmp(x: Package, y: Package) -> int: - if x.version == y.version: - return 0 - return int(x.version < y.version or -1) - - packages.sort(key=cmp_to_key(cmp)) - - release = None - for package in packages: - if package.is_prerelease(): - if self.option("preview"): - release = package - - break - - continue - - release = package - - break - - if release is None: - self.line("No new release found") - return 1 - - if release.version == Version.parse(__version__): - self.line("You are using the latest version") - return 0 - - self.line(f"Updating Poetry to {release.version}") - self.line("") - - self.update(release) - - self.line("") - self.line( - f"Poetry ({release.version}) is installed now. Great!" - ) - - return 0 - - def update(self, release: Package) -> None: - from poetry.utils.env import EnvManager - - version = release.version - - env = EnvManager.get_system_env(naive=True) - - # We can't use is_relative_to() since it's only available in Python 3.9+ - try: - env.path.relative_to(self.data_dir) - except ValueError: - # Poetry was not installed using the recommended installer - from poetry.console.exceptions import PoetrySimpleConsoleException - - raise PoetrySimpleConsoleException( - "Poetry was not installed with the recommended installer, " - "so it cannot be updated automatically." + help = """\ +The self update command updates Poetry version in its current runtime \ +environment. +""" + + def _system_project_handle(self) -> int: + self.write("Updating Poetry version ...\n\n") + application = cast(Application, self.application) + add_command: AddCommand = cast(AddCommand, application.find("add")) + add_command.set_env(self.env) + application._configure_installer(add_command, self._io) + + argv = ["add", f"poetry@{self.argument('version')}"] + + if self.option("dry-run"): + argv.append("--dry-run") + + if self.option("preview"): + argv.append("--allow-prereleases") + + return add_command.run( + IO( + StringInput(" ".join(argv)), + self._io.output, + self._io.error_output, ) - - self._update(version) - self._make_bin() - - def _update(self, version: Version) -> None: - from poetry.core.packages.dependency import Dependency - from poetry.core.packages.project_package import ProjectPackage - - from poetry.config.config import Config - 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(naive=True) - 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=Config.create(), - installed=installed, ) - installer.update(True) - installer.dry_run(self.option("dry-run")) - installer.run() - - def _make_bin(self) -> None: - 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) - ) diff --git a/src/poetry/utils/helpers.py b/src/poetry/utils/helpers.py index 69743be2f52..0742b58c57f 100644 --- a/src/poetry/utils/helpers.py +++ b/src/poetry/utils/helpers.py @@ -6,9 +6,11 @@ import stat import tempfile +from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import Iterator from typing import Mapping @@ -32,6 +34,16 @@ def module_name(name: str) -> str: return canonicalize_name(name).replace(".", "_").replace("-", "_") +@contextmanager +def directory(path: Path) -> Iterator[Path]: + cwd = Path.cwd() + try: + os.chdir(path) + yield path + finally: + os.chdir(cwd) + + def _on_rm_error(func: Callable[[str], None], path: str, exc_info: Exception) -> None: if not os.path.exists(path): return diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 076683eb90d..220deb66682 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -9,10 +9,7 @@ from poetry.core.semver.version import Version from poetry.__version__ import __version__ -from poetry.console.exceptions import PoetrySimpleConsoleException 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.env import EnvManager @@ -24,28 +21,47 @@ from pytest_mock import MockerFixture from poetry.utils.env import VirtualEnv + from tests.helpers import TestRepository from tests.types import CommandTesterFactory FIXTURES = Path(__file__).parent.joinpath("fixtures") -@pytest.fixture() -def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: - return command_tester_factory("self update") +@pytest.fixture +def installed_repository() -> Repository: + return Repository() -def test_self_update_can_update_from_recommended_installation( - tester: CommandTester, - http: type[httpretty.httpretty], +@pytest.fixture(autouse=True) +def save_environ(environ: None) -> Repository: + yield + + +@pytest.fixture(autouse=True) +def setup_mocks( mocker: MockerFixture, - environ: None, tmp_venv: VirtualEnv, + installed_repository: Repository, + http: type[httpretty.httpretty], ): mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) + mocker.patch("poetry.installation.executor.pip_install") + mocker.patch( + "poetry.installation.installer.Installer._get_installed", + return_value=installed_repository, + ) + + +@pytest.fixture() +def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: + return command_tester_factory("self update") - command = tester.command - command._data_dir = tmp_venv.path.parent +def test_self_update_can_update_from_recommended_installation( + tester: CommandTester, + repo: TestRepository, + installed_repository: TestRepository, +): new_version = Version.parse(__version__).next_minor().text old_poetry = Package("poetry", __version__) @@ -54,73 +70,28 @@ def test_self_update_can_update_from_recommended_installation( 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) + repo.add_package(new_poetry) + repo.add_package(Package("cleo", "1.0.0")) tester.execute() expected_output = f"""\ -Updating Poetry to 1.2.0 +Updating Poetry version ... + +Using version ^{new_version} for poetry Updating dependencies Resolving dependencies... -Package operations: 0 installs, 2 updates, 0 removals - - - Updating cleo (0.8.2 -> 1.0.0) - - Updating poetry ({__version__} -> {new_version}) +Writing lock file -Updating the poetry script +Package operations: 0 installs, 2 updates, 0 removals -Poetry ({new_version}) is installed now. Great! + • Updating cleo (0.8.2 -> 1.0.0) + • Updating poetry ({__version__} -> {new_version}) """ assert tester.io.fetch_output() == expected_output - - -def test_self_update_does_not_update_non_recommended_installation( - tester: CommandTester, - http: type[httpretty.httpretty], - mocker: MockerFixture, - environ: None, - tmp_venv: VirtualEnv, -): - mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) - - command = tester.command - - 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 - - with pytest.raises(PoetrySimpleConsoleException): - tester.execute() From 153b8f071fd6d08e33e92dddedfbd946001e4e3d Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 15 Apr 2022 14:13:09 +0200 Subject: [PATCH 02/12] cli: add self show command --- src/poetry/console/application.py | 1 + src/poetry/console/commands/group_command.py | 2 +- .../console/commands/self/show/__init__.py | 33 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/poetry/console/commands/self/show/__init__.py diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 04b03f6c42a..3ba194827cc 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -87,6 +87,7 @@ def _load() -> type[Command]: "self install", "self remove", "self update", + "self show", # Source commands "source add", "source remove", diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index 4e84f0da5e1..b1b9395e5c0 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -85,7 +85,7 @@ def activated_groups(self) -> set[str]: for key in {"with", "without", "only"}: groups[key] = { group.strip() - for groups in self.option(key) + for groups in self.option(key, "") for group in groups.split(",") } diff --git a/src/poetry/console/commands/self/show/__init__.py b/src/poetry/console/commands/self/show/__init__.py new file mode 100644 index 00000000000..b13a8873d23 --- /dev/null +++ b/src/poetry/console/commands/self/show/__init__.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from cleo.helpers import option + +from poetry.console.commands.self.self_command import SelfCommand +from poetry.console.commands.show import ShowCommand + + +class SelfShowCommand(SelfCommand, ShowCommand): + name = "self show" + options = [ + option("addons", None, "List only add-on packages installed."), + *[o for o in ShowCommand.options if o.name in {"tree", "latest", "outdated"}], + ] + description = "Show packages from Poetry's runtime environment." + help = f"""\ +The self show command behaves similar to the show command, but +working within Poetry's runtime environment. This lists all packages installed within +the Poetry install environment. + +To show only additional packages that have been added via self add and their +dependencies use self show --addons. + +This is managed in the {SelfCommand.get_default_system_pyproject_file()} \ +file. +""" + + @property + def activated_groups(self) -> set[str]: + if self.option("addons", False): + return {SelfCommand.ADDITIONAL_PACKAGE_GROUP} + + return super(ShowCommand, self).activated_groups From 411bd750256a7dbaafb2b19e957f7bc8fbc562d2 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 15 Apr 2022 14:20:38 +0200 Subject: [PATCH 03/12] cli: plugins show -> self show plugins --- src/poetry/console/application.py | 1 + src/poetry/console/commands/plugin/show.py | 89 ++------ .../console/commands/self/show/plugins.py | 95 +++++++++ tests/console/commands/plugin/test_show.py | 176 +--------------- .../commands/self/test_show_plugins.py | 192 ++++++++++++++++++ 5 files changed, 312 insertions(+), 241 deletions(-) create mode 100644 src/poetry/console/commands/self/show/plugins.py create mode 100644 tests/console/commands/self/test_show_plugins.py diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 3ba194827cc..298a1a2fe17 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -88,6 +88,7 @@ def _load() -> type[Command]: "self remove", "self update", "self show", + "self show plugins", # Source commands "source add", "source remove", diff --git a/src/poetry/console/commands/plugin/show.py b/src/poetry/console/commands/plugin/show.py index 2ff223914cc..9481f591fbd 100644 --- a/src/poetry/console/commands/plugin/show.py +++ b/src/poetry/console/commands/plugin/show.py @@ -1,9 +1,13 @@ from __future__ import annotations -from collections import defaultdict -from typing import Any +from typing import cast +from cleo.io.inputs.string_input import StringInput +from cleo.io.io import IO + +from poetry.console.application import Application from poetry.console.commands.command import Command +from poetry.console.commands.self.show.plugins import SelfShowPluginsCommand class PluginShowCommand(Command): @@ -11,75 +15,22 @@ class PluginShowCommand(Command): name = "plugin show" description = "Shows information about the currently installed plugins." + help = ( + "This command is deprecated. Use self show plugins " + "command instead." + ) def handle(self) -> int: - from poetry.plugins.application_plugin import ApplicationPlugin - from poetry.plugins.plugin import Plugin - from poetry.plugins.plugin_manager import PluginManager - from poetry.repositories.installed_repository import InstalledRepository - from poetry.utils.env import EnvManager - from poetry.utils.helpers import canonicalize_name - from poetry.utils.helpers import pluralize - - plugins: dict[str, dict[str, Any]] = defaultdict( - lambda: { - "package": None, - "plugins": [], - "application_plugins": [], - } - ) + self.line_error(self.help) - entry_points = ( - PluginManager(ApplicationPlugin.group).get_plugin_entry_points() - + PluginManager(Plugin.group).get_plugin_entry_points() + application = cast(Application, self.application) + command: SelfShowPluginsCommand = cast( + SelfShowPluginsCommand, application.find("self show plugins") ) - - system_env = EnvManager.get_system_env(naive=True) - installed_repository = InstalledRepository.load( - system_env, with_dependencies=True + return command.run( + IO( + StringInput(""), + self._io.output, + self._io.error_output, + ) ) - - packages_by_name = {pkg.name: pkg for pkg in installed_repository.packages} - - for entry_point in entry_points: - plugin = entry_point.load() - category = "plugins" - if issubclass(plugin, ApplicationPlugin): - category = "application_plugins" - - assert entry_point.distro is not None - package = packages_by_name[canonicalize_name(entry_point.distro.name)] - plugins[package.pretty_name]["package"] = package - plugins[package.pretty_name][category].append(entry_point) - - for name, info in plugins.items(): - package = info["package"] - description = " " + package.description if package.description else "" - self.line("") - self.line(f" • {name} ({package.version}){description}") - provide_line = " " - if info["plugins"]: - count = len(info["plugins"]) - provide_line += f" {count} plugin{pluralize(count)}" - - if info["application_plugins"]: - if info["plugins"]: - provide_line += " and" - - count = len(info["application_plugins"]) - provide_line += ( - f" {count} application plugin{pluralize(count)}" - ) - - self.line(provide_line) - - if package.requires: - self.line("") - self.line(" Dependencies") - for dependency in package.requires: - self.line( - f" - {dependency.pretty_name}" - f" ({dependency.pretty_constraint})" - ) - - return 0 diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py new file mode 100644 index 00000000000..381e1008119 --- /dev/null +++ b/src/poetry/console/commands/self/show/plugins.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from collections import defaultdict +from typing import TYPE_CHECKING +from typing import DefaultDict + +from poetry.console.commands.self.self_command import SelfCommand + + +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + +class SelfShowPluginsCommand(SelfCommand): + name = "self show plugins" + description = "Shows information about the currently installed plugins." + help = """\ +The self show plugins command lists all installed Poetry plugins. + +Plugins can be added and removed using the self add and self remove \ +commands respectively. + +This command does not list packages that do not provide a Poetry plugin. +""" + + def _system_project_handle(self) -> int: + from poetry.plugins.application_plugin import ApplicationPlugin + from poetry.plugins.plugin import Plugin + from poetry.plugins.plugin_manager import PluginManager + from poetry.repositories.installed_repository import InstalledRepository + from poetry.utils.env import EnvManager + from poetry.utils.helpers import canonicalize_name + from poetry.utils.helpers import pluralize + + plugins: DefaultDict[str, dict[str, Package | list[str]]] = defaultdict( + lambda: { + "package": None, + "plugins": [], + "application_plugins": [], + } + ) + + entry_points = ( + PluginManager(ApplicationPlugin.group).get_plugin_entry_points() + + PluginManager(Plugin.group).get_plugin_entry_points() + ) + + system_env = EnvManager.get_system_env(naive=True) + installed_repository = InstalledRepository.load( + system_env, with_dependencies=True + ) + + packages_by_name = {pkg.name: pkg for pkg in installed_repository.packages} + + for entry_point in entry_points: + plugin = entry_point.load() + category = "plugins" + if issubclass(plugin, ApplicationPlugin): + category = "application_plugins" + + package = packages_by_name[canonicalize_name(entry_point.distro.name)] + plugins[package.pretty_name]["package"] = package + plugins[package.pretty_name][category].append(entry_point) + + for name, info in plugins.items(): + package = info["package"] + description = " " + package.description if package.description else "" + self.line("") + self.line(f" • {name} ({package.version}){description}") + provide_line = " " + if info["plugins"]: + count = len(info["plugins"]) + provide_line += f" {count} plugin{pluralize(count)}" + + if info["application_plugins"]: + if info["plugins"]: + provide_line += " and" + + count = len(info["application_plugins"]) + provide_line += ( + f" {count} application plugin{pluralize(count)}" + ) + + self.line(provide_line) + + if package.requires: + self.line("") + self.line(" Dependencies") + for dependency in package.requires: + self.line( + f" - {dependency.pretty_name}" + f" ({dependency.pretty_constraint})" + ) + + return 0 diff --git a/tests/console/commands/plugin/test_show.py b/tests/console/commands/plugin/test_show.py index 5b6a888c24b..719c14fec17 100644 --- a/tests/console/commands/plugin/test_show.py +++ b/tests/console/commands/plugin/test_show.py @@ -4,189 +4,21 @@ import pytest -from entrypoints import Distribution -from entrypoints import EntryPoint as _EntryPoint -from poetry.core.packages.package import Package - -from poetry.factory import Factory -from poetry.plugins.application_plugin import ApplicationPlugin -from poetry.plugins.plugin import Plugin - if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester - from pytest_mock import MockerFixture - from poetry.plugins.base_plugin import BasePlugin - from poetry.repositories import Repository - from tests.helpers import PoetryTestApplication from tests.types import CommandTesterFactory -class EntryPoint(_EntryPoint): - def load(self) -> type[BasePlugin]: - if "ApplicationPlugin" in self.object_name: - return ApplicationPlugin - - return Plugin - - @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("plugin show") -@pytest.fixture() -def plugin_package() -> Package: - return Package("poetry-plugin", "1.2.3") - - -@pytest.fixture() -def plugin_distro(plugin_package: Package) -> Distribution: - return Distribution(plugin_package.name, plugin_package.version.to_string()) - - -@pytest.mark.parametrize("entrypoint_name", ["poetry-plugin", "not-package-name"]) -def test_show_displays_installed_plugins( - app: PoetryTestApplication, - tester: CommandTester, - installed: Repository, - mocker: MockerFixture, - plugin_package: Package, - plugin_distro: Distribution, - entrypoint_name: str, -): - mocker.patch( - "entrypoints.get_group_all", - side_effect=[ - [ - EntryPoint( - entrypoint_name, - "poetry_plugin.plugins:ApplicationPlugin", - "FirstApplicationPlugin", - distro=plugin_distro, - ) - ], - [ - EntryPoint( - entrypoint_name, - "poetry_plugin.plugins:Plugin", - "FirstPlugin", - distro=plugin_distro, - ) - ], - ], - ) - - installed.add_package(plugin_package) - - tester.execute("") - - expected = """ - • poetry-plugin (1.2.3) - 1 plugin and 1 application plugin -""" - - assert tester.io.fetch_output() == expected - - -def test_show_displays_installed_plugins_with_multiple_plugins( - app: PoetryTestApplication, - tester: CommandTester, - installed: Repository, - mocker: MockerFixture, - plugin_package: Package, - plugin_distro: Distribution, -): - mocker.patch( - "entrypoints.get_group_all", - side_effect=[ - [ - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:ApplicationPlugin", - "FirstApplicationPlugin", - distro=plugin_distro, - ), - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:ApplicationPlugin", - "SecondApplicationPlugin", - distro=plugin_distro, - ), - ], - [ - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:Plugin", - "FirstPlugin", - distro=plugin_distro, - ), - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:Plugin", - "SecondPlugin", - distro=plugin_distro, - ), - ], - ], - ) - - installed.add_package(plugin_package) - +def test_deprecation_warning(tester: CommandTester) -> None: tester.execute("") - - expected = """ - • poetry-plugin (1.2.3) - 2 plugins and 2 application plugins -""" - - assert tester.io.fetch_output() == expected - - -def test_show_displays_installed_plugins_with_dependencies( - app: PoetryTestApplication, - tester: CommandTester, - installed: Repository, - mocker: MockerFixture, - plugin_package: Package, - plugin_distro: Distribution, -): - mocker.patch( - "entrypoints.get_group_all", - side_effect=[ - [ - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:ApplicationPlugin", - "FirstApplicationPlugin", - distro=plugin_distro, - ) - ], - [ - EntryPoint( - "poetry-plugin", - "poetry_plugin.plugins:Plugin", - "FirstPlugin", - distro=plugin_distro, - ) - ], - ], + assert ( + tester.io.fetch_error() + == "This command is deprecated. Use self show plugins command instead.\n" ) - - plugin_package.add_dependency(Factory.create_dependency("foo", ">=1.2.3")) - plugin_package.add_dependency(Factory.create_dependency("bar", "<4.5.6")) - installed.add_package(plugin_package) - - tester.execute("") - - expected = """ - • poetry-plugin (1.2.3) - 1 plugin and 1 application plugin - - Dependencies - - foo (>=1.2.3) - - bar (<4.5.6) -""" - - assert tester.io.fetch_output() == expected diff --git a/tests/console/commands/self/test_show_plugins.py b/tests/console/commands/self/test_show_plugins.py new file mode 100644 index 00000000000..b519d938c73 --- /dev/null +++ b/tests/console/commands/self/test_show_plugins.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from entrypoints import Distribution +from entrypoints import EntryPoint as _EntryPoint +from poetry.core.packages.package import Package + +from poetry.factory import Factory +from poetry.plugins.application_plugin import ApplicationPlugin +from poetry.plugins.plugin import Plugin + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + from pytest_mock import MockerFixture + + from poetry.plugins.base_plugin import BasePlugin + from poetry.repositories import Repository + from tests.helpers import PoetryTestApplication + from tests.types import CommandTesterFactory + + +class EntryPoint(_EntryPoint): + def load(self) -> type[BasePlugin]: + if "ApplicationPlugin" in self.object_name: + return ApplicationPlugin + + return Plugin + + +@pytest.fixture() +def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: + return command_tester_factory("self show plugins") + + +@pytest.fixture() +def plugin_package() -> Package: + return Package("poetry-plugin", "1.2.3") + + +@pytest.fixture() +def plugin_distro(plugin_package: Package) -> Distribution: + return Distribution(plugin_package.name, plugin_package.version.to_string(True)) + + +@pytest.mark.parametrize("entrypoint_name", ["poetry-plugin", "not-package-name"]) +def test_show_displays_installed_plugins( + app: PoetryTestApplication, + tester: CommandTester, + installed: Repository, + mocker: MockerFixture, + plugin_package: Package, + plugin_distro: Distribution, + entrypoint_name: str, +): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + entrypoint_name, + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + distro=plugin_distro, + ) + ], + [ + EntryPoint( + entrypoint_name, + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + distro=plugin_distro, + ) + ], + ], + ) + + installed.add_package(plugin_package) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 1 plugin and 1 application plugin +""" + + assert tester.io.fetch_output() == expected + + +def test_show_displays_installed_plugins_with_multiple_plugins( + app: PoetryTestApplication, + tester: CommandTester, + installed: Repository, + mocker: MockerFixture, + plugin_package: Package, + plugin_distro: Distribution, +): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + distro=plugin_distro, + ), + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "SecondApplicationPlugin", + distro=plugin_distro, + ), + ], + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + distro=plugin_distro, + ), + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "SecondPlugin", + distro=plugin_distro, + ), + ], + ], + ) + + installed.add_package(plugin_package) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 2 plugins and 2 application plugins +""" + + assert tester.io.fetch_output() == expected + + +def test_show_displays_installed_plugins_with_dependencies( + app: PoetryTestApplication, + tester: CommandTester, + installed: Repository, + mocker: MockerFixture, + plugin_package: Package, + plugin_distro: Distribution, +): + mocker.patch( + "entrypoints.get_group_all", + side_effect=[ + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:ApplicationPlugin", + "FirstApplicationPlugin", + distro=plugin_distro, + ) + ], + [ + EntryPoint( + "poetry-plugin", + "poetry_plugin.plugins:Plugin", + "FirstPlugin", + distro=plugin_distro, + ) + ], + ], + ) + + plugin_package.add_dependency(Factory.create_dependency("foo", ">=1.2.3")) + plugin_package.add_dependency(Factory.create_dependency("bar", "<4.5.6")) + installed.add_package(plugin_package) + + tester.execute("") + + expected = """ + • poetry-plugin (1.2.3) + 1 plugin and 1 application plugin + + Dependencies + - foo (>=1.2.3) + - bar (<4.5.6) +""" + + assert tester.io.fetch_output() == expected From 973e42006414830af312d47ab2d3fead557d211a Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 15 Apr 2022 18:25:46 +0200 Subject: [PATCH 04/12] cmd/remove: handle missing group sections --- src/poetry/console/commands/remove.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/poetry/console/commands/remove.py b/src/poetry/console/commands/remove.py index 9a3b6025b4c..3e04c0132df 100644 --- a/src/poetry/console/commands/remove.py +++ b/src/poetry/console/commands/remove.py @@ -81,12 +81,17 @@ def handle(self) -> int: if not poetry_content["dev-dependencies"]: del poetry_content["dev-dependencies"] else: - removed = self._remove_packages( - packages, poetry_content["group"][group].get("dependencies", {}), group - ) - - if not poetry_content["group"][group]: - del poetry_content["group"][group] + removed = [] + if "group" in poetry_content: + if group in poetry_content["group"]: + removed = self._remove_packages( + packages, + poetry_content["group"][group].get("dependencies", {}), + group, + ) + + if not poetry_content["group"][group]: + del poetry_content["group"][group] if "group" in poetry_content and not poetry_content["group"]: del poetry_content["group"] From b08495f93ae61a622df42fbeb9f109a929ef7d1f Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 15 Apr 2022 18:26:54 +0200 Subject: [PATCH 05/12] deprecate plugin commands and route to self --- src/poetry/console/commands/add.py | 14 +- src/poetry/console/commands/plugin/add.py | 177 ++---------------- src/poetry/console/commands/plugin/remove.py | 43 ++--- src/poetry/console/commands/self/add.py | 17 ++ tests/console/commands/plugin/conftest.py | 64 ------- tests/console/commands/plugin/test_remove.py | 140 -------------- tests/console/commands/self/conftest.py | 56 ++++++ .../deprecated_plugin}/__init__.py | 0 .../self/deprecated_plugin/test_add.py | 31 +++ .../self/deprecated_plugin/test_remove.py | 56 ++++++ .../deprecated_plugin}/test_show.py | 0 .../test_add.py => self/test_add_plugins.py} | 122 +++++------- .../commands/self/test_remove_plugins.py | 108 +++++++++++ tests/console/commands/self/test_update.py | 37 +--- tests/console/commands/self/utils.py | 33 ++++ 15 files changed, 391 insertions(+), 507 deletions(-) delete mode 100644 tests/console/commands/plugin/conftest.py delete mode 100644 tests/console/commands/plugin/test_remove.py create mode 100644 tests/console/commands/self/conftest.py rename tests/console/commands/{plugin => self/deprecated_plugin}/__init__.py (100%) create mode 100644 tests/console/commands/self/deprecated_plugin/test_add.py create mode 100644 tests/console/commands/self/deprecated_plugin/test_remove.py rename tests/console/commands/{plugin => self/deprecated_plugin}/test_show.py (100%) rename tests/console/commands/{plugin/test_add.py => self/test_add_plugins.py} (65%) create mode 100644 tests/console/commands/self/test_remove_plugins.py create mode 100644 tests/console/commands/self/utils.py diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py index 1d00402742a..6c3b859c29c 100644 --- a/src/poetry/console/commands/add.py +++ b/src/poetry/console/commands/add.py @@ -278,6 +278,14 @@ def get_existing_packages_from_input( return existing_packages + @property + def _hint_update_packages(self) -> str: + return ( + "\nIf you want to update it to the latest compatible version, you can use" + " `poetry update package`.\nIf you prefer to upgrade it to the latest" + " available version, you can use `poetry add package@latest`.\n" + ) + def notify_about_existing_packages(self, existing_packages: list[str]) -> None: self.line( "The following packages are already present in the pyproject.toml and will" @@ -285,8 +293,4 @@ def notify_about_existing_packages(self, existing_packages: list[str]) -> None: ) for name in existing_packages: self.line(f" • {name}") - self.line( - "\nIf you want to update it to the latest compatible version, you can use" - " `poetry update package`.\nIf you prefer to upgrade it to the latest" - " available version, you can use `poetry add package@latest`.\n" - ) + self.line(self._hint_update_packages) diff --git a/src/poetry/console/commands/plugin/add.py b/src/poetry/console/commands/plugin/add.py index ea41fbb1f71..0491e2bea5f 100644 --- a/src/poetry/console/commands/plugin/add.py +++ b/src/poetry/console/commands/plugin/add.py @@ -1,16 +1,15 @@ from __future__ import annotations -import os - -from typing import Any from typing import cast from cleo.helpers import argument from cleo.helpers import option +from cleo.io.inputs.string_input import StringInput +from cleo.io.io import IO from poetry.console.application import Application from poetry.console.commands.init import InitCommand -from poetry.console.commands.update import UpdateCommand +from poetry.console.commands.self.add import SelfAddCommand class PluginAddCommand(InitCommand): @@ -31,146 +30,33 @@ class PluginAddCommand(InitCommand): " --verbose).", ) ] - - help = """ + deprecation = ( + "This command is deprecated. Use self add command instead." + "" + ) + help = f""" The plugin add command installs Poetry plugins globally. It works similarly to the add command: -If you do not specify a version constraint, poetry will choose a suitable one based on\ - the available package versions. - -You can specify a package in the following forms: - - - A single name (requests) - - A name and a constraint (requests@^2.23.0) - - A git url (git+https://github.com/python-poetry/poetry.git) - - A git url with a revision\ - (git+https://github.com/python-poetry/poetry.git#develop) - - A git SSH url (git+ssh://github.com/python-poetry/poetry.git) - - A git SSH url with a revision\ - (git+ssh://github.com/python-poetry/poetry.git#develop) - - A file path (../my-package/my-package.whl) - - A directory (../my-package/) - - A url (https://example.com/packages/my-package-0.1.0.tar.gz)\ +{SelfAddCommand.examples} + +{deprecation} """ def handle(self) -> int: - from pathlib import Path - - import tomlkit - - from cleo.io.inputs.string_input import StringInput - from cleo.io.io import IO - from poetry.core.packages.project_package import ProjectPackage - from poetry.core.pyproject.toml import PyProjectTOML - from poetry.core.semver.helpers import parse_constraint - - from poetry.factory import Factory - from poetry.repositories.installed_repository import InstalledRepository - from poetry.utils.env import EnvManager - - plugins = self.argument("plugins") - - # Plugins should be installed in the system env to be globally available - system_env = EnvManager.get_system_env(naive=True) - - env_dir = Path(os.getenv("POETRY_HOME") or system_env.path) - - # We check for the plugins existence first. - if env_dir.joinpath("pyproject.toml").exists(): - pyproject: dict[str, Any] = tomlkit.loads( - env_dir.joinpath("pyproject.toml").read_text(encoding="utf-8") - ) - poetry_content = pyproject["tool"]["poetry"] - existing_packages = self.get_existing_packages_from_input( - plugins, poetry_content, "dependencies" - ) - - if existing_packages: - self.notify_about_existing_packages(existing_packages) - - plugins = [plugin for plugin in plugins if plugin not in existing_packages] - - if not plugins: - return 0 - - plugins = self._determine_requirements(plugins) - - # We retrieve the packages installed in the system environment. - # We assume that this environment will be a self contained virtual environment - # built by the official installer or by pipx. - # If not, it might lead to side effects since other installed packages might not - # be required by Poetry but still be taken into account when resolving - # dependencies. - installed_repository = InstalledRepository.load( - system_env, with_dependencies=True - ) + self.line_error(self.deprecation) - root_package = None - for package in installed_repository.packages: - if package.name == "poetry": - root_package = ProjectPackage(package.name, package.version) - for dependency in package.requires: - root_package.add_dependency(dependency) - - break - - assert root_package is not None - - root_package.python_versions = ".".join( - str(v) for v in system_env.version_info[:3] - ) - # We create a `pyproject.toml` file based on all the information - # we have about the current environment. - if not env_dir.joinpath("pyproject.toml").exists(): - Factory.create_pyproject_from_package( - root_package, - env_dir, - ) - - # We add the plugins to the dependencies section of the previously - # created `pyproject.toml` file - pyproject_toml = PyProjectTOML(env_dir.joinpath("pyproject.toml")) - poetry_content = pyproject_toml.poetry_config - poetry_dependency_section = poetry_content["dependencies"] - plugin_names = [] - for plugin in plugins: - if "version" in plugin: - # Validate version constraint - parse_constraint(plugin["version"]) - - constraint: dict[str, Any] = tomlkit.inline_table() - for name, value in plugin.items(): - if name == "name": - continue - - constraint[name] = value - - if len(constraint) == 1 and "version" in constraint: - constraint = constraint["version"] - - poetry_dependency_section[plugin["name"]] = constraint - plugin_names.append(plugin["name"]) + application = cast(Application, self.application) + command: SelfAddCommand = cast(SelfAddCommand, application.find("self add")) + application._configure_installer(command, self.io) - pyproject_toml.save() + argv: list[str] = ["add", *self.argument("plugins")] - # From this point forward, all the logic will be deferred to - # the update command, by using the previously created `pyproject.toml` - # file. - application = cast(Application, self.application) - update_command: UpdateCommand = cast(UpdateCommand, application.find("update")) - # We won't go through the event dispatching done by the application - # so we need to configure the command manually - update_command.set_poetry(Factory().create_poetry(env_dir)) - update_command.set_env(system_env) - application._configure_installer(update_command, self._io) - - argv = ["update"] + plugin_names - if self.option("dry-run"): + if self.option("--dry-run"): argv.append("--dry-run") - exit_code: int = update_command.run( + exit_code: int = command.run( IO( StringInput(" ".join(argv)), self._io.output, @@ -178,30 +64,3 @@ def handle(self) -> int: ) ) return exit_code - - def get_existing_packages_from_input( - self, packages: list[str], poetry_content: dict[str, Any], target_section: str - ) -> list[str]: - existing_packages = [] - - for name in packages: - for key in poetry_content[target_section]: - if key.lower() == name.lower(): - existing_packages.append(name) - - return existing_packages - - def notify_about_existing_packages(self, existing_packages: list[str]) -> None: - self.line( - "The following plugins are already present in the " - "pyproject.toml file and will be skipped:\n" - ) - for name in existing_packages: - self.line(f" • {name}") - - self.line( - "\nIf you want to update it to the latest compatible version, " - "you can use `poetry plugin update package`.\n" - "If you prefer to upgrade it to the latest available version, " - "you can use `poetry plugin add package@latest`.\n" - ) diff --git a/src/poetry/console/commands/plugin/remove.py b/src/poetry/console/commands/plugin/remove.py index b7918feafa2..6f4274230a3 100644 --- a/src/poetry/console/commands/plugin/remove.py +++ b/src/poetry/console/commands/plugin/remove.py @@ -1,15 +1,15 @@ from __future__ import annotations -import os - from typing import cast from cleo.helpers import argument from cleo.helpers import option +from cleo.io.inputs.string_input import StringInput +from cleo.io.io import IO from poetry.console.application import Application from poetry.console.commands.command import Command -from poetry.console.commands.remove import RemoveCommand +from poetry.console.commands.self.remove import SelfRemoveCommand class PluginRemoveCommand(Command): @@ -31,35 +31,26 @@ class PluginRemoveCommand(Command): ) ] - def handle(self) -> int: - from pathlib import Path - - from cleo.io.inputs.string_input import StringInput - from cleo.io.io import IO - - from poetry.factory import Factory - from poetry.utils.env import EnvManager + help = ( + "This command is deprecated. Use self remove command instead." + "" + ) - plugins = self.argument("plugins") - - system_env = EnvManager.get_system_env(naive=True) - env_dir = Path(os.getenv("POETRY_HOME") or system_env.path) + def handle(self) -> int: + self.line_error(self.help) - # From this point forward, all the logic will be deferred to - # the remove command, by using the global `pyproject.toml` file. application = cast(Application, self.application) - remove_command: RemoveCommand = cast(RemoveCommand, application.find("remove")) - # We won't go through the event dispatching done by the application - # so we need to configure the command manually - remove_command.set_poetry(Factory().create_poetry(env_dir)) - remove_command.set_env(system_env) - application._configure_installer(remove_command, self._io) + command: SelfRemoveCommand = cast( + SelfRemoveCommand, application.find("self remove") + ) + application._configure_installer(command, self.io) + + argv: list[str] = ["remove", *self.argument("plugins")] - argv = ["remove"] + plugins - if self.option("dry-run"): + if self.option("--dry-run"): argv.append("--dry-run") - exit_code: int = remove_command.run( + exit_code: int = command.run( IO( StringInput(" ".join(argv)), self._io.output, diff --git a/src/poetry/console/commands/self/add.py b/src/poetry/console/commands/self/add.py index 09e5c5b92fa..487239d43cb 100644 --- a/src/poetry/console/commands/self/add.py +++ b/src/poetry/console/commands/self/add.py @@ -1,5 +1,8 @@ from __future__ import annotations +from poetry.core.semver.version import Version + +from poetry.__version__ import __version__ from poetry.console.commands.add import AddCommand from poetry.console.commands.self.self_command import SelfCommand @@ -21,3 +24,17 @@ class SelfAddCommand(SelfCommand, AddCommand): {AddCommand.examples} """ + + @property + def _hint_update_packages(self) -> str: + version = Version.parse(__version__) + flags = "" + + if not version.is_stable(): + flags = " --preview" + + return ( + "\nIf you want to update it to the latest compatible version, you can use" + f" `poetry self update{flags}`.\nIf you prefer to upgrade it to the latest" + " available version, you can use `poetry self add package@latest`.\n" + ) diff --git a/tests/console/commands/plugin/conftest.py b/tests/console/commands/plugin/conftest.py deleted file mode 100644 index 025772c805f..00000000000 --- a/tests/console/commands/plugin/conftest.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from poetry.core.packages.package import Package - -from poetry.__version__ import __version__ -from poetry.factory import Factory -from poetry.repositories.installed_repository import InstalledRepository -from poetry.repositories.pool import Pool -from poetry.utils.env import EnvManager - - -if TYPE_CHECKING: - from cleo.io.io import IO - from pytest_mock import MockerFixture - - from poetry.config.config import Config - from poetry.config.source import Source - from poetry.poetry import Poetry - from poetry.repositories import Repository - from poetry.utils.env import MockEnv - from tests.helpers import TestRepository - from tests.types import SourcesFactory - - -@pytest.fixture() -def installed() -> InstalledRepository: - repository = InstalledRepository() - - repository.add_package(Package("poetry", __version__)) - - return repository - - -def configure_sources_factory(repo: TestRepository) -> SourcesFactory: - def _configure_sources( - poetry: Poetry, - sources: Source, - config: Config, - io: IO, - disable_cache: bool = False, - ) -> None: - pool = Pool() - pool.add_repository(repo) - poetry.set_pool(pool) - - return _configure_sources - - -@pytest.fixture(autouse=True) -def setup_mocks( - mocker: MockerFixture, - env: MockEnv, - repo: TestRepository, - installed: Repository, -) -> None: - mocker.patch.object(EnvManager, "get_system_env", return_value=env) - mocker.patch.object(InstalledRepository, "load", return_value=installed) - mocker.patch.object( - Factory, "configure_sources", side_effect=configure_sources_factory(repo) - ) diff --git a/tests/console/commands/plugin/test_remove.py b/tests/console/commands/plugin/test_remove.py deleted file mode 100644 index 646ad50de5a..00000000000 --- a/tests/console/commands/plugin/test_remove.py +++ /dev/null @@ -1,140 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -import tomlkit - -from poetry.core.packages.package import Package - -from poetry.__version__ import __version__ -from poetry.layouts.layout import POETRY_DEFAULT - - -if TYPE_CHECKING: - from cleo.testers.command_tester import CommandTester - - from poetry.console.commands.remove import RemoveCommand - from poetry.repositories import Repository - from poetry.utils.env import MockEnv - from tests.helpers import PoetryTestApplication - from tests.types import CommandTesterFactory - - -@pytest.fixture() -def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: - return command_tester_factory("plugin remove") - - -@pytest.fixture() -def pyproject(env: MockEnv) -> None: - pyproject = tomlkit.loads(POETRY_DEFAULT) - content = pyproject["tool"]["poetry"] - - content["name"] = "poetry" - content["version"] = __version__ - content["description"] = "" - content["authors"] = ["Sébastien Eustace "] - - dependency_section = content["dependencies"] - dependency_section["python"] = "^3.6" - - env.path.joinpath("pyproject.toml").write_text( - tomlkit.dumps(pyproject), encoding="utf-8" - ) - - -@pytest.fixture(autouse=True) -def install_plugin(env: MockEnv, installed: Repository, pyproject: None) -> None: - lock_content = { - "package": [ - { - "name": "poetry-plugin", - "version": "1.2.3", - "category": "main", - "optional": False, - "platform": "*", - "python-versions": "*", - "checksum": [], - }, - ], - "metadata": { - "python-versions": "^3.6", - "platform": "*", - "content-hash": "123456789", - "hashes": {"poetry-plugin": []}, - }, - } - - env.path.joinpath("poetry.lock").write_text( - tomlkit.dumps(lock_content), encoding="utf-8" - ) - - pyproject_toml = tomlkit.loads( - env.path.joinpath("pyproject.toml").read_text(encoding="utf-8") - ) - content = pyproject_toml["tool"]["poetry"] - - dependency_section = content["dependencies"] - dependency_section["poetry-plugin"] = "^1.2.3" - - env.path.joinpath("pyproject.toml").write_text( - tomlkit.dumps(pyproject_toml), encoding="utf-8" - ) - - installed.add_package(Package("poetry-plugin", "1.2.3")) - - -def test_remove_installed_package( - app: PoetryTestApplication, tester: CommandTester, env: MockEnv -): - tester.execute("poetry-plugin") - - expected = """\ -Updating dependencies -Resolving dependencies... - -Writing lock file - -Package operations: 0 installs, 0 updates, 1 removal - - • Removing poetry-plugin (1.2.3) -""" - - assert tester.io.fetch_output() == expected - - remove_command: RemoveCommand = app.find("remove") - assert remove_command.poetry.file.parent == env.path - assert remove_command.poetry.locker.lock.parent == env.path - assert remove_command.poetry.locker.lock.exists() - assert not remove_command.installer.executor._dry_run - - content = remove_command.poetry.file.read()["tool"]["poetry"] - assert "poetry-plugin" not in content["dependencies"] - - -def test_remove_installed_package_dry_run( - app: PoetryTestApplication, tester: CommandTester, env: MockEnv -): - tester.execute("poetry-plugin --dry-run") - - expected = """\ -Updating dependencies -Resolving dependencies... - -Package operations: 0 installs, 0 updates, 1 removal - - • Removing poetry-plugin (1.2.3) - • Removing poetry-plugin (1.2.3) -""" - - assert tester.io.fetch_output() == expected - - remove_command: RemoveCommand = app.find("remove") - assert remove_command.poetry.file.parent == env.path - assert remove_command.poetry.locker.lock.parent == env.path - assert remove_command.poetry.locker.lock.exists() - assert remove_command.installer.executor._dry_run - - content = remove_command.poetry.file.read()["tool"]["poetry"] - assert "poetry-plugin" in content["dependencies"] diff --git a/tests/console/commands/self/conftest.py b/tests/console/commands/self/conftest.py new file mode 100644 index 00000000000..6a175b5fdf0 --- /dev/null +++ b/tests/console/commands/self/conftest.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.packages.package import Package + +from poetry.__version__ import __version__ +from poetry.repositories import Pool +from poetry.utils.env import EnvManager + + +if TYPE_CHECKING: + import httpretty + + from pytest_mock import MockerFixture + + from poetry.repositories.repository import Repository + from poetry.utils.env import VirtualEnv + from tests.helpers import TestRepository + + +@pytest.fixture(autouse=True) +def _patch_repos(repo: TestRepository, installed: Repository) -> None: + poetry = Package("poetry", __version__) + repo.add_package(poetry) + installed.add_package(poetry) + + +@pytest.fixture(autouse=True) +def save_environ(environ: None) -> Repository: + yield + + +@pytest.fixture() +def pool(repo: TestRepository) -> Pool: + return Pool([repo]) + + +@pytest.fixture(autouse=True) +def setup_mocks( + mocker: MockerFixture, + tmp_venv: VirtualEnv, + installed: Repository, + pool: Pool, + http: type[httpretty.httpretty], +) -> None: + mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) + mocker.patch("poetry.repositories.pool.Pool.find_packages", pool.find_packages) + mocker.patch("poetry.repositories.pool.Pool.package", pool.package) + mocker.patch("poetry.installation.executor.pip_install") + mocker.patch( + "poetry.installation.installer.Installer._get_installed", + return_value=installed, + ) diff --git a/tests/console/commands/plugin/__init__.py b/tests/console/commands/self/deprecated_plugin/__init__.py similarity index 100% rename from tests/console/commands/plugin/__init__.py rename to tests/console/commands/self/deprecated_plugin/__init__.py diff --git a/tests/console/commands/self/deprecated_plugin/test_add.py b/tests/console/commands/self/deprecated_plugin/test_add.py new file mode 100644 index 00000000000..998ffc12ac6 --- /dev/null +++ b/tests/console/commands/self/deprecated_plugin/test_add.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.packages.package import Package + +from poetry.__version__ import __version__ + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + from tests.helpers import TestRepository + from tests.types import CommandTesterFactory + + +@pytest.fixture() +def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: + return command_tester_factory("plugin add") + + +def test_deprecation_warning(tester: CommandTester, repo: TestRepository) -> None: + repo.add_package(Package("poetry", __version__)) + repo.add_package(Package("poetry-plugin", "1.0")) + tester.execute("poetry-plugin") + assert ( + tester.io.fetch_error() + == "This command is deprecated. Use self add command instead.\n" + ) diff --git a/tests/console/commands/self/deprecated_plugin/test_remove.py b/tests/console/commands/self/deprecated_plugin/test_remove.py new file mode 100644 index 00000000000..072b30c134b --- /dev/null +++ b/tests/console/commands/self/deprecated_plugin/test_remove.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage + +from poetry.__version__ import __version__ +from poetry.console.commands.self.self_command import SelfCommand +from poetry.factory import Factory +from tests.console.commands.self.utils import get_self_command_dependencies + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + from tests.helpers import TestRepository + from tests.types import CommandTesterFactory + + +@pytest.fixture() +def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: + return command_tester_factory("plugin remove") + + +def test_deprecation_warning(tester: CommandTester, repo: TestRepository) -> None: + plugin = Package("poetry-plugin", "1.2.3") + + repo.add_package(Package("poetry", __version__)) + repo.add_package(plugin) + + package = ProjectPackage("poetry-instance", __version__) + package.add_dependency( + Dependency(plugin.name, "^1.2.3", groups=[SelfCommand.ADDITIONAL_PACKAGE_GROUP]) + ) + + content = Factory.create_pyproject_from_package(package) + system_pyproject_file = SelfCommand.get_default_system_pyproject_file() + system_pyproject_file.write_text(content.as_string(), encoding="utf-8") + + dependencies = get_self_command_dependencies(locked=False) + assert "poetry-plugin" in dependencies + + tester.execute("poetry-plugin") + + assert ( + tester.io.fetch_error() + == "This command is deprecated. Use self remove command instead.\n" + ) + + dependencies = get_self_command_dependencies() + assert "poetry-plugin" not in dependencies + assert not dependencies diff --git a/tests/console/commands/plugin/test_show.py b/tests/console/commands/self/deprecated_plugin/test_show.py similarity index 100% rename from tests/console/commands/plugin/test_show.py rename to tests/console/commands/self/deprecated_plugin/test_show.py diff --git a/tests/console/commands/plugin/test_add.py b/tests/console/commands/self/test_add_plugins.py similarity index 65% rename from tests/console/commands/plugin/test_add.py rename to tests/console/commands/self/test_add_plugins.py index 2eef6384972..0259342a8fd 100644 --- a/tests/console/commands/plugin/test_add.py +++ b/tests/console/commands/self/test_add_plugins.py @@ -6,51 +6,38 @@ from poetry.core.packages.package import Package +from poetry.console.commands.self.self_command import SelfCommand from poetry.factory import Factory +from tests.console.commands.self.utils import get_self_command_dependencies if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester - from pytest_mock import MockerFixture - from poetry.console.commands.update import UpdateCommand - from poetry.repositories import Repository - from poetry.utils.env import MockEnv - from tests.helpers import PoetryTestApplication from tests.helpers import TestRepository from tests.types import CommandTesterFactory @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: - return command_tester_factory("plugin add") + return command_tester_factory("self add") def assert_plugin_add_result( tester: CommandTester, - app: PoetryTestApplication, - env: MockEnv, expected: str, constraint: str | dict[str, str], ) -> None: assert tester.io.fetch_output() == expected + dependencies = get_self_command_dependencies() - update_command: UpdateCommand = app.find("update") - assert update_command.poetry.file.parent == env.path - assert update_command.poetry.locker.lock.parent == env.path - assert update_command.poetry.locker.lock.exists() - - content = update_command.poetry.file.read()["tool"]["poetry"] - assert "poetry-plugin" in content["dependencies"] - assert content["dependencies"]["poetry-plugin"] == constraint + assert "poetry-plugin" in dependencies + assert dependencies["poetry-plugin"] == constraint def test_add_no_constraint( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, ): repo.add_package(Package("poetry-plugin", "0.1.0")) @@ -58,6 +45,7 @@ def test_add_no_constraint( expected = """\ Using version ^0.1.0 for poetry-plugin + Updating dependencies Resolving dependencies... @@ -67,22 +55,19 @@ def test_add_no_constraint( • Installing poetry-plugin (0.1.0) """ - assert_plugin_add_result(tester, app, env, expected, "^0.1.0") + assert_plugin_add_result(tester, expected, "^0.1.0") def test_add_with_constraint( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, ): repo.add_package(Package("poetry-plugin", "0.1.0")) repo.add_package(Package("poetry-plugin", "0.2.0")) tester.execute("poetry-plugin@^0.2.0") - expected = """\ + expected = """ Updating dependencies Resolving dependencies... @@ -93,21 +78,18 @@ def test_add_with_constraint( • Installing poetry-plugin (0.2.0) """ - assert_plugin_add_result(tester, app, env, expected, "^0.2.0") + assert_plugin_add_result(tester, expected, "^0.2.0") def test_add_with_git_constraint( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, ): repo.add_package(Package("pendulum", "2.0.5")) tester.execute("git+https://github.com/demo/poetry-plugin.git") - expected = """\ + expected = """ Updating dependencies Resolving dependencies... @@ -120,23 +102,20 @@ def test_add_with_git_constraint( """ assert_plugin_add_result( - tester, app, env, expected, {"git": "https://github.com/demo/poetry-plugin.git"} + tester, expected, {"git": "https://github.com/demo/poetry-plugin.git"} ) def test_add_with_git_constraint_with_extras( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, ): repo.add_package(Package("pendulum", "2.0.5")) repo.add_package(Package("tomlkit", "0.7.0")) tester.execute("git+https://github.com/demo/poetry-plugin.git[foo]") - expected = """\ + expected = """ Updating dependencies Resolving dependencies... @@ -151,8 +130,6 @@ def test_add_with_git_constraint_with_extras( assert_plugin_add_result( tester, - app, - env, expected, { "git": "https://github.com/demo/poetry-plugin.git", @@ -162,24 +139,22 @@ def test_add_with_git_constraint_with_extras( def test_add_existing_plugin_warns_about_no_operation( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, + installed: TestRepository, ): - env.path.joinpath("pyproject.toml").write_text( - """\ + SelfCommand.get_default_system_pyproject_file().write_text( + f"""\ [tool.poetry] -name = "poetry" +name = "poetry-instance" version = "1.2.0" description = "Python dependency management and packaging made easy." -authors = [ - "Sébastien Eustace " -] +authors = [] [tool.poetry.dependencies] python = "^3.6" + +[tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" """, encoding="utf-8", @@ -191,46 +166,35 @@ def test_add_existing_plugin_warns_about_no_operation( tester.execute("poetry-plugin") - expected = """\ -The following plugins are already present in the pyproject.toml file and will be\ + expected = f"""\ +The following packages are already present in the pyproject.toml and will be\ skipped: • poetry-plugin - -If you want to update it to the latest compatible version,\ - you can use `poetry plugin update package`. -If you prefer to upgrade it to the latest available version,\ - you can use `poetry plugin add package@latest`. - +{tester.command._hint_update_packages} +Nothing to add. """ assert tester.io.fetch_output() == expected - update_command: UpdateCommand = app.find("update") - # The update command should not have been called - assert update_command.poetry.file.parent != env.path - def test_add_existing_plugin_updates_if_requested( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, - mocker: MockerFixture, + repo: TestRepository, + installed: TestRepository, ): - env.path.joinpath("pyproject.toml").write_text( - """\ + SelfCommand.get_default_system_pyproject_file().write_text( + f"""\ [tool.poetry] -name = "poetry" +name = "poetry-instance" version = "1.2.0" description = "Python dependency management and packaging made easy." -authors = [ - "Sébastien Eustace " -] +authors = [] [tool.poetry.dependencies] python = "^3.6" + +[tool.poetry.group.{SelfCommand.ADDITIONAL_PACKAGE_GROUP}.dependencies] poetry-plugin = "^1.2.3" """, encoding="utf-8", @@ -245,6 +209,7 @@ def test_add_existing_plugin_updates_if_requested( expected = """\ Using version ^2.3.4 for poetry-plugin + Updating dependencies Resolving dependencies... @@ -255,15 +220,13 @@ def test_add_existing_plugin_updates_if_requested( • Updating poetry-plugin (1.2.3 -> 2.3.4) """ - assert_plugin_add_result(tester, app, env, expected, "^2.3.4") + assert_plugin_add_result(tester, expected, "^2.3.4") def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( - app: PoetryTestApplication, - repo: TestRepository, tester: CommandTester, - env: MockEnv, - installed: Repository, + repo: TestRepository, + installed: TestRepository, ): poetry_package = Package("poetry", "1.2.0") poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0")) @@ -282,6 +245,7 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( expected = """\ Using version ^1.2.3 for poetry-plugin + Updating dependencies Resolving dependencies... @@ -293,4 +257,4 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed( • Installing poetry-plugin (1.2.3) """ - assert_plugin_add_result(tester, app, env, expected, "^1.2.3") + assert_plugin_add_result(tester, expected, "^1.2.3") diff --git a/tests/console/commands/self/test_remove_plugins.py b/tests/console/commands/self/test_remove_plugins.py new file mode 100644 index 00000000000..644e5748510 --- /dev/null +++ b/tests/console/commands/self/test_remove_plugins.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +import tomlkit + +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.packages.project_package import ProjectPackage + +from poetry.__version__ import __version__ +from poetry.console.commands.self.self_command import SelfCommand +from poetry.factory import Factory +from tests.console.commands.self.utils import get_self_command_dependencies + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + from poetry.repositories import Repository + from tests.types import CommandTesterFactory + + +@pytest.fixture() +def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: + return command_tester_factory("self remove") + + +@pytest.fixture(autouse=True) +def install_plugin(installed: Repository) -> None: + package = ProjectPackage("poetry-instance", __version__) + plugin = Package("poetry-plugin", "1.2.3") + + package.add_dependency( + Dependency(plugin.name, "^1.2.3", groups=[SelfCommand.ADDITIONAL_PACKAGE_GROUP]) + ) + content = Factory.create_pyproject_from_package(package) + system_pyproject_file = SelfCommand.get_default_system_pyproject_file() + system_pyproject_file.write_text(content.as_string(), encoding="utf-8") + + lock_content = { + "package": [ + { + "name": "poetry-plugin", + "version": "1.2.3", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "^3.6", + "platform": "*", + "content-hash": "123456789", + "hashes": {"poetry-plugin": []}, + }, + } + system_pyproject_file.parent.joinpath("poetry.lock").write_text( + tomlkit.dumps(lock_content), encoding="utf-8" + ) + + installed.add_package(plugin) + + +def test_remove_installed_package(tester: CommandTester): + tester.execute("poetry-plugin") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +Package operations: 0 installs, 0 updates, 1 removal + + • Removing poetry-plugin (1.2.3) +""" + assert tester.io.fetch_output() == expected + + dependencies = get_self_command_dependencies() + + assert "poetry-plugin" not in dependencies + assert not dependencies + + +def test_remove_installed_package_dry_run(tester: CommandTester): + tester.execute("poetry-plugin --dry-run") + + expected = f"""\ +Updating dependencies +Resolving dependencies... + +Package operations: 0 installs, 0 updates, 1 removal, 1 skipped + + • Removing poetry-plugin (1.2.3) + • Removing poetry-plugin (1.2.3) + • Installing poetry ({__version__}): Skipped for the following reason: Already \ +installed +""" + + assert tester.io.fetch_output() == expected + + dependencies = get_self_command_dependencies() + + assert "poetry-plugin" in dependencies diff --git a/tests/console/commands/self/test_update.py b/tests/console/commands/self/test_update.py index 220deb66682..1c20920d0b2 100644 --- a/tests/console/commands/self/test_update.py +++ b/tests/console/commands/self/test_update.py @@ -10,48 +10,17 @@ from poetry.__version__ import __version__ from poetry.factory import Factory -from poetry.repositories.repository import Repository -from poetry.utils.env import EnvManager if TYPE_CHECKING: - import httpretty - from cleo.testers.command_tester import CommandTester - from pytest_mock import MockerFixture - from poetry.utils.env import VirtualEnv from tests.helpers import TestRepository from tests.types import CommandTesterFactory FIXTURES = Path(__file__).parent.joinpath("fixtures") -@pytest.fixture -def installed_repository() -> Repository: - return Repository() - - -@pytest.fixture(autouse=True) -def save_environ(environ: None) -> Repository: - yield - - -@pytest.fixture(autouse=True) -def setup_mocks( - mocker: MockerFixture, - tmp_venv: VirtualEnv, - installed_repository: Repository, - http: type[httpretty.httpretty], -): - mocker.patch.object(EnvManager, "get_system_env", return_value=tmp_venv) - mocker.patch("poetry.installation.executor.pip_install") - mocker.patch( - "poetry.installation.installer.Installer._get_installed", - return_value=installed_repository, - ) - - @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("self update") @@ -60,7 +29,7 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: def test_self_update_can_update_from_recommended_installation( tester: CommandTester, repo: TestRepository, - installed_repository: TestRepository, + installed: TestRepository, ): new_version = Version.parse(__version__).next_minor().text @@ -70,8 +39,8 @@ def test_self_update_can_update_from_recommended_installation( new_poetry = Package("poetry", new_version) new_poetry.add_dependency(Factory.create_dependency("cleo", "^1.0.0")) - installed_repository.add_package(old_poetry) - installed_repository.add_package(Package("cleo", "0.8.2")) + installed.add_package(old_poetry) + installed.add_package(Package("cleo", "0.8.2")) repo.add_package(new_poetry) repo.add_package(Package("cleo", "1.0.0")) diff --git a/tests/console/commands/self/utils.py b/tests/console/commands/self/utils.py new file mode 100644 index 00000000000..793f36a021d --- /dev/null +++ b/tests/console/commands/self/utils.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from poetry.factory import Factory + + +if TYPE_CHECKING: + from tomlkit.container import Table as TOMLTable + + +def get_self_command_dependencies(locked: bool = True) -> TOMLTable: + from poetry.console.commands.self.self_command import SelfCommand + from poetry.locations import CONFIG_DIR + + system_pyproject_file = SelfCommand.get_default_system_pyproject_file() + + assert system_pyproject_file.exists() + assert system_pyproject_file.parent == Path(CONFIG_DIR) + + if locked: + assert system_pyproject_file.parent.joinpath("poetry.lock").exists() + + poetry = Factory().create_poetry(system_pyproject_file.parent, disable_plugins=True) + + content = poetry.file.read()["tool"]["poetry"] + + assert "group" in content + assert SelfCommand.ADDITIONAL_PACKAGE_GROUP in content["group"] + assert "dependencies" in content["group"][SelfCommand.ADDITIONAL_PACKAGE_GROUP] + + return content["group"][SelfCommand.ADDITIONAL_PACKAGE_GROUP]["dependencies"] From 7847f6367aa269576264c098001568d72d2cd03d Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 19 Apr 2022 14:13:09 +0200 Subject: [PATCH 06/12] plugin manager: use system env paths for discovery --- .../console/commands/self/show/plugins.py | 8 +++----- src/poetry/plugins/plugin_manager.py | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py index 381e1008119..41daadfd747 100644 --- a/src/poetry/console/commands/self/show/plugins.py +++ b/src/poetry/console/commands/self/show/plugins.py @@ -40,12 +40,10 @@ def _system_project_handle(self) -> int: } ) - entry_points = ( - PluginManager(ApplicationPlugin.group).get_plugin_entry_points() - + PluginManager(Plugin.group).get_plugin_entry_points() - ) - system_env = EnvManager.get_system_env(naive=True) + entry_points = PluginManager(ApplicationPlugin.group).get_plugin_entry_points( + env=system_env + ) + PluginManager(Plugin.group).get_plugin_entry_points(env=system_env) installed_repository = InstalledRepository.load( system_env, with_dependencies=True ) diff --git a/src/poetry/plugins/plugin_manager.py b/src/poetry/plugins/plugin_manager.py index f219bb09b1a..34dcc4b640a 100644 --- a/src/poetry/plugins/plugin_manager.py +++ b/src/poetry/plugins/plugin_manager.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging +import sys -from typing import Any +from typing import TYPE_CHECKING import entrypoints @@ -10,6 +11,12 @@ from poetry.plugins.plugin import Plugin +if TYPE_CHECKING: + from typing import Any + + from poetry.utils.env import Env + + logger = logging.getLogger(__name__) @@ -23,19 +30,20 @@ def __init__(self, group: str, disable_plugins: bool = False) -> None: self._disable_plugins = disable_plugins self._plugins: list[Plugin] = [] - def load_plugins(self) -> None: + def load_plugins(self, env: Env | None = None) -> None: if self._disable_plugins: return - plugin_entrypoints = self.get_plugin_entry_points() + plugin_entrypoints = self.get_plugin_entry_points(env=env) for entrypoint in plugin_entrypoints: self._load_plugin_entrypoint(entrypoint) - def get_plugin_entry_points(self) -> list[entrypoints.EntryPoint]: - + def get_plugin_entry_points( + self, env: Env | None = None + ) -> list[entrypoints.EntryPoint]: entry_points: list[entrypoints.EntryPoint] = entrypoints.get_group_all( - self._group + self._group, path=env.sys_path if env else sys.path ) return entry_points From c0f39626334a27148edcbfd16479ebcf11e51edd Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Sun, 24 Apr 2022 00:46:41 +0200 Subject: [PATCH 07/12] application: make installer configuration public --- src/poetry/console/application.py | 12 +++++++----- src/poetry/console/commands/plugin/add.py | 2 +- src/poetry/console/commands/plugin/remove.py | 2 +- src/poetry/console/commands/self/update.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 298a1a2fe17..77d4a1470c8 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -109,7 +109,7 @@ def __init__(self) -> None: dispatcher = EventDispatcher() dispatcher.add_listener(COMMAND, self.register_command_loggers) dispatcher.add_listener(COMMAND, self.configure_env) - dispatcher.add_listener(COMMAND, self.configure_installer) + dispatcher.add_listener(COMMAND, self.configure_installer_for_event) self.set_event_dispatcher(dispatcher) command_loader = CommandLoader({name: load_command(name) for name in COMMANDS}) @@ -301,8 +301,9 @@ def configure_env( command.set_env(env) - def configure_installer( - self, event: ConsoleCommandEvent, event_name: str, _: Any + @classmethod + def configure_installer_for_event( + cls, event: ConsoleCommandEvent, event_name: str, _: Any ) -> None: from poetry.console.commands.installer_command import InstallerCommand @@ -315,9 +316,10 @@ def configure_installer( if command.installer is not None: return - self._configure_installer(command, event.io) + cls.configure_installer_for_command(command, event.io) - def _configure_installer(self, command: InstallerCommand, io: IO) -> None: + @staticmethod + def configure_installer_for_command(command: InstallerCommand, io: IO) -> None: from poetry.installation.installer import Installer poetry = command.poetry diff --git a/src/poetry/console/commands/plugin/add.py b/src/poetry/console/commands/plugin/add.py index 0491e2bea5f..e6ec66c53d7 100644 --- a/src/poetry/console/commands/plugin/add.py +++ b/src/poetry/console/commands/plugin/add.py @@ -49,7 +49,7 @@ def handle(self) -> int: application = cast(Application, self.application) command: SelfAddCommand = cast(SelfAddCommand, application.find("self add")) - application._configure_installer(command, self.io) + application.configure_installer_for_command(command, self.io) argv: list[str] = ["add", *self.argument("plugins")] diff --git a/src/poetry/console/commands/plugin/remove.py b/src/poetry/console/commands/plugin/remove.py index 6f4274230a3..e174007ce8f 100644 --- a/src/poetry/console/commands/plugin/remove.py +++ b/src/poetry/console/commands/plugin/remove.py @@ -43,7 +43,7 @@ def handle(self) -> int: command: SelfRemoveCommand = cast( SelfRemoveCommand, application.find("self remove") ) - application._configure_installer(command, self.io) + application.configure_installer_for_command(command, self.io) argv: list[str] = ["remove", *self.argument("plugins")] diff --git a/src/poetry/console/commands/self/update.py b/src/poetry/console/commands/self/update.py index 55ad1b75f35..bed034e0ca0 100644 --- a/src/poetry/console/commands/self/update.py +++ b/src/poetry/console/commands/self/update.py @@ -40,7 +40,7 @@ def _system_project_handle(self) -> int: application = cast(Application, self.application) add_command: AddCommand = cast(AddCommand, application.find("add")) add_command.set_env(self.env) - application._configure_installer(add_command, self._io) + application.configure_installer_for_command(add_command, self._io) argv = ["add", f"poetry@{self.argument('version')}"] From ed73f95bc261a60ebe3e3796c83de4131c1a9028 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Sat, 7 May 2022 19:08:13 +0200 Subject: [PATCH 08/12] typing: fix mypy inspection issues --- src/poetry/console/commands/plugin/show.py | 4 +- .../console/commands/self/self_command.py | 4 +- .../console/commands/self/show/__init__.py | 3 +- .../console/commands/self/show/plugins.py | 53 +++++++++++-------- src/poetry/console/commands/self/update.py | 3 +- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/poetry/console/commands/plugin/show.py b/src/poetry/console/commands/plugin/show.py index 9481f591fbd..fcac6f85193 100644 --- a/src/poetry/console/commands/plugin/show.py +++ b/src/poetry/console/commands/plugin/show.py @@ -27,10 +27,12 @@ def handle(self) -> int: command: SelfShowPluginsCommand = cast( SelfShowPluginsCommand, application.find("self show plugins") ) - return command.run( + + exit_code: int = command.run( IO( StringInput(""), self._io.output, self._io.error_output, ) ) + return exit_code diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py index 877981ecf7f..35018325981 100644 --- a/src/poetry/console/commands/self/self_command.py +++ b/src/poetry/console/commands/self/self_command.py @@ -45,7 +45,7 @@ def reset_env(self) -> None: @property def env(self) -> Env: - if self._env is None or not isinstance(self._env, SystemEnv): + if not isinstance(self._env, SystemEnv): self.reset_env() return self._env @@ -102,7 +102,7 @@ def _system_project_handle(self) -> int: The default implementations handles cases where a `self` command delegates handling to an existing command. Eg: `SelfAddCommand(SelfCommand, AddCommand)`. """ - return super().handle() + return cast(int, super().handle()) def reset(self) -> None: """ diff --git a/src/poetry/console/commands/self/show/__init__.py b/src/poetry/console/commands/self/show/__init__.py index b13a8873d23..7212d69c7e1 100644 --- a/src/poetry/console/commands/self/show/__init__.py +++ b/src/poetry/console/commands/self/show/__init__.py @@ -30,4 +30,5 @@ def activated_groups(self) -> set[str]: if self.option("addons", False): return {SelfCommand.ADDITIONAL_PACKAGE_GROUP} - return super(ShowCommand, self).activated_groups + groups: set[str] = super(ShowCommand, self).activated_groups + return groups diff --git a/src/poetry/console/commands/self/show/plugins.py b/src/poetry/console/commands/self/show/plugins.py index 41daadfd747..2e80e9b01a5 100644 --- a/src/poetry/console/commands/self/show/plugins.py +++ b/src/poetry/console/commands/self/show/plugins.py @@ -1,16 +1,24 @@ from __future__ import annotations -from collections import defaultdict +import dataclasses + from typing import TYPE_CHECKING -from typing import DefaultDict from poetry.console.commands.self.self_command import SelfCommand if TYPE_CHECKING: + from entrypoints import EntryPoint from poetry.core.packages.package import Package +@dataclasses.dataclass +class PluginPackage: + package: Package + plugins: list[EntryPoint] = dataclasses.field(default_factory=list) + application_plugins: list[EntryPoint] = dataclasses.field(default_factory=list) + + class SelfShowPluginsCommand(SelfCommand): name = "self show plugins" description = "Shows information about the currently installed plugins." @@ -32,13 +40,7 @@ def _system_project_handle(self) -> int: from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import pluralize - plugins: DefaultDict[str, dict[str, Package | list[str]]] = defaultdict( - lambda: { - "package": None, - "plugins": [], - "application_plugins": [], - } - ) + plugins: dict[str, PluginPackage] = {} system_env = EnvManager.get_system_env(naive=True) entry_points = PluginManager(ApplicationPlugin.group).get_plugin_entry_points( @@ -48,33 +50,42 @@ def _system_project_handle(self) -> int: system_env, with_dependencies=True ) - packages_by_name = {pkg.name: pkg for pkg in installed_repository.packages} + packages_by_name: dict[str, Package] = { + pkg.name: pkg for pkg in installed_repository.packages + } for entry_point in entry_points: plugin = entry_point.load() - category = "plugins" - if issubclass(plugin, ApplicationPlugin): - category = "application_plugins" + assert entry_point.distro is not None package = packages_by_name[canonicalize_name(entry_point.distro.name)] - plugins[package.pretty_name]["package"] = package - plugins[package.pretty_name][category].append(entry_point) + + name = package.pretty_name + info = plugins.get(name) or PluginPackage(package=package) + + if issubclass(plugin, ApplicationPlugin): + info.application_plugins.append(entry_point) + else: + info.plugins.append(entry_point) + + plugins[name] = info for name, info in plugins.items(): - package = info["package"] + package = info.package description = " " + package.description if package.description else "" self.line("") self.line(f" • {name} ({package.version}){description}") provide_line = " " - if info["plugins"]: - count = len(info["plugins"]) + + if info.plugins: + count = len(info.plugins) provide_line += f" {count} plugin{pluralize(count)}" - if info["application_plugins"]: - if info["plugins"]: + if info.application_plugins: + if info.plugins: provide_line += " and" - count = len(info["application_plugins"]) + count = len(info.application_plugins) provide_line += ( f" {count} application plugin{pluralize(count)}" ) diff --git a/src/poetry/console/commands/self/update.py b/src/poetry/console/commands/self/update.py index bed034e0ca0..96bcd79f4fe 100644 --- a/src/poetry/console/commands/self/update.py +++ b/src/poetry/console/commands/self/update.py @@ -50,10 +50,11 @@ def _system_project_handle(self) -> int: if self.option("preview"): argv.append("--allow-prereleases") - return add_command.run( + exit_code: int = add_command.run( IO( StringInput(" ".join(argv)), self._io.output, self._io.error_output, ) ) + return exit_code From ac9453a8d891178bafaaf2b260be8020be1ed156 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 27 May 2022 01:04:15 +0200 Subject: [PATCH 09/12] self: pin poetry version in system pyproject.toml --- src/poetry/console/commands/self/self_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py index 35018325981..68d1e997080 100644 --- a/src/poetry/console/commands/self/self_command.py +++ b/src/poetry/console/commands/self/self_command.py @@ -68,7 +68,7 @@ def generate_system_pyproject(self) -> None: preserved[key] = content[key] package = ProjectPackage(name="poetry-instance", version=__version__) - package.add_dependency(Dependency(name="poetry", constraint=f"^{__version__}")) + package.add_dependency(Dependency(name="poetry", constraint=f"{__version__}")) package.python_versions = ".".join(str(v) for v in self.env.version_info[:3]) From 3b8c85c63850921bad2817809ea206fdb0774fd0 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 27 May 2022 01:05:13 +0200 Subject: [PATCH 10/12] self: add lock command --- src/poetry/console/application.py | 1 + src/poetry/console/commands/self/lock.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/poetry/console/commands/self/lock.py diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 77d4a1470c8..d6ce9690cfe 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -85,6 +85,7 @@ def _load() -> type[Command]: # Self commands "self add", "self install", + "self lock", "self remove", "self update", "self show", diff --git a/src/poetry/console/commands/self/lock.py b/src/poetry/console/commands/self/lock.py new file mode 100644 index 00000000000..13743fa787f --- /dev/null +++ b/src/poetry/console/commands/self/lock.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from poetry.console.commands.lock import LockCommand +from poetry.console.commands.self.self_command import SelfCommand + + +class SelfLockCommand(SelfCommand, LockCommand): + name = "self lock" + description = "Lock the Poetry installation's system requirements." + help = f"""\ +The self lock command reads this Poetry installation's system requirements as \ +specified in the {SelfCommand.get_default_system_pyproject_file()} file. + +The system dependencies are locked in the \ +{SelfCommand.get_default_system_pyproject_file().parent.joinpath("poetry.lock")} \ +file. +""" From 6aa6395e12d564bcf73b5aee2726ceabaefd4b26 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 27 May 2022 02:33:12 +0200 Subject: [PATCH 11/12] doc: add self commands --- docs/cli.md | 187 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 140 insertions(+), 47 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 11de33fa6db..69476c852b5 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -710,53 +710,6 @@ To only remove a specific package from a cache, you have to specify the cache en poetry cache clear pypi:requests:2.24.0 ``` -## plugin - -The `plugin` namespace regroups sub commands to manage Poetry plugins. - -### `plugin add` - -The `plugin add` command installs Poetry plugins and make them available at runtime. - -For example, to install the `poetry-plugin` plugin, you can run: - -```bash -poetry plugin add poetry-plugin -``` - -The package specification formats supported by the `plugin add` command are the same as the ones supported -by the [`add` command](#add). - -If you just want to check what would happen by installing a plugin, you can use the `--dry-run` option - -```bash -poetry plugin add poetry-plugin --dry-run -``` - -#### Options - -* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). - -### `plugin show` - -The `plugin show` command lists all the currently installed plugins. - -```bash -poetry plugin show -``` - -### `plugin remove` - -The `plugin remove` command removes installed plugins. - -```bash -poetry plugin remove poetry-plugin -``` - -#### Options - -* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). - ## source The `source` namespace regroups sub commands to manage repository sources for a Poetry project. @@ -851,3 +804,143 @@ The `list` command displays all the available Poetry commands. ```bash poetry list ``` + +## self + +The `self` namespace regroups sub commands to manage the Poetry installation itself. + +{{% note %}} +Use of these commands will create the required `pyproject.toml` and `poetry.lock` files in your +[configuration directory]({{< relref "configuration" >}}). +{{% /note %}} + +### `self add` + +The `self add` command installs Poetry plugins and make them available at runtime. Additionally, it can +also be used to upgrade Poetry's own dependencies or inject additional packages into the runtime +environment + +{{% note %}} +The `self add` command works exactly like the [`add` command](#add). However, is different in that the packages +managed are for Poetry's runtime environment. + +The package specification formats supported by the `self add` command are the same as the ones supported +by the [`add` command](#add). +{{% /note %}} + +For example, to install the `poetry-plugin-export` plugin, you can run: + +```bash +poetry self add poetry-plugin-export +``` + +To update to the latest `poetry-core` version, you can run: + +```bash +poetry self add poetry-core@latest +``` + +To add a keyring provider `artifacts-keyring`, you can run: + +```bash +poetry self add artifacts-keyring +``` + +### Options + +* `--editable (-e)`: Add vcs/path dependencies as editable. +* `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed) +* `--allow-prereleases`: Accept prereleases. +* `--source`: Name of the source to use to install the package. +* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). + +### `self update` + +The `self update` command updates Poetry version in its current runtime environment. + +{{% note %}} +The `self update` command works exactly like the [`update` command](#update). However, +is different in that the packages managed are for Poetry's runtime environment. +{{% /note %}} + +```bash +poetry self update +``` + +### Options + +* `--preview`: Allow the installation of pre-release versions. +* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). + +### `self lock` + +The `self lock` command reads this Poetry installation's system `pyproject.toml` file. The system +dependencies are locked in the corresponding `poetry.lock` file. + +```bash +poetry self lock +``` + +### Options + +* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--no-update`: Do not update locked versions, only refresh lock file. + +### `self show` + +The `self show` command behaves similar to the show command, but +working within Poetry's runtime environment. This lists all packages installed within +the Poetry install environment. + +To show only additional packages that have been added via self add and their +dependencies use `self show --addons`. + +```bash +poetry self show +``` + +### Options + +* `--addons`: List only add-on packages installed. +* `--tree`: List the dependencies as a tree. +* `--latest (-l)`: Show the latest version. +* `--outdated (-o)`: Show the latest version but only for packages that are outdated. + +### `self show plugins` + +The `self show plugins` command lists all the currently installed plugins. + +```bash +poetry self show plugins +``` + +### `self remove` + +The `self remove` command removes an installed addon package. + +```bash +poetry self remove poetry-plugin-export +``` + +#### Options + +* `--dry-run`: Outputs the operations but will not execute anything (implicitly enables --verbose). + +### `self install` + +The `self install` command ensures all additional packages specified are installed in the current +runtime environment. + +{{% note %}} +The `self install` command works similar to the [`install` command](#install). However, +is different in that the packages managed are for Poetry's runtime environment. +{{% /note %}} + +```bash +poetry self install --sync +``` + +### Options + +* `--sync`: Synchronize the environment with the locked packages and the specified groups. +* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). From 5847c8348d34a8e439d5d753697efcfb122d74f4 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Fri, 27 May 2022 02:35:51 +0200 Subject: [PATCH 12/12] doc: update plugin management commands --- docs/plugins.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 761b4d68f19..7f44ba02672 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -191,30 +191,30 @@ Installed plugin packages are automatically loaded when Poetry starts up. You have multiple ways to install plugins for Poetry -### The `plugin add` command +### The `self add` command This is the easiest way and should account for all the ways Poetry can be installed. ```bash -poetry plugin add poetry-plugin +poetry self add poetry-plugin ``` -The `plugin add` command will ensure that the plugin is compatible with the current version of Poetry +The `self add` command will ensure that the plugin is compatible with the current version of Poetry and install the needed packages for the plugin to work. -The package specification formats supported by the `plugin add` command are the same as the ones supported +The package specification formats supported by the `self add` command are the same as the ones supported by the [`add` command]({{< relref "cli#add" >}}). -If you no longer need a plugin and want to uninstall it, you can use the `plugin remove` command. +If you no longer need a plugin and want to uninstall it, you can use the `self remove` command. ```shell -poetry plugin remove poetry-plugin +poetry self remove poetry-plugin ``` You can also list all currently installed plugins by running: ```shell -poetry plugin show +poetry self show ``` ### With `pipx inject`