Merge 7c7b1a4295
into 860a56580b
This commit is contained in:
commit
3906f81808
10 changed files with 183 additions and 2 deletions
|
@ -120,7 +120,8 @@ def main():
|
|||
|
||||
plugin_directories = list(options.plugin_dirs)
|
||||
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 = []
|
||||
for directory in plugin_directories:
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, base_directory, options=Namespace()):
|
||||
def __init__(self, base_directory, options=Namespace(), dispatcher=None):
|
||||
self._base_directory = base_directory
|
||||
self._defaults = {}
|
||||
self._options = options
|
||||
self._dispatcher = dispatcher
|
||||
pass
|
||||
|
||||
def set_base_directory(self, base_directory):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .clean import Clean
|
||||
from .conditional import Conditional
|
||||
from .create import Create
|
||||
from .link import Link
|
||||
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