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#
10from typing import TYPE_CHECKING, Any, Optional, cast, Iterable, Union, Callable
11from ..exceptions import XMLSchemaException, XMLSchemaWarning, XMLSchemaValueError
12from ..etree import etree_tostring
13from ..aliases import ElementType, NamespacesType, SchemaElementType, ModelParticleType
14from ..helpers import get_prefixed_qname, etree_getpath, is_etree_element
15
16if TYPE_CHECKING:
17    from ..resources import XMLResource
18    from .xsdbase import XsdValidator
19    from .groups import XsdGroup
20
21ValidatorType = Union['XsdValidator', Callable[[Any], None]]
22
23
24class XMLSchemaValidatorError(XMLSchemaException):
25    """
26    Base class for XSD validator errors.
27
28    :param validator: the XSD validator.
29    :param message: the error message.
30    :param elem: the element that contains the error.
31    :param source: the XML resource that contains the error.
32    :param namespaces: is an optional mapping from namespace prefix to URI.
33    :ivar path: the XPath of the element, calculated when the element is set \
34    or the XML resource is set.
35    """
36    path: Optional[str]
37
38    def __init__(self, validator: ValidatorType,
39                 message: str,
40                 elem: Optional[ElementType] = None,
41                 source: Optional['XMLResource'] = None,
42                 namespaces: Optional[NamespacesType] = None) -> None:
43        self.path = None
44        self.validator = validator
45        self.message = message[:-1] if message[-1] in ('.', ':') else message
46        self.namespaces = namespaces
47        self.source = source
48        self.elem = elem
49
50    def __str__(self) -> str:
51        if self.elem is None:
52            return self.message
53
54        msg = ['%s:\n' % self.message]
55        elem_as_string = cast(str, etree_tostring(self.elem, self.namespaces, '  ', 20))
56        msg.append("Schema:\n\n%s\n" % elem_as_string)
57
58        if self.path is not None:
59            msg.append("Path: %s\n" % self.path)
60        if self.schema_url is not None:
61            msg.append("Schema URL: %s\n" % self.schema_url)
62            if self.origin_url not in (None, self.schema_url):
63                msg.append("Origin URL: %s\n" % self.origin_url)
64        return '\n'.join(msg)
65
66    @property
67    def msg(self) -> str:
68        return self.__str__()
69
70    def __setattr__(self, name: str, value: Any) -> None:
71        if name == 'elem' and value is not None:
72            if not is_etree_element(value):
73                raise XMLSchemaValueError(
74                    "'elem' attribute requires an Element, not %r." % type(value)
75                )
76            if self.root is not None:
77                self.path = etree_getpath(value, self.root, self.namespaces,
78                                          relative=False, add_position=True)
79                if self.source is not None and self.source.is_lazy():
80                    value = None  # Don't save the element of a lazy resource
81        super(XMLSchemaValidatorError, self).__setattr__(name, value)
82
83    @property
84    def sourceline(self) -> Any:
85        return getattr(self.elem, 'sourceline', None)
86
87    @property
88    def root(self) -> Optional[ElementType]:
89        try:
90            return self.source.root  # type: ignore[union-attr]
91        except AttributeError:
92            return None
93
94    @property
95    def schema_url(self) -> Optional[str]:
96        url: Optional[str]
97        try:
98            url = self.validator.schema.source.url  # type: ignore[union-attr]
99        except AttributeError:
100            return None
101        else:
102            return url
103
104    @property
105    def origin_url(self) -> Optional[str]:
106        url: Optional[str]
107        try:
108            url = self.validator.maps.validator.source.url  # type: ignore[union-attr]
109        except AttributeError:
110            return None
111        else:
112            return url
113
114
115class XMLSchemaNotBuiltError(XMLSchemaValidatorError, RuntimeError):
116    """
117    Raised when there is an improper usage attempt of a not built XSD validator.
118
119    :param validator: the XSD validator.
120    :param message: the error message.
121    """
122    def __init__(self, validator: 'XsdValidator', message: str) -> None:
123        super(XMLSchemaNotBuiltError, self).__init__(
124            validator=validator,
125            message=message,
126            elem=getattr(validator, 'elem', None),
127            source=getattr(validator, 'source', None),
128            namespaces=getattr(validator, 'namespaces', None)
129        )
130
131
132class XMLSchemaParseError(XMLSchemaValidatorError, SyntaxError):  # type: ignore[misc]
133    """
134    Raised when an error is found during the building of an XSD validator.
135
136    :param validator: the XSD validator.
137    :param message: the error message.
138    :param elem: the element that contains the error.
139    """
140    def __init__(self, validator: 'XsdValidator', message: str,
141                 elem: Optional[ElementType] = None) -> None:
142        super(XMLSchemaParseError, self).__init__(
143            validator=validator,
144            message=message,
145            elem=elem if elem is not None else getattr(validator, 'elem', None),
146            source=getattr(validator, 'source', None),
147            namespaces=getattr(validator, 'namespaces', None),
148        )
149
150
151class XMLSchemaModelError(XMLSchemaValidatorError, ValueError):
152    """
153    Raised when a model error is found during the checking of a model group.
154
155    :param group: the XSD model group.
156    :param message: the error message.
157    """
158    def __init__(self, group: 'XsdGroup', message: str) -> None:
159        super(XMLSchemaModelError, self).__init__(
160            validator=group,
161            message=message,
162            elem=getattr(group, 'elem', None),
163            source=getattr(group, 'source', None),
164            namespaces=getattr(group, 'namespaces', None)
165        )
166
167
168class XMLSchemaModelDepthError(XMLSchemaModelError):
169    """Raised when recursion depth is exceeded while iterating a model group."""
170    def __init__(self, group: 'XsdGroup') -> None:
171        msg = "maximum model recursion depth exceeded while iterating {!r}".format(group)
172        super(XMLSchemaModelDepthError, self).__init__(group, message=msg)
173
174
175class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError):
176    """
177    Raised when the XML data is not validated with the XSD component or schema.
178    It's used by decoding and encoding methods. Encoding validation errors do
179    not include XML data element and source, so the error is limited to a message
180    containing object representation and a reason.
181
182    :param validator: the XSD validator.
183    :param obj: the not validated XML data.
184    :param reason: the detailed reason of failed validation.
185    :param source: the XML resource that contains the error.
186    :param namespaces: is an optional mapping from namespace prefix to URI.
187    """
188    def __init__(self,
189                 validator: ValidatorType,
190                 obj: Any,
191                 reason: Optional[str] = None,
192                 source: Optional['XMLResource'] = None,
193                 namespaces: Optional[NamespacesType] = None) -> None:
194        if not isinstance(obj, str):
195            _obj = obj
196        else:
197            _obj = obj.encode('ascii', 'xmlcharrefreplace').decode('utf-8')
198
199        super(XMLSchemaValidationError, self).__init__(
200            validator=validator,
201            message="failed validating {!r} with {!r}".format(_obj, validator),
202            elem=obj if is_etree_element(obj) else None,
203            source=source,
204            namespaces=namespaces,
205        )
206        self.obj = obj
207        self.reason = reason
208
209    def __repr__(self) -> str:
210        return '%s(reason=%r)' % (self.__class__.__name__, self.reason)
211
212    def __str__(self) -> str:
213        msg = ['%s:\n' % self.message]
214
215        if self.reason is not None:
216            msg.append('Reason: %s\n' % self.reason)
217
218        if hasattr(self.validator, 'tostring'):
219            chunk = self.validator.tostring('  ', 20)  # type: ignore[union-attr]
220            msg.append("Schema:\n\n%s\n" % chunk)
221
222        if self.elem is not None and is_etree_element(self.elem):
223            try:
224                elem_as_string = cast(str, etree_tostring(self.elem, self.namespaces, '  ', 20))
225            except (ValueError, TypeError):        # pragma: no cover
226                elem_as_string = repr(self.elem)   # pragma: no cover
227
228            if hasattr(self.elem, 'sourceline'):
229                line = getattr(self.elem, 'sourceline')
230                msg.append("Instance (line %r):\n\n%s\n" % (line, elem_as_string))
231            else:
232                msg.append("Instance:\n\n%s\n" % elem_as_string)
233
234        if self.path is not None:
235            msg.append("Path: %s\n" % self.path)
236
237        if len(msg) == 1:
238            return msg[0][:-2]
239
240        return '\n'.join(msg)
241
242
243class XMLSchemaDecodeError(XMLSchemaValidationError):
244    """
245    Raised when an XML data string is not decodable to a Python object.
246
247    :param validator: the XSD validator.
248    :param obj: the not validated XML data.
249    :param decoder: the XML data decoder.
250    :param reason: the detailed reason of failed validation.
251    :param source: the XML resource that contains the error.
252    :param namespaces: is an optional mapping from namespace prefix to URI.
253    """
254    message = "failed decoding {!r} with {!r}.\n"
255
256    def __init__(self, validator: Union['XsdValidator', Callable[[Any], None]],
257                 obj: Any,
258                 decoder: Any,
259                 reason: Optional[str] = None,
260                 source: Optional['XMLResource'] = None,
261                 namespaces: Optional[NamespacesType] = None) -> None:
262        super(XMLSchemaDecodeError, self).__init__(validator, obj, reason, source, namespaces)
263        self.decoder = decoder
264
265
266class XMLSchemaEncodeError(XMLSchemaValidationError):
267    """
268    Raised when an object is not encodable to an XML data string.
269
270    :param validator: the XSD validator.
271    :param obj: the not validated XML data.
272    :param encoder: the XML encoder.
273    :param reason: the detailed reason of failed validation.
274    :param source: the XML resource that contains the error.
275    :param namespaces: is an optional mapping from namespace prefix to URI.
276    """
277    message = "failed encoding {!r} with {!r}.\n"
278
279    def __init__(self, validator: Union['XsdValidator', Callable[[Any], None]],
280                 obj: Any,
281                 encoder: Any,
282                 reason: Optional[str] = None,
283                 source: Optional['XMLResource'] = None,
284                 namespaces: Optional[NamespacesType] = None) -> None:
285        super(XMLSchemaEncodeError, self).__init__(validator, obj, reason, source, namespaces)
286        self.encoder = encoder
287
288
289class XMLSchemaChildrenValidationError(XMLSchemaValidationError):
290    """
291    Raised when a child element is not validated.
292
293    :param validator: the XSD validator.
294    :param elem: the not validated XML element.
295    :param index: the child index.
296    :param particle: the model particle that generated the error. Maybe the validator itself.
297    :param occurs: the particle occurrences.
298    :param expected: the expected element tags/object names.
299    :param source: the XML resource that contains the error.
300    :param namespaces: is an optional mapping from namespace prefix to URI.
301    """
302    def __init__(self, validator: 'XsdValidator',
303                 elem: ElementType,
304                 index: int,
305                 particle: ModelParticleType,
306                 occurs: int = 0,
307                 expected: Optional[Iterable[SchemaElementType]] = None,
308                 source: Optional['XMLResource'] = None,
309                 namespaces: Optional[NamespacesType] = None) -> None:
310
311        self.index = index
312        self.particle = particle
313        self.occurs = occurs
314        self.expected = expected
315
316        tag = get_prefixed_qname(elem.tag, validator.namespaces, use_empty=False)
317        if index >= len(elem):
318            reason = "The content of element %r is not complete." % tag
319        else:
320            child_tag = get_prefixed_qname(elem[index].tag, validator.namespaces, use_empty=False)
321            reason = "Unexpected child with tag %r at position %d." % (child_tag, index + 1)
322
323        if occurs and particle.is_missing(occurs):
324            reason += " The particle %r occurs %d times but the minimum is %d." % (
325                particle, occurs, particle.min_occurs
326            )
327        elif particle.is_over(occurs):
328            reason += " The particle %r occurs %r times but the maximum is %r." % (
329                particle, occurs, particle.max_occurs
330            )
331
332        if expected is None:
333            pass
334        else:
335            expected_tags = []
336            for xsd_element in expected:
337                name = xsd_element.prefixed_name
338                if name is not None:
339                    expected_tags.append(name)
340                elif getattr(xsd_element, 'process_contents', '') == 'strict':
341                    expected_tags.append(
342                        'from %r namespace/s' % xsd_element.namespace  # type: ignore[union-attr]
343                    )
344
345            if not expected_tags:
346                pass
347            elif len(expected_tags) > 1:
348                reason += " Tag (%s) expected." % ' | '.join(repr(tag) for tag in expected_tags)
349            elif expected_tags[0].startswith('from '):
350                reason += " Tag %s expected." % expected_tags[0]
351            else:
352                reason += " Tag %r expected." % expected_tags[0]
353
354        super(XMLSchemaChildrenValidationError, self).\
355            __init__(validator, elem, reason, source, namespaces)
356
357
358class XMLSchemaIncludeWarning(XMLSchemaWarning):
359    """A schema include fails."""
360
361
362class XMLSchemaImportWarning(XMLSchemaWarning):
363    """A schema namespace import fails."""
364
365
366class XMLSchemaTypeTableWarning(XMLSchemaWarning):
367    """Not equivalent type table found in model."""
368