Bug 1062221 - Add a TypedList type and refactor mozbuild.util lists. r=gps
authorMike Hommey <mh+mozilla@glandium.org>
Thu, 02 Oct 2014 09:14:06 +0900
changeset 231498 0a2548fb4c6a838227363af776f6f6f349497533
parent 231497 3103826177af7977b5090b81ccb84e4105710070
child 231499 23eb4e460b71abd665b6c872b1a395b34134410b
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1062221
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 1062221 - Add a TypedList type and refactor mozbuild.util lists. r=gps
python/mozbuild/mozbuild/test/test_util.py
python/mozbuild/mozbuild/util.py
--- a/python/mozbuild/mozbuild/test/test_util.py
+++ b/python/mozbuild/mozbuild/test/test_util.py
@@ -23,16 +23,17 @@ from mozbuild.util import (
     memoize,
     memoized_property,
     resolve_target_to_make,
     MozbuildDeletionError,
     HierarchicalStringList,
     HierarchicalStringListWithFlagsFactory,
     StrictOrderingOnAppendList,
     StrictOrderingOnAppendListWithFlagsFactory,
+    TypedList,
     UnsortedError,
 )
 
 if sys.version_info[0] == 3:
     str_type = 'str'
 else:
     str_type = 'unicode'
 
@@ -532,10 +533,133 @@ class TestMemoize(unittest.TestCase):
 
         instance = foo()
         self.assertEqual(instance._count, 0)
         self.assertEqual(instance.wrapped, 42)
         self.assertEqual(instance._count, 1)
         self.assertEqual(instance.wrapped, 42)
         self.assertEqual(instance._count, 1)
 
+
+class TestTypedList(unittest.TestCase):
+    def test_init(self):
+        cls = TypedList(int)
+        l = cls()
+        self.assertEqual(len(l), 0)
+
+        l = cls([1, 2, 3])
+        self.assertEqual(len(l), 3)
+
+        with self.assertRaises(ValueError):
+            cls([1, 2, 'c'])
+
+    def test_extend(self):
+        cls = TypedList(int)
+        l = cls()
+        l.extend([1, 2])
+        self.assertEqual(len(l), 2)
+        self.assertIsInstance(l, cls)
+
+        with self.assertRaises(ValueError):
+            l.extend([3, 'c'])
+
+        self.assertEqual(len(l), 2)
+
+    def test_slicing(self):
+        cls = TypedList(int)
+        l = cls()
+        l[:] = [1, 2]
+        self.assertEqual(len(l), 2)
+        self.assertIsInstance(l, cls)
+
+        with self.assertRaises(ValueError):
+            l[:] = [3, 'c']
+
+        self.assertEqual(len(l), 2)
+
+    def test_add(self):
+        cls = TypedList(int)
+        l = cls()
+        l2 = l + [1, 2]
+        self.assertEqual(len(l), 0)
+        self.assertEqual(len(l2), 2)
+        self.assertIsInstance(l2, cls)
+
+        with self.assertRaises(ValueError):
+            l2 = l + [3, 'c']
+
+        self.assertEqual(len(l), 0)
+
+    def test_iadd(self):
+        cls = TypedList(int)
+        l = cls()
+        l += [1, 2]
+        self.assertEqual(len(l), 2)
+        self.assertIsInstance(l, cls)
+
+        with self.assertRaises(ValueError):
+            l += [3, 'c']
+
+        self.assertEqual(len(l), 2)
+
+    def test_add_coercion(self):
+        objs = []
+
+        class Foo(object):
+            def __init__(self, obj):
+                objs.append(obj)
+
+        cls = TypedList(Foo)
+        l = cls()
+        l += [1, 2]
+        self.assertEqual(len(objs), 2)
+        self.assertEqual(type(l[0]), Foo)
+        self.assertEqual(type(l[1]), Foo)
+
+        # Adding a TypedList to a TypedList shouldn't trigger coercion again
+        l2 = cls()
+        l2 += l
+        self.assertEqual(len(objs), 2)
+        self.assertEqual(type(l2[0]), Foo)
+        self.assertEqual(type(l2[1]), Foo)
+
+        # Adding a TypedList to a TypedList shouldn't even trigger the code
+        # that does coercion at all.
+        l2 = cls()
+        list.__setslice__(l, 0, -1, [1, 2])
+        l2 += l
+        self.assertEqual(len(objs), 2)
+        self.assertEqual(type(l2[0]), int)
+        self.assertEqual(type(l2[1]), int)
+
+    def test_memoized(self):
+        cls = TypedList(int)
+        cls2 = TypedList(str)
+        self.assertEqual(TypedList(int), cls)
+        self.assertNotEqual(cls, cls2)
+
+
+class TypedTestStrictOrderingOnAppendList(unittest.TestCase):
+    def test_init(self):
+        class Unicode(unicode):
+            def __init__(self, other):
+                if not isinstance(other, unicode):
+                    raise ValueError()
+                super(Unicode, self).__init__(other)
+
+        cls = TypedList(Unicode, StrictOrderingOnAppendList)
+        l = cls()
+        self.assertEqual(len(l), 0)
+
+        l = cls(['a', 'b', 'c'])
+        self.assertEqual(len(l), 3)
+
+        with self.assertRaises(UnsortedError):
+            cls(['c', 'b', 'a'])
+
+        with self.assertRaises(ValueError):
+            cls(['a', 'b', 3])
+
+        self.assertEqual(len(l), 3)
+
+
 if __name__ == '__main__':
     main()
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -228,52 +228,61 @@ def resolve_target_to_make(topobjdir, ta
         # happens exactly once.
         if target != 'Makefile' and os.path.exists(make_path):
             return (reldir, target)
 
         target = os.path.join(os.path.basename(reldir), target)
         reldir = os.path.dirname(reldir)
 
 
-class List(list):
-    """A list specialized for moz.build environments.
+class ListMixin(object):
+    def __init__(self, iterable=[]):
+        if not isinstance(iterable, list):
+            raise ValueError('List can only be created from other list instances.')
 
-    We overload the assignment and append operations to require that the
-    appended thing is a list. This avoids bad surprises coming from appending
-    a string to a list, which would just add each letter of the string.
-    """
+        return super(ListMixin, self).__init__(iterable)
+
     def extend(self, l):
         if not isinstance(l, list):
             raise ValueError('List can only be extended with other list instances.')
 
-        return list.extend(self, l)
+        return super(ListMixin, self).extend(l)
 
     def __setslice__(self, i, j, sequence):
         if not isinstance(sequence, list):
             raise ValueError('List can only be sliced with other list instances.')
 
-        return list.__setslice__(self, i, j, sequence)
+        return super(ListMixin, self).__setslice__(i, j, sequence)
 
     def __add__(self, other):
         # Allow None is a special case because it makes undefined variable
         # references in moz.build behave better.
         other = [] if other is None else other
         if not isinstance(other, list):
             raise ValueError('Only lists can be appended to lists.')
 
-        return List(list.__add__(self, other))
+        new_list = self.__class__(self)
+        new_list.extend(other)
+        return new_list
 
     def __iadd__(self, other):
         other = [] if other is None else other
         if not isinstance(other, list):
             raise ValueError('Only lists can be appended to lists.')
 
-        list.__iadd__(self, other)
+        return super(ListMixin, self).__iadd__(other)
+
 
-        return self
+class List(ListMixin, list):
+    """A list specialized for moz.build environments.
+
+    We overload the assignment and append operations to require that the
+    appended thing is a list. This avoids bad surprises coming from appending
+    a string to a list, which would just add each letter of the string.
+    """
 
 
 class UnsortedError(Exception):
     def __init__(self, srtd, original):
         assert len(srtd) == len(original)
 
         self.sorted = srtd
         self.original = original
@@ -292,73 +301,61 @@ class UnsortedError(Exception):
         s.write('The incoming list is unsorted starting at element %d. ' %
             self.i)
         s.write('We expected "%s" but got "%s"' % (
             self.sorted[self.i], self.original[self.i]))
 
         return s.getvalue()
 
 
-class StrictOrderingOnAppendList(list):
-    """A list specialized for moz.build environments.
-
-    We overload the assignment and append operations to require that incoming
-    elements be ordered. This enforces cleaner style in moz.build files.
-    """
+class StrictOrderingOnAppendListMixin(object):
     @staticmethod
     def ensure_sorted(l):
         if isinstance(l, StrictOrderingOnAppendList):
             return
 
         srtd = sorted(l, key=lambda x: x.lower())
 
         if srtd != l:
             raise UnsortedError(srtd, l)
 
     def __init__(self, iterable=[]):
-        StrictOrderingOnAppendList.ensure_sorted(iterable)
+        StrictOrderingOnAppendListMixin.ensure_sorted(iterable)
 
-        list.__init__(self, iterable)
+        super(StrictOrderingOnAppendListMixin, self).__init__(iterable)
 
     def extend(self, l):
-        if not isinstance(l, list):
-            raise ValueError('List can only be extended with other list instances.')
+        StrictOrderingOnAppendListMixin.ensure_sorted(l)
 
-        StrictOrderingOnAppendList.ensure_sorted(l)
-
-        return list.extend(self, l)
+        return super(StrictOrderingOnAppendListMixin, self).extend(l)
 
     def __setslice__(self, i, j, sequence):
-        if not isinstance(sequence, list):
-            raise ValueError('List can only be sliced with other list instances.')
+        StrictOrderingOnAppendListMixin.ensure_sorted(sequence)
 
-        StrictOrderingOnAppendList.ensure_sorted(sequence)
-
-        return list.__setslice__(self, i, j, sequence)
+        return super(StrictOrderingOnAppendListMixin, self).__setslice__(i, j,
+            sequence)
 
     def __add__(self, other):
-        if not isinstance(other, list):
-            raise ValueError('Only lists can be appended to lists.')
+        StrictOrderingOnAppendListMixin.ensure_sorted(other)
 
-        new_list = StrictOrderingOnAppendList()
-        # Can't extend with self because it may already be the result of
-        # several extensions and not be ordered.
-        list.extend(new_list, self)
-        new_list.extend(other)
-        return new_list
+        return super(StrictOrderingOnAppendListMixin, self).__add__(other)
 
     def __iadd__(self, other):
-        if not isinstance(other, list):
-            raise ValueError('Only lists can be appended to lists.')
+        StrictOrderingOnAppendListMixin.ensure_sorted(other)
+
+        return super(StrictOrderingOnAppendListMixin, self).__iadd__(other)
+
 
-        StrictOrderingOnAppendList.ensure_sorted(other)
+class StrictOrderingOnAppendList(ListMixin, StrictOrderingOnAppendListMixin,
+        list):
+    """A list specialized for moz.build environments.
 
-        list.__iadd__(self, other)
-
-        return self
+    We overload the assignment and append operations to require that incoming
+    elements be ordered. This enforces cleaner style in moz.build files.
+    """
 
 
 class MozbuildDeletionError(Exception):
     pass
 
 
 def FlagsFactory(flags):
     """Returns a class which holds optional flags for an item in a list.
@@ -812,8 +809,69 @@ class memoized_property(object):
     def __init__(self, func):
         self.func = func
 
     def __get__(self, instance, cls):
         name = '_%s' % self.func.__name__
         if not hasattr(instance, name):
             setattr(instance, name, self.func(instance))
         return getattr(instance, name)
+
+
+class TypedListMixin(object):
+    '''Mixin for a list with type coercion. See TypedList.'''
+
+    def _ensure_type(self, l):
+        if isinstance(l, self.__class__):
+            return l
+
+        def normalize(e):
+            if not isinstance(e, self.TYPE):
+                e = self.TYPE(e)
+            return e
+
+        return [normalize(e) for e in l]
+
+    def __init__(self, iterable=[]):
+        iterable = self._ensure_type(iterable)
+
+        super(TypedListMixin, self).__init__(iterable)
+
+    def extend(self, l):
+        l = self._ensure_type(l)
+
+        return super(TypedListMixin, self).extend(l)
+
+    def __setslice__(self, i, j, sequence):
+        sequence = self._ensure_type(sequence)
+
+        return super(TypedListMixin, self).__setslice__(i, j,
+            sequence)
+
+    def __add__(self, other):
+        other = self._ensure_type(other)
+
+        return super(TypedListMixin, self).__add__(other)
+
+    def __iadd__(self, other):
+        other = self._ensure_type(other)
+
+        return super(TypedListMixin, self).__iadd__(other)
+
+    def append(self, other):
+        self += [other]
+
+
+@memoize
+def TypedList(type, base_class=List):
+    '''A list with type coercion.
+
+    The given ``type`` is what list elements are being coerced to. It may do
+    strict validation, throwing ValueError exceptions.
+
+    A ``base_class`` type can be given for more specific uses than a List. For
+    example, a Typed StrictOrderingOnAppendList can be created with:
+
+       TypedList(unicode, StrictOrderingOnAppendList)
+    '''
+    class _TypedList(TypedListMixin, base_class):
+        TYPE = type
+    return _TypedList