summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnish Athalye <me@anishathalye.com>2016-03-02 20:53:19 -0500
committerAnish Athalye <me@anishathalye.com>2016-03-03 09:42:50 -0500
commitf52bbd1eec0d6fb103c5fcbb75c03ca223fffdb7 (patch)
tree8578282f6ce6cdb02eab51a9bcd18c590f7a127a
parentdaf8d82e02c2bcc476932790fa25076631c4c312 (diff)
Add default options
This feature was implemented with feedback from Aleks Kamko <aykamko@gmail.com> and Casey Rodarmor <casey@rodarmor.com>.
-rw-r--r--README.md35
-rw-r--r--dotbot/context.py23
-rw-r--r--dotbot/dispatcher.py16
-rw-r--r--dotbot/plugin.py5
-rw-r--r--plugins/clean.py2
-rw-r--r--plugins/link.py28
-rw-r--r--plugins/shell.py9
-rw-r--r--test/tests/defaults.bash34
8 files changed, 126 insertions, 26 deletions
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:
@@ -98,6 +102,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 <<EOF
+- link:
+ ~/.f: f
+EOF
+'
+
+test_expect_failure 'test-fail' '
+grep "apple" ~/.f
+'
+
+test_expect_success 'run' '
+run_dotbot <<EOF
+- defaults:
+ link:
+ relink: true
+
+- link:
+ ~/.f: f
+EOF
+'
+
+test_expect_success 'test' '
+grep "apple" ~/.f
+'