Bug 1255450 - [mach] Implement 'wildcard' settings for enabling sections with user-defined options, r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 28 Mar 2016 10:52:16 -0400
changeset 316590 99146ae25db725f0a1b37b624ec47fea07366e03
parent 316589 5f410a4601c184a77d6c9ac7a4fa5660aff7e359
child 316591 b552927e1a1fe9f916811236eb30352be72eaaaf
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1255450
milestone48.0a1
Bug 1255450 - [mach] Implement 'wildcard' settings for enabling sections with user-defined options, r=gps Some sections should support user-defined options. For example, in an [alias] section, the option names are not well-defined, rather specified by the user. This patch allows user-defined option names for any section that has a 'section.*' option defined. Even with 'section.*', option types are still well-defined. MozReview-Commit-ID: L34W9v9Fy28
python/mach/docs/settings.rst
python/mach/mach/config.py
python/mach/mach/test/test_config.py
--- a/python/mach/docs/settings.rst
+++ b/python/mach/docs/settings.rst
@@ -49,16 +49,36 @@ string, boolean, int, pos_int, path
 ``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.
 
+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')
+
+This allows configuration files like this:
+
+.. parsed-literal::
+
+    [foo]
+    arbitrary1 = some string
+    arbitrary2 = some other string
+
+
 
 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:
 
--- a/python/mach/mach/config.py
+++ b/python/mach/mach/config.py
@@ -208,28 +208,35 @@ class ConfigSettings(collections.Mapping
 
     class ConfigSection(collections.MutableMapping, object):
         """Represents an individual config section."""
         def __init__(self, config, name, settings):
             object.__setattr__(self, '_config', config)
             object.__setattr__(self, '_name', name)
             object.__setattr__(self, '_settings', settings)
 
+            wildcard = any(s == '*' for s in self._settings)
+            object.__setattr__(self, '_wildcard', wildcard)
+
         @property
         def options(self):
             try:
                 return self._config.options(self._name)
             except NoSectionError:
                 return []
 
+        def get_meta(self, option):
+            if option in self._settings:
+                return self._settings[option]
+            if self._wildcard:
+                return self._settings['*']
+            raise KeyError('Option not registered with provider: %s' % option)
+
         def _validate(self, option, value):
-            if option not in self._settings:
-                raise KeyError('Option not registered with provider: %s' % option)
-
-            meta = self._settings[option]
+            meta = self.get_meta(option)
             meta['type_cls'].validate(value)
 
             if 'choices' in meta and value not in meta['choices']:
                 raise ValueError("Value '%s' must be one of: %s" % (
                                  value, ', '.join(sorted(meta['choices']))))
 
         # MutableMapping interface
         def __len__(self):
@@ -237,34 +244,33 @@ class ConfigSettings(collections.Mapping
 
         def __iter__(self):
             return iter(self.options)
 
         def __contains__(self, k):
             return self._config.has_option(self._name, k)
 
         def __getitem__(self, k):
-            meta = self._settings[k]
+            meta = self.get_meta(k)
 
             if self._config.has_option(self._name, k):
                 v = meta['type_cls'].from_config(self._config, self._name, k)
             else:
                 v = meta.get('default', DefaultValue)
 
             if v == DefaultValue:
                 raise KeyError('No default value registered: %s' % k)
 
             self._validate(k, v)
             return v
 
-
         def __setitem__(self, k, v):
             self._validate(k, v)
+            meta = self.get_meta(k)
 
-            meta = self._settings[k]
             if not self._config.has_section(self._name):
                 self._config.add_section(self._name)
 
             self._config.set(self._name, k, meta['type_cls'].to_config(v))
 
         def __delitem__(self, k):
             self._config.remove_option(self._name, k)
 
@@ -411,17 +417,17 @@ class ConfigSettings(collections.Mapping
                 fh.write('msgid "%s.%s.full"\n' % (section, option))
                 fh.write('msgstr ""\n\n')
 
             fh.write('# End of section %s\n\n' % section)
 
     def option_help(self, section, option):
         """Obtain the translated help messages for an option."""
 
-        meta = self[section]._settings[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)
--- a/python/mach/mach/test/test_config.py
+++ b/python/mach/mach/test/test_config.py
@@ -83,16 +83,24 @@ class Provider3(object):
 @SettingsProvider
 class Provider4(object):
     config_settings = [
         ('foo.abc', StringType, 'a', {'choices': set('abc')}),
         ('foo.xyz', StringType, 'w', {'choices': set('xyz')}),
     ]
 
 
+@SettingsProvider
+class Provider5(object):
+    config_settings = [
+        ('foo.*', 'string'),
+        ('foo.bar', 'string'),
+    ]
+
+
 class TestConfigSettings(unittest.TestCase):
     def test_empty(self):
         s = ConfigSettings()
 
         self.assertEqual(len(s), 0)
         self.assertNotIn('foo', s)
 
     def test_duplicate_option(self):
@@ -206,16 +214,34 @@ class TestConfigSettings(unittest.TestCa
             foo.xyz
 
         with self.assertRaises(ValueError):
             foo.abc = 'e'
 
         foo.abc = 'b'
         foo.xyz = 'y'
 
+    def test_wildcard_options(self):
+        s = ConfigSettings()
+        s.register_provider(Provider5)
+
+        foo = s.foo
+
+        self.assertIn('*', foo._settings)
+        self.assertNotIn('*', foo)
+
+        foo.baz = 'value1'
+        foo.bar = 'value2'
+
+        self.assertIn('baz', foo)
+        self.assertEqual(foo.baz, 'value1')
+
+        self.assertIn('bar', foo)
+        self.assertEqual(foo.bar, 'value2')
+
     def test_file_reading_single(self):
         temp = NamedTemporaryFile(mode='wt')
         temp.write(CONFIG1)
         temp.flush()
 
         s = ConfigSettings()
         s.register_provider(Provider1)