Bug 1255450 - [mach] Create setting for defining command aliases, r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 23 Mar 2016 17:34:35 -0400
changeset 292819 355e8bb48aeeef736df7ca21351f7e87caf90668
parent 292818 84ba3a5bc33cc4af94f5a46465a6f921d794041b
child 292820 b5600b3a7deb840b40307ac6cbfac96ea71f50ae
push id30167
push userkwierso@gmail.com
push dateTue, 12 Apr 2016 22:28:26 +0000
treeherdermozilla-central@fb125ff927ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1255450
milestone48.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 1255450 - [mach] Create setting for defining command aliases, r=gps These config options can be defined in ~/.mozbuild/machrc or topsrcdir/machrc. Aliases work similar to the identically named option in an hgrc. For example: [alias] browser-test = mochitest -f browser mochitest = mochitest -f plain MozReview-Commit-ID: CnOocEslRUI
python/mach/docs/settings.rst
python/mach/mach/dispatcher.py
python/mach/mach/locale/en_US/LC_MESSAGES/alias.mo
python/mach/mach/locale/en_US/LC_MESSAGES/alias.po
python/mach/mach/test/common.py
python/mach/mach/test/providers/basic.py
python/mach/mach/test/test_dispatcher.py
python/moz.build
--- a/python/mach/docs/settings.rst
+++ b/python/mach/docs/settings.rst
@@ -8,16 +8,30 @@ Mach can read settings in from a set of 
 configuration files are either named ``mach.ini`` or ``.machrc`` and
 are specified by the bootstrap script. In mozilla-central, these files
 can live in ``~/.mozbuild`` and/or ``topsrcdir``.
 
 Settings can be specified anywhere, and used both by mach core or
 individual commands.
 
 
+Core Settings
+=============
+
+These settings are implemented by mach core.
+
+* alias - Create a command alias. This is useful if you want to alias a command to something else, optionally including some defaults. It can either be used to create an entire new command, or provide defaults for an existing one. For example:
+
+.. parsed-literal::
+
+    [alias]
+    mochitest = mochitest -f browser
+    browser-test = mochitest -f browser
+
+
 Defining Settings
 =================
 
 Settings need to be explicitly defined, along with their type,
 otherwise mach will throw when loading the configuration files.
 
 To define settings, use the :func:`~decorators.SettingsProvider`
 decorator in an existing mach command module. E.g:
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -1,26 +1,34 @@
 # 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 absolute_import, unicode_literals
 
 import argparse
 import difflib
+import shlex
 import sys
 
 from operator import itemgetter
 
 from .base import (
-    MachError,
     NoCommandError,
     UnknownCommandError,
     UnrecognizedArgumentError,
 )
+from .decorators import SettingsProvider
+
+
+@SettingsProvider
+class DispatchSettings():
+    config_settings = [
+        ('alias.*', 'string'),
+    ]
 
 
 class CommandFormatter(argparse.HelpFormatter):
     """Custom formatter to format just a subcommand."""
 
     def add_usage(self, *args):
         pass
 
@@ -105,37 +113,29 @@ class CommandAction(argparse.Action):
                     # -- is in command arguments
                     if '-h' in args[:args.index('--')] or '--help' in args[:args.index('--')]:
                         # Honor -h or --help only if it appears before --
                         self._handle_command_help(parser, command)
                         sys.exit(0)
                 else:
                     self._handle_command_help(parser, command)
                     sys.exit(0)
-
-
         else:
             raise NoCommandError()
 
-        # Command suggestion
+        # First see if the this is a user-defined alias
+        if command in self._context.settings.alias:
+            alias = self._context.settings.alias[command]
+            defaults = shlex.split(alias)
+            command = defaults.pop(0)
+            args = defaults + args
+
         if command not in self._mach_registrar.command_handlers:
-            # Make sure we don't suggest any deprecated commands.
-            names = [h.name for h in self._mach_registrar.command_handlers.values()
-                        if h.cls.__name__ != 'DeprecatedCommands']
-            # We first try to look for a valid command that is very similar to the given command.
-            suggested_commands = difflib.get_close_matches(command, names, cutoff=0.8)
-            # If we find more than one matching command, or no command at all, we give command suggestions instead
-            # (with a lower matching threshold). All commands that start with the given command (for instance: 'mochitest-plain',
-            # 'mochitest-chrome', etc. for 'mochitest-') are also included.
-            if len(suggested_commands) != 1:
-                suggested_commands = set(difflib.get_close_matches(command, names, cutoff=0.5))
-                suggested_commands |= {cmd for cmd in names if cmd.startswith(command)}
-                raise UnknownCommandError(command, 'run', suggested_commands)
-            sys.stderr.write("We're assuming the '%s' command is '%s' and we're executing it for you.\n\n" % (command, suggested_commands[0]))
-            command = suggested_commands[0]
+            # Try to find similar commands, may raise UnknownCommandError.
+            command = self._suggest_command(command)
 
         handler = self._mach_registrar.command_handlers.get(command)
 
         usage = '%(prog)s [global arguments] ' + command + \
             ' [command arguments]'
 
         subcommand = None
 
@@ -277,17 +277,17 @@ class CommandAction(argparse.Action):
 
                 description = handler.description
                 group.add_argument(command, help=description,
                     action='store_true')
 
         if disabled_commands and 'disabled' in r.categories:
             title, description, _priority = r.categories['disabled']
             group = parser.add_argument_group(title, description)
-            if verbose == True:
+            if verbose:
                 for c in disabled_commands:
                     group.add_argument(c['command'], help=c['description'],
                                        action='store_true')
 
         parser.print_help()
 
     def _populate_command_group(self, parser, handler, group):
         extra_groups = {}
@@ -399,16 +399,35 @@ class CommandAction(argparse.Action):
             parser.description = format_docstring(handler.docstring)
 
         parser.formatter_class = argparse.RawDescriptionHelpFormatter
 
         parser.print_help()
         print('')
         c_parser.print_help()
 
+    def _suggest_command(self, command):
+        # Make sure we don't suggest any deprecated commands.
+        names = [h.name for h in self._mach_registrar.command_handlers.values()
+                    if h.cls.__name__ != 'DeprecatedCommands']
+        # We first try to look for a valid command that is very similar to the given command.
+        suggested_commands = difflib.get_close_matches(command, names, cutoff=0.8)
+        # If we find more than one matching command, or no command at all,
+        # we give command suggestions instead (with a lower matching threshold).
+        # All commands that start with the given command (for instance:
+        # 'mochitest-plain', 'mochitest-chrome', etc. for 'mochitest-')
+        # are also included.
+        if len(suggested_commands) != 1:
+            suggested_commands = set(difflib.get_close_matches(command, names, cutoff=0.5))
+            suggested_commands |= {cmd for cmd in names if cmd.startswith(command)}
+            raise UnknownCommandError(command, 'run', suggested_commands)
+        sys.stderr.write("We're assuming the '%s' command is '%s' and we're "
+                         "executing it for you.\n\n" % (command, suggested_commands[0]))
+        return suggested_commands[0]
+
 
 class NoUsageFormatter(argparse.HelpFormatter):
     def _format_usage(self, *args, **kwargs):
         return ""
 
 
 def format_docstring(docstring):
     """Format a raw docstring into something suitable for presentation.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6631808417c8d7e8fc1c79aa04e422d383e2960c
GIT binary patch
literal 193
zc$~W@#4?ou2$+Fb28c}<AixKS<^f_~Am#<)D5!V_5IX>ICJ>7O@hXsDVoqjav7VM*
zT4_!WoKu{UUsS@t;9QiNSdyxcsF0kWo12)Iq5zgx$WK!!$w*a5%P-1RNU#Bm*(um6
V*g#d;DcB?yr5D>J=;1Jc0RRCjE8YMA
new file mode 100644
--- /dev/null
+++ b/python/mach/mach/locale/en_US/LC_MESSAGES/alias.po
@@ -0,0 +1,9 @@
+#
+msgid ""
+msgstr ""
+
+msgid "alias.*.short"
+msgstr "Create a command alias"
+
+msgid "alias.*.full"
+msgstr "Create a command alias of the form `<alias> = <command> <args>`."
--- a/python/mach/mach/test/common.py
+++ b/python/mach/mach/test/common.py
@@ -11,30 +11,35 @@ import unittest
 from mach.main import Mach
 from mach.base import CommandContext
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 class TestBase(unittest.TestCase):
     provider_dir = os.path.join(here, 'providers')
 
-    def _run_mach(self, args, provider_file=None, entry_point=None, context_handler=None):
+    def get_mach(self, provider_file=None, entry_point=None, context_handler=None):
         m = Mach(os.getcwd())
         m.define_category('testing', 'Mach unittest', 'Testing for mach core', 10)
         m.populate_context_handler = context_handler
 
         if provider_file:
             m.load_commands_from_file(os.path.join(self.provider_dir, provider_file))
 
         if entry_point:
             m.load_commands_from_entry_point(entry_point)
 
+        return m
+
+    def _run_mach(self, argv, *args, **kwargs):
+        m = self.get_mach(*args, **kwargs)
+
         stdout = StringIO()
         stderr = StringIO()
         stdout.encoding = 'UTF-8'
         stderr.encoding = 'UTF-8'
 
         try:
-            result = m.run(args, stdout=stdout, stderr=stderr)
+            result = m.run(argv, stdout=stdout, stderr=stderr)
         except SystemExit:
             result = None
 
         return (result, stdout.getvalue(), stderr.getvalue())
--- a/python/mach/mach/test/providers/basic.py
+++ b/python/mach/mach/test/providers/basic.py
@@ -1,15 +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 mach.decorators import (
+    CommandArgument,
     CommandProvider,
     Command,
 )
 
+
 @CommandProvider
 class ConditionsProvider(object):
     @Command('cmd_foo', category='testing')
     def run_foo(self):
         pass
+
+    @Command('cmd_bar', category='testing')
+    @CommandArgument('--baz', action="store_true",
+                     help='Run with baz')
+    def run_bar(self, baz=None):
+        pass
new file mode 100644
--- /dev/null
+++ b/python/mach/mach/test/test_dispatcher.py
@@ -0,0 +1,61 @@
+# 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
+
+import os
+from cStringIO import StringIO
+
+from mach.base import CommandContext
+from mach.registrar import Registrar
+from mach.test.common import TestBase
+
+from mozunit import main
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class TestDispatcher(TestBase):
+    """Tests dispatch related code"""
+
+    def get_parser(self, config=None):
+        mach = self.get_mach('basic.py')
+
+        for provider in Registrar.settings_providers:
+            mach.settings.register_provider(provider)
+
+        if config:
+            if isinstance(config, basestring):
+                config = StringIO(config)
+            mach.settings.load_fps([config])
+
+        context = CommandContext(settings=mach.settings)
+        return mach.get_argument_parser(context)
+
+    def test_command_aliases(self):
+        config = """
+[alias]
+foo = cmd_foo
+bar = cmd_bar
+baz = cmd_bar --baz
+cmd_bar = cmd_bar --baz
+"""
+        parser = self.get_parser(config=config)
+
+        args = parser.parse_args(['foo'])
+        self.assertEquals(args.command, 'cmd_foo')
+
+        def assert_bar_baz(argv):
+            args = parser.parse_args(argv)
+            self.assertEquals(args.command, 'cmd_bar')
+            self.assertTrue(args.command_args.baz)
+
+        # The following should all result in |cmd_bar --baz|
+        assert_bar_baz(['bar', '--baz'])
+        assert_bar_baz(['baz'])
+        assert_bar_baz(['cmd_bar'])
+
+
+if __name__ == '__main__':
+    main()
--- a/python/moz.build
+++ b/python/moz.build
@@ -17,16 +17,17 @@ SPHINX_PYTHON_PACKAGE_DIRS += [
     'mozversioncontrol/mozversioncontrol',
 ]
 
 SPHINX_TREES['mach'] = 'mach/docs'
 
 PYTHON_UNIT_TESTS += [
     'mach/mach/test/test_conditions.py',
     'mach/mach/test/test_config.py',
+    'mach/mach/test/test_dispatcher.py',
     'mach/mach/test/test_entry_point.py',
     'mach/mach/test/test_error_output.py',
     'mach/mach/test/test_logger.py',
     'mozbuild/dumbmake/test/test_dumbmake.py',
     'mozbuild/mozbuild/test/action/test_buildlist.py',
     'mozbuild/mozbuild/test/action/test_generate_browsersearch.py',
     'mozbuild/mozbuild/test/action/test_package_fennec_apk.py',
     'mozbuild/mozbuild/test/backend/test_android_eclipse.py',