Bug 1132771 - Implement strongly typed named tuples; r=glandium
☠☠ backed out by ce21e7a57cf8 ☠ ☠
authorGregory Szorc <gps@mozilla.com>
Thu, 26 Feb 2015 09:38:43 -0800
changeset 261575 acdd5491f10ecf8ea4e1a14150f9a2e282e2cf5d
parent 261574 4013d256b5910404cc04bb3caaf696b8ee551fc5
child 261576 ed135df395751194bf379584a4d210f14ac849b4
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs1132771
milestone39.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 1132771 - Implement strongly typed named tuples; r=glandium An upcoming patch introduces a use case for a strongly typed named tuple. So, we introduce a generic factory function that can produce these types.
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
@@ -27,16 +27,17 @@ from mozbuild.util import (
     memoized_property,
     resolve_target_to_make,
     MozbuildDeletionError,
     HierarchicalStringList,
     HierarchicalStringListWithFlagsFactory,
     StrictOrderingOnAppendList,
     StrictOrderingOnAppendListWithFlagsFactory,
     TypedList,
+    TypedNamedTuple,
     UnsortedError,
 )
 
 if sys.version_info[0] == 3:
     str_type = 'str'
 else:
     str_type = 'unicode'
 
@@ -658,16 +659,43 @@ class TypedTestStrictOrderingOnAppendLis
         with self.assertRaises(UnsortedError):
             cls(['c', 'b', 'a'])
 
         with self.assertRaises(ValueError):
             cls(['a', 'b', 3])
 
         self.assertEqual(len(l), 3)
 
+
+class TestTypedNamedTuple(unittest.TestCase):
+    def test_simple(self):
+        FooBar = TypedNamedTuple('FooBar', [('foo', unicode), ('bar', int)])
+
+        t = FooBar(foo='foo', bar=2)
+        self.assertEquals(type(t), FooBar)
+        self.assertEquals(t.foo, 'foo')
+        self.assertEquals(t.bar, 2)
+        self.assertEquals(t[0], 'foo')
+        self.assertEquals(t[1], 2)
+
+        FooBar('foo', 2)
+
+        with self.assertRaises(TypeError):
+            FooBar('foo', 'not integer')
+        with self.assertRaises(TypeError):
+            FooBar(2, 4)
+
+        # Passing a tuple as the first argument is the same as passing multiple
+        # arguments.
+        t1 = ('foo', 3)
+        t2 = FooBar(t1)
+        self.assertEquals(type(t2), FooBar)
+        self.assertEqual(FooBar(t1), FooBar('foo', 3))
+
+
 class TestGroupUnifiedFiles(unittest.TestCase):
     FILES = ['%s.cpp' % letter for letter in string.ascii_lowercase]
 
     def test_multiple_files(self):
         mapping = list(group_unified_files(self.FILES, 'Unified', 'cpp', 5))
 
         def check_mapping(index, expected_num_source_files):
             (unified_file, source_files) = mapping[index]
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -812,16 +812,66 @@ class memoized_property(object):
 
     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)
 
 
+def TypedNamedTuple(name, fields):
+    """Factory for named tuple types with strong typing.
+
+    Arguments are an iterable of 2-tuples. The first member is the
+    the field name. The second member is a type the field will be validated
+    to be.
+
+    Construction of instances varies from ``collections.namedtuple``.
+
+    First, if a single tuple argument is given to the constructor, this is
+    treated as the equivalent of passing each tuple value as a separate
+    argument into __init__. e.g.::
+
+        t = (1, 2)
+        TypedTuple(t) == TypedTuple(1, 2)
+
+    This behavior is meant for moz.build files, so vanilla tuples are
+    automatically cast to typed tuple instances.
+
+    Second, fields in the tuple are validated to be instances of the specified
+    type. This is done via an ``isinstance()`` check. To allow multiple types,
+    pass a tuple as the allowed types field.
+    """
+    cls = collections.namedtuple(name, (name for name, typ in fields))
+
+    class TypedTuple(cls):
+        __slots__ = ()
+
+        def __new__(klass, *args, **kwargs):
+            if len(args) == 1 and not kwargs and isinstance(args[0], tuple):
+                args = args[0]
+
+            return super(TypedTuple, klass).__new__(klass, *args, **kwargs)
+
+        def __init__(self, *args, **kwargs):
+            for i, (fname, ftype) in enumerate(self._fields):
+                value = self[i]
+
+                if not isinstance(value, ftype):
+                    raise TypeError('field in tuple not of proper type: %s; '
+                                    'got %s, expected %s' % (fname,
+                                    type(value), ftype))
+
+            super(TypedTuple, self).__init__(*args, **kwargs)
+
+    TypedTuple._fields = fields
+
+    return TypedTuple
+
+
 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):