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