Add generic conditional container plugin.
This commit is contained in:
parent
d2f76a2593
commit
7c7b1a4295
10 changed files with 183 additions and 2 deletions
|
@ -120,7 +120,8 @@ def main():
|
||||||
|
|
||||||
plugin_directories = list(options.plugin_dirs)
|
plugin_directories = list(options.plugin_dirs)
|
||||||
if not options.disable_built_in_plugins:
|
if not options.disable_built_in_plugins:
|
||||||
from .plugins import Clean, Create, Link, Shell
|
from .plugins import Clean, Create, Link, Shell, Conditional
|
||||||
|
from .conditions import ShellCondition, TtyCondition
|
||||||
plugin_paths = []
|
plugin_paths = []
|
||||||
for directory in plugin_directories:
|
for directory in plugin_directories:
|
||||||
for plugin_path in glob.glob(os.path.join(directory, "*.py")):
|
for plugin_path in glob.glob(os.path.join(directory, "*.py")):
|
||||||
|
|
27
dotbot/condition.py
Normal file
27
dotbot/condition.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import dotbot
|
||||||
|
import dotbot.util
|
||||||
|
|
||||||
|
from .messenger import Messenger
|
||||||
|
|
||||||
|
class Condition(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Abstract base class for conditions that test whether dotbot should execute tasks/actions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self._context = context
|
||||||
|
self._log = Messenger()
|
||||||
|
|
||||||
|
def can_handle(self, directive):
|
||||||
|
"""
|
||||||
|
Returns true if the Condition can handle the directive.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def handle(self, directive, data):
|
||||||
|
"""
|
||||||
|
Executes the test.
|
||||||
|
Returns the boolean value returned by the test
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
2
dotbot/conditions/__init__.py
Normal file
2
dotbot/conditions/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .shell import ShellCondition
|
||||||
|
from .tty import TtyCondition
|
20
dotbot/conditions/shell.py
Normal file
20
dotbot/conditions/shell.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from ..condition import Condition
|
||||||
|
import dotbot.util
|
||||||
|
|
||||||
|
class ShellCondition(Condition):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Condition testing an arbitrary shell command and evaluating to true if the return code is zero
|
||||||
|
"""
|
||||||
|
|
||||||
|
_directive = "shell"
|
||||||
|
|
||||||
|
def can_handle(self, directive):
|
||||||
|
return directive == self._directive
|
||||||
|
|
||||||
|
def handle(self, directive, command):
|
||||||
|
if directive != self._directive:
|
||||||
|
raise ValueError("ShellCondition cannot handle directive %s" % directive)
|
||||||
|
|
||||||
|
ret = dotbot.util.shell_command(command, cwd=self._context.base_directory())
|
||||||
|
return ret == 0
|
20
dotbot/conditions/tty.py
Normal file
20
dotbot/conditions/tty.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from ..condition import Condition
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class TtyCondition(Condition):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Condition testing if stdin is a TTY (allowing to request input from the user)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_directive = "tty"
|
||||||
|
|
||||||
|
def can_handle(self, directive):
|
||||||
|
return directive == self._directive
|
||||||
|
|
||||||
|
def handle(self, directive, data=True):
|
||||||
|
if directive != self._directive:
|
||||||
|
raise ValueError("Tty cannot handle directive %s" % directive)
|
||||||
|
expected = data if data is not None else True
|
||||||
|
return expected == (sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty())
|
|
@ -8,10 +8,11 @@ class Context(object):
|
||||||
Contextual data and information for plugins.
|
Contextual data and information for plugins.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, base_directory, options=Namespace()):
|
def __init__(self, base_directory, options=Namespace(), dispatcher=None):
|
||||||
self._base_directory = base_directory
|
self._base_directory = base_directory
|
||||||
self._defaults = {}
|
self._defaults = {}
|
||||||
self._options = options
|
self._options = options
|
||||||
|
self._dispatcher = dispatcher
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_base_directory(self, base_directory):
|
def set_base_directory(self, base_directory):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .clean import Clean
|
from .clean import Clean
|
||||||
|
from .conditional import Conditional
|
||||||
from .create import Create
|
from .create import Create
|
||||||
from .link import Link
|
from .link import Link
|
||||||
from .shell import Shell
|
from .shell import Shell
|
||||||
|
|
43
dotbot/plugins/conditional.py
Normal file
43
dotbot/plugins/conditional.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import dotbot
|
||||||
|
from dotbot.dispatcher import Dispatcher
|
||||||
|
from dotbot.tester import Tester
|
||||||
|
|
||||||
|
class Conditional(dotbot.Plugin):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Conditionally execute nested commands based on the result of configured test(s)
|
||||||
|
'''
|
||||||
|
|
||||||
|
_directive = "conditional"
|
||||||
|
|
||||||
|
def can_handle(self, directive):
|
||||||
|
return directive == self._directive
|
||||||
|
|
||||||
|
def handle(self, directive, data):
|
||||||
|
if directive != self._directive:
|
||||||
|
raise ValueError("Conditional cannot handle directive %s" % directive)
|
||||||
|
return self._process_conditional(data)
|
||||||
|
|
||||||
|
def _process_conditional(self, data):
|
||||||
|
success = True
|
||||||
|
tests = data.get("if")
|
||||||
|
test_result = Tester(self._context).evaluate(tests)
|
||||||
|
|
||||||
|
tasks = data.get("then") if test_result else data.get("else")
|
||||||
|
self._log.info("executing sub-commands")
|
||||||
|
# TODO prepend/extract defaults if scope_defaults is False
|
||||||
|
if tasks is not None:
|
||||||
|
return self._execute_tasks(tasks)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _execute_tasks(self, data):
|
||||||
|
# TODO improve handling of defaults either by reusing context/dispatcher -OR- prepend defaults & extract at end
|
||||||
|
dispatcher = Dispatcher(self._context.base_directory(),
|
||||||
|
only=self._context.options().only,
|
||||||
|
skip=self._context.options().skip,
|
||||||
|
options=self._context.options())
|
||||||
|
# if the data is a dictionary, wrap it in a list
|
||||||
|
data = data if type(data) is list else [ data ]
|
||||||
|
return dispatcher.dispatch(data)
|
||||||
|
# return self._context._dispatcher.dispatch(data)
|
40
dotbot/tester.py
Normal file
40
dotbot/tester.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from dotbot.condition import Condition
|
||||||
|
from dotbot.messenger import Messenger
|
||||||
|
|
||||||
|
class Tester(object):
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self._context = context
|
||||||
|
self._log = Messenger()
|
||||||
|
self.__load_conditions()
|
||||||
|
|
||||||
|
def __load_conditions(self):
|
||||||
|
self._plugins = [plugin(self._context) for plugin in Condition.__subclasses__()]
|
||||||
|
|
||||||
|
def evaluate(self, tests):
|
||||||
|
normalized = self.normalize_tests(tests)
|
||||||
|
|
||||||
|
for task in normalized:
|
||||||
|
for action in task:
|
||||||
|
for plugin in self._plugins:
|
||||||
|
if plugin.can_handle(action):
|
||||||
|
try:
|
||||||
|
local_success = plugin.handle(action, task[action])
|
||||||
|
if not local_success:
|
||||||
|
return False
|
||||||
|
except Exception as err:
|
||||||
|
self._log.error("An error was encountered while testing condition %s" % action)
|
||||||
|
self._log.debug(err)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def normalize_tests(self, tests):
|
||||||
|
if isinstance(tests, str):
|
||||||
|
return [ { 'shell': tests } ]
|
||||||
|
elif isinstance(tests, dict):
|
||||||
|
return [ tests ]
|
||||||
|
elif isinstance(tests, list):
|
||||||
|
return map(lambda test: { 'shell': test } if isinstance(test, str) else test, tests)
|
||||||
|
else:
|
||||||
|
# TODO error
|
||||||
|
return []
|
26
sample.conf.yaml
Normal file
26
sample.conf.yaml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
- defaults:
|
||||||
|
link:
|
||||||
|
relink: true
|
||||||
|
create: true
|
||||||
|
|
||||||
|
- clean: ['~']
|
||||||
|
|
||||||
|
- conditional:
|
||||||
|
if:
|
||||||
|
tty: true
|
||||||
|
then:
|
||||||
|
- shell:
|
||||||
|
- 'echo "Running from a TTY"'
|
||||||
|
|
||||||
|
- conditional:
|
||||||
|
if:
|
||||||
|
shell: "command -v python"
|
||||||
|
then:
|
||||||
|
- shell:
|
||||||
|
- 'echo "python is available"'
|
||||||
|
|
||||||
|
- conditional:
|
||||||
|
if: "command -v foo"
|
||||||
|
else:
|
||||||
|
- shell:
|
||||||
|
- 'echo "foo is not available"'
|
Loading…
Reference in a new issue