python/mach/README.rst

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     1 ====
     2 mach
     3 ====
     5 Mach (German for *do*) is a generic command dispatcher for the command
     6 line.
     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.
    13 Features
    14 ========
    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:
    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.
    25 Command categories
    26   Mach commands can be grouped into categories when displayed in help.
    27   This is currently not possible with argparse.
    29 Logging management
    30   Mach provides a facility for logging (both classical text and
    31   structured) that is available to any command handler.
    33 Settings files
    34   Mach provides a facility for reading settings from an ini-like file
    35   format.
    37 Components
    38 ==========
    40 Mach is conceptually composed of the following components:
    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.
    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.
    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.
    59 Project State
    60 =============
    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.
    69 Implementing Commands
    70 ---------------------
    72 Mach commands are defined via Python decorators.
    74 All the relevant decorators are defined in the *mach.decorators* module.
    75 The important decorators are as follows:
    77 CommandProvider
    78   A class decorator that denotes that a class contains mach
    79   commands. The decorator takes no arguments.
    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.
    87 CommandArgument
    88   A method decorator that defines an argument to the command. Its
    89   arguments are essentially proxied to ArgumentParser.add_argument()
    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.
    96 Here is a complete example::
    98     from mach.decorators import (
    99         CommandArgument,
   100         CommandProvider,
   101         Command,
   102     )
   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.
   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.
   119 The Python modules defining mach commands do not need to live inside the
   120 main mach source tree.
   122 Conditionally Filtering Commands
   123 --------------------------------
   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.
   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.
   137 Here is an example:
   139     from mach.decorators import (
   140         CommandProvider,
   141         Command,
   142     )
   144     def build_available(cls):
   145         """The build needs to be available."""
   146         return cls.build_path is not None
   148     @CommandProvider
   149     class MyClass(MachCommandBase):
   150         def __init__(self, build_path=None):
   151             self.build_path = build_path
   153         @Command('run_tests', conditions=[build_available])
   154         def run_tests(self):
   155             # Do stuff here.
   157 It is important to make sure that any state needed by the condition is
   158 available to instances of the command provider.
   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:
   164     m = mach.main.Mach()
   165     m.require_conditions = True
   167 Minimizing Code in Commands
   168 ---------------------------
   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.
   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.
   182 To keep code size small, the body of a command method should be limited
   183 to:
   185 1. Obtaining user input (parsing arguments, prompting, etc)
   186 2. Calling into some other Python package
   187 3. Formatting output
   189 Of course, these recommendations can be ignored if you want to risk
   190 slower performance.
   192 In the future, the mach driver may cache the dispatching information or
   193 have it intelligently loaded to facilitate lazy loading.
   195 Logging
   196 =======
   198 Mach configures a built-in logging facility so commands can easily log
   199 data.
   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:
   206 * A string *action*
   207 * A dict of log message fields
   208 * A formatting string
   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.
   215 Depending on how logging is configured, your logged event could get
   216 written a couple of different ways.
   218 JSON Logging
   219 ------------
   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:
   225 * Decimal wall clock time in seconds since UNIX epoch
   226 * String *action* of message
   227 * Object with structured message data
   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?
   236 Logging for Humans
   237 ------------------
   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.
   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.
   250 In addition, messages intended for humans typically prepends every line
   251 with the time passed since the application started.
   253 Logging HOWTO
   254 -------------
   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.
   263 If you were logging to a logger directly, you would do something like:
   265     logger.log(logging.INFO, 'My name is {name}',
   266         extra={'action': 'my_name', 'params': {'name': 'Gregory'}})
   268 The JSON logging would produce something like:
   270     [1339985554.306338, "my_name", {"name": "Gregory"}]
   272 Human logging would produce something like:
   274      0.52 My name is Gregory
   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:
   281     import logging
   282     from mach.mixin.logging import LoggingMixin
   284     class MyClass(LoggingMixin):
   285         def foo(self):
   286              self.log(logging.INFO, 'foo_start', {'bar': True},
   287                  'Foo performed. Bar: {bar}')
   289 Entry Points
   290 ============
   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.::
   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
   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.::
   311     mach.load_commands_from_entry_point("mach.external.providers")
   313 Adding Global Arguments
   314 =======================
   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.
   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.::
   324    mach = mach.main.Mach(os.getcwd())
   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.')

mercurial