Bug 1065306 - Part 3: Extract DotProperties helper. r=lucasr,mshal
authorNick Alexander <nalexander@mozilla.com>
Wed, 01 Oct 2014 23:23:28 -0700
changeset 208448 0b6eef1ff9cda54bb6adc861f25cfa134bd162fd
parent 208447 d305d2d5576adc64a033f2c4721fb61722d7b260
child 208449 f2901a47e53d27bcda68a23795ca4ba41f0b346e
push id27584
push userryanvm@gmail.com
push dateThu, 02 Oct 2014 17:12:29 +0000
treeherdermozilla-central@b85c260821ab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr, mshal
bugs1065306
milestone35.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 1065306 - Part 3: Extract DotProperties helper. r=lucasr,mshal The only substantive change here is to stop stripping the 'browser.suggestedsites.' prefix from each line when reading region.properties.
python/moz.build
python/mozbuild/mozbuild/action/generate_suggestedsites.py
python/mozbuild/mozbuild/dotproperties.py
python/mozbuild/mozbuild/test/test_dotproperties.py
--- a/python/moz.build
+++ b/python/moz.build
@@ -38,16 +38,17 @@ PYTHON_UNIT_TESTS += [
     'mozbuild/mozbuild/test/frontend/__init__.py',
     'mozbuild/mozbuild/test/frontend/test_context.py',
     'mozbuild/mozbuild/test/frontend/test_emitter.py',
     'mozbuild/mozbuild/test/frontend/test_namespaces.py',
     'mozbuild/mozbuild/test/frontend/test_reader.py',
     'mozbuild/mozbuild/test/frontend/test_sandbox.py',
     'mozbuild/mozbuild/test/test_base.py',
     'mozbuild/mozbuild/test/test_containers.py',
+    'mozbuild/mozbuild/test/test_dotproperties.py',
     'mozbuild/mozbuild/test/test_expression.py',
     'mozbuild/mozbuild/test/test_jarmaker.py',
     'mozbuild/mozbuild/test/test_line_endings.py',
     'mozbuild/mozbuild/test/test_makeutil.py',
     'mozbuild/mozbuild/test/test_mozconfig.py',
     'mozbuild/mozbuild/test/test_mozinfo.py',
     'mozbuild/mozbuild/test/test_preprocessor.py',
     'mozbuild/mozbuild/test/test_pythonutil.py',
--- a/python/mozbuild/mozbuild/action/generate_suggestedsites.py
+++ b/python/mozbuild/mozbuild/action/generate_suggestedsites.py
@@ -24,82 +24,44 @@ 4. Generate a JSON representation of eac
 write the result to suggestedsites.json on the locale-specific raw resource
 directory e.g. raw/suggestedsites.json, raw-pt-rBR/suggestedsites.json.
 '''
 
 from __future__ import print_function
 
 import argparse
 import json
-import re
 import sys
 import os
 
+from mozbuild.dotproperties import (
+    DotProperties,
+)
 from mozbuild.util import (
     FileAvoidWrite,
 )
 from mozpack.files import (
     FileFinder,
 )
 import mozpack.path as mozpath
 
 
-def read_properties_file(filename):
-    """Reads a properties file into a dict.
-
-    Ignores empty, comment lines, and keys not starting with the prefix for
-    suggested sites ('browser.suggestedsites'). Removes the prefix from all
-    matching keys i.e. turns 'browser.suggestedsites.foo' into simply 'foo'
-    """
-    prefix = 'browser.suggestedsites.'
-    properties = {}
-    for l in open(filename, 'rt').readlines():
-        line = l.strip()
-        if not line.startswith(prefix):
-            continue
-        (k, v) = re.split('\s*=\s*', line, 1)
-        properties[k[len(prefix):]] = v
-    return properties
-
-
 def merge_properties(filename, srcdirs):
     """Merges properties from the given file in the given source directories."""
-    properties = {}
+    properties = DotProperties()
     for srcdir in srcdirs:
         path = mozpath.join(srcdir, filename)
         try:
-            properties.update(read_properties_file(path))
-        except IOError, e:
+            properties.update(path)
+        except IOError:
             # Ignore non-existing files
             continue
     return properties
 
 
-def get_site_list_from_properties(properties):
-    """Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar']."""
-    prefix = 'list.'
-    indexes = []
-    for k, v in properties.iteritems():
-        if not k.startswith(prefix):
-            continue
-        indexes.append(int(k[len(prefix):]))
-    return [properties[prefix + str(index)] for index in sorted(indexes)]
-
-
-def get_site_from_properties(name, properties):
-    """Turns {'foo.title':'title', ...} into {'title':'title', ...}."""
-    prefix = '{name}.'.format(name=name)
-    try:
-        site = dict((k, properties[prefix + k]) for k in ('title', 'url', 'bgcolor'))
-    except IndexError, e:
-        raise Exception("Could not find required property for '{name}: {error}'"
-                        .format(name=name, error=str(e)))
-    return site
-
-
 def main(args):
     parser = argparse.ArgumentParser()
     parser.add_argument('--verbose', '-v', default=False, action='store_true',
                         help='be verbose')
     parser.add_argument('--silent', '-s', default=False, action='store_true',
                         help='be silent')
     parser.add_argument('--android-package-name', metavar='NAME',
                         required=True,
@@ -110,30 +72,30 @@ def main(args):
     parser.add_argument('--srcdir', metavar='SRCDIR',
                         action='append', required=True,
                         help='directories to read inputs from, in order of priority')
     parser.add_argument('output', metavar='OUTPUT',
                         help='output')
     opts = parser.parse_args(args)
 
     # Use reversed order so that the first srcdir has higher priority to override keys.
-    all_properties = merge_properties('region.properties', reversed(opts.srcdir))
-    names = get_site_list_from_properties(all_properties)
+    properties = merge_properties('region.properties', reversed(opts.srcdir))
+    names = properties.get_list('browser.suggestedsites.list')
     if opts.verbose:
         print('Reading {len} suggested sites: {names}'.format(len=len(names), names=names))
 
     # Keep these two in sync.
     image_url_template = 'android.resource://%s/drawable/suggestedsites_{name}' % opts.android_package_name
     drawables_template = 'drawable*/suggestedsites_{name}.*'
 
     # Load properties corresponding to each site name and define their
     # respective image URL.
     sites = []
     for name in names:
-        site = get_site_from_properties(name, all_properties)
+        site = properties.get_dict('browser.suggestedsites.{name}'.format(name=name), required_keys=('title', 'url', 'bgcolor'))
         site['imageurl'] = image_url_template.format(name=name)
         sites.append(site)
 
         # Now check for existence of an appropriately named drawable.  If none
         # exists, throw.  This stops a locale discovering, at runtime, that the
         # corresponding drawable was not added to en-US.
         if not opts.resources:
             continue
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/dotproperties.py
@@ -0,0 +1,79 @@
+# 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/.
+
+# This file contains utility functions for reading .properties files, like
+# region.properties.
+
+
+from __future__ import unicode_literals
+
+import re
+import os
+import sys
+
+if sys.version_info[0] == 3:
+    str_type = str
+else:
+    str_type = basestring
+
+class DotProperties:
+    r'''A thin representation of a key=value .properties file.'''
+
+    def __init__(self, file=None):
+        self._properties = {}
+        if file:
+            self.update(file)
+
+    def update(self, file):
+        '''Updates properties from a file name or file-like object.
+
+        Ignores empty lines and comment lines.'''
+
+        if isinstance(file, str_type):
+            f = open(file, 'rt')
+        else:
+            f = file
+
+        for l in f.readlines():
+            line = l.strip()
+            if not line or line.startswith('#'):
+                continue
+            (k, v) = re.split('\s*=\s*', line, 1)
+            self._properties[k] = v
+
+    def get(self, key, default=None):
+        return self._properties.get(key, default)
+
+    def get_list(self, prefix):
+        '''Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar'].
+
+        Returns [] to indicate an empty or missing list.'''
+
+        if not prefix.endswith('.'):
+            prefix = prefix + '.'
+        indexes = []
+        for k, v in self._properties.iteritems():
+            if not k.startswith(prefix):
+                continue
+            indexes.append(int(k[len(prefix):]))
+        return [self._properties[prefix + str(index)] for index in sorted(indexes)]
+
+    def get_dict(self, prefix, required_keys=[]):
+        '''Turns {'foo.title':'title', ...} into {'title':'title', ...}.
+
+        If |required_keys| is present, it must be an iterable of required key
+        names.  If a required key is not present, ValueError is thrown.
+
+        Returns {} to indicate an empty or missing dict.'''
+
+        if not prefix.endswith('.'):
+            prefix = prefix + '.'
+
+        D = dict((k[len(prefix):], v) for k, v in self._properties.iteritems() if k.startswith(prefix))
+
+        for required_key in required_keys:
+            if not required_key in D:
+                raise ValueError('Required key %s not present' % required_key)
+
+        return D
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/test_dotproperties.py
@@ -0,0 +1,89 @@
+# 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
+import sys
+import unittest
+
+from StringIO import StringIO
+
+from mozbuild.dotproperties import (
+    DotProperties,
+)
+
+from mozunit import (
+    main,
+)
+
+if sys.version_info[0] == 3:
+    str_type = 'str'
+else:
+    str_type = 'unicode'
+
+
+class TestDotProperties(unittest.TestCase):
+    def test_get(self):
+        contents = StringIO('''
+key=value
+''')
+        p = DotProperties(contents)
+        self.assertEqual(p.get('missing'), None)
+        self.assertEqual(p.get('missing', 'default'), 'default')
+        self.assertEqual(p.get('key'), 'value')
+
+
+    def test_update(self):
+        contents = StringIO('''
+old=old value
+key=value
+''')
+        p = DotProperties(contents)
+        self.assertEqual(p.get('old'), 'old value')
+        self.assertEqual(p.get('key'), 'value')
+
+        new_contents = StringIO('''
+key=new value
+''')
+        p.update(new_contents)
+        self.assertEqual(p.get('old'), 'old value')
+        self.assertEqual(p.get('key'), 'new value')
+
+
+    def test_get_list(self):
+        contents = StringIO('''
+list.0=A
+list.1=B
+list.2=C
+
+order.1=B
+order.0=A
+order.2=C
+''')
+        p = DotProperties(contents)
+        self.assertEqual(p.get_list('missing'), [])
+        self.assertEqual(p.get_list('list'), ['A', 'B', 'C'])
+        self.assertEqual(p.get_list('order'), ['A', 'B', 'C'])
+
+
+    def test_get_dict(self):
+        contents = StringIO('''
+A.title=title A
+
+B.title=title B
+B.url=url B
+''')
+        p = DotProperties(contents)
+        self.assertEqual(p.get_dict('missing'), {})
+        self.assertEqual(p.get_dict('A'), {'title': 'title A'})
+        self.assertEqual(p.get_dict('B'), {'title': 'title B', 'url': 'url B'})
+        with self.assertRaises(ValueError):
+            p.get_dict('A', required_keys=['title', 'url'])
+        with self.assertRaises(ValueError):
+            p.get_dict('missing', required_keys=['key'])
+
+
+if __name__ == '__main__':
+    main()