Bug 1296530 - Add an only_when context manager, and a when argument to include(). r=chmanchester

--HG--
extra : rebase_source : a74c539148e0a0597d1ff4af85ccf917cc37e1d4
This commit is contained in:
Mike Hommey 2016-10-12 16:56:11 +09:00
parent b527447baf
commit 5983667dd8
2 changed files with 278 additions and 45 deletions

View File

@ -31,6 +31,7 @@ from mozbuild.configure.util import (
from mozbuild.util import (
exec_,
memoize,
memoized_property,
ReadOnlyDict,
ReadOnlyNamespace,
)
@ -50,14 +51,25 @@ class SandboxDependsFunction(object):
class DependsFunction(object):
__slots__ = ('func', 'dependencies', 'sandboxed')
def __init__(self, sandbox, func, dependencies):
__slots__ = (
'func', 'dependencies', 'when', 'sandboxed', 'sandbox', '_result')
def __init__(self, sandbox, func, dependencies, when=None):
assert isinstance(sandbox, ConfigureSandbox)
self.func = func
self.dependencies = dependencies
self.sandboxed = wraps(func)(SandboxDependsFunction())
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)
elif not sandbox._help:
sandbox._execution_queue.append((sandbox._value_for, (self,)))
@property
def name(self):
return self.func.__name__
@ -69,6 +81,14 @@ class DependsFunction(object):
for d in self.dependencies
]
@memoized_property
def result(self):
if self.when and not self.sandbox._value_for(self.when):
return None
resolved_args = [self.sandbox._value_for(d) for d in self.dependencies]
return self.func(*resolved_args)
def __repr__(self):
return '<%s.%s %s(%s)>' % (
self.__class__.__module__,
@ -78,6 +98,50 @@ class DependsFunction(object):
)
class CombinedDependsFunction(DependsFunction):
def __init__(self, sandbox, func, dependencies):
@memoize
@wraps(func)
def wrapper(*args):
return func(args)
flatten_deps = []
for d in dependencies:
if isinstance(d, CombinedDependsFunction) and d.func == wrapper:
for d2 in d.dependencies:
if d2 not in flatten_deps:
flatten_deps.append(d2)
elif d not in flatten_deps:
flatten_deps.append(d)
# Automatically add a --help dependency if one of the dependencies
# depends on it.
for d in flatten_deps:
if (isinstance(d, DependsFunction) and
sandbox._help_option in d.dependencies):
flatten_deps.insert(0, sandbox._help_option)
break
super(CombinedDependsFunction, self).__init__(
sandbox, wrapper, flatten_deps)
@memoized_property
def result(self):
# Ignore --help for the combined result
deps = self.dependencies
if deps[0] == self.sandbox._help_option:
deps = deps[1:]
resolved_args = [self.sandbox._value_for(d) for d in deps]
return self.func(*resolved_args)
def __eq__(self, other):
return (isinstance(other, self.__class__) and
self.func == other.func and
set(self.dependencies) == set(other.dependencies))
def __ne__(self, other):
return not self == other
class SandboxedGlobal(dict):
'''Identifiable dict type for use as function global'''
@ -91,7 +155,7 @@ class ConfigureSandbox(dict):
This is a different kind of sandboxing than the one used for moz.build
processing.
The sandbox has 8 primitives:
The sandbox has 9 primitives:
- option
- depends
- template
@ -100,9 +164,11 @@ class ConfigureSandbox(dict):
- set_config
- set_define
- imply_option
- only_when
`option`, `include`, `set_config`, `set_define` and `imply_option` are
functions. `depends`, `template`, and `imports` are decorators.
functions. `depends`, `template`, and `imports` are decorators. `only_when`
is a context_manager.
These primitives are declared as name_impl methods to this class and
the mapping name -> name_impl is done automatically in __getitem__.
@ -165,9 +231,12 @@ class ConfigureSandbox(dict):
# Queue of functions to execute, with their arguments
self._execution_queue = []
# Store the `when`s associated to some options/depends.
# Store the `when`s associated to some options.
self._conditions = {}
# A list of conditions to apply as a default `when` for every *_impl()
self._default_conditions = []
self._helper = CommandLineHelper(environ, argv)
assert isinstance(config, dict)
@ -353,16 +422,7 @@ class ConfigureSandbox(dict):
elif self._help or need_help_dependency:
raise ConfigureError("Missing @depends for `%s`: '--help'" %
obj.name)
return self._value_for_depends_real(obj)
@memoize
def _value_for_depends_real(self, obj):
when = self._conditions.get(obj)
if when and not self._value_for(when):
return None
resolved_args = [self._value_for(d) for d in obj.dependencies]
return obj.func(*resolved_args)
return obj.result
@memoize
def _value_for_option(self, option):
@ -441,6 +501,35 @@ class ConfigureSandbox(dict):
callee_name))
return arg
def _normalize_when(self, when, callee_name):
if when is not None:
when = self._dependency(when, callee_name, 'when')
if self._default_conditions:
# Create a pseudo @depends function for the combination of all
# default conditions and `when`.
dependencies = [when] if when else []
dependencies.extend(self._default_conditions)
if len(dependencies) == 1:
return dependencies[0]
return CombinedDependsFunction(self, all, dependencies)
return when
@contextmanager
def only_when_impl(self, when):
'''Implementation of only_when()
`only_when` is a context manager that essentially makes calls to
other sandbox functions within the context block ignored.
'''
when = self._normalize_when(when, 'only_when')
if when and self._default_conditions[-1:] != [when]:
self._default_conditions.append(when)
yield
self._default_conditions.pop()
else:
yield
def option_impl(self, *args, **kwargs):
'''Implementation of option()
This function creates and returns an Option() object, passing it the
@ -450,9 +539,7 @@ class ConfigureSandbox(dict):
Command line argument/environment variable parsing for this Option is
handled here.
'''
when = kwargs.get('when')
if when is not None:
when = self._dependency(when, 'option', 'when')
when = self._normalize_when(kwargs.get('when'), 'option')
args = [self._resolve(arg) for arg in args]
kwargs = {k: self._resolve(v) for k, v in kwargs.iteritems()
if k != 'when'}
@ -501,9 +588,7 @@ class ConfigureSandbox(dict):
"depends_impl() got an unexpected keyword argument '%s'"
% k)
when = kwargs.get('when')
if when is not None:
when = self._dependency(when, '@depends', 'when')
when = self._normalize_when(kwargs.get('when'), '@depends')
dependencies = tuple(self._dependency(arg, '@depends') for arg in args)
@ -522,34 +607,24 @@ class ConfigureSandbox(dict):
raise ConfigureError(
'Cannot decorate generator functions with @depends')
func, glob = self._prepare_function(func)
depends = DependsFunction(self, func, dependencies)
if when:
self._conditions[depends] = when
# Only @depends functions with a dependency on '--help' are
# executed immediately. Everything else is queued for later
# execution.
if self._help_option in dependencies:
self._value_for(depends)
elif not self._help:
self._execution_queue.append((self._value_for, (depends,)))
depends = DependsFunction(self, func, dependencies, when=when)
return depends.sandboxed
return decorator
def include_impl(self, what):
def include_impl(self, what, when=None):
'''Implementation of include().
Allows to include external files for execution in the sandbox.
It is possible to use a @depends function as argument, in which case
the result of the function is the file name to include. This latter
feature is only really meant for --enable-application/--enable-project.
'''
what = self._resolve(what)
if what:
if not isinstance(what, types.StringTypes):
raise TypeError("Unexpected type: '%s'" % type(what).__name__)
self.include_file(what)
with self.only_when_impl(when):
what = self._resolve(what)
if what:
if not isinstance(what, types.StringTypes):
raise TypeError("Unexpected type: '%s'" % type(what).__name__)
self.include_file(what)
def template_impl(self, func):
'''Implementation of @template.
@ -701,8 +776,7 @@ class ConfigureSandbox(dict):
in which case the result from these functions is used. If the result
of either function is None, the configuration item is not set.
'''
if when is not None:
when = self._dependency(when, 'set_config', 'when')
when = self._normalize_when(when, 'set_config')
self._execution_queue.append((
self._resolve_and_set, (self._config, name, value, when)))
@ -715,8 +789,7 @@ class ConfigureSandbox(dict):
is None, the define is not set. If the result is False, the define is
explicitly undefined (-U).
'''
if when is not None:
when = self._dependency(when, 'set_define', 'when')
when = self._normalize_when(when, 'set_define')
defines = self._config.setdefault('DEFINES', {})
self._execution_queue.append((
@ -788,8 +861,7 @@ class ConfigureSandbox(dict):
"the `imply_option` call."
% option)
if when is not None:
when = self._dependency(when, 'imply_option', 'when')
when = self._normalize_when(when, 'imply_option')
prefix, name, values = Option.split_option(option)
if values != ():

View File

@ -902,6 +902,78 @@ class TestConfigure(unittest.TestCase):
self.assertEquals(e.exception.message, "Unexpected type: 'int'")
def test_include_when(self):
with MockedOpen({
os.path.join(test_data_path, 'moz.configure'): textwrap.dedent('''
@depends('--help')
def always(_):
return True
@depends('--help')
def never(_):
return False
option('--with-foo', help='foo')
include('always.configure', when=always)
include('never.configure', when=never)
include('foo.configure', when='--with-foo')
set_config('FOO', foo)
set_config('BAR', bar)
set_config('QUX', qux)
'''),
os.path.join(test_data_path, 'always.configure'): textwrap.dedent('''
option('--with-bar', help='bar')
@depends('--with-bar')
def bar(x):
if x:
return 'bar'
'''),
os.path.join(test_data_path, 'never.configure'): textwrap.dedent('''
option('--with-qux', help='qux')
@depends('--with-qux')
def qux(x):
if x:
return 'qux'
'''),
os.path.join(test_data_path, 'foo.configure'): textwrap.dedent('''
option('--with-foo-really', help='really foo')
@depends('--with-foo-really')
def foo(x):
if x:
return 'foo'
include('foo2.configure', when='--with-foo-really')
'''),
os.path.join(test_data_path, 'foo2.configure'): textwrap.dedent('''
set_config('FOO2', True)
'''),
}):
config = self.get_config()
self.assertEquals(config, {})
config = self.get_config(['--with-foo'])
self.assertEquals(config, {})
config = self.get_config(['--with-bar'])
self.assertEquals(config, {
'BAR': 'bar',
})
with self.assertRaises(InvalidOptionError) as e:
self.get_config(['--with-qux'])
self.assertEquals(
e.exception.message,
'--with-qux is not available in this configuration'
)
config = self.get_config(['--with-foo', '--with-foo-really'])
self.assertEquals(config, {
'FOO': 'foo',
'FOO2': True,
})
def test_sandbox_failures(self):
with self.assertRaises(KeyError) as e:
with self.moz_configure('''
@ -1147,6 +1219,95 @@ class TestConfigure(unittest.TestCase):
self.assertEquals(e.exception.message,
"Invalid argument to @imports: 'os*'")
def test_only_when(self):
moz_configure = '''
option('--enable-when', help='when')
@depends('--enable-when', '--help')
def when(value, _):
return bool(value)
with only_when(when):
option('--foo', nargs='*', help='foo')
@depends('--foo')
def foo(value):
return value
set_config('FOO', foo)
set_define('FOO', foo)
# It is possible to depend on a function defined in a only_when
# block. It then resolves to `None`.
set_config('BAR', depends(foo)(lambda x: x))
set_define('BAR', depends(foo)(lambda x: x))
'''
with self.moz_configure(moz_configure):
config = self.get_config()
self.assertEqual(config, {
'DEFINES': {},
})
config = self.get_config(['--enable-when'])
self.assertEqual(config, {
'BAR': NegativeOptionValue(),
'FOO': NegativeOptionValue(),
'DEFINES': {
'BAR': NegativeOptionValue(),
'FOO': NegativeOptionValue(),
},
})
config = self.get_config(['--enable-when', '--foo=bar'])
self.assertEqual(config, {
'BAR': PositiveOptionValue(['bar']),
'FOO': PositiveOptionValue(['bar']),
'DEFINES': {
'BAR': PositiveOptionValue(['bar']),
'FOO': PositiveOptionValue(['bar']),
},
})
# The --foo option doesn't exist when --enable-when is not given.
with self.assertRaises(InvalidOptionError) as e:
self.get_config(['--foo'])
self.assertEquals(e.exception.message,
'--foo is not available in this configuration')
# Cannot depend on an option defined in a only_when block, because we
# don't know what OptionValue would make sense.
with self.moz_configure(moz_configure + '''
set_config('QUX', depends('--foo')(lambda x: x))
'''):
with self.assertRaises(ConfigureError) as e:
self.get_config()
self.assertEquals(e.exception.message,
'@depends function needs the same `when` as '
'options it depends on')
with self.moz_configure(moz_configure + '''
set_config('QUX', depends('--foo', when=when)(lambda x: x))
'''):
self.get_config(['--enable-when'])
# Using imply_option for an option defined in a only_when block fails
# similarly if the imply_option happens outside the block.
with self.moz_configure('''
imply_option('--foo', True)
''' + moz_configure):
with self.assertRaises(InvalidOptionError) as e:
self.get_config()
self.assertEquals(e.exception.message,
'--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'])
if __name__ == '__main__':
main()