diff --git a/poetry/console/application.py b/poetry/console/application.py index 3285e681c87..e2f9f3b6a3e 100644 --- a/poetry/console/application.py +++ b/poetry/console/application.py @@ -72,6 +72,7 @@ def _load() -> Type[Command]: "env use", # Plugin commands "plugin add", + "plugin remove", "plugin show", # Self commands "self update", diff --git a/poetry/console/commands/plugin/remove.py b/poetry/console/commands/plugin/remove.py new file mode 100644 index 00000000000..cb8143175f7 --- /dev/null +++ b/poetry/console/commands/plugin/remove.py @@ -0,0 +1,73 @@ +import os + +from typing import TYPE_CHECKING +from typing import cast + +from cleo.helpers import argument +from cleo.helpers import option + +from poetry.console.commands.command import Command + + +if TYPE_CHECKING: + from poetry.console.application import Application # noqa + from poetry.console.commands.remove import RemoveCommand + + +class PluginRemoveCommand(Command): + + name = "plugin remove" + + description = "Removes installed plugins" + + arguments = [ + argument("plugins", "The names of the plugins to install.", multiple=True), + ] + + options = [ + option( + "dry-run", + None, + "Output the operations but do not execute anything (implicitly enables --verbose).", + ) + ] + + 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 + + plugins = self.argument("plugins") + + system_env = EnvManager.get_system_env() + env_dir = Path( + os.getenv("POETRY_HOME") if os.getenv("POETRY_HOME") else system_env.path + ) + + # 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) + + argv = ["remove"] + plugins + if self.option("dry-run"): + argv.append("--dry-run") + + return remove_command.run( + IO( + StringInput(" ".join(argv)), + self._io.output, + self._io.error_output, + ) + ) diff --git a/poetry/console/commands/remove.py b/poetry/console/commands/remove.py index a7b2e00a26f..e2b65c46a72 100644 --- a/poetry/console/commands/remove.py +++ b/poetry/console/commands/remove.py @@ -1,6 +1,7 @@ from cleo.helpers import argument from cleo.helpers import option +from ...utils.helpers import canonicalize_name from .installer_command import InstallerCommand @@ -54,12 +55,17 @@ def handle(self) -> int: for key in requirements: del poetry_content[section][key] - # Write the new content back - self.poetry.file.write(content) + dependencies = ( + self.poetry.package.requires + if section == "dependencies" + else self.poetry.package.dev_requires + ) - # Update packages - self.reset_poetry() + for i, dependency in enumerate(reversed(dependencies)): + if dependency.name == canonicalize_name(key): + del dependencies[-i] + # Update packages self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False) ) @@ -76,15 +82,7 @@ def handle(self) -> int: raise - if status != 0 or self.option("dry-run"): - # Revert changes - if not self.option("dry-run"): - self.line_error( - "\n" - "Removal failed, reverting pyproject.toml " - "to its original content." - ) - - self.poetry.file.write(original_content) + if not self.option("dry-run"): + self.poetry.file.write(content) return status diff --git a/tests/console/commands/plugin/test_remove.py b/tests/console/commands/plugin/test_remove.py index e69de29bb2d..0a66d8717cc 100644 --- a/tests/console/commands/plugin/test_remove.py +++ b/tests/console/commands/plugin/test_remove.py @@ -0,0 +1,189 @@ +import pytest +import tomlkit + +from poetry.__version__ import __version__ +from poetry.core.packages.package import Package +from poetry.factory import Factory +from poetry.layouts.layout import POETRY_DEFAULT +from poetry.repositories.installed_repository import InstalledRepository +from poetry.repositories.pool import Pool +from poetry.utils.env import EnvManager + + +@pytest.fixture() +def tester(command_tester_factory): + return command_tester_factory("plugin remove") + + +@pytest.fixture() +def installed(): + repository = InstalledRepository() + + repository.add_package(Package("poetry", __version__)) + + return repository + + +def configure_sources_factory(repo): + def _configure_sources(poetry, sources, config, io): + pool = Pool() + pool.add_repository(repo) + poetry.set_pool(pool) + + return _configure_sources + + +@pytest.fixture(autouse=True) +def setup_mocks(mocker, env, repo, installed): + 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) + ) + + +@pytest.fixture() +def pyproject(env): + 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" + ) + + +def test_remove_installed_package(app, repo, tester, env, installed, pyproject): + 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 = tomlkit.loads( + env.path.joinpath("pyproject.toml").read_text(encoding="utf-8") + ) + content = pyproject["tool"]["poetry"] + + dependency_section = content["dependencies"] + dependency_section["poetry-plugin"] = "^1.2.3" + + env.path.joinpath("pyproject.toml").write_text( + tomlkit.dumps(pyproject), encoding="utf-8" + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + 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 = 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, repo, tester, env, installed, pyproject): + 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 = tomlkit.loads( + env.path.joinpath("pyproject.toml").read_text(encoding="utf-8") + ) + content = pyproject["tool"]["poetry"] + + dependency_section = content["dependencies"] + dependency_section["poetry-plugin"] = "^1.2.3" + + env.path.joinpath("pyproject.toml").write_text( + tomlkit.dumps(pyproject), encoding="utf-8" + ) + + installed.add_package(Package("poetry-plugin", "1.2.3")) + + tester.execute("poetry-plugin --dry-run") + + expected = """\ +Updating dependencies +Resolving dependencies... + +Writing lock file + +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 = 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"]