1# 2# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies). 3# All rights reserved. 4# This file is distributed under the terms of the MIT License. 5# See the file 'LICENSE' in the root directory of the present 6# distribution, or http://opensource.org/licenses/MIT. 7# 8# @author Davide Brunato <brunato@sissa.it> 9# 10""" 11This module contains classes for managing maps related to namespaces. 12""" 13import re 14from typing import Any, Container, Dict, Iterator, List, Optional, MutableMapping, \ 15 Mapping, TypeVar 16 17from .exceptions import XMLSchemaValueError, XMLSchemaTypeError 18from .helpers import local_name 19from .aliases import NamespacesType 20 21 22### 23# Base classes for managing namespaces 24 25class NamespaceResourcesMap(MutableMapping[str, Any]): 26 """ 27 Dictionary for storing information about namespace resources. The values are 28 lists of objects. Setting an existing value appends the object to the value. 29 Setting a value with a list sets/replaces the value. 30 """ 31 __slots__ = ('_store',) 32 33 def __init__(self, *args: Any, **kwargs: Any): 34 self._store: Dict[str, List[Any]] = {} 35 self.update(*args, **kwargs) 36 37 def __getitem__(self, uri: str) -> Any: 38 return self._store[uri] 39 40 def __setitem__(self, uri: str, value: Any) -> None: 41 if isinstance(value, list): 42 self._store[uri] = value[:] 43 else: 44 try: 45 self._store[uri].append(value) 46 except KeyError: 47 self._store[uri] = [value] 48 49 def __delitem__(self, uri: str) -> None: 50 del self._store[uri] 51 52 def __iter__(self) -> Iterator[str]: 53 return iter(self._store) 54 55 def __len__(self) -> int: 56 return len(self._store) 57 58 def __repr__(self) -> str: 59 return repr(self._store) 60 61 def clear(self) -> None: 62 self._store.clear() 63 64 65class NamespaceMapper(MutableMapping[str, str]): 66 """ 67 A class to map/unmap namespace prefixes to URIs. The mapped namespaces are 68 automatically registered when set. Namespaces can be updated overwriting 69 the existing registration or inserted using an alternative prefix. 70 71 :param namespaces: initial data with namespace prefixes and URIs. \ 72 The provided dictionary is bound with the instance, otherwise a new \ 73 empty dictionary is used. 74 :param strip_namespaces: if set to `True` uses name mapping methods that strip \ 75 namespace information. 76 """ 77 __slots__ = '_namespaces', 'strip_namespaces', '__dict__' 78 _namespaces: NamespacesType 79 80 def __init__(self, namespaces: Optional[NamespacesType] = None, 81 strip_namespaces: bool = False): 82 if namespaces is None: 83 self._namespaces = {} 84 else: 85 self._namespaces = namespaces 86 self.strip_namespaces = strip_namespaces 87 88 def __setattr__(self, name: str, value: str) -> None: 89 if name == 'strip_namespaces': 90 if value: 91 self.map_qname = self.unmap_qname = self._local_name # type: ignore[assignment] 92 elif getattr(self, 'strip_namespaces', False): 93 self.map_qname = self._map_qname # type: ignore[assignment] 94 self.unmap_qname = self._unmap_qname # type: ignore[assignment] 95 super(NamespaceMapper, self).__setattr__(name, value) 96 97 def __getitem__(self, prefix: str) -> str: 98 return self._namespaces[prefix] 99 100 def __setitem__(self, prefix: str, uri: str) -> None: 101 self._namespaces[prefix] = uri 102 103 def __delitem__(self, prefix: str) -> None: 104 del self._namespaces[prefix] 105 106 def __iter__(self) -> Iterator[str]: 107 return iter(self._namespaces) 108 109 def __len__(self) -> int: 110 return len(self._namespaces) 111 112 @property 113 def namespaces(self) -> NamespacesType: 114 return self._namespaces 115 116 @property 117 def default_namespace(self) -> Optional[str]: 118 return self._namespaces.get('') 119 120 def clear(self) -> None: 121 self._namespaces.clear() 122 123 def insert_item(self, prefix: str, uri: str) -> None: 124 """ 125 A method for setting an item that checks the prefix before inserting. 126 In case of collision the prefix is changed adding a numerical suffix. 127 """ 128 if not prefix: 129 if '' not in self._namespaces: 130 self._namespaces[prefix] = uri 131 return 132 elif self._namespaces[''] == uri: 133 return 134 prefix = 'default' 135 136 while prefix in self._namespaces: 137 if self._namespaces[prefix] == uri: 138 return 139 match = re.search(r'(\d+)$', prefix) 140 if match: 141 index = int(match.group()) + 1 142 prefix = prefix[:match.span()[0]] + str(index) 143 else: 144 prefix += '0' 145 self._namespaces[prefix] = uri 146 147 def _map_qname(self, qname: str) -> str: 148 """ 149 Converts an extended QName to the prefixed format. Only registered 150 namespaces are mapped. 151 152 :param qname: a QName in extended format or a local name. 153 :return: a QName in prefixed format or a local name. 154 """ 155 try: 156 if qname[0] != '{' or not self._namespaces: 157 return qname 158 namespace, local_part = qname[1:].split('}') 159 except IndexError: 160 return qname 161 except ValueError: 162 raise XMLSchemaValueError("the argument 'qname' has a wrong format: %r" % qname) 163 except TypeError: 164 raise XMLSchemaTypeError("the argument 'qname' must be a string-like object") 165 166 for prefix, uri in sorted(self._namespaces.items(), reverse=True): 167 if uri == namespace: 168 return '%s:%s' % (prefix, local_part) if prefix else local_part 169 else: 170 return qname 171 172 map_qname = _map_qname 173 174 def _unmap_qname(self, qname: str, 175 name_table: Optional[Container[Optional[str]]] = None) -> str: 176 """ 177 Converts a QName in prefixed format or a local name to the extended QName format. 178 Local names are converted only if a default namespace is included in the instance. 179 If a *name_table* is provided a local name is mapped to the default namespace 180 only if not found in the name table. 181 182 :param qname: a QName in prefixed format or a local name 183 :param name_table: an optional lookup table for checking local names. 184 :return: a QName in extended format or a local name. 185 """ 186 try: 187 if qname[0] == '{' or not self._namespaces: 188 return qname 189 prefix, name = qname.split(':') 190 except IndexError: 191 return qname 192 except ValueError: 193 if ':' in qname: 194 raise XMLSchemaValueError("the argument 'qname' has a wrong format: %r" % qname) 195 if not self._namespaces.get(''): 196 return qname 197 elif name_table is None or qname not in name_table: 198 return '{%s}%s' % (self._namespaces.get(''), qname) 199 else: 200 return qname 201 except (TypeError, AttributeError): 202 raise XMLSchemaTypeError("the argument 'qname' must be a string-like object") 203 else: 204 try: 205 uri = self._namespaces[prefix] 206 except KeyError: 207 return qname 208 else: 209 return '{%s}%s' % (uri, name) if uri else name 210 211 unmap_qname = _unmap_qname 212 213 @staticmethod 214 def _local_name(qname: str, *_args: Any, **_kwargs: Any) -> str: 215 return local_name(qname) 216 217 def transfer(self, namespaces: NamespacesType) -> None: 218 """ 219 Transfers compatible prefix/namespace registrations from a dictionary. 220 Registrations added to namespace mapper instance are deleted from argument. 221 222 :param namespaces: a dictionary containing prefix/namespace registrations. 223 """ 224 transferred = [] 225 for k, v in namespaces.items(): 226 if k in self._namespaces: 227 if v != self._namespaces[k]: 228 continue 229 else: 230 self[k] = v 231 transferred.append(k) 232 233 for k in transferred: 234 del namespaces[k] 235 236 237T = TypeVar('T') 238 239 240class NamespaceView(Mapping[str, T]): 241 """ 242 A read-only map for filtered access to a dictionary that stores 243 objects mapped from QNames in extended format. 244 """ 245 __slots__ = 'target_dict', 'namespace', '_key_fmt' 246 247 def __init__(self, qname_dict: Dict[str, T], namespace_uri: str): 248 self.target_dict = qname_dict 249 self.namespace = namespace_uri 250 if namespace_uri: 251 self._key_fmt = '{' + namespace_uri + '}%s' 252 else: 253 self._key_fmt = '%s' 254 255 def __getitem__(self, key: str) -> T: 256 return self.target_dict[self._key_fmt % key] 257 258 def __len__(self) -> int: 259 if not self.namespace: 260 return len([k for k in self.target_dict if not k or k[0] != '{']) 261 return len([k for k in self.target_dict 262 if k and k[0] == '{' and self.namespace == k[1:k.rindex('}')]]) 263 264 def __iter__(self) -> Iterator[str]: 265 if not self.namespace: 266 for k in self.target_dict: 267 if not k or k[0] != '{': 268 yield k 269 else: 270 for k in self.target_dict: 271 if k and k[0] == '{' and self.namespace == k[1:k.rindex('}')]: 272 yield k[k.rindex('}') + 1:] 273 274 def __repr__(self) -> str: 275 return '%s(%s)' % (self.__class__.__name__, str(self.as_dict())) 276 277 def __contains__(self, key: object) -> bool: 278 if isinstance(key, str): 279 return self._key_fmt % key in self.target_dict 280 return key in self.target_dict 281 282 def __eq__(self, other: Any) -> Any: 283 return self.as_dict() == other 284 285 def as_dict(self, fqn_keys: bool = False) -> Dict[str, T]: 286 if not self.namespace: 287 return { 288 k: v for k, v in self.target_dict.items() if not k or k[0] != '{' 289 } 290 elif fqn_keys: 291 return { 292 k: v for k, v in self.target_dict.items() 293 if k and k[0] == '{' and self.namespace == k[1:k.rindex('}')] 294 } 295 else: 296 return { 297 k[k.rindex('}') + 1:]: v for k, v in self.target_dict.items() 298 if k and k[0] == '{' and self.namespace == k[1:k.rindex('}')] 299 } 300