User guide


In ActionTree, you create the graph of the actions to be executed and then call the execute method of its root.

For example, let’s say you want to generate three files, and then concatenate them to a fourth file.

Let’s start by the utility functions, not related to ActionTree:

>>> def create_file(name):
...   with open(name, "w") as f:
...     f.write("This is {}\\n".format(name))
>>> def concat_files(files, name):
...   with open(name, "w") as output:
...     for file in files:
...       with open(file) as input:
...         output.write(

Then, here is how you use them with ActionTree. Import it:

>>> from ActionTree import *

Create the graph of actions:

>>> from functools import partial
>>> concat = Action(partial(concat_files, ["first", "second", "third"], "fourth"), "concat")
>>> concat.add_dependency(Action(partial(create_file, "first"), "create first"))
>>> concat.add_dependency(Action(partial(create_file, "second"), "create second"))
>>> concat.add_dependency(Action(partial(create_file, "third"), "create third"))

Execute the actions:

>>> concat.execute()

You have no guaranty about the order of execution of the create_file actions, but you are sure that they are all finished before the concat_files action starts.

You can execute them in parallel, keeping the same guaranties:

>>> concat.execute(jobs=3)

Specialized actions

Action accepts a callable in its constructor to be usable without subclassing, but it’s also easy to specialize. The previous example could be rewriten like:

>>> class CreateFile(Action):
...   def __init__(self, name):
...     super(CreateFile, self).__init__(self.__create, "create {}".format(name))
...     self.__name = name
...   def __create(self):
...     with open(self.__name, "w") as f:
...       f.write("This is {}\\n".format(self.__name))
>>> class ConcatFiles(Action):
...   def __init__(self, files, name):
...     super(ConcatFiles, self).__init__(self.__concat, "concat")
...     self.__files = files
...     self.__name = name
...   def __concat(self):
...     with open(self.__name, "w") as output:
...       for file in self.__files:
...         with open(file) as input:
...           output.write(
>>> concat = ConcatFiles(["first", "second", "third"], "fourth")
>>> concat.add_dependency(CreateFile("first"))
>>> concat.add_dependency(CreateFile("second"))
>>> concat.add_dependency(CreateFile("third"))
>>> concat.execute()


If you just want to know what would be done, use Action.get_preview():

>>> concat.get_preview()
['create ...', 'create ...', 'create ...', 'concat']

As said earlier, you have no guaranty about the order of the first three actions, so get_preview() returns one possible order.

The values returned by get_preview() are the labels passed in the constructor of Action, so they can be anything you want, not just strings.

Stock actions

ActionTree is shipped with some stock actions for common tasks.

Say you want to compile two C++ files and link them:

>>> from ActionTree.stock import CallSubprocess
>>> link = CallSubprocess(["g++", "-o", "test", "a.o", "b.o"])
>>> link.add_dependency(
...   CallSubprocess(["g++", "-c", "doc/a.cpp", "-o", "a.o"])
... )
>>> link.add_dependency(
...   CallSubprocess(["g++", "-c", "doc/b.cpp", "-o", "b.o"])
... )
>>> link.execute(jobs=2)


You can easily draw a graph of your action and its dependencies with DependencyGraph:

>>> from ActionTree.drawings import DependencyGraph
>>> g = DependencyGraph(concat)
>>> g.write_to_png("doc/doctest/concat.png")


You can draw an execution report with ExecutionReport:

>>> from ActionTree.drawings import ExecutionReport
>>> report = ExecutionReport(link)
>>> report.write_to_png("doc/doctest/link_report.png")


And if some action fails, you get:

>>> link.add_dependency(
...   CallSubprocess(["g++", "-c", "doc/c.cpp", "-o", "c.o"])
... )
>>> link.execute(keep_going=True)
Traceback (most recent call last):
CompoundException: [CalledProcessError()]
>>> ExecutionReport(link).write_to_png("doc/doctest/failed_link_report.png")