Skip to content

Commit f0b5263

Browse files
authored
Add support for processing .pth files in project plugins (#10661)
1 parent 9158b9b commit f0b5263

File tree

3 files changed

+67
-0
lines changed

3 files changed

+67
-0
lines changed

docs/plugins.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,16 @@ in the `tool.poetry.requires-plugins` section of the pyproject.toml file:
263263
```toml
264264
[tool.poetry.requires-plugins]
265265
my-application-plugin = ">1.0"
266+
custom-plugin = {path = "custom_plugin", develop = true}
266267
```
267268

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

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

274277
{{% warning %}}
275278
You can even overwrite a plugin in Poetry's own environment with another version.

src/poetry/plugins/plugin_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from functools import cached_property
1010
from importlib import metadata
1111
from pathlib import Path
12+
from site import addsitedir
1213
from typing import TYPE_CHECKING
1314

1415
import tomlkit
@@ -62,7 +63,10 @@ def add_project_plugin_path(directory: Path) -> None:
6263

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

6771
@classmethod
6872
def ensure_project_plugins(cls, poetry: Poetry, io: IO) -> None:

tests/plugins/test_plugin_manager.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import shutil
4+
import sys
45

56
from pathlib import Path
67
from typing import TYPE_CHECKING
@@ -116,6 +117,12 @@ def io() -> BufferedIO:
116117
return BufferedIO()
117118

118119

120+
@pytest.fixture(autouse=True)
121+
def mock_sys_path(mocker: MockerFixture) -> None:
122+
sys_path_copy = sys.path.copy()
123+
mocker.patch("poetry.plugins.plugin_manager.sys.path", new=sys_path_copy)
124+
125+
119126
@pytest.fixture()
120127
def manager_factory(poetry: Poetry, io: BufferedIO) -> ManagerFactory:
121128
def _manager(group: str = Plugin.group) -> PluginManager:
@@ -187,6 +194,59 @@ def test_add_project_plugin_path(
187194
} == {"my-application-plugin 1.0"}
188195

189196

197+
def test_add_project_plugin_path_addsitedir_called(
198+
poetry_with_plugins: Poetry,
199+
io: BufferedIO,
200+
mocker: MockerFixture,
201+
) -> None:
202+
"""Test that addsitedir is called when plugin path exists."""
203+
cache = ProjectPluginCache(poetry_with_plugins, io)
204+
cache._path.mkdir(parents=True, exist_ok=True)
205+
206+
mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
207+
208+
PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)
209+
210+
# sys.path is mocked, so we can check it was modified
211+
assert str(cache._path) in sys.path
212+
assert sys.path[0] == str(cache._path)
213+
mock_addsitedir.assert_called_once_with(str(cache._path))
214+
215+
216+
def test_add_project_plugin_path_no_addsitedir_when_path_missing(
217+
poetry_with_plugins: Poetry,
218+
mocker: MockerFixture,
219+
) -> None:
220+
"""Test that addsitedir is not called when plugin path doesn't exist."""
221+
cache = ProjectPluginCache(poetry_with_plugins, BufferedIO())
222+
# Ensure the plugin path does not exist
223+
if cache._path.exists():
224+
shutil.rmtree(cache._path)
225+
226+
mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
227+
initial_sys_path = sys.path.copy()
228+
229+
PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)
230+
231+
assert sys.path == initial_sys_path
232+
mock_addsitedir.assert_not_called()
233+
234+
235+
def test_add_project_plugin_path_no_pyproject(
236+
tmp_path: Path,
237+
mocker: MockerFixture,
238+
) -> None:
239+
"""Test that no action is taken when pyproject.toml is missing."""
240+
mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
241+
initial_sys_path = sys.path.copy()
242+
243+
# Call with a directory that has no pyproject.toml
244+
PluginManager.add_project_plugin_path(tmp_path)
245+
246+
assert sys.path == initial_sys_path
247+
mock_addsitedir.assert_not_called()
248+
249+
190250
def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None:
191251
PluginManager.ensure_project_plugins(poetry, io)
192252

0 commit comments

Comments
 (0)