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, name=name,
section=section, section=section,
string=e.string)) 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): def recursive_interpolate(obj, interpolator, config_path):
@ -79,21 +88,54 @@ def recursive_interpolate(obj, interpolator, config_path):
class TemplateWithDefaults(Template): 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 # Modified from python2.7/string.py
def substitute(self, mapping): def substitute(self, mapping):
# Helper function for .sub() # Helper function for .sub()
def convert(mo): def convert(mo):
# Check the most common path first.
named = mo.group('named') or mo.group('braced') 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 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] val = mapping[named]
return '%s' % (val,) return '%s' % (val,)
if mo.group('escaped') is not None: if mo.group('escaped') is not None:
@ -110,6 +152,11 @@ class InvalidInterpolation(Exception):
self.string = string self.string = string
class UnsetRequiredSubstitution(Exception):
def __init__(self, custom_err_msg):
self.err = custom_err_msg
PATH_JOKER = '[^.]+' PATH_JOKER = '[^.]+'

View File

@ -37,7 +37,6 @@ class ConsoleWarningFormatterTestCase(unittest.TestCase):
def test_format_unicode_info(self): def test_format_unicode_info(self):
message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95'
output = self.formatter.format(make_log_record(logging.INFO, message)) output = self.formatter.format(make_log_record(logging.INFO, message))
print(output)
assert output == message.decode('utf-8') assert output == message.decode('utf-8')
def test_format_unicode_warn(self): 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 Interpolator
from compose.config.interpolation import InvalidInterpolation from compose.config.interpolation import InvalidInterpolation
from compose.config.interpolation import TemplateWithDefaults 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_0 as V2_0
from compose.const import COMPOSEFILE_V2_3 as V2_3 from compose.const import COMPOSEFILE_V2_3 as V2_3
from compose.const import COMPOSEFILE_V3_4 as V3_4 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): 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 ${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): 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"
assert defaults_interpolator("ok ${BAR-def}") == "ok " 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"