summaryrefslogtreecommitdiff
path: root/src/buildstream/_options
diff options
context:
space:
mode:
authorChandan Singh <csingh43@bloomberg.net>2019-04-24 22:53:19 +0100
committerChandan Singh <csingh43@bloomberg.net>2019-05-21 12:41:18 +0100
commit070d053e5cc47e572e9f9e647315082bd7a15c63 (patch)
tree7fb0fdff52f9b5f8a18ec8fe9c75b661f9e0839e /src/buildstream/_options
parent6c59e7901a52be961c2a1b671cf2b30f90bc4d0a (diff)
downloadbuildstream-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__.py20
-rw-r--r--src/buildstream/_options/option.py112
-rw-r--r--src/buildstream/_options/optionarch.py84
-rw-r--r--src/buildstream/_options/optionbool.py58
-rw-r--r--src/buildstream/_options/optioneltmask.py46
-rw-r--r--src/buildstream/_options/optionenum.py77
-rw-r--r--src/buildstream/_options/optionflags.py86
-rw-r--r--src/buildstream/_options/optionos.py41
-rw-r--r--src/buildstream/_options/optionpool.py295
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