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

--HG--
extra : rebase_source : 4bc9554d8652e02e290c6a190634f1a72cdbadc3
This commit is contained in:
Andrew Halberstadt 2017-08-24 16:17:40 -04:00
parent 4cd937870c
commit 508e9aad6b
13 changed files with 69 additions and 205 deletions

View File

@ -43,8 +43,8 @@ decorator in an existing mach command module. E.g:
@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``
@ -55,11 +55,15 @@ 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.
@ -77,7 +81,7 @@ as the option name. For example:
.. parsed-literal::
('foo.*', 'string')
('foo.*', 'string', 'desc')
This allows configuration files like this:
@ -88,20 +92,16 @@ This allows configuration files like this:
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
@ -122,9 +122,9 @@ For example:
@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

View File

@ -4,23 +4,15 @@
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):
@ -39,94 +31,27 @@ class Settings(object):
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))
meta = self._settings[section].get_meta(option)
desc = meta['description']
if option != '*':
print(';%s =' % option)
print('')
if short:
print('%s.%s -- %s' % (section, option, desc.splitlines()[0]))
continue
@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
if option == '*':
option = '<option>'
self.was_prompted = False
if 'choices' in meta:
value = "{%s}" % ', '.join(meta['choices'])
else:
value = '<%s>' % types[meta['type_cls']]
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)
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()
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
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))

View File

@ -12,22 +12,11 @@ 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
@ -40,14 +29,6 @@ else:
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
@ -336,7 +317,7 @@ class ConfigSettings(collections.Mapping):
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.
@ -350,6 +331,9 @@ class ConfigSettings(collections.Mapping):
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)
@ -362,11 +346,8 @@ class ConfigSettings(collections.Mapping):
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:
@ -410,25 +391,6 @@ class ConfigSettings(collections.Mapping):
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

View File

@ -338,13 +338,9 @@ def SettingsProvider(cls):
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

View File

@ -22,7 +22,11 @@ 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()),
]

View File

@ -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>`."

View File

@ -43,27 +43,27 @@ 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'),
]
@ -72,27 +72,27 @@ 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'),
]

View File

@ -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."

View File

@ -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"

View File

@ -1279,7 +1279,11 @@ class Install(MachCommandBase):
@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