From f52bbd1eec0d6fb103c5fcbb75c03ca223fffdb7 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Wed, 2 Mar 2016 20:53:19 -0500 Subject: [PATCH] Add default options This feature was implemented with feedback from Aleks Kamko and Casey Rodarmor . --- README.md | 35 +++++++++++++++++++++++++++++++++++ dotbot/context.py | 23 +++++++++++++++++++++++ dotbot/dispatcher.py | 16 ++++++++++------ dotbot/plugin.py | 5 +++-- plugins/clean.py | 2 +- plugins/link.py | 28 +++++++++++++++------------- plugins/shell.py | 9 +++++---- test/tests/defaults.bash | 34 ++++++++++++++++++++++++++++++++++ 8 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 dotbot/context.py create mode 100644 test/tests/defaults.bash diff --git a/README.md b/README.md index 3ed74f5..1b42f75 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ Here's an example of a complete configuration. The conventional name for the configuration file is `install.conf.yaml`. ```yaml +- defaults: + link: + relink: true + - clean: ['~'] - link: @@ -97,6 +101,13 @@ The conventional name for this file is `install.conf.json`. ```json [ + { + "defaults": { + "link": { + "relink": true + } + } + }, { "clean": ["~"] }, @@ -225,6 +236,30 @@ Clean commands are specified as an array of directories to be cleaned. - clean: ['~'] ``` +### Defaults + +Default options for plugins can be specified so that options don't have to be +repeated many times. This can be very useful to use with the link command, for +example. + +Defaults apply to all commands that follow setting the defaults. Defaults can +be set multiple times; each change replaces the defaults with a new set of +options. + +#### Format + +Defaults are specified as a dictionary mapping action names to settings, which +are dictionaries from option names to values. + +#### Example + +```yaml +- defaults: + link: + create: true + relink: true +``` + ### Plugins Dotbot also supports custom directives implemented by plugins. Plugins are diff --git a/dotbot/context.py b/dotbot/context.py new file mode 100644 index 0000000..b2dbd6c --- /dev/null +++ b/dotbot/context.py @@ -0,0 +1,23 @@ +import copy + +class Context(object): + ''' + Contextual data and information for plugins. + ''' + + def __init__(self, base_directory): + self._base_directory = base_directory + self._defaults = {} + pass + + def set_base_directory(self, base_directory): + self._base_directory = base_directory + + def base_directory(self): + return self._base_directory + + def set_defaults(self, defaults): + self._defaults = defaults + + def defaults(self): + return copy.deepcopy(self._defaults) diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index 79231a0..cc07435 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -1,26 +1,30 @@ import os from .plugin import Plugin from .messenger import Messenger +from .context import Context class Dispatcher(object): def __init__(self, base_directory): self._log = Messenger() - self._set_base_directory(base_directory) + self._setup_context(base_directory) self._load_plugins() - def _set_base_directory(self, base_directory): + def _setup_context(self, base_directory): path = os.path.abspath(os.path.realpath( os.path.expanduser(base_directory))) - if os.path.exists(path): - self._base_directory = path - else: + if not os.path.exists(path): raise DispatchError('Nonexistent base directory') + self._context = Context(path) def dispatch(self, tasks): success = True for task in tasks: for action in task: handled = False + if action == 'defaults': + self._context.set_defaults(task[action]) # replace, not update + handled = True + # keep going, let other plugins handle this if they want for plugin in self._plugins: if plugin.can_handle(action): try: @@ -36,7 +40,7 @@ class Dispatcher(object): return success def _load_plugins(self): - self._plugins = [plugin(self._base_directory) + self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()] class DispatchError(Exception): diff --git a/dotbot/plugin.py b/dotbot/plugin.py index a79639e..56d4da8 100644 --- a/dotbot/plugin.py +++ b/dotbot/plugin.py @@ -1,12 +1,13 @@ from .messenger import Messenger +from .context import Context class Plugin(object): ''' Abstract base class for commands that process directives. ''' - def __init__(self, base_directory): - self._base_directory = base_directory + def __init__(self, context): + self._context = context self._log = Messenger() def can_handle(self, directive): diff --git a/plugins/clean.py b/plugins/clean.py index 22ec450..dbd11af 100644 --- a/plugins/clean.py +++ b/plugins/clean.py @@ -36,7 +36,7 @@ class Clean(dotbot.Plugin): 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): + if self._in_directory(path, self._context.base_directory()): self._log.lowinfo('Removing invalid link %s -> %s' % (path, os.path.join(os.path.dirname(path), os.readlink(path)))) os.remove(path) diff --git a/plugins/link.py b/plugins/link.py index 3bb5686..30a77c9 100644 --- a/plugins/link.py +++ b/plugins/link.py @@ -17,25 +17,27 @@ class Link(dotbot.Plugin): def _process_links(self, links): success = True + defaults = self._context.defaults().get('link', {}) for destination, source in links.items(): source = os.path.expandvars(source) destination = os.path.expandvars(destination) + relative = defaults.get('relative', False) + force = defaults.get('force', False) + relink = defaults.get('relink', False) + create = defaults.get('create', False) if isinstance(source, dict): # extended config + relative = source.get('relative', relative) + force = source.get('force', force) + relink = source.get('relink', relink) + create = source.get('create', create) path = source['path'] - relative = source.get('relative', False) - 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: - relative = False path = source + if create: + success &= self._create(destination) + if force or relink: + success &= self._delete(path, destination, force=force) success &= self._link(path, destination, relative) if success: self._log.info('All links have been set up') @@ -79,7 +81,7 @@ class Link(dotbot.Plugin): def _delete(self, source, path, force): success = True - source = os.path.join(self._base_directory, source) + source = os.path.join(self._context.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) @@ -110,7 +112,7 @@ class Link(dotbot.Plugin): Returns true if successfully linked files. ''' success = False - source = os.path.join(self._base_directory, source) + source = os.path.join(self._context.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' % diff --git a/plugins/shell.py b/plugins/shell.py index a2d9c1a..f321ca3 100644 --- a/plugins/shell.py +++ b/plugins/shell.py @@ -18,17 +18,18 @@ class Shell(dotbot.Plugin): def _process_commands(self, data): success = True + defaults = self._context.defaults().get('shell', {}) 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: + if item.get('stdin', defaults.get('stdin', False)) is True: stdin = None - if item.get('stdout', False) is True: + if item.get('stdout', defaults.get('stdout', False)) is True: stdout = None - if item.get('stderr', False) is True: + if item.get('stderr', defaults.get('stderr', False)) is True: stderr = None elif isinstance(item, list): cmd = item[0] @@ -41,7 +42,7 @@ class Shell(dotbot.Plugin): else: self._log.lowinfo('%s [%s]' % (msg, cmd)) ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout, - stderr=stderr, cwd=self._base_directory) + stderr=stderr, cwd=self._context.base_directory()) if ret != 0: success = False self._log.warning('Command [%s] failed' % cmd) diff --git a/test/tests/defaults.bash b/test/tests/defaults.bash new file mode 100644 index 0000000..595f950 --- /dev/null +++ b/test/tests/defaults.bash @@ -0,0 +1,34 @@ +test_description='defaults setting works' +. '../test-lib.bash' + +test_expect_success 'setup' ' +echo "apple" > ${DOTFILES}/f && +echo "grape" > ~/f && +ln -s ~/f ~/.f +' + +test_expect_failure 'run-fail' ' +run_dotbot <