Bug 1132716 - Allow setter functions in FlagsFactory draft
authorGregory Szorc <gps@mozilla.com>
Mon, 16 Feb 2015 11:27:11 -0800
changeset 243047 951663fa5951fccf20d9933fae30ce084b56bd3c
parent 242502 81f979b17fbdc4ffdc2c0657c8d8b6333383f9a5
child 243048 7f1ba6df67a715847ba99842269472c765219871
push id698
push usergszorc@mozilla.com
push dateMon, 16 Feb 2015 20:34:37 +0000
bugs1132716
milestone38.0a1
Bug 1132716 - Allow setter functions in FlagsFactory Advanced use cases of FlagsFactory may wish to perform additional data verification at attribute set time. This patch introduces a mechanism to allow setter functions to be declared for individual flags on FlagsFactory instances. The use case for this feature is to perform additional verification or type coercion at variable assignment time. For example, we may wish to convert a generic tuple into a dedicated type.
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
@@ -22,16 +22,17 @@ from mozunit import (
 from mozbuild.util import (
     FileAvoidWrite,
     group_unified_files,
     hash_file,
     memoize,
     memoized_property,
     resolve_target_to_make,
     MozbuildDeletionError,
+    FlagsFactory,
     HierarchicalStringList,
     HierarchicalStringListWithFlagsFactory,
     StrictOrderingOnAppendList,
     StrictOrderingOnAppendListWithFlagsFactory,
     TypedList,
     UnsortedError,
 )
 
@@ -466,16 +467,42 @@ class TestHierarchicalStringListWithFlag
 
         l.x['y'].foo = True
         self.assertEqual(l.x['y'].foo, True)
 
         with self.assertRaises(AttributeError):
             l.x['y'].baz = False
 
 
+class TestFlagsFactory(unittest.TestCase):
+    def test_get_set_callbacks(self):
+        def set1(f, n, v):
+            self.assertEqual(n, 'key1')
+            return v
+
+        def set2(f, n, v):
+            self.assertEqual(n, 'key2')
+            self.assertEqual(v, 'dummy')
+            return (n, v)
+
+        cls = FlagsFactory({
+            'key1': (unicode, set1),
+            'key2': (unicode, set2),
+        })
+        flags = cls()
+
+        self.assertEqual(flags.key1, '')
+
+        flags.key1 = 'foo'
+        self.assertEqual(flags.key1, 'foo')
+
+        flags.key2 = 'dummy'
+        self.assertEqual(flags.key2, ('key2', 'dummy'))
+
+
 class TestMemoize(unittest.TestCase):
     def test_memoize(self):
         self._count = 0
         @memoize
         def wrapped(a, b):
             self._count += 1
             return a + b
 
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -357,51 +357,77 @@ class StrictOrderingOnAppendList(ListMix
 class MozbuildDeletionError(Exception):
     pass
 
 
 def FlagsFactory(flags):
     """Returns a class which holds optional flags for an item in a list.
 
     The flags are defined in the dict given as argument, where keys are
-    the flag names, and values the type used for the value of that flag.
+    the flag names, and values define how values for that flag are handled.
+
+    If the value is a ``type``, we validate that assigned values are instances
+    of that type. And, the default value is the default value for this type.
+
+    If the value is a ``tuple``, additional control over values is allowed
+    by specifying a callback for the set event. The tuple consists of the
+    type of the value and a callback. The callback receives the flags
+    instance, the name/flag being set, and the value being set. The returned
+    value will be the flag's value.
 
     The resulting class is used by the various <TypeName>WithFlagsFactory
     functions below.
     """
     assert isinstance(flags, dict)
-    assert all(isinstance(v, type) for v in flags.values())
+    newflags = {}
+    for k, v in flags.items():
+        if isinstance(v, type):
+            newflags[k] = v
+        elif isinstance(v, tuple):
+            assert len(v) == 2
+            assert isinstance(v[0], type)
+            assert hasattr(v[1], '__call__')
+            newflags[k] = v
+        else:
+            raise ValueError('flags values must be type or 2-tuple')
 
     class Flags(object):
-        __slots__ = flags.keys()
-        _flags = flags
+        __slots__ = newflags.keys()
+        _flags = newflags
 
         def update(self, **kwargs):
             for k, v in kwargs.iteritems():
                 setattr(self, k, v)
 
         def __getattr__(self, name):
             if name not in self.__slots__:
                 raise AttributeError("'%s' object has no attribute '%s'" %
                                      (self.__class__.__name__, name))
             try:
                 return object.__getattr__(self, name)
             except AttributeError:
-                value = self._flags[name]()
+                v = self._flags[name]
+                t = v if isinstance(v, type) else v[0]
+                value = t()
                 self.__setattr__(name, value)
                 return value
 
         def __setattr__(self, name, value):
             if name not in self.__slots__:
                 raise AttributeError("'%s' object has no attribute '%s'" %
                                      (self.__class__.__name__, name))
-            if not isinstance(value, self._flags[name]):
-                raise TypeError("'%s' attribute of class '%s' must be '%s'" %
-                                (name, self.__class__.__name__,
-                                 self._flags[name].__name__))
+
+            v = self._flags[name]
+            if isinstance(v, type):
+                if not isinstance(value, v):
+                    raise TypeError("'%s' attribute of class '%s' must be '%s'" %
+                                    (name, self.__class__.__name__, v.__name__))
+            else:
+                value = v[1](self, name, value)
+
             return object.__setattr__(self, name, value)
 
         def __delattr__(self, name):
             raise MozbuildDeletionError('Unable to delete attributes for this object')
 
     return Flags