# HG changeset patch # User Franz Glasner # Date 1620287151 -7200 # Node ID 98490375d90cbe7e64b9387ab95ee0ae44970344 # Parent 5427ca342c1ed5206f38e3a0ada68cde7644b419 Allow variable name quoting to be used in .getvar() and .getvar_s() and references diff -r 5427ca342c1e -r 98490375d90c CHANGES.txt --- 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. diff -r 5427ca342c1e -r 98490375d90c configmix/compat.py --- 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) diff -r 5427ca342c1e -r 98490375d90c configmix/config.py --- 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) diff -r 5427ca342c1e -r 98490375d90c docs/changes.rst --- 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 diff -r 5427ca342c1e -r 98490375d90c tests/data/quoting.yml --- /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./}}' diff -r 5427ca342c1e -r 98490375d90c tests/test.py --- 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()