Support ${VAR:?err} syntax for mandatory variables

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2018-01-03 18:30:26 -08:00
parent bcc13d7fae
commit e400c05de0
3 changed files with 94 additions and 10 deletions

View File

@ -60,6 +60,15 @@ def interpolate_value(name, config_key, value, section, interpolator):
name=name,
section=section,
string=e.string))
except UnsetRequiredSubstitution as e:
raise ConfigurationError(
'Missing mandatory value for "{config_key}" option in {section} "{name}": {err}'.format(
config_key=config_key,
name=name,
section=section,
err=e.err
)
)
def recursive_interpolate(obj, interpolator, config_path):
@ -79,21 +88,54 @@ def recursive_interpolate(obj, interpolator, config_path):
class TemplateWithDefaults(Template):
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]*)?'
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>:?[-?])[^}]*)?',
}
@staticmethod
def process_braced_group(braced, sep, mapping):
if ':-' == sep:
var, _, default = braced.partition(':-')
return mapping.get(var) or default
elif '-' == sep:
var, _, default = braced.partition('-')
return mapping.get(var, default)
elif ':?' == sep:
var, _, err = braced.partition(':?')
result = mapping.get(var)
if not result:
raise UnsetRequiredSubstitution(err)
return result
elif '?' == sep:
var, _, err = braced.partition('?')
if var in mapping:
return mapping.get(var)
raise UnsetRequiredSubstitution(err)
# Modified from python2.7/string.py
def substitute(self, mapping):
# Helper function for .sub()
def convert(mo):
# Check the most common path first.
named = mo.group('named') or mo.group('braced')
braced = mo.group('braced')
if braced is not None:
sep = mo.group('sep')
result = self.process_braced_group(braced, sep, mapping)
if result:
return result
if named is not None:
if ':-' in named:
var, _, default = named.partition(':-')
return mapping.get(var) or default
if '-' in named:
var, _, default = named.partition('-')
return mapping.get(var, default)
val = mapping[named]
return '%s' % (val,)
if mo.group('escaped') is not None:
@ -110,6 +152,11 @@ class InvalidInterpolation(Exception):
self.string = string
class UnsetRequiredSubstitution(Exception):
def __init__(self, custom_err_msg):
self.err = custom_err_msg
PATH_JOKER = '[^.]+'

View File

@ -37,7 +37,6 @@ class ConsoleWarningFormatterTestCase(unittest.TestCase):
def test_format_unicode_info(self):
message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95'
output = self.formatter.format(make_log_record(logging.INFO, message))
print(output)
assert output == message.decode('utf-8')
def test_format_unicode_warn(self):

View File

@ -9,6 +9,7 @@ from compose.config.interpolation import interpolate_environment_variables
from compose.config.interpolation import Interpolator
from compose.config.interpolation import InvalidInterpolation
from compose.config.interpolation import TemplateWithDefaults
from compose.config.interpolation import UnsetRequiredSubstitution
from compose.const import COMPOSEFILE_V2_0 as V2_0
from compose.const import COMPOSEFILE_V2_3 as V2_3
from compose.const import COMPOSEFILE_V3_4 as V3_4
@ -357,9 +358,46 @@ def test_interpolate_with_value(defaults_interpolator):
def test_interpolate_missing_with_default(defaults_interpolator):
assert defaults_interpolator("ok ${missing:-def}") == "ok def"
assert defaults_interpolator("ok ${missing-def}") == "ok def"
assert defaults_interpolator("ok ${BAR:-/non:-alphanumeric}") == "ok /non:-alphanumeric"
def test_interpolate_with_empty_and_default_value(defaults_interpolator):
assert defaults_interpolator("ok ${BAR:-def}") == "ok def"
assert defaults_interpolator("ok ${BAR-def}") == "ok "
def test_interpolate_mandatory_values(defaults_interpolator):
assert defaults_interpolator("ok ${FOO:?bar}") == "ok first"
assert defaults_interpolator("ok ${FOO?bar}") == "ok first"
assert defaults_interpolator("ok ${BAR?bar}") == "ok "
with pytest.raises(UnsetRequiredSubstitution) as e:
defaults_interpolator("not ok ${BAR:?high bar}")
assert e.value.err == 'high bar'
with pytest.raises(UnsetRequiredSubstitution) as e:
defaults_interpolator("not ok ${BAZ?dropped the bazz}")
assert e.value.err == 'dropped the bazz'
def test_interpolate_mandatory_no_err_msg(defaults_interpolator):
with pytest.raises(UnsetRequiredSubstitution) as e:
defaults_interpolator("not ok ${BAZ?}")
assert e.value.err == ''
def test_interpolate_mixed_separators(defaults_interpolator):
assert defaults_interpolator("ok ${BAR:-/non:-alphanumeric}") == "ok /non:-alphanumeric"
assert defaults_interpolator("ok ${BAR:-:?wwegegr??:?}") == "ok :?wwegegr??:?"
assert defaults_interpolator("ok ${BAR-:-hello}") == 'ok '
with pytest.raises(UnsetRequiredSubstitution) as e:
defaults_interpolator("not ok ${BAR:?xazz:-redf}")
assert e.value.err == 'xazz:-redf'
assert defaults_interpolator("ok ${BAR?...:?bar}") == "ok "
def test_unbraced_separators(defaults_interpolator):
assert defaults_interpolator("ok $FOO:-bar") == "ok first:-bar"
assert defaults_interpolator("ok $BAZ?error") == "ok ?error"