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