From def05688c02a2cbb540e5fcaaf31b369ccc18043 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Sun, 8 Dec 2024 18:11:13 -0500 Subject: [PATCH] Make Dispatcher have all plugins by default This used to be the behavior, and then b5499c7dc5b300462f3ce1c2a3d9b7a76233b39b changed this, breaking some plugins (some of which subsequently implemented workarounds). This patch restores the behavior, so that if a Dispatcher is constructed without explicitly passing in plugins, it has access to all plugins. This change seems safe, in that it's unlikely that any plugins were relying on the behavior between b5499c7dc5b300462f3ce1c2a3d9b7a76233b39b and this patch, where a Dispatcher without an explicit plugin list behaved as if there were no plugins---because such a Dispatcher is not very useful! --- dotbot/cli.py | 3 ++- dotbot/dispatcher.py | 13 ++++++++++++- tests/dotbot_plugin_dispatcher_no_plugins.py | 18 ++++++++++++++++++ tests/test_cli.py | 14 ++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 tests/dotbot_plugin_dispatcher_no_plugins.py diff --git a/dotbot/cli.py b/dotbot/cli.py index 3a14d2d..84a1377 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -7,7 +7,7 @@ from argparse import ArgumentParser, RawTextHelpFormatter import dotbot from .config import ConfigReader, ReadingError -from .dispatcher import Dispatcher, DispatchError +from .dispatcher import Dispatcher, DispatchError, _all_plugins from .messenger import Level, Messenger from .plugins import Clean, Create, Link, Shell from .util import module @@ -151,6 +151,7 @@ def main(): # default to directory of config file base_directory = os.path.dirname(os.path.abspath(options.config_file)) os.chdir(base_directory) + _all_plugins[:] = plugins # for backwards compatibility, see dispatcher.py dispatcher = Dispatcher( base_directory, only=options.only, diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index 6660d88..5ed7ad5 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -5,6 +5,14 @@ from .context import Context from .messenger import Messenger from .plugin import Plugin +# Before b5499c7dc5b300462f3ce1c2a3d9b7a76233b39b, Dispatcher auto-loaded all +# plugins, but after that change, plugins are passed in explicitly (and loaded +# in cli.py). There are some plugins that rely on the old Dispatcher behavior, +# so this is a workaround for implementing similar functionality: when +# Dispatcher is constructed without an explicit list of plugins, _all_plugins is +# used instead. +_all_plugins = [] # filled in by cli.py + class Dispatcher: def __init__( @@ -16,9 +24,12 @@ class Dispatcher: options=Namespace(), plugins=None, ): + # if the caller wants no plugins, the caller needs to explicitly pass in + # plugins=[] self._log = Messenger() self._setup_context(base_directory, options, plugins) - plugins = plugins or [] + if plugins is None: + plugins = _all_plugins self._plugins = [plugin(self._context) for plugin in plugins] self._only = only self._skip = skip diff --git a/tests/dotbot_plugin_dispatcher_no_plugins.py b/tests/dotbot_plugin_dispatcher_no_plugins.py new file mode 100644 index 0000000..734169a --- /dev/null +++ b/tests/dotbot_plugin_dispatcher_no_plugins.py @@ -0,0 +1,18 @@ +# https://github.com/anishathalye/dotbot/issues/339, https://github.com/anishathalye/dotbot/pull/332 +# if plugins instantiate a Dispatcher without explicitly passing in plugins, +# the Dispatcher should have access to all plugins (matching context.plugins()) + +import dotbot +from dotbot.dispatcher import Dispatcher + + +class Dispatch(dotbot.Plugin): + def can_handle(self, directive): + return directive == "dispatch" + + def handle(self, directive, data): + dispatcher = Dispatcher( + base_directory=self._context.base_directory(), + options=self._context.options(), + ) + return dispatcher.dispatch(data) diff --git a/tests/test_cli.py b/tests/test_cli.py index a9fc7ae..136533b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -199,3 +199,17 @@ def test_plugin_context_plugin(capfd, home, dotfiles, run_dotbot): stdout = capfd.readouterr().out.splitlines() assert any(line.startswith("apple") for line in stdout) + + +def test_plugin_dispatcher_no_plugins(capfd, home, dotfiles, run_dotbot): + """Verify that plugins instantiating Dispatcher without plugins work.""" + + plugin_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "dotbot_plugin_dispatcher_no_plugins.py" + ) + shutil.copy(plugin_file, os.path.join(dotfiles.directory, "plugin.py")) + dotfiles.write_config([{"dispatch": [{"shell": [{"command": "echo apple", "stdout": True}]}]}]) + run_dotbot("--plugin", os.path.join(dotfiles.directory, "plugin.py")) + + stdout = capfd.readouterr().out.splitlines() + assert any(line.startswith("apple") for line in stdout)