summaryrefslogtreecommitdiff
path: root/python/mach/docs/commands.rst
blob: af2973dd7e795a06e699c927b3d526624ac3a0b0 (plain)
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
.. _mach_commands:

=====================
Implementing Commands
=====================

Mach commands are defined via Python decorators.

All the relevant decorators are defined in the *mach.decorators* module.
The important decorators are as follows:

:py:func:`CommandProvider <mach.decorators.CommandProvider>`
  A class decorator that denotes that a class contains mach
  commands. The decorator takes no arguments.

:py:func:`Command <mach.decorators.Command>`
  A method decorator that denotes that the method should be called when
  the specified command is requested. The decorator takes a command name
  as its first argument and a number of additional arguments to
  configure the behavior of the command.

:py:func:`CommandArgument <mach.decorators.CommandArgument>`
  A method decorator that defines an argument to the command. Its
  arguments are essentially proxied to ArgumentParser.add_argument()

:py:func:`SubCommand <mach.decorators.SubCommand>`
  A method decorator that denotes that the method should be a
  sub-command to an existing ``@Command``. The decorator takes the
  parent command name as its first argument and the sub-command name
  as its second argument.

  ``@CommandArgument`` can be used on ``@SubCommand`` instances just
  like they can on ``@Command`` instances.

Classes with the ``@CommandProvider`` decorator **must** have an
``__init__`` method that accepts 1 or 2 arguments. If it accepts 2
arguments, the 2nd argument will be a
:py:class:`mach.base.CommandContext` instance.

Here is a complete example:

.. code-block:: python

   from mach.decorators import (
       CommandArgument,
       CommandProvider,
       Command,
   )

   @CommandProvider
   class MyClass(object):
       @Command('doit', help='Do ALL OF THE THINGS.')
       @CommandArgument('--force', '-f', action='store_true',
           help='Force doing it.')
       def doit(self, force=False):
           # Do stuff here.

When the module is loaded, the decorators tell mach about all handlers.
When mach runs, it takes the assembled metadata from these handlers and
hooks it up to the command line driver. Under the hood, arguments passed
to the decorators are being used to help mach parse command arguments,
formulate arguments to the methods, etc. See the documentation in the
:py:mod:`mach.base` module for more.

The Python modules defining mach commands do not need to live inside the
main mach source tree.

Conditionally Filtering Commands
================================

Sometimes it might only make sense to run a command given a certain
context. For example, running tests only makes sense if the product
they are testing has been built, and said build is available. To make
sure a command is only runnable from within a correct context, you can
define a series of conditions on the
:py:func:`Command <mach.decorators.Command>` decorator.

A condition is simply a function that takes an instance of the
:py:func:`mach.decorators.CommandProvider` class as an argument, and
returns ``True`` or ``False``. If any of the conditions defined on a
command return ``False``, the command will not be runnable. The
docstring of a condition function is used in error messages, to explain
why the command cannot currently be run.

Here is an example:

.. code-block:: python

   from mach.decorators import (
       CommandProvider,
       Command,
   )

   def build_available(cls):
       """The build needs to be available."""
       return cls.build_path is not None

    @CommandProvider
   class MyClass(MachCommandBase):
       def __init__(self, build_path=None):
           self.build_path = build_path

       @Command('run_tests', conditions=[build_available])
       def run_tests(self):
           # Do stuff here.

It is important to make sure that any state needed by the condition is
available to instances of the command provider.

By default all commands without any conditions applied will be runnable,
but it is possible to change this behaviour by setting
``require_conditions`` to ``True``:

.. code-block:: python

   m = mach.main.Mach()
   m.require_conditions = True

Minimizing Code in Commands
===========================

Mach command modules, classes, and methods work best when they are
minimal dispatchers. The reason is import bloat. Currently, the mach
core needs to import every Python file potentially containing mach
commands for every command invocation. If you have dozens of commands or
commands in modules that import a lot of Python code, these imports
could slow mach down and waste memory.

It is thus recommended that mach modules, classes, and methods do as
little work as possible. Ideally the module should only import from
the :py:mod:`mach` package. If you need external modules, you should
import them from within the command method.

To keep code size small, the body of a command method should be limited
to:

1. Obtaining user input (parsing arguments, prompting, etc)
2. Calling into some other Python package
3. Formatting output

Of course, these recommendations can be ignored if you want to risk
slower performance.

In the future, the mach driver may cache the dispatching information or
have it intelligently loaded to facilitate lazy loading.