Constructing namespaces

The base case of loading a single module of tasks works fine initially, but advanced users typically need more organization, such as separating tasks into a tree of nested namespaces.

The Collection class provides an API for organizing tasks (and their configuration) into a tree-like structure. When referenced by strings (e.g. on the CLI or in pre/post hooks) tasks in nested namespaces use a dot-separated syntax, e.g. docs.build.

In this section, we show how building namespaces with this API is flexible but also allows following Python package layouts with minimal boilerplate.

Starting out

One unnamed Collection is always the namespace root; in the implicit base case, Invoke creates one for you from the tasks in your tasks module. Create your own, named namespace or ns, to set up an explicit namespace (i.e. to skip the default “pull in all Task objects” behavior):

from invoke import Collection

ns = Collection()
# or: namespace = Collection()

Add tasks with Collection.add_task. add_task can take an Task object, such as those generated by the task decorator:

from invoke import Collection, task

@task
def release(c):
    c.run("python setup.py sdist register upload")

ns = Collection()
ns.add_task(release)

Our available tasks list now looks like this:

$ invoke --list
Available tasks:

    release

Naming your tasks

By default, a task’s function name is used as its namespace identifier, but you may override this by giving a name argument to either @task (i.e. at definition time) or Collection.add_task (i.e. at binding/attachment time).

For example, say you have a variable name collision in your tasks module – perhaps you want to expose a dir task, which shadows a Python builtin. Naming your function itself dir is a bad idea, but you can name the function something like dir_ and then tell @task the “real” name:

@task(name='dir')
def dir_(c):
    # ...

On the other side, you might have obtained a task object that doesn’t fit with the names you want in your namespace, and can rename it at attachment time. Maybe we want to rename our release task to be called deploy instead:

ns = Collection()
ns.add_task(release, name='deploy')

The result:

$ invoke --list
Available tasks:

    deploy

Note

The name kwarg is the 2nd argument to add_task, so those in a hurry can phrase it as:

ns.add_task(release, 'deploy')

Aliases

Tasks may have additional names or aliases, given as the aliases keyword argument; these are appended to, instead of replacing, any implicit or explicit name value:

ns.add_task(release, aliases=('deploy', 'pypi'))

Result, with three names for the same task:

$ invoke --list
Available tasks:

    release
    deploy
    pypi

Note

The convenience decorator @task is another method of setting aliases (e.g. @task(aliases=('foo', 'bar')), and is useful for ensuring a given task always has some aliases set no matter how it’s added to a namespace.

Dashes vs underscores

In the common case of functions-as-tasks, you’ll often find yourself writing task names that contain underscores:

@task
def my_awesome_task(c):
    print("Awesome!")

Similar to how task arguments are processed to turn their underscores into dashes (since that’s a common command-line convention) all underscores in task or collection names are interpreted to be dashes instead, by default:

$ inv --list
Available tasks:

  my-awesome-task

$ inv my-awesome-task
Awesome!

If you’d prefer the underscores to remain instead, you can update your configuration to set tasks.auto_dash_names to False in one of the non-runtime config files (system, user, or project.) For example, in ~/.invoke.yml:

tasks:
    auto_dash_names: false

Note

In the interests of avoiding confusion, this setting is “exclusive” in nature - underscored version of task names are not valid on the CLI unless auto_dash_names is disabled. (However, at the pure function level within Python, they must continue to be referenced with underscores, as dashed names are not valid Python syntax!)

Nesting collections

The point of namespacing is to have sub-namespaces; to do this in Invoke, create additional Collection instances and add them to their parent collection via Collection.add_collection. For example, let’s say we have a couple of documentation tasks:

@task
def build_docs(c):
    c.run("sphinx-build docs docs/_build")

@task
def clean_docs(c):
    c.run("rm -rf docs/_build")

We can bundle them up into a new, named collection like so:

docs = Collection('docs')
docs.add_task(build_docs, 'build')
docs.add_task(clean_docs, 'clean')

And then add this new collection under the root namespace with add_collection:

ns.add_collection(docs)

The result (assuming for now that ns currently just contains the original release task):

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

As with tasks, collections may be explicitly bound to their parents with a different name than they were originally given (if any) via a name kwarg (also, as with add_task, the 2nd regular arg):

ns.add_collection(docs, 'sphinx')

Result:

$ invoke --list
Available tasks:

    release
    sphinx.build
    sphinx.clean

Importing modules as collections

A simple tactic which Invoke itself uses in the trivial, single-module case is to use Collection.from_module – a classmethod serving as an alternate Collection constructor which takes a Python module object as its first argument.

Modules given to this method are scanned for Task instances, which are added to a new Collection. By default, this collection’s name is taken from the module name (the __name__ attribute), though it can also be supplied explicitly.

Note

As with the default task module, you can override this default loading behavior by declaring a ns or namespace Collection object at top level in the loaded module.

For example, let’s reorganize our earlier single-file example into a Python package with several submodules. First, tasks/release.py:

from invoke import task

@task
def release(c):
    c.run("python setup.py sdist register upload")

And tasks/docs.py:

from invoke import task

@task
def build(c):
    c.run("sphinx-build docs docs/_build")

@task
def clean(c):
    c.run("rm -rf docs/_build")

Tying them together is tasks/__init__.py:

from invoke import Collection

import release, docs

ns = Collection()
ns.add_collection(Collection.from_module(release))
ns.add_collection(Collection.from_module(docs))

This form of the API is a little unwieldy in practice. Thankfully there’s a shortcut: add_collection will notice when handed a module object as its first argument and call Collection.from_module for you internally:

ns = Collection()
ns.add_collection(release)
ns.add_collection(docs)

Either way, the result:

$ invoke --list
Available tasks:

    release.release
    docs.build
    docs.clean

Default tasks

Tasks may be declared as the default task to invoke for the collection they belong to, e.g. by giving default=True to @task (or to Collection.add_task.) This is useful when you have a bunch of related tasks in a namespace but one of them is the most commonly used, and maps well to the namespace as a whole.

For example, in the documentation submodule we’ve been experimenting with so far, the build task makes sense as a default, so we can say things like invoke docs as a shortcut to invoke docs.build. This is easy to do:

@task(default=True)
def build(c):
    # ...

When imported into the root namespace (as shown above) this alters the output of --list, highlighting the fact that docs.build can be invoked as docs if desired:

$ invoke --list
Available tasks:

    release.release
    docs.build (docs)
    docs.clean

Default subcollections

As of version 1.5, this functionality is also extended to subcollections: a subcollection can be specified as the default when being added to its parent collection, and that subcollection’s own default task (or sub-subcollection!) will be invoked as the default for the parent.

An example probably makes that clearer. Here’s a tiny inline task tree with two subcollections, each with their own default task:

from invoke import Collection, task

@task(default=True)
def build_all(c):
    print("build ALL THE THINGS!")

@task
def build_wheel(c):
    print("Just the wheel")

build = Collection(all=build_all, wheel=build_wheel)

@task(default=True)
def build_docs(c):
    print("Code without docs is no code at all")

docs = Collection(build_docs)

Then we tie those into one top level collection, setting the build subcollection as the overall default:

ns = Collection()
ns.add_collection(build, default=True)
ns.add_collection(docs)

The result is that build.all becomes the absolute default task:

$ invoke
build ALL THE THINGS!

Mix and match

You’re not limited to the specific tactics shown above – now that you know the basic tools of add_task and add_collection, use whatever approach best fits your needs.

For example, let’s say you wanted to keep things organized into submodules, but wanted to “promote” release.release back to the top level for convenience’s sake. Just because it’s stored in a module doesn’t mean we must use add_collection – we could instead import the task itself and use add_task directly:

from invoke import Collection

import docs
from release import release

ns = Collection()
ns.add_collection(docs)
ns.add_task(release)

Result:

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

More shortcuts

Finally, you can even skip add_collection and add_task if your needs are simple enough – Collection’s constructor will take unknown arguments and build the namespace from their values as appropriate:

from invoke import Collection

import docs, release

ns = Collection(release.release, docs)

Notice how we gave both a task object (release.release) and a module containing tasks (docs). The result is identical to the above:

$ invoke --list
Available tasks:

    release
    docs.build
    docs.clean

If given as keyword arguments, the keywords act like the name arguments do in the add_* methods. Naturally, both can be mixed together as well:

ns = Collection(docs, deploy=release.release)

Result:

$ invoke --list
Available tasks:

    deploy
    docs.build
    docs.clean

Note

You can still name these Collection objects with a leading string argument if desired, which can be handy when building sub-collections.