Decorators

A Decorator is a class that alters the workings of a Dodo Command script by extending or modifying the arguments that are passed to Dodo.run. For this purpose, the arguments for Dodo.run are constructed as a tree. Initially, this tree only has a root node that holds all arguments. Each decorator may restructure the argument tree by adding new tree nodes and changing existing nodes. Dodo Commands obtains the final list of arguments by flattening the tree in a pre-order fashion. This allows each decorator to prepend or append arguments, or completely rewrite the tree.

Constructing a tree

Each node in the tree is of type ArgsTreeNode. A node has attributes args (these are the command line arguments) and is_horizontal (this flag determines how the arguments are printed when the --echo or --confirm flag is used). To add a child node, call node.add_child().

Prepending an argument

The following example shows a decorator that prepends the arguments with the path to a debugger executable. The decorator should be placed in a decorators directory inside a commands directory:

# file: my_commands/decorators/debugger.py

class Decorator:  # noqa
    def add_arguments(self, parser):  # noqa
        parser.add_argument(
            '--use-debugger',
            action='store_true',
            default=False,
            help="Run the command through the debugger"
        )

    def modify_args(self, dodo_args, root_node, cwd):  # noqa
        if not getattr(dodo_args, 'use_debugger', False):
            return root_node, cwd

        # Create a new root node with just the path to the debugger
        # Note that "debugger" is a name tag which is only used internally
        # to identify nodes in the tree.
        debugger_node = ArgsTreeNode(
            "debugger", args=[Dodo.get_config('/BUILD/debugger')]
        )

        # Since we want to make the debugger path a prefix, we add the
        # original arguments as a child node. When the tree is flattened
        # in a pre-order fashion, this will give the correct result.
        debugger_node.add_child(root_node)
        return debugger_node, cwd

Note that the decorator returns both the new argument tree and a current working directory. This means that it’s possible to change the current working directory for the decorated command as well.

Appending an argument

This is similar to prepending, except that we do not need to create a new node

# file: my_commands/decorators/foo.py

class Decorator:  # noqa
    def modify_args(self, dodo_args, root_node, cwd):  # noqa
        root_node.args.append('--foo')
        return root_node, cwd

Mapping decorators to commands

Not all decorators are compatible with all commands. For example, only some commands can be run inside a debugger. Therefore, the configuration contains a list of decorated command for each decorator. In this list, wildcards are allowed, and you can exclude commands by prefixing them with an exclamation mark:

ROOT:
  decorators:
    # Use a wildcard to decorate all commands, but exclude the foo command
    debugger: ['*', '!foo']
    # The cmake and runserver scripts can be run inside docker
    docker: ['cmake', 'runserver']

Using DecoratorScope in scripts

The DecoratorScope context manager makes it possible to enforce the use of a decorator inside a section of the the command script:

from dodo_commands import DecoratorScope

with DecoratorScope("docker"):
    # This command will run inside docker
    Dodo.run(["ls"])

It’s also possible to force a decorator to be disabled. In this case, even if the command was decorated, the Dodo.run calls inside DecoratorScope will not use the decorator:

from dodo_commands import DecoratorScope

with DecoratorScope("docker", remove=True):
    # This command will not run inside docker
    Dodo.run(["ls"])

Printing arguments

The structure of the argument tree determines how arguments are printed when the --echo or --confirm flag is used. We’ve seen above that nodes in the tree are created with the ArgsTreeNode constructor. The arguments in this node are indented in correspondence to the node’s depth in the tree. The ArgsTreeNode constructor takes an optional argument is_horizontal that determines if arguments are printed horizontally or vertically, e.g.

docker_node = ArgsTreeNode("docker", args=['docker', 'run'])
tty_node = ArgsTreeNode(
    ["tty", args=['--rm', '--interactive', '--tty'],
    is_horizontal=True
)
docker_node.add_child(tty_node)

# add more nodes to the tree...
# assume cmake is decorated with the docker decorator
dodo cmake --echo

produces

docker run  \
  --rm --interactive --tty  \
  --name=cmake  \
  dodo_tutorial:1604  \
  cmake -DCMAKE_BUILD_TYPE=release /home/maarten/projects/dodo_tutorial/src