File: namespaces.rst

package info (click to toggle)
python-invoke 0.11.1%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: buster, stretch
  • size: 1,136 kB
  • ctags: 1,702
  • sloc: python: 5,614; makefile: 37; sh: 36
file content (332 lines) | stat: -rw-r--r-- 9,390 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
=======================
Constructing namespaces
=======================

The :doc:`base case </getting_started>` 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 <collection-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, run

    @task
    def release():
        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_():
        # ...

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 simply say::

        ns.add_task(release, 'deploy')


Aliases
-------

.. FIXME: add back aliases and merge at add_task time, as we do with name. HURR

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.
        
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():
        run("sphinx-build docs docs/_build")

    @task
    def clean_docs():
        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, run

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

And ``tasks/docs.py``::

    from invoke import task, run

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

    @task
    def clean():
        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():
        # ...

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`` -- simply 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.