mirror of
1
0
Fork 0

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.
This commit is contained in:
Kurt McKee 2022-04-30 20:19:22 -05:00
parent a8dd89f48f
commit b5499c7dc5
7 changed files with 38 additions and 25 deletions

View File

@ -1,4 +1,4 @@
import os, glob import glob
import sys import sys
from argparse import ArgumentParser, RawTextHelpFormatter from argparse import ArgumentParser, RawTextHelpFormatter
@ -6,6 +6,7 @@ from .config import ConfigReader, ReadingError
from .dispatcher import Dispatcher, DispatchError from .dispatcher import Dispatcher, DispatchError
from .messenger import Messenger from .messenger import Messenger
from .messenger import Level from .messenger import Level
from .plugins import Clean, Create, Link, Shell
from .util import module from .util import module
import dotbot import dotbot
@ -118,9 +119,10 @@ def main():
else: else:
log.use_color(sys.stdout.isatty()) log.use_color(sys.stdout.isatty())
plugins = []
plugin_directories = list(options.plugin_dirs) plugin_directories = list(options.plugin_dirs)
if not options.disable_built_in_plugins: if not options.disable_built_in_plugins:
from .plugins import Clean, Create, Link, Shell plugins.extend([Clean, Create, Link, Shell])
plugin_paths = [] plugin_paths = []
for directory in plugin_directories: for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, "*.py")): for plugin_path in glob.glob(os.path.join(directory, "*.py")):
@ -129,7 +131,7 @@ def main():
plugin_paths.append(plugin_path) plugin_paths.append(plugin_path)
for plugin_path in plugin_paths: for plugin_path in plugin_paths:
abspath = os.path.abspath(plugin_path) abspath = os.path.abspath(plugin_path)
module.load(abspath) plugins.extend(module.load(abspath))
if not options.config_file: if not options.config_file:
log.error("No configuration file specified") log.error("No configuration file specified")
exit(1) exit(1)
@ -151,6 +153,7 @@ def main():
skip=options.skip, skip=options.skip,
exit_on_failure=options.exit_on_failure, exit_on_failure=options.exit_on_failure,
options=options, options=options,
plugins=plugins,
) )
success = dispatcher.dispatch(tasks) success = dispatcher.dispatch(tasks)
if success: if success:

View File

@ -7,11 +7,12 @@ from .context import Context
class Dispatcher(object): class Dispatcher(object):
def __init__( 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._log = Messenger()
self._setup_context(base_directory, options) 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._only = only
self._skip = skip self._skip = skip
self._exit = exit_on_failure self._exit = exit_on_failure
@ -65,9 +66,6 @@ class Dispatcher(object):
return False return False
return success return success
def _load_plugins(self):
self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()]
class DispatchError(Exception): class DispatchError(Exception):
pass pass

View File

@ -1,10 +1,10 @@
import os import os
import sys import sys
import dotbot from ..plugin import Plugin
class Clean(dotbot.Plugin): class Clean(Plugin):
""" """
Cleans broken symbolic links. Cleans broken symbolic links.
""" """

View File

@ -1,8 +1,9 @@
import os import os
import dotbot
from ..plugin import Plugin
class Create(dotbot.Plugin): class Create(Plugin):
""" """
Create empty paths. Create empty paths.
""" """

View File

@ -2,11 +2,12 @@ import os
import sys import sys
import glob import glob
import shutil 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. Symbolically links dotfiles.
""" """
@ -139,7 +140,7 @@ class Link(dotbot.Plugin):
return success return success
def _test_success(self, command): 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: if ret != 0:
self._log.debug("Test '%s' returned false" % command) self._log.debug("Test '%s' returned false" % command)
return ret == 0 return ret == 0

View File

@ -1,10 +1,8 @@
import os from ..plugin import Plugin
import subprocess from ..util import shell_command
import dotbot
import dotbot.util
class Shell(dotbot.Plugin): class Shell(Plugin):
""" """
Run arbitrary shell commands. Run arbitrary shell commands.
""" """
@ -50,7 +48,7 @@ class Shell(dotbot.Plugin):
self._log.lowinfo("%s [%s]" % (msg, cmd)) self._log.lowinfo("%s [%s]" % (msg, cmd))
stdout = options.get("stdout", stdout) stdout = options.get("stdout", stdout)
stderr = options.get("stderr", stderr) stderr = options.get("stderr", stderr)
ret = dotbot.util.shell_command( ret = shell_command(
cmd, cmd,
cwd=self._context.base_directory(), cwd=self._context.base_directory(),
enable_stdin=stdin, enable_stdin=stdin,

View File

@ -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. # We keep references to loaded modules so they don't get garbage collected.
loaded_modules = [] loaded_modules = []
@ -7,8 +10,17 @@ loaded_modules = []
def load(path): def load(path):
basename = os.path.basename(path) basename = os.path.basename(path)
module_name, extension = os.path.splitext(basename) module_name, extension = os.path.splitext(basename)
plugin = load_module(module_name, path) loaded_module = load_module(module_name, path)
loaded_modules.append(plugin) 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): if sys.version_info >= (3, 5):