From 6942be8ac90cce426151372c7d36bcae369e2a7e Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Wed, 23 Oct 2019 09:47:49 +0200 Subject: [PATCH] Optimize qname_to_prefixed() and get_namespace() helpers - use_empty optional argument added to qname_to_prefixed() --- xmlschema/namespaces.py | 3 +++ xmlschema/qnames.py | 32 +++++++++++++++++------------- xmlschema/tests/test_helpers.py | 18 ++++++++++++++++- xmlschema/validators/exceptions.py | 5 +++-- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/xmlschema/namespaces.py b/xmlschema/namespaces.py index 44cd453..67f8e4b 100644 --- a/xmlschema/namespaces.py +++ b/xmlschema/namespaces.py @@ -70,6 +70,9 @@ NAMESPACE_PATTERN = re.compile(r'{([^}]*)}') def get_namespace(name): + if not name or name[0] != '{': + return '' + try: return NAMESPACE_PATTERN.match(name).group(1) except (AttributeError, TypeError): diff --git a/xmlschema/qnames.py b/xmlschema/qnames.py index eb4f27d..0f80411 100644 --- a/xmlschema/qnames.py +++ b/xmlschema/qnames.py @@ -224,34 +224,38 @@ def local_name(qname): return qname -def qname_to_prefixed(qname, namespaces): +def qname_to_prefixed(qname, namespaces, use_empty=True): """ - Transforms a fully qualified name into a prefixed name using a namespace map. - Returns the *qname* argument if it's not a fully qualified name or if it has - boolean value `False`. + Maps a QName in extended format to a QName in prefixed format. + Do not change local names and QNames in prefixed format. - :param qname: an extended QName or a local name. + :param qname: a QName or a local name. :param namespaces: a map from prefixes to namespace URIs. + :param use_empty: if `True` use the empty prefix for mapping. :return: a QName in prefixed format or a local name. """ - if not qname: + if not qname or qname[0] != '{': return qname namespace = get_namespace(qname) - for prefix, uri in sorted(filter(lambda x: x[1] == namespace, namespaces.items()), reverse=True): - if not uri: - return '%s:%s' % (prefix, qname) if prefix else qname - elif prefix: - return qname.replace('{%s}' % uri, '%s:' % prefix) - else: - return qname.replace('{%s}' % uri, '') + prefixes = [x for x in namespaces if namespaces[x] == namespace] + + if not prefixes: + return qname + elif prefixes[0]: + return '%s:%s' % (prefixes[0], qname.split('}', 1)[1]) + elif len(prefixes) > 1: + return '%s:%s' % (prefixes[1], qname.split('}', 1)[1]) + elif use_empty: + return qname.split('}', 1)[1] else: return qname def qname_to_extended(qname, namespaces): """ - Converts a QName in prefixed format or a local name to the extended QName format. + Maps a QName in prefixed format or a local name to the extended QName format. + Local names are mapped if *namespaces* has a not empty default namespace. :param qname: a QName in prefixed format or a local name. :param namespaces: a map from prefixes to namespace URIs. diff --git a/xmlschema/tests/test_helpers.py b/xmlschema/tests/test_helpers.py index be195ef..5a9c894 100644 --- a/xmlschema/tests/test_helpers.py +++ b/xmlschema/tests/test_helpers.py @@ -40,6 +40,9 @@ class TestHelpers(unittest.TestCase): self.assertEqual(get_namespace(XSD_SIMPLE_TYPE), XSD_NAMESPACE) self.assertEqual(get_namespace(''), '') self.assertEqual(get_namespace(None), '') + self.assertEqual(get_namespace('{}name'), '') + self.assertEqual(get_namespace('{ }name'), ' ') + self.assertEqual(get_namespace('{ ns }name'), ' ns ') def test_get_qname_functions(self): self.assertEqual(get_qname(XSD_NAMESPACE, 'element'), XSD_ELEMENT) @@ -81,8 +84,21 @@ class TestHelpers(unittest.TestCase): self.assertEqual(qname_to_prefixed('', {}), '') self.assertEqual(qname_to_prefixed('type', {'': XSI_NAMESPACE}), 'type') - self.assertEqual(qname_to_prefixed('type', {'ns': ''}), 'ns:type') self.assertEqual(qname_to_prefixed('type', {'': ''}), 'type') + self.assertEqual(qname_to_prefixed('{}type', {'': ''}), 'type') + self.assertEqual(qname_to_prefixed('{}type', {'': ''}, use_empty=False), '{}type') + + # Attention! in XML the empty namespace (that means no namespace) can be + # associated only with empty prefix, so these cases should never happen. + self.assertEqual(qname_to_prefixed('{}type', {'p': ''}), 'p:type') + self.assertEqual(qname_to_prefixed('type', {'p': ''}), 'type') + + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns'}, use_empty=True), 'type') + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns'}, use_empty=False), '{ns}type') + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns', 'p': 'ns'}, use_empty=True), 'p:type') + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns', 'p': 'ns'}, use_empty=False), 'p:type') + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns', 'p': 'ns0'}, use_empty=True), 'type') + self.assertEqual(qname_to_prefixed('{ns}type', {'': 'ns', 'p': 'ns0'}, use_empty=False), '{ns}type') def test_get_xsd_annotation(self): elem = etree_element(XSD_SCHEMA) diff --git a/xmlschema/validators/exceptions.py b/xmlschema/validators/exceptions.py index 3ed988f..4ff969a 100644 --- a/xmlschema/validators/exceptions.py +++ b/xmlschema/validators/exceptions.py @@ -15,6 +15,7 @@ from __future__ import unicode_literals from ..compat import PY3, string_base_type from ..exceptions import XMLSchemaException, XMLSchemaWarning, XMLSchemaValueError +from ..namespaces import get_namespace from ..qnames import qname_to_prefixed from ..etree import etree_tostring, etree_getpath from ..helpers import is_etree_element @@ -317,11 +318,11 @@ class XMLSchemaChildrenValidationError(XMLSchemaValidationError): self.occurs = occurs self.expected = expected - tag = qname_to_prefixed(elem.tag, validator.namespaces) + tag = qname_to_prefixed(elem.tag, validator.namespaces, use_empty=False) if index >= len(elem): reason = "The content of element %r is not complete." % tag else: - child_tag = qname_to_prefixed(elem[index].tag, validator.namespaces) + child_tag = qname_to_prefixed(elem[index].tag, validator.namespaces, use_empty=False) reason = "Unexpected child with tag %r at position %d." % (child_tag, index + 1) if occurs and particle.is_missing(occurs):