.. _task-namespaces: ======================= Constructing namespaces ======================= The :doc:`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 :ref:`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`. `~.Collection.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 <.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 `~.Collection.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 <.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: 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 :ref:`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 <.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 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.