Commit f5df3af9 by Sébastien Eustace Committed by Arun Babu Neelicattu

Add a plugin add command

parent d151f743
...@@ -538,3 +538,46 @@ To only remove a specific package from a cache, you have to specify the cache en ...@@ -538,3 +538,46 @@ To only remove a specific package from a cache, you have to specify the cache en
```bash ```bash
poetry cache clear pypi:requests:2.24.0 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 list`
The `plugin list` command lists all the currently installed plugins.
```bash
poetry plugin list
```
### `plugin remove`
The `plugin remove` command removes installed plugins.
```bash
poetry plugin remove poetry-plugin
```
...@@ -78,15 +78,15 @@ from poetry.plugins.application_plugin import ApplicationPlugin ...@@ -78,15 +78,15 @@ from poetry.plugins.application_plugin import ApplicationPlugin
class CustomCommand(Command): class CustomCommand(Command):
name = "my-command" name = "my-command"
def handle(self) -> int: def handle(self) -> int:
self.line("My command") self.line("My command")
return 0 return 0
def factory(): def factory():
return CustomCommand() return CustomCommand()
...@@ -127,8 +127,6 @@ foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin" ...@@ -127,8 +127,6 @@ foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin"
Plugins can also listen to specific events and act on them if necessary. Plugins can also listen to specific events and act on them if necessary.
There are two types of events: application events and generic events.
These events are fired by [Cleo](https://github.com/sdispater/cleo) These events are fired by [Cleo](https://github.com/sdispater/cleo)
and are accessible from the `cleo.events.console_events` module. and are accessible from the `cleo.events.console_events` module.
...@@ -159,10 +157,10 @@ class MyApplicationPlugin(ApplicationPlugin): ...@@ -159,10 +157,10 @@ class MyApplicationPlugin(ApplicationPlugin):
def load_dotenv( def load_dotenv(
self, event: ConsoleCommandEvent, event_name: str, dispatcher: EventDispatcher self, event: ConsoleCommandEvent, event_name: str, dispatcher: EventDispatcher
) -> None: ) -> None:
command = event.io command = event.command
if not isinstance(command, EnvCommand): if not isinstance(command, EnvCommand):
return return
io = event.io io = event.io
if io.is_debug(): if io.is_debug():
...@@ -170,3 +168,64 @@ class MyApplicationPlugin(ApplicationPlugin): ...@@ -170,3 +168,64 @@ class MyApplicationPlugin(ApplicationPlugin):
load_dotenv() 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 list
```
### 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
```
...@@ -70,6 +70,8 @@ COMMANDS = [ ...@@ -70,6 +70,8 @@ COMMANDS = [
"env list", "env list",
"env remove", "env remove",
"env use", "env use",
# Plugin commands
"plugin add",
# Self commands # Self commands
"self update", "self update",
] ]
...@@ -78,6 +80,7 @@ COMMANDS = [ ...@@ -78,6 +80,7 @@ COMMANDS = [
if TYPE_CHECKING: if TYPE_CHECKING:
from cleo.io.inputs.definition import Definition from cleo.io.inputs.definition import Definition
from poetry.console.commands.installer_command import InstallerCommand
from poetry.poetry import Poetry from poetry.poetry import Poetry
...@@ -92,8 +95,8 @@ class Application(BaseApplication): ...@@ -92,8 +95,8 @@ class Application(BaseApplication):
dispatcher = EventDispatcher() dispatcher = EventDispatcher()
dispatcher.add_listener(COMMAND, self.register_command_loggers) dispatcher.add_listener(COMMAND, self.register_command_loggers)
dispatcher.add_listener(COMMAND, self.set_env) dispatcher.add_listener(COMMAND, self.configure_env)
dispatcher.add_listener(COMMAND, self.set_installer) dispatcher.add_listener(COMMAND, self.configure_installer)
self.set_event_dispatcher(dispatcher) self.set_event_dispatcher(dispatcher)
command_loader = CommandLoader({name: load_command(name) for name in COMMANDS}) command_loader = CommandLoader({name: load_command(name) for name in COMMANDS})
...@@ -239,7 +242,9 @@ class Application(BaseApplication): ...@@ -239,7 +242,9 @@ class Application(BaseApplication):
logger.setLevel(level) 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 from .commands.env_command import EnvCommand
command: EnvCommand = cast(EnvCommand, event.command) command: EnvCommand = cast(EnvCommand, event.command)
...@@ -262,7 +267,7 @@ class Application(BaseApplication): ...@@ -262,7 +267,7 @@ class Application(BaseApplication):
command.set_env(env) command.set_env(env)
def set_installer( def configure_installer(
self, event: ConsoleCommandEvent, event_name: str, _: Any self, event: ConsoleCommandEvent, event_name: str, _: Any
) -> None: ) -> None:
from .commands.installer_command import InstallerCommand from .commands.installer_command import InstallerCommand
...@@ -276,11 +281,14 @@ class Application(BaseApplication): ...@@ -276,11 +281,14 @@ class Application(BaseApplication):
if command.installer is not None: if command.installer is not None:
return return
self._configure_installer(command, event.io)
def _configure_installer(self, command: "InstallerCommand", io: "IO") -> None:
from poetry.installation.installer import Installer from poetry.installation.installer import Installer
poetry = command.poetry poetry = command.poetry
installer = Installer( installer = Installer(
event.io, io,
command.env, command.env,
poetry.package, poetry.package,
poetry.locker, poetry.locker,
......
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Optional
from cleo.commands.command import Command as BaseCommand from cleo.commands.command import Command as BaseCommand
...@@ -11,9 +12,17 @@ if TYPE_CHECKING: ...@@ -11,9 +12,17 @@ if TYPE_CHECKING:
class Command(BaseCommand): class Command(BaseCommand):
loggers = [] loggers = []
_poetry: Optional["Poetry"] = None
@property @property
def poetry(self) -> "Poetry": 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": def get_application(self) -> "Application":
return self.application return self.application
......
...@@ -4,7 +4,7 @@ from .command import Command ...@@ -4,7 +4,7 @@ from .command import Command
if TYPE_CHECKING: if TYPE_CHECKING:
from poetry.utils.env import VirtualEnv from poetry.utils.env import Env
class EnvCommand(Command): class EnvCommand(Command):
...@@ -14,8 +14,8 @@ class EnvCommand(Command): ...@@ -14,8 +14,8 @@ class EnvCommand(Command):
super(EnvCommand, self).__init__() super(EnvCommand, self).__init__()
@property @property
def env(self) -> "VirtualEnv": def env(self) -> "Env":
return self._env return self._env
def set_env(self, env: "VirtualEnv") -> None: def set_env(self, env: "Env") -> None:
self._env = env self._env = env
...@@ -430,20 +430,32 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -430,20 +430,32 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
result.append(pair) result.append(pair)
continue continue
elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath( elif (os.path.sep in requirement or "/" in requirement) and (
requirement cwd.joinpath(requirement).exists()
).exists(): or Path(requirement).expanduser().exists()
path = cwd.joinpath(requirement) 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(): if path.is_file():
package = Provider.get_package_from_file(path.resolve()) package = Provider.get_package_from_file(path.resolve())
else: else:
package = Provider.get_package_from_directory(path) package = Provider.get_package_from_directory(path.resolve())
result.append( result.append(
dict( dict(
[ [
("name", package.name), ("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 []) + ([("extras", extras)] if extras else [])
) )
......
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 <c1>plugin add</c1> command installs Poetry plugins globally.
It works similarly to the <c1>add</c1> 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 (<b>requests</b>)
- A name and a constraint (<b>requests@^2.23.0</b>)
- A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
- A git url with a revision (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
- A git SSH url (<b>git+ssh://github.com/python-poetry/poetry.git</b>)
- A git SSH url with a revision (<b>git+ssh://github.com/python-poetry/poetry.git#develop</b>)
- A file path (<b>../my-package/my-package.whl</b>)
- A directory (<b>../my-package/</b>)
- A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\
"""
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 "
"<c2>pyproject.toml</c2> file and will be skipped:\n"
)
for name in existing_packages:
self.line(" • <c1>{name}</c1>".format(name=name))
self.line(
"\nIf you want to update it to the latest compatible version, "
"you can use `<c2>poetry plugin update package</c2>`.\n"
"If you prefer to upgrade it to the latest available version, "
"you can use `<c2>poetry plugin add package@latest</c2>`.\n"
)
...@@ -4,6 +4,7 @@ from __future__ import unicode_literals ...@@ -4,6 +4,7 @@ from __future__ import unicode_literals
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from cleo.io.io import IO from cleo.io.io import IO
...@@ -80,32 +81,9 @@ class Factory(BaseFactory): ...@@ -80,32 +81,9 @@ class Factory(BaseFactory):
) )
# Configuring sources # Configuring sources
sources = poetry.local_config.get("source", []) self.configure_sources(
for source in sources: poetry, poetry.local_config.get("source", []), config, io
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)
# 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 = PluginManager("plugin", disable_plugins=disable_plugins)
plugin_manager.load_plugins() plugin_manager.load_plugins()
...@@ -154,8 +132,39 @@ class Factory(BaseFactory): ...@@ -154,8 +132,39 @@ class Factory(BaseFactory):
return 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( def create_legacy_repository(
self, source: Dict[str, str], auth_config: Config cls, source: Dict[str, str], auth_config: Config
) -> "LegacyRepository": ) -> "LegacyRepository":
from .repositories.legacy_repository import LegacyRepository from .repositories.legacy_repository import LegacyRepository
from .utils.helpers import get_cert from .utils.helpers import get_cert
...@@ -178,3 +187,49 @@ class Factory(BaseFactory): ...@@ -178,3 +187,49 @@ class Factory(BaseFactory):
cert=get_cert(auth_config, name), cert=get_cert(auth_config, name),
client_cert=get_client_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"
)
...@@ -40,7 +40,7 @@ class Installer: ...@@ -40,7 +40,7 @@ class Installer:
locker: Locker, locker: Locker,
pool: Pool, pool: Pool,
config: Config, config: Config,
installed: Union[InstalledRepository, None] = None, installed: Union[Repository, None] = None,
executor: Optional[Executor] = None, executor: Optional[Executor] = None,
): ):
self._io = io self._io = io
......
...@@ -2,9 +2,11 @@ from pathlib import Path ...@@ -2,9 +2,11 @@ from pathlib import Path
from .utils.appdirs import user_cache_dir from .utils.appdirs import user_cache_dir
from .utils.appdirs import user_config_dir from .utils.appdirs import user_config_dir
from .utils.appdirs import user_data_dir
CACHE_DIR = user_cache_dir("pypoetry") CACHE_DIR = user_cache_dir("pypoetry")
DATA_DIR = user_data_dir("pypoetry")
CONFIG_DIR = user_config_dir("pypoetry") CONFIG_DIR = user_config_dir("pypoetry")
REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories" REPOSITORY_CACHE_DIR = Path(CACHE_DIR) / "cache" / "repositories"
...@@ -431,7 +431,6 @@ class Provider: ...@@ -431,7 +431,6 @@ class Provider:
] ]
def complete_package(self, package: DependencyPackage) -> DependencyPackage: def complete_package(self, package: DependencyPackage) -> DependencyPackage:
if package.is_root(): if package.is_root():
package = package.clone() package = package.clone()
requires = package.all_requires requires = package.all_requires
......
...@@ -100,10 +100,12 @@ class InstalledRepository(Repository): ...@@ -100,10 +100,12 @@ class InstalledRepository(Repository):
return True return True
@classmethod @classmethod
def load(cls, env: Env) -> "InstalledRepository": def load(cls, env: Env, with_dependencies: bool = False) -> "InstalledRepository":
""" """
Load installed packages. Load installed packages.
""" """
from poetry.core.packages.dependency import Dependency
repo = cls() repo = cls()
seen = set() seen = set()
...@@ -118,6 +120,11 @@ class InstalledRepository(Repository): ...@@ -118,6 +120,11 @@ class InstalledRepository(Repository):
package = Package(name, version, version) package = Package(name, version, version)
package.description = distribution.metadata.get("summary", "") 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: if package.name in seen:
continue continue
......
...@@ -803,7 +803,7 @@ class EnvManager(object): ...@@ -803,7 +803,7 @@ class EnvManager(object):
p_venv = os.path.normcase(str(venv)) p_venv = os.path.normcase(str(venv))
if any(p.startswith(p_venv) for p in paths): if any(p.startswith(p_venv) for p in paths):
# Running properly in the virtualenv, don't need to do anything # 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) return VirtualEnv(venv)
...@@ -874,7 +874,12 @@ class EnvManager(object): ...@@ -874,7 +874,12 @@ class EnvManager(object):
elif file_path.is_dir(): elif file_path.is_dir():
shutil.rmtree(str(file_path)) 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"): if hasattr(sys, "real_prefix"):
return Path(sys.real_prefix) return Path(sys.real_prefix)
......
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 <sebastien@eustace.io>"
]
[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 <sebastien@eustace.io>"
]
[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"
[tool.poetry]
name = "poetry-plugin"
version = "0.1.2"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
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]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment