|
1 ==== |
|
2 mach |
|
3 ==== |
|
4 |
|
5 Mach (German for *do*) is a generic command dispatcher for the command |
|
6 line. |
|
7 |
|
8 To use mach, you install the mach core (a Python package), create an |
|
9 executable *driver* script (named whatever you want), and write mach |
|
10 commands. When the *driver* is executed, mach dispatches to the |
|
11 requested command handler automatically. |
|
12 |
|
13 Features |
|
14 ======== |
|
15 |
|
16 On a high level, mach is similar to using argparse with subparsers (for |
|
17 command handling). When you dig deeper, mach offers a number of |
|
18 additional features: |
|
19 |
|
20 Distributed command definitions |
|
21 With optparse/argparse, you have to define your commands on a central |
|
22 parser instance. With mach, you annotate your command methods with |
|
23 decorators and mach finds and dispatches to them automatically. |
|
24 |
|
25 Command categories |
|
26 Mach commands can be grouped into categories when displayed in help. |
|
27 This is currently not possible with argparse. |
|
28 |
|
29 Logging management |
|
30 Mach provides a facility for logging (both classical text and |
|
31 structured) that is available to any command handler. |
|
32 |
|
33 Settings files |
|
34 Mach provides a facility for reading settings from an ini-like file |
|
35 format. |
|
36 |
|
37 Components |
|
38 ========== |
|
39 |
|
40 Mach is conceptually composed of the following components: |
|
41 |
|
42 core |
|
43 The mach core is the core code powering mach. This is a Python package |
|
44 that contains all the business logic that makes mach work. The mach |
|
45 core is common to all mach deployments. |
|
46 |
|
47 commands |
|
48 These are what mach dispatches to. Commands are simply Python methods |
|
49 registered as command names. The set of commands is unique to the |
|
50 environment mach is deployed in. |
|
51 |
|
52 driver |
|
53 The *driver* is the entry-point to mach. It is simply an executable |
|
54 script that loads the mach core, tells it where commands can be found, |
|
55 then asks the mach core to handle the current request. The driver is |
|
56 unique to the deployed environment. But, it's usually based on an |
|
57 example from this source tree. |
|
58 |
|
59 Project State |
|
60 ============= |
|
61 |
|
62 mach was originally written as a command dispatching framework to aid |
|
63 Firefox development. While the code is mostly generic, there are still |
|
64 some pieces that closely tie it to Mozilla/Firefox. The goal is for |
|
65 these to eventually be removed and replaced with generic features so |
|
66 mach is suitable for anybody to use. Until then, mach may not be the |
|
67 best fit for you. |
|
68 |
|
69 Implementing Commands |
|
70 --------------------- |
|
71 |
|
72 Mach commands are defined via Python decorators. |
|
73 |
|
74 All the relevant decorators are defined in the *mach.decorators* module. |
|
75 The important decorators are as follows: |
|
76 |
|
77 CommandProvider |
|
78 A class decorator that denotes that a class contains mach |
|
79 commands. The decorator takes no arguments. |
|
80 |
|
81 Command |
|
82 A method decorator that denotes that the method should be called when |
|
83 the specified command is requested. The decorator takes a command name |
|
84 as its first argument and a number of additional arguments to |
|
85 configure the behavior of the command. |
|
86 |
|
87 CommandArgument |
|
88 A method decorator that defines an argument to the command. Its |
|
89 arguments are essentially proxied to ArgumentParser.add_argument() |
|
90 |
|
91 Classes with the *@CommandProvider* decorator *must* have an *__init__* |
|
92 method that accepts 1 or 2 arguments. If it accepts 2 arguments, the |
|
93 2nd argument will be a *MachCommandContext* instance. This is just a named |
|
94 tuple containing references to objects provided by the mach driver. |
|
95 |
|
96 Here is a complete example:: |
|
97 |
|
98 from mach.decorators import ( |
|
99 CommandArgument, |
|
100 CommandProvider, |
|
101 Command, |
|
102 ) |
|
103 |
|
104 @CommandProvider |
|
105 class MyClass(object): |
|
106 @Command('doit', help='Do ALL OF THE THINGS.') |
|
107 @CommandArgument('--force', '-f', action='store_true', |
|
108 help='Force doing it.') |
|
109 def doit(self, force=False): |
|
110 # Do stuff here. |
|
111 |
|
112 When the module is loaded, the decorators tell mach about all handlers. |
|
113 When mach runs, it takes the assembled metadata from these handlers and |
|
114 hooks it up to the command line driver. Under the hood, arguments passed |
|
115 to the decorators are being used to help mach parse command arguments, |
|
116 formulate arguments to the methods, etc. See the documentation in the |
|
117 *mach.base* module for more. |
|
118 |
|
119 The Python modules defining mach commands do not need to live inside the |
|
120 main mach source tree. |
|
121 |
|
122 Conditionally Filtering Commands |
|
123 -------------------------------- |
|
124 |
|
125 Sometimes it might only make sense to run a command given a certain |
|
126 context. For example, running tests only makes sense if the product |
|
127 they are testing has been built, and said build is available. To make |
|
128 sure a command is only runnable from within a correct context, you can |
|
129 define a series of conditions on the *Command* decorator. |
|
130 |
|
131 A condition is simply a function that takes an instance of the |
|
132 *CommandProvider* class as an argument, and returns True or False. If |
|
133 any of the conditions defined on a command return False, the command |
|
134 will not be runnable. The doc string of a condition function is used in |
|
135 error messages, to explain why the command cannot currently be run. |
|
136 |
|
137 Here is an example: |
|
138 |
|
139 from mach.decorators import ( |
|
140 CommandProvider, |
|
141 Command, |
|
142 ) |
|
143 |
|
144 def build_available(cls): |
|
145 """The build needs to be available.""" |
|
146 return cls.build_path is not None |
|
147 |
|
148 @CommandProvider |
|
149 class MyClass(MachCommandBase): |
|
150 def __init__(self, build_path=None): |
|
151 self.build_path = build_path |
|
152 |
|
153 @Command('run_tests', conditions=[build_available]) |
|
154 def run_tests(self): |
|
155 # Do stuff here. |
|
156 |
|
157 It is important to make sure that any state needed by the condition is |
|
158 available to instances of the command provider. |
|
159 |
|
160 By default all commands without any conditions applied will be runnable, |
|
161 but it is possible to change this behaviour by setting *require_conditions* |
|
162 to True: |
|
163 |
|
164 m = mach.main.Mach() |
|
165 m.require_conditions = True |
|
166 |
|
167 Minimizing Code in Commands |
|
168 --------------------------- |
|
169 |
|
170 Mach command modules, classes, and methods work best when they are |
|
171 minimal dispatchers. The reason is import bloat. Currently, the mach |
|
172 core needs to import every Python file potentially containing mach |
|
173 commands for every command invocation. If you have dozens of commands or |
|
174 commands in modules that import a lot of Python code, these imports |
|
175 could slow mach down and waste memory. |
|
176 |
|
177 It is thus recommended that mach modules, classes, and methods do as |
|
178 little work as possible. Ideally the module should only import from |
|
179 the *mach* package. If you need external modules, you should import them |
|
180 from within the command method. |
|
181 |
|
182 To keep code size small, the body of a command method should be limited |
|
183 to: |
|
184 |
|
185 1. Obtaining user input (parsing arguments, prompting, etc) |
|
186 2. Calling into some other Python package |
|
187 3. Formatting output |
|
188 |
|
189 Of course, these recommendations can be ignored if you want to risk |
|
190 slower performance. |
|
191 |
|
192 In the future, the mach driver may cache the dispatching information or |
|
193 have it intelligently loaded to facilitate lazy loading. |
|
194 |
|
195 Logging |
|
196 ======= |
|
197 |
|
198 Mach configures a built-in logging facility so commands can easily log |
|
199 data. |
|
200 |
|
201 What sets the logging facility apart from most loggers you've seen is |
|
202 that it encourages structured logging. Instead of conventional logging |
|
203 where simple strings are logged, the internal logging mechanism logs all |
|
204 events with the following pieces of information: |
|
205 |
|
206 * A string *action* |
|
207 * A dict of log message fields |
|
208 * A formatting string |
|
209 |
|
210 Essentially, instead of assembling a human-readable string at |
|
211 logging-time, you create an object holding all the pieces of data that |
|
212 will constitute your logged event. For each unique type of logged event, |
|
213 you assign an *action* name. |
|
214 |
|
215 Depending on how logging is configured, your logged event could get |
|
216 written a couple of different ways. |
|
217 |
|
218 JSON Logging |
|
219 ------------ |
|
220 |
|
221 Where machines are the intended target of the logging data, a JSON |
|
222 logger is configured. The JSON logger assembles an array consisting of |
|
223 the following elements: |
|
224 |
|
225 * Decimal wall clock time in seconds since UNIX epoch |
|
226 * String *action* of message |
|
227 * Object with structured message data |
|
228 |
|
229 The JSON-serialized array is written to a configured file handle. |
|
230 Consumers of this logging stream can just perform a readline() then feed |
|
231 that into a JSON deserializer to reconstruct the original logged |
|
232 message. They can key off the *action* element to determine how to |
|
233 process individual events. There is no need to invent a parser. |
|
234 Convenient, isn't it? |
|
235 |
|
236 Logging for Humans |
|
237 ------------------ |
|
238 |
|
239 Where humans are the intended consumer of a log message, the structured |
|
240 log message are converted to more human-friendly form. This is done by |
|
241 utilizing the *formatting* string provided at log time. The logger |
|
242 simply calls the *format* method of the formatting string, passing the |
|
243 dict containing the message's fields. |
|
244 |
|
245 When *mach* is used in a terminal that supports it, the logging facility |
|
246 also supports terminal features such as colorization. This is done |
|
247 automatically in the logging layer - there is no need to control this at |
|
248 logging time. |
|
249 |
|
250 In addition, messages intended for humans typically prepends every line |
|
251 with the time passed since the application started. |
|
252 |
|
253 Logging HOWTO |
|
254 ------------- |
|
255 |
|
256 Structured logging piggybacks on top of Python's built-in logging |
|
257 infrastructure provided by the *logging* package. We accomplish this by |
|
258 taking advantage of *logging.Logger.log()*'s *extra* argument. To this |
|
259 argument, we pass a dict with the fields *action* and *params*. These |
|
260 are the string *action* and dict of message fields, respectively. The |
|
261 formatting string is passed as the *msg* argument, like normal. |
|
262 |
|
263 If you were logging to a logger directly, you would do something like: |
|
264 |
|
265 logger.log(logging.INFO, 'My name is {name}', |
|
266 extra={'action': 'my_name', 'params': {'name': 'Gregory'}}) |
|
267 |
|
268 The JSON logging would produce something like: |
|
269 |
|
270 [1339985554.306338, "my_name", {"name": "Gregory"}] |
|
271 |
|
272 Human logging would produce something like: |
|
273 |
|
274 0.52 My name is Gregory |
|
275 |
|
276 Since there is a lot of complexity using logger.log directly, it is |
|
277 recommended to go through a wrapping layer that hides part of the |
|
278 complexity for you. The easiest way to do this is by utilizing the |
|
279 LoggingMixin: |
|
280 |
|
281 import logging |
|
282 from mach.mixin.logging import LoggingMixin |
|
283 |
|
284 class MyClass(LoggingMixin): |
|
285 def foo(self): |
|
286 self.log(logging.INFO, 'foo_start', {'bar': True}, |
|
287 'Foo performed. Bar: {bar}') |
|
288 |
|
289 Entry Points |
|
290 ============ |
|
291 |
|
292 It is possible to use setuptools' entry points to load commands |
|
293 directly from python packages. A mach entry point is a function which |
|
294 returns a list of files or directories containing mach command |
|
295 providers. e.g.:: |
|
296 |
|
297 def list_providers(): |
|
298 providers = [] |
|
299 here = os.path.abspath(os.path.dirname(__file__)) |
|
300 for p in os.listdir(here): |
|
301 if p.endswith('.py'): |
|
302 providers.append(os.path.join(here, p)) |
|
303 return providers |
|
304 |
|
305 See http://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins |
|
306 for more information on creating an entry point. To search for entry |
|
307 point plugins, you can call *load_commands_from_entry_point*. This |
|
308 takes a single parameter called *group*. This is the name of the entry |
|
309 point group to load and defaults to ``mach.providers``. e.g.:: |
|
310 |
|
311 mach.load_commands_from_entry_point("mach.external.providers") |
|
312 |
|
313 Adding Global Arguments |
|
314 ======================= |
|
315 |
|
316 Arguments to mach commands are usually command-specific. However, |
|
317 mach ships with a handful of global arguments that apply to all |
|
318 commands. |
|
319 |
|
320 It is possible to extend the list of global arguments. In your |
|
321 *mach driver*, simply call ``add_global_argument()`` on your |
|
322 ``mach.main.Mach`` instance. e.g.:: |
|
323 |
|
324 mach = mach.main.Mach(os.getcwd()) |
|
325 |
|
326 # Will allow --example to be specified on every mach command. |
|
327 mach.add_global_argument('--example', action='store_true', |
|
328 help='Demonstrate an example global argument.') |