diff options
author | Chandan Singh <csingh43@bloomberg.net> | 2019-04-24 22:53:19 +0100 |
---|---|---|
committer | Chandan Singh <csingh43@bloomberg.net> | 2019-05-21 12:41:18 +0100 |
commit | 070d053e5cc47e572e9f9e647315082bd7a15c63 (patch) | |
tree | 7fb0fdff52f9b5f8a18ec8fe9c75b661f9e0839e /src/buildstream/_options | |
parent | 6c59e7901a52be961c2a1b671cf2b30f90bc4d0a (diff) | |
download | buildstream-070d053e5cc47e572e9f9e647315082bd7a15c63.tar.gz |
Move source from 'buildstream' to 'src/buildstream'
This was discussed in #1008.
Fixes #1009.
Diffstat (limited to 'src/buildstream/_options')
-rw-r--r-- | src/buildstream/_options/__init__.py | 20 | ||||
-rw-r--r-- | src/buildstream/_options/option.py | 112 | ||||
-rw-r--r-- | src/buildstream/_options/optionarch.py | 84 | ||||
-rw-r--r-- | src/buildstream/_options/optionbool.py | 58 | ||||
-rw-r--r-- | src/buildstream/_options/optioneltmask.py | 46 | ||||
-rw-r--r-- | src/buildstream/_options/optionenum.py | 77 | ||||
-rw-r--r-- | src/buildstream/_options/optionflags.py | 86 | ||||
-rw-r--r-- | src/buildstream/_options/optionos.py | 41 | ||||
-rw-r--r-- | src/buildstream/_options/optionpool.py | 295 |
9 files changed, 819 insertions, 0 deletions
diff --git a/src/buildstream/_options/__init__.py b/src/buildstream/_options/__init__.py new file mode 100644 index 000000000..70bbe35aa --- /dev/null +++ b/src/buildstream/_options/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .optionpool import OptionPool diff --git a/src/buildstream/_options/option.py b/src/buildstream/_options/option.py new file mode 100644 index 000000000..ffdb4d272 --- /dev/null +++ b/src/buildstream/_options/option.py @@ -0,0 +1,112 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import _yaml + + +# Shared symbols for validation purposes +# +OPTION_SYMBOLS = [ + 'type', + 'description', + 'variable' +] + + +# Option() +# +# An abstract class representing a project option. +# +# Concrete classes must be created to handle option types, +# the loaded project options is a collection of typed Option +# instances. +# +class Option(): + + # Subclasses use this to specify the type name used + # for the yaml format and error messages + OPTION_TYPE = None + + def __init__(self, name, definition, pool): + self.name = name + self.description = None + self.variable = None + self.value = None + self.pool = pool + self.load(definition) + + # load() + # + # Loads the option attributes from the descriptions + # in the project.conf + # + # Args: + # node (dict): The loaded YAML dictionary describing + # the option + def load(self, node): + self.description = _yaml.node_get(node, str, 'description') + self.variable = _yaml.node_get(node, str, 'variable', default_value=None) + + # Assert valid symbol name for variable name + if self.variable is not None: + p = _yaml.node_get_provenance(node, 'variable') + _yaml.assert_symbol_name(p, self.variable, 'variable name') + + # load_value() + # + # Loads the value of the option in string form. + # + # Args: + # node (Mapping): The YAML loaded key/value dictionary + # to load the value from + # transform (callbable): Transform function for variable substitution + # + def load_value(self, node, *, transform=None): + pass # pragma: nocover + + # set_value() + # + # Sets the value of an option from a string passed + # to buildstream on the command line + # + # Args: + # value (str): The value in string form + # + def set_value(self, value): + pass # pragma: nocover + + # get_value() + # + # Gets the value of an option in string form, this + # is for the purpose of exporting option values to + # variables which must be in string form. + # + # Returns: + # (str): The value in string form + # + def get_value(self): + pass # pragma: nocover + + # resolve() + # + # Called on each option once, after all configuration + # and cli options have been passed. + # + def resolve(self): + pass # pragma: nocover diff --git a/src/buildstream/_options/optionarch.py b/src/buildstream/_options/optionarch.py new file mode 100644 index 000000000..0e2963c84 --- /dev/null +++ b/src/buildstream/_options/optionarch.py @@ -0,0 +1,84 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import _yaml +from .._exceptions import LoadError, LoadErrorReason, PlatformError +from .._platform import Platform +from .optionenum import OptionEnum + + +# OptionArch +# +# An enumeration project option which does not allow +# definition of a default value, but instead tries to set +# the default value to the machine architecture introspected +# using `uname` +# +# Note that when using OptionArch in a project, it will automatically +# bail out of the host machine `uname` reports a machine architecture +# not supported by the project, in the case that no option was +# specifically specified +# +class OptionArch(OptionEnum): + + OPTION_TYPE = 'arch' + + def load(self, node): + super(OptionArch, self).load(node, allow_default_definition=False) + + def load_default_value(self, node): + arch = Platform.get_host_arch() + + default_value = None + + for index, value in enumerate(self.values): + try: + canonical_value = Platform.canonicalize_arch(value) + if default_value is None and canonical_value == arch: + default_value = value + # Do not terminate the loop early to ensure we validate + # all values in the list. + except PlatformError as e: + provenance = _yaml.node_get_provenance(node, key='values', indices=[index]) + prefix = "" + if provenance: + prefix = "{}: ".format(provenance) + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}Invalid value for {} option '{}': {}" + .format(prefix, self.OPTION_TYPE, self.name, e)) + + if default_value is None: + # Host architecture is not supported by the project. + # Do not raise an error here as the user may override it. + # If the user does not override it, an error will be raised + # by resolve()/validate(). + default_value = arch + + return default_value + + def resolve(self): + + # Validate that the default machine arch reported by uname() is + # explicitly supported by the project, only if it was not + # overridden by user configuration or cli. + # + # If the value is specified on the cli or user configuration, + # then it will already be valid. + # + self.validate(self.value) diff --git a/src/buildstream/_options/optionbool.py b/src/buildstream/_options/optionbool.py new file mode 100644 index 000000000..867de22df --- /dev/null +++ b/src/buildstream/_options/optionbool.py @@ -0,0 +1,58 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import _yaml +from .._exceptions import LoadError, LoadErrorReason +from .option import Option, OPTION_SYMBOLS + + +# OptionBool +# +# A boolean project option +# +class OptionBool(Option): + + OPTION_TYPE = 'bool' + + def load(self, node): + + super(OptionBool, self).load(node) + _yaml.node_validate(node, OPTION_SYMBOLS + ['default']) + self.value = _yaml.node_get(node, bool, 'default') + + def load_value(self, node, *, transform=None): + if transform: + self.set_value(transform(_yaml.node_get(node, str, self.name))) + else: + self.value = _yaml.node_get(node, bool, self.name) + + def set_value(self, value): + if value in ('True', 'true'): + self.value = True + elif value in ('False', 'false'): + self.value = False + else: + raise LoadError(LoadErrorReason.INVALID_DATA, + "Invalid value for boolean option {}: {}".format(self.name, value)) + + def get_value(self): + if self.value: + return "1" + else: + return "0" diff --git a/src/buildstream/_options/optioneltmask.py b/src/buildstream/_options/optioneltmask.py new file mode 100644 index 000000000..09c2ce8c2 --- /dev/null +++ b/src/buildstream/_options/optioneltmask.py @@ -0,0 +1,46 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import utils +from .optionflags import OptionFlags + + +# OptionEltMask +# +# A flags option which automatically only allows element +# names as values. +# +class OptionEltMask(OptionFlags): + + OPTION_TYPE = 'element-mask' + + def load(self, node): + # Ask the parent constructor to disallow value definitions, + # we define those automatically only. + super(OptionEltMask, self).load(node, allow_value_definitions=False) + + # Here we want all valid elements as possible values, + # but we'll settle for just the relative filenames + # of files ending with ".bst" in the project element directory + def load_valid_values(self, node): + values = [] + for filename in utils.list_relative_paths(self.pool.element_path): + if filename.endswith('.bst'): + values.append(filename) + return values diff --git a/src/buildstream/_options/optionenum.py b/src/buildstream/_options/optionenum.py new file mode 100644 index 000000000..095b9c356 --- /dev/null +++ b/src/buildstream/_options/optionenum.py @@ -0,0 +1,77 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import _yaml +from .._exceptions import LoadError, LoadErrorReason +from .option import Option, OPTION_SYMBOLS + + +# OptionEnum +# +# An enumeration project option +# +class OptionEnum(Option): + + OPTION_TYPE = 'enum' + + def load(self, node, allow_default_definition=True): + super(OptionEnum, self).load(node) + + valid_symbols = OPTION_SYMBOLS + ['values'] + if allow_default_definition: + valid_symbols += ['default'] + + _yaml.node_validate(node, valid_symbols) + + self.values = _yaml.node_get(node, list, 'values', default_value=[]) + if not self.values: + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: No values specified for {} option '{}'" + .format(_yaml.node_get_provenance(node), self.OPTION_TYPE, self.name)) + + # Allow subclass to define the default value + self.value = self.load_default_value(node) + + def load_value(self, node, *, transform=None): + self.value = _yaml.node_get(node, str, self.name) + if transform: + self.value = transform(self.value) + self.validate(self.value, _yaml.node_get_provenance(node, self.name)) + + def set_value(self, value): + self.validate(value) + self.value = value + + def get_value(self): + return self.value + + def validate(self, value, provenance=None): + if value not in self.values: + prefix = "" + if provenance: + prefix = "{}: ".format(provenance) + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}Invalid value for {} option '{}': {}\n" + .format(prefix, self.OPTION_TYPE, self.name, value) + + "Valid values: {}".format(", ".join(self.values))) + + def load_default_value(self, node): + value = _yaml.node_get(node, str, 'default') + self.validate(value, _yaml.node_get_provenance(node, 'default')) + return value diff --git a/src/buildstream/_options/optionflags.py b/src/buildstream/_options/optionflags.py new file mode 100644 index 000000000..0271208d9 --- /dev/null +++ b/src/buildstream/_options/optionflags.py @@ -0,0 +1,86 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> + +from .. import _yaml +from .._exceptions import LoadError, LoadErrorReason +from .option import Option, OPTION_SYMBOLS + + +# OptionFlags +# +# A flags project option +# +class OptionFlags(Option): + + OPTION_TYPE = 'flags' + + def load(self, node, allow_value_definitions=True): + super(OptionFlags, self).load(node) + + valid_symbols = OPTION_SYMBOLS + ['default'] + if allow_value_definitions: + valid_symbols += ['values'] + + _yaml.node_validate(node, valid_symbols) + + # Allow subclass to define the valid values + self.values = self.load_valid_values(node) + if not self.values: + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: No values specified for {} option '{}'" + .format(_yaml.node_get_provenance(node), self.OPTION_TYPE, self.name)) + + self.value = _yaml.node_get(node, list, 'default', default_value=[]) + self.validate(self.value, _yaml.node_get_provenance(node, 'default')) + + def load_value(self, node, *, transform=None): + self.value = _yaml.node_get(node, list, self.name) + if transform: + self.value = [transform(x) for x in self.value] + self.value = sorted(self.value) + self.validate(self.value, _yaml.node_get_provenance(node, self.name)) + + def set_value(self, value): + # Strip out all whitespace, allowing: "value1, value2 , value3" + stripped = "".join(value.split()) + + # Get the comma separated values + list_value = stripped.split(',') + + self.validate(list_value) + self.value = sorted(list_value) + + def get_value(self): + return ",".join(self.value) + + def validate(self, value, provenance=None): + for flag in value: + if flag not in self.values: + prefix = "" + if provenance: + prefix = "{}: ".format(provenance) + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}Invalid value for flags option '{}': {}\n" + .format(prefix, self.name, value) + + "Valid values: {}".format(", ".join(self.values))) + + def load_valid_values(self, node): + # Allow the more descriptive error to raise when no values + # exist rather than bailing out here (by specifying default_value) + return _yaml.node_get(node, list, 'values', default_value=[]) diff --git a/src/buildstream/_options/optionos.py b/src/buildstream/_options/optionos.py new file mode 100644 index 000000000..2d46b70ba --- /dev/null +++ b/src/buildstream/_options/optionos.py @@ -0,0 +1,41 @@ + +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Raoul Hidalgo Charman <raoul.hidalgocharman@codethink.co.uk> + +import platform +from .optionenum import OptionEnum + + +# OptionOS +# +class OptionOS(OptionEnum): + + OPTION_TYPE = 'os' + + def load(self, node): + super(OptionOS, self).load(node, allow_default_definition=False) + + def load_default_value(self, node): + return platform.uname().system + + def resolve(self): + + # Validate that the default OS reported by uname() is explicitly + # supported by the project, if not overridden by user config or cli. + self.validate(self.value) diff --git a/src/buildstream/_options/optionpool.py b/src/buildstream/_options/optionpool.py new file mode 100644 index 000000000..de3af3e15 --- /dev/null +++ b/src/buildstream/_options/optionpool.py @@ -0,0 +1,295 @@ +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# + +import jinja2 + +from .. import _yaml +from .._exceptions import LoadError, LoadErrorReason +from .optionbool import OptionBool +from .optionenum import OptionEnum +from .optionflags import OptionFlags +from .optioneltmask import OptionEltMask +from .optionarch import OptionArch +from .optionos import OptionOS + + +_OPTION_TYPES = { + OptionBool.OPTION_TYPE: OptionBool, + OptionEnum.OPTION_TYPE: OptionEnum, + OptionFlags.OPTION_TYPE: OptionFlags, + OptionEltMask.OPTION_TYPE: OptionEltMask, + OptionArch.OPTION_TYPE: OptionArch, + OptionOS.OPTION_TYPE: OptionOS, +} + + +class OptionPool(): + + def __init__(self, element_path): + # We hold on to the element path for the sake of OptionEltMask + self.element_path = element_path + + # + # Private members + # + self._options = {} # The Options + self._variables = None # The Options resolved into typed variables + + # jinja2 environment, with default globals cleared out of the way + self._environment = jinja2.Environment(undefined=jinja2.StrictUndefined) + self._environment.globals = [] + + # load() + # + # Loads the options described in the project.conf + # + # Args: + # node (dict): The loaded YAML options + # + def load(self, options): + + for option_name, option_definition in _yaml.node_items(options): + + # Assert that the option name is a valid symbol + p = _yaml.node_get_provenance(options, option_name) + _yaml.assert_symbol_name(p, option_name, "option name", allow_dashes=False) + + opt_type_name = _yaml.node_get(option_definition, str, 'type') + try: + opt_type = _OPTION_TYPES[opt_type_name] + except KeyError: + p = _yaml.node_get_provenance(option_definition, 'type') + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: Invalid option type '{}'".format(p, opt_type_name)) + + option = opt_type(option_name, option_definition, self) + self._options[option_name] = option + + # load_yaml_values() + # + # Loads the option values specified in a key/value + # dictionary loaded from YAML + # + # Args: + # node (dict): The loaded YAML options + # + def load_yaml_values(self, node, *, transform=None): + for option_name in _yaml.node_keys(node): + try: + option = self._options[option_name] + except KeyError as e: + p = _yaml.node_get_provenance(node, option_name) + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: Unknown option '{}' specified" + .format(p, option_name)) from e + option.load_value(node, transform=transform) + + # load_cli_values() + # + # Loads the option values specified in a list of tuples + # collected from the command line + # + # Args: + # cli_options (list): A list of (str, str) tuples + # ignore_unknown (bool): Whether to silently ignore unknown options. + # + def load_cli_values(self, cli_options, *, ignore_unknown=False): + for option_name, option_value in cli_options: + try: + option = self._options[option_name] + except KeyError as e: + if not ignore_unknown: + raise LoadError(LoadErrorReason.INVALID_DATA, + "Unknown option '{}' specified on the command line" + .format(option_name)) from e + else: + option.set_value(option_value) + + # resolve() + # + # Resolves the loaded options, this is just a step which must be + # performed after loading all options and their values, and before + # ever trying to evaluate an expression + # + def resolve(self): + self._variables = {} + for option_name, option in self._options.items(): + # Delegate one more method for options to + # do some last minute validation once any + # overrides have been performed. + # + option.resolve() + + self._variables[option_name] = option.value + + # export_variables() + # + # Exports the option values which are declared + # to be exported, to the passed dictionary. + # + # Variable values are exported in string form + # + # Args: + # variables (dict): A variables dictionary + # + def export_variables(self, variables): + for _, option in self._options.items(): + if option.variable: + _yaml.node_set(variables, option.variable, option.get_value()) + + # printable_variables() + # + # Exports all option names and string values + # to the passed dictionary in alphabetical order. + # + # Args: + # variables (dict): A variables dictionary + # + def printable_variables(self, variables): + for key in sorted(self._options): + variables[key] = self._options[key].get_value() + + # process_node() + # + # Args: + # node (node): A YAML Loaded dictionary + # + def process_node(self, node): + + # A conditional will result in composition, which can + # in turn add new conditionals to the root. + # + # Keep processing conditionals on the root node until + # all directly nested conditionals are resolved. + # + while self._process_one_node(node): + pass + + # Now recurse into nested dictionaries and lists + # and process any indirectly nested conditionals. + # + for _, value in _yaml.node_items(node): + if _yaml.is_node(value): + self.process_node(value) + elif isinstance(value, list): + self._process_list(value) + + ####################################################### + # Private Methods # + ####################################################### + + # _evaluate() + # + # Evaluates a jinja2 style expression with the loaded options in context. + # + # Args: + # expression (str): The jinja2 style expression + # + # Returns: + # (bool): Whether the expression resolved to a truthy value or a falsy one. + # + # Raises: + # LoadError: If the expression failed to resolve for any reason + # + def _evaluate(self, expression): + + # + # Variables must be resolved at this point. + # + try: + template_string = "{{% if {} %}} True {{% else %}} False {{% endif %}}".format(expression) + template = self._environment.from_string(template_string) + context = template.new_context(self._variables, shared=True) + result = template.root_render_func(context) + evaluated = jinja2.utils.concat(result) + val = evaluated.strip() + + if val == "True": + return True + elif val == "False": + return False + else: # pragma: nocover + raise LoadError(LoadErrorReason.EXPRESSION_FAILED, + "Failed to evaluate expression: {}".format(expression)) + except jinja2.exceptions.TemplateError as e: + raise LoadError(LoadErrorReason.EXPRESSION_FAILED, + "Failed to evaluate expression ({}): {}".format(expression, e)) + + # Recursion assistent for lists, in case there + # are lists of lists. + # + def _process_list(self, values): + for value in values: + if _yaml.is_node(value): + self.process_node(value) + elif isinstance(value, list): + self._process_list(value) + + # Process a single conditional, resulting in composition + # at the root level on the passed node + # + # Return true if a conditional was processed. + # + def _process_one_node(self, node): + conditions = _yaml.node_get(node, list, '(?)', default_value=None) + assertion = _yaml.node_get(node, str, '(!)', default_value=None) + + # Process assersions first, we want to abort on the first encountered + # assertion in a given dictionary, and not lose an assertion due to + # it being overwritten by a later assertion which might also trigger. + if assertion is not None: + p = _yaml.node_get_provenance(node, '(!)') + raise LoadError(LoadErrorReason.USER_ASSERTION, + "{}: {}".format(p, assertion.strip())) + + if conditions is not None: + + # Collect provenance first, we need to delete the (?) key + # before any composition occurs. + provenance = [ + _yaml.node_get_provenance(node, '(?)', indices=[i]) + for i in range(len(conditions)) + ] + _yaml.node_del(node, '(?)') + + for condition, p in zip(conditions, provenance): + tuples = list(_yaml.node_items(condition)) + if len(tuples) > 1: + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: Conditional statement has more than one key".format(p)) + + expression, value = tuples[0] + try: + apply_fragment = self._evaluate(expression) + except LoadError as e: + # Prepend the provenance of the error + raise LoadError(e.reason, "{}: {}".format(p, e)) from e + + if not _yaml.is_node(value): + raise LoadError(LoadErrorReason.ILLEGAL_COMPOSITE, + "{}: Only values of type 'dict' can be composed.".format(p)) + + # Apply the yaml fragment if its condition evaluates to true + if apply_fragment: + _yaml.composite(node, value) + + return True + + return False |