mirror of
1
0
Fork 0
This commit is contained in:
Gregory Furlong 2022-12-13 09:43:26 +00:00 committed by GitHub
commit 3906f81808
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 2 deletions

View File

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

View File

@ -1,4 +1,5 @@
from .clean import Clean
from .conditional import Conditional
from .create import Create
from .link import Link
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"'