diff options
author | Chandan Singh <chandan@chandansingh.net> | 2019-05-21 17:08:31 +0000 |
---|---|---|
committer | Chandan Singh <chandan@chandansingh.net> | 2019-05-21 17:08:31 +0000 |
commit | 25172ed2d7b39cab799b1f2788d818d39ce3ee33 (patch) | |
tree | 423acdd8368649ced4c0fe7a4b92e586a13308a2 /src/buildstream/_options/optionpool.py | |
parent | 6c59e7901a52be961c2a1b671cf2b30f90bc4d0a (diff) | |
parent | 488b1f71c32bdae1d46b0aa1f07c82df0f0e53e2 (diff) | |
download | buildstream-25172ed2d7b39cab799b1f2788d818d39ce3ee33.tar.gz |
Merge branch 'chandan/src-directory' into 'master'
Move source from 'buildstream' to 'src/buildstream'
Closes #1009
See merge request BuildStream/buildstream!1322
Diffstat (limited to 'src/buildstream/_options/optionpool.py')
-rw-r--r-- | src/buildstream/_options/optionpool.py | 295 |
1 files changed, 295 insertions, 0 deletions
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 |