python/mach/README.rst

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/python/mach/README.rst	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,328 @@
     1.4 +====
     1.5 +mach
     1.6 +====
     1.7 +
     1.8 +Mach (German for *do*) is a generic command dispatcher for the command
     1.9 +line.
    1.10 +
    1.11 +To use mach, you install the mach core (a Python package), create an
    1.12 +executable *driver* script (named whatever you want), and write mach
    1.13 +commands. When the *driver* is executed, mach dispatches to the
    1.14 +requested command handler automatically.
    1.15 +
    1.16 +Features
    1.17 +========
    1.18 +
    1.19 +On a high level, mach is similar to using argparse with subparsers (for
    1.20 +command handling). When you dig deeper, mach offers a number of
    1.21 +additional features:
    1.22 +
    1.23 +Distributed command definitions
    1.24 +  With optparse/argparse, you have to define your commands on a central
    1.25 +  parser instance. With mach, you annotate your command methods with
    1.26 +  decorators and mach finds and dispatches to them automatically.
    1.27 +
    1.28 +Command categories
    1.29 +  Mach commands can be grouped into categories when displayed in help.
    1.30 +  This is currently not possible with argparse.
    1.31 +
    1.32 +Logging management
    1.33 +  Mach provides a facility for logging (both classical text and
    1.34 +  structured) that is available to any command handler.
    1.35 +
    1.36 +Settings files
    1.37 +  Mach provides a facility for reading settings from an ini-like file
    1.38 +  format.
    1.39 +
    1.40 +Components
    1.41 +==========
    1.42 +
    1.43 +Mach is conceptually composed of the following components:
    1.44 +
    1.45 +core
    1.46 +  The mach core is the core code powering mach. This is a Python package
    1.47 +  that contains all the business logic that makes mach work. The mach
    1.48 +  core is common to all mach deployments.
    1.49 +
    1.50 +commands
    1.51 +  These are what mach dispatches to. Commands are simply Python methods
    1.52 +  registered as command names. The set of commands is unique to the
    1.53 +  environment mach is deployed in.
    1.54 +
    1.55 +driver
    1.56 +  The *driver* is the entry-point to mach. It is simply an executable
    1.57 +  script that loads the mach core, tells it where commands can be found,
    1.58 +  then asks the mach core to handle the current request. The driver is
    1.59 +  unique to the deployed environment. But, it's usually based on an
    1.60 +  example from this source tree.
    1.61 +
    1.62 +Project State
    1.63 +=============
    1.64 +
    1.65 +mach was originally written as a command dispatching framework to aid
    1.66 +Firefox development. While the code is mostly generic, there are still
    1.67 +some pieces that closely tie it to Mozilla/Firefox. The goal is for
    1.68 +these to eventually be removed and replaced with generic features so
    1.69 +mach is suitable for anybody to use. Until then, mach may not be the
    1.70 +best fit for you.
    1.71 +
    1.72 +Implementing Commands
    1.73 +---------------------
    1.74 +
    1.75 +Mach commands are defined via Python decorators.
    1.76 +
    1.77 +All the relevant decorators are defined in the *mach.decorators* module.
    1.78 +The important decorators are as follows:
    1.79 +
    1.80 +CommandProvider
    1.81 +  A class decorator that denotes that a class contains mach
    1.82 +  commands. The decorator takes no arguments.
    1.83 +
    1.84 +Command
    1.85 +  A method decorator that denotes that the method should be called when
    1.86 +  the specified command is requested. The decorator takes a command name
    1.87 +  as its first argument and a number of additional arguments to
    1.88 +  configure the behavior of the command.
    1.89 +
    1.90 +CommandArgument
    1.91 +  A method decorator that defines an argument to the command. Its
    1.92 +  arguments are essentially proxied to ArgumentParser.add_argument()
    1.93 +
    1.94 +Classes with the *@CommandProvider* decorator *must* have an *__init__*
    1.95 +method that accepts 1 or 2 arguments. If it accepts 2 arguments, the
    1.96 +2nd argument will be a *MachCommandContext* instance. This is just a named
    1.97 +tuple containing references to objects provided by the mach driver.
    1.98 +
    1.99 +Here is a complete example::
   1.100 +
   1.101 +    from mach.decorators import (
   1.102 +        CommandArgument,
   1.103 +        CommandProvider,
   1.104 +        Command,
   1.105 +    )
   1.106 +
   1.107 +    @CommandProvider
   1.108 +    class MyClass(object):
   1.109 +        @Command('doit', help='Do ALL OF THE THINGS.')
   1.110 +        @CommandArgument('--force', '-f', action='store_true',
   1.111 +            help='Force doing it.')
   1.112 +        def doit(self, force=False):
   1.113 +            # Do stuff here.
   1.114 +
   1.115 +When the module is loaded, the decorators tell mach about all handlers.
   1.116 +When mach runs, it takes the assembled metadata from these handlers and
   1.117 +hooks it up to the command line driver. Under the hood, arguments passed
   1.118 +to the decorators are being used to help mach parse command arguments,
   1.119 +formulate arguments to the methods, etc. See the documentation in the
   1.120 +*mach.base* module for more.
   1.121 +
   1.122 +The Python modules defining mach commands do not need to live inside the
   1.123 +main mach source tree.
   1.124 +
   1.125 +Conditionally Filtering Commands
   1.126 +--------------------------------
   1.127 +
   1.128 +Sometimes it might only make sense to run a command given a certain
   1.129 +context. For example, running tests only makes sense if the product
   1.130 +they are testing has been built, and said build is available. To make
   1.131 +sure a command is only runnable from within a correct context, you can
   1.132 +define a series of conditions on the *Command* decorator.
   1.133 +
   1.134 +A condition is simply a function that takes an instance of the
   1.135 +*CommandProvider* class as an argument, and returns True or False. If
   1.136 +any of the conditions defined on a command return False, the command
   1.137 +will not be runnable. The doc string of a condition function is used in
   1.138 +error messages, to explain why the command cannot currently be run.
   1.139 +
   1.140 +Here is an example:
   1.141 +
   1.142 +    from mach.decorators import (
   1.143 +        CommandProvider,
   1.144 +        Command,
   1.145 +    )
   1.146 +
   1.147 +    def build_available(cls):
   1.148 +        """The build needs to be available."""
   1.149 +        return cls.build_path is not None
   1.150 +
   1.151 +    @CommandProvider
   1.152 +    class MyClass(MachCommandBase):
   1.153 +        def __init__(self, build_path=None):
   1.154 +            self.build_path = build_path
   1.155 +
   1.156 +        @Command('run_tests', conditions=[build_available])
   1.157 +        def run_tests(self):
   1.158 +            # Do stuff here.
   1.159 +
   1.160 +It is important to make sure that any state needed by the condition is
   1.161 +available to instances of the command provider.
   1.162 +
   1.163 +By default all commands without any conditions applied will be runnable,
   1.164 +but it is possible to change this behaviour by setting *require_conditions*
   1.165 +to True:
   1.166 +
   1.167 +    m = mach.main.Mach()
   1.168 +    m.require_conditions = True
   1.169 +
   1.170 +Minimizing Code in Commands
   1.171 +---------------------------
   1.172 +
   1.173 +Mach command modules, classes, and methods work best when they are
   1.174 +minimal dispatchers. The reason is import bloat. Currently, the mach
   1.175 +core needs to import every Python file potentially containing mach
   1.176 +commands for every command invocation. If you have dozens of commands or
   1.177 +commands in modules that import a lot of Python code, these imports
   1.178 +could slow mach down and waste memory.
   1.179 +
   1.180 +It is thus recommended that mach modules, classes, and methods do as
   1.181 +little work as possible. Ideally the module should only import from
   1.182 +the *mach* package. If you need external modules, you should import them
   1.183 +from within the command method.
   1.184 +
   1.185 +To keep code size small, the body of a command method should be limited
   1.186 +to:
   1.187 +
   1.188 +1. Obtaining user input (parsing arguments, prompting, etc)
   1.189 +2. Calling into some other Python package
   1.190 +3. Formatting output
   1.191 +
   1.192 +Of course, these recommendations can be ignored if you want to risk
   1.193 +slower performance.
   1.194 +
   1.195 +In the future, the mach driver may cache the dispatching information or
   1.196 +have it intelligently loaded to facilitate lazy loading.
   1.197 +
   1.198 +Logging
   1.199 +=======
   1.200 +
   1.201 +Mach configures a built-in logging facility so commands can easily log
   1.202 +data.
   1.203 +
   1.204 +What sets the logging facility apart from most loggers you've seen is
   1.205 +that it encourages structured logging. Instead of conventional logging
   1.206 +where simple strings are logged, the internal logging mechanism logs all
   1.207 +events with the following pieces of information:
   1.208 +
   1.209 +* A string *action*
   1.210 +* A dict of log message fields
   1.211 +* A formatting string
   1.212 +
   1.213 +Essentially, instead of assembling a human-readable string at
   1.214 +logging-time, you create an object holding all the pieces of data that
   1.215 +will constitute your logged event. For each unique type of logged event,
   1.216 +you assign an *action* name.
   1.217 +
   1.218 +Depending on how logging is configured, your logged event could get
   1.219 +written a couple of different ways.
   1.220 +
   1.221 +JSON Logging
   1.222 +------------
   1.223 +
   1.224 +Where machines are the intended target of the logging data, a JSON
   1.225 +logger is configured. The JSON logger assembles an array consisting of
   1.226 +the following elements:
   1.227 +
   1.228 +* Decimal wall clock time in seconds since UNIX epoch
   1.229 +* String *action* of message
   1.230 +* Object with structured message data
   1.231 +
   1.232 +The JSON-serialized array is written to a configured file handle.
   1.233 +Consumers of this logging stream can just perform a readline() then feed
   1.234 +that into a JSON deserializer to reconstruct the original logged
   1.235 +message. They can key off the *action* element to determine how to
   1.236 +process individual events. There is no need to invent a parser.
   1.237 +Convenient, isn't it?
   1.238 +
   1.239 +Logging for Humans
   1.240 +------------------
   1.241 +
   1.242 +Where humans are the intended consumer of a log message, the structured
   1.243 +log message are converted to more human-friendly form. This is done by
   1.244 +utilizing the *formatting* string provided at log time. The logger
   1.245 +simply calls the *format* method of the formatting string, passing the
   1.246 +dict containing the message's fields.
   1.247 +
   1.248 +When *mach* is used in a terminal that supports it, the logging facility
   1.249 +also supports terminal features such as colorization. This is done
   1.250 +automatically in the logging layer - there is no need to control this at
   1.251 +logging time.
   1.252 +
   1.253 +In addition, messages intended for humans typically prepends every line
   1.254 +with the time passed since the application started.
   1.255 +
   1.256 +Logging HOWTO
   1.257 +-------------
   1.258 +
   1.259 +Structured logging piggybacks on top of Python's built-in logging
   1.260 +infrastructure provided by the *logging* package. We accomplish this by
   1.261 +taking advantage of *logging.Logger.log()*'s *extra* argument. To this
   1.262 +argument, we pass a dict with the fields *action* and *params*. These
   1.263 +are the string *action* and dict of message fields, respectively. The
   1.264 +formatting string is passed as the *msg* argument, like normal.
   1.265 +
   1.266 +If you were logging to a logger directly, you would do something like:
   1.267 +
   1.268 +    logger.log(logging.INFO, 'My name is {name}',
   1.269 +        extra={'action': 'my_name', 'params': {'name': 'Gregory'}})
   1.270 +
   1.271 +The JSON logging would produce something like:
   1.272 +
   1.273 +    [1339985554.306338, "my_name", {"name": "Gregory"}]
   1.274 +
   1.275 +Human logging would produce something like:
   1.276 +
   1.277 +     0.52 My name is Gregory
   1.278 +
   1.279 +Since there is a lot of complexity using logger.log directly, it is
   1.280 +recommended to go through a wrapping layer that hides part of the
   1.281 +complexity for you. The easiest way to do this is by utilizing the
   1.282 +LoggingMixin:
   1.283 +
   1.284 +    import logging
   1.285 +    from mach.mixin.logging import LoggingMixin
   1.286 +
   1.287 +    class MyClass(LoggingMixin):
   1.288 +        def foo(self):
   1.289 +             self.log(logging.INFO, 'foo_start', {'bar': True},
   1.290 +                 'Foo performed. Bar: {bar}')
   1.291 +
   1.292 +Entry Points
   1.293 +============
   1.294 +
   1.295 +It is possible to use setuptools' entry points to load commands
   1.296 +directly from python packages. A mach entry point is a function which
   1.297 +returns a list of files or directories containing mach command
   1.298 +providers. e.g.::
   1.299 +
   1.300 +    def list_providers():
   1.301 +        providers = []
   1.302 +        here = os.path.abspath(os.path.dirname(__file__))
   1.303 +        for p in os.listdir(here):
   1.304 +            if p.endswith('.py'):
   1.305 +                providers.append(os.path.join(here, p))
   1.306 +        return providers
   1.307 +
   1.308 +See http://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins
   1.309 +for more information on creating an entry point. To search for entry
   1.310 +point plugins, you can call *load_commands_from_entry_point*. This
   1.311 +takes a single parameter called *group*. This is the name of the entry
   1.312 +point group to load and defaults to ``mach.providers``. e.g.::
   1.313 +
   1.314 +    mach.load_commands_from_entry_point("mach.external.providers")
   1.315 +
   1.316 +Adding Global Arguments
   1.317 +=======================
   1.318 +
   1.319 +Arguments to mach commands are usually command-specific. However,
   1.320 +mach ships with a handful of global arguments that apply to all
   1.321 +commands.
   1.322 +
   1.323 +It is possible to extend the list of global arguments. In your
   1.324 +*mach driver*, simply call ``add_global_argument()`` on your
   1.325 +``mach.main.Mach`` instance. e.g.::
   1.326 +
   1.327 +   mach = mach.main.Mach(os.getcwd())
   1.328 +
   1.329 +   # Will allow --example to be specified on every mach command.
   1.330 +   mach.add_global_argument('--example', action='store_true',
   1.331 +       help='Demonstrate an example global argument.')

mercurial