From d4f1a460622494c55354051bf6553a7c9cf5f3f1 Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Thu, 26 Feb 2015 09:38:43 -0800 Subject: [PATCH] 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. --HG-- extra : rebase_source : 7f4d17ff28925fbe8d850c036605aa03a38f0ef2 extra : source : acdd5491f10ecf8ea4e1a14150f9a2e282e2cf5d --- python/mozbuild/mozbuild/test/test_util.py | 28 ++++++++++++ python/mozbuild/mozbuild/util.py | 50 ++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/python/mozbuild/mozbuild/test/test_util.py b/python/mozbuild/mozbuild/test/test_util.py index c5eb89f36d87..3599a8cf3321 100644 --- a/python/mozbuild/mozbuild/test/test_util.py +++ b/python/mozbuild/mozbuild/test/test_util.py @@ -32,6 +32,7 @@ from mozbuild.util import ( StrictOrderingOnAppendList, StrictOrderingOnAppendListWithFlagsFactory, TypedList, + TypedNamedTuple, UnsortedError, ) @@ -663,6 +664,33 @@ class TypedTestStrictOrderingOnAppendList(unittest.TestCase): 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] diff --git a/python/mozbuild/mozbuild/util.py b/python/mozbuild/mozbuild/util.py index 87714c2b641d..f420e20d1757 100644 --- a/python/mozbuild/mozbuild/util.py +++ b/python/mozbuild/mozbuild/util.py @@ -817,6 +817,56 @@ class memoized_property(object): 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.'''