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`.
```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

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

View File

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

View File

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

View File

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

View File

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

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
'