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 TaskLoader to create a custom loader:

class doit.cmd_base.TaskLoader[source]

task-loader interface responsible of creating Task objects

Subclasses must implement the method load_tasks

Variables:cmd_options – (list of dict) see cmdparse.CmdOption for dict format
load_tasks(cmd, opt_values, pos_args)[source]

load tasks and DOIT_CONFIG

Returns:

(tuple) list of Task, dict with DOIT_CONFIG options

Parameters:
  • cmd – (doit.cmd_base.Command) current command being executed
  • opt_values – (dict) with values for cmd_options
  • pos_args – (list str) positional arguments from command line

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 python

import sys

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

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

class MyLoader(TaskLoader):
    @staticmethod
    def load_tasks(cmd, opt_values, pos_args):
        task_list = [dict_to_task(my_builtin_task)]
        config = {'verbosity': 2}
        return task_list, config


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 python

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, **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 positinal 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 __future__ import print_function
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 implemented in the class FooCmd, located at the module my_plugins.py:

[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 GLOBAL section of your config file.

[GLOBAL]
loader = my_loader

[LOADER]
my_loader = my_plugins:MyLoader