Bug 1363811 - Allow "direct" access to namespace attributes from DependsFunctions. r?chmanchester draft
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 17 May 2017 16:09:01 +0900
changeset 580725 cde7a0e4ac03926ee8d7941da1f0ad92a0daef39
parent 580724 364bf647bf8aaff1f64f4f85c0d7aad8d0405e6a
child 580726 48484f3995aa9395d41597c9c3ba4c12e4ddd85d
push id59648
push userbmo:mh+mozilla@glandium.org
push dateThu, 18 May 2017 22:06:14 +0000
reviewerschmanchester
bugs1363811
milestone55.0a1
Bug 1363811 - Allow "direct" access to namespace attributes from DependsFunctions. r?chmanchester To make things simpler in configure code, as well as to allow the linter to skip bugging about some --help dependencies, we make the following work: something.some_attr where the result is equivalent to, currently: delayed_getattr(something, 'some_attr')
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/configure/lint.py
python/mozbuild/mozbuild/test/configure/test_configure.py
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -42,16 +42,17 @@ class ConfigureError(Exception):
     pass
 
 
 class SandboxDependsFunction(object):
     '''Sandbox-visible representation of @depends functions.'''
     def __init__(self, unsandboxed):
         self._or = unsandboxed.__or__
         self._and = unsandboxed.__and__
+        self._getattr = unsandboxed.__getattr__
 
     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 '
@@ -59,16 +60,19 @@ class SandboxDependsFunction(object):
         return self._or(other).sandboxed
 
     def __and__(self, other):
         if not isinstance(other, SandboxDependsFunction):
             raise ConfigureError('Can only do binary arithmetic operations '
                                  'with another @depends function.')
         return self._and(other).sandboxed
 
+    def __getattr__(self, key):
+        return self._getattr(key).sandboxed
+
     def __nonzero__(self):
         raise ConfigureError(
             'Cannot do boolean operations on @depends functions.')
 
 
 class DependsFunction(object):
     __slots__ = (
         '_func', '_name', 'dependencies', 'when', 'sandboxed', 'sandbox',
@@ -154,16 +158,29 @@ class DependsFunction(object):
     def and_impl(iterable):
         # Applies "and" to all the items of iterable.
         # e.g. if iterable contains a, b and c, returns `a and b and c`.
         for i in iterable:
             if not i:
                 return i
         return i
 
+    def __getattr__(self, key):
+        if key.startswith('_'):
+            return super(DependsFunction, self).__getattr__(key)
+        # Our function may return None or an object that simply doesn't have
+        # the wanted key. In that case, just return None.
+        return TrivialDependsFunction(
+            self.sandbox, lambda x: getattr(x, key, None), [self], self.when)
+
+
+class TrivialDependsFunction(DependsFunction):
+    '''Like a DependsFunction, but the linter won't expect it to have a
+    dependency on --help ever.'''
+
 
 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/configure/lint.py
+++ b/python/mozbuild/mozbuild/configure/lint.py
@@ -8,16 +8,17 @@ import inspect
 from functools import wraps
 from StringIO import StringIO
 from . import (
     CombinedDependsFunction,
     ConfigureError,
     ConfigureSandbox,
     DependsFunction,
     SandboxedGlobal,
+    TrivialDependsFunction,
 )
 from .lint_util import disassemble_as_iter
 from mozbuild.util import memoize
 
 
 class LintSandbox(ConfigureSandbox):
     def __init__(self, environ=None, argv=None, stdout=None, stderr=None):
         out = StringIO()
@@ -69,17 +70,17 @@ class LintSandbox(ConfigureSandbox):
                     else:
                         dep = dep.option
                     raise ConfigureError(
                         '%s: The dependency on `%s` is unused.'
                         % (loc, dep)
                     )
 
     def _missing_help_dependency(self, obj):
-        if isinstance(obj, CombinedDependsFunction):
+        if isinstance(obj, (CombinedDependsFunction, TrivialDependsFunction)):
             return False
         if isinstance(obj, DependsFunction):
             if (self._help_option in obj.dependencies or
                 obj in (self._always, self._never)):
                 return False
             func, glob = self.unwrap(obj._func)
             # We allow missing --help dependencies for functions that:
             # - don't use @imports
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -1325,11 +1325,57 @@ class TestConfigure(unittest.TestCase):
                             [x for x in (foo_opt, bar_opt, baz_opt) if x])
                         self.assertEqual(config, {
                             'FOOorBAR': foo_value or bar_value,
                             'FOOorBARorBAZ': foo_value or bar_value or baz_value,
                             'FOOandBAR': foo_value and bar_value,
                             'FOOandBARandBAZ': foo_value and bar_value and baz_value,
                         })
 
+    def test_depends_getattr(self):
+        with self.moz_configure('''
+            @imports(_from='mozbuild.util', _import='ReadOnlyNamespace')
+            def namespace(**kwargs):
+                return ReadOnlyNamespace(**kwargs)
+
+            option('--foo', nargs=1, help='foo')
+            @depends('--foo')
+            def foo(value):
+                return value
+
+            option('--bar', nargs=1, help='bar')
+            @depends('--bar')
+            def bar(value):
+                return value or None
+
+            @depends(foo, bar)
+            def foobar(foo, bar):
+                return namespace(foo=foo, bar=bar)
+
+            set_config('FOO', foobar.foo)
+            set_config('BAR', foobar.bar)
+            set_config('BAZ', foobar.baz)
+        '''):
+            config = self.get_config()
+            self.assertEqual(config, {
+                'FOO': NegativeOptionValue(),
+            })
+
+            config = self.get_config(['--foo=foo'])
+            self.assertEqual(config, {
+                'FOO': PositiveOptionValue(('foo',)),
+            })
+
+            config = self.get_config(['--bar=bar'])
+            self.assertEqual(config, {
+                'FOO': NegativeOptionValue(),
+                'BAR': PositiveOptionValue(('bar',)),
+            })
+
+            config = self.get_config(['--foo=foo', '--bar=bar'])
+            self.assertEqual(config, {
+                'FOO': PositiveOptionValue(('foo',)),
+                'BAR': PositiveOptionValue(('bar',)),
+            })
+
 
 if __name__ == '__main__':
     main()