Mercurial > hgrepos > Python > libs > ConfigMix
changeset 542:f71d34dda19f
Add an optional C-implementation for configmix.config.unquote and configmix.config.pathstr2path.
This is currently for Python 3.5+.
It is tested with Python 3.7 and Python3.8 (FreeBSD 12.2 amd64, LLVM 10.0.1).
A build for the stable API ("abi3") fails because PyUnicode_New() is currently
not in the stable API.
Also includes are extended tests for unquote() and pathstr2path().
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Fri, 31 Dec 2021 21:24:16 +0100 |
| parents | 25b61f0a1958 |
| children | 491413368c7c |
| files | .hgkwarchive MANIFEST.in configmix/_speedups.c configmix/config.py setup.py tests/_perf_config.py tests/test.py |
| diffstat | 7 files changed, 620 insertions(+), 29 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgkwarchive Wed Dec 29 13:33:11 2021 +0100 +++ b/.hgkwarchive Fri Dec 31 21:24:16 2021 +0100 @@ -1,4 +1,5 @@ [patterns] configmix/**.py = RCS, reST +configmix/**.c = RCS, reST path:README.txt = reST path:docs/conf.py =
--- a/MANIFEST.in Wed Dec 29 13:33:11 2021 +0100 +++ b/MANIFEST.in Fri Dec 31 21:24:16 2021 +0100 @@ -1,4 +1,5 @@ include .hg* *.txt requirement* +include configmix/*.c graft docs graft tests prune docs/_build
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/configmix/_speedups.c Fri Dec 31 21:24:16 2021 +0100 @@ -0,0 +1,399 @@ +/* -*- coding: utf-8 -*- */ +/* + * Speedups for configmix. + * + * :Copyright: (c) 2021, Franz Glasner. All rights reserved. + * :License: BSD-3-Clause. See LICENSE.txt for details. + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + + +const char _id[] = "@(#)configmix._speedups $Header$"; +static const char release[] = "|VCSRevision|"; +static const char date[] = "|VCSJustDate|"; + + +/* + * Module state holds pre-created some objects + */ +struct speedups_state { + PyObject *DOT; + PyObject *QUOTE; + PyObject *EMPTY_STR; +}; + + +static +int +_hex2ucs4(PyObject *s, Py_ssize_t end, Py_UCS4 *result) +{ + Py_ssize_t i; + Py_UCS4 c; + Py_UCS4 r = 0; + + for (i=1; i < end; i++) { + r *= 16; + c = PyUnicode_ReadChar(s, i); + if ((c >= 48) && (c <= 57)) { /* 0 - 9 */ + r += (c - 48); + } + else { + if ((c >= 97) && (c <= 102)) { /* a - f */ + r += (c - 87); + } + else { + if ((c >= 65) && (c <= 70)) { /* A - F */ + r += (c - 55); + } + else { + PyErr_SetString(PyExc_ValueError, "invalid base-16 literal"); + return -1; + } + } + } + } + *result = r; + return 0; /* success */ +} + + +static +PyObject * +_hex2string(PyObject *s, Py_ssize_t end) +{ + Py_UCS4 c; + PyObject *u = NULL; + + if (_hex2ucs4(s, end, &c) != 0) + return NULL; + u = PyUnicode_New(1, c); /* ARGH: not in the stable API */ + if (u == NULL) + return NULL; + if (PyUnicode_WriteChar(u, 0, c) != 0) { + Py_DECREF(u); + return NULL; + } + return u; +} + + +static +PyObject * +_fast_unquote(PyObject *self, PyObject *s, struct speedups_state *sstate) +{ + Py_ssize_t find; + Py_ssize_t s_len; + Py_ssize_t parts_len; + PyObject *res; + PyObject *res_parts = NULL; + PyObject *parts = NULL; + PyObject *o; + PyObject *pb; + Py_ssize_t pb_len; + Py_ssize_t i; + Py_UCS4 c; + + if (!PyUnicode_Check(s)) { + PyErr_SetString(PyExc_TypeError, "a (unicode) string type is expected"); + return NULL; + } + s_len = PyUnicode_GetLength(s); + if (s_len < 0) { + return NULL; + } + if (s_len == 0) { + Py_INCREF(s); + return s; + } + find = PyUnicode_FindChar(s, '%', 0, s_len, 1); + if (find == -2) { + return NULL; + } + if (find == -1) { + Py_INCREF(s); + return s; + } + + if (sstate == NULL) { + sstate = PyModule_GetState(self); + if (sstate == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no module state available"); + return NULL; + } + } + parts = PyUnicode_Split(s, sstate->QUOTE, -1); + if (parts == NULL) { + goto error; + } + parts_len = PyList_Size(parts); + if (parts_len < 0) { + goto error; + } + res_parts = PyTuple_New((parts_len-1)*2 + 1); + if (res_parts == NULL) { + goto error; + } + + o = PyList_GetItem(parts, 0); /* borrowed */ + if (o == NULL) { + goto error; + } + /* + * The first item may be also the empty string if `s' starts with + * a quoted character. + */ + Py_INCREF(o); /* because PyTuple_SetItem steals -- and o is borrowed */ + PyTuple_SetItem(res_parts, 0, o); + + for (i=1; i<parts_len; i++) { + pb = PyList_GetItem(parts, i); /* borrowed */ + pb_len = PyUnicode_GetLength(pb); + if (pb_len < 1) { + PyErr_SetString(PyExc_ValueError, "unknown quote syntax string"); + goto error; + } + c = PyUnicode_ReadChar(pb, 0); + switch (c) { + case 0x55: /* U */ + if (pb_len < 9) { + PyErr_SetString(PyExc_ValueError, "quote syntax: length too small"); + goto error; + } + o = _hex2string(pb, 9); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, (i-1)*2 + 1, o); /* steals */ + o = PyUnicode_Substring(pb, 9, pb_len); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, i*2, o); /* steals */ + break; + case 0x75: /* u */ + if (pb_len < 5) { + PyErr_SetString(PyExc_ValueError, "quote syntax: length too small"); + goto error; + } + o = _hex2string(pb, 5); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, (i-1)*2 + 1, o); /* steals */ + o = PyUnicode_Substring(pb, 5, pb_len); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, i*2, o); /* steals */ + break; + case 0x78: /* x */ + if (pb_len < 3) { + PyErr_SetString(PyExc_ValueError, "quote syntax: length too small"); + goto error; + } + o = _hex2string(pb, 3); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, (i-1)*2 + 1, o); /* steals */ + o = PyUnicode_Substring(pb, 3, pb_len); + if (o == NULL) { + goto error; + } + PyTuple_SetItem(res_parts, i*2, o); /* steals */ + break; + + default: + PyErr_SetString(PyExc_ValueError, "unknown quote syntax string"); + goto error; + } + } + + res = PyUnicode_Join(sstate->EMPTY_STR, res_parts); + if (res == NULL) { + goto error; + } + Py_DECREF(parts); + Py_DECREF(res_parts); + return res; + +error: + Py_XDECREF(res_parts); + Py_XDECREF(parts); + return NULL; +} + + +static +PyObject * +fast_unquote(PyObject *self, PyObject *s) +{ + return _fast_unquote(self, s, NULL); +} + + +static +PyObject * +fast_pathstr2path(PyObject *self, PyObject *varname) +{ + Py_ssize_t varname_len; + PyObject *parts = NULL; + Py_ssize_t parts_len; + PyObject *res = NULL; + Py_ssize_t i; + PyObject *o; + PyObject *u; + struct speedups_state *sstate; + + if (!PyUnicode_Check(varname)) { + PyErr_SetString(PyExc_TypeError, "a (unicode) string type is expected"); + return NULL; + } + varname_len = PyUnicode_GetLength(varname); + if (varname_len < 0) { + return NULL; + } + if (varname_len == 0) { + return PyTuple_New(0); + } + + sstate = PyModule_GetState(self); + if (sstate == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no module state available"); + return NULL; + } + parts = PyUnicode_Split(varname, sstate->DOT, -1); + if (parts == NULL) { + goto error; + } + parts_len = PyList_Size(parts); + if (parts_len < 0) { + goto error; + } + res = PyTuple_New(parts_len); + if (res == NULL) { + goto error; + } + for (i=0; i < parts_len; i++) { + o = PyList_GetItem(parts, i); /* borrowed */ + u = _fast_unquote(self, o, sstate); + if (u == NULL) { + goto error; + } + PyTuple_SetItem(res, i, u); /* steals */ + } + + Py_DECREF(parts); + return res; + +error: + Py_XDECREF(parts); + Py_XDECREF(res); + return NULL; +} + + +static struct PyMethodDef speedups_methods[] = { + {"fast_unquote", fast_unquote, METH_O, PyDoc_STR("C-implementation of configmix.unquote")}, + {"fast_pathstr2path", fast_pathstr2path, METH_O, PyDoc_STR("C-implementation of configmix.pathstr2path")}, + {NULL, NULL, 0, NULL} +}; + + +static +int +speedups_exec(PyObject *module) +{ + struct speedups_state *sstate = PyModule_GetState(module); + + if (sstate == NULL) { + PyErr_SetString(PyExc_ImportError, "no module state available yet"); + return -1; + } + + PyModule_AddStringConstant(module, "__release__", release); + PyModule_AddStringConstant(module, "__date__", date); + PyModule_AddStringConstant(module, "__author__", "Franz Glasner"); + + sstate->DOT = PyUnicode_FromStringAndSize(".", 1); + if (sstate->DOT == NULL) { + return -1; + } + PyUnicode_InternInPlace(&(sstate->DOT)); + + sstate->QUOTE = PyUnicode_FromStringAndSize("%", 1); + if (sstate->QUOTE == NULL) { + return -1; + } + PyUnicode_InternInPlace(&(sstate->QUOTE)); + + sstate->EMPTY_STR = PyUnicode_FromStringAndSize("", 0); + if (sstate->EMPTY_STR == NULL) { + return -1; + } + PyUnicode_InternInPlace(&(sstate->EMPTY_STR)); + + return 0; +} + + +static +int +speeeupds_traverse(PyObject *module, visitproc visit, void *arg) +{ + struct speedups_state *sstate = PyModule_GetState(module); + + if (sstate != NULL) { + Py_VISIT(sstate->DOT); + Py_VISIT(sstate->QUOTE); + Py_VISIT(sstate->EMPTY_STR); + } + return 0; +} + + +static +int +speedups_clear(PyObject *module) +{ + struct speedups_state *sstate = PyModule_GetState(module); + + if (sstate != NULL) { + Py_CLEAR(sstate->DOT); + Py_CLEAR(sstate->QUOTE); + Py_CLEAR(sstate->EMPTY_STR); + } + return 0; +} + + +static struct PyModuleDef_Slot speedups_slots[] = { + {Py_mod_exec, speedups_exec}, + {0, NULL} +}; + + +static struct PyModuleDef speedups_def = { + PyModuleDef_HEAD_INIT, /* m_base */ + "_speedups", /* m_name (relative) */ + PyDoc_STR("Speedups for configmix"), /* m_doc */ + sizeof(struct speedups_state), /* m_size */ + speedups_methods, /* m_methods */ + speedups_slots, /* m_slots */ + speeeupds_traverse, /* m_traverse */ + speedups_clear, /* m_clear */ + NULL /* m_free */ +}; + + +PyMODINIT_FUNC +PyInit__speedups(void) +{ + /* + * Use multi-phase extension module initialization (PEP 489). + * This is Python 3.5+. + */ + return PyModuleDef_Init(&speedups_def); +}
--- a/configmix/config.py Wed Dec 29 13:33:11 2021 +0100 +++ b/configmix/config.py Fri Dec 31 21:24:16 2021 +0100 @@ -30,6 +30,11 @@ from .variables import lookup_varns, lookup_filter from .compat import u, uchr, n, str_and_u, PY2 from .constants import REF_NAMESPACE, NONE_FILTER, EMPTY_FILTER +try: + from ._speedups import fast_unquote, fast_pathstr2path +except ImportError: + fast_unquote = None + fast_pathstr2path = None _MARKER = object() @@ -280,7 +285,7 @@ return s -def unquote(s): +def py_unquote(s): """Unquote the content of `s`: handle all patterns ``%xNN``, ``%uNNNN`` or ``%UNNNNNNNN``. @@ -317,7 +322,13 @@ return _EMPTY_STR.join(res) -def pathstr2path(varname): +if fast_unquote: + unquote = fast_unquote +else: + unquote = py_unquote + + +def py_pathstr2path(varname): """Parse a dot-separated path string `varname` into a tuple of unquoted path items @@ -345,6 +356,12 @@ return tuple() +if fast_pathstr2path: + pathstr2path = fast_pathstr2path +else: + pathstr2path = py_pathstr2path + + def _split_ns(varname): """Split the variable name string `varname` into the namespace and the namespace-specific name @@ -355,7 +372,6 @@ :rtype: tuple(str or None, str) """ - ns, sep, rest = varname.partition(_NS_SEPARATOR) if sep: return (unquote(ns), rest) @@ -366,7 +382,7 @@ def _split_filters(varname): """Split off the filter part from the `varname` string - :type varname: str + :type varname: str :return: The tuple of the variable name without the filters and a list of filters :rtype: tuple(str, list)
--- a/setup.py Wed Dec 29 13:33:11 2021 +0100 +++ b/setup.py Fri Dec 31 21:24:16 2021 +0100 @@ -3,6 +3,7 @@ import re import os +import platform import sys try: from setuptools import setup @@ -43,6 +44,58 @@ all_requirements.extend(yaml_requirements) all_requirements.extend(toml_requirements) + +cmdclass = {} +ext_modules = [] + +# +# Handle the optinal C-extension for Python3.7+ and CPython only. +# PyPy does not need this. +# + +if (platform.python_implementation() == "CPython" + and (sys.version_info[0] > 3 + or (sys.version_info[0] == 3 and sys.version_info[1] >= 7))): + + py_limited_api = False + + if py_limited_api: + define_macros = [("Py_LIMITED_API", "0x03070000")] + else: + define_macros = [] + + try: + from setuptools import Extension + except ImportError: + from distutils.core import Extension + + ext_modules = [ + Extension( + name="configmix._speedups", + sources=["configmix/_speedups.c"], + define_macros=define_macros, + py_limited_api=py_limited_api, + optional=True + ), + ] + + if py_limited_api: + # + # Build a wheel that is properly named using the stable API + # + try: + import wheel.bdist_wheel + except ImportError: + cmdclass = {} + else: + class BDistWheel(wheel.bdist_wheel.bdist_wheel): + def finalize_options(self): + # Synchronize this with Py_LIMITED_API + self.py_limited_api = 'cp37' + super().finalize_options() + + cmdclass = {'bdist_wheel': BDistWheel} + setup( name="ConfigMix", version=version, @@ -69,8 +122,10 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], python_requires=">=2.6", + ext_modules=ext_modules, + cmdclass=cmdclass, extras_require={ - "aws" : aws_requirements, + "aws" : aws_requirements, "toml": toml_requirements, "yaml": yaml_requirements, "all" : all_requirements,
--- a/tests/_perf_config.py Wed Dec 29 13:33:11 2021 +0100 +++ b/tests/_perf_config.py Fri Dec 31 21:24:16 2021 +0100 @@ -15,11 +15,20 @@ opts = sys.argv[1:] all = not opts or "all" in opts +try: + from configmix.config import fast_unquote, fast_pathstr2path +except ImportError: + fast_unquote = fast_pathstr2path = None + setup = """ import os import configmix -import configmix.config +from configmix.config import _HIER_SEPARATOR, quote, py_pathstr2path, py_unquote +try: + from configmix.config import fast_unquote, fast_pathstr2path +except ImportError: + fast_unquote = fast_pathstr2path = None TESTDATADIR = os.path.join( os.path.abspath(os.path.dirname(configmix.__file__)), @@ -27,10 +36,6 @@ "tests", "data") -unquote = configmix.unquote -quote = configmix.quote -pathstr2path = configmix.pathstr2path - cfg = configmix.load(os.path.join(TESTDATADIR, "conf_perf.py")) se = u"" @@ -43,12 +48,20 @@ num_quote = 1 * num if all or "quote" in opts or "unquote" in opts or "path" in opts: - print("unquote/nothing/split: %.4f" % timeit.timeit('a = tuple([unquote(vp) for vp in u"abc.def.hij".split(configmix.config._HIER_SEPARATOR)])', setup=setup, number=num_quote)) - print("unquote/yes/split: %.4f" % timeit.timeit('a = [unquote(vp) for vp in u"ab%x20.def.h%x2ej".split(configmix.config._HIER_SEPARATOR)]', setup=setup, number=num_quote)) - print("unquote/nothing/no-split: %.4f" % timeit.timeit('a = [unquote(vp) for vp in (u"abc," u"def", u"hij")]', setup=setup, number=num_quote)) - print("unquote/yes/no-split: %.4f" % timeit.timeit('a = [unquote(vp) for vp in (u"ab%x20", u"def", u"h%x2ej")]', setup=setup, number=num_quote)) - print("pathstr2path/non-empty: %.4f" % timeit.timeit('a = pathstr2path(s1)', setup=setup, number=num_quote)) - print("pathstr2path/empty: %.4f" % timeit.timeit('a = pathstr2path(se)', setup=setup, number=num_quote)) + print("unquote/nothing/split: %.4f" % timeit.timeit('a = [py_unquote(vp) for vp in u"abc.def.hij".split(_HIER_SEPARATOR)]', setup=setup, number=num_quote)) + print("unquote/yes/split: %.4f" % timeit.timeit('a = [py_unquote(vp) for vp in u"ab%x20.def.h%x2ej".split(_HIER_SEPARATOR)]', setup=setup, number=num_quote)) + print("unquote/nothing/no-split: %.4f" % timeit.timeit('a = [py_unquote(vp) for vp in (u"abc," u"def", u"hij")]', setup=setup, number=num_quote)) + print("unquote/yes/no-split: %.4f" % timeit.timeit('a = [py_unquote(vp) for vp in (u"ab%x20", u"def", u"h%x2ej")]', setup=setup, number=num_quote)) + if fast_unquote: + print("fast-unquote/nothing/split: %.4f" % timeit.timeit('a = [fast_unquote(vp) for vp in u"abc.def.hij".split(_HIER_SEPARATOR)]', setup=setup, number=num_quote)) + print("fast-unquote/yes/split: %.4f" % timeit.timeit('a = [fast_unquote(vp) for vp in u"ab%x20.def.h%x2ej".split(_HIER_SEPARATOR)]', setup=setup, number=num_quote)) + print("fast-unquote/nothing/no-split: %.4f" % timeit.timeit('a = [fast_unquote(vp) for vp in (u"abc," u"def", u"hij")]', setup=setup, number=num_quote)) + print("fast-unquote/yes/no-split: %.4f" % timeit.timeit('a = [fast_unquote(vp) for vp in (u"ab%x20", u"def", u"h%x2ej")]', setup=setup, number=num_quote)) + print("pathstr2path/non-empty: %.4f" % timeit.timeit('a = py_pathstr2path(s1)', setup=setup, number=num_quote)) + print("pathstr2path/empty: %.4f" % timeit.timeit('a = py_pathstr2path(se)', setup=setup, number=num_quote)) + if fast_pathstr2path: + print("fast-pathstr2path/non-empty: %.4f" % timeit.timeit('a = fast_pathstr2path(s1)', setup=setup, number=num_quote)) + print("fast-pathstr2path/empty: %.4f" % timeit.timeit('a = fast_pathstr2path(se)', setup=setup, number=num_quote)) print("quote/nothing: %.4f" % timeit.timeit('a = [quote(vp) for vp in (u"abc", u"def", u"hij")]', setup=setup, number=num_quote)) print("quote/yes: %.4f" % timeit.timeit('a = [quote(vp) for vp in (u"ab:c", u"def", u"h.ij")]', setup=setup, number=num_quote)) print("="*50)
--- a/tests/test.py Wed Dec 29 13:33:11 2021 +0100 +++ b/tests/test.py Fri Dec 31 21:24:16 2021 +0100 @@ -17,6 +17,7 @@ import configmix.json import configmix.py import configmix.toml +import configmix.config from configmix.compat import u, PY2 @@ -1215,19 +1216,6 @@ "value", self._cfg.getvar_s("events.qc-2021%x2e1-5G-summit.xref.%x23")) - def test_quoting_and_unquoting_are_inverse(self): - for c in """%.:#|"'{}[]""": - qc = configmix.quote(c) - self.assertTrue(qc.startswith("%x") and len(qc) == 4) - self.assertEqual(c, configmix.unquote(qc)) - - def test_quoting_and_unquoting_are_identical(self): - # other characters - for c in """abc09/""": - qc = configmix.quote(c) - self.assertEqual(c, qc) - self.assertEqual(c, configmix.unquote(qc)) - def test_namespace_quoting(self): v1 = self._cfg.getvar("PY:version") v2 = self._cfg.getvar("P%x59:version") @@ -1784,5 +1772,123 @@ self.assertEqual(u"in the root namespace", jcfg[2]) +class _TParserMixin: + def test_quote_and_unquote_empty(self): + e = configmix.quote(u"") + self.assertEqual(u"", e) + self.assertEqual(u"", self.unquote(e)) + + def test_quoting_and_unquoting_are_inverse(self): + for c in u"""%.:#|"'{}[]""": + qc = configmix.quote(c) + self.assertTrue(qc.startswith(u"%x") and len(qc) == 4) + self.assertEqual(c, self.unquote(qc)) + + def test_quoting_and_unquoting_are_identical(self): + # other characters + for c in configmix.config._QUOTE_SAFE: + qc = configmix.quote(c) + self.assertEqual(c, qc) + self.assertEqual(c, self.unquote(qc)) + + def test_unquote_unimax(self): + self.assertEqual(u"\U00019001", self.unquote(u"%U00019001")) + self.assertEqual(u"X\U00019AF1Z", self.unquote(u"X%U00019aF1Z")) + + def test_unquote_base_plane(self): + self.assertEqual(u"\uFFFF", self.unquote(u"%uffff")) + self.assertEqual(u"X\uFFFFZ", self.unquote(u"X%uffffZ")) + + def test_unquote_latin(self): + self.assertEqual(u"\xFF", self.unquote(u"%xff")) + self.assertEqual(u"X\xFFZ", self.unquote(u"X%xffZ")) + + def test_unquote_zero(self): + self.assertEqual(u"\x00", self.unquote(u"%x00")) + self.assertEqual(u"X\x00Z", self.unquote(u"X%x00Z")) + + def test_unquote_adjacent_x(self): + self.assertEqual(u"\x00\x01\xA0\xB0\xFF", + self.unquote(u"%x00%x01%xA0%xB0%xFF")) + + def test_unquote_adjacent_u(self): + self.assertEqual(u"\u0000\u0001\u00A0\uABCD\uFEFE", + self.unquote(u"%u0000%u0001%u00A0%uabCD%ufeFE")) + + def test_unquote_adjacent_U(self): + self.assertEqual( + u"\U00000000\U00000001\U000000A0\U0001ABCD\U0001FEFE", + self.unquote(u"%U00000000%U00000001%U000000A0%U0001abCD%U0001feFE")) + + def test_invalid_hex_digits(self): + self.assertRaises( + ValueError, + self.unquote, + u"%xgG") + self.assertRaises( + ValueError, + self.unquote, + u"%ugGGG") + self.assertRaises( + ValueError, + self.unquote, + u"%UgGGGgGGG") + + def test_invalid_too_short(self): + self.assertRaises( + ValueError, + self.unquote, + u"%x0") + self.assertRaises( + ValueError, + self.unquote, + u"%u000") + self.assertRaises( + ValueError, + self.unquote, + u"%U0000000") + + def test_invalid_too_short_no_sigil(self): + self.assertRaises( + ValueError, + self.unquote, + u"%") + + def test_empty_pathstr(self): + # an empty path string returns an empty path tuple + self.assertEqual(tuple(), self.pathstr2path(u"")) + + def test_split(self): + p = self.pathstr2path(u"abc.def.hik.jkl") + self.assertEqual((u"abc", u"def", u"hik", u"jkl"), p) + + def test_split_all_empty_parts(self): + p = self.pathstr2path(u"....") + self.assertEqual((u"", u"", u"", u"", u""), p) + + def test_split_all_empty_tail(self): + p = self.pathstr2path(u"1.2.") + self.assertEqual((u"1", u"2", u""), p) + + def test_split_unquote(self): + p = self.pathstr2path(u"a%x2Eb.c%u002Ed.e%U0000002Ef") + self.assertEqual((u"a.b", u"c.d", u"e.f"), p) + + +class T09Parser(_TParserMixin, unittest.TestCase): + + def setUp(self): + self.unquote = configmix.config.py_unquote + self.pathstr2path = configmix.config.py_pathstr2path + + +if configmix.config.fast_unquote is not None: + class T10FastParser(_TParserMixin, unittest.TestCase): + + def setUp(self): + self.unquote = configmix.config.fast_unquote + self.pathstr2path = configmix.config.fast_pathstr2path + + if __name__ == "__main__": unittest.main()
