Source code for condconfigparser.condconfig

# -*- coding: utf-8 -*-

# condconfig.py --- RawConditionalConfig class for CondConfigParser
#
# Copyright (c) 2014, 2015, Florent Rougon
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the CondConfigParser Project.

""":class:`RawConditionalConfig` class for CondConfigParser.

This module defines the :class:`condconfigparser.RawConditionalConfig`
class, which is the main application programming interface for using
CondConfigParser.

"""

import io
import functools
import operator

from .exceptions import InvalidUsage, UndefinedVariablesInAssignment, \
    UndefinedVariablesInPredicate
from .lexer import Lexer
from .parser import Parser


[docs] class DummyContextManager: """Do-nothing context manager. When used in a :keyword:`with` statement, instances of this class simply return the object that was passed to the constructor. """
[docs] def __init__(self, stream): self.stream = stream
[docs] def __enter__(self): return self.stream
[docs] def __exit__(self, *exc): return False
[docs] class RawConditionalConfig: """Class for dealing with files conforming to CondConfigParser syntax. This class provides the main part of the API for using CondConfigParser. """
[docs] def __init__(self, input, extvars=()): """Initialize a :class:`RawConditionalConfig` instance. :param input: input stream or string ("configuration file") :type input: file-like or string :param extvars: indicates the :term:`external variables <external variable>` :type extvars: sequence or set :return: a :class:`RawConditionalConfig` instance Read the configuration from *input* and check that the variables used therein can all be computed, directly or indirectly from the variables whose names are listed in *extvars*. """ if isinstance(input, str): ctxManager = io.StringIO(input) else: ctxManager = DummyContextManager(input) with ctxManager as stream: lexer = Lexer(stream) # Root node of the abstract syntax tree self._tree = Parser(lexer).buildTree() self._assignments = self._tree.assignments self._config = self._tree.config #: Names of variables defined externally, *i.e.*, not in the #: configuration file self.extvars = extvars self._checkUsesOfUndefinedVariables()
[docs] def _checkAssignments(self): """Check that all variable assignments can be executed. Check that all variable assignments can be executed, assuming that all variables whose names are listed in :attr:`extvars` are defined. If at least one variable assignment makes use of an undefined variable, raise :exc:`UndefinedVariablesInAssignment`. Otherwise, return the set of all defined variables (well, their names). """ # Set of variables that are well-defined (whose value can be computed # using the already-defined variables) defined = set(self.extvars) for variableNode, tree in self._assignments: undef = tree.undefVariables(defined) if undef: raise UndefinedVariablesInAssignment(variableNode.token, undef) else: defined.add(variableNode.name) return defined
[docs] def _checkPredicates(self, definedVars): """Check that all predicates can be evaluated. Raise :exc:`UndefinedVariablesInPredicate` if, and only if at least one predicate uses an undefined variable. """ for section in self._config: undef = section.predicate.undefVariables(definedVars) if undef: raise UndefinedVariablesInPredicate(section.startToken, undef)
[docs] def _checkUsesOfUndefinedVariables(self): """ Check that no variable assignment or predicate uses undefined variables. Raise :exc:`UndefinedVariablesInAssignment` or :exc:`UndefinedVariablesInPredicate` appropriately, otherwise return the set of all defined variables (well, their names). """ definedVars = self._checkAssignments() self._checkPredicates(definedVars) return definedVars
[docs] def computeVars(self, context): """Perform all variable assignments. :param dict context: mapping giving the value to use for every :term:`external variable`. More precisely, for each name of an external variable, it gives the value for initialization of this variable before starting to perform variable assignments. :return: a new dictionary giving the value of every variable, be it external or not. """ for varName in self.extvars: if varName not in context: raise InvalidUsage("missing external variable in 'context' " "argument: {!r}".format(varName)) variables = dict(context) # new dict object for variableNode, tree in self._assignments: variables[variableNode.name] = tree.eval(variables) return variables
[docs] def eval(self, context, *, flat=False): r"""Compute the values of variables and evaluate predicates. :param dict context: mapping giving the value to use for every :term:`external variable`. More precisely, for each name of an external variable, it gives the value for initialization of this variable before starting to perform variable assignments. :param bool flat: if true, the second element of the return value will be a list of :term:`raw configuration lines <raw configuration line>`; otherwise, it will be a list of lists of raw configuration lines, one for each section, starting with the :ref:`default, unconditional section <default-unconditional-section>`. :return: a :class:`tuple` of the form :samp:`({variables}, {lines})` where: - *variables* is a new dictionary giving the value of every variable, be it external or not; - *lines* is a list as indicated above in the *flat* parameter description. The following interactive session illustrates the effect of the *flat* argument:: >>> cfg = '''\ ... { var1 = (extvar1 == "foobar") # parentheses only for clarity ... var2 = var1 and "baz" in extvar2 } ... ... raw cfg default1 ... raw cfg default2 ... ... [ var2 or extvar1 == "quux" ] ... raw cfg a1 ... raw cfg a2 ... raw cfg a3 ... ... [ var1 ] ... raw cfg b1 ... raw cfg b2 ... ... [ not var1 ] ... raw cfg c1 ... raw cfg c2 ... ''' >>> config = RawConditionalConfig(cfg, extvars=("extvar1", "extvar2")) >>> context = {"extvar1": "quux", ... "extvar2": [12, "abc", [False, "def"]]} >>> [config.eval(context), config.eval(context, flat=True)] == \ ... [({'extvar1': 'quux', ... 'extvar2': [12, 'abc', [False, 'def']], ... 'var1': False, ... 'var2': False}, ... [['raw cfg default1', 'raw cfg default2'], ... ['raw cfg a1', 'raw cfg a2', 'raw cfg a3'], ... ['raw cfg c1', 'raw cfg c2']]), ... ({'extvar1': 'quux', ... 'extvar2': [12, 'abc', [False, 'def']], ... 'var1': False, ... 'var2': False}, ... ['raw cfg default1', ... 'raw cfg default2', ... 'raw cfg a1', ... 'raw cfg a2', ... 'raw cfg a3', ... 'raw cfg c1', ... 'raw cfg c2'])] True >>> The default value for the *flat* parameter (``False``) preserves as much information as possible, allowing user applications to implement things such as :ref:`conditional sections <conditional-sections>` overriding the :ref:`default, unconditional section <default-unconditional-section>`. """ variables = self.computeVars(context) res = [self._config.defaultConfigLines] for section in self._config.sections: if section.predicate.eval(variables): res.append(section.rawConfigLines) if flat: return (variables, functools.reduce(operator.add, res, [])) else: return (variables, res)