Bug 1163112 - [mach core] Consolidate functionality between Main._run and Registrar.dispatch, r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 08 May 2015 17:03:15 -0400
changeset 243552 8df1ea116b91c96d7cc2c855c0d9d17777bb0b39
parent 243551 77c19e3e5a440a3ba36140f91be123cf87c7b821
child 243553 ce727ed41dedbb42c7311de7edb8865ce2cca5d1
push id28741
push userkwierso@gmail.com
push dateTue, 12 May 2015 23:24:40 +0000
treeherdermozilla-central@d476776d920d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1163112
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1163112 - [mach core] Consolidate functionality between Main._run and Registrar.dispatch, r=gps
python/mach/mach/main.py
python/mach/mach/registrar.py
python/mach/mach/test/test_conditions.py
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -11,17 +11,16 @@ from collections import Iterable
 import argparse
 import codecs
 import imp
 import logging
 import os
 import sys
 import traceback
 import uuid
-import sys
 
 from .base import (
     CommandContext,
     MachError,
     NoCommandError,
     UnknownCommandError,
     UnrecognizedArgumentError,
 )
@@ -87,23 +86,16 @@ Did you want to %s any of these commands
 '''
 
 UNRECOGNIZED_ARGUMENT_ERROR = r'''
 It looks like you passed an unrecognized argument into mach.
 
 The %s command does not accept the arguments: %s
 '''.lstrip()
 
-INVALID_COMMAND_CONTEXT = r'''
-It looks like you tried to run a mach command from an invalid context. The %s
-command failed to meet the following conditions: %s
-
-Run |mach help| to show a list of all commands available to the current context.
-'''.lstrip()
-
 INVALID_ENTRY_POINT = r'''
 Entry points should return a list of command providers or directories
 containing command providers. The following entry point is invalid:
 
     %s
 
 You are seeing this because there is an error in an external module attempting
 to implement a mach command. Please fix the error, or uninstall the module from
@@ -153,17 +145,17 @@ class ContextWrapper(object):
         object.__setattr__(self, '_handler', handler)
 
     def __getattribute__(self, key):
         try:
             return getattr(object.__getattribute__(self, '_context'), key)
         except AttributeError as e:
             try:
                 ret = object.__getattribute__(self, '_handler')(self, key)
-            except AttributeError, TypeError:
+            except (AttributeError, TypeError):
                 # TypeError is in case the handler comes from old code not
                 # taking a key argument.
                 raise e
             setattr(self, key, ret)
             return ret
 
     def __setattr__(self, key, value):
         setattr(object.__getattribute__(self, '_context'), key, value)
@@ -421,55 +413,27 @@ To see more help for a specific command,
             write_interval=args.log_interval, write_times=write_times)
 
         self.load_settings(args)
 
         if not hasattr(args, 'mach_handler'):
             raise MachError('ArgumentParser result missing mach handler info.')
 
         handler = getattr(args, 'mach_handler')
-        cls = handler.cls
-
-        if handler.pass_context:
-            instance = cls(context)
-        else:
-            instance = cls()
-
-        if handler.conditions:
-            fail_conditions = []
-            for c in handler.conditions:
-                if not c(instance):
-                    fail_conditions.append(c)
-
-            if fail_conditions:
-                print(self._condition_failed_message(handler.name, fail_conditions))
-                return 1
-
-        fn = getattr(instance, handler.method)
 
         try:
-            if args.debug_command:
-                import pdb
-                result = pdb.runcall(fn, **vars(args.command_args))
-            else:
-                result = fn(**vars(args.command_args))
-
-            if not result:
-                result = 0
-
-            assert isinstance(result, (int, long))
-
-            return result
+            return Registrar._run_command_handler(handler, context=context,
+                debug_command=args.debug_command, **vars(args.command_args))
         except KeyboardInterrupt as ki:
             raise ki
         except Exception as e:
             exc_type, exc_value, exc_tb = sys.exc_info()
 
-            # The first frame is us and is never used.
-            stack = traceback.extract_tb(exc_tb)[1:]
+            # The first two frames are us and are never used.
+            stack = traceback.extract_tb(exc_tb)[2:]
 
             # If we have nothing on the stack, the exception was raised as part
             # of calling the @Command method itself. This likely means a
             # mismatch between @CommandArgument and arguments to the method.
             # e.g. there exists a @CommandArgument without the corresponding
             # argument on the method. We handle that here until the module
             # loader grows the ability to validate better.
             if not len(stack):
@@ -506,26 +470,16 @@ To see more help for a specific command,
 
             return 1
 
     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})
 
-    @classmethod
-    def _condition_failed_message(cls, name, conditions):
-        msg = ['\n']
-        for c in conditions:
-            part = ['  %s' % c.__name__]
-            if c.__doc__ is not None:
-                part.append(c.__doc__)
-            msg.append(' - '.join(part))
-        return INVALID_COMMAND_CONTEXT % (name, '\n'.join(msg))
-
     def _print_error_header(self, argv, fh):
         fh.write('Error running mach:\n\n')
         fh.write('    ')
         fh.write(repr(argv))
         fh.write('\n\n')
 
     def _print_exception(self, fh, exc_type, exc_value, stack):
         fh.write(ERROR_FOOTER)
--- a/python/mach/mach/registrar.py
+++ b/python/mach/mach/registrar.py
@@ -1,16 +1,23 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
 from .base import MachError
 
+INVALID_COMMAND_CONTEXT = r'''
+It looks like you tried to run a mach command from an invalid context. The %s
+command failed to meet the following conditions: %s
+
+Run |mach help| to show a list of all commands available to the current context.
+'''.lstrip()
+
 
 class MachRegistrar(object):
     """Container for mach command and config providers."""
 
     def __init__(self):
         self.command_handlers = {}
         self.commands_by_category = {}
         self.settings_providers = set()
@@ -33,33 +40,75 @@ class MachRegistrar(object):
 
     def register_settings_provider(self, cls):
         self.settings_providers.add(cls)
 
     def register_category(self, name, title, description, priority=50):
         self.categories[name] = (title, description, priority)
         self.commands_by_category[name] = set()
 
-    def dispatch(self, name, context=None, **args):
-        """Dispatch/run a command.
+    @classmethod
+    def _condition_failed_message(cls, name, conditions):
+        msg = ['\n']
+        for c in conditions:
+            part = ['  %s' % c.__name__]
+            if c.__doc__ is not None:
+                part.append(c.__doc__)
+            msg.append(' - '.join(part))
+        return INVALID_COMMAND_CONTEXT % (name, '\n'.join(msg))
 
-        Commands can use this to call other commands.
-        """
-
-        # TODO The logic in this function overlaps with code in
-        # mach.main.Main._run() and should be consolidated.
-        handler = self.command_handlers[name]
+    def _run_command_handler(self, handler, context=None, debug_command=False, **kwargs):
         cls = handler.cls
 
         if handler.pass_context and not context:
             raise Exception('mach command class requires context.')
 
         if handler.pass_context:
             instance = cls(context)
         else:
             instance = cls()
 
+        if handler.conditions:
+            fail_conditions = []
+            for c in handler.conditions:
+                if not c(instance):
+                    fail_conditions.append(c)
+
+            if fail_conditions:
+                print(self._condition_failed_message(handler.name, fail_conditions))
+                return 1
+
         fn = getattr(instance, handler.method)
 
-        return fn(**args) or 0
+        if debug_command:
+            import pdb
+            result = pdb.runcall(fn, **kwargs)
+        else:
+            result = fn(**kwargs)
+
+        result = result or 0
+        assert isinstance(result, (int, long))
+        return result
+
+    def dispatch(self, name, context=None, argv=None, **kwargs):
+        """Dispatch/run a command.
+
+        Commands can use this to call other commands.
+        """
+        # TODO handler.subcommand_handlers are ignored
+        handler = self.command_handlers[name]
+
+        if handler.parser:
+            parser = handler.parser
+
+            # save and restore existing defaults so **kwargs don't persist across
+            # subsequent invocations of Registrar.dispatch()
+            old_defaults = parser._defaults.copy()
+            parser.set_defaults(**kwargs)
+            kwargs, _ = parser.parse_known_args(argv or [])
+            kwargs = vars(kwargs)
+            parser._defaults = old_defaults
+
+        return self._run_command_handler(handler, context=context, **kwargs)
+
 
 
 Registrar = MachRegistrar()
--- a/python/mach/mach/test/test_conditions.py
+++ b/python/mach/mach/test/test_conditions.py
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
 import os
 
 from mach.base import MachError
 from mach.main import Mach
+from mach.registrar import Registrar
 from mach.test.common import TestBase
 
 from mozunit import main
 
 
 def _populate_context(context, key=None):
     if key is None:
         return
@@ -43,24 +44,24 @@ class TestConditions(TestBase):
         def is_bar():
             """Bar must be true"""
         fail_conditions = [is_bar]
 
         for name in ('cmd_bar', 'cmd_foobar'):
             result, stdout, stderr = self._run_mach([name])
             self.assertEquals(1, result)
 
-            fail_msg = Mach._condition_failed_message(name, fail_conditions)
+            fail_msg = Registrar._condition_failed_message(name, fail_conditions)
             self.assertEquals(fail_msg.rstrip(), stdout.rstrip())
 
         for name in ('cmd_bar_ctx', 'cmd_foobar_ctx'):
             result, stdout, stderr = self._run_mach([name], _populate_context)
             self.assertEquals(1, result)
 
-            fail_msg = Mach._condition_failed_message(name, fail_conditions)
+            fail_msg = Registrar._condition_failed_message(name, fail_conditions)
             self.assertEquals(fail_msg.rstrip(), stdout.rstrip())
 
     def test_invalid_type(self):
         """Test that a condition which is not callable raises an exception."""
 
         m = Mach(os.getcwd())
         m.define_category('testing', 'Mach unittest', 'Testing for mach core', 10)
         self.assertRaises(MachError, m.load_commands_from_file,