summaryrefslogtreecommitdiff
path: root/dotbot
diff options
context:
space:
mode:
authorCasey Rodarmor <casey@rodarmor.com>2016-01-16 19:00:15 -0800
committerAnish Athalye <me@anishathalye.com>2016-02-06 15:14:35 -0500
commiteeb4c284fb71dd62eaf5ee7017e23b64c2c899d7 (patch)
tree0f79d448c9f0864153d96d36bf43b263b2a15e6d /dotbot
parentba9e9cbe709b3a736d10dae0922b09a5eb0cde87 (diff)
Add plugin loader
Diffstat (limited to 'dotbot')
-rw-r--r--dotbot/__init__.py1
-rw-r--r--dotbot/cli.py27
-rw-r--r--dotbot/dispatcher.py4
-rw-r--r--dotbot/executor/__init__.py4
-rw-r--r--dotbot/executor/cleaner.py52
-rw-r--r--dotbot/executor/commandrunner.py53
-rw-r--r--dotbot/executor/linker.py142
-rw-r--r--dotbot/plugin.py (renamed from dotbot/executor/executor.py)8
-rw-r--r--dotbot/util/module.py29
9 files changed, 60 insertions, 260 deletions
diff --git a/dotbot/__init__.py b/dotbot/__init__.py
index 401da57..1d03464 100644
--- a/dotbot/__init__.py
+++ b/dotbot/__init__.py
@@ -1 +1,2 @@
from .cli import main
+from .plugin import Plugin
diff --git a/dotbot/cli.py b/dotbot/cli.py
index dc90909..d77ab42 100644
--- a/dotbot/cli.py
+++ b/dotbot/cli.py
@@ -1,8 +1,11 @@
+import os, glob
+
from argparse import ArgumentParser
from .config import ConfigReader, ReadingError
from .dispatcher import Dispatcher, DispatchError
from .messenger import Messenger
from .messenger import Level
+from .util import module
def add_options(parser):
parser.add_argument('-Q', '--super-quiet', dest='super_quiet', action='store_true',
@@ -17,6 +20,12 @@ def add_options(parser):
parser.add_argument('-c', '--config-file', nargs=1, dest='config_file',
help='run commands given in CONFIGFILE', metavar='CONFIGFILE',
required=True)
+ parser.add_argument('-p', '--plugin', action='append', dest='plugins', default=[],
+ help='load PLUGIN as a plugin', metavar='PLUGIN')
+ parser.add_argument('--disable-built-in-plugins', dest='disable_built_in_plugins',
+ action='store_true', help='disable built-in plugins')
+ parser.add_argument('--plugin-dir', action='append', dest='plugin_dirs', default=[],
+ metavar='PLUGIN_DIR', help='load all plugins in PLUGIN_DIR')
def read_config(config_file):
reader = ConfigReader(config_file)
@@ -28,12 +37,24 @@ def main():
parser = ArgumentParser()
add_options(parser)
options = parser.parse_args()
- if (options.super_quiet):
+ if options.super_quiet:
log.set_level(Level.WARNING)
- if (options.quiet):
+ if options.quiet:
log.set_level(Level.INFO)
- if (options.verbose):
+ if options.verbose:
log.set_level(Level.DEBUG)
+ plugin_directories = list(options.plugin_dirs)
+ if not options.disable_built_in_plugins:
+ plugin_directories.append(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
+ plugin_paths = []
+ for directory in plugin_directories:
+ for plugin_path in glob.glob(os.path.join(directory, '*.py')):
+ plugin_paths.append(plugin_path)
+ for plugin_path in options.plugins:
+ plugin_paths.append(plugin_path)
+ for plugin_path in plugin_paths:
+ abspath = os.path.abspath(plugin_path)
+ module.load(abspath)
tasks = read_config(options.config_file[0])
if not isinstance(tasks, list):
raise ReadingError('Configuration file must be a list of tasks')
diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py
index c3c7fe8..79231a0 100644
--- a/dotbot/dispatcher.py
+++ b/dotbot/dispatcher.py
@@ -1,5 +1,5 @@
import os
-from .executor import Executor
+from .plugin import Plugin
from .messenger import Messenger
class Dispatcher(object):
@@ -37,7 +37,7 @@ class Dispatcher(object):
def _load_plugins(self):
self._plugins = [plugin(self._base_directory)
- for plugin in Executor.__subclasses__()]
+ for plugin in Plugin.__subclasses__()]
class DispatchError(Exception):
pass
diff --git a/dotbot/executor/__init__.py b/dotbot/executor/__init__.py
deleted file mode 100644
index d87ca4b..0000000
--- a/dotbot/executor/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .executor import Executor
-from .linker import Linker
-from .cleaner import Cleaner
-from .commandrunner import CommandRunner
diff --git a/dotbot/executor/cleaner.py b/dotbot/executor/cleaner.py
deleted file mode 100644
index 504c0de..0000000
--- a/dotbot/executor/cleaner.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import os
-from . import Executor
-
-class Cleaner(Executor):
- '''
- Cleans broken symbolic links.
- '''
-
- _directive = 'clean'
-
- def can_handle(self, directive):
- return directive == self._directive
-
- def handle(self, directive, data):
- if directive != self._directive:
- raise ValueError('Cleaner cannot handle directive %s' % directive)
- return self._process_clean(data)
-
- def _process_clean(self, targets):
- success = True
- for target in targets:
- success &= self._clean(target)
- if success:
- self._log.info('All targets have been cleaned')
- else:
- self._log.error('Some targets were not successfully cleaned')
- return success
-
- def _clean(self, target):
- '''
- Cleans all the broken symbolic links in target that point to
- a subdirectory of the base directory.
- '''
- if not os.path.isdir(os.path.expanduser(target)):
- self._log.debug('Ignoring nonexistent directory %s' % target)
- return True
- for item in os.listdir(os.path.expanduser(target)):
- path = os.path.join(os.path.expanduser(target), item)
- if not os.path.exists(path) and os.path.islink(path):
- if self._in_directory(path, self._base_directory):
- self._log.lowinfo('Removing invalid link %s -> %s' %
- (path, os.path.join(os.path.dirname(path), os.readlink(path))))
- os.remove(path)
- return True
-
- def _in_directory(self, path, directory):
- '''
- Returns true if the path is in the directory.
- '''
- directory = os.path.join(os.path.realpath(directory), '')
- path = os.path.realpath(path)
- return os.path.commonprefix([path, directory]) == directory
diff --git a/dotbot/executor/commandrunner.py b/dotbot/executor/commandrunner.py
deleted file mode 100644
index 45d40f5..0000000
--- a/dotbot/executor/commandrunner.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import os, subprocess
-from . import Executor
-
-class CommandRunner(Executor):
- '''
- Run arbitrary shell commands.
- '''
-
- _directive = 'shell'
-
- def can_handle(self, directive):
- return directive == self._directive
-
- def handle(self, directive, data):
- if directive != self._directive:
- raise ValueError('CommandRunner cannot handle directive %s' %
- directive)
- return self._process_commands(data)
-
- def _process_commands(self, data):
- success = True
- with open(os.devnull, 'w') as devnull:
- for item in data:
- stdin = stdout = stderr = devnull
- if isinstance(item, dict):
- cmd = item['command']
- msg = item.get('description', None)
- if item.get('stdin', False) is True:
- stdin = None
- if item.get('stdout', False) is True:
- stdout = None
- if item.get('stderr', False) is True:
- stderr = None
- elif isinstance(item, list):
- cmd = item[0]
- msg = item[1] if len(item) > 1 else None
- else:
- cmd = item
- msg = None
- if msg is None:
- self._log.lowinfo(cmd)
- else:
- self._log.lowinfo('%s [%s]' % (msg, cmd))
- ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout,
- stderr=stderr, cwd=self._base_directory)
- if ret != 0:
- success = False
- self._log.warning('Command [%s] failed' % cmd)
- if success:
- self._log.info('All commands have been executed')
- else:
- self._log.error('Some commands were not successfully executed')
- return success
diff --git a/dotbot/executor/linker.py b/dotbot/executor/linker.py
deleted file mode 100644
index 5c12ea0..0000000
--- a/dotbot/executor/linker.py
+++ /dev/null
@@ -1,142 +0,0 @@
-import os, shutil
-from . import Executor
-
-class Linker(Executor):
- '''
- Symbolically links dotfiles.
- '''
-
- _directive = 'link'
-
- def can_handle(self, directive):
- return directive == self._directive
-
- def handle(self, directive, data):
- if directive != self._directive:
- raise ValueError('Linker cannot handle directive %s' % directive)
- return self._process_links(data)
-
- def _process_links(self, links):
- success = True
- for destination, source in links.items():
- source = os.path.expandvars(source)
- destination = os.path.expandvars(destination)
- if isinstance(source, dict):
- # extended config
- path = source['path']
- force = source.get('force', False)
- relink = source.get('relink', False)
- create = source.get('create', False)
- if create:
- success &= self._create(destination)
- if force:
- success &= self._delete(path, destination, force=True)
- elif relink:
- success &= self._delete(path, destination, force=False)
- else:
- path = source
- success &= self._link(path, destination)
- if success:
- self._log.info('All links have been set up')
- else:
- self._log.error('Some links were not successfully set up')
- return success
-
- def _is_link(self, path):
- '''
- Returns true if the path is a symbolic link.
- '''
- return os.path.islink(os.path.expanduser(path))
-
- def _link_destination(self, path):
- '''
- Returns the absolute path to the destination of the symbolic link.
- '''
- path = os.path.expanduser(path)
- rel_dest = os.readlink(path)
- return os.path.join(os.path.dirname(path), rel_dest)
-
- def _exists(self, path):
- '''
- Returns true if the path exists.
- '''
- path = os.path.expanduser(path)
- return os.path.exists(path)
-
- def _create(self, path):
- success = True
- parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir))
- if not self._exists(parent):
- try:
- os.makedirs(parent)
- except OSError:
- self._log.warning('Failed to create directory %s' % parent)
- success = False
- else:
- self._log.lowinfo('Creating directory %s' % parent)
- return success
-
- def _delete(self, source, path, force):
- success = True
- source = os.path.join(self._base_directory, source)
- if ((self._is_link(path) and self._link_destination(path) != source) or
- (self._exists(path) and not self._is_link(path))):
- fullpath = os.path.expanduser(path)
- removed = False
- try:
- if os.path.islink(fullpath):
- os.unlink(fullpath)
- removed = True
- elif force:
- if os.path.isdir(fullpath):
- shutil.rmtree(fullpath)
- removed = True
- else:
- os.remove(fullpath)
- removed = True
- except OSError:
- self._log.warning('Failed to remove %s' % path)
- success = False
- else:
- if removed:
- self._log.lowinfo('Removing %s' % path)
- return success
-
- def _link(self, source, link_name):
- '''
- Links link_name to source.
-
- Returns true if successfully linked files.
- '''
- success = False
- source = os.path.join(self._base_directory, source)
- if (not self._exists(link_name) and self._is_link(link_name) and
- self._link_destination(link_name) != source):
- self._log.warning('Invalid link %s -> %s' %
- (link_name, self._link_destination(link_name)))
- elif not self._exists(link_name) and self._exists(source):
- try:
- os.symlink(source, os.path.expanduser(link_name))
- except OSError:
- self._log.warning('Linking failed %s -> %s' % (link_name, source))
- else:
- self._log.lowinfo('Creating link %s -> %s' % (link_name, source))
- success = True
- elif self._exists(link_name) and not self._is_link(link_name):
- self._log.warning(
- '%s already exists but is a regular file or directory' %
- link_name)
- elif self._is_link(link_name) and self._link_destination(link_name) != source:
- self._log.warning('Incorrect link %s -> %s' %
- (link_name, self._link_destination(link_name)))
- elif not self._exists(source):
- if self._is_link(link_name):
- self._log.warning('Nonexistent target %s -> %s' %
- (link_name, source))
- else:
- self._log.warning('Nonexistent target for %s : %s' %
- (link_name, source))
- else:
- self._log.lowinfo('Link exists %s -> %s' % (link_name, source))
- success = True
- return success
diff --git a/dotbot/executor/executor.py b/dotbot/plugin.py
index e6862c0..a79639e 100644
--- a/dotbot/executor/executor.py
+++ b/dotbot/plugin.py
@@ -1,6 +1,6 @@
-from ..messenger import Messenger
+from .messenger import Messenger
-class Executor(object):
+class Plugin(object):
'''
Abstract base class for commands that process directives.
'''
@@ -11,7 +11,7 @@ class Executor(object):
def can_handle(self, directive):
'''
- Returns true if the Executor can handle the directive.
+ Returns true if the Plugin can handle the directive.
'''
raise NotImplementedError
@@ -19,6 +19,6 @@ class Executor(object):
'''
Executes the directive.
- Returns true if the Executor successfully handled the directive.
+ Returns true if the Plugin successfully handled the directive.
'''
raise NotImplementedError
diff --git a/dotbot/util/module.py b/dotbot/util/module.py
new file mode 100644
index 0000000..af6b0ed
--- /dev/null
+++ b/dotbot/util/module.py
@@ -0,0 +1,29 @@
+import sys, os.path
+
+# We keep references to loaded modules so they don't get garbage collected.
+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)
+
+if sys.version_info >= (3, 5):
+ import importlib.util
+
+ def load_module(module_name, path):
+ spec = importlib.util.spec_from_file_location(module_name, path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+elif sys.version_info >= (3, 3):
+ from importlib.machinery import SourceFileLoader
+
+ def load_module(module_name, path):
+ return SourceFileLoader(module_name, path).load_module()
+else:
+ import imp
+
+ def load_module(module_name, path):
+ return imp.load_source(module_name, path)