1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
#
# Copyright (c), 2016-2024, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
"""Helper functions for QNames and namespaces."""
import re
from collections.abc import Iterable, MutableMapping
from typing import Optional
from xmlschema.exceptions import XMLSchemaValueError, XMLSchemaTypeError
from xmlschema.names import XML_NAMESPACE
from xmlschema.aliases import NsmapType
def get_namespace(qname: str) -> str:
"""
Returns the namespace URI associated with a QName in extended form or a local name.
If the argument is not conformant to QName format returns the empty string, which
means no namespace.
"""
try:
if qname[0] != '{':
return ''
namespace, _ = qname[1:].split('}')
except (IndexError, ValueError):
return ''
except TypeError:
raise XMLSchemaTypeError("the argument must be a string-like object")
else:
return namespace
def get_namespace_ext(qname: str, namespaces: Optional[NsmapType] = None) -> str:
"""
Returns the namespace URI associated with a QName. If a namespace map is
provided tries to resolve a prefixed QName and then to extract the namespace.
:param qname: an extended QName or a local name or a prefixed QName.
:param namespaces: optional mapping from prefixes to namespace URIs.
"""
if not namespaces:
return get_namespace(qname)
else:
return get_namespace(get_extended_qname(qname, namespaces))
def get_qname(uri: Optional[str], name: str) -> str:
"""
Returns an expanded QName from URI and local part. If any argument has boolean value
`False` or if the name is already an expanded QName, returns the *name* argument.
:param uri: namespace URI
:param name: local or qualified name
:return: string or the name argument
"""
try:
if name[0] in '{./[' or not uri:
return name
except IndexError:
return ''
except TypeError:
raise XMLSchemaTypeError("the 2nd argument must be a string-like object")
else:
return f'{{{uri}}}{name}'
def local_name(qname: str) -> str:
"""
Return the local part of an expanded QName or a prefixed name. If the name
is `None` or empty returns the *name* argument.
:param qname: an expanded QName or a prefixed name or a local name.
"""
try:
if qname[0] == '{':
_namespace, qname = qname.split('}')
elif ':' in qname:
_prefix, qname = qname.split(':')
except IndexError:
return ''
except ValueError:
raise XMLSchemaValueError("the argument 'qname' has an invalid value %r" % qname)
except TypeError:
raise XMLSchemaTypeError("the argument 'qname' must be a string-like object")
else:
return qname
def get_prefixed_qname(qname: str,
namespaces: Optional[MutableMapping[str, str]],
use_empty: bool = True) -> str:
"""
Get the prefixed form of a QName, using a namespace map.
:param qname: an extended QName or a local name or a prefixed QName.
:param namespaces: an optional mapping from prefixes to namespace URIs.
:param use_empty: if `True` use the empty prefix for mapping.
"""
if not namespaces or not qname or qname[0] != '{':
return qname
namespace = get_namespace(qname)
prefixes = [x for x in namespaces if namespaces[x] == namespace]
if not prefixes:
return qname
elif prefixes[0]:
return f"{prefixes[0]}:{qname.split('}', 1)[1]}"
elif len(prefixes) > 1:
return f"{prefixes[1]}:{qname.split('}', 1)[1]}"
elif use_empty:
return qname.split('}', 1)[1]
else:
return qname
def get_extended_qname(qname: str, namespaces: Optional[MutableMapping[str, str]]) -> str:
"""
Get the extended form of a QName, using a namespace map.
Local names are mapped to the default namespace.
:param qname: a prefixed QName or a local name or an extended QName.
:param namespaces: an optional mapping from prefixes to namespace URIs.
"""
if not namespaces:
return qname
try:
if qname[0] == '{':
return qname
except IndexError:
return qname
try:
prefix, name = qname.split(':', 1)
except ValueError:
if not namespaces.get(''):
return qname
else:
return f"{{{namespaces['']}}}{qname}"
else:
try:
uri = namespaces[prefix]
except KeyError:
return qname
else:
return f'{{{uri}}}{name}' if uri else name
def update_namespaces(namespaces: NsmapType,
xmlns: Iterable[tuple[str, str]],
root_declarations: bool = False) -> None:
"""
Update a namespace map without overwriting existing declarations.
If a duplicate prefix is encountered in a xmlns declaration, and
this is mapped to a different namespace, adds the namespace using
a different generated prefix. The empty prefix '' is used only if
it's declared at root level to avoid erroneous mapping of local
names. In other cases it uses the prefix 'default' as substitute.
:param namespaces: the target namespace map.
:param xmlns: an iterable containing couples of namespace declarations.
:param root_declarations: provide `True` if the namespace declarations \
belong to the root element, `False` otherwise (default).
"""
for prefix, uri in xmlns:
if not prefix:
if not uri:
continue
elif '' not in namespaces:
if root_declarations:
namespaces[''] = uri
continue
elif namespaces[''] == uri:
continue
prefix = 'default'
while prefix in namespaces:
if namespaces[prefix] == uri:
break
match = re.search(r'(\d+)$', prefix)
if match:
index = int(match.group()) + 1
prefix = prefix[:match.span()[0]] + str(index)
else:
prefix += '0'
else:
namespaces[prefix] = uri
def get_namespace_map(namespaces: Optional[NsmapType]) -> NsmapType:
"""Returns a new and checked namespace map."""
namespaces = {k: v for k, v in namespaces.items()} if namespaces else {}
if namespaces.get('xml', XML_NAMESPACE) != XML_NAMESPACE:
msg = f"reserved prefix 'xml' can be used only for {XML_NAMESPACE!r} namespace"
raise XMLSchemaValueError(msg)
return namespaces
|