Bug 1322025 - Allow to combine two DependsFunctions with "|". r=chmanchester draft
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 25 Jan 2017 17:42:33 +0900
changeset 467004 8a5f8d07a994e4af834f96bcb8d1da9cbc15ed4c
parent 467003 88a5a2db38e25e037dbb9e4adce1b7d38d684d66
child 467005 84047cec9dbe36ee92d5394dde6d849b2146e4e4
push id43083
push userbmo:mh+mozilla@glandium.org
push dateFri, 27 Jan 2017 00:32:10 +0000
reviewerschmanchester
bugs1322025
milestone54.0a1
Bug 1322025 - Allow to combine two DependsFunctions with "|". r=chmanchester Ideally, it would have been better if it were "or", but it's not possible to override "or" in python ; __or__ is for "|". This does feel magic, but it's also shorter than adding something like @depends_any(), and while we're only adding "|" as of this change, we can add other operations such as "&" in the future, or __getattr__ for things like milestone.is_nightly. An alternative form in moz.configure could require the @depends function to be called, e.g. "a() | b()" instead of "a | b", but I'm not particularly convinced that one is less magic than the other. This feature is hooked up such that b is not resolved if a is true, although in practice, it will still be resolved in Sandbox.run... but not when --help is passed. In the long run, the forced resolution of @depends functions will be removed from Sandbox.run.
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/test/configure/test_configure.py
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -39,33 +39,42 @@ import mozpack.path as mozpath
 
 
 class ConfigureError(Exception):
     pass
 
 
 class SandboxDependsFunction(object):
     '''Sandbox-visible representation of @depends functions.'''
+    def __init__(self, unsandboxed):
+        self._or = unsandboxed.__or__
+
     def __call__(self, *arg, **kwargs):
         raise ConfigureError('The `%s` function may not be called'
                              % self.__name__)
 
+    def __or__(self, other):
+        if not isinstance(other, SandboxDependsFunction):
+            raise ConfigureError('Can only do binary arithmetic operations '
+                                 'with another @depends function.')
+        return self._or(other).sandboxed
+
 
 class DependsFunction(object):
     __slots__ = (
         '_func', '_name', 'dependencies', 'when', 'sandboxed', 'sandbox',
         '_result')
 
     def __init__(self, sandbox, func, dependencies, when=None):
         assert isinstance(sandbox, ConfigureSandbox)
         assert not inspect.isgeneratorfunction(func)
         self._func = func
         self._name = func.__name__
         self.dependencies = dependencies
-        self.sandboxed = wraps(func)(SandboxDependsFunction())
+        self.sandboxed = wraps(func)(SandboxDependsFunction(self))
         self.sandbox = sandbox
         self.when = when
         sandbox._depends[self.sandboxed] = self
 
         # Only @depends functions with a dependency on '--help' are executed
         # immediately. Everything else is queued for later execution.
         if sandbox._help_option in dependencies:
             sandbox._value_for(self)
@@ -100,16 +109,33 @@ class DependsFunction(object):
     def __repr__(self):
         return '<%s.%s %s(%s)>' % (
             self.__class__.__module__,
             self.__class__.__name__,
             self.name,
             ', '.join(repr(d) for d in self.dependencies),
         )
 
+    def __or__(self, other):
+        if isinstance(other, SandboxDependsFunction):
+            other = self.sandbox._depends.get(other)
+        assert isinstance(other, DependsFunction)
+        assert self.sandbox is other.sandbox
+        return CombinedDependsFunction(self.sandbox, self.first_true,
+                                       (self, other))
+
+    @staticmethod
+    def first_true(iterable):
+        # Like the builtin any(), but returns the first element that is true,
+        # instead of True. If none are true, returns the last element.
+        for i in iterable:
+            if i:
+                return i
+        return i
+
 
 class CombinedDependsFunction(DependsFunction):
     def __init__(self, sandbox, func, dependencies):
         flatten_deps = []
         for d in dependencies:
             if isinstance(d, CombinedDependsFunction) and d._func is func:
                 for d2 in d.dependencies:
                     if d2 not in flatten_deps:
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -1282,11 +1282,45 @@ class TestConfigure(unittest.TestCase):
                               '--foo is not available in this configuration')
 
         # And similarly doesn't fail when the condition is true.
         with self.moz_configure('''
             imply_option('--foo', True)
         ''' + moz_configure):
             self.get_config(['--enable-when'])
 
+    def test_depends_or(self):
+        with self.moz_configure('''
+            option('--foo', nargs=1, help='foo')
+            @depends('--foo')
+            def foo(value):
+                return value or None
+
+            option('--bar', nargs=1, help='bar')
+            @depends('--bar')
+            def bar(value):
+                return value
+
+            set_config('FOOBAR', foo | bar)
+        '''):
+            config = self.get_config()
+            self.assertEqual(config, {
+                'FOOBAR': NegativeOptionValue(),
+            })
+
+            config = self.get_config(['--foo=foo'])
+            self.assertEqual(config, {
+                'FOOBAR': PositiveOptionValue(('foo',)),
+            })
+
+            config = self.get_config(['--bar=bar'])
+            self.assertEqual(config, {
+                'FOOBAR': PositiveOptionValue(('bar',)),
+            })
+
+            config = self.get_config(['--foo=foo', '--bar=bar'])
+            self.assertEqual(config, {
+                'FOOBAR': PositiveOptionValue(('foo',)),
+            })
+
 
 if __name__ == '__main__':
     main()