Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,16 @@ in the `tool.poetry.requires-plugins` section of the pyproject.toml file:
```toml
[tool.poetry.requires-plugins]
my-application-plugin = ">1.0"
custom-plugin = {path = "custom_plugin", develop = true}
```

If the plugin is not installed in Poetry's own environment when running `poetry install`,
it will be installed only for the current project under `.poetry/plugins`
in the project's directory.

The syntax to specify `plugins` is the same as for [dependencies]({{< relref "managing-dependencies" >}}).
Plugins can be installed in editable mode using path dependencies with `develop = true`,
which is useful for plugin development.

{{% warning %}}
You can even overwrite a plugin in Poetry's own environment with another version.
Expand Down
4 changes: 4 additions & 0 deletions src/poetry/plugins/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from functools import cached_property
from importlib import metadata
from pathlib import Path
from site import addsitedir
from typing import TYPE_CHECKING

import tomlkit
Expand Down Expand Up @@ -62,7 +63,10 @@ def add_project_plugin_path(directory: Path) -> None:

plugin_path = pyproject_toml.parent / ProjectPluginCache.PATH
if plugin_path.exists():
# insert at the beginning to allow overriding dependencies
EnvManager.get_system_env(naive=True).sys_path.insert(0, str(plugin_path))
# process .pth files (among other things)
addsitedir(str(plugin_path))

@classmethod
def ensure_project_plugins(cls, poetry: Poetry, io: IO) -> None:
Expand Down
60 changes: 60 additions & 0 deletions tests/plugins/test_plugin_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import shutil
import sys

from pathlib import Path
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -116,6 +117,12 @@ def io() -> BufferedIO:
return BufferedIO()


@pytest.fixture(autouse=True)
def mock_sys_path(mocker: MockerFixture) -> None:
sys_path_copy = sys.path.copy()
mocker.patch("poetry.plugins.plugin_manager.sys.path", new=sys_path_copy)


@pytest.fixture()
def manager_factory(poetry: Poetry, io: BufferedIO) -> ManagerFactory:
def _manager(group: str = Plugin.group) -> PluginManager:
Expand Down Expand Up @@ -187,6 +194,59 @@ def test_add_project_plugin_path(
} == {"my-application-plugin 1.0"}


def test_add_project_plugin_path_addsitedir_called(
poetry_with_plugins: Poetry,
io: BufferedIO,
mocker: MockerFixture,
) -> None:
"""Test that addsitedir is called when plugin path exists."""
cache = ProjectPluginCache(poetry_with_plugins, io)
cache._path.mkdir(parents=True, exist_ok=True)

mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")

PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)

# sys.path is mocked, so we can check it was modified
assert str(cache._path) in sys.path
assert sys.path[0] == str(cache._path)
mock_addsitedir.assert_called_once_with(str(cache._path))


def test_add_project_plugin_path_no_addsitedir_when_path_missing(
poetry_with_plugins: Poetry,
mocker: MockerFixture,
) -> None:
"""Test that addsitedir is not called when plugin path doesn't exist."""
cache = ProjectPluginCache(poetry_with_plugins, BufferedIO())
# Ensure the plugin path does not exist
if cache._path.exists():
shutil.rmtree(cache._path)

mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
initial_sys_path = sys.path.copy()

PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)

assert sys.path == initial_sys_path
mock_addsitedir.assert_not_called()


def test_add_project_plugin_path_no_pyproject(
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""Test that no action is taken when pyproject.toml is missing."""
mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
initial_sys_path = sys.path.copy()

# Call with a directory that has no pyproject.toml
PluginManager.add_project_plugin_path(tmp_path)

assert sys.path == initial_sys_path
mock_addsitedir.assert_not_called()


def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None:
PluginManager.ensure_project_plugins(poetry, io)

Expand Down