diff --git a/poetry_monoranger_plugin/lock_modifier.py b/poetry_monoranger_plugin/lock_modifier.py
index 60467c9..289fb50 100644
--- a/poetry_monoranger_plugin/lock_modifier.py
+++ b/poetry_monoranger_plugin/lock_modifier.py
@@ -50,7 +50,9 @@ def execute(self, event: ConsoleCommandEvent):
io.write_line("Running command from monorepo root directory")
monorepo_root = (command.poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
- monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
+ monorepo_root_poetry = Factory().create_poetry(
+ cwd=monorepo_root, io=io, disable_cache=command.poetry.disable_cache
+ )
installer = Installer(
io,
diff --git a/poetry_monoranger_plugin/monorepo_adder.py b/poetry_monoranger_plugin/monorepo_adder.py
index 2625fdd..7204e10 100644
--- a/poetry_monoranger_plugin/monorepo_adder.py
+++ b/poetry_monoranger_plugin/monorepo_adder.py
@@ -126,7 +126,7 @@ def post_execute(self, event: ConsoleTerminateEvent):
return
monorepo_root = (poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
- monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
+ monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io, disable_cache=poetry.disable_cache)
installer = Installer(
io,
diff --git a/poetry_monoranger_plugin/path_rewriter.py b/poetry_monoranger_plugin/path_rewriter.py
index d424dfe..7d31e4a 100644
--- a/poetry_monoranger_plugin/path_rewriter.py
+++ b/poetry_monoranger_plugin/path_rewriter.py
@@ -55,6 +55,19 @@ def execute(self, event: ConsoleCommandEvent):
main_deps_group.remove_dependency(dependency.name)
main_deps_group.add_dependency(pinned)
+ def _get_dependency_pyproject(self, poetry: Poetry, dependency: DirectoryDependency) -> PyProjectTOML:
+ pyproject_file = poetry.pyproject_path.parent / dependency.path / "pyproject.toml"
+
+ if not pyproject_file.exists():
+ raise RuntimeError(f"Could not find pyproject.toml in {dependency.path}")
+
+ dep_pyproject: PyProjectTOML = PyProjectTOML(pyproject_file)
+
+ if not dep_pyproject.is_poetry_project():
+ raise RuntimeError(f"Directory {dependency.path} is not a valid poetry project")
+
+ return dep_pyproject
+
def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
"""Pins a directory dependency to a specific version based on the plugin configuration.
@@ -69,15 +82,7 @@ def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
RuntimeError: If the pyproject.toml file is not found or is not a valid Poetry project.
ValueError: If the version rewrite rule is invalid.
"""
- pyproject_file = poetry.pyproject_path.parent / dependency.path / "pyproject.toml"
-
- if not pyproject_file.exists():
- raise RuntimeError(f"Could not find pyproject.toml in {dependency.path}")
-
- dep_pyproject: PyProjectTOML = PyProjectTOML(pyproject_file)
-
- if not dep_pyproject.is_poetry_project():
- raise RuntimeError(f"Directory {dependency.path} is not a valid poetry project")
+ dep_pyproject: PyProjectTOML = self._get_dependency_pyproject(poetry, dependency)
name = cast(str, dep_pyproject.poetry_config["name"])
version = cast(str, dep_pyproject.poetry_config["version"])
diff --git a/poetry_monoranger_plugin/venv_modifier.py b/poetry_monoranger_plugin/venv_modifier.py
index bde641f..0fe2f42 100644
--- a/poetry_monoranger_plugin/venv_modifier.py
+++ b/poetry_monoranger_plugin/venv_modifier.py
@@ -66,7 +66,7 @@ def execute(self, event: ConsoleCommandEvent):
poetry = command.poetry
monorepo_root = (poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
- monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
+ monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io, disable_cache=poetry.disable_cache)
io.write_line(f"Using monorepo root venv {monorepo_root.name}>\n")
env_manager = EnvManager(monorepo_root_poetry, io=io)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..fc07718
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+from typing import Type
+from unittest.mock import Mock
+
+import pytest
+from poetry.core.packages.dependency import Dependency
+from poetry.core.packages.dependency_group import DependencyGroup
+from poetry.core.packages.directory_dependency import DirectoryDependency
+from poetry.poetry import Poetry
+
+
+@pytest.fixture
+def mock_event_gen():
+ from poetry.console.commands.command import Command
+
+ def _factory(command_cls: Type[Command], disable_cache: bool):
+ from cleo.events.console_command_event import ConsoleCommandEvent
+
+ main_grp = DependencyGroup("main")
+ main_grp.add_dependency(DirectoryDependency("packageB", Path("../packageB"), develop=True))
+ main_grp.add_dependency(Dependency("numpy", "==1.5.0"))
+
+ mock_command = Mock(spec=command_cls)
+ mock_command.poetry = Mock(spec=Poetry)
+ mock_command.poetry.pyproject_path = Path("/monorepo_root/packageA/pyproject.toml")
+ mock_command.poetry.package = Mock()
+ mock_command.poetry.package.name = "packageA"
+ mock_command.poetry.package.dependency_group = Mock()
+ mock_command.poetry.package.dependency_group.return_value = main_grp
+ mock_command.poetry.locker = Mock()
+ mock_command.poetry.pool = Mock()
+ mock_command.poetry.config = Mock()
+ mock_command.poetry.disable_cache = disable_cache
+ mock_command.option = Mock(return_value=False)
+
+ mock_io = Mock()
+
+ mock_event = Mock(spec=ConsoleCommandEvent)
+ mock_event.command = mock_command
+ mock_event.io = mock_io
+
+ return mock_event
+
+ return _factory
+
+
+@pytest.fixture
+def mock_terminate_event_gen(mock_event_gen):
+ from poetry.console.commands.command import Command
+
+ def _factory(command_cls: Type[Command], disable_cache: bool):
+ from cleo.events.console_terminate_event import ConsoleTerminateEvent
+
+ mock_event = mock_event_gen(command_cls, disable_cache)
+ mock_io = mock_event.io
+ mock_command = mock_event.command
+ del mock_event
+
+ mock_terminate_event = Mock(spec=ConsoleTerminateEvent)
+ mock_terminate_event.command = mock_command
+ mock_terminate_event.io = mock_io
+
+ return mock_terminate_event
+
+ return _factory
diff --git a/tests/test_lock_modifier.py b/tests/test_lock_modifier.py
new file mode 100644
index 0000000..c879612
--- /dev/null
+++ b/tests/test_lock_modifier.py
@@ -0,0 +1,47 @@
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from poetry.console.commands.lock import LockCommand
+from poetry.installation.installer import Installer
+from poetry.poetry import Poetry
+
+from poetry_monoranger_plugin.config import MonorangerConfig
+from poetry_monoranger_plugin.lock_modifier import LockModifier
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_modifications_for_lock_command(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(LockCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ lock_modifier = LockModifier(config)
+
+ with (
+ patch("poetry_monoranger_plugin.lock_modifier.Factory.create_poetry", autospec=True) as mock_create_poetry,
+ patch("poetry_monoranger_plugin.lock_modifier.Installer", autospec=True) as mock_installer_cls,
+ ):
+ mock_create_poetry.return_value = Mock(spec=Poetry)
+ mock_installer_cls.return_value = Mock(spec=Installer)
+
+ lock_modifier.execute(mock_event)
+
+ # A new poetry project object at the monorepo root should be created
+ mock_create_poetry.assert_called_once()
+ assert mock_create_poetry.call_args[1]["cwd"] == Path("/monorepo_root").resolve()
+ assert mock_create_poetry.call_args[1]["io"] == mock_event.io
+ assert mock_create_poetry.call_args[1]["disable_cache"] == disable_cache
+
+ # A new installer should be created with the monorepo root poetry project
+ mock_installer_cls.assert_called_once()
+ # Env is remained unchanged as it is the responsibility of venv_modifier.py
+ assert mock_installer_cls.call_args[0][1] == mock_command.env
+ assert mock_installer_cls.call_args[0][2] == mock_create_poetry.return_value.package
+ assert mock_installer_cls.call_args[0][3] == mock_create_poetry.return_value.locker
+ assert mock_installer_cls.call_args[0][4] == mock_create_poetry.return_value.pool
+ assert mock_installer_cls.call_args[0][5] == mock_create_poetry.return_value.config
+ assert mock_installer_cls.call_args[1]["disable_cache"] == mock_create_poetry.return_value.disable_cache
+
+ # The new poetry and installer objects should be set on the command
+ assert mock_command.set_poetry.call_args[0][0] == mock_create_poetry.return_value
+ assert mock_command.set_installer.call_args[0][0] == mock_installer_cls.return_value
diff --git a/tests/test_monorepo_adder.py b/tests/test_monorepo_adder.py
new file mode 100644
index 0000000..991e062
--- /dev/null
+++ b/tests/test_monorepo_adder.py
@@ -0,0 +1,73 @@
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from poetry.console.commands.add import AddCommand
+from poetry.installation.installer import Installer
+from poetry.poetry import Poetry
+
+from poetry_monoranger_plugin.config import MonorangerConfig
+from poetry_monoranger_plugin.monorepo_adder import DummyInstaller, MonorepoAdderRemover
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_modifications_for_addremove_command(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(AddCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ adder_remover = MonorepoAdderRemover(config)
+
+ with patch("poetry_monoranger_plugin.monorepo_adder.Poetry.__new__", autospec=True) as mock_poetry:
+ mock_poetry.return_value = Mock(spec=Poetry)
+
+ adder_remover.execute(mock_event)
+
+ mock_poetry.assert_called_once()
+ assert mock_command.set_poetry.call_args[0][0] == mock_poetry.return_value
+ assert isinstance(mock_command.set_installer.call_args[0][0], DummyInstaller)
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_modifications_post_addremove_command(mock_terminate_event_gen, disable_cache: bool):
+ # Here we test the .post_execute command
+ mock_event = mock_terminate_event_gen(AddCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ adder_remover = MonorepoAdderRemover(config)
+
+ with (
+ patch("poetry_monoranger_plugin.monorepo_adder.Factory.create_poetry", autospec=True) as mock_create_poetry,
+ patch("poetry_monoranger_plugin.monorepo_adder.Installer", autospec=True) as mock_installer_cls,
+ ):
+ mock_create_poetry.return_value = Mock(spec=Poetry)
+ mock_installer_cls.return_value = Mock(spec=Installer)
+
+ adder_remover.post_execute(mock_event)
+
+ # A new poetry project object at the monorepo root should be created
+ mock_create_poetry.assert_called_once()
+ assert mock_create_poetry.call_args[1]["cwd"] == Path("/monorepo_root").resolve()
+ assert mock_create_poetry.call_args[1]["io"] == mock_event.io
+ assert mock_create_poetry.call_args[1]["disable_cache"] == disable_cache
+
+ # A new installer should be created with the monorepo root poetry project
+ mock_installer_cls.assert_called_once()
+ # Env is remained unchanged as it is the responsibility of venv_modifier.py
+ assert mock_installer_cls.call_args[0][1] == mock_command.env
+ assert mock_installer_cls.call_args[0][2] == mock_create_poetry.return_value.package
+ assert mock_installer_cls.call_args[0][3] == mock_create_poetry.return_value.locker
+ assert mock_installer_cls.call_args[0][4] == mock_create_poetry.return_value.pool
+ assert mock_installer_cls.call_args[0][5] == mock_create_poetry.return_value.config
+ assert mock_installer_cls.call_args[1]["disable_cache"] == mock_create_poetry.return_value.disable_cache
+
+ # Check settings of installer
+ assert mock_installer_cls.return_value.dry_run.call_args[0][0] == mock_command.option("dry-run")
+ assert mock_installer_cls.return_value.verbose.call_args[0][0] == mock_event.io.is_verbose()
+ assert mock_installer_cls.return_value.update.call_args[0][0] is True
+ assert mock_installer_cls.return_value.execute_operations.call_args[0][0] is not mock_command.option("lock")
+
+ # The whitelist should contain the package name
+ assert mock_installer_cls.return_value.whitelist.call_args[0][0] == [mock_command.poetry.package.name]
+
+ # The installer should be run
+ assert mock_installer_cls.return_value.run.call_count == 1
diff --git a/tests/test_path_rewriter.py b/tests/test_path_rewriter.py
new file mode 100644
index 0000000..61e31f9
--- /dev/null
+++ b/tests/test_path_rewriter.py
@@ -0,0 +1,41 @@
+import copy
+from unittest.mock import Mock, patch
+
+import pytest
+from poetry.console.commands.build import BuildCommand
+from poetry.core.packages.directory_dependency import DirectoryDependency
+from poetry.core.pyproject.toml import PyProjectTOML
+
+from poetry_monoranger_plugin.config import MonorangerConfig
+from poetry_monoranger_plugin.path_rewriter import PathRewriter
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_path_rewriting_for_build_command(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(BuildCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../", version_rewrite_rule="==")
+ path_rewriter = PathRewriter(config)
+
+ original_dependencies = copy.deepcopy(mock_command.poetry.package.dependency_group.return_value.dependencies)
+
+ with patch(
+ "poetry_monoranger_plugin.path_rewriter.PathRewriter._get_dependency_pyproject", autospec=True
+ ) as mock_get_dep:
+ mock_get_dep.return_value = Mock(spec=PyProjectTOML)
+ mock_get_dep.return_value.poetry_config = {"version": "0.1.0", "name": "packageB"}
+
+ path_rewriter.execute(mock_event)
+
+ new_dependencies = mock_command.poetry.package.dependency_group.return_value
+
+ assert len(new_dependencies.dependencies) == len(original_dependencies)
+ # sort the dependencies by name to ensure they are in the same order
+ original_dependencies = sorted(original_dependencies, key=lambda x: x.name)
+ new_dependencies = sorted(new_dependencies.dependencies, key=lambda x: x.name)
+ for i, dep in enumerate(new_dependencies):
+ assert dep.name == original_dependencies[i].name
+ if isinstance(original_dependencies[i], DirectoryDependency):
+ assert dep.pretty_constraint == "0.1.0"
+ else:
+ assert dep.pretty_constraint == original_dependencies[i].pretty_constraint
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 0000000..703e2b7
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,69 @@
+from unittest.mock import Mock, patch
+
+import pytest
+from poetry.console.commands.add import AddCommand
+from poetry.console.commands.build import BuildCommand
+from poetry.console.commands.command import Command
+from poetry.console.commands.env_command import EnvCommand
+from poetry.console.commands.install import InstallCommand
+from poetry.console.commands.lock import LockCommand
+from poetry.console.commands.remove import RemoveCommand
+from poetry.console.commands.update import UpdateCommand
+
+from poetry_monoranger_plugin.config import MonorangerConfig
+from poetry_monoranger_plugin.plugin import Monoranger
+
+
+def test_activates_plugin_with_valid_config():
+ application = Mock()
+ application.poetry.pyproject.data = {
+ "tool": {"poetry-monoranger-plugin": {"enabled": True, "monorepo_root": "../"}}
+ }
+ application.event_dispatcher = Mock()
+ plugin = Monoranger()
+ plugin.activate(application)
+
+ assert plugin.plugin_conf.enabled is True
+ assert plugin.plugin_conf.monorepo_root == "../"
+ application.event_dispatcher.add_listener.assert_called()
+
+
+def test_does_not_activate_plugin_with_disabled_config():
+ application = Mock()
+ application.poetry.pyproject.data = {}
+ application.event_dispatcher = Mock()
+ plugin = Monoranger()
+ plugin.activate(application)
+
+ assert plugin.plugin_conf is None
+ application.event_dispatcher.add_listener.assert_not_called()
+
+
+@pytest.mark.parametrize(
+ "cmd_type",
+ [
+ AddCommand,
+ RemoveCommand,
+ BuildCommand,
+ EnvCommand,
+ LockCommand,
+ InstallCommand,
+ UpdateCommand,
+ ],
+)
+def test_handles_all_command_events(mock_event_gen, cmd_type: type[Command]):
+ cmd_to_patch: dict[type[Command], str] = {
+ AddCommand: "poetry_monoranger_plugin.monorepo_adder.MonorepoAdderRemover.execute",
+ RemoveCommand: "poetry_monoranger_plugin.monorepo_adder.MonorepoAdderRemover.execute",
+ BuildCommand: "poetry_monoranger_plugin.path_rewriter.PathRewriter.execute",
+ EnvCommand: "poetry_monoranger_plugin.venv_modifier.VenvModifier.execute",
+ LockCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
+ InstallCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
+ UpdateCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
+ }
+ event = mock_event_gen(cmd_type, disable_cache=False)
+ plugin = Monoranger()
+ plugin.plugin_conf = MonorangerConfig(enabled=True, monorepo_root="../")
+ with patch(cmd_to_patch[cmd_type]) as mock_execute:
+ plugin.console_command_event_listener(event, "", Mock())
+ mock_execute.assert_called_once_with(event)
diff --git a/tests/test_venv_modifier.py b/tests/test_venv_modifier.py
new file mode 100644
index 0000000..deaf003
--- /dev/null
+++ b/tests/test_venv_modifier.py
@@ -0,0 +1,102 @@
+import os
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from poetry.console.commands.env_command import EnvCommand
+from poetry.console.commands.installer_command import InstallerCommand
+from poetry.installation.installer import Installer
+from poetry.poetry import Poetry
+
+from poetry_monoranger_plugin.config import MonorangerConfig
+from poetry_monoranger_plugin.venv_modifier import VenvModifier
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_modifications_for_env_command(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(EnvCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ venv_modifier = VenvModifier(config)
+
+ environ = os.environ.copy()
+ environ.pop("VIRTUAL_ENV", None)
+ with (
+ patch("poetry_monoranger_plugin.venv_modifier.Factory.create_poetry", autospec=True) as mock_create_poetry,
+ patch("poetry_monoranger_plugin.venv_modifier.EnvManager.create_venv", autospec=True) as mock_create_venv,
+ patch.dict("os.environ", environ, clear=True),
+ ):
+ mock_create_poetry.return_value = Mock(spec=Poetry)
+ mock_create_venv.return_value = Mock()
+
+ venv_modifier.execute(mock_event)
+
+ # create_poetry is called with the correct args
+ mock_create_poetry.assert_called_once()
+ assert mock_create_poetry.call_args[1]["cwd"] == Path("/monorepo_root")
+ assert mock_create_poetry.call_args[1]["io"] == mock_event.io
+ assert mock_create_poetry.call_args[1]["disable_cache"] == disable_cache
+
+ # create_venv is called and EnvManager was created with the correct args
+ mock_create_venv.assert_called_once()
+ # Check if 'EnvManager()._poetry' contains the Mock output of mock_create_poetry.
+ # Use the 'self' argument to mock_create_venv to access the 'EnvManager' instance.
+ assert mock_create_venv.call_args[0][0]._poetry == mock_create_poetry.return_value
+
+ # The new venv object is attached to the original command
+ mock_command.set_env.assert_called_once()
+ assert mock_command.set_env.call_args[0][0] == mock_create_venv.return_value
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_executes_modifications_for_installer_command(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(InstallerCommand, disable_cache=disable_cache)
+ mock_command = mock_event.command
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ venv_modifier = VenvModifier(config)
+
+ environ = os.environ.copy()
+ environ.pop("VIRTUAL_ENV", None)
+ with (
+ patch("poetry_monoranger_plugin.venv_modifier.Factory.create_poetry", autospec=True) as mock_create_poetry,
+ patch("poetry_monoranger_plugin.venv_modifier.EnvManager.create_venv", autospec=True) as mock_create_venv,
+ patch("poetry_monoranger_plugin.venv_modifier.Installer", autospec=True) as mock_installer_cls,
+ patch.dict("os.environ", environ, clear=True),
+ ):
+ mock_create_poetry.return_value = Mock(spec=Poetry)
+ mock_create_venv.return_value = Mock()
+ mock_installer_cls.return_value = Mock(spec=Installer)
+
+ venv_modifier.execute(mock_event)
+
+ # Installer is created with all args from the original command except the env
+ mock_installer_cls.assert_called_once()
+ assert mock_installer_cls.call_args[0][1] == mock_create_venv.return_value
+ assert mock_installer_cls.call_args[0][2] == mock_command.poetry.package
+ assert mock_installer_cls.call_args[0][3] == mock_command.poetry.locker
+ assert mock_installer_cls.call_args[0][4] == mock_command.poetry.pool
+ assert mock_installer_cls.call_args[0][5] == mock_command.poetry.config
+ assert mock_installer_cls.call_args[1]["disable_cache"] == mock_command.poetry.disable_cache
+
+ mock_command.set_installer.assert_called_once()
+ assert mock_command.set_installer.call_args[0][0] == mock_installer_cls.return_value
+
+
+@pytest.mark.parametrize("disable_cache", [True, False])
+def test_does_not_activate_venv_if_already_in_venv(mock_event_gen, disable_cache: bool):
+ mock_event = mock_event_gen(EnvCommand, disable_cache=disable_cache)
+ config = MonorangerConfig(enabled=True, monorepo_root="../")
+ venv_modifier = VenvModifier(config)
+
+ environ = os.environ.copy()
+ environ["VIRTUAL_ENV"] = "/some/venv"
+ with (
+ patch("poetry_monoranger_plugin.venv_modifier.Factory.create_poetry", autospec=True) as mock_create_poetry,
+ patch("poetry_monoranger_plugin.venv_modifier.EnvManager.create_venv", autospec=True) as mock_create_venv,
+ patch.dict("os.environ", environ, clear=True),
+ ):
+ venv_modifier.execute(mock_event)
+
+ mock_create_poetry.assert_not_called()
+ mock_create_venv.assert_not_called()
+ mock_event.command.set_env.assert_not_called()