Bug 1257823 - Move imply_option() to the global scope draft
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 23 Mar 2016 14:18:57 +0900
changeset 343869 5f3eaeacbb233c2e1c276fbe11422e35ba7540a2
parent 343868 da90bf5b50866a8974585f4355bf5db15c01f2e3
child 343870 bca81ea8743049e08802b4e5d6860e64ee81f0d7
push id13691
push userbmo:mh+mozilla@glandium.org
push dateWed, 23 Mar 2016 10:00:34 +0000
bugs1257823
milestone48.0a1
Bug 1257823 - Move imply_option() to the global scope Like set_config and set_define, we move imply_option to the global scope. Note: similarly again, the move is split in 3 parts. While for the other 2, the intermediate steps still work, in this case, it's possible the intermediate steps are semi broken, with error handling not working as expected. The unit tests pass, so I'd say it's fine, considering the code that I think is semi broken goes away in 2 commits. Plus, the next 2 commits will be folded with this one...
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure
python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure
python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure
python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure
python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure
python/mozbuild/mozbuild/test/configure/test_configure.py
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -9,18 +9,20 @@ import os
 import sys
 import types
 from collections import OrderedDict
 from functools import wraps
 from mozbuild.configure.options import (
     CommandLineHelper,
     ConflictingOptionError,
     InvalidOptionError,
+    NegativeOptionValue,
     Option,
     OptionValue,
+    PositiveOptionValue,
 )
 from mozbuild.configure.help import HelpFormatter
 from mozbuild.util import (
     ReadOnlyDict,
     ReadOnlyNamespace,
 )
 import mozpack.path as mozpath
 
@@ -46,17 +48,17 @@ class DependsOutput(object):
 
     def __init__(self):
         super(DependsOutput, self).__init__()
         self.implied_options = []
 
     def imply_option(self, option, reason=None):
         if not isinstance(option, types.StringTypes):
             raise TypeError('imply_option must be given a string')
-        self.implied_options.append((option, reason))
+        self.implied_options.append((option, inspect.stack()[1], reason))
 
 
 def forbidden_import(*args, **kwargs):
     raise ImportError('Importing modules is forbidden')
 
 
 class ConfigureSandbox(dict):
     """Represents a sandbox for executing Python code for build configuration.
@@ -125,17 +127,17 @@ class ConfigureSandbox(dict):
         self._results = {}
         # Store values for each Option, as per returned by Option.get_value
         self._option_values = {}
         # Store raw option (as per command line or environment) for each Option
         self._raw_options = {}
 
         # Store options added with `imply_option`, and the reason they were
         # added (which can either have been given to `imply_option`, or
-        # infered.
+        # inferred.
         self._implied_options = {}
 
         # Store all results from _prepare_function
         self._prepared_functions = set()
 
         self._helper = CommandLineHelper(environ, argv)
 
         assert isinstance(config, dict)
@@ -180,21 +182,20 @@ class ConfigureSandbox(dict):
         '''Executes the given file within the sandbox, and ensure the overall
         consistency of the executed script.'''
         self.exec_file(path)
 
         # All command line arguments should have been removed (handled) by now.
         for arg in self._helper:
             without_value = arg.split('=', 1)[0]
             if arg in self._implied_options:
-                func, reason = self._implied_options[arg]
+                frameinfo, reason = self._implied_options[arg]
                 raise ConfigureError(
-                    '`%s`, emitted by `%s` in `%s`, was not handled.'
-                    % (without_value, func.__name__,
-                       func.func_code.co_filename))
+                    '`%s`, emitted from `%s` line `%d`, was not handled.'
+                    % (without_value, frameinfo[1], frameinfo[2]))
             raise InvalidOptionError('Unknown option: %s' % without_value)
 
         # All options must be referenced by some @depends function
         for option in self._options.itervalues():
             if option not in self._seen:
                 raise ConfigureError(
                     'Option `%s` is not handled ; reference it with a @depends'
                     % option.option
@@ -257,17 +258,17 @@ class ConfigureSandbox(dict):
         if option.name:
             self._options[option.name] = option
         if option.env:
             self._options[option.env] = option
 
         try:
             value, option_string = self._helper.handle(option)
         except ConflictingOptionError as e:
-            func, reason = self._implied_options[e.arg]
+            frameinfo, reason = self._implied_options[e.arg]
             raise InvalidOptionError(
                 "'%s' implied by '%s' conflicts with '%s' from the %s"
                 % (e.arg, reason, e.old_arg, e.old_origin))
 
         if self._help:
             self._help.add(option)
 
         self._option_values[option] = value
@@ -349,34 +350,34 @@ class ConfigureSandbox(dict):
                             "`%s` must depend on '--help'"
                             % (func.__name__, arg.__name__, arg.__name__))
 
             if self._help and not with_help:
                 return dummy
 
             self._results[func] = func(*resolved_args)
 
-            for option, reason in result.implied_options:
+            for option, frameinfo, reason in result.implied_options:
                 self._helper.add(option, 'implied')
                 if not reason:
                     deps = []
                     for arg in dependencies:
                         if not isinstance(arg, Option):
                             raise ConfigureError(
                                 "Cannot infer what implied '%s'" % option)
                         if arg == self._help_option:
                             continue
                         deps.append(self._raw_options.get(arg) or
                                     self.arg.option)
                     if len(deps) != 1:
                         raise ConfigureError(
                             "Cannot infer what implied '%s'" % option)
                     reason = deps[0]
 
-                self._implied_options[option] = func, reason
+                self._implied_options[option] = frameinfo, reason
 
             return dummy
 
         return decorator
 
     def include_impl(self, what):
         '''Implementation of include().
         Allows to include external files for execution in the sandbox.
@@ -451,16 +452,81 @@ class ConfigureSandbox(dict):
         `value` can be references to @depends functions, in which case the
         result from these functions is used. If the result of such functions
         is None, the define is not set. If the result is False, the define is
         explicitly undefined (-U).
         '''
         defines = self._config.setdefault('DEFINES', {})
         self._resolve_and_set(defines, name, value)
 
+    def imply_option_impl(self, option, value, reason=None):
+        '''Implementation of imply_option().
+        Injects additional options as if they had been passed on the command
+        line. The `option` argument is a string as in option()'s `name` or
+        `env`. The option must be declared after `imply_option` references it.
+        The `value` argument indicates the value to pass to the option.
+        It can be True (the positive option is injected*), False (the negative
+        option is injected*), None (the option is not injected), a string or a
+        tuple (the positive option is injected with the given values).
+        The `value` argument can also be (and usually) is a reference to a
+        @depends function, in which case the result of that function will be
+        used as per the descripted mapping above.
+        The `reason` argument indicates what caused the option to be implied.
+        It is necessary when it cannot be inferred from the `value`.
+
+        * the positive option is --enable-foo/--with-foo, the negative option
+        is --disable-foo/--without-foo.
+            imply_option('--enable-foo', True)
+            imply_option('--disable-foo', True)
+          are both equivalent to `--enable-foo` on the command line.
+
+            imply_option('--enable-foo', False)
+            imply_option('--disable-foo', False)
+          are both equivalent to `--disable-foo` on the command line.
+
+            imply_option('--enable-foo', None)
+            imply_option('--disable-foo', None)
+          are both equivalent to not passing any flag on the command line.
+
+        It is recommended to use the positive form ('--enable' or '--with') for
+        `option`.
+        '''
+        if not reason and isinstance(value, DummyFunction):
+            deps = self._depends[value][1]
+            possible_reasons = [d for d in deps if d != self._help_option]
+            if len(possible_reasons) == 1:
+                if isinstance(possible_reasons[0], Option):
+                    reason = (self._raw_options.get(possible_reasons[0]) or
+                              possible_reasons[0].option)
+
+        if not reason or not isinstance(value, DummyFunction):
+            raise ConfigureError(
+                "Cannot infer what implies '%s'. Please add a `reason` to "
+                "the `imply_option` call."
+                % option)
+
+        value = self._resolve(value, need_help_dependency=False)
+        if value is not None:
+            if isinstance(value, OptionValue):
+                pass
+            elif value is True:
+                value = PositiveOptionValue()
+            elif value is False or value == ():
+                value = NegativeOptionValue()
+            elif isinstance(value, types.StringTypes):
+                value = PositiveOptionValue((value,))
+            elif isinstance(value, tuple):
+                value = PositiveOptionValue(value)
+            else:
+                raise TypeError("Unexpected type: '%s'" % type(value))
+
+            option = value.format(option)
+            self._helper.add(option, 'implied')
+            self._implied_options[option] = inspect.stack()[1], reason
+
     def _prepare_function(self, func):
         '''Alter the given function global namespace with the common ground
         for @depends, @template and @advanced.
         '''
         if not inspect.isfunction(func):
             raise TypeError("Unexpected type: '%s'" % type(func))
         if func in self._prepared_functions:
             return func, func.func_globals
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure
@@ -0,0 +1,24 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+option('--enable-foo', help='enable foo')
+
+@depends('--enable-foo', '--help')
+def foo(value, help):
+    if value:
+        return True
+
+imply_option('--enable-bar', foo)
+
+
+option('--enable-bar', help='enable bar')
+
+@depends('--enable-bar')
+def bar(value):
+    if value:
+        return value
+
+set_config('BAR', bar)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure
@@ -0,0 +1,31 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+option('--enable-hoge', help='enable hoge')
+
+@depends('--enable-hoge')
+def hoge(value):
+    return value
+
+
+option('--enable-foo', help='enable foo')
+
+@depends('--enable-foo', hoge, '--help')
+def foo(value, help):
+    if value:
+        return True
+
+imply_option('--enable-bar', foo)
+
+
+option('--enable-bar', help='enable bar')
+
+@depends('--enable-bar')
+def bar(value):
+    if value:
+        return value
+
+set_config('BAR', bar)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure
@@ -0,0 +1,34 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+option('--enable-foo', help='enable foo')
+
+@depends('--enable-foo')
+def foo(value):
+    if value:
+        return False
+
+imply_option('--enable-bar', foo)
+
+
+option('--disable-hoge', help='enable hoge')
+
+@depends('--disable-hoge')
+def hoge(value):
+    if not value:
+        return False
+
+imply_option('--enable-bar', hoge)
+
+
+option('--enable-bar', default=True, help='enable bar')
+
+@depends('--enable-bar')
+def bar(value):
+    if not value:
+        return value
+
+set_config('BAR', bar)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure
@@ -0,0 +1,24 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+option('--enable-foo', help='enable foo')
+
+@depends('--enable-foo')
+def foo(value):
+    if value:
+        return True
+
+imply_option('--enable-bar', foo)
+
+
+option('--enable-bar', help='enable bar')
+
+@depends('--enable-bar')
+def bar(value):
+    if value:
+        return value
+
+set_config('BAR', bar)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure
@@ -0,0 +1,24 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+option('--enable-foo', nargs='*', help='enable foo')
+
+@depends('--enable-foo')
+def foo(value):
+    if value:
+        return value
+
+imply_option('--enable-bar', foo)
+
+
+option('--enable-bar', nargs='*', help='enable bar')
+
+@depends('--enable-bar')
+def bar(value):
+    if value:
+        return value
+
+set_config('BAR', bar)
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 from StringIO import StringIO
 import sys
+import traceback
 import unittest
 
 from mozunit import main
 
 from mozbuild.configure.options import (
     InvalidOptionError,
     NegativeOptionValue,
     PositiveOptionValue,
@@ -371,11 +372,103 @@ class TestConfigure(unittest.TestCase):
         self.assertEquals(config['DEFINES'], {'BAR': False})
 
         with self.assertRaises(ConfigureError):
             # Both --set-foo and --set-name=FOO are going to try to
             # set_define('FOO'...)
             self.get_config(['--set-foo', '--set-name=FOO'],
                             configure='set_define.configure')
 
+    def test_imply_option_simple(self):
+        config = self.get_config([], configure='imply_option/simple.configure')
+        self.assertEquals(config, {})
+
+        config = self.get_config(['--enable-foo'],
+                                 configure='imply_option/simple.configure')
+        self.assertIn('BAR', config)
+        self.assertEquals(config['BAR'], PositiveOptionValue())
+
+        with self.assertRaises(InvalidOptionError) as e:
+            config = self.get_config(['--enable-foo', '--disable-bar'],
+                                     configure='imply_option/simple.configure')
+
+        self.assertEquals(
+            e.exception.message,
+            "'--enable-bar' implied by '--enable-foo' conflicts with "
+            "'--disable-bar' from the command-line")
+
+    def test_imply_option_negative(self):
+        config = self.get_config([],
+                                 configure='imply_option/negative.configure')
+        self.assertEquals(config, {})
+
+        config = self.get_config(['--enable-foo'],
+                                 configure='imply_option/negative.configure')
+        self.assertIn('BAR', config)
+        self.assertEquals(config['BAR'], NegativeOptionValue())
+
+        with self.assertRaises(InvalidOptionError) as e:
+            config = self.get_config(
+                ['--enable-foo', '--enable-bar'],
+                configure='imply_option/negative.configure')
+
+        self.assertEquals(
+            e.exception.message,
+            "'--disable-bar' implied by '--enable-foo' conflicts with "
+            "'--enable-bar' from the command-line")
+
+        config = self.get_config(['--disable-hoge'],
+                                 configure='imply_option/negative.configure')
+        self.assertIn('BAR', config)
+        self.assertEquals(config['BAR'], NegativeOptionValue())
+
+        with self.assertRaises(InvalidOptionError) as e:
+            config = self.get_config(
+                ['--disable-hoge', '--enable-bar'],
+                configure='imply_option/negative.configure')
+
+        self.assertEquals(
+            e.exception.message,
+            "'--disable-bar' implied by '--disable-hoge' conflicts with "
+            "'--enable-bar' from the command-line")
+
+    def test_imply_option_values(self):
+        config = self.get_config([], configure='imply_option/values.configure')
+        self.assertEquals(config, {})
+
+        config = self.get_config(['--enable-foo=a'],
+                                 configure='imply_option/values.configure')
+        self.assertIn('BAR', config)
+        self.assertEquals(config['BAR'], PositiveOptionValue(('a',)))
+
+        config = self.get_config(['--enable-foo=a,b'],
+                                 configure='imply_option/values.configure')
+        self.assertIn('BAR', config)
+        self.assertEquals(config['BAR'], PositiveOptionValue(('a','b')))
+
+        with self.assertRaises(InvalidOptionError) as e:
+            config = self.get_config(['--enable-foo=a,b', '--disable-bar'],
+                                     configure='imply_option/values.configure')
+
+        self.assertEquals(
+            e.exception.message,
+            "'--enable-bar=a,b' implied by '--enable-foo' conflicts with "
+            "'--disable-bar' from the command-line")
+
+    def test_imply_option_infer(self):
+        config = self.get_config([], configure='imply_option/infer.configure')
+
+        with self.assertRaises(InvalidOptionError) as e:
+            config = self.get_config(['--enable-foo', '--disable-bar'],
+                                     configure='imply_option/infer.configure')
+
+        self.assertEquals(
+            e.exception.message,
+            "'--enable-bar' implied by '--enable-foo' conflicts with "
+            "'--disable-bar' from the command-line")
+
+
+        with self.assertRaises(ConfigureError) as e:
+            self.get_config([], configure='imply_option/infer_ko.configure')
+
 
 if __name__ == '__main__':
     main()