mirror of
1
0
Fork 0

Add generic conditional container plugin.

This commit is contained in:
Gregory Furlong 2022-10-01 16:30:44 -04:00
parent d2f76a2593
commit 7c7b1a4295
10 changed files with 183 additions and 2 deletions

View File

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

View File

@ -0,0 +1,2 @@
from .shell import ShellCondition
from .tty import TtyCondition

View 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
View 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())

View File

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

View File

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

View 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
View 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
View 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"'