Bug 1393590 - [mach] Use description field for settings instead of gettext locales, r?gps draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 24 Aug 2017 16:17:40 -0400
changeset 653087 b09fc6e1851e74e25d9464be51e09ced2c22eaf1
parent 652924 b6b8e616de32af50c9a174006b3a7ed914130aa5
child 728259 daba62c31ef5d59281399116302f1d76950956fb
push id76237
push userahalberstadt@mozilla.com
push dateFri, 25 Aug 2017 15:13:27 +0000
reviewersgps
bugs1393590
milestone57.0a1
Bug 1393590 - [mach] Use description field for settings instead of gettext locales, r?gps This preserves ./mach settings' --list option. If --list is passed in, we call splitlines() on the description and print only the first line. The full multi-line description will be printed otherwise. This also displays the type and/or choices of the option as appropriate. MozReview-Commit-ID: 7UMsN9qslWt
python/mach/docs/settings.rst
python/mach/mach/commands/settings.py
python/mach/mach/config.py
python/mach/mach/decorators.py
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/test_config.py
python/mozbuild/mozbuild/locale/en-US/LC_MESSAGES/mozbuild.mo
python/mozbuild/mozbuild/locale/en-US/LC_MESSAGES/mozbuild.po
python/mozbuild/mozbuild/locale/en_US/LC_MESSAGES/runprefs.mo
python/mozbuild/mozbuild/locale/en_US/LC_MESSAGES/runprefs.po
python/mozbuild/mozbuild/mach_commands.py
--- a/python/mach/docs/settings.rst
+++ b/python/mach/docs/settings.rst
@@ -38,33 +38,37 @@ decorator in an existing mach command mo
 
 .. code-block:: python
 
     from mach.decorators import SettingsProvider
 
     @SettingsProvider
     class ArbitraryClassName(object):
         config_settings = [
-            ('foo.bar', 'string'),
-            ('foo.baz', 'int', 0, set([0,1,2])),
+            ('foo.bar', 'string', "A helpful description"),
+            ('foo.baz', 'int', "Another description", 0, {'choices': set([0,1,2])}),
         ]
 
 ``@SettingsProvider``'s must specify a variable called ``config_settings``
 that returns a list of tuples. Alternatively, it can specify a function
 called ``config_settings`` that returns a list of tuples.
 
 Each tuple is of the form:
 
 .. code-block:: python
 
-    ('<section>.<option>', '<type>', default, extra)
+    ('<section>.<option>', '<type>', '<description>', default, extra)
 
 ``type`` is a string and can be one of:
 string, boolean, int, pos_int, path
 
+``description`` is a string explaining how to define the settings and
+where they get used. Descriptions should ideally be multi-line paragraphs
+where the first line acts as a short description.
+
 ``default`` is optional, and provides a default value in case none was
 specified by any of the configuration files.
 
 ``extra`` is also optional and is a dict containing additional key/value
 pairs to add to the setting's metadata. The following keys may be specified
 in the ``extra`` dict:
     * ``choices`` - A set of allowed values for the setting.
 
@@ -72,41 +76,37 @@ Wildcards
 ---------
 
 Sometimes a section should allow arbitrarily defined options from the user, such
 as the ``alias`` section mentioned above. To define a section like this, use ``*``
 as the option name. For example:
 
 .. parsed-literal::
 
-    ('foo.*', 'string')
+    ('foo.*', 'string', 'desc')
 
 This allows configuration files like this:
 
 .. parsed-literal::
 
     [foo]
     arbitrary1 = some string
     arbitrary2 = some other string
 
 
-Documenting Settings
-====================
+Finding Settings
+================
 
-All settings must at least be documented in the en_US locale. Otherwise,
-running ``mach settings`` will raise. Mach uses gettext to perform localization.
-
-A handy command exists to generate the localization files:
+You can see which settings are available as well as their description and
+expected values by running:
 
 .. parsed-literal::
 
-    mach settings locale-gen <section>
-
-You'll be prompted to add documentation for all options in section with the
-en_US locale. To add documentation in another locale, pass in ``--locale``.
+    ./mach settings  # or
+    ./mach settings --list
 
 
 Accessing Settings
 ==================
 
 Now that the settings are defined and documented, they're accessible from
 individual mach commands if the command receives a context in its constructor.
 For example:
@@ -117,19 +117,19 @@ For example:
         Command,
         CommandProvider,
         SettingsProvider,
     )
 
     @SettingsProvider
     class ExampleSettings(object):
         config_settings = [
-            ('a.b', 'string', 'default'),
-            ('foo.bar', 'string'),
-            ('foo.baz', 'int', 0, {'choices': set([0,1,2])}),
+            ('a.b', 'string', 'desc', 'default'),
+            ('foo.bar', 'string', 'desc',),
+            ('foo.baz', 'int', 'desc', 0, {'choices': set([0,1,2])}),
         ]
 
     @CommandProvider
     class Commands(object):
         def __init__(self, context):
             self.settings = context.settings
 
         @Command('command', category='misc',
--- a/python/mach/mach/commands/settings.py
+++ b/python/mach/mach/commands/settings.py
@@ -1,31 +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 absolute_import, print_function, unicode_literals
 
-import os
 from textwrap import TextWrapper
 
+from mach.config import TYPE_CLASSES
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
-    SubCommand,
 )
 
-POLIB_NOT_FOUND = """
-Could not detect the 'polib' package on the local system.
-Please run:
-
-    pip install polib
-""".lstrip()
-
 
 @CommandProvider
 class Settings(object):
     """Interact with settings for mach.
 
     Currently, we only provide functionality to view what settings are
     available. In the future, this module will be used to modify settings, help
     people create configs via a wizard, etc.
@@ -34,99 +26,32 @@ class Settings(object):
         self._settings = context.settings
 
     @Command('settings', category='devenv',
              description='Show available config settings.')
     @CommandArgument('-l', '--list', dest='short', action='store_true',
                      help='Show settings in a concise list')
     def settings(self, short=None):
         """List available settings."""
-        if short:
-            for section in sorted(self._settings):
-                for option in sorted(self._settings[section]._settings):
-                    short, full = self._settings.option_help(section, option)
-                    print('%s.%s -- %s' % (section, option, short))
-            return
-
+        types = {v: k for k, v in TYPE_CLASSES.items()}
         wrapper = TextWrapper(initial_indent='# ', subsequent_indent='# ')
-        for section in sorted(self._settings):
-            print('[%s]' % section)
+        for i, section in enumerate(sorted(self._settings)):
+            if not short:
+                print('%s[%s]' % ('' if i == 0 else '\n', section))
 
             for option in sorted(self._settings[section]._settings):
-                short, full = self._settings.option_help(section, option)
-                print(wrapper.fill(full))
-
-                if option != '*':
-                    print(';%s =' % option)
-                    print('')
+                meta = self._settings[section].get_meta(option)
+                desc = meta['description']
 
-    @SubCommand('settings', 'locale-gen',
-                description='Generate or update gettext .po and .mo locale files.')
-    @CommandArgument('sections', nargs='*',
-                     help='A list of strings in the form of either <section> or '
-                          '<section>.<option> to translate. By default, prompt to '
-                          'translate all applicable options.')
-    @CommandArgument('--locale', default='en_US',
-                     help='Locale to generate, defaults to en_US.')
-    @CommandArgument('--overwrite', action='store_true', default=False,
-                     help='Overwrite pre-existing entries in .po files.')
-    def locale_gen(self, sections, **kwargs):
-        try:
-            import polib
-        except ImportError:
-            print(POLIB_NOT_FOUND)
-            return 1
-
-        self.was_prompted = False
-
-        sections = sections or self._settings
-        for section in sections:
-            self._gen_section(section, **kwargs)
-
-        if not self.was_prompted:
-            print("All specified options already have an {} translation. "
-                  "To overwrite existing translations, pass --overwrite."
-                  .format(kwargs['locale']))
-
-    def _gen_section(self, section, **kwargs):
-        if '.' in section:
-            section, option = section.split('.')
-            return self._gen_option(section, option, **kwargs)
+                if short:
+                    print('%s.%s -- %s' % (section, option, desc.splitlines()[0]))
+                    continue
 
-        for option in sorted(self._settings[section]._settings):
-            self._gen_option(section, option, **kwargs)
-
-    def _gen_option(self, section, option, locale, overwrite):
-        import polib
-
-        meta = self._settings[section]._settings[option]
-
-        localedir = os.path.join(meta['localedir'], locale, 'LC_MESSAGES')
-        if not os.path.isdir(localedir):
-            os.makedirs(localedir)
-
-        path = os.path.join(localedir, '{}.po'.format(section))
-        if os.path.isfile(path):
-            po = polib.pofile(path)
-        else:
-            po = polib.POFile()
+                if option == '*':
+                    option = '<option>'
 
-        optionid = '{}.{}'.format(section, option)
-        for name in ('short', 'full'):
-            msgid = '{}.{}'.format(optionid, name)
-            entry = po.find(msgid)
-            if not entry:
-                entry = polib.POEntry(msgid=msgid)
-                po.append(entry)
-
-            if entry in po.translated_entries() and not overwrite:
-                continue
+                if 'choices' in meta:
+                    value = "{%s}" % ', '.join(meta['choices'])
+                else:
+                    value = '<%s>' % types[meta['type_cls']]
 
-            self.was_prompted = True
-
-            msgstr = raw_input("Translate {} to {}:\n"
-                               .format(msgid, locale))
-            entry.msgstr = msgstr
-
-        if self.was_prompted:
-            mopath = os.path.join(localedir, '{}.mo'.format(section))
-            po.save(path)
-            po.save_as_mofile(mopath)
+                print(wrapper.fill(desc))
+                print(';%s=%s' % (option, value))
--- a/python/mach/mach/config.py
+++ b/python/mach/mach/config.py
@@ -7,52 +7,33 @@ This file defines classes for representi
 
 Config data is modeled as key-value pairs. Keys are grouped together into named
 sections. Individual config settings (options) have metadata associated with
 them. This metadata includes type, default value, valid values, etc.
 
 The main interface to config data is the ConfigSettings class. 1 or more
 ConfigProvider classes are associated with ConfigSettings and define what
 settings are available.
-
-Descriptions of individual config options can be translated to multiple
-languages using gettext. Each option has associated with it a domain and locale
-directory. By default, the domain is the section the option is in and the
-locale directory is the "locale" directory beneath the directory containing the
-module that defines it.
-
-People implementing ConfigProvider instances are expected to define a complete
-gettext .po and .mo file for the en_US locale. The |mach settings locale-gen|
-command can be used to populate these files.
 """
 
 from __future__ import absolute_import, unicode_literals
 
 import collections
-import gettext
 import os
 import sys
 from functools import wraps
 
 if sys.version_info[0] == 3:
     from configparser import RawConfigParser, NoSectionError
     str_type = str
 else:
     from ConfigParser import RawConfigParser, NoSectionError
     str_type = basestring
 
 
-TRANSLATION_NOT_FOUND = """
-No translation files detected for {section}, there must at least be a
-translation for the 'en_US' locale. To generate these files, run:
-
-    mach settings locale-gen {section}
-""".lstrip()
-
-
 class ConfigException(Exception):
     pass
 
 
 class ConfigType(object):
     """Abstract base class for config values."""
 
     @staticmethod
@@ -331,47 +312,47 @@ class ConfigSettings(collections.Mapping
         for fp in fps:
             self._config.readfp(fp)
 
     def write(self, fh):
         """Write the config to a file object."""
         self._config.write(fh)
 
     @classmethod
-    def _format_metadata(cls, provider, section, option, type_cls,
+    def _format_metadata(cls, provider, section, option, type_cls, description,
                          default=DefaultValue, extra=None):
         """Formats and returns the metadata for a setting.
 
         Each setting must have:
 
             section -- str section to which the setting belongs. This is how
                 settings are grouped.
 
             option -- str id for the setting. This must be unique within the
                 section it appears.
 
             type -- a ConfigType-derived type defining the type of the setting.
 
+            description -- str describing how to use the setting and where it
+                applies.
+
         Each setting has the following optional parameters:
 
             default -- The default value for the setting. If None (the default)
                 there is no default.
 
             extra -- A dict of additional key/value pairs to add to the
                 setting metadata.
         """
         if isinstance(type_cls, basestring):
             type_cls = TYPE_CLASSES[type_cls]
 
         meta = {
-            'short': '%s.short' % option,
-            'full': '%s.full' % option,
+            'description': description,
             'type_cls': type_cls,
-            'domain': section,
-            'localedir': provider.config_settings_locale_directory,
         }
 
         if default != DefaultValue:
             meta['default'] = default
 
         if extra:
             meta.update(extra)
 
@@ -405,35 +386,16 @@ class ConfigSettings(collections.Mapping
                 if k in section:
                     raise ConfigException('Setting already registered: %s.%s' %
                                           section_name, k)
 
                 section[k] = v
 
             self._settings[section_name] = section
 
-    def option_help(self, section, option):
-        """Obtain the translated help messages for an option."""
-
-        meta = self[section].get_meta(option)
-
-        # Providers should always have an en_US translation. If they don't,
-        # they are coded wrong and this will raise.
-        default = gettext.translation(meta['domain'], meta['localedir'],
-                                      ['en_US'])
-
-        t = gettext.translation(meta['domain'], meta['localedir'],
-                                fallback=True)
-        t.add_fallback(default)
-
-        short = t.ugettext('%s.%s.short' % (section, option))
-        full = t.ugettext('%s.%s.full' % (section, option))
-
-        return (short, full)
-
     def _finalize(self):
         if self._finalized:
             return
 
         for section, settings in self._settings.items():
             s = ConfigSettings.ConfigSection(self._config, section, settings)
             self._sections[section] = s
 
--- a/python/mach/mach/decorators.py
+++ b/python/mach/mach/decorators.py
@@ -333,18 +333,14 @@ def SettingsProvider(cls):
     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.
     """
     if not hasattr(cls, 'config_settings'):
         raise MachError('@SettingsProvider must contain a config_settings attribute. It '
                         'may either be a list of tuples, or a callable that returns a list '
                         'of tuples. Each tuple must be of the form:\n'
-                        '(<section>.<option>, <type_cls>, <default>, <choices>)\n'
+                        '(<section>.<option>, <type_cls>, <description>, <default>, <choices>)\n'
                         'as specified by ConfigSettings._format_metadata.')
 
-    if not hasattr(cls, 'config_settings_locale_directory'):
-        cls_dir = os.path.dirname(inspect.getfile(cls))
-        cls.config_settings_locale_directory = os.path.join(cls_dir, 'locale')
-
     Registrar.register_settings_provider(cls)
     return cls
 
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -17,17 +17,21 @@ from .base import (
     UnrecognizedArgumentError,
 )
 from .decorators import SettingsProvider
 
 
 @SettingsProvider
 class DispatchSettings():
     config_settings = [
-        ('alias.*', 'string'),
+        ('alias.*', 'string', """
+Create a command alias of the form `<alias>=<command> <args>`.
+Aliases can also be used to set default arguments:
+<command>=<command> <args>
+""".strip()),
     ]
 
 
 class CommandFormatter(argparse.HelpFormatter):
     """Custom formatter to format just a subcommand."""
 
     def add_usage(self, *args):
         pass
deleted file mode 100644
index 6631808417c8d7e8fc1c79aa04e422d383e2960c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/python/mach/mach/locale/en_US/LC_MESSAGES/alias.po
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-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/test_config.py
+++ b/python/mach/mach/test/test_config.py
@@ -38,66 +38,66 @@ CONFIG2 = r"""
 [foo]
 
 bar = value2
 """
 
 @SettingsProvider
 class Provider1(object):
     config_settings = [
-        ('foo.bar', StringType),
-        ('foo.baz', PathType),
+        ('foo.bar', StringType, 'desc'),
+        ('foo.baz', PathType, 'desc'),
     ]
 
 
 @SettingsProvider
 class ProviderDuplicate(object):
     config_settings = [
-        ('dupesect.foo', StringType),
-        ('dupesect.foo', StringType),
+        ('dupesect.foo', StringType, 'desc'),
+        ('dupesect.foo', StringType, 'desc'),
     ]
 
 
 @SettingsProvider
 class Provider2(object):
     config_settings = [
-        ('a.string', StringType),
-        ('a.boolean', BooleanType),
-        ('a.pos_int', PositiveIntegerType),
-        ('a.int', IntegerType),
-        ('a.path', PathType),
+        ('a.string', StringType, 'desc'),
+        ('a.boolean', BooleanType, 'desc'),
+        ('a.pos_int', PositiveIntegerType, 'desc'),
+        ('a.int', IntegerType, 'desc'),
+        ('a.path', PathType, 'desc'),
     ]
 
 
 @SettingsProvider
 class Provider3(object):
     @classmethod
     def config_settings(cls):
         return [
-            ('a.string', 'string'),
-            ('a.boolean', 'boolean'),
-            ('a.pos_int', 'pos_int'),
-            ('a.int', 'int'),
-            ('a.path', 'path'),
+            ('a.string', 'string', 'desc'),
+            ('a.boolean', 'boolean', 'desc'),
+            ('a.pos_int', 'pos_int', 'desc'),
+            ('a.int', 'int', 'desc'),
+            ('a.path', 'path', 'desc'),
         ]
 
 
 @SettingsProvider
 class Provider4(object):
     config_settings = [
-        ('foo.abc', StringType, 'a', {'choices': set('abc')}),
-        ('foo.xyz', StringType, 'w', {'choices': set('xyz')}),
+        ('foo.abc', StringType, 'desc', 'a', {'choices': set('abc')}),
+        ('foo.xyz', StringType, 'desc', 'w', {'choices': set('xyz')}),
     ]
 
 
 @SettingsProvider
 class Provider5(object):
     config_settings = [
-        ('foo.*', 'string'),
-        ('foo.bar', 'string'),
+        ('foo.*', 'string', 'desc'),
+        ('foo.bar', 'string', 'desc'),
     ]
 
 
 class TestConfigSettings(unittest.TestCase):
     def test_empty(self):
         s = ConfigSettings()
 
         self.assertEqual(len(s), 0)
deleted file mode 100644
index be7711cb2fcfc927de59407bac54fb613dfab0fe..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/python/mozbuild/mozbuild/locale/en-US/LC_MESSAGES/mozbuild.po
+++ /dev/null
@@ -1,8 +0,0 @@
-msgid "build.threads.short"
-msgstr "Thread Count"
-
-msgid "build.threads.full"
-msgstr "The number of threads to use when performing CPU intensive tasks. "
-       "This constrols the level of parallelization. The default value is "
-       "the number of cores in your machine."
-
deleted file mode 100644
index 84a020d1976a96eed2b16d8f64c3d90ef20df5a2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/python/mozbuild/mozbuild/locale/en_US/LC_MESSAGES/runprefs.po
+++ /dev/null
@@ -1,10 +0,0 @@
-#
-msgid ""
-msgstr ""
-
-msgid "runprefs.*.short"
-msgstr "Pass a pref into Firefox when using `mach run`"
-
-msgid "runprefs.*.full"
-msgstr ""
-"Pass a pref into Firefox when using `mach run`, of the form foo.bar=value"
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -1274,17 +1274,21 @@ class Install(MachCommandBase):
         ret = self._run_make(directory=".", target='install', ensure_exit_code=False)
         if ret == 0:
             self.notify('Install complete')
         return ret
 
 @SettingsProvider
 class RunSettings():
     config_settings = [
-        ('runprefs.*', 'string'),
+        ('runprefs.*', 'string', """
+Pass a pref into Firefox when using `mach run`, of the form `foo.bar=value`.
+Prefs will automatically be cast into the appropriate type. Integers can be
+single quoted to force them to be strings.
+""".strip()),
     ]
 
 @CommandProvider
 class RunProgram(MachCommandBase):
     """Run the compiled program."""
 
     prog_group = 'the compiled program'