diff --git a/docs/docs/plugins.md b/docs/docs/plugins.md new file mode 100644 index 00000000000..7abe4c3eaec --- /dev/null +++ b/docs/docs/plugins.md @@ -0,0 +1,147 @@ +# Plugins + +You may 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 `clikit.api.io.IO`. + +Using these two objects all configuration can be read +and all internal objects and state can be manipulated as desired. + +Example: + +```python +from poetry.plugins import Plugin + + +class MyPlugin(Plugin): + + def activate(self, poetry, io): # type: (Poetry, IO) -> None + version = self.get_custom_version() + io.write_line("Setting package version to {}".format(version)) + + poetry.package.version = version + + def get_custom_version(self): # type: () -> 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 poetry.plugins import ApplicationPlugin + + +class MyApplicationPlugin(ApplicationPlugin): + + def activate(self, application): + application.add(FooCommand()) +``` + +It also must be declared in the `pyproject.toml` file as a `application.plugin` plugin: + +```toml +[tool.poetry.plugins."poetry.application.plugin"] +foo-command = "poetry_demo_plugin.plugin:MyApplicationPlugin" +``` + + +### Event handler + +Plugins can also listens to specific events and act on them if necessary. + +There are two types of events: application events and generic events. + +All event types are represented by the `poetry.events.Events` enum class. +Here are the various events fired during Poetry's execution process: + +- `APPLICATION_BOOT`: occurs before the application is fully booted. + +And since Poetry's application is powered by [CliKit](https://github.com/sdispater/clikit), +the following events are also fired. Note that all events are accessible from +the `clikit.api.event.ConsoleEvents` enum. + +- `PRE_RESOLVE`: occurs before resolving the command. +- `PRE_HANDLE`: occurs before the command is executed. +- `CONFIG`: occurs before the application's configuration is finalized. + +Let's see how to implement an application event handler. For this example +we want to add an option to the application and, if it is set, trigger +a specific handler. + +!!!note + + This is how the `-h/--help` option of poetry works. + +```python +from clikit.api.event import ConsoleEvents +from clikit.api.resolver import ResolvedCommand +from poetry.plugins import ApplicationPlugin + + +class MyApplicationPlugin(ApplicationPlugin): + + def activate(self, application): + application.config.add_option("foo", description="Call the foo command") + application.add_command(FooCommmand()) + application.event_dispatcher.add_listener(ConsoleEvents.PRE_RESOLVE, self.resolve_foo_command) + + def resolve_foo_command(self, event, event_name, dispatcher): + # The event is a PreResolveEvent instance which gives + # access to the raw arguments and the application + args = event.raw_args + application = event.application + + if args.has_token("--foo"): + command = application.find("foo") + + # Enable lenient parsing + parsed_args = command.parse(args, True) + + event.set_resolved_command(ResolvedCommand(command, parsed_args)) + + # Since we have properly resolved the command + # there is no need to go further, so we stop + # the event propagation. + event.stop_propagation() +``` 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 182f389363d..384ce88af69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1341,7 +1341,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pathlib2", "contextlib2", "unittest2"] [metadata] -content-hash = "e0b632d8363fdf9f70d93901ff537714611bfc31705a897f6d2fb3bc010bca0a" +content-hash = "8309a041bb341ef06ad282d6849e8372498045087e0c3e6d21d81020aed50094" python-versions = "~2.7 || ^3.4" [metadata.files] diff --git a/poetry/console/application.py b/poetry/console/application.py index 1e6df4fa361..ee9c53634fd 100644 --- a/poetry/console/application.py +++ b/poetry/console/application.py @@ -1,6 +1,19 @@ +import sys + from cleo import Application as BaseApplication +from clikit.api.args.format import ArgsFormat +from clikit.api.command import CommandCollection +from clikit.api.io import IO +from clikit.api.io.flags import VERY_VERBOSE +from clikit.args import ArgvArgs +from clikit.io import ConsoleIO +from clikit.io import NullIO +from clikit.ui.components.exception_trace import ExceptionTrace from poetry import __version__ +from poetry.events.application_boot_event import ApplicationBootEvent +from poetry.events.events import Events +from poetry.plugins.plugin_manager import PluginManager from .commands.about import AboutCommand from .commands.add import AddCommand @@ -29,14 +42,21 @@ class Application(BaseApplication): def __init__(self): - super(Application, self).__init__( - "poetry", __version__, config=ApplicationConfig("poetry", __version__) - ) - + self._config = ApplicationConfig("poetry", __version__) + self._preliminary_io = ConsoleIO() + self._dispatcher = None + self._commands = CommandCollection() + self._named_commands = CommandCollection() + self._default_commands = CommandCollection() + self._global_args_format = ArgsFormat() + self._booted = False self._poetry = None + self._io = NullIO() - for command in self.get_default_commands(): - self.add(command) + # Enable trace output for exceptions thrown during boot + self._preliminary_io.set_verbosity(VERY_VERBOSE) + + self._disable_plugins = False @property def poetry(self): @@ -46,10 +66,82 @@ def poetry(self): 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 + ) + self._poetry.set_event_dispatcher(self._config.dispatcher) return self._poetry + def run(self, args=None, input_stream=None, output_stream=None, error_stream=None): + self._io = self._preliminary_io + + try: + if args is None: + args = ArgvArgs() + + self._disable_plugins = ( + args.has_token("--no-plugins") + or args.tokens + and args.tokens[0] == "new" + ) + + if not self._disable_plugins: + plugin_manager = PluginManager("application.plugin") + plugin_manager.load_plugins() + plugin_manager.activate(self) + + self.boot() + + io_factory = self._config.io_factory + + self._io = io_factory( + self, args, input_stream, output_stream, error_stream + ) # type: IO + + resolved_command = self.resolve_command(args) + command = resolved_command.command + parsed_args = resolved_command.args + + status_code = command.handle(parsed_args, self._io) + except Exception as e: + if not self._config.is_exception_caught(): + raise + + trace = ExceptionTrace(e) + trace.render(self._io) + + status_code = self.exception_to_exit_code(e) + + if self._config.is_terminated_after_run(): + sys.exit(status_code) + + return status_code + + def boot(self): # type: () -> None + if self._booted: + return + + dispatcher = self._config.dispatcher + + self._dispatcher = dispatcher + self._global_args_format = ArgsFormat( + list(self._config.arguments.values()) + list(self._config.options.values()) + ) + + for command_config in self._config.command_configs: + self.add_command(command_config) + + for command in self.get_default_commands(): + self.add(command) + + if dispatcher and dispatcher.has_listeners(Events.APPLICATION_BOOT): + dispatcher.dispatch( + Events.APPLICATION_BOOT, ApplicationBootEvent(self._config) + ) + + self._booted = True + def reset_poetry(self): # type: () -> None self._poetry = None @@ -89,6 +181,14 @@ def get_default_commands(self): # type: () -> list return commands + def get_plugin_commands(self): + plugin_manager = self.poetry.plugin_manager + providers = plugin_manager.command_providers + + for provider in providers: + for command in provider.commands: + yield command + if __name__ == "__main__": Application().run() diff --git a/poetry/console/config/application_config.py b/poetry/console/config/application_config.py index 36b72e9969e..cc27b54b71f 100644 --- a/poetry/console/config/application_config.py +++ b/poetry/console/config/application_config.py @@ -34,6 +34,8 @@ class ApplicationConfig(BaseApplicationConfig): def configure(self): super(ApplicationConfig, self).configure() + self.add_option("no-plugins", description="Disable plugins") + self.add_style(Style("c1").fg("cyan")) self.add_style(Style("info").fg("blue")) self.add_style(Style("comment").fg("green")) diff --git a/poetry/events/__init__.py b/poetry/events/__init__.py new file mode 100644 index 00000000000..c3f6af3f4f6 --- /dev/null +++ b/poetry/events/__init__.py @@ -0,0 +1,2 @@ +from .application_boot_event import ApplicationBootEvent +from .events import Events diff --git a/poetry/events/application_boot_event.py b/poetry/events/application_boot_event.py new file mode 100644 index 00000000000..0a802b4d46c --- /dev/null +++ b/poetry/events/application_boot_event.py @@ -0,0 +1,18 @@ +from clikit.api.event import Event + + +class ApplicationBootEvent(Event): + """ + Event triggered when the application before the application is booted. + + It receives an ApplicationConfig instance. + """ + + def __init__(self, config): + super(ApplicationBootEvent, self).__init__() + + self._config = config + + @property + def config(self): + return self._config diff --git a/poetry/events/events.py b/poetry/events/events.py new file mode 100644 index 00000000000..4e4481efaa2 --- /dev/null +++ b/poetry/events/events.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class Events(Enum): + + # The APPLICATION_BOOT event occurs before + # the console application is fully booted + APPLICATION_BOOT = "application-boot" diff --git a/poetry/factory.py b/poetry/factory.py index 90c9cec3295..4d67f527acd 100644 --- a/poetry/factory.py +++ b/poetry/factory.py @@ -17,6 +17,7 @@ from .packages.dependency import Dependency 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 from .spdx import license_by_id @@ -30,7 +31,7 @@ class Factory: """ def create_poetry( - self, cwd=None, io=None + self, cwd=None, io=None, disable_plugins=False ): # type: (Optional[Path], Optional[IO]) -> Poetry if io is None: io = NullIO() @@ -190,6 +191,11 @@ def create_poetry( if io.is_debug(): io.write_line("Deactivating the PyPI repository") + plugin_manager = PluginManager("plugin", disable_plugins) + plugin_manager.load_plugins() + poetry.set_plugin_manager(plugin_manager) + plugin_manager.activate(poetry, io) + return poetry @classmethod diff --git a/poetry/packages/project_package.py b/poetry/packages/project_package.py index 584da3a8ed4..bc8136e8213 100644 --- a/poetry/packages/project_package.py +++ b/poetry/packages/project_package.py @@ -1,3 +1,6 @@ +from typing import Union + +from poetry.semver import Version from poetry.semver import VersionRange from poetry.semver import parse_constraint from poetry.version.markers import parse_marker @@ -29,6 +32,14 @@ def to_dependency(self): return dependency + def set_version(self, version): # type: (Union[str, Version]) -> None + if not isinstance(version, Version): + self._version = Version.parse(version) + self._pretty_version = self._pretty_version or version + else: + self._version = version + self._pretty_version = self._pretty_version or self._version.text + @property def python_versions(self): return self._python_versions diff --git a/poetry/plugins/__init__.py b/poetry/plugins/__init__.py new file mode 100644 index 00000000000..5684298b31d --- /dev/null +++ b/poetry/plugins/__init__.py @@ -0,0 +1,3 @@ +from .application_plugin import ApplicationPlugin +from .plugin import Plugin +from .plugin_manager import PluginManager 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..9b4b0a73b34 --- /dev/null +++ b/poetry/plugins/plugin.py @@ -0,0 +1,15 @@ +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..a5d5dbd9360 --- /dev/null +++ b/poetry/plugins/plugin_manager.py @@ -0,0 +1,48 @@ +import entrypoints + +from .application_plugin import ApplicationPlugin +from .plugin import Plugin + + +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 = entrypoints.get_group_all("poetry.{}".format(self._type)) + + for entrypoint in plugin_entrypoints: + self._load_plugin_entrypoint(entrypoint) + + 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 + 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 157e2488cf2..d415977c341 100644 --- a/poetry/poetry.py +++ b/poetry/poetry.py @@ -1,10 +1,13 @@ from __future__ import absolute_import from __future__ import unicode_literals +from clikit.api.event import EventDispatcher + from .__version__ import __version__ from .config.config import Config from .packages import Locker from .packages import ProjectPackage +from .plugins import PluginManager from .repositories.pool import Pool from .utils._compat import Path from .utils.toml_file import TomlFile @@ -27,6 +30,8 @@ def __init__( self._local_config = local_config self._locker = locker self._config = config + self._event_dispatcher = EventDispatcher() + self._plugin_manager = None self._pool = Pool() @property @@ -53,6 +58,14 @@ def pool(self): # type: () -> Pool def config(self): # type: () -> Config return self._config + @property + def event_dispatcher(self): # type: () -> EventDispatcher + return self._event_dispatcher + + @property + def plugin_manager(self): # type: () -> PluginManager + return self._plugin_manager + def set_locker(self, locker): # type: (Locker) -> Poetry self._locker = locker @@ -67,3 +80,15 @@ def set_config(self, config): # type: (Config) -> Poetry self._config = config return self + + def set_event_dispatcher( + self, event_dispatcher + ): # type: (EventDispatcher) -> Poetry + self._event_dispatcher = event_dispatcher + + return self + + def set_plugin_manager(self, plugin_manager): # type: (PluginManager) -> Poetry + self._plugin_manager = plugin_manager + + return self diff --git a/pyproject.toml b/pyproject.toml index 283091dfbc1..c29c529c998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ html5lib = "^1.0" shellingham = "^1.1" tomlkit = "^0.5.8" pexpect = "^4.7.0" +entrypoints = "^0.3" # The typing module is not in the stdlib in Python 2.7 and 3.4 typing = { version = "^3.6", python = "~2.7 || ~3.4" } @@ -56,6 +57,8 @@ keyring = [ # Use subprocess32 for Python 2.7 and 3.4 subprocess32 = { version = "^3.5", python = "~2.7 || ~3.4" } importlib-metadata = {version = "~1.1.3", python = "<3.8"} +# enum34 is needed for Python 2.7 +enum34 = { version = "^1.1", python = "~2.7" } [tool.poetry.dev-dependencies] pytest = "^4.1" @@ -103,6 +106,7 @@ known_third_party = [ "cachy", "cleo", "clikit", + "entrypoint", "html5lib", "httpretty", "jsonschema", diff --git a/tests/console/commands/test_export.py b/tests/console/commands/test_export.py index 58049a1bba9..50257424eaa 100644 --- a/tests/console/commands/test_export.py +++ b/tests/console/commands/test_export.py @@ -68,7 +68,10 @@ def poetry(repo, tmp_dir): @pytest.fixture def app(poetry): - return Application(poetry) + application = Application(poetry) + application.boot() + + return application def test_export_exports_requirements_txt_file_locks_if_no_lock_file(app, repo): diff --git a/tests/console/conftest.py b/tests/console/conftest.py index 6750fbbcfdd..314fd7386ee 100644 --- a/tests/console/conftest.py +++ b/tests/console/conftest.py @@ -173,6 +173,7 @@ def poetry(repo, project_directory, config): def app(poetry): app_ = Application(poetry) app_.config.set_terminate_after_run(False) + app_.boot() return app_ diff --git a/tests/console/test_application.py b/tests/console/test_application.py new file mode 100644 index 00000000000..4d87ba2b555 --- /dev/null +++ b/tests/console/test_application.py @@ -0,0 +1,66 @@ +import re + +from cleo.testers 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): + """ + Foo Command + + foo + """ + + def handle(self): + self.line("foo called") + + return 0 + + +class AddCommandPlugin(ApplicationPlugin): + def activate(self, application): + application.add(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() + app.config.set_terminate_after_run(False) + + tester = ApplicationTester(app) + tester.execute("") + + assert re.search(r"foo\s+Foo Command", tester.io.fetch_output()) is not 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() + app.config.set_terminate_after_run(False) + + tester = ApplicationTester(app) + tester.execute("foo") + + assert "foo called\n" == tester.io.fetch_output() + assert 0 == tester.status_code diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/plugins/test_plugin_manager.py b/tests/plugins/test_plugin_manager.py new file mode 100644 index 00000000000..f0bc89589a5 --- /dev/null +++ b/tests/plugins/test_plugin_manager.py @@ -0,0 +1,111 @@ +import pytest + +from clikit.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 import PluginManager +from poetry.poetry import Poetry +from poetry.utils._compat import Path + + +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 bf3493ae5fa..087f72973d6 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -4,7 +4,9 @@ import pytest +from entrypoints import EntryPoint from poetry.factory import Factory +from poetry.plugins.plugin import Plugin from poetry.utils._compat import PY2 from poetry.utils._compat import Path from poetry.utils.toml_file import TomlFile @@ -13,6 +15,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") @@ -205,3 +213,14 @@ def test_create_poetry_with_local_config(fixture_dir): assert not poetry.config.get("virtualenvs.in-project") assert not poetry.config.get("virtualenvs.create") + + +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