def _resolve_variable(cls, config, substitution): """ :param config: :param substitution: :return: (is_resolved, resolved_variable) """ variable = substitution.variable try: return True, config.get(variable) except ConfigMissingException: # default to environment variable value = os.environ.get(variable) if value is None: if substitution.optional: return False, None else: raise ConfigSubstitutionException( "Cannot resolve variable ${{{variable}}} (line: {line}, col: {col})" .format(variable=variable, line=lineno(substitution.loc, substitution.instring), col=col(substitution.loc, substitution.instring))) elif isinstance(value, ConfigList) or isinstance( value, ConfigTree): raise ConfigSubstitutionException( "Cannot substitute variable ${{{variable}}} because it does not point to a " "string, int, float, boolean or null {type} (line:{line}, col: {col})" .format(variable=variable, type=value.__class__.__name__, line=lineno(substitution.loc, substitution.instring), col=col(substitution.loc, substitution.instring))) return True, value
def _fixup_self_references(cls, config, accept_unresolved=False): if isinstance(config, ConfigTree) and config.root: for key in config: # Traverse history of element history = config.history[key] previous_item = history[0] for current_item in history[1:]: for substitution in cls._find_substitutions(current_item): prop_path = ConfigTree.parse_key(substitution.variable) if len(prop_path) > 1 and config.get(substitution.variable, None) is not None: continue # If value is present in latest version, don't do anything if prop_path[0] == key: if isinstance(previous_item, ConfigValues) and not accept_unresolved: # We hit a dead end, we cannot evaluate raise ConfigSubstitutionException( "Property {variable} cannot be substituted. Check for cycles.".format( variable=substitution.variable ) ) else: value = previous_item if len(prop_path) == 1 else previous_item.get(".".join(prop_path[1:])) _, _, current_item = cls._do_substitute(substitution, value) previous_item = current_item if len(history) == 1: # special case, when self optional referencing without existing for substitution in cls._find_substitutions(previous_item): prop_path = ConfigTree.parse_key(substitution.variable) if len(prop_path) > 1 and config.get(substitution.variable, None) is not None: continue # If value is present in latest version, don't do anything if prop_path[0] == key and substitution.optional: cls._do_substitute(substitution, None)
def _resolve_substitutions(config, substitutions): if len(substitutions) > 0: _substitutions = set(substitutions) for i in range(len(substitutions)): unresolved = False for substitution in list(_substitutions): resolved_value = ConfigParser._resolve_variable( config, substitution) if isinstance(resolved_value, ConfigValues): unresolved = True else: # replace token by substitution config_values = substitution.parent # if there is more than one element in the config values then it's a string tokens = substitution.parent.tokens config_values.put(substitution.index, resolved_value) config_values.parent[ config_values.key] = config_values.transform() _substitutions.remove(substitution) if not unresolved: break else: raise ConfigSubstitutionException( "Cannot resolve {variables}. Check for cycles.".format( variables=', '.join( '${' + substitution.variable + '}' for substitution in _substitutions))) return config
def resolve_substitutions(config): ConfigParser._fixup_self_references(config) substitutions = ConfigParser._find_substitutions(config) if len(substitutions) > 0: unresolved = True any_unresolved = True _substitutions = [] while any_unresolved and len(substitutions) > 0 and set(substitutions) != set(_substitutions): unresolved = False any_unresolved = True _substitutions = substitutions[:] for substitution in _substitutions: is_optional_resolved, resolved_value = ConfigParser._resolve_variable(config, substitution) # if the substitution is optional if not is_optional_resolved and substitution.optional: resolved_value = None unresolved, new_substitutions, result = ConfigParser._do_substitute(substitution, resolved_value, is_optional_resolved) any_unresolved = unresolved or any_unresolved substitutions.extend(new_substitutions) if not isinstance(result, ConfigValues): substitutions.remove(substitution) ConfigParser._final_fixup(config) if unresolved: raise ConfigSubstitutionException("Cannot resolve {variables}. Check for cycles.".format( variables=', '.join('${{{variable}}}: (line: {line}, col: {col})'.format( variable=substitution.variable, line=lineno(substitution.loc, substitution.instring), col=col(substitution.loc, substitution.instring)) for substitution in substitutions))) return config
def _resolve_variable(config, substitution): variable = substitution.variable try: return config.get(variable) except: # default to environment variable value = os.environ.get(variable) if value is None: raise ConfigSubstitutionException("Cannot resolve variable ${{{variable}}}".format(variable=variable)) elif isinstance(value, ConfigList) or isinstance(value, ConfigTree): raise ConfigSubstitutionException( "Cannot substitute variable ${{{variable}}} because it does not point to a string, int, float, boolean or null (type)".format( variable=variable, type=value.__class__.__name__) ) return value
def resolve_substitutions(cls, config, accept_unresolved=False): has_unresolved = False cls._fixup_self_references(config, accept_unresolved) substitutions = cls._find_substitutions(config) if len(substitutions) > 0: unresolved = True any_unresolved = True _substitutions = [] cache = {} while any_unresolved and len(substitutions) > 0 and set(substitutions) != set(_substitutions): unresolved = False any_unresolved = True _substitutions = substitutions[:] for substitution in _substitutions: is_optional_resolved, resolved_value = cls._resolve_variable(config, substitution) # if the substitution is optional if not is_optional_resolved and substitution.optional: resolved_value = None if isinstance(resolved_value, ConfigValues): parents = cache.get(resolved_value) if parents is None: parents = [] link = resolved_value while isinstance(link, ConfigValues): parents.append(link) link = link.overriden_value cache[resolved_value] = parents if isinstance(resolved_value, ConfigValues) \ and substitution.parent in parents \ and hasattr(substitution.parent, 'overriden_value') \ and substitution.parent.overriden_value: # self resolution, backtrack resolved_value = substitution.parent.overriden_value unresolved, new_substitutions, result = cls._do_substitute(substitution, resolved_value, is_optional_resolved) any_unresolved = unresolved or any_unresolved substitutions.extend(new_substitutions) if not isinstance(result, ConfigValues): substitutions.remove(substitution) cls._final_fixup(config) if unresolved: has_unresolved = True if not accept_unresolved: raise ConfigSubstitutionException("Cannot resolve {variables}. Check for cycles.".format( variables=', '.join('${{{variable}}}: (line: {line}, col: {col})'.format( variable=substitution.variable, line=lineno(substitution.loc, substitution.instring), col=col(substitution.loc, substitution.instring)) for substitution in substitutions))) cls._final_fixup(config) return has_unresolved
def parse(cls, content, basedir=None, resolve=True, unresolved_value=DEFAULT_SUBSTITUTION): """parse a HOCON content :param content: HOCON content to parse :type content: basestring :param resolve: if true, resolve substitutions :type resolve: boolean :param unresolved_value: assigned value to unresolved substitution. If overriden with a default value, it will replace all unresolved values by the default value. If it is set to pyhocon.STR_SUBSTITUTION then it will replace the value by its substitution expression (e.g., ${x}) :type unresolved_value: boolean :return: a ConfigTree or a list """ unescape_pattern = re.compile(r'\\.') def replace_escape_sequence(match): value = match.group(0) return cls.REPLACEMENTS.get(value, value) def norm_string(value): return unescape_pattern.sub(replace_escape_sequence, value) def unescape_string(tokens): return ConfigUnquotedString(norm_string(tokens[0])) def parse_multi_string(tokens): # remove the first and last 3 " return tokens[0][3:-3] def convert_number(tokens): n = tokens[0] try: return int(n, 10) except ValueError: return float(n) def convert_period(tokens): period_value = int(tokens.value) period_identifier = tokens.unit period_unit = next((single_unit for single_unit, values in cls.get_supported_period_type_map().items() if period_identifier in values)) return period(period_value, period_unit) # ${path} or ${?path} for optional substitution SUBSTITUTION_PATTERN = r"\$\{(?P<optional>\?)?(?P<variable>[^}]+)\}(?P<ws>[ \t]*)" def create_substitution(instring, loc, token): # remove the ${ and } match = re.match(SUBSTITUTION_PATTERN, token[0]) variable = match.group('variable') ws = match.group('ws') optional = match.group('optional') == '?' substitution = ConfigSubstitution(variable, optional, ws, instring, loc) return substitution # ${path} or ${?path} for optional substitution STRING_PATTERN = '"(?P<value>(?:[^"\\\\]|\\\\.)*)"(?P<ws>[ \t]*)' def create_quoted_string(instring, loc, token): # remove the ${ and } match = re.match(STRING_PATTERN, token[0]) value = norm_string(match.group('value')) ws = match.group('ws') return ConfigQuotedString(value, ws, instring, loc) def include_config(instring, loc, token): url = None file = None required = False if token[0] == 'required': required = True final_tokens = token[1:] else: final_tokens = token if len(final_tokens) == 1: # include "test" value = final_tokens[0].value if isinstance( final_tokens[0], ConfigQuotedString) else final_tokens[0] if value.startswith("http://") or value.startswith( "https://") or value.startswith("file://"): url = value else: file = value elif len(final_tokens) == 2: # include url("test") or file("test") value = final_tokens[1].value if isinstance( final_tokens[1], ConfigQuotedString) else final_tokens[1] if final_tokens[0] == 'url': url = value elif final_tokens[0] == 'package': file = cls.resolve_package_path(value) else: file = value if url is not None: logger.debug('Loading config from url %s', url) obj = ConfigFactory.parse_URL(url, resolve=False, required=required, unresolved_value=NO_SUBSTITUTION) elif file is not None: path = file if basedir is None else os.path.join(basedir, file) def _make_prefix(path): return ('<root>' if path is None else '[%s]' % path).ljust(55).replace('\\', '/') _prefix = _make_prefix(path) def _load(path): _prefix = _make_prefix(path) logger.debug('%s Loading config from file %r', _prefix, path) obj = ConfigFactory.parse_file( path, resolve=False, required=required, unresolved_value=NO_SUBSTITUTION) logger.debug('%s Result: %s', _prefix, obj) return obj if '*' in path or '?' in path: paths = glob(path, recursive=True) obj = None def _merge(a, b): if a is None or b is None: return a or b elif isinstance(a, ConfigTree) and isinstance( b, ConfigTree): return ConfigTree.merge_configs(a, b) elif isinstance(a, list) and isinstance(b, list): return a + b else: raise ConfigException( 'Unable to make such include (merging unexpected types: {a} and {b}', a=type(a), b=type(b)) logger.debug('%s Loading following configs: %s', _prefix, paths) for p in paths: obj = _merge(obj, _load(p)) logger.debug('%s Result: %s', _prefix, obj) else: logger.debug('%s Loading single config: %s', _prefix, path) obj = _load(path) else: raise ConfigException( 'No file or URL specified at: {loc}: {instring}', loc=loc, instring=instring) return ConfigInclude(obj if isinstance(obj, list) else obj.items()) @contextlib.contextmanager def set_default_white_spaces(): default = ParserElement.DEFAULT_WHITE_CHARS ParserElement.setDefaultWhitespaceChars(' \t') yield ParserElement.setDefaultWhitespaceChars(default) with set_default_white_spaces(): assign_expr = Forward() true_expr = Keyword("true", caseless=True).setParseAction( replaceWith(True)) false_expr = Keyword("false", caseless=True).setParseAction( replaceWith(False)) null_expr = Keyword("null", caseless=True).setParseAction( replaceWith(NoneValue())) key = QuotedString( '"', escChar='\\', unquoteResults=False) | Word(alphanums + alphas8bit + '._- /') eol = Word('\n\r').suppress() eol_comma = Word('\n\r,').suppress() comment = (Literal('#') | Literal('//')) - SkipTo(eol | StringEnd()) comment_eol = Suppress(Optional(eol_comma) + comment) comment_no_comma_eol = (comment | eol).suppress() number_expr = Regex( r'[+-]?(\d*\.\d+|\d+(\.\d+)?)([eE][+\-]?\d+)?(?=$|[ \t]*([\$\}\],#\n\r]|//))', re.DOTALL).setParseAction(convert_number) # Flatten the list of lists with unit strings. period_types = list( itertools.chain(*cls.get_supported_period_type_map().values())) # `Or()` tries to match the longest expression if more expressions # are matching. We employ this to match e.g.: 'weeks' so that we # don't end up with 'w' and 'eeks'. Note that 'weeks' but also 'w' # are valid unit identifiers. # Allow only spaces as a valid separator between value and unit. # E.g. \t as a separator is invalid: '10<TAB>weeks'. period_expr = ( Word(nums)('value') + ZeroOrMore(White(ws=' ')).suppress() + Or(period_types)('unit') + WordEnd(alphanums).suppress()).setParseAction(convert_period) # multi line string using """ # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969 multiline_string = Regex( '""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string) # single quoted line string quoted_string = Regex( r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string) # unquoted string that takes the rest of the line until an optional comment # we support .properties multiline support which is like this: # line1 \ # line2 \ # so a backslash precedes the \n unquoted_string = Regex( r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).setParseAction(unescape_string) substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*' ).setParseAction(create_substitution) string_expr = multiline_string | quoted_string | unquoted_string value_expr = period_expr | number_expr | true_expr | false_expr | null_expr | string_expr include_content = (quoted_string | ( (Keyword('url') | Keyword('file') | Keyword('package')) - Literal('(').suppress() - quoted_string - Literal(')').suppress())) include_expr = (Keyword("include", caseless=True).suppress() + (include_content | (Keyword("required") - Literal('(').suppress() - include_content - Literal(')').suppress())) ).setParseAction(include_config) root_dict_expr = Forward() dict_expr = Forward() list_expr = Forward() multi_value_expr = ZeroOrMore(comment_eol | include_expr | substitution_expr | dict_expr | list_expr | value_expr | (Literal('\\') - eol).suppress()) # for a dictionary : or = is optional # last zeroOrMore is because we can have t = {a:4} {b: 6} {c: 7} which is dictionary concatenation inside_dict_expr = ConfigTreeParser( ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma)) inside_root_dict_expr = ConfigTreeParser( ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma), root=True) dict_expr << Suppress('{') - inside_dict_expr - Suppress('}') root_dict_expr << Suppress('{') - inside_root_dict_expr - Suppress( '}') list_entry = ConcatenatedValueParser(multi_value_expr) list_expr << Suppress('[') - ListParser(list_entry - ZeroOrMore( eol_comma - list_entry)) - Suppress(']') # special case when we have a value assignment where the string can potentially be the remainder of the line assign_expr << Group(key - ZeroOrMore(comment_no_comma_eol) - ( dict_expr | (Literal('=') | Literal(':') | Literal('+=')) - ZeroOrMore(comment_no_comma_eol) - ConcatenatedValueParser(multi_value_expr))) # the file can be { ... } where {} can be omitted or [] config_expr = ZeroOrMore(comment_eol | eol) + ( list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore(comment_eol | eol_comma) config = config_expr.parseString(content, parseAll=True)[0] if resolve: allow_unresolved = resolve and unresolved_value is not DEFAULT_SUBSTITUTION and unresolved_value is not MANDATORY_SUBSTITUTION has_unresolved = cls.resolve_substitutions( config, allow_unresolved) if has_unresolved and unresolved_value is MANDATORY_SUBSTITUTION: raise ConfigSubstitutionException( 'resolve cannot be set to True and unresolved_value to MANDATORY_SUBSTITUTION' ) if unresolved_value is not NO_SUBSTITUTION and unresolved_value is not DEFAULT_SUBSTITUTION: cls.unresolve_substitutions_to_value(config, unresolved_value) return config
def _resolve_substitutions(config): # traverse config to find all the substitutions def find_substitutions(item): """Convert HOCON input into a JSON output :return: JSON string representation :type return: basestring """ if isinstance(item, ConfigValues): return item.get_substitutions() substitutions = [] if isinstance(item, ConfigTree): for key, child in item.items(): substitutions += find_substitutions(child) elif isinstance(item, list): for child in item: substitutions += find_substitutions(child) return substitutions substitutions = find_substitutions(config) if len(substitutions) > 0: _substitutions = set(substitutions) for i in range(len(substitutions)): unresolved = False for substitution in list(_substitutions): is_optional_resolved, resolved_value = ConfigParser._resolve_variable( config, substitution) # if the substitution is optional if not is_optional_resolved and substitution.optional: resolved_value = None if isinstance(resolved_value, ConfigValues): unresolved = True else: # replace token by substitution config_values = substitution.parent # if it is a string, then add the extra ws that was present in the original string after the substitution formatted_resolved_value = \ resolved_value + substitution.ws \ if isinstance(resolved_value, str) and substitution.index < len(config_values.tokens) - 1 else resolved_value config_values.put(substitution.index, formatted_resolved_value) transformation = config_values.transform() if transformation is None and not is_optional_resolved: # if it does not override anything remove the key # otherwise put back old value that it was overriding if config_values.overriden_value is None: del config_values.parent[config_values.key] else: config_values.parent[ config_values. key] = config_values.overriden_value else: result = transformation[0] if isinstance( transformation, list) else transformation config_values.parent[config_values.key] = result _substitutions.remove(substitution) if not unresolved: break else: raise ConfigSubstitutionException( "Cannot resolve {variables}. Check for cycles.". format(variables=', '.join( '${{{variable}}}: (line: {line}, col: {col})'.format( variable=substitution.variable, line=lineno(substitution.loc, substitution.instring), col=col(substitution.loc, substitution.instring)) for substitution in _substitutions))) return config
def parse(cls, content, basedir=None, resolve=True, unresolved_value=DEFAULT_SUBSTITUTION): """parse a HOCON content :param content: HOCON content to parse :type content: basestring :param resolve: if true, resolve substitutions :type resolve: boolean :param unresolved_value: assigned value value to unresolved substitution. If overriden with a default value, it will replace all unresolved value to the default value. If it is set to to pyhocon.STR_SUBSTITUTION then it will replace the value by its substitution expression (e.g., ${x}) :type unresolved_value: boolean :return: a ConfigTree or a list """ unescape_pattern = re.compile(r'\\.') def replace_escape_sequence(match): value = match.group(0) return cls.REPLACEMENTS.get(value, value) def norm_string(value): return unescape_pattern.sub(replace_escape_sequence, value) def unescape_string(tokens): return ConfigUnquotedString(norm_string(tokens[0])) def parse_multi_string(tokens): # remove the first and last 3 " return tokens[0][3: -3] def convert_number(tokens): n = tokens[0] try: return int(n, 10) except ValueError: return float(n) # ${path} or ${?path} for optional substitution SUBSTITUTION_PATTERN = r"\$\{(?P<optional>\?)?(?P<variable>[^}]+)\}(?P<ws>[ \t]*)" def create_substitution(instring, loc, token): # remove the ${ and } match = re.match(SUBSTITUTION_PATTERN, token[0]) variable = match.group('variable') ws = match.group('ws') optional = match.group('optional') == '?' substitution = ConfigSubstitution(variable, optional, ws, instring, loc) return substitution # ${path} or ${?path} for optional substitution STRING_PATTERN = '"(?P<value>(?:[^"\\\\]|\\\\.)*)"(?P<ws>[ \t]*)' def create_quoted_string(instring, loc, token): # remove the ${ and } match = re.match(STRING_PATTERN, token[0]) value = norm_string(match.group('value')) ws = match.group('ws') return ConfigQuotedString(value, ws, instring, loc) def include_config(instring, loc, token): url = None file = None required = False if token[0] == 'required': required = True final_tokens = token[1:] else: final_tokens = token if len(final_tokens) == 1: # include "test" value = final_tokens[0].value if isinstance(final_tokens[0], ConfigQuotedString) else final_tokens[0] if value.startswith("http://") or value.startswith("https://") or value.startswith("file://"): url = value else: file = value elif len(final_tokens) == 2: # include url("test") or file("test") value = final_tokens[1].value if isinstance(token[1], ConfigQuotedString) else final_tokens[1] if final_tokens[0] == 'url': url = value else: file = value if url is not None: logger.debug('Loading config from url %s', url) obj = ConfigFactory.parse_URL( url, resolve=False, required=required, unresolved_value=NO_SUBSTITUTION ) elif file is not None: path = file if basedir is None else os.path.join(basedir, file) logger.debug('Loading config from file %s', path) obj = ConfigFactory.parse_file( path, resolve=False, required=required, unresolved_value=NO_SUBSTITUTION ) else: raise ConfigException('No file or URL specified at: {loc}: {instring}', loc=loc, instring=instring) return ConfigInclude(obj if isinstance(obj, list) else obj.items()) ParserElement.setDefaultWhitespaceChars(' \t') assign_expr = Forward() true_expr = Keyword("true", caseless=True).setParseAction(replaceWith(True)) false_expr = Keyword("false", caseless=True).setParseAction(replaceWith(False)) null_expr = Keyword("null", caseless=True).setParseAction(replaceWith(NoneValue())) key = QuotedString('"', escChar='\\', unquoteResults=False) | Word(alphanums + alphas8bit + '._- /') eol = Word('\n\r').suppress() eol_comma = Word('\n\r,').suppress() comment = (Literal('#') | Literal('//')) - SkipTo(eol | StringEnd()) comment_eol = Suppress(Optional(eol_comma) + comment) comment_no_comma_eol = (comment | eol).suppress() number_expr = Regex(r'[+-]?(\d*\.\d+|\d+(\.\d+)?)([eE][+\-]?\d+)?(?=$|[ \t]*([\$\}\],#\n\r]|//))', re.DOTALL).setParseAction(convert_number) # multi line string using """ # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969 multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string) # single quoted line string quoted_string = Regex(r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string) # unquoted string that takes the rest of the line until an optional comment # we support .properties multiline support which is like this: # line1 \ # line2 \ # so a backslash precedes the \n unquoted_string = Regex(r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).setParseAction(unescape_string) substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*').setParseAction(create_substitution) string_expr = multiline_string | quoted_string | unquoted_string value_expr = number_expr | true_expr | false_expr | null_expr | string_expr include_content = (quoted_string | ((Keyword('url') | Keyword('file')) - Literal('(').suppress() - quoted_string - Literal(')').suppress())) include_expr = ( Keyword("include", caseless=True).suppress() + ( include_content | ( Keyword("required") - Literal('(').suppress() - include_content - Literal(')').suppress() ) ) ).setParseAction(include_config) root_dict_expr = Forward() dict_expr = Forward() list_expr = Forward() multi_value_expr = ZeroOrMore(comment_eol | include_expr | substitution_expr | dict_expr | list_expr | value_expr | (Literal( '\\') - eol).suppress()) # for a dictionary : or = is optional # last zeroOrMore is because we can have t = {a:4} {b: 6} {c: 7} which is dictionary concatenation inside_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma)) inside_root_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma), root=True) dict_expr << Suppress('{') - inside_dict_expr - Suppress('}') root_dict_expr << Suppress('{') - inside_root_dict_expr - Suppress('}') list_entry = ConcatenatedValueParser(multi_value_expr) list_expr << Suppress('[') - ListParser(list_entry - ZeroOrMore(eol_comma - list_entry)) - Suppress(']') # special case when we have a value assignment where the string can potentially be the remainder of the line assign_expr << Group( key - ZeroOrMore(comment_no_comma_eol) - (dict_expr | (Literal('=') | Literal(':') | Literal('+=')) - ZeroOrMore( comment_no_comma_eol) - ConcatenatedValueParser(multi_value_expr)) ) # the file can be { ... } where {} can be omitted or [] config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore( comment_eol | eol_comma) config = config_expr.parseString(content, parseAll=True)[0] if resolve: allow_unresolved = resolve and unresolved_value is not DEFAULT_SUBSTITUTION and unresolved_value is not MANDATORY_SUBSTITUTION has_unresolved = cls.resolve_substitutions(config, allow_unresolved) if has_unresolved and unresolved_value is MANDATORY_SUBSTITUTION: raise ConfigSubstitutionException('resolve cannot be set to True and unresolved_value to MANDATORY_SUBSTITUTION') if unresolved_value is not NO_SUBSTITUTION and unresolved_value is not DEFAULT_SUBSTITUTION: cls.unresolve_substitutions_to_value(config, unresolved_value) return config