Bug 1077272 - Allow argument groups in mach commands. r=gps.
authorNicholas Nethercote <nnethercote@mozilla.com>
Wed, 08 Oct 2014 15:11:50 -0700
changeset 232719 c547a8d0f0768da36ecaa73bcf0e4fb71f65fe14
parent 232718 33d851b1e655d727f2678f586417fb324945aff5
child 232720 b8cd2e3b170465af2cfc6d2dd41844a2e52ab2f3
push idunknown
push userunknown
push dateunknown
reviewersgps
bugs1077272
milestone35.0a1
Bug 1077272 - Allow argument groups in mach commands. r=gps.
python/mach/mach/base.py
python/mach/mach/decorators.py
python/mach/mach/dispatcher.py
--- a/python/mach/mach/base.py
+++ b/python/mach/mach/base.py
@@ -83,23 +83,28 @@ class MethodHandler(object):
 
         # argparse.ArgumentParser instance to use as the basis for command
         # arguments.
         'parser',
 
         # Arguments added to this command's parser. This is a 2-tuple of
         # positional and named arguments, respectively.
         'arguments',
+
+        # Argument groups added to this command's parser.
+        'argument_group_names',
     )
 
     def __init__(self, cls, method, name, category=None, description=None,
-        conditions=None, parser=None, arguments=None, pass_context=False):
+        conditions=None, parser=None, arguments=None,
+        argument_group_names=None, pass_context=False):
 
         self.cls = cls
         self.method = method
         self.name = name
         self.category = category
         self.description = description
         self.conditions = conditions or []
         self.parser = parser
         self.arguments = arguments or []
+        self.argument_group_names = argument_group_names or []
         self.pass_context = pass_context
 
--- a/python/mach/mach/decorators.py
+++ b/python/mach/mach/decorators.py
@@ -77,19 +77,22 @@ def CommandProvider(cls):
 
         for c in conditions:
             if not hasattr(c, '__call__'):
                 msg = msg % (command_name, type(c))
                 raise MachError(msg)
 
         arguments = getattr(value, '_mach_command_args', None)
 
+        argument_group_names = getattr(value, '_mach_command_arg_group_names', None)
+
         handler = MethodHandler(cls, attr, command_name, category=category,
             description=description, conditions=conditions, parser=parser,
-            arguments=arguments, pass_context=pass_context)
+            arguments=arguments, argument_group_names=argument_group_names,
+            pass_context=pass_context)
 
         Registrar.register_command_handler(handler)
 
     return cls
 
 
 class Command(object):
     """Decorator for functions or methods that provide a mach subcommand.
@@ -140,29 +143,62 @@ class CommandArgument(object):
         def foo(self):
             pass
     """
     def __init__(self, *args, **kwargs):
         if kwargs.get('nargs') == argparse.REMAINDER:
             # These are the assertions we make in dispatcher.py about
             # those types of CommandArguments.
             assert len(args) == 1
-            assert all(k in ('default', 'nargs', 'help') for k in kwargs)
+            assert all(k in ('default', 'nargs', 'help', 'group') for k in kwargs)
         self._command_args = (args, kwargs)
 
     def __call__(self, func):
         command_args = getattr(func, '_mach_command_args', [])
 
         command_args.insert(0, self._command_args)
 
         func._mach_command_args = command_args
 
         return func
 
 
+class CommandArgumentGroup(object):
+    """Decorator for additional argument groups to mach subcommands.
+
+    This decorator should be used to add arguments groups to mach commands.
+    Arguments to the decorator are proxied to
+    ArgumentParser.add_argument_group().
+
+    For example:
+
+        @Command('foo', helps='Run the foo action')
+        @CommandArgumentGroup('group1')
+        @CommandArgument('-b', '--bar', group='group1', action='store_true',
+            default=False, help='Enable bar mode.')
+        def foo(self):
+            pass
+
+    The name should be chosen so that it makes sense as part of the phrase
+    'Command Arguments for <name>' because that's how it will be shown in the
+    help message.
+    """
+    def __init__(self, group_name):
+        self._group_name = group_name
+
+    def __call__(self, func):
+        command_arg_group_names = getattr(func, '_mach_command_arg_group_names', [])
+
+        command_arg_group_names.insert(0, self._group_name)
+
+        func._mach_command_arg_group_names = command_arg_group_names
+
+        return func
+
+
 def SettingsProvider(cls):
     """Class decorator to denote that this class provides Mach settings.
 
     When this decorator is encountered, the underlying class will automatically
     be registered with the Mach registrar and will (likely) be hooked up to the
     mach driver.
 
     This decorator is only allowed on mach.config.ConfigProvider classes.
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -141,16 +141,21 @@ class CommandAction(argparse.Action):
         if handler.parser:
             subparser = handler.parser
         else:
             subparser = argparse.ArgumentParser(**parser_args)
 
         remainder = None
 
         for arg in handler.arguments:
+            # Remove our group keyword; it's not needed here.
+            group_name = arg[1].get('group')
+            if group_name:
+                del arg[1]['group']
+
             if arg[1].get('nargs') == argparse.REMAINDER:
                 # parse_known_args expects all argparse.REMAINDER ('...')
                 # arguments to be all stuck together. Instead, we want them to
                 # pick any extra argument, wherever they are.
                 # Assume a limited CommandArgument for those arguments.
                 assert len(arg[0]) == 1
                 assert all(k in ('default', 'nargs', 'help') for k in arg[1])
                 remainder = arg
@@ -283,17 +288,28 @@ class CommandAction(argparse.Action):
 
             if not handler.description:
                 handler.description = c_parser.description
                 c_parser.description = None
         else:
             c_parser = argparse.ArgumentParser(**parser_args)
             group = c_parser.add_argument_group('Command Arguments')
 
+        extra_groups = {}
+        for group_name in handler.argument_group_names:
+            group_full_name = 'Command Arguments for ' + group_name
+            extra_groups[group_name] = \
+                c_parser.add_argument_group(group_full_name)
+
         for arg in handler.arguments:
+            # Apply our group keyword.
+            group_name = arg[1].get('group')
+            if group_name:
+                del arg[1]['group']
+                group = extra_groups[group_name]
             group.add_argument(*arg[0], **arg[1])
 
         # This will print the description of the command below the usage.
         description = handler.description
         if description:
             parser.description = description
 
         parser.usage = '%(prog)s [global arguments] ' + command + \