Extending doit

doit is built to be extended and this can be done in several levels. So far we have seen:

  1. User’s can create new ways to define when a task is up-to-date using the uptodate task parameter (more)

  2. You can customize how tasks are executed by creating new Action types (more)

  3. Tasks can be created in different styles by creating custom task creators (more)

  4. The output can be configured by creating custom reports (more)

Apart from those, doit also provides a plugin system and expose it’s internal API so you can create new applications on top of doit.

task loader customization

The task loader controls the source/creation of tasks. Normally doit tasks are defined in a dodo.py file. This file is loaded, and the list of tasks is created from the dict containing task meta-data from the task-creator functions.

Subclass TaskLoader2 to create a custom loader:

class doit.cmd_base.TaskLoader2[source]

Interface of task loaders with new-style API.

Variables

cmd_options – (list of dict) see cmdparse.CmdOption for dict format

This API separates the loading of the configuration and the loading of the actual tasks, which enables additional elements to be available during task creation.

load_doit_config()[source]

Load doit configuration.

The method must not be called before invocation of setup.

Returns

(dict) Dictionary of doit configuration values.

load_tasks(cmd, pos_args)[source]

Load tasks.

The method must not be called before invocation of load_doit_config.

Parameters
  • cmd – (doit.cmd_base.Command) current command being executed

  • pos_args – (list str) positional arguments from command line

Returns

(List[Task])

setup(opt_values)[source]

Delayed initialization.

To be implemented if the data is needed by derived classes.

Parameters

opt_values – (dict) with values for cmd_options

Before the introduction of TaskLoader2 a now deprecated loader interface TaskLoader was used, which did not separate the setup, configuration loading and task loading phases explicit.

The main program is implemented in the DoitMain. It’s constructor takes an instance of the task loader to be used.

Example: pre-defined task

In the full example below a application is created where the only task available is defined using a dict (so no dodo.py will be used).

#! /usr/bin/env python3

import sys

from doit.task import dict_to_task
from doit.cmd_base import TaskLoader2
from doit.doit_cmd import DoitMain

my_builtin_task = {
    'name': 'sample_task',
    'actions': ['echo hello from built in'],
    'doc': 'sample doc',
}


class MyLoader(TaskLoader2):
    def setup(self, opt_values):
        pass

    def load_doit_config(self):
        return {'verbosity': 2}

    def load_tasks(self, cmd, pos_args):
        task_list = [dict_to_task(my_builtin_task)]
        return task_list


if __name__ == "__main__":
    sys.exit(DoitMain(MyLoader()).run(sys.argv[1:]))

Example: load tasks from a module

The ModuleTaskLoader can be used to load tasks from a specified module, where this module specifies tasks in the same way as in dodo.py. ModuleTaskLoader is included in doit source.

#! /usr/bin/env python3

import sys

from doit.cmd_base import ModuleTaskLoader
from doit.doit_cmd import DoitMain

if __name__ == "__main__":
    import my_module_with_tasks
    sys.exit(DoitMain(ModuleTaskLoader(my_module_with_tasks)).run(sys.argv[1:]))

ModuleTaskLoader can take also take a dict where its items are functions or methods of an object.

command customization

In doit a command usually perform some kind of operations on tasks. run to execute tasks, list to display available tasks, etc.

Most of the time you should really be creating tasks but when developing a custom application on top of doit it may make sense to provide some extra commands…

To create a new command, subclass doit.cmd_base.Command set some class variables and implement the execute method.

class doit.cmd_base.Command(config=None, bin_name='doit', opt_vals=None, **kwargs)[source]

third-party should subclass this for commands that do no use tasks

Variables
  • name – (str) name of sub-cmd to be use from cmdline

  • doc_purpose – (str) single line cmd description

  • doc_usage – (str) describe accepted parameters

  • doc_description – (str) long description/help for cmd

  • cmd_options – (list of dict) see cmdparse.CmdOption for dict format

execute(opt_values, pos_args)[source]

execute command :param opt_values: (dict) with cmd_options values :param pos_args: (list) of cmd-line positional arguments

cmd_options uses the same format as task parameters.

If the command needs to access tasks it should sub-class doit.cmd_base.DoitCmdBase.

Example: scaffolding

A common example is applications that provide some kind of scaffolding when creating new projects.

from doit.cmd_base import Command


class Init(Command):
    doc_purpose = 'create a project scaffolding'
    doc_usage = ''
    doc_description = """This is a multiline command description.
It will be displayed on `doit help init`"""

    def execute(self, opt_values, pos_args):
        print("TODO: create some files for my project")

plugins

doit plugin system is based on the use of entry points, the plugin does not need to implement any kind of “plugin interface”. It needs only to implement the API of the component it is extending.

Plugins can be enabled in 2 different ways:

  • local plugins are enabled through the doit.cfg file.

  • plugins installed with setuptools (that provide an entry point), are automatically enabled on installation.

Check this sample plugin for details on how to create a plugin.

config plugin

To enable a plugin create a section named after the plugin category. The value is an entry point to the python class/function/object that implements the plugin. The format is <module-name>:<attribute-name>.

Example of command plugin configured in pyproject.toml, implemented in the class FooCmd, located at the module my_plugins.py:

[tool.doit.plugins.command]
foo = "my_plugins:FooCmd"

Similarly, in doit.cfg:

[COMMAND]
foo = my_plugins:FooCmd

Note

The python module containing the plugin must be in the PYTHONPATH.

category COMMAND

Creates a new sub-command. Check command section for details on how to create a new command.

category BACKEND

Implements the internal doit DB storage system. Check the module doit/dependency.py to see the existing implementation / API.

category REPORTER

Register a custom reporter as introduced in the custom reporter section.

category LOADER

Creates a custom task loader. Check loader section for details on how to create a new command.

Apart from getting the plugin you also need to indicate which loader will be used. In the tool.doit section in pyproject.toml:

[tool.doit]
loader = "my_loader"

[tool.doit.plugins.loader]
my_loader = "my_plugins:Loader"

In the GLOBAL section of doit.cfg:

[GLOBAL]
loader = my_loader

[LOADER]
my_loader = my_plugins:MyLoader