瀏覽代碼

Merge pull request #5531 from docker/5505-required-variables

Support ${VAR:?err} syntax for mandatory variables
Joffrey F 7 年之前
父節點
當前提交
1b39817172
共有 3 個文件被更改,包括 94 次插入10 次删除
  1. 55 8
      compose/config/interpolation.py
  2. 0 1
      tests/unit/cli/formatter_test.py
  3. 39 1
      tests/unit/config/interpolation_test.py

+ 55 - 8
compose/config/interpolation.py

@@ -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 = '[^.]+'
 
 

+ 0 - 1
tests/unit/cli/formatter_test.py

@@ -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):

+ 39 - 1
tests/unit/config/interpolation_test.py

@@ -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"