changeset 320:98490375d90c

Allow variable name quoting to be used in .getvar() and .getvar_s() and references
author Franz Glasner <fzglas.hg@dom66.de>
date Thu, 06 May 2021 09:45:51 +0200
parents 5427ca342c1e
children 7a0f3c256cf4
files CHANGES.txt configmix/compat.py configmix/config.py docs/changes.rst tests/data/quoting.yml tests/test.py
diffstat 6 files changed, 148 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Wed May 05 23:12:01 2021 +0200
+++ b/CHANGES.txt	Thu May 06 09:45:51 2021 +0200
@@ -15,6 +15,11 @@
 n/a (n/a)
 ~~~~~~~~~
 
+- **[breaking] [feature]**
+  Allowed quoting of variables.
+
+  This is important for variable names that contain ``.``, ``:`` or ``|``.
+
 - **[breaking] [misc]**
   Moved some important public constants from :py:mod:`configmix` into
   the :py:mod:`configmix.constants` module.
--- a/configmix/compat.py	Wed May 05 23:12:01 2021 +0200
+++ b/configmix/compat.py	Thu May 06 09:45:51 2021 +0200
@@ -14,7 +14,8 @@
            "text_to_native_os_str",
            "native_os_str_to_text",
            "u",
-           "u2fs"]
+           "u2fs",
+           "uchr"]
 
 
 import sys
@@ -67,6 +68,9 @@
             return s
         return s.encode(_FS_ENCODING)
 
+    def uchr(n):
+        return unichr(n)
+
 else:
 
     def text_to_native_os_str(s, encoding=None):
@@ -96,3 +100,6 @@
         """
         assert isinstance(s, str)
         return s
+
+    def uchr(n):
+        return chr(n)
--- a/configmix/config.py	Wed May 05 23:12:01 2021 +0200
+++ b/configmix/config.py	Thu May 06 09:45:51 2021 +0200
@@ -28,7 +28,7 @@
     from urlparse import urlsplit
 
 from .variables import lookup_varns, lookup_filter
-from .compat import u
+from .compat import u, uchr
 from .constants import REF_NAMESPACE
 
 
@@ -70,6 +70,7 @@
     _STARTTOK_REF = _STARTTOK + REF_NAMESPACE + _NS_SEPARATOR
     _ENDTOK_REF = _ENDTOK
     _DOT = u(b'.')
+    _QUOTE = u(b'%')
 
     def getvarl(self, *names, default=_MARKER, namespace=None):
         """Get a variable where the hierarchy is given in `names` as sequence
@@ -104,7 +105,7 @@
         """
         varns, varname = self._split_ns(varname)
         if not varns:
-            varnameparts = varname.split(self._HIER_SEPARATOR)
+            varnameparts = [self.unquote(vp) for vp in varname.split(self._HIER_SEPARATOR)]
         else:
             varnameparts = (varname,)
         return self.getvarl(*varnameparts, namespace=varns)
@@ -365,3 +366,46 @@
             else:
                 value = filterfn(self, value)
         return value
+
+    @classmethod
+    def quote(klass, s):
+        """Quote a key to protect all dangerous chars: ``%``, ``.``, ``:``
+        and ``|``
+
+        """
+        qc = klass._QUOTE
+        s = s.replace(qc, qc + "x25")
+        s = s.replace(klass._DOT, qc + "x2e")
+        s = s.replace(klass._NS_SEPARATOR, qc + "x3a")
+        return s.replace(klass._FILTER_SEPARATOR, qc + "x7c")
+
+    @classmethod
+    def unquote(klass, s):
+        """Unquote the content of `s`: handle all patterns ``%xXX``,
+        ``%uXXXX`` or `%UXXXXXXXX``
+
+        """
+        if klass._QUOTE not in s:
+            return s
+        res = []
+        parts = s.split(klass._QUOTE)
+        res.append(parts[0])
+        for p in parts[1:]:
+            if p.startswith(u(b'x')):
+                if len(p) < 3:
+                    raise ValueError("quote syntax: length too small")
+                res.append(uchr(int(p[1:3], 16)))
+                res.append(p[3:])
+            elif p.startswith(u(b'u')):
+                if len(p) < 5:
+                    raise ValueError("quote syntax: length too small")
+                res.append(uchr(int(p[1:5], 16)))
+                res.append(p[5:])
+            elif p.startswith(u(b'U')):
+                if len(p) < 9:
+                    raise ValueError("quote syntax: length too small")
+                res.append(uchr(int(p[1:9], 16)))
+                res.append(p[9:])
+            else:
+                raise ValueError("unknown quote syntax string: {}".format(s))
+        return ''.join(res)
--- a/docs/changes.rst	Wed May 05 23:12:01 2021 +0200
+++ b/docs/changes.rst	Thu May 06 09:45:51 2021 +0200
@@ -19,12 +19,13 @@
 none
 ----
 
+- Allow quoting of variables names
+
 - Move some important public constants from :py:mod:`configmix` into
   the :py:mod:`configmix.constants` module.
 
-    This is technically a breaking change while the author does not
-    believe that any of the current clients is affected by this
-    change.
+These are technically a breaking changes while the author does not
+believe that any of the current clients is affected by both changes.
 
 
 0.9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/quoting.yml	Thu May 06 09:45:51 2021 +0200
@@ -0,0 +1,19 @@
+# -*- coding: utf-8; mode: yaml; indent-tabs-mode: nil; -*-
+#
+# For quoting and unquoting tests
+#
+%YAML 1.1
+---
+
+':|%.':
+  '.':
+    ':':
+      '%':
+        '|':
+          '/': 'value'
+
+events:
+  'qc-2021.1-5G-summit':
+    name: "5G Summit"
+    xname: '{{%x3a%x7c%x25%x2e.%u002e.%U0000003a.%x25.%x7c./}}'
+    xref: '{{ref:#%u003a%x7c%U00000025%x2e.%x2e.%x3a.%x25.%x7c./}}'
--- a/tests/test.py	Wed May 05 23:12:01 2021 +0200
+++ b/tests/test.py	Thu May 06 09:45:51 2021 +0200
@@ -710,5 +710,71 @@
         self.assertEqual("{{testref.here.params.params.evalex}}", v2)
 
 
+class T07Quoting(unittest.TestCase):
+
+    def setUp(self):
+        self._reset()
+        self._cfg = configmix.load(os.path.join(TESTDATADIR, "quoting.yml"))
+
+    def tearDown(self):
+        self._reset()
+
+    def _reset(self):
+        configmix.clear_assoc()
+        for pat, fmode in configmix.DEFAULT_ASSOC:
+            configmix.set_assoc(pat, fmode)
+
+    def test_getvar(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvar("%x3a%x7c%x25%x2e.%x2e.%x3a.%x25.%x7c./"))
+        self.assertEqual(
+            "value",
+            self._cfg.getvar(
+                "%u003a%u007c%u0025%u002e.%u002e.%u003a.%u0025.%u007c./"))
+        self.assertEqual(
+            "value",
+            self._cfg.getvar(
+                "%U0000003a%U0000007c%U00000025%U0000002e.%U0000002e.%U0000003a.%U00000025.%U0000007c./"))
+
+    def test_getvar_s(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvar_s("%x3a%x7c%x25%x2e.%x2e.%x3a.%x25.%x7c./"))
+        self.assertEqual(
+            "value",
+            self._cfg.getvar_s(
+                "%u003a%u007c%u0025%u002e.%u002e.%u003a.%u0025.%u007c./"))
+        self.assertEqual(
+            "value",
+            self._cfg.getvar_s(
+                "%U0000003a%U0000007c%U00000025%U0000002e.%U0000002e.%U0000003a.%U00000025.%U0000007c./"))
+
+    def test_getvarl(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvarl(":|%.", ".", ":", "%", "|", "/"))
+
+    def test_getvarl_s(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvarl_s(":|%.", ".", ":", "%", "|", "/"))
+
+    def test_interpolation1(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvarl_s("events", "qc-2021.1-5G-summit", "xname"))
+
+    def test_interpolation2(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvar_s("events.qc-2021%x2e1-5G-summit.xname"))
+
+    def test_reference(self):
+        self.assertEqual(
+            "value",
+            self._cfg.getvar_s("events.qc-2021%x2e1-5G-summit.xref"))
+
+
 if __name__ == "__main__":
     unittest.main()