Bug 1132771 - Implement strongly typed named tuples; r=glandium
authorGregory Szorc <gps@mozilla.com>
Thu, 26 Feb 2015 09:38:43 -0800
changeset 231461 7f95e5b32756598efd2acda1ba525de177445704
parent 231460 58044df73cb096b392fb380ce66591cc049b05ce
child 231462 d5c17696d9d598dd0de47d4d1ca01217d070c62d
push id28352
push usercbook@mozilla.com
push dateTue, 03 Mar 2015 12:51:09 +0000
treeherdermozilla-central@e545f650c695 [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):