User-defined Python#

While the baseline doit and doitoml features work very well for running well-known CLI functions with tokens or raw shell actions, sometimes a little programmability can go a long, portable way.

Use a Python function as an action#

The py action offers user-defined importable Python names for actions, using the entry_point notation of module.submodule:function.

The imported function must return None or True on success. A return value of False, or any raised Exception, is considered a failure, and any other return value is an error.

Many Python stdlib functions that accept simple, JSON-compatible values work out of the box.

[tool.doitoml.tasks.copy]
actions = [
    {py = {"shutil:copytree" = {kwargs = {src = "from/path/", dst = "to/path"} } } }
]
{
  "doitoml": {
    "tasks": {
      "copy": {
        "actions": {
          "py": {
            "shutil:copytree": {
              "kwargs": {"src": "from/path/", "dst": "to/path/"}
            }
          }
        }
      }
    }
  }
}

Use a Python function as an up-to-date checker#

When capturing paths in file_dep and targets, or the various built-in doit.tools functions like config_changed, run_once are insufficient, custom python functions may also be used as up-to-date checkers.

For example, with a project layout like…

./
  ├─ pyproject.toml
  └─ my_checkers.py

…where some checkers are defined in my_checkers.py:

# my_checkers.py
from datetime import datetime

def is_weekend():
    return datetime.now().isoweekday() > 5

…a task can reference this checker in uptodate:

# child/pyproject.toml
[tool.doitoml.tasks.greet]
uptodate = [{py={"my_checkers:is_weekend" = {}}}]
actions = [["echo", "hello", "weekday"]]

Finding importable functions#

By default, doit will put the current working directory on Python’s sys.path, meaning any importable name will be available.

Warning

Be careful with naming modules in a way that would overload Python’s standard library!

For example, with a simple project layout like:

./
  ├─ pyproject.toml
  └─ my_actions.py

A where my_actions.py defines one function:

# my_actions.py
def greet(greeting: str, *greeted: List[str]):
    print(greeting, *greeted)

Can be referenced as:

# pyproject.toml
[tool.doitoml.tasks.greet]
actions = [
    {py = {"my_actions:greet" = {args = ["hello", "world"] } } }
]

sys.path#

In more complex projects, the simple path hack may not be sufficient, and can be further customized by prepending the importable name with an additional {path}:.

For example, with a project layout like:

./
  ├─ pyproject.toml
  ├─ child/
  │  └─ pyproject.toml
  └─ my_actions
      ├─ __init__.py
      └─ greetings.py

The pyproject.toml in the child directory can extend sys.path to find the greetings module:

# child/pyproject.toml
[tool.doitoml.tasks.greet]
actions = [
    {py = {"../my_actions:greetings:greet" = {args = ["hello", "world"] } } }
]

Importing dodo#

In a project with doit’s default dodo.py layout, the dodo module itself can be imported…

# pyproject.toml
[tool.doitoml.tasks.greet]
actions = [
  { py = {"dodo:greet" = { kwargs = { whom = "world" } } } }
]

[tool.doitoml.tasks.greet]
actions = [{ py = {"dodo:dump" = { } } }]

… and even explore a DoiTOML instance.

# dodo.py
from doitoml import DoiTOML
doitoml = DoiTOML()
globals().update(doitoml.tasks())

def greet(whom):
    print(f"Hello {whom}")
    return True

def dump():
    from pygments import highlight
    from pygments.lexers import YamlLexer
    from pygments.formatters import TerminalFormatter
    from yaml import safe_dump
    print(
        highlight(
            safe_dump(doitoml.config.to_dict()),
            YamlLexer(),
            TerminalFormatter(bg="dark")
        )
    )