diff --git a/doc/configuration.rst b/doc/configuration.rst index 22a03f85..fdd67f3b 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -68,6 +68,17 @@ Check the :ref:`plugins ` section for an introduction on available plugin categories. +per-task sections +^^^^^^^^^^^^^^^^^ + +To configure options for a specific task, use a section with +the task name prefixed with "task:":: + + [task:make_cookies] + cookie_type = chocolate + temp = 375F + duration = 12 + configuration at *dodo.py* -------------------------- diff --git a/doit/cmd_base.py b/doit/cmd_base.py index eb256608..c7da5966 100644 --- a/doit/cmd_base.py +++ b/doit/cmd_base.py @@ -384,9 +384,17 @@ def load_doit_config(self): return loader.load_doit_config(self.namespace) def load_tasks(self, cmd, pos_args): - return loader.load_tasks(self.namespace, self.cmd_names, + tasks = loader.load_tasks(self.namespace, self.cmd_names, cmd.execute_tasks) + # Add task options from config, if present + if self.config is not None: + for task in tasks: + task_stanza = 'task:' + task.name + if task_stanza in self.config: + task.cfg_values = self.config[task_stanza] + + return tasks class ModuleTaskLoader(NamespaceTaskLoader): """load tasks from a module/dictionary containing task generators diff --git a/doit/control.py b/doit/control.py index 8d0f27d6..969b31c2 100644 --- a/doit/control.py +++ b/doit/control.py @@ -5,7 +5,6 @@ import re from .exceptions import InvalidTask, InvalidCommand, InvalidDodoFile -from .cmdparse import TaskParse, CmdOption from .task import Task, DelayedLoaded from .loader import generate_tasks @@ -156,9 +155,10 @@ def add_filtered_task(seq, f_name): if f_name in self.tasks: # parse task_selection the_task = self.tasks[f_name] - # remaining items are other tasks not positional options - taskcmd = TaskParse([CmdOption(opt) for opt in the_task.params]) - the_task.options, seq = taskcmd.parse(seq) + + # Initialize options for the task + seq = the_task.init_options(seq) + # if task takes positional parameters set all as pos_arg_val if the_task.pos_arg is not None: the_task.pos_arg_val = seq diff --git a/doit/task.py b/doit/task.py index f2fbe029..2ad04f83 100644 --- a/doit/task.py +++ b/doit/task.py @@ -154,7 +154,7 @@ class Task(object): 'getargs': ((dict,), ()), 'title': ((Callable,), (None,)), 'watch': ((list, tuple), ()), - } + } def __init__(self, name, actions, file_dep=(), targets=(), @@ -230,6 +230,7 @@ def __init__(self, name, actions, file_dep=(), targets=(), self.values = {} self.verbosity = verbosity self.custom_title = title + self.cfg_values = None # clean if clean is True: @@ -358,17 +359,28 @@ def update_deps(self, deps): self._expand_map[dep](self, dep_values) - def init_options(self): + def init_options(self, args=None): """Put default values on options. - This will only be used, if params options were not passed - on the command line. + This function will only initialize task options once. If provided the args + parameter will be parsed for command line arguments intended for this task. + + Return value: unparsed command line task arguments or None. """ if self.options is None: + self.options = {} taskcmd = TaskParse([CmdOption(opt) for opt in self.params]) - # ignore positional parameters - self.options = taskcmd.parse('')[0] + if self.cfg_values is not None: + taskcmd.overwrite_defaults(self.cfg_values) + if args is None: + # ignore positional parameters + self.options.update(taskcmd.parse('')[0]) + return None + else: + parsed_options, args = taskcmd.parse(args) + self.options.update(parsed_options) + return args def _init_getargs(self): """task getargs attribute define implicit task dependencies""" diff --git a/tests/test_cmd_base.py b/tests/test_cmd_base.py index 2b95d371..a476fb7d 100644 --- a/tests/test_cmd_base.py +++ b/tests/test_cmd_base.py @@ -145,6 +145,25 @@ def test_load_tasks_from_module(self): assert ['xxx1'] == [t.name for t in task_list] assert {'verbose': 2} == config + def test_task_config(self): + 'Ensure that doit.cfg specified task parameters are applied.' + + cmd = Command() + members = {'task_foo': lambda: {'actions':[], + 'params': [{ + 'name': 'x', + 'default': None, + 'long': 'x' + }]}, + 'DOIT_CONFIG': {'task:foo': {'x': 1}}, + } + loader = ModuleTaskLoader(members) + loader.setup({}) + loader.config = loader.load_doit_config() + task_list = loader.load_tasks(cmd, []) + task = task_list.pop() + task.init_options() + assert 1 == task.options['x'] class TestDodoTaskLoader(object): def test_load_tasks(self, restore_cwd): diff --git a/tests/test_control.py b/tests/test_control.py index 10eec131..5425b436 100644 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -59,44 +59,45 @@ def test_bug770150_task_dependency_from_target(self): TaskControl([t1, t2, t3]) assert ['taskZ', 'taskX'] == t2.task_dep - -TASKS_SAMPLE = [Task("t1", [""], doc="t1 doc string"), - Task("t2", [""], doc="t2 doc string"), - Task("g1", None, doc="g1 doc string"), - Task("g1.a", [""], doc="g1.a doc string", subtask_of='g1'), - Task("g1.b", [""], doc="g1.b doc string", subtask_of='g1'), - Task("t3", [""], doc="t3 doc string", - params=[{'name':'opt1','long':'message','default':''}])] - +@pytest.fixture +def tasks_sample(): + return [Task("t1", [""], doc="t1 doc string"), + Task("t2", [""], doc="t2 doc string"), + Task("g1", None, doc="g1 doc string"), + Task("g1.a", [""], doc="g1.a doc string", subtask_of='g1'), + Task("g1.b", [""], doc="g1.b doc string", subtask_of='g1'), + Task("t3", [""], doc="t3 doc string", + params=[{'name':'opt1','long':'message','default':''}]) + ] class TestTaskControlCmdOptions(object): - def testFilter(self): + def testFilter(self, tasks_sample): filter_ = ['t2', 't3'] - tc = TaskControl(TASKS_SAMPLE) + tc = TaskControl(tasks_sample) assert filter_ == tc._filter_tasks(filter_) - def testProcessSelection(self): + def testProcessSelection(self, tasks_sample): filter_ = ['t2', 't3'] - tc = TaskControl(TASKS_SAMPLE) + tc = TaskControl(tasks_sample) tc.process(filter_) assert filter_ == tc.selected_tasks - def testProcessAll(self): - tc = TaskControl(TASKS_SAMPLE) + def testProcessAll(self, tasks_sample): + tc = TaskControl(tasks_sample) tc.process(None) assert ['t1', 't2', 'g1', 'g1.a', 'g1.b', 't3'] == tc.selected_tasks - def testFilterPattern(self): - tc = TaskControl(TASKS_SAMPLE) + def testFilterPattern(self, tasks_sample): + tc = TaskControl(tasks_sample) assert ['t1', 'g1', 'g1.a', 'g1.b'] == tc._filter_tasks(['*1*']) - def testFilterSubtask(self): + def testFilterSubtask(self, tasks_sample): filter_ = ["t1", "g1.b"] - tc = TaskControl(TASKS_SAMPLE) + tc = TaskControl(tasks_sample) assert filter_ == tc._filter_tasks(filter_) - def testFilterTarget(self): - tasks = list(TASKS_SAMPLE) + def testFilterTarget(self, tasks_sample): + tasks = list(tasks_sample) tasks.append(Task("tX", [""],[],["targetX"])) tc = TaskControl(tasks) assert ['tX'] == tc._filter_tasks(["targetX"]) @@ -184,8 +185,8 @@ def test_filter_delayed_regex_auto(self): # filter a non-existent task raises an error - def testFilterWrongName(self): - tc = TaskControl(TASKS_SAMPLE) + def testFilterWrongName(self, tasks_sample): + tc = TaskControl(tasks_sample) pytest.raises(InvalidCommand, tc._filter_tasks, ['no']) def testFilterWrongSubtaskName(self): @@ -194,19 +195,19 @@ def testFilterWrongSubtaskName(self): tc = TaskControl([t1, t2]) pytest.raises(InvalidCommand, tc._filter_tasks, ['taskX:no']) - def testFilterEmptyList(self): + def testFilterEmptyList(self, tasks_sample): filter_ = [] - tc = TaskControl(TASKS_SAMPLE) + tc = TaskControl(tasks_sample) assert filter_ == tc._filter_tasks(filter_) - def testOptions(self): + def testOptions(self, tasks_sample): options = ["t3", "--message", "hello option!", "t1"] - tc = TaskControl(TASKS_SAMPLE) + tc = TaskControl(tasks_sample) assert ['t3', 't1'] == tc._filter_tasks(options) assert "hello option!" == tc.tasks['t3'].options['opt1'] - def testPosParam(self): - tasks = list(TASKS_SAMPLE) + def testPosParam(self, tasks_sample): + tasks = list(tasks_sample) tasks.append(Task("tP", [""],[],[], pos_arg='myp')) tc = TaskControl(tasks) args = ["tP", "hello option!", "t1"] diff --git a/tests/test_task.py b/tests/test_task.py index ab6ddc69..8b1c8f3b 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -118,6 +118,29 @@ def test_options(self): assert 'pos' == t.pos_arg assert None == t.pos_arg_val # always uninitialized + def test_options_from_cfg(self): + 'Ensure that doit.cfg can specify task options.' + p1 = {'name': 'x', 'long': 'x', 'default': None} + t = task.Task("MyName", None, params=[p1]) + t.cfg_values = {'x': 1} + assert t.options is None + t.init_options() + assert t.options is not None + assert 1 == t.options['x'] + + def test_options_from_cfg_override(self): + 'Ensure that doit.cfg specified task options can be replaced by command line specified options.' + + p1 = {'name': 'x', 'long': 'x', 'default': None, 'type': int} + p2 = {'name': 'y', 'long': 'y', 'default': 2, 'type': int} + t = task.Task("MyName", None, params=[p1, p2]) + t.cfg_values = {'x': 1} + assert t.options is None + t.init_options(['--x=2']) + assert t.options is not None + assert 2 == t.options['x'] + assert 2 == t.options['y'] + def test_setup(self): t = task.Task("task5", ['action'], setup=["task2"]) assert ["task2"] == t.setup_tasks