mirror of
1
0
Fork 0

Add default options

This feature was implemented with feedback from Aleks Kamko
<aykamko@gmail.com> and Casey Rodarmor <casey@rodarmor.com>.
This commit is contained in:
Anish Athalye 2016-03-02 20:53:19 -05:00
parent daf8d82e02
commit f52bbd1eec
8 changed files with 126 additions and 26 deletions

View File

@ -78,6 +78,10 @@ Here's an example of a complete configuration.
The conventional name for the configuration file is `install.conf.yaml`. The conventional name for the configuration file is `install.conf.yaml`.
```yaml ```yaml
- defaults:
link:
relink: true
- clean: ['~'] - clean: ['~']
- link: - link:
@ -97,6 +101,13 @@ The conventional name for this file is `install.conf.json`.
```json ```json
[ [
{
"defaults": {
"link": {
"relink": true
}
}
},
{ {
"clean": ["~"] "clean": ["~"]
}, },
@ -225,6 +236,30 @@ Clean commands are specified as an array of directories to be cleaned.
- clean: ['~'] - 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 ### Plugins
Dotbot also supports custom directives implemented by plugins. Plugins are Dotbot also supports custom directives implemented by plugins. Plugins are

23
dotbot/context.py Normal file
View File

@ -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)

View File

@ -1,26 +1,30 @@
import os import os
from .plugin import Plugin from .plugin import Plugin
from .messenger import Messenger from .messenger import Messenger
from .context import Context
class Dispatcher(object): class Dispatcher(object):
def __init__(self, base_directory): def __init__(self, base_directory):
self._log = Messenger() self._log = Messenger()
self._set_base_directory(base_directory) self._setup_context(base_directory)
self._load_plugins() self._load_plugins()
def _set_base_directory(self, base_directory): def _setup_context(self, base_directory):
path = os.path.abspath(os.path.realpath( path = os.path.abspath(os.path.realpath(
os.path.expanduser(base_directory))) os.path.expanduser(base_directory)))
if os.path.exists(path): if not os.path.exists(path):
self._base_directory = path
else:
raise DispatchError('Nonexistent base directory') raise DispatchError('Nonexistent base directory')
self._context = Context(path)
def dispatch(self, tasks): def dispatch(self, tasks):
success = True success = True
for task in tasks: for task in tasks:
for action in task: for action in task:
handled = False 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: for plugin in self._plugins:
if plugin.can_handle(action): if plugin.can_handle(action):
try: try:
@ -36,7 +40,7 @@ class Dispatcher(object):
return success return success
def _load_plugins(self): def _load_plugins(self):
self._plugins = [plugin(self._base_directory) self._plugins = [plugin(self._context)
for plugin in Plugin.__subclasses__()] for plugin in Plugin.__subclasses__()]
class DispatchError(Exception): class DispatchError(Exception):

View File

@ -1,12 +1,13 @@
from .messenger import Messenger from .messenger import Messenger
from .context import Context
class Plugin(object): class Plugin(object):
''' '''
Abstract base class for commands that process directives. Abstract base class for commands that process directives.
''' '''
def __init__(self, base_directory): def __init__(self, context):
self._base_directory = base_directory self._context = context
self._log = Messenger() self._log = Messenger()
def can_handle(self, directive): def can_handle(self, directive):

View File

@ -36,7 +36,7 @@ class Clean(dotbot.Plugin):
for item in os.listdir(os.path.expanduser(target)): for item in os.listdir(os.path.expanduser(target)):
path = os.path.join(os.path.expanduser(target), item) path = os.path.join(os.path.expanduser(target), item)
if not os.path.exists(path) and os.path.islink(path): 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' % self._log.lowinfo('Removing invalid link %s -> %s' %
(path, os.path.join(os.path.dirname(path), os.readlink(path)))) (path, os.path.join(os.path.dirname(path), os.readlink(path))))
os.remove(path) os.remove(path)

View File

@ -17,25 +17,27 @@ class Link(dotbot.Plugin):
def _process_links(self, links): def _process_links(self, links):
success = True success = True
defaults = self._context.defaults().get('link', {})
for destination, source in links.items(): for destination, source in links.items():
source = os.path.expandvars(source) source = os.path.expandvars(source)
destination = os.path.expandvars(destination) 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): if isinstance(source, dict):
# extended config # 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'] 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: else:
relative = False
path = source 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) success &= self._link(path, destination, relative)
if success: if success:
self._log.info('All links have been set up') self._log.info('All links have been set up')
@ -79,7 +81,7 @@ class Link(dotbot.Plugin):
def _delete(self, source, path, force): def _delete(self, source, path, force):
success = True 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 if ((self._is_link(path) and self._link_destination(path) != source) or
(self._exists(path) and not self._is_link(path))): (self._exists(path) and not self._is_link(path))):
fullpath = os.path.expanduser(path) fullpath = os.path.expanduser(path)
@ -110,7 +112,7 @@ class Link(dotbot.Plugin):
Returns true if successfully linked files. Returns true if successfully linked files.
''' '''
success = False 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 if (not self._exists(link_name) and self._is_link(link_name) and
self._link_destination(link_name) != source): self._link_destination(link_name) != source):
self._log.warning('Invalid link %s -> %s' % self._log.warning('Invalid link %s -> %s' %

View File

@ -18,17 +18,18 @@ class Shell(dotbot.Plugin):
def _process_commands(self, data): def _process_commands(self, data):
success = True success = True
defaults = self._context.defaults().get('shell', {})
with open(os.devnull, 'w') as devnull: with open(os.devnull, 'w') as devnull:
for item in data: for item in data:
stdin = stdout = stderr = devnull stdin = stdout = stderr = devnull
if isinstance(item, dict): if isinstance(item, dict):
cmd = item['command'] cmd = item['command']
msg = item.get('description', None) msg = item.get('description', None)
if item.get('stdin', False) is True: if item.get('stdin', defaults.get('stdin', False)) is True:
stdin = None stdin = None
if item.get('stdout', False) is True: if item.get('stdout', defaults.get('stdout', False)) is True:
stdout = None stdout = None
if item.get('stderr', False) is True: if item.get('stderr', defaults.get('stderr', False)) is True:
stderr = None stderr = None
elif isinstance(item, list): elif isinstance(item, list):
cmd = item[0] cmd = item[0]
@ -41,7 +42,7 @@ class Shell(dotbot.Plugin):
else: else:
self._log.lowinfo('%s [%s]' % (msg, cmd)) self._log.lowinfo('%s [%s]' % (msg, cmd))
ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout, 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: if ret != 0:
success = False success = False
self._log.warning('Command [%s] failed' % cmd) self._log.warning('Command [%s] failed' % cmd)

34
test/tests/defaults.bash Normal file
View File

@ -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
'