Bug 799262 - Formal API for loading mach command modules; r=jhammel
authorGregory Szorc <gps@mozilla.com>
Wed, 10 Oct 2012 11:08:09 -0700
changeset 110958 2a0e2af364bc635500fc8fe7129b217119c5ff03
parent 110915 ec10630b1a5406c28d1ac84bd314938374404d04
child 110959 428846e73299df3194ebb8a9172429e2356704ee
push idunknown
push userunknown
push dateunknown
reviewersjhammel
bugs799262
milestone19.0a1
Bug 799262 - Formal API for loading mach command modules; r=jhammel
mach
python/mach/mach/main.py
--- a/mach
+++ b/mach
@@ -39,9 +39,10 @@ try:
     import mach.main
 except ImportError:
     sys.path[0:0] = [os.path.join(our_dir, path) for path in SEARCH_PATHS]
 
     import mach.main
 
 # All of the code is in a module because EVERYTHING IS A LIBRARY.
 mach = mach.main.Mach(our_dir)
+mach.load_commands_from_sys_path()
 sys.exit(mach.run(sys.argv[1:]))
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -8,16 +8,17 @@
 from __future__ import unicode_literals
 
 import argparse
 import codecs
 import imp
 import logging
 import os
 import sys
+import uuid
 
 from mozbuild.base import BuildConfig
 from mozbuild.config import ConfigSettings
 from mozbuild.logger import LoggingManager
 
 from mach.registrar import populate_argument_parser
 
 
@@ -35,18 +36,16 @@ CONSUMED_ARGUMENTS = [
     'logfile',
     'log_interval',
     'command',
     'cls',
     'method',
     'func',
 ]
 
-MODULES_SCANNED = False
-
 
 class ArgumentParser(argparse.ArgumentParser):
     """Custom implementation argument parser to make things look pretty."""
 
     def error(self, message):
         """Custom error reporter to give more helpful text on bad commands."""
         if not message.startswith('argument command: invalid choice'):
             argparse.ArgumentParser.error(self, message)
@@ -108,20 +107,64 @@ To see more help for a specific command,
 
         self.cwd = cwd
         self.log_manager = LoggingManager()
         self.logger = logging.getLogger(__name__)
         self.settings = ConfigSettings()
 
         self.log_manager.register_structured_logger(self.logger)
 
-        if not MODULES_SCANNED:
-            self._load_modules()
+    def load_commands_from_sys_path(self):
+        """Discover and load mach command modules from sys.path.
+
+        This iterates over all paths on sys.path. If the path contains a
+        "mach/commands" subdirectory, all .py files in that directory will be
+        loaded and examined for mach commands.
+        """
+        # 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
+
+            self.load_commands_from_directory(commands_path)
 
-        MODULES_SCANNED = True
+    def load_commands_from_directory(self, path):
+        """Scan for mach commands from modules in a directory.
+
+        This takes a path to a directory, loads the .py files in it, and
+        registers and found mach command providers with this mach instance.
+        """
+        for f in sorted(os.listdir(path)):
+            if not f.endswith('.py') or f == '__init__.py':
+                continue
+
+            full_path = os.path.join(path, f)
+            module_name = 'mach.commands.%s' % f[0:-3]
+
+            self.load_commands_from_file(full_path, module_name=module_name)
+
+    def load_commands_from_file(self, path, module_name=None):
+        """Scan for mach commands from a file.
+
+        This takes a path to a file and loads it as a Python module under the
+        module name specified. If no name is specified, a random one will be
+        chosen.
+        """
+        if module_name is None:
+            module_name = 'mach.commands.%s' % uuid.uuid1().get_hex()
+
+        imp.load_source(module_name, path)
 
     def run(self, argv):
         """Runs mach with arguments provided from the command line.
 
         Returns the integer exit code that should be used. 0 means success. All
         other values indicate failure.
         """
 
@@ -212,45 +255,16 @@ To see more help for a specific command,
 
         return result
 
     def log(self, level, action, params, format_str):
         """Helper method to record a structured log event."""
         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.
 
         Currently, we only support loading settings from a single file.
         Ideally, we support loading from multiple files. This is supported by
         the ConfigSettings API. However, that API currently doesn't track where
         individual values come from, so if we load from multiple sources then
         save, we effectively do a full copy. We don't want this. Until