diff --git a/docs/docs/cli.md b/docs/docs/cli.md
index 70208501c87..1bf9ce1b134 100644
--- a/docs/docs/cli.md
+++ b/docs/docs/cli.md
@@ -538,3 +538,46 @@ To only remove a specific package from a cache, you have to specify the cache en
```bash
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
+```
diff --git a/docs/docs/plugins.md b/docs/docs/plugins.md
new file mode 100644
index 00000000000..637306323a9
--- /dev/null
+++ b/docs/docs/plugins.md
@@ -0,0 +1,231 @@
+# Plugins
+
+Poetry supports using and building plugins if you wish to
+alter or expand Poetry's functionality with your own.
+
+For example if your environment poses special requirements
+on the behaviour of Poetry which do not apply to the majority of its users
+or if you wish to accomplish something with Poetry in a way that is not desired by most users.
+
+In these cases you could consider creating a plugin to handle your specific logic.
+
+
+## Creating a plugin
+
+A plugin is a regular Python package which ships its code as part of the package
+and may also depend on further packages.
+
+### Plugin package
+
+The plugin package must depend on Poetry
+and declare a proper [plugin](/docs/pyproject/#plugins) in the `pyproject.toml` file.
+
+```toml
+[tool.poetry]
+name = "my-poetry-plugin"
+version = "1.0.0"
+
+# ...
+[tool.poetry.dependency]
+python = "~2.7 || ^3.7"
+poetry = "^1.0"
+
+[tool.poetry.plugins."poetry.plugin"]
+demo = "poetry_demo_plugin.plugin:MyPlugin"
+```
+
+### Generic plugins
+
+Every plugin has to supply a class which implements the `poetry.plugins.Plugin` interface.
+
+The `activate()` method of the plugin is called after the plugin is loaded
+and receives an instance of `Poetry` as well as an instance of `cleo.io.IO`.
+
+Using these two objects all configuration can be read
+and all public internal objects and state can be manipulated as desired.
+
+Example:
+
+```python
+from cleo.io.io import IO
+
+from poetry.plugins.plugin import Plugin
+from poetry.poetry import Poetry
+
+
+class MyPlugin(Plugin):
+
+ def activate(self, poetry: Poetry, io: IO):
+ version = self.get_custom_version()
+ io.write_line(f"Setting package version to {version}")
+ poetry.package.set_version(version)
+
+ def get_custom_version(self) -> str:
+ ...
+```
+
+### Application plugins
+
+If you want to add commands or options to the `poetry` script you need
+to create an application plugin which implements the `poetry.plugins.ApplicationPlugin` interface.
+
+The `activate()` method of the application plugin is called after the plugin is loaded
+and receives an instance of `console.Application`.
+
+```python
+from cleo.commands.command import Command
+from poetry.plugins.application_plugin import ApplicationPlugin
+
+
+class CustomCommand(Command):
+
+ name = "my-command"
+
+ def handle(self) -> int:
+ self.line("My command")
+
+ return 0
+
+
+def factory():
+ return CustomCommand()
+
+
+class MyApplicationPlugin(ApplicationPlugin):
+ def activate(self, application):
+ application.command_loader.register_factory("my-command", factory)
+```
+
+!!!note
+
+ It's possible to do the following to register the command:
+
+ ```python
+ application.add(MyCommand())
+ ```
+
+ However, it is **strongly** recommended to register a new factory
+ in the command loader to defer the loading of the command when it's actually
+ called.
+
+ This will help keep the performances of Poetry good.
+
+The plugin also must be declared in the `pyproject.toml` file of the plugin package
+as an `application.plugin` plugin:
+
+```toml
+[tool.poetry.plugins."poetry.application.plugin"]
+foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin"
+```
+
+!!!warning
+
+ A plugin **must not** remove or modify in any way the core commands of Poetry.
+
+
+### Event handler
+
+Plugins can also listen to specific events and act on them if necessary.
+
+These events are fired by [Cleo](https://github.com/sdispater/cleo)
+and are accessible from the `cleo.events.console_events` module.
+
+- `COMMAND`: this event allows attaching listeners before any command is executed.
+- `SIGNAL`: this event allows some actions to be performed after the command execution is interrupted.
+- `TERMINATE`: this event allows listeners to be attached after the command.
+- `ERROR`: this event occurs when an uncaught exception is raised.
+
+Let's see how to implement an application event handler. For this example
+we will see how to load environment variables from a `.env` file before executing
+a command.
+
+
+```python
+from cleo.events.console_events import COMMAND
+from cleo.events.console_command_event import ConsoleCommandEvent
+from cleo.events.event_dispatcher import EventDispatcher
+from dotenv import load_dotenv
+from poetry.console.application import Application
+from poetry.console.commands.env_command import EnvCommand
+from poetry.plugins.application_plugin import ApplicationPlugin
+
+
+class MyApplicationPlugin(ApplicationPlugin):
+ def activate(self, application: Application):
+ application.event_dispatcher.add_listener(COMMAND, self.load_dotenv)
+
+ def load_dotenv(
+ self, event: ConsoleCommandEvent, event_name: str, dispatcher: EventDispatcher
+ ) -> None:
+ command = event.command
+ if not isinstance(command, EnvCommand):
+ return
+
+ io = event.io
+
+ if io.is_debug():
+ io.write_line("Loading environment variables.")
+
+ load_dotenv()
+```
+
+
+## Using plugins
+
+Installed plugin packages are automatically loaded when Poetry starts up.
+
+You have multiple ways to install plugins for Poetry
+
+### The `plugin add` command
+
+This is the easiest way and should account for all the ways Poetry can be installed.
+
+```bash
+poetry plugin add poetry-plugin
+```
+
+The `plugin 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
+by the [`add` command](/docs/cli/#add).
+
+If you no longer need a plugin and want to uninstall it, you can use the `plugin remove` command.
+
+```shell
+poetry plugin remove poetry-plugin
+```
+
+You can also list all currently installed plugins by running:
+
+```shell
+poetry plugin show
+```
+
+### With `pipx inject`
+
+If you used `pipx` to install Poetry you can add the plugin packages via the `pipx inject` command.
+
+```shell
+pipx inject poetry poetry-plugin
+```
+
+If you want to uninstall a plugin, you can run:
+
+```shell
+pipx runpip poetry uninstall poetry-plugin
+```
+
+### With `pip`
+
+If you used `pip` to install Poetry you can add the plugin packages via the `pip install` command.
+
+```shell
+pip install --user poetry-plugin
+```
+
+If you want to uninstall a plugin, you can run:
+
+```shell
+pip uninstall poetry-plugin
+```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index c35ef9579b4..e055a6b6370 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -16,6 +16,7 @@ nav:
- Repositories: repositories.md
- Managing environments: managing-environments.md
- Dependency specification: dependency-specification.md
+ - Plugins: plugins.md
- The pyproject.toml file: pyproject.md
- Contributing: contributing.md
- FAQ: faq.md
diff --git a/poetry.lock b/poetry.lock
index fb8897009e2..9b75e396995 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -173,6 +173,14 @@ category = "main"
optional = false
python-versions = "*"
+[[package]]
+name = "entrypoints"
+version = "0.3"
+description = "Discover and load entry points from installed packages."
+category = "main"
+optional = false
+python-versions = ">=2.7"
+
[[package]]
name = "filelock"
version = "3.0.12"
@@ -697,7 +705,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata]
lock-version = "1.1"
python-versions = "^3.6"
-content-hash = "6cbc07e5853bcf1280421b77b6fca85f2f7eb5a6ff12049f65ea116b256d94ea"
+content-hash = "c72b0807603d4902cff83901d0e65165e243937b5be90b05c17d3c92a06b4fc8"
[metadata.files]
appdirs = [
@@ -859,6 +867,10 @@ distlib = [
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
]
+entrypoints = [
+ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
+ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
+]
filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
diff --git a/poetry/console/application.py b/poetry/console/application.py
index e54cbb5d846..d9d9277d274 100644
--- a/poetry/console/application.py
+++ b/poetry/console/application.py
@@ -19,10 +19,10 @@
from cleo.io.inputs.input import Input
from cleo.io.io import IO
from cleo.io.outputs.output import Output
-from cleo.loaders.factory_command_loader import FactoryCommandLoader
from poetry.__version__ import __version__
+from .command_loader import CommandLoader
from .commands.command import Command
@@ -70,12 +70,19 @@ def _load() -> Type[Command]:
"env list",
"env remove",
"env use",
+ # Plugin commands
+ "plugin add",
+ "plugin remove",
+ "plugin show",
# Self commands
"self update",
]
if TYPE_CHECKING:
+ from cleo.io.inputs.definition import Definition
+
+ from poetry.console.commands.installer_command import InstallerCommand
from poetry.poetry import Poetry
@@ -84,16 +91,17 @@ def __init__(self) -> None:
super(Application, self).__init__("poetry", __version__)
self._poetry = None
+ self._io: Optional[IO] = None
+ self._disable_plugins = False
+ self._plugins_loaded = False
dispatcher = EventDispatcher()
dispatcher.add_listener(COMMAND, self.register_command_loggers)
- dispatcher.add_listener(COMMAND, self.set_env)
- dispatcher.add_listener(COMMAND, self.set_installer)
+ dispatcher.add_listener(COMMAND, self.configure_env)
+ dispatcher.add_listener(COMMAND, self.configure_installer)
self.set_event_dispatcher(dispatcher)
- command_loader = FactoryCommandLoader(
- {name: load_command(name) for name in COMMANDS}
- )
+ command_loader = CommandLoader({name: load_command(name) for name in COMMANDS})
self.set_command_loader(command_loader)
@property
@@ -105,10 +113,16 @@ def poetry(self) -> "Poetry":
if self._poetry is not None:
return self._poetry
- self._poetry = Factory().create_poetry(Path.cwd())
+ self._poetry = Factory().create_poetry(
+ Path.cwd(), io=self._io, disable_plugins=self._disable_plugins
+ )
return self._poetry
+ @property
+ def command_loader(self) -> CommandLoader:
+ return self._command_loader
+
def reset_poetry(self) -> None:
self._poetry = None
@@ -138,8 +152,17 @@ def create_io(
io.output.set_formatter(formatter)
io.error_output.set_formatter(formatter)
+ self._io = io
+
return io
+ def _run(self, io: IO) -> int:
+ self._disable_plugins = io.input.parameter_option("--no-plugins")
+
+ self._load_plugins(io)
+
+ return super()._run(io)
+
def _configure_io(self, io: IO) -> None:
# We need to check if the command being run
# is the "run" command.
@@ -221,7 +244,9 @@ def register_command_loggers(
logger.setLevel(level)
- def set_env(self, event: ConsoleCommandEvent, event_name: str, _: Any) -> None:
+ def configure_env(
+ self, event: ConsoleCommandEvent, event_name: str, _: Any
+ ) -> None:
from .commands.env_command import EnvCommand
command: EnvCommand = cast(EnvCommand, event.command)
@@ -244,7 +269,7 @@ def set_env(self, event: ConsoleCommandEvent, event_name: str, _: Any) -> None:
command.set_env(env)
- def set_installer(
+ def configure_installer(
self, event: ConsoleCommandEvent, event_name: str, _: Any
) -> None:
from .commands.installer_command import InstallerCommand
@@ -258,11 +283,14 @@ def set_installer(
if command.installer is not None:
return
+ self._configure_installer(command, event.io)
+
+ def _configure_installer(self, command: "InstallerCommand", io: "IO") -> None:
from poetry.installation.installer import Installer
poetry = command.poetry
installer = Installer(
- event.io,
+ io,
command.env,
poetry.package,
poetry.locker,
@@ -272,6 +300,33 @@ def set_installer(
installer.use_executor(poetry.config.get("experimental.new-installer", False))
command.set_installer(installer)
+ def _load_plugins(self, io: IO) -> None:
+ if self._plugins_loaded:
+ return
+
+ self._disable_plugins = io.input.has_parameter_option("--no-plugins")
+
+ if not self._disable_plugins:
+ from poetry.plugins.plugin_manager import PluginManager
+
+ manager = PluginManager("application.plugin")
+ manager.load_plugins()
+ manager.activate(self)
+
+ self._plugins_loaded = True
+
+ @property
+ def _default_definition(self) -> "Definition":
+ from cleo.io.inputs.option import Option
+
+ definition = super()._default_definition
+
+ definition.add_option(
+ Option("--no-plugins", flag=True, description="Disables plugins.")
+ )
+
+ return definition
+
def main() -> int:
return Application().run()
diff --git a/poetry/console/command_loader.py b/poetry/console/command_loader.py
new file mode 100644
index 00000000000..852abe07dc6
--- /dev/null
+++ b/poetry/console/command_loader.py
@@ -0,0 +1,12 @@
+from typing import Callable
+
+from cleo.exceptions import LogicException
+from cleo.loaders.factory_command_loader import FactoryCommandLoader
+
+
+class CommandLoader(FactoryCommandLoader):
+ def register_factory(self, command_name: str, factory: Callable) -> None:
+ if command_name in self._factories:
+ raise LogicException(f'The command "{command_name}" already exists.')
+
+ self._factories[command_name] = factory
diff --git a/poetry/console/commands/command.py b/poetry/console/commands/command.py
index be87fe99b7a..a717fa4e666 100644
--- a/poetry/console/commands/command.py
+++ b/poetry/console/commands/command.py
@@ -1,4 +1,5 @@
from typing import TYPE_CHECKING
+from typing import Optional
from cleo.commands.command import Command as BaseCommand
@@ -11,9 +12,17 @@
class Command(BaseCommand):
loggers = []
+ _poetry: Optional["Poetry"] = None
+
@property
def poetry(self) -> "Poetry":
- return self.get_application().poetry
+ if self._poetry is None:
+ return self.get_application().poetry
+
+ return self._poetry
+
+ def set_poetry(self, poetry: "Poetry") -> None:
+ self._poetry = poetry
def get_application(self) -> "Application":
return self.application
diff --git a/poetry/console/commands/env_command.py b/poetry/console/commands/env_command.py
index beb40e1e88e..fd44b415c00 100644
--- a/poetry/console/commands/env_command.py
+++ b/poetry/console/commands/env_command.py
@@ -4,7 +4,7 @@
if TYPE_CHECKING:
- from poetry.utils.env import VirtualEnv
+ from poetry.utils.env import Env
class EnvCommand(Command):
@@ -14,8 +14,8 @@ def __init__(self) -> None:
super(EnvCommand, self).__init__()
@property
- def env(self) -> "VirtualEnv":
+ def env(self) -> "Env":
return self._env
- def set_env(self, env: "VirtualEnv") -> None:
+ def set_env(self, env: "Env") -> None:
self._env = env
diff --git a/poetry/console/commands/init.py b/poetry/console/commands/init.py
index 2e10f2ead01..b2e4fd0a315 100644
--- a/poetry/console/commands/init.py
+++ b/poetry/console/commands/init.py
@@ -430,20 +430,32 @@ def _parse_requirements(self, requirements: List[str]) -> List[Dict[str, str]]:
result.append(pair)
continue
- elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath(
- requirement
- ).exists():
- path = cwd.joinpath(requirement)
+ elif (os.path.sep in requirement or "/" in requirement) and (
+ cwd.joinpath(requirement).exists()
+ or Path(requirement).expanduser().exists()
+ and Path(requirement).expanduser().is_absolute()
+ ):
+ path = Path(requirement).expanduser()
+ is_absolute = path.is_absolute()
+
+ if not path.is_absolute():
+ path = cwd.joinpath(requirement)
+
if path.is_file():
package = Provider.get_package_from_file(path.resolve())
else:
- package = Provider.get_package_from_directory(path)
+ package = Provider.get_package_from_directory(path.resolve())
result.append(
dict(
[
("name", package.name),
- ("path", path.relative_to(cwd).as_posix()),
+ (
+ "path",
+ path.relative_to(cwd).as_posix()
+ if not is_absolute
+ else path.as_posix(),
+ ),
]
+ ([("extras", extras)] if extras else [])
)
diff --git a/.coveragerc b/poetry/console/commands/plugin/__init__.py
similarity index 100%
rename from .coveragerc
rename to poetry/console/commands/plugin/__init__.py
diff --git a/poetry/console/commands/plugin/add.py b/poetry/console/commands/plugin/add.py
new file mode 100644
index 00000000000..8418aa25c89
--- /dev/null
+++ b/poetry/console/commands/plugin/add.py
@@ -0,0 +1,203 @@
+import os
+
+from typing import TYPE_CHECKING
+from typing import Dict
+from typing import List
+from typing import cast
+
+from cleo.helpers import argument
+from cleo.helpers import option
+
+from ..init import InitCommand
+
+
+if TYPE_CHECKING:
+ from poetry.console.application import Application # noqa
+ from poetry.console.commands.update import UpdateCommand # noqa
+
+
+class PluginAddCommand(InitCommand):
+
+ name = "plugin add"
+
+ description = "Adds new 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).",
+ )
+ ]
+
+ help = """
+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)\
+"""
+
+ 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.pyproject.toml import PyProjectTOML
+ from poetry.core.semver.helpers import parse_constraint
+ from poetry.factory import Factory
+ from poetry.packages.project_package import ProjectPackage
+ 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()
+
+ env_dir = Path(
+ os.getenv("POETRY_HOME") if os.getenv("POETRY_HOME") else system_env.path
+ )
+
+ # We check for the plugins existence first.
+ if env_dir.joinpath("pyproject.toml").exists():
+ pyproject = 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 taken into account when resolving dependencies.
+ installed_repository = InstalledRepository.load(
+ system_env, with_dependencies=True
+ )
+
+ 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
+
+ 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 = PyProjectTOML(env_dir.joinpath("pyproject.toml"))
+ poetry_content = pyproject.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 = 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"])
+
+ pyproject.save()
+
+ # 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"):
+ argv.append("--dry-run")
+
+ return update_command.run(
+ IO(
+ StringInput(" ".join(argv)),
+ self._io.output,
+ self._io.error_output,
+ )
+ )
+
+ def get_existing_packages_from_input(
+ self, packages: List[str], poetry_content: Dict, 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(" • {name}".format(name=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/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/plugin/show.py b/poetry/console/commands/plugin/show.py
new file mode 100644
index 00000000000..ced1a0fd282
--- /dev/null
+++ b/poetry/console/commands/plugin/show.py
@@ -0,0 +1,95 @@
+from collections import defaultdict
+from typing import TYPE_CHECKING
+from typing import DefaultDict
+from typing import Dict
+from typing import List
+from typing import Union
+
+from poetry.console.commands.command import Command
+
+
+if TYPE_CHECKING:
+ from poetry.core.packages.package import Package
+
+
+class PluginShowCommand(Command):
+
+ name = "plugin show"
+
+ description = "Shows information about the currently installed plugins."
+
+ def handle(self) -> int:
+ from poetry.plugins.application_plugin import ApplicationPlugin
+ 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
+
+ plugins: DefaultDict[str, Dict[str, Union["Package", List[str]]]] = defaultdict(
+ lambda: {
+ "package": None,
+ "plugins": [],
+ "application_plugins": [],
+ }
+ )
+
+ entry_points = (
+ PluginManager("application.plugin").get_plugin_entry_points()
+ + PluginManager("plugin").get_plugin_entry_points()
+ )
+
+ system_env = EnvManager.get_system_env()
+ 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.name)]
+ plugins[package.pretty_name]["package"] = package
+ plugins[package.pretty_name][category].append(entry_point)
+
+ for name, info in plugins.items():
+ package = info["package"]
+ self.line("")
+ self.line(
+ " • {} ({}){}".format(
+ name,
+ package.version,
+ " " + package.description if package.description else "",
+ )
+ )
+ provide_line = " "
+ if info["plugins"]:
+ provide_line += " {} plugin{}".format(
+ len(info["plugins"]), "s" if len(info["plugins"]) > 1 else ""
+ )
+
+ if info["application_plugins"]:
+ if info["plugins"]:
+ provide_line += " and"
+
+ provide_line += " {} application plugin{}".format(
+ len(info["application_plugins"]),
+ "s" if len(info["application_plugins"]) > 1 else "",
+ )
+
+ self.line(provide_line)
+
+ if package.requires:
+ self.line("")
+ self.line(" Dependencies")
+ for dependency in package.requires:
+ self.line(
+ " - {} ({})".format(
+ dependency.pretty_name, dependency.pretty_constraint
+ )
+ )
+
+ return 0
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/poetry/console/events/__init__.py b/poetry/console/events/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/poetry/console/events/console_events.py b/poetry/console/events/console_events.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/poetry/factory.py b/poetry/factory.py
index 43555eef6c3..36078bed1e5 100644
--- a/poetry/factory.py
+++ b/poetry/factory.py
@@ -4,6 +4,7 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Dict
+from typing import List
from typing import Optional
from cleo.io.io import IO
@@ -16,6 +17,8 @@
from .config.file_config_source import FileConfigSource
from .locations import CONFIG_DIR
from .packages.locker import Locker
+from .packages.project_package import ProjectPackage
+from .plugins.plugin_manager import PluginManager
from .poetry import Poetry
from .repositories.pypi_repository import PyPiRepository
@@ -30,7 +33,10 @@ class Factory(BaseFactory):
"""
def create_poetry(
- self, cwd: Optional[Path] = None, io: Optional[IO] = None
+ self,
+ cwd: Optional[Path] = None,
+ io: Optional[IO] = None,
+ disable_plugins: bool = False,
) -> Poetry:
if io is None:
io = NullIO()
@@ -75,35 +81,21 @@ def create_poetry(
)
# Configuring sources
- sources = poetry.local_config.get("source", [])
- for source in sources:
- repository = self.create_legacy_repository(source, config)
- is_default = source.get("default", False)
- is_secondary = source.get("secondary", False)
- if io.is_debug():
- message = "Adding repository {} ({})".format(
- repository.name, repository.url
- )
- if is_default:
- message += " and setting it as the default one"
- elif is_secondary:
- message += " and setting it as secondary"
-
- io.write_line(message)
-
- poetry.pool.add_repository(repository, is_default, secondary=is_secondary)
+ self.configure_sources(
+ poetry, poetry.local_config.get("source", []), config, io
+ )
- # Always put PyPI last to prefer private repositories
- # but only if we have no other default source
- if not poetry.pool.has_default():
- has_sources = bool(sources)
- poetry.pool.add_repository(PyPiRepository(), not has_sources, has_sources)
- else:
- if io.is_debug():
- io.write_line("Deactivating the PyPI repository")
+ plugin_manager = PluginManager("plugin", disable_plugins=disable_plugins)
+ plugin_manager.load_plugins()
+ poetry.set_plugin_manager(plugin_manager)
+ plugin_manager.activate(poetry, io)
return poetry
+ @classmethod
+ def get_package(cls, name: str, version: str) -> ProjectPackage:
+ return ProjectPackage(name, version, version)
+
@classmethod
def create_config(cls, io: Optional[IO] = None) -> Config:
if io is None:
@@ -140,8 +132,39 @@ def create_config(cls, io: Optional[IO] = None) -> Config:
return config
+ @classmethod
+ def configure_sources(
+ cls, poetry: "Poetry", sources: List[Dict[str, str]], config: "Config", io: "IO"
+ ) -> None:
+ for source in sources:
+ repository = cls.create_legacy_repository(source, config)
+ is_default = source.get("default", False)
+ is_secondary = source.get("secondary", False)
+ if io.is_debug():
+ message = "Adding repository {} ({})".format(
+ repository.name, repository.url
+ )
+ if is_default:
+ message += " and setting it as the default one"
+ elif is_secondary:
+ message += " and setting it as secondary"
+
+ io.write_line(message)
+
+ poetry.pool.add_repository(repository, is_default, secondary=is_secondary)
+
+ # Always put PyPI last to prefer private repositories
+ # but only if we have no other default source
+ if not poetry.pool.has_default():
+ has_sources = bool(sources)
+ poetry.pool.add_repository(PyPiRepository(), not has_sources, has_sources)
+ else:
+ if io.is_debug():
+ io.write_line("Deactivating the PyPI repository")
+
+ @classmethod
def create_legacy_repository(
- self, source: Dict[str, str], auth_config: Config
+ cls, source: Dict[str, str], auth_config: Config
) -> "LegacyRepository":
from .repositories.legacy_repository import LegacyRepository
from .utils.helpers import get_cert
@@ -164,3 +187,49 @@ def create_legacy_repository(
cert=get_cert(auth_config, name),
client_cert=get_client_cert(auth_config, name),
)
+
+ @classmethod
+ def create_pyproject_from_package(
+ cls, package: "ProjectPackage", path: "Path"
+ ) -> None:
+ import tomlkit
+
+ from poetry.layouts.layout import POETRY_DEFAULT
+
+ pyproject = tomlkit.loads(POETRY_DEFAULT)
+ content = pyproject["tool"]["poetry"]
+
+ content["name"] = package.name
+ content["version"] = package.version.text
+ content["description"] = package.description
+ content["authors"] = package.authors
+
+ dependency_section = content["dependencies"]
+ dependency_section["python"] = package.python_versions
+
+ for dep in package.requires:
+ constraint = tomlkit.inline_table()
+ if dep.is_vcs():
+ constraint[dep.vcs] = dep.source_url
+
+ if dep.reference:
+ constraint["rev"] = dep.reference
+ elif dep.is_file() or dep.is_directory():
+ constraint["path"] = dep.source_url
+ else:
+ constraint["version"] = dep.pretty_constraint
+
+ if not dep.marker.is_any():
+ constraint["markers"] = str(dep.marker)
+
+ if dep.extras:
+ constraint["extras"] = list(sorted(dep.extras))
+
+ if len(constraint) == 1 and "version" in constraint:
+ constraint = constraint["version"]
+
+ dependency_section[dep.name] = constraint
+
+ path.joinpath("pyproject.toml").write_text(
+ pyproject.as_string(), encoding="utf-8"
+ )
diff --git a/poetry/installation/installer.py b/poetry/installation/installer.py
index b778711479b..bc254c135e3 100644
--- a/poetry/installation/installer.py
+++ b/poetry/installation/installer.py
@@ -40,7 +40,7 @@ def __init__(
locker: Locker,
pool: Pool,
config: Config,
- installed: Union[InstalledRepository, None] = None,
+ installed: Union[Repository, None] = None,
executor: Optional[Executor] = None,
):
self._io = io
diff --git a/poetry/locations.py b/poetry/locations.py
index 5bd4b7feb17..ff38c9c9e82 100644
--- a/poetry/locations.py
+++ b/poetry/locations.py
@@ -2,9 +2,11 @@
from .utils.appdirs import user_cache_dir
from .utils.appdirs import user_config_dir
+from .utils.appdirs import user_data_dir
CACHE_DIR = user_cache_dir("pypoetry")
+DATA_DIR = user_data_dir("pypoetry")
CONFIG_DIR = user_config_dir("pypoetry")
REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories"
diff --git a/poetry/packages/project_package.py b/poetry/packages/project_package.py
new file mode 100644
index 00000000000..22379c2026f
--- /dev/null
+++ b/poetry/packages/project_package.py
@@ -0,0 +1,23 @@
+from typing import TYPE_CHECKING
+from typing import Optional
+from typing import Union
+
+from poetry.core.packages.project_package import ProjectPackage as _ProjectPackage
+
+
+if TYPE_CHECKING:
+ from poetry.core.semver.version import Version # noqa
+
+
+class ProjectPackage(_ProjectPackage):
+ def set_version(
+ self, version: Union[str, "Version"], pretty_version: Optional[str] = None
+ ) -> "ProjectPackage":
+ from poetry.core.semver.version import Version # noqa
+
+ if not isinstance(version, Version):
+ self._version = Version.parse(version)
+ self._pretty_version = pretty_version or version
+ else:
+ self._version = version
+ self._pretty_version = pretty_version or version.text
diff --git a/poetry/plugins/__init__.py b/poetry/plugins/__init__.py
new file mode 100644
index 00000000000..c81eb48d65f
--- /dev/null
+++ b/poetry/plugins/__init__.py
@@ -0,0 +1,5 @@
+from .application_plugin import ApplicationPlugin
+from .plugin import Plugin
+
+
+__all__ = ["ApplicationPlugin", "Plugin"]
diff --git a/poetry/plugins/application_plugin.py b/poetry/plugins/application_plugin.py
new file mode 100644
index 00000000000..0f896282172
--- /dev/null
+++ b/poetry/plugins/application_plugin.py
@@ -0,0 +1,12 @@
+from .base_plugin import BasePlugin
+
+
+class ApplicationPlugin(BasePlugin):
+ """
+ Base class for plugins.
+ """
+
+ type = "application.plugin"
+
+ def activate(self, application):
+ raise NotImplementedError()
diff --git a/poetry/plugins/base_plugin.py b/poetry/plugins/base_plugin.py
new file mode 100644
index 00000000000..9e287c8178c
--- /dev/null
+++ b/poetry/plugins/base_plugin.py
@@ -0,0 +1,6 @@
+class BasePlugin(object):
+ """
+ Base class for all plugin types
+ """
+
+ PLUGIN_API_VERSION = "1.0.0"
diff --git a/poetry/plugins/plugin.py b/poetry/plugins/plugin.py
new file mode 100644
index 00000000000..0c2f0711a29
--- /dev/null
+++ b/poetry/plugins/plugin.py
@@ -0,0 +1,14 @@
+from .base_plugin import BasePlugin
+
+
+class Plugin(BasePlugin):
+ """
+ Generic plugin not related to the console application.
+ The activate() method must be implemented and receives
+ the Poetry instance.
+ """
+
+ type = "plugin"
+
+ def activate(self, poetry, io):
+ raise NotImplementedError()
diff --git a/poetry/plugins/plugin_manager.py b/poetry/plugins/plugin_manager.py
new file mode 100644
index 00000000000..6f9e8f49ba2
--- /dev/null
+++ b/poetry/plugins/plugin_manager.py
@@ -0,0 +1,60 @@
+import logging
+
+from typing import List
+
+import entrypoints
+
+from .application_plugin import ApplicationPlugin
+from .plugin import Plugin
+
+
+logger = logging.getLogger(__name__)
+
+
+class PluginManager(object):
+ """
+ This class registers and activates plugins.
+ """
+
+ def __init__(self, type, disable_plugins=False): # type: (str, bool) -> None
+ self._type = type
+ self._disable_plugins = disable_plugins
+ self._plugins = []
+
+ def load_plugins(self): # type: () -> None
+ if self._disable_plugins:
+ return
+
+ plugin_entrypoints = self.get_plugin_entry_points()
+
+ for entrypoint in plugin_entrypoints:
+ self._load_plugin_entrypoint(entrypoint)
+
+ def get_plugin_entry_points(self) -> List[entrypoints.EntryPoint]:
+ return entrypoints.get_group_all("poetry.{}".format(self._type))
+
+ def add_plugin(self, plugin): # type: (Plugin) -> None
+ if not isinstance(plugin, (Plugin, ApplicationPlugin)):
+ raise ValueError(
+ "The Poetry plugin must be an instance of Plugin or ApplicationPlugin"
+ )
+
+ self._plugins.append(plugin)
+
+ def activate(self, *args, **kwargs):
+ for plugin in self._plugins:
+ plugin.activate(*args, **kwargs)
+
+ def _load_plugin_entrypoint(
+ self, entrypoint
+ ): # type: (entrypoints.EntryPoint) -> None
+ logger.debug("Loading the {} plugin".format(entrypoint.name))
+
+ plugin = entrypoint.load()
+
+ if not issubclass(plugin, (Plugin, ApplicationPlugin)):
+ raise ValueError(
+ "The Poetry plugin must be an instance of Plugin or ApplicationPlugin"
+ )
+
+ self.add_plugin(plugin())
diff --git a/poetry/poetry.py b/poetry/poetry.py
index 5c248d46224..54fee74b7fc 100644
--- a/poetry/poetry.py
+++ b/poetry/poetry.py
@@ -11,6 +11,7 @@
from .config.config import Config
from .packages.locker import Locker
+ from .plugins.plugin_manager import PluginManager
from .repositories.pool import Pool
@@ -33,6 +34,7 @@ def __init__(
self._locker = locker
self._config = config
self._pool = Pool()
+ self._plugin_manager = None
@property
def locker(self) -> "Locker":
@@ -60,3 +62,8 @@ def set_config(self, config: "Config") -> "Poetry":
self._config = config
return self
+
+ def set_plugin_manager(self, plugin_manager: "PluginManager") -> "Poetry":
+ self._plugin_manager = plugin_manager
+
+ return self
diff --git a/poetry/puzzle/provider.py b/poetry/puzzle/provider.py
index f18a56e0579..3d384a6697d 100644
--- a/poetry/puzzle/provider.py
+++ b/poetry/puzzle/provider.py
@@ -431,7 +431,6 @@ def incompatibilities_for(
]
def complete_package(self, package: DependencyPackage) -> DependencyPackage:
-
if package.is_root():
package = package.clone()
requires = package.all_requires
diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py
index c3f9e13c764..f33bbd9b99f 100644
--- a/poetry/repositories/installed_repository.py
+++ b/poetry/repositories/installed_repository.py
@@ -100,10 +100,12 @@ def is_vcs_package(cls, package: Union[Path, Package], env: Env) -> bool:
return True
@classmethod
- def load(cls, env: Env) -> "InstalledRepository":
+ def load(cls, env: Env, with_dependencies: bool = False) -> "InstalledRepository":
"""
Load installed packages.
"""
+ from poetry.core.packages.dependency import Dependency
+
repo = cls()
seen = set()
@@ -118,6 +120,11 @@ def load(cls, env: Env) -> "InstalledRepository":
package = Package(name, version, version)
package.description = distribution.metadata.get("summary", "")
+ if with_dependencies:
+ for require in distribution.metadata.get_all("requires-dist", []):
+ dep = Dependency.create_from_pep_508(require)
+ package.add_dependency(dep)
+
if package.name in seen:
continue
diff --git a/poetry/utils/env.py b/poetry/utils/env.py
index a4588617d1e..4678cfb5558 100644
--- a/poetry/utils/env.py
+++ b/poetry/utils/env.py
@@ -803,7 +803,7 @@ def create_venv(
p_venv = os.path.normcase(str(venv))
if any(p.startswith(p_venv) for p in paths):
# Running properly in the virtualenv, don't need to do anything
- return SystemEnv(Path(sys.prefix), self.get_base_prefix())
+ return self.get_system_env()
return VirtualEnv(venv)
@@ -874,7 +874,12 @@ def remove_venv(cls, path: Union[Path, str]) -> None:
elif file_path.is_dir():
shutil.rmtree(str(file_path))
- def get_base_prefix(self) -> Path:
+ @classmethod
+ def get_system_env(cls) -> "SystemEnv":
+ return SystemEnv(Path(sys.prefix), cls.get_base_prefix())
+
+ @classmethod
+ def get_base_prefix(cls) -> Path:
if hasattr(sys, "real_prefix"):
return Path(sys.real_prefix)
diff --git a/pyproject.toml b/pyproject.toml
index a487a789dbf..b68c65adea7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,7 @@ pexpect = "^4.7.0"
packaging = "^20.4"
virtualenv = "^20.4.3"
keyring = "^21.2.0"
+entrypoints = "^0.3"
importlib-metadata = {version = "^1.6.0", python = "<3.8"}
[tool.poetry.dev-dependencies]
@@ -98,3 +99,9 @@ exclude = '''
| tests/.*/setup.py
)/
'''
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "if TYPE_CHECKING:"
+]
diff --git a/tests/console/commands/plugin/__init__.py b/tests/console/commands/plugin/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/console/commands/plugin/test_add.py b/tests/console/commands/plugin/test_add.py
new file mode 100644
index 00000000000..fdcc46ed423
--- /dev/null
+++ b/tests/console/commands/plugin/test_add.py
@@ -0,0 +1,297 @@
+import pytest
+
+from poetry.__version__ import __version__
+from poetry.core.packages.package import Package
+from poetry.factory import Factory
+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 add")
+
+
+@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)
+ )
+
+
+def test_add_no_constraint(app, repo, tester, env, installed):
+ repo.add_package(Package("poetry-plugin", "0.1.0"))
+
+ tester.execute("poetry-plugin")
+
+ expected = """\
+Using version ^0.1.0 for poetry-plugin
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 1 install, 0 updates, 0 removals
+
+ • Installing poetry-plugin (0.1.0)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = 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"] == "^0.1.0"
+
+
+def test_add_with_constraint(app, repo, tester, env, installed):
+ 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 = """\
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 1 install, 0 updates, 0 removals
+
+ • Installing poetry-plugin (0.2.0)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = app.find("update")
+ assert update_command.poetry.file.parent == env.path
+ assert update_command.poetry.locker.lock.parent == env.path
+
+ content = update_command.poetry.file.read()["tool"]["poetry"]
+ assert "poetry-plugin" in content["dependencies"]
+ assert content["dependencies"]["poetry-plugin"] == "^0.2.0"
+
+
+def test_add_with_git_constraint(app, repo, tester, env, installed):
+ repo.add_package(Package("pendulum", "2.0.5"))
+
+ tester.execute("git+https://github.com/demo/poetry-plugin.git")
+
+ expected = """\
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 2 installs, 0 updates, 0 removals
+
+ • Installing pendulum (2.0.5)
+ • Installing poetry-plugin (0.1.2 9cf87a2)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = app.find("update")
+ assert update_command.poetry.file.parent == env.path
+ assert update_command.poetry.locker.lock.parent == env.path
+
+ content = update_command.poetry.file.read()["tool"]["poetry"]
+ assert "poetry-plugin" in content["dependencies"]
+ assert content["dependencies"]["poetry-plugin"] == {
+ "git": "https://github.com/demo/poetry-plugin.git"
+ }
+
+
+def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed):
+ 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 = """\
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 3 installs, 0 updates, 0 removals
+
+ • Installing pendulum (2.0.5)
+ • Installing tomlkit (0.7.0)
+ • Installing poetry-plugin (0.1.2 9cf87a2)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = app.find("update")
+ assert update_command.poetry.file.parent == env.path
+ assert update_command.poetry.locker.lock.parent == env.path
+
+ content = update_command.poetry.file.read()["tool"]["poetry"]
+ assert "poetry-plugin" in content["dependencies"]
+ assert content["dependencies"]["poetry-plugin"] == {
+ "git": "https://github.com/demo/poetry-plugin.git",
+ "extras": ["foo"],
+ }
+
+
+def test_add_existing_plugin_warns_about_no_operation(
+ app, repo, tester, env, installed
+):
+ env.path.joinpath("pyproject.toml").write_text(
+ """\
+[tool.poetry]
+name = "poetry"
+version = "1.2.0"
+description = "Python dependency management and packaging made easy."
+authors = [
+ "Sébastien Eustace "
+]
+
+[tool.poetry.dependencies]
+python = "^3.6"
+poetry-plugin = "^1.2.3"
+""",
+ encoding="utf-8",
+ )
+
+ installed.add_package(Package("poetry-plugin", "1.2.3"))
+
+ repo.add_package(Package("poetry-plugin", "1.2.3"))
+
+ tester.execute("poetry-plugin")
+
+ expected = """\
+The following plugins are already present in the pyproject.toml file 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`.
+
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = 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, repo, tester, env, installed, mocker
+):
+ env.path.joinpath("pyproject.toml").write_text(
+ """\
+[tool.poetry]
+name = "poetry"
+version = "1.2.0"
+description = "Python dependency management and packaging made easy."
+authors = [
+ "Sébastien Eustace "
+]
+
+[tool.poetry.dependencies]
+python = "^3.6"
+poetry-plugin = "^1.2.3"
+""",
+ encoding="utf-8",
+ )
+
+ installed.add_package(Package("poetry-plugin", "1.2.3"))
+
+ repo.add_package(Package("poetry-plugin", "1.2.3"))
+ repo.add_package(Package("poetry-plugin", "2.3.4"))
+
+ tester.execute("poetry-plugin@latest")
+
+ expected = """\
+Using version ^2.3.4 for poetry-plugin
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 0 installs, 1 update, 0 removals
+
+ • Updating poetry-plugin (1.2.3 -> 2.3.4)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = 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"] == "^2.3.4"
+
+
+def test_adding_a_plugin_can_update_poetry_dependencies_if_needed(
+ app, repo, tester, env, installed
+):
+ poetry_package = Package("poetry", "1.2.0")
+ poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0"))
+
+ plugin_package = Package("poetry-plugin", "1.2.3")
+ plugin_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.2"))
+
+ installed.add_package(poetry_package)
+ installed.add_package(Package("tomlkit", "0.7.1"))
+
+ repo.add_package(plugin_package)
+ repo.add_package(Package("tomlkit", "0.7.1"))
+ repo.add_package(Package("tomlkit", "0.7.2"))
+
+ tester.execute("poetry-plugin")
+
+ expected = """\
+Using version ^1.2.3 for poetry-plugin
+Updating dependencies
+Resolving dependencies...
+
+Writing lock file
+
+Package operations: 1 install, 1 update, 0 removals
+
+ • Updating tomlkit (0.7.1 -> 0.7.2)
+ • Installing poetry-plugin (1.2.3)
+"""
+
+ assert tester.io.fetch_output() == expected
+
+ update_command = 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"] == "^1.2.3"
diff --git a/tests/console/commands/plugin/test_remove.py b/tests/console/commands/plugin/test_remove.py
new file mode 100644
index 00000000000..0a66d8717cc
--- /dev/null
+++ 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"]
diff --git a/tests/console/commands/plugin/test_show.py b/tests/console/commands/plugin/test_show.py
new file mode 100644
index 00000000000..80c990376da
--- /dev/null
+++ b/tests/console/commands/plugin/test_show.py
@@ -0,0 +1,172 @@
+import pytest
+
+from entrypoints import EntryPoint as _EntryPoint
+
+from poetry.__version__ import __version__
+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
+from poetry.repositories.installed_repository import InstalledRepository
+from poetry.repositories.pool import Pool
+from poetry.utils.env import EnvManager
+
+
+class EntryPoint(_EntryPoint):
+ def load(self):
+ if "ApplicationPlugin" in self.object_name:
+ return ApplicationPlugin
+
+ return Plugin
+
+
+@pytest.fixture()
+def tester(command_tester_factory):
+ return command_tester_factory("plugin show")
+
+
+@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)
+ )
+
+
+def test_show_displays_installed_plugins(app, tester, installed, mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ side_effect=[
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:ApplicationPlugin",
+ "FirstApplicationPlugin",
+ )
+ ],
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:Plugin",
+ "FirstPlugin",
+ )
+ ],
+ ],
+ )
+
+ installed.add_package(Package("poetry-plugin", "1.2.3"))
+
+ 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, tester, installed, mocker
+):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ side_effect=[
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:ApplicationPlugin",
+ "FirstApplicationPlugin",
+ ),
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:ApplicationPlugin",
+ "SecondApplicationPlugin",
+ ),
+ ],
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:Plugin",
+ "FirstPlugin",
+ ),
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:Plugin",
+ "SecondPlugin",
+ ),
+ ],
+ ],
+ )
+
+ installed.add_package(Package("poetry-plugin", "1.2.3"))
+
+ 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, tester, installed, mocker
+):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ side_effect=[
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:ApplicationPlugin",
+ "FirstApplicationPlugin",
+ )
+ ],
+ [
+ EntryPoint(
+ "poetry-plugin",
+ "poetry_plugin.plugins:Plugin",
+ "FirstPlugin",
+ )
+ ],
+ ],
+ )
+
+ plugin = Package("poetry-plugin", "1.2.3")
+ plugin.add_dependency(Factory.create_dependency("foo", ">=1.2.3"))
+ plugin.add_dependency(Factory.create_dependency("bar", "<4.5.6"))
+ installed.add_package(plugin)
+
+ 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/test_application.py b/tests/console/test_application.py
new file mode 100644
index 00000000000..6e4e5bab98f
--- /dev/null
+++ b/tests/console/test_application.py
@@ -0,0 +1,101 @@
+import re
+
+from cleo.testers.application_tester import ApplicationTester
+from entrypoints import EntryPoint
+
+from poetry.console.application import Application
+from poetry.console.commands.command import Command
+from poetry.plugins.application_plugin import ApplicationPlugin
+
+
+class FooCommand(Command):
+ name = "foo"
+
+ description = "Foo Command"
+
+ def handle(self):
+ self.line("foo called")
+
+ return 0
+
+
+class AddCommandPlugin(ApplicationPlugin):
+ def activate(self, application: Application):
+ application.command_loader.register_factory("foo", lambda: FooCommand())
+
+
+def test_application_with_plugins(mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint(
+ "my-plugin", "tests.console.test_application", "AddCommandPlugin"
+ )
+ ],
+ )
+
+ app = Application()
+
+ tester = ApplicationTester(app)
+ tester.execute("")
+
+ assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is not None
+ assert 0 == tester.status_code
+
+
+def test_application_with_plugins_disabled(mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint(
+ "my-plugin", "tests.console.test_application", "AddCommandPlugin"
+ )
+ ],
+ )
+
+ app = Application()
+
+ tester = ApplicationTester(app)
+ tester.execute("--no-plugins")
+
+ assert re.search(r"\s+foo\s+Foo Command", tester.io.fetch_output()) is None
+ assert 0 == tester.status_code
+
+
+def test_application_execute_plugin_command(mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint(
+ "my-plugin", "tests.console.test_application", "AddCommandPlugin"
+ )
+ ],
+ )
+
+ app = Application()
+
+ tester = ApplicationTester(app)
+ tester.execute("foo")
+
+ assert "foo called\n" == tester.io.fetch_output()
+ assert 0 == tester.status_code
+
+
+def test_application_execute_plugin_command_with_plugins_disabled(mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint(
+ "my-plugin", "tests.console.test_application", "AddCommandPlugin"
+ )
+ ],
+ )
+
+ app = Application()
+
+ tester = ApplicationTester(app)
+ tester.execute("foo --no-plugins")
+
+ assert "" == tester.io.fetch_output()
+ assert '\nThe command "foo" does not exist.\n' == tester.io.fetch_error()
+ assert 1 == tester.status_code
diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin/poetry_plugin/__init__.py b/tests/fixtures/git/github.com/demo/poetry-plugin/poetry_plugin/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml b/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml
new file mode 100644
index 00000000000..b45d9d976eb
--- /dev/null
+++ b/tests/fixtures/git/github.com/demo/poetry-plugin/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "poetry-plugin"
+version = "0.1.2"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = "^3.6"
+pendulum = "^2.0"
+tomlkit = {version = "^0.7.0", optional = true}
+
+[tool.poetry.extras]
+foo = ["tomlkit"]
+
+[tool.poetry.dev-dependencies]
diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py
new file mode 100644
index 00000000000..b8a4c5d5230
--- /dev/null
+++ b/tests/plugins/test_plugin_manager.py
@@ -0,0 +1,112 @@
+from pathlib import Path
+
+import pytest
+
+from cleo.io.buffered_io import BufferedIO
+from entrypoints import EntryPoint
+
+from poetry.packages.locker import Locker
+from poetry.packages.project_package import ProjectPackage
+from poetry.plugins import ApplicationPlugin
+from poetry.plugins import Plugin
+from poetry.plugins.plugin_manager import PluginManager
+from poetry.poetry import Poetry
+
+
+CWD = Path(__file__).parent.parent / "fixtures" / "simple_project"
+
+
+class MyPlugin(Plugin):
+ def activate(self, poetry, io):
+ io.write_line("Updating version")
+ poetry.package.set_version("9.9.9")
+
+
+class MyCommandPlugin(ApplicationPlugin):
+ @property
+ def commands(self):
+ return []
+
+
+class InvalidPlugin:
+ def activate(self, poetry, io):
+ io.write_line("Updating version")
+ poetry.package.version = "9.9.9"
+
+
+@pytest.fixture()
+def poetry(tmp_dir, config):
+ poetry = Poetry(
+ CWD / "pyproject.toml",
+ {},
+ ProjectPackage("simple-project", "1.2.3"),
+ Locker(CWD / "poetry.lock", {}),
+ config,
+ )
+
+ return poetry
+
+
+@pytest.fixture()
+def io():
+ return BufferedIO()
+
+
+@pytest.fixture()
+def manager_factory(poetry, io):
+ def _manager(type="plugin"):
+ return PluginManager(type)
+
+ return _manager
+
+
+@pytest.fixture()
+def no_plugin_manager(poetry, io):
+ return PluginManager("plugin", disable_plugins=True)
+
+
+def test_load_plugins_and_activate(manager_factory, poetry, io, mocker):
+ manager = manager_factory()
+
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint("my-plugin", "tests.plugins.test_plugin_manager", "MyPlugin")
+ ],
+ )
+
+ manager.load_plugins()
+ manager.activate(poetry, io)
+
+ assert "9.9.9" == poetry.package.version.text
+ assert "Updating version\n" == io.fetch_output()
+
+
+def test_load_plugins_with_invalid_plugin(manager_factory, poetry, io, mocker):
+ manager = manager_factory()
+
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint(
+ "my-plugin", "tests.plugins.test_plugin_manager", "InvalidPlugin"
+ )
+ ],
+ )
+
+ with pytest.raises(ValueError):
+ manager.load_plugins()
+
+
+def test_load_plugins_with_plugins_disabled(no_plugin_manager, poetry, io, mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[
+ EntryPoint("my-plugin", "tests.plugins.test_plugin_manager", "MyPlugin")
+ ],
+ )
+
+ no_plugin_manager.load_plugins()
+
+ assert "1.2.3" == poetry.package.version.text
+ assert "" == io.fetch_output()
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 7a4e9d5ac4a..f966a8407fb 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -6,8 +6,11 @@
import pytest
+from entrypoints import EntryPoint
+
from poetry.core.toml.file import TOMLFile
from poetry.factory import Factory
+from poetry.plugins.plugin import Plugin
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
@@ -15,6 +18,12 @@
fixtures_dir = Path(__file__).parent / "fixtures"
+class MyPlugin(Plugin):
+ def activate(self, poetry, io):
+ io.write_line("Updating version")
+ poetry.package.set_version("9.9.9")
+
+
def test_create_poetry():
poetry = Factory().create_poetry(fixtures_dir / "sample_project")
@@ -224,3 +233,14 @@ def test_create_poetry_with_local_config(fixture_dir):
assert not poetry.config.get("virtualenvs.create")
assert not poetry.config.get("virtualenvs.options.always-copy")
assert not poetry.config.get("virtualenvs.options.system-site-packages")
+
+
+def test_create_poetry_with_plugins(mocker):
+ mocker.patch(
+ "entrypoints.get_group_all",
+ return_value=[EntryPoint("my-plugin", "tests.test_factory", "MyPlugin")],
+ )
+
+ poetry = Factory().create_poetry(fixtures_dir / "sample_project")
+
+ assert "9.9.9" == poetry.package.version.text