Bug 794509 - Part 3: Automatically discover mach commands in sys.path; r=jhammel

DONTBUILD (NPOTB)
This commit is contained in:
Gregory Szorc 2012-10-05 12:19:19 -07:00
parent 5385e05615
commit 60db6ddf8e
2 changed files with 59 additions and 23 deletions

View File

@ -14,14 +14,14 @@ Subversion, and Mercurial and provides a common driver for multiple
subcommands.
Subcommands are implemented by decorating a class inheritting from
mozbuild.base.MozbuildObject and by decorating methods that act as subcommand
handlers.
mozbuild.base.MozbuildObject and by decorating methods that act as
subcommand handlers.
Relevant decorators are defined in the *mach.base* module. There are the
*Command* and *CommandArgument* decorators, which should be used on methods
to denote that a specific method represents a handler for a mach subcommand.
There is also the *CommandProvider* decorator, which is applied to a class
to denote that it contains mach subcommands.
Relevant decorators are defined in the *mach.base* module. There are
the *Command* and *CommandArgument* decorators, which should be used
on methods to denote that a specific method represents a handler for
a mach subcommand. There is also the *CommandProvider* decorator,
which is applied to a class to denote that it contains mach subcommands.
Here is a complete example:
@ -41,16 +41,20 @@ Here is a complete example:
# 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 as arguments to *argparse.ArgumentParser.add_parser()*
and *argparse.ArgumentParser.add_argument()*. See the documentation for
*argparse* for more.
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 as arguments to
*argparse.ArgumentParser.add_parser()* and
*argparse.ArgumentParser.add_argument()*. See the documentation in the
*mach.base* module for more.
Currently, you also need to hook up some plumbing in
*mach.main.Mach*. In the future, we hope to have automatic detection
of submodules.
The Python modules defining mach commands do not need to live inside the
main mach source tree. If a path on *sys.path* contains a *mach/commands*
directory, modules will be loaded automatically by mach and any classes
containing the decorators described above will be detected and loaded
automatically by mach. So, to add a new subcommand to mach, you just need
to ensure your Python module is present on *sys.path*.
Minimizing Code in Mach
-----------------------

View File

@ -9,6 +9,7 @@ from __future__ import unicode_literals
import argparse
import codecs
import imp
import logging
import os
import sys
@ -19,13 +20,6 @@ from mozbuild.logger import LoggingManager
from mach.registrar import populate_argument_parser
# Import sub-command modules
# TODO Bug 794509 do this via auto-discovery. Update README once this is
# done.
from mach.commands.build import Build
from mach.commands.settings import Settings
from mach.commands.testing import Testing
from mach.commands.warnings import Warnings
# Classes inheriting from ConfigProvider that provide settings.
# TODO this should come from auto-discovery somehow.
@ -46,6 +40,8 @@ CONSUMED_ARGUMENTS = [
'func',
]
MODULES_SCANNED = False
class ArgumentParser(argparse.ArgumentParser):
"""Custom implementation argument parser to make things look pretty."""
@ -106,6 +102,8 @@ To see more help for a specific command, run:
"""
def __init__(self, cwd):
global MODULES_SCANNED
assert os.path.isdir(cwd)
self.cwd = cwd
@ -115,6 +113,11 @@ To see more help for a specific command, run:
self.log_manager.register_structured_logger(self.logger)
if not MODULES_SCANNED:
self._load_modules()
MODULES_SCANNED = True
def run(self, argv):
"""Runs mach with arguments provided from the command line.
@ -214,6 +217,35 @@ To see more help for a specific command, run:
self.logger.log(level, format_str,
extra={'action': action, 'params': params})
def _load_modules(self):
"""Scan over Python modules looking for mach command providers."""
# Create parent module otherwise Python complains when the parent is
# missing.
if b'mach.commands' not in sys.modules:
mod = imp.new_module(b'mach.commands')
sys.modules[b'mach.commands'] = mod
for path in sys.path:
# We only support importing .py files from directories.
commands_path = os.path.join(path, 'mach', 'commands')
if not os.path.isdir(commands_path):
continue
# We only support loading modules in the immediate mach.commands
# module, not sub-modules. Walking the tree would be trivial to
# implement if it were ever desired.
for f in sorted(os.listdir(commands_path)):
if not f.endswith('.py') or f == '__init__.py':
continue
full_path = os.path.join(commands_path, f)
module_name = 'mach.commands.%s' % f[0:-3]
imp.load_source(module_name, full_path)
def load_settings(self, args):
"""Determine which settings files apply and load them.