"""
cfg_loader.interpolation
~~~~~~~~~~~~~~~~~~~~~~~~
Implement functions to substitute environment variable input data
:copyright: Copyright 2017 by ConsenSys France.
:license: BSD, see :ref:`license` for more details.
"""
import re
import string
from .exceptions import UnsetRequiredSubstitution, InvalidSubstitution
# Brace formatted syntax separators (c.f. https://docs.docker.com/compose/compose-file/#variable-substitution)
SEPARATOR_DEFAULT_IF_EMPTY = ':-'
SEPARATOR_DEFAULT_IF_UNSET = '-'
SEPARATOR_ERROR_IF_EMPTY = ':?'
SEPARATOR_ERROR_IF_UNSET = '?'
[docs]class SubstitutionTemplate(string.Template):
"""Class used to substitute environment variables in a string
It implements specification from docker-compose environ variable substitution
(c.f. https://docs.docker.com/compose/compose-file/#variable-substitution)
Examples with basic substitution
>>> template = SubstitutionTemplate('${VARIABLE}')
>>> template.substitute({'VARIABLE': 'value'})
'value'
>>> template.substitute({'VARIABLE': ''})
''
>>> template.substitute({})
Traceback (most recent call last):
...
KeyError: 'VARIABLE'
Examples with substitution if variable is empty or unset (separator: ":-")
>>> template = SubstitutionTemplate('${VARIABLE:-default}')
>>> template.substitute({'VARIABLE': 'value'})
'value'
>>> template.substitute({'VARIABLE': ''})
'default'
>>> template.substitute({})
'default'
Examples with substitution if variable is empty (separator: "-"):
>>> template = SubstitutionTemplate('${VARIABLE-default}')
>>> template.substitute({'VARIABLE': 'value'})
'value'
>>> template.substitute({'VARIABLE': ''})
''
>>> template.substitute({})
'default'
Examples with error raised if variable is unset (separator: "?")
>>> template = SubstitutionTemplate('${VARIABLE?err}')
>>> template.substitute({'VARIABLE': 'value'})
'value'
>>> template.substitute({'VARIABLE': ''})
''
>>> template.substitute({})
Traceback (most recent call last):
...
cfg_loader.exceptions.UnsetRequiredSubstitution: err
Examples with error raised if variable is empty or unset (separator: ":?")
>>> template = SubstitutionTemplate('${VARIABLE:?err}')
>>> template.substitute({'VARIABLE': 'value'})
'value'
>>> template.substitute({'VARIABLE': ''})
Traceback (most recent call last):
...
cfg_loader.exceptions.UnsetRequiredSubstitution: err
>>> template.substitute({})
Traceback (most recent call last):
...
cfg_loader.exceptions.UnsetRequiredSubstitution: err
"""
pattern = r"""
%(delim)s(?:
(?P<escaped>%(delim)s) |
(?P<named>%(id)s) |
{(?P<braced>%(bid)s)} |
(?P<invalid>)
)
""" % {
'delim': re.escape('$'),
'id': r'[_a-z][_a-z0-9]*',
'bid': r'[_a-z][_a-z0-9]*(?:(?P<sep>:?[-?])[^}]*)?',
}
[docs] def substitute(self, mapping):
"""Substitute values indexed by mapping into `template`
:param mapping: Mapping containing values to substitute
:type mapping: dict
"""
def convert(mo):
named, braced = mo.group('named') or mo.group('braced'), mo.group('braced')
if braced is not None:
sep = mo.group('sep')
if sep:
return process_braced_group(braced, sep, mapping)
if named is not None:
val = mapping[named]
return '%s' % (val,)
if mo.group('escaped') is not None: # pragma: no branch
return self.delimiter
if mo.group('invalid') is not None: # pragma: no branch
raise ValueError('Invalid placeholder: {}'.format(self.template))
return self.pattern.sub(convert, self.template)
def process_braced_group(braced, sep, mapping):
"""Parse a braced formatted syntax and returns substituted value or raise error
It implements specification from docker-compose environ variable substitution
(c.f. https://docs.docker.com/compose/compose-file/#variable-substitution)
:param braced: Braced formatted syntax to substitute
:type braced: str
:param sep: Separator in the braced syntax
:type sep: str
:param mapping: Mapping with values to substitute
:type mapping: dict
"""
if sep == SEPARATOR_DEFAULT_IF_EMPTY:
var, _, default = braced.partition(SEPARATOR_DEFAULT_IF_EMPTY)
return mapping.get(var) or default
elif sep == SEPARATOR_DEFAULT_IF_UNSET:
var, _, default = braced.partition(SEPARATOR_DEFAULT_IF_UNSET)
return mapping.get(var, default)
elif sep == SEPARATOR_ERROR_IF_EMPTY:
var, _, err = braced.partition(SEPARATOR_ERROR_IF_EMPTY)
rv = mapping.get(var)
if not rv:
raise UnsetRequiredSubstitution(err)
return rv
elif sep == SEPARATOR_ERROR_IF_UNSET: # pragma: no branch
var, _, err = braced.partition(SEPARATOR_ERROR_IF_UNSET)
if var in mapping:
return mapping.get(var)
raise UnsetRequiredSubstitution(err)
[docs]class Interpolator:
"""Class used to substitute environment variables in complex object
:param substitution_mapping: Mapping with values to substitute
:type substitution_mapping: dict
Example
>>> interpolator = Interpolator(substitution_mapping={'VARIABLE': 'value'})
>>> interpolator.interpolate('${VARIABLE} in complex string')
'value in complex string'
>>> result = interpolator.interpolate_recursive({'key1': '${VARIABLE}', 'key2': ['element', '${EXTRA-default}']})
>>> result == {'key1': 'value', 'key2': ['element', 'default']}
True
"""
def __init__(self, substitution_mapping=None, substitution_template=SubstitutionTemplate):
self._substitution_template = substitution_template
self._substitution_mapping = substitution_mapping or {}
[docs] def interpolate(self, string):
"""Substitute environment variable in a string"""
try:
return self._substitution_template(string).substitute(self._substitution_mapping)
except ValueError as e:
raise InvalidSubstitution(e)
[docs] def interpolate_recursive(self, obj):
"""Substitute environment variable in an object"""
if isinstance(obj, str):
return self.interpolate(obj)
elif isinstance(obj, dict):
return {key: self.interpolate_recursive(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [self.interpolate_recursive(element) for element in obj]
return obj