From b5499c7dc5b300462f3ce1c2a3d9b7a76233b39b Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Sat, 30 Apr 2022 20:19:22 -0500 Subject: [PATCH] Separate module importing from plugin identification This change allows the test framework to reliably specify which plugins to load and use within the same process. Previously, plugins were loaded by importing files and then accessing the Plugin class' list of subclasses. Now, it's possible to run dotbot multiple times without plugins accruing across runs with different configurations and CLI arguments. In addition, this fixes some circular imports that were previously avoided because plugins were imported in a function. --- dotbot/cli.py | 9 ++++++--- dotbot/dispatcher.py | 8 +++----- dotbot/plugins/clean.py | 4 ++-- dotbot/plugins/create.py | 5 +++-- dotbot/plugins/link.py | 9 +++++---- dotbot/plugins/shell.py | 10 ++++------ dotbot/util/module.py | 18 +++++++++++++++--- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/dotbot/cli.py b/dotbot/cli.py index 28485d1..b230b6d 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -1,4 +1,4 @@ -import os, glob +import glob import sys from argparse import ArgumentParser, RawTextHelpFormatter @@ -6,6 +6,7 @@ from .config import ConfigReader, ReadingError from .dispatcher import Dispatcher, DispatchError from .messenger import Messenger from .messenger import Level +from .plugins import Clean, Create, Link, Shell from .util import module import dotbot @@ -118,9 +119,10 @@ def main(): else: log.use_color(sys.stdout.isatty()) + plugins = [] plugin_directories = list(options.plugin_dirs) if not options.disable_built_in_plugins: - from .plugins import Clean, Create, Link, Shell + plugins.extend([Clean, Create, Link, Shell]) plugin_paths = [] for directory in plugin_directories: for plugin_path in glob.glob(os.path.join(directory, "*.py")): @@ -129,7 +131,7 @@ def main(): plugin_paths.append(plugin_path) for plugin_path in plugin_paths: abspath = os.path.abspath(plugin_path) - module.load(abspath) + plugins.extend(module.load(abspath)) if not options.config_file: log.error("No configuration file specified") exit(1) @@ -151,6 +153,7 @@ def main(): skip=options.skip, exit_on_failure=options.exit_on_failure, options=options, + plugins=plugins, ) success = dispatcher.dispatch(tasks) if success: diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index 18f0b0a..630c895 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -7,11 +7,12 @@ from .context import Context class Dispatcher(object): def __init__( - self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace() + self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace(), plugins=None, ): self._log = Messenger() self._setup_context(base_directory, options) - self._load_plugins() + plugins = plugins or [] + self._plugins = [plugin(self._context) for plugin in plugins] self._only = only self._skip = skip self._exit = exit_on_failure @@ -65,9 +66,6 @@ class Dispatcher(object): return False return success - def _load_plugins(self): - self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()] - class DispatchError(Exception): pass diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py index e2671ad..1d17b72 100644 --- a/dotbot/plugins/clean.py +++ b/dotbot/plugins/clean.py @@ -1,10 +1,10 @@ import os import sys -import dotbot +from ..plugin import Plugin -class Clean(dotbot.Plugin): +class Clean(Plugin): """ Cleans broken symbolic links. """ diff --git a/dotbot/plugins/create.py b/dotbot/plugins/create.py index 85557a6..c593d52 100644 --- a/dotbot/plugins/create.py +++ b/dotbot/plugins/create.py @@ -1,8 +1,9 @@ import os -import dotbot + +from ..plugin import Plugin -class Create(dotbot.Plugin): +class Create(Plugin): """ Create empty paths. """ diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index c938080..3e8c91e 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -2,11 +2,12 @@ import os import sys import glob import shutil -import dotbot -import dotbot.util + +from ..plugin import Plugin +from ..util import shell_command -class Link(dotbot.Plugin): +class Link(Plugin): """ Symbolically links dotfiles. """ @@ -139,7 +140,7 @@ class Link(dotbot.Plugin): return success def _test_success(self, command): - ret = dotbot.util.shell_command(command, cwd=self._context.base_directory()) + ret = shell_command(command, cwd=self._context.base_directory()) if ret != 0: self._log.debug("Test '%s' returned false" % command) return ret == 0 diff --git a/dotbot/plugins/shell.py b/dotbot/plugins/shell.py index bbdcb6d..2ee0d68 100644 --- a/dotbot/plugins/shell.py +++ b/dotbot/plugins/shell.py @@ -1,10 +1,8 @@ -import os -import subprocess -import dotbot -import dotbot.util +from ..plugin import Plugin +from ..util import shell_command -class Shell(dotbot.Plugin): +class Shell(Plugin): """ Run arbitrary shell commands. """ @@ -50,7 +48,7 @@ class Shell(dotbot.Plugin): self._log.lowinfo("%s [%s]" % (msg, cmd)) stdout = options.get("stdout", stdout) stderr = options.get("stderr", stderr) - ret = dotbot.util.shell_command( + ret = shell_command( cmd, cwd=self._context.base_directory(), enable_stdin=stdin, diff --git a/dotbot/util/module.py b/dotbot/util/module.py index ded485a..183cac2 100644 --- a/dotbot/util/module.py +++ b/dotbot/util/module.py @@ -1,4 +1,7 @@ -import sys, os.path +import os +import sys + +from dotbot.plugin import Plugin # We keep references to loaded modules so they don't get garbage collected. loaded_modules = [] @@ -7,8 +10,17 @@ loaded_modules = [] def load(path): basename = os.path.basename(path) module_name, extension = os.path.splitext(basename) - plugin = load_module(module_name, path) - loaded_modules.append(plugin) + loaded_module = load_module(module_name, path) + plugins = [] + for name in dir(loaded_module): + possible_plugin = getattr(loaded_module, name) + try: + if issubclass(possible_plugin, Plugin) and possible_plugin is not Plugin: + plugins.append(possible_plugin) + except TypeError: + pass + loaded_modules.append(loaded_module) + return plugins if sys.version_info >= (3, 5):