2017-07-07 12:48:19 +02:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
#
|
2019-01-20 16:53:23 +01:00
|
|
|
|
# Copyright (c), 2016-2019, SISSA (International School for Advanced Studies).
|
2017-07-07 12:48:19 +02:00
|
|
|
|
# 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>
|
|
|
|
|
#
|
|
|
|
|
"""
|
|
|
|
|
This module contains base functions and classes XML Schema components.
|
|
|
|
|
"""
|
2018-09-15 22:18:16 +02:00
|
|
|
|
from __future__ import unicode_literals
|
2017-07-07 12:48:19 +02:00
|
|
|
|
import re
|
|
|
|
|
|
2018-11-15 10:54:11 +01:00
|
|
|
|
from ..compat import PY3, string_base_type, unicode_type
|
2017-12-14 13:38:23 +01:00
|
|
|
|
from ..exceptions import XMLSchemaValueError, XMLSchemaTypeError
|
2019-08-01 17:16:12 +02:00
|
|
|
|
from ..qnames import XSD_ANNOTATION, XSD_APPINFO, XSD_DOCUMENTATION, XML_LANG, \
|
2019-10-07 15:31:18 +02:00
|
|
|
|
XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE, XSD_ID, XSD_OVERRIDE, \
|
|
|
|
|
get_qname, local_name, qname_to_prefixed
|
|
|
|
|
from ..etree import etree_tostring
|
|
|
|
|
from ..helpers import is_etree_element
|
2019-09-18 23:24:41 +02:00
|
|
|
|
from .exceptions import XMLSchemaParseError, XMLSchemaValidationError, \
|
|
|
|
|
XMLSchemaDecodeError, XMLSchemaEncodeError
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2019-08-01 07:03:03 +02:00
|
|
|
|
|
2018-06-07 23:30:41 +02:00
|
|
|
|
XSD_VALIDATION_MODES = {'strict', 'lax', 'skip'}
|
|
|
|
|
"""
|
|
|
|
|
XML Schema validation modes
|
|
|
|
|
Ref.: https://www.w3.org/TR/xmlschema11-1/#key-va
|
|
|
|
|
"""
|
|
|
|
|
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2018-08-06 07:11:37 +02:00
|
|
|
|
class XsdValidator(object):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-08-06 07:11:37 +02:00
|
|
|
|
Common base class for XML Schema validator, that represents a PSVI (Post Schema Validation
|
|
|
|
|
Infoset) information item. A concrete XSD validator have to report its validity collecting
|
|
|
|
|
building errors and implementing the properties.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2018-08-06 07:11:37 +02:00
|
|
|
|
:param validation: defines the XSD validation mode to use for build the validator, \
|
|
|
|
|
its value can be 'strict', 'lax' or 'skip'. Strict mode is the default.
|
2018-06-07 23:30:41 +02:00
|
|
|
|
:type validation: str
|
2018-08-06 07:11:37 +02:00
|
|
|
|
|
|
|
|
|
:ivar validation: XSD validation mode.
|
|
|
|
|
:vartype validation: str
|
|
|
|
|
:ivar errors: XSD validator building errors.
|
|
|
|
|
:vartype errors: list
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2019-07-30 21:06:54 +02:00
|
|
|
|
xsd_version = None
|
|
|
|
|
|
2018-01-27 16:40:20 +01:00
|
|
|
|
def __init__(self, validation='strict'):
|
2018-06-07 23:30:41 +02:00
|
|
|
|
if validation not in XSD_VALIDATION_MODES:
|
|
|
|
|
raise XMLSchemaValueError("validation argument can be 'strict', 'lax' or 'skip': %r" % validation)
|
2018-01-27 16:40:20 +01:00
|
|
|
|
self.validation = validation
|
2018-08-06 07:11:37 +02:00
|
|
|
|
self.errors = []
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2018-07-26 15:19:26 +02:00
|
|
|
|
def __str__(self):
|
|
|
|
|
# noinspection PyCompatibility,PyUnresolvedReferences
|
|
|
|
|
return unicode(self).encode("utf-8")
|
|
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
|
return self.__repr__()
|
|
|
|
|
|
|
|
|
|
if PY3:
|
|
|
|
|
__str__ = __unicode__
|
|
|
|
|
|
2017-12-14 13:38:23 +01:00
|
|
|
|
@property
|
|
|
|
|
def built(self):
|
2018-01-11 10:59:43 +01:00
|
|
|
|
"""
|
2019-07-30 14:19:52 +02:00
|
|
|
|
Property that is ``True`` if XSD validator has been fully parsed and built,
|
|
|
|
|
``False`` otherwise. For schemas the property is checked on all global
|
|
|
|
|
components. For XSD components check only the building of local subcomponents.
|
2018-01-11 10:59:43 +01:00
|
|
|
|
"""
|
2017-12-14 13:38:23 +01:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def validation_attempted(self):
|
|
|
|
|
"""
|
2019-04-07 11:50:22 +02:00
|
|
|
|
Property that returns the *validation status* of the XSD validator. It can be 'full', 'partial' or 'none'.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2018-01-11 10:59:43 +01:00
|
|
|
|
| https://www.w3.org/TR/xmlschema-1/#e-validation_attempted
|
|
|
|
|
| https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#e-validation_attempted
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-01-11 10:59:43 +01:00
|
|
|
|
@property
|
|
|
|
|
def validity(self):
|
|
|
|
|
"""
|
2018-08-06 07:11:37 +02:00
|
|
|
|
Property that returns the XSD validator's validity. It can be ‘valid’, ‘invalid’ or ‘notKnown’.
|
2018-01-11 10:59:43 +01:00
|
|
|
|
|
|
|
|
|
| https://www.w3.org/TR/xmlschema-1/#e-validity
|
|
|
|
|
| https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#e-validity
|
|
|
|
|
"""
|
2019-07-30 14:19:52 +02:00
|
|
|
|
if self.errors or any(comp.errors for comp in self.iter_components()):
|
2018-01-11 10:59:43 +01:00
|
|
|
|
return 'invalid'
|
|
|
|
|
elif self.built:
|
|
|
|
|
return 'valid'
|
|
|
|
|
else:
|
|
|
|
|
return 'notKnown'
|
|
|
|
|
|
2017-12-14 13:38:23 +01:00
|
|
|
|
def iter_components(self, xsd_classes=None):
|
|
|
|
|
"""
|
2018-08-11 08:03:18 +02:00
|
|
|
|
Creates an iterator for traversing all XSD components of the validator.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2018-08-11 08:03:18 +02:00
|
|
|
|
:param xsd_classes: returns only a specific class/classes of components, \
|
2017-12-14 13:38:23 +01:00
|
|
|
|
otherwise returns all components.
|
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def all_errors(self):
|
|
|
|
|
"""
|
2018-08-06 07:11:37 +02:00
|
|
|
|
A list with all the building errors of the XSD validator and its components.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
|
|
|
|
errors = []
|
|
|
|
|
for comp in self.iter_components():
|
|
|
|
|
if comp.errors:
|
|
|
|
|
errors.extend(comp.errors)
|
|
|
|
|
return errors
|
|
|
|
|
|
2019-04-07 11:50:22 +02:00
|
|
|
|
def copy(self):
|
|
|
|
|
validator = object.__new__(self.__class__)
|
|
|
|
|
validator.__dict__.update(self.__dict__)
|
|
|
|
|
validator.errors = self.errors[:]
|
|
|
|
|
return validator
|
|
|
|
|
|
|
|
|
|
__copy__ = copy
|
|
|
|
|
|
2019-07-30 14:19:52 +02:00
|
|
|
|
def parse_error(self, error, elem=None, validation=None):
|
2018-08-07 17:57:35 +02:00
|
|
|
|
"""
|
|
|
|
|
Helper method for registering parse errors. Does nothing if validation mode is 'skip'.
|
|
|
|
|
Il validation mode is 'lax' collects the error, otherwise raise the error.
|
|
|
|
|
|
|
|
|
|
:param error: can be a parse error or an error message.
|
|
|
|
|
:param elem: the Element instance related to the error, for default uses the 'elem' \
|
|
|
|
|
attribute of the validator, if it's present.
|
2019-07-30 14:19:52 +02:00
|
|
|
|
:param validation: overrides the default validation mode of the validator.
|
2018-08-07 17:57:35 +02:00
|
|
|
|
"""
|
2019-07-30 14:19:52 +02:00
|
|
|
|
if validation not in XSD_VALIDATION_MODES:
|
|
|
|
|
validation = self.validation
|
|
|
|
|
if validation == 'skip':
|
2018-08-07 17:57:35 +02:00
|
|
|
|
return
|
|
|
|
|
|
2018-10-27 09:17:33 +02:00
|
|
|
|
if is_etree_element(elem):
|
2019-01-26 10:46:43 +01:00
|
|
|
|
pass
|
2018-10-27 09:17:33 +02:00
|
|
|
|
elif elem is None:
|
|
|
|
|
elem = getattr(self, 'elem', None)
|
|
|
|
|
else:
|
|
|
|
|
raise XMLSchemaValueError("'elem' argument must be an Element instance, not %r." % elem)
|
|
|
|
|
|
2018-08-07 17:57:35 +02:00
|
|
|
|
if isinstance(error, XMLSchemaParseError):
|
|
|
|
|
error.validator = self
|
|
|
|
|
error.namespaces = getattr(self, 'namespaces', None)
|
|
|
|
|
error.elem = elem
|
|
|
|
|
error.source = getattr(self, 'source', None)
|
2018-11-15 10:54:11 +01:00
|
|
|
|
elif isinstance(error, Exception):
|
2019-08-05 17:57:34 +02:00
|
|
|
|
message = unicode_type(error).strip()
|
|
|
|
|
if message[0] in '\'"' and message[0] == message[-1]:
|
|
|
|
|
message = message.strip('\'"')
|
|
|
|
|
error = XMLSchemaParseError(self, message, elem)
|
2018-08-07 17:57:35 +02:00
|
|
|
|
elif isinstance(error, string_base_type):
|
2018-11-15 10:54:11 +01:00
|
|
|
|
error = XMLSchemaParseError(self, error, elem)
|
2018-08-07 17:57:35 +02:00
|
|
|
|
else:
|
2018-11-15 10:54:11 +01:00
|
|
|
|
raise XMLSchemaValueError("'error' argument must be an exception or a string, not %r." % error)
|
2018-08-07 17:57:35 +02:00
|
|
|
|
|
2019-07-30 14:19:52 +02:00
|
|
|
|
if validation == 'lax':
|
2018-08-07 17:57:35 +02:00
|
|
|
|
self.errors.append(error)
|
|
|
|
|
else:
|
|
|
|
|
raise error
|
|
|
|
|
|
2019-02-23 10:40:55 +01:00
|
|
|
|
def _parse_xpath_default_namespace(self, elem):
|
|
|
|
|
"""
|
|
|
|
|
Parse XSD 1.1 xpathDefaultNamespace attribute for schema, alternative, assert, assertion
|
|
|
|
|
and selector declarations, checking if the value is conforming to the specification. In
|
|
|
|
|
case the attribute is missing or for wrong attribute values defaults to ''.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
value = elem.attrib['xpathDefaultNamespace']
|
|
|
|
|
except KeyError:
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
value = value.strip()
|
|
|
|
|
if value == '##local':
|
|
|
|
|
return ''
|
|
|
|
|
elif value == '##defaultNamespace':
|
|
|
|
|
return getattr(self, 'default_namespace')
|
|
|
|
|
elif value == '##targetNamespace':
|
|
|
|
|
return getattr(self, 'target_namespace')
|
|
|
|
|
elif len(value.split()) == 1:
|
|
|
|
|
return value
|
|
|
|
|
else:
|
|
|
|
|
admitted_values = ('##defaultNamespace', '##targetNamespace', '##local')
|
|
|
|
|
msg = "wrong value %r for 'xpathDefaultNamespace' attribute, can be (anyURI | %s)."
|
|
|
|
|
self.parse_error(msg % (value, ' | '.join(admitted_values)), elem)
|
|
|
|
|
return ''
|
|
|
|
|
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
2018-08-06 07:11:37 +02:00
|
|
|
|
class XsdComponent(XsdValidator):
|
2017-07-07 12:48:19 +02:00
|
|
|
|
"""
|
2018-08-06 07:11:37 +02:00
|
|
|
|
Class for XSD components. See: https://www.w3.org/TR/xmlschema-ref/
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
|
|
|
|
:param elem: ElementTree's node containing the definition.
|
2018-08-06 07:11:37 +02:00
|
|
|
|
:param schema: the XMLSchema object that owns the definition.
|
2018-08-08 15:47:24 +02:00
|
|
|
|
:param parent: the XSD parent, `None` means that is a global component that has the schema as parent.
|
2018-08-06 07:11:37 +02:00
|
|
|
|
:param name: name of the component, maybe overwritten by the parse of the `elem` argument.
|
|
|
|
|
|
2019-04-15 18:47:51 +02:00
|
|
|
|
:cvar qualified: for name matching, unqualified matching may be admitted only for elements and attributes.
|
2018-08-06 07:11:37 +02:00
|
|
|
|
:vartype qualified: bool
|
2017-07-07 12:48:19 +02:00
|
|
|
|
"""
|
|
|
|
|
_REGEX_SPACE = re.compile(r'\s')
|
|
|
|
|
_REGEX_SPACES = re.compile(r'\s+')
|
2019-07-30 21:06:54 +02:00
|
|
|
|
_ADMITTED_TAGS = ()
|
2018-07-26 15:19:26 +02:00
|
|
|
|
|
2019-04-15 18:47:51 +02:00
|
|
|
|
parent = None
|
2019-03-21 07:14:58 +01:00
|
|
|
|
name = None
|
2019-07-29 16:01:48 +02:00
|
|
|
|
ref = None
|
2018-08-06 07:11:37 +02:00
|
|
|
|
qualified = True
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
2019-04-15 18:47:51 +02:00
|
|
|
|
def __init__(self, elem, schema, parent=None, name=None):
|
2018-01-27 16:40:20 +01:00
|
|
|
|
super(XsdComponent, self).__init__(schema.validation)
|
2019-03-21 07:14:58 +01:00
|
|
|
|
if name is not None:
|
|
|
|
|
assert name and (name[0] == '{' or not schema.target_namespace), \
|
|
|
|
|
"name=%r argument: must be a qualified name of the target namespace." % name
|
|
|
|
|
self.name = name
|
2019-04-15 18:47:51 +02:00
|
|
|
|
if parent is not None:
|
|
|
|
|
self.parent = parent
|
2017-07-07 12:48:19 +02:00
|
|
|
|
self.schema = schema
|
|
|
|
|
self.elem = elem
|
|
|
|
|
|
|
|
|
|
def __setattr__(self, name, value):
|
|
|
|
|
if name == "elem":
|
2018-07-13 18:43:43 +02:00
|
|
|
|
if not is_etree_element(value):
|
2017-07-07 12:48:19 +02:00
|
|
|
|
raise XMLSchemaTypeError("%r attribute must be an Etree Element: %r" % (name, value))
|
2019-07-30 21:06:54 +02:00
|
|
|
|
elif value.tag not in self._ADMITTED_TAGS:
|
2017-07-07 12:48:19 +02:00
|
|
|
|
raise XMLSchemaValueError(
|
|
|
|
|
"wrong XSD element %r for %r, must be one of %r." % (
|
|
|
|
|
local_name(value.tag), self,
|
2019-07-30 21:06:54 +02:00
|
|
|
|
[local_name(tag) for tag in self._ADMITTED_TAGS]
|
2017-07-07 12:48:19 +02:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
super(XsdComponent, self).__setattr__(name, value)
|
|
|
|
|
self._parse()
|
|
|
|
|
return
|
|
|
|
|
elif name == "schema":
|
|
|
|
|
if hasattr(self, 'schema') and self.schema.target_namespace != value.target_namespace:
|
|
|
|
|
raise XMLSchemaValueError(
|
|
|
|
|
"cannot change 'schema' attribute of %r: the actual %r has a different "
|
|
|
|
|
"target namespace than %r." % (self, self.schema, value)
|
|
|
|
|
)
|
|
|
|
|
super(XsdComponent, self).__setattr__(name, value)
|
|
|
|
|
|
2019-07-30 21:06:54 +02:00
|
|
|
|
@property
|
|
|
|
|
def xsd_version(self):
|
|
|
|
|
return self.schema.XSD_VERSION
|
|
|
|
|
|
2018-08-08 15:47:24 +02:00
|
|
|
|
def is_global(self):
|
2019-09-18 23:24:41 +02:00
|
|
|
|
"""Returns `True` if the instance is a global component, `False` if it's local."""
|
2018-08-08 15:47:24 +02:00
|
|
|
|
return self.parent is None
|
|
|
|
|
|
2019-09-18 23:24:41 +02:00
|
|
|
|
def is_override(self):
|
|
|
|
|
"""Returns `True` if the instance is an override of a global component."""
|
|
|
|
|
if self.parent is not None:
|
|
|
|
|
return False
|
|
|
|
|
return any(self.elem in x for x in self.schema.root if x.tag == XSD_OVERRIDE)
|
|
|
|
|
|
2018-08-24 07:20:08 +02:00
|
|
|
|
@property
|
|
|
|
|
def schema_elem(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""The reference element of the schema for the component instance."""
|
2018-08-24 07:20:08 +02:00
|
|
|
|
return self.elem
|
|
|
|
|
|
2018-08-06 16:48:29 +02:00
|
|
|
|
@property
|
|
|
|
|
def source(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""Property that references to schema source."""
|
2018-08-06 16:48:29 +02:00
|
|
|
|
return self.schema.source
|
|
|
|
|
|
2018-01-20 06:11:43 +01:00
|
|
|
|
@property
|
|
|
|
|
def target_namespace(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""Property that references to schema's targetNamespace."""
|
2018-01-20 06:11:43 +01:00
|
|
|
|
return self.schema.target_namespace
|
|
|
|
|
|
2018-08-07 23:29:47 +02:00
|
|
|
|
@property
|
|
|
|
|
def default_namespace(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""Property that references to schema's default namespaces."""
|
2018-08-07 23:29:47 +02:00
|
|
|
|
return self.schema.namespaces.get('')
|
|
|
|
|
|
2018-01-20 06:11:43 +01:00
|
|
|
|
@property
|
|
|
|
|
def namespaces(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""Property that references to schema's namespace mapping."""
|
2018-02-16 14:19:56 +01:00
|
|
|
|
return self.schema.namespaces
|
2018-01-20 06:11:43 +01:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def maps(self):
|
2018-08-24 19:39:45 +02:00
|
|
|
|
"""Property that references to schema's global maps."""
|
2018-01-20 06:11:43 +01:00
|
|
|
|
return self.schema.maps
|
|
|
|
|
|
2019-08-01 17:16:12 +02:00
|
|
|
|
@property
|
|
|
|
|
def any_type(self):
|
|
|
|
|
"""Property that references to the xs:anyType instance of the global maps."""
|
|
|
|
|
return self.schema.maps.types[XSD_ANY_TYPE]
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def any_simple_type(self):
|
|
|
|
|
"""Property that references to the xs:anySimpleType instance of the global maps."""
|
|
|
|
|
return self.schema.maps.types[XSD_ANY_SIMPLE_TYPE]
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def any_atomic_type(self):
|
|
|
|
|
"""Property that references to the xs:anyAtomicType instance of the global maps."""
|
|
|
|
|
return self.schema.maps.types[XSD_ANY_ATOMIC_TYPE]
|
|
|
|
|
|
2017-07-07 12:48:19 +02:00
|
|
|
|
def __repr__(self):
|
2018-01-21 12:13:44 +01:00
|
|
|
|
if self.name is None:
|
2018-09-15 22:18:16 +02:00
|
|
|
|
return '<%s at %#x>' % (self.__class__.__name__, id(self))
|
2019-07-30 14:19:52 +02:00
|
|
|
|
elif self.ref is not None:
|
|
|
|
|
return '%s(ref=%r)' % (self.__class__.__name__, self.prefixed_name)
|
2018-01-21 12:13:44 +01:00
|
|
|
|
else:
|
2018-09-15 22:18:16 +02:00
|
|
|
|
return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name)
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
2018-04-05 09:28:49 +02:00
|
|
|
|
def _parse(self):
|
2018-08-07 23:29:47 +02:00
|
|
|
|
del self.errors[:]
|
2018-04-05 09:28:49 +02:00
|
|
|
|
try:
|
2018-10-08 15:32:50 +02:00
|
|
|
|
if self.elem[0].tag == XSD_ANNOTATION:
|
2018-08-08 15:47:24 +02:00
|
|
|
|
self.annotation = XsdAnnotation(self.elem[0], self.schema, self)
|
2018-04-05 09:28:49 +02:00
|
|
|
|
else:
|
|
|
|
|
self.annotation = None
|
|
|
|
|
except (TypeError, IndexError):
|
|
|
|
|
self.annotation = None
|
|
|
|
|
|
2019-07-29 16:01:48 +02:00
|
|
|
|
def _parse_reference(self):
|
|
|
|
|
"""
|
|
|
|
|
Helper method for referable components. Returns `True` if a valid reference QName
|
|
|
|
|
is found without any error, otherwise returns `None`. Sets an id-related name for
|
|
|
|
|
the component ('nameless_<id of the instance>') if both the attributes 'ref' and
|
|
|
|
|
'name' are missing.
|
|
|
|
|
"""
|
|
|
|
|
ref = self.elem.get('ref')
|
|
|
|
|
if ref is None:
|
|
|
|
|
if 'name' in self.elem.attrib:
|
|
|
|
|
return
|
|
|
|
|
elif self.parent is None:
|
|
|
|
|
self.parse_error("missing attribute 'name' in a global %r" % type(self))
|
|
|
|
|
else:
|
|
|
|
|
self.parse_error("missing both attributes 'name' and 'ref' in local %r" % type(self))
|
|
|
|
|
self.name = 'nameless_%s' % str(id(self))
|
|
|
|
|
elif self.parent is None:
|
|
|
|
|
self.parse_error("attribute 'ref' not allowed in a global %r" % type(self))
|
|
|
|
|
elif 'name' in self.elem.attrib:
|
|
|
|
|
self.parse_error("attributes 'name' and 'ref' are mutually exclusive")
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
self.name = self.schema.resolve_qname(ref)
|
2019-08-01 07:03:03 +02:00
|
|
|
|
except (KeyError, ValueError, RuntimeError) as err:
|
2019-07-29 16:01:48 +02:00
|
|
|
|
self.parse_error(err)
|
|
|
|
|
else:
|
|
|
|
|
if self._parse_child_component(self.elem) is not None:
|
|
|
|
|
self.parse_error("a reference component cannot has child definitions/declarations")
|
|
|
|
|
return True
|
|
|
|
|
|
2019-08-10 23:58:00 +02:00
|
|
|
|
def _parse_boolean_attribute(self, name):
|
|
|
|
|
try:
|
|
|
|
|
value = self.elem.attrib[name].strip()
|
|
|
|
|
except KeyError:
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
if value in ('true', '1'):
|
|
|
|
|
return True
|
|
|
|
|
elif value in ('false', '0'):
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
self.parse_error("wrong value %r for boolean attribute %r" % (value, name))
|
|
|
|
|
|
2019-07-29 16:01:48 +02:00
|
|
|
|
def _parse_child_component(self, elem, strict=True):
|
2019-07-30 21:06:54 +02:00
|
|
|
|
child = None
|
|
|
|
|
for index, child in enumerate(filter(lambda x: x.tag != XSD_ANNOTATION, elem)):
|
2019-06-26 07:19:46 +02:00
|
|
|
|
if not strict:
|
2019-07-30 21:06:54 +02:00
|
|
|
|
return child
|
2019-06-26 07:19:46 +02:00
|
|
|
|
elif index:
|
|
|
|
|
msg = "too many XSD components, unexpected {!r} found at position {}"
|
2019-07-30 21:06:54 +02:00
|
|
|
|
self.parse_error(msg.format(child, index), elem)
|
|
|
|
|
return child
|
2019-03-01 09:33:06 +01:00
|
|
|
|
|
2019-02-07 23:10:31 +01:00
|
|
|
|
def _parse_target_namespace(self):
|
|
|
|
|
"""
|
|
|
|
|
XSD 1.1 targetNamespace attribute in elements and attributes declarations.
|
|
|
|
|
"""
|
2019-08-17 09:36:24 +02:00
|
|
|
|
if 'targetNamespace' not in self.elem.attrib:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._target_namespace = self.elem.attrib['targetNamespace'].strip()
|
|
|
|
|
if 'name' not in self.elem.attrib:
|
|
|
|
|
self.parse_error("attribute 'name' must be present when 'targetNamespace' attribute is provided")
|
|
|
|
|
if 'form' in self.elem.attrib:
|
|
|
|
|
self.parse_error("attribute 'form' must be absent when 'targetNamespace' attribute is provided")
|
|
|
|
|
if self._target_namespace != self.schema.target_namespace:
|
|
|
|
|
if self.parent is None:
|
|
|
|
|
self.parse_error("a global attribute must has the same namespace as its parent schema")
|
|
|
|
|
|
|
|
|
|
xsd_type = self.get_parent_type()
|
|
|
|
|
if xsd_type and xsd_type.parent is None and \
|
|
|
|
|
(xsd_type.derivation != 'restriction' or xsd_type.base_type is self.any_type):
|
|
|
|
|
self.parse_error("a declaration contained in a global complexType "
|
|
|
|
|
"must has the same namespace as its parent schema")
|
|
|
|
|
|
2019-09-11 18:43:13 +02:00
|
|
|
|
if not self._target_namespace and self.name[0] == '{':
|
|
|
|
|
self.name = local_name(self.name)
|
|
|
|
|
elif self.name[0] != '{':
|
|
|
|
|
self.name = '{%s}%s' % (self._target_namespace, self.name)
|
|
|
|
|
else:
|
|
|
|
|
self.name = '{%s}%s' % (self._target_namespace, local_name(self.name))
|
2019-02-07 23:10:31 +01:00
|
|
|
|
|
2017-12-13 19:21:13 +01:00
|
|
|
|
@property
|
|
|
|
|
def local_name(self):
|
|
|
|
|
return local_name(self.name)
|
|
|
|
|
|
2018-02-03 16:32:51 +01:00
|
|
|
|
@property
|
|
|
|
|
def qualified_name(self):
|
|
|
|
|
return get_qname(self.target_namespace, self.name)
|
|
|
|
|
|
2017-12-14 13:38:23 +01:00
|
|
|
|
@property
|
|
|
|
|
def prefixed_name(self):
|
|
|
|
|
return qname_to_prefixed(self.name, self.namespaces)
|
|
|
|
|
|
2017-07-07 12:48:19 +02:00
|
|
|
|
@property
|
|
|
|
|
def id(self):
|
|
|
|
|
"""The ``'id'`` attribute of the component tag, ``None`` if missing."""
|
|
|
|
|
return self.elem.get('id')
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def validation_attempted(self):
|
|
|
|
|
return 'full' if self.built else 'partial'
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def built(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2019-09-04 23:13:50 +02:00
|
|
|
|
def is_matching(self, name, default_namespace=None, **kwargs):
|
2018-09-09 11:57:08 +02:00
|
|
|
|
"""
|
2019-07-08 09:23:10 +02:00
|
|
|
|
Returns `True` if the component name is matching the name provided as argument,
|
|
|
|
|
`False` otherwise. For XSD elements the matching is extended to substitutes.
|
2018-09-09 11:57:08 +02:00
|
|
|
|
|
|
|
|
|
:param name: a local or fully-qualified name.
|
|
|
|
|
:param default_namespace: used if it's not None and not empty for completing the name \
|
|
|
|
|
argument in case it's a local name.
|
2019-09-04 23:13:50 +02:00
|
|
|
|
:param kwargs: additional options that can be used by certain components.
|
2018-09-09 11:57:08 +02:00
|
|
|
|
"""
|
2018-09-08 11:19:51 +02:00
|
|
|
|
if not name:
|
2018-07-26 13:43:47 +02:00
|
|
|
|
return self.name == name
|
2018-09-08 11:19:51 +02:00
|
|
|
|
elif name[0] == '{':
|
|
|
|
|
return self.qualified_name == name
|
|
|
|
|
elif not default_namespace:
|
2018-07-26 13:43:47 +02:00
|
|
|
|
return self.name == name or not self.qualified and self.local_name == name
|
2018-09-08 11:19:51 +02:00
|
|
|
|
else:
|
|
|
|
|
qname = '{%s}%s' % (default_namespace, name)
|
|
|
|
|
return self.qualified_name == qname or not self.qualified and self.local_name == name
|
2018-07-26 13:43:47 +02:00
|
|
|
|
|
2019-09-04 23:13:50 +02:00
|
|
|
|
def match(self, name, default_namespace=None, **kwargs):
|
2018-09-09 11:57:08 +02:00
|
|
|
|
"""Returns the component if its name is matching the name provided as argument, `None` otherwise."""
|
2019-09-04 23:13:50 +02:00
|
|
|
|
return self if self.is_matching(name, default_namespace, **kwargs) else None
|
2018-09-09 11:57:08 +02:00
|
|
|
|
|
2019-04-09 16:56:28 +02:00
|
|
|
|
def get_global(self):
|
|
|
|
|
"""Returns the global XSD component that contains the component instance."""
|
|
|
|
|
if self.parent is None:
|
|
|
|
|
return self
|
|
|
|
|
component = self.parent
|
|
|
|
|
while component is not self:
|
|
|
|
|
if component.parent is None:
|
|
|
|
|
return component
|
|
|
|
|
component = component.parent
|
|
|
|
|
|
2019-08-01 17:16:12 +02:00
|
|
|
|
def get_parent_type(self):
|
|
|
|
|
"""
|
|
|
|
|
Returns the nearest XSD type that contains the component instance,
|
|
|
|
|
or `None` if the component doesn't have an XSD type parent.
|
|
|
|
|
"""
|
|
|
|
|
component = self.parent
|
|
|
|
|
while component is not self and component is not None:
|
|
|
|
|
if isinstance(component, XsdType):
|
|
|
|
|
return component
|
|
|
|
|
component = component.parent
|
|
|
|
|
|
2017-07-07 12:48:19 +02:00
|
|
|
|
def iter_components(self, xsd_classes=None):
|
2018-08-11 08:03:18 +02:00
|
|
|
|
"""
|
|
|
|
|
Creates an iterator for XSD subcomponents.
|
|
|
|
|
|
|
|
|
|
:param xsd_classes: provide a class or a tuple of classes to iterates over only a \
|
|
|
|
|
specific classes of components.
|
|
|
|
|
"""
|
2017-07-07 12:48:19 +02:00
|
|
|
|
if xsd_classes is None or isinstance(self, xsd_classes):
|
|
|
|
|
yield self
|
|
|
|
|
|
2018-08-11 08:03:18 +02:00
|
|
|
|
def iter_ancestors(self, xsd_classes=None):
|
|
|
|
|
"""
|
|
|
|
|
Creates an iterator for XSD ancestor components, schema excluded. Stops when the component
|
|
|
|
|
is global or if the ancestor is not an instance of the specified class/classes.
|
|
|
|
|
|
|
|
|
|
:param xsd_classes: provide a class or a tuple of classes to iterates over only a \
|
|
|
|
|
specific classes of components.
|
|
|
|
|
"""
|
|
|
|
|
ancestor = self
|
|
|
|
|
while True:
|
|
|
|
|
ancestor = ancestor.parent
|
|
|
|
|
if ancestor is None:
|
|
|
|
|
break
|
|
|
|
|
elif xsd_classes is None or isinstance(ancestor, xsd_classes):
|
|
|
|
|
yield ancestor
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
|
2018-08-24 19:39:45 +02:00
|
|
|
|
def tostring(self, indent='', max_lines=None, spaces_for_tab=4):
|
2017-07-07 12:48:19 +02:00
|
|
|
|
"""
|
2018-07-26 13:43:47 +02:00
|
|
|
|
Returns the XML elements that declare or define the component as a string.
|
2017-07-07 12:48:19 +02:00
|
|
|
|
"""
|
2018-08-27 15:05:12 +02:00
|
|
|
|
if self.elem is None:
|
|
|
|
|
return str(None) # Incomplete component
|
|
|
|
|
return etree_tostring(self.schema_elem, self.namespaces, indent, max_lines, spaces_for_tab)
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class XsdAnnotation(XsdComponent):
|
|
|
|
|
"""
|
2019-08-19 13:52:30 +02:00
|
|
|
|
Class for XSD *annotation* definitions.
|
|
|
|
|
|
2019-08-24 23:39:20 +02:00
|
|
|
|
:ivar appinfo: a list containing the xs:appinfo children.
|
|
|
|
|
:ivar documentation: a list containing the xs:documentation children.
|
2019-08-19 13:52:30 +02:00
|
|
|
|
|
|
|
|
|
.. <annotation
|
|
|
|
|
id = ID
|
|
|
|
|
{any attributes with non-schema namespace . . .}>
|
|
|
|
|
Content: (appinfo | documentation)*
|
|
|
|
|
</annotation>
|
|
|
|
|
|
|
|
|
|
.. <appinfo
|
|
|
|
|
source = anyURI
|
|
|
|
|
{any attributes with non-schema namespace . . .}>
|
|
|
|
|
Content: ({any})*
|
|
|
|
|
</appinfo>
|
|
|
|
|
|
|
|
|
|
.. <documentation
|
|
|
|
|
source = anyURI
|
|
|
|
|
xml:lang = language
|
|
|
|
|
{any attributes with non-schema namespace . . .}>
|
|
|
|
|
Content: ({any})*
|
|
|
|
|
</documentation>
|
2017-07-07 12:48:19 +02:00
|
|
|
|
"""
|
2019-07-30 21:06:54 +02:00
|
|
|
|
_ADMITTED_TAGS = {XSD_ANNOTATION}
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def built(self):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _parse(self):
|
2018-08-07 23:29:47 +02:00
|
|
|
|
del self.errors[:]
|
2017-07-07 12:48:19 +02:00
|
|
|
|
self.appinfo = []
|
|
|
|
|
self.documentation = []
|
|
|
|
|
for child in self.elem:
|
2018-10-08 15:32:50 +02:00
|
|
|
|
if child.tag == XSD_APPINFO:
|
2017-07-07 12:48:19 +02:00
|
|
|
|
for key in child.attrib:
|
|
|
|
|
if key != 'source':
|
2018-08-07 17:57:35 +02:00
|
|
|
|
self.parse_error("wrong attribute %r for appinfo declaration." % key)
|
2017-07-07 12:48:19 +02:00
|
|
|
|
self.appinfo.append(child)
|
2018-10-08 15:32:50 +02:00
|
|
|
|
elif child.tag == XSD_DOCUMENTATION:
|
2017-07-07 12:48:19 +02:00
|
|
|
|
for key in child.attrib:
|
|
|
|
|
if key not in ['source', XML_LANG]:
|
2018-08-07 17:57:35 +02:00
|
|
|
|
self.parse_error("wrong attribute %r for documentation declaration." % key)
|
2017-07-07 12:48:19 +02:00
|
|
|
|
self.documentation.append(child)
|
|
|
|
|
|
|
|
|
|
|
2018-04-05 09:28:49 +02:00
|
|
|
|
class XsdType(XsdComponent):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Common base class for XSD types."""
|
|
|
|
|
|
2019-03-28 14:08:36 +01:00
|
|
|
|
abstract = False
|
2019-10-12 21:19:01 +02:00
|
|
|
|
block = None
|
2018-04-05 12:28:04 +02:00
|
|
|
|
base_type = None
|
|
|
|
|
derivation = None
|
2019-04-07 11:50:22 +02:00
|
|
|
|
redefine = None
|
2019-03-02 12:27:03 +01:00
|
|
|
|
_final = None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def final(self):
|
|
|
|
|
return self.schema.final_default if self._final is None else self._final
|
2018-04-05 12:28:04 +02:00
|
|
|
|
|
2018-04-05 09:50:44 +02:00
|
|
|
|
@property
|
|
|
|
|
def built(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2019-09-21 09:11:58 +02:00
|
|
|
|
@property
|
|
|
|
|
def content_type_label(self):
|
|
|
|
|
if self.is_empty():
|
|
|
|
|
return 'empty'
|
|
|
|
|
elif self.has_simple_content():
|
|
|
|
|
return 'simple'
|
|
|
|
|
elif self.is_element_only():
|
|
|
|
|
return 'element-only'
|
|
|
|
|
elif self.has_mixed_content():
|
|
|
|
|
return 'mixed'
|
|
|
|
|
else:
|
|
|
|
|
return 'unknown'
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def root_type(self):
|
|
|
|
|
"""The root type of the type definition hierarchy. Is itself for a root type."""
|
|
|
|
|
if self.base_type is None:
|
|
|
|
|
return self # Note that a XsdUnion type is always considered a root type
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if self.base_type.is_simple():
|
|
|
|
|
return self.base_type.primitive_type
|
|
|
|
|
else:
|
|
|
|
|
return self.base_type.content_type.primitive_type
|
|
|
|
|
except AttributeError:
|
|
|
|
|
# The type has complex or XsdList content
|
|
|
|
|
return self.base_type
|
|
|
|
|
|
2018-04-05 09:28:49 +02:00
|
|
|
|
@staticmethod
|
|
|
|
|
def is_simple():
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Returns `True` if the instance is a simpleType, `False` otherwise."""
|
2017-07-07 12:48:19 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-04-05 09:28:49 +02:00
|
|
|
|
@staticmethod
|
|
|
|
|
def is_complex():
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Returns `True` if the instance is a complexType, `False` otherwise."""
|
2018-04-05 09:28:49 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-05-17 11:07:07 +02:00
|
|
|
|
@staticmethod
|
|
|
|
|
def is_atomic():
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Returns `True` if the instance is an atomic simpleType, `False` otherwise."""
|
2019-10-11 13:26:17 +02:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def is_datetime():
|
|
|
|
|
"""
|
|
|
|
|
Returns `True` if the instance is a datetime/duration XSD builtin-type, `False` otherwise.
|
|
|
|
|
"""
|
|
|
|
|
return False
|
2018-05-17 11:07:07 +02:00
|
|
|
|
|
2018-04-05 09:28:49 +02:00
|
|
|
|
def is_empty(self):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Returns `True` if the instance has an empty value or content, `False` otherwise."""
|
2018-04-05 09:28:49 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def is_emptiable(self):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""Returns `True` if the instance has an emptiable value or content, `False` otherwise."""
|
2018-04-05 09:28:49 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-04-05 12:28:04 +02:00
|
|
|
|
def has_simple_content(self):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""
|
|
|
|
|
Returns `True` if the instance is a simpleType or a complexType with simple
|
|
|
|
|
content, `False` otherwise.
|
|
|
|
|
"""
|
2018-04-05 09:28:49 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-04-05 12:28:04 +02:00
|
|
|
|
def has_mixed_content(self):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""
|
|
|
|
|
Returns `True` if the instance is a complexType with mixed content, `False` otherwise.
|
|
|
|
|
"""
|
2018-04-05 12:28:04 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def is_element_only(self):
|
2019-08-19 13:52:30 +02:00
|
|
|
|
"""
|
|
|
|
|
Returns `True` if the instance is a complexType with element-only content, `False` otherwise.
|
|
|
|
|
"""
|
2017-07-07 12:48:19 +02:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2018-04-05 12:28:04 +02:00
|
|
|
|
def is_derived(self, other, derivation=None):
|
2019-04-15 18:47:51 +02:00
|
|
|
|
raise NotImplementedError
|
2018-04-05 12:28:04 +02:00
|
|
|
|
|
2019-10-12 21:19:01 +02:00
|
|
|
|
def is_blocked(self, xsd_element):
|
|
|
|
|
"""
|
|
|
|
|
Returns `True` if the base type derivation is blocked, `False` otherwise.
|
|
|
|
|
"""
|
|
|
|
|
xsd_type = xsd_element.type
|
|
|
|
|
if self is xsd_type:
|
2019-09-21 09:11:58 +02:00
|
|
|
|
return False
|
2019-10-12 21:19:01 +02:00
|
|
|
|
|
|
|
|
|
block = ('%s %s' % (xsd_element.block, xsd_type.block)).strip()
|
|
|
|
|
if not block:
|
2019-09-21 09:11:58 +02:00
|
|
|
|
return False
|
2019-10-12 21:19:01 +02:00
|
|
|
|
block = {x for x in block.split() if x in ('extension', 'restriction')}
|
|
|
|
|
|
|
|
|
|
return any(self.is_derived(xsd_type, derivation) for derivation in block)
|
2019-09-21 09:11:58 +02:00
|
|
|
|
|
2019-09-17 16:37:16 +02:00
|
|
|
|
def is_dynamic_consistent(self, other):
|
2019-09-18 23:24:41 +02:00
|
|
|
|
return self.is_derived(other) or hasattr(other, 'member_types') and \
|
|
|
|
|
any(self.is_derived(mt) for mt in other.member_types)
|
2019-09-17 16:37:16 +02:00
|
|
|
|
|
2019-03-26 07:32:44 +01:00
|
|
|
|
def is_key(self):
|
|
|
|
|
return self.name == XSD_ID or self.is_derived(self.maps.types[XSD_ID])
|
|
|
|
|
|
2019-08-26 08:10:58 +02:00
|
|
|
|
def text_decode(self, text):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2017-07-07 12:48:19 +02:00
|
|
|
|
|
2018-08-06 16:48:29 +02:00
|
|
|
|
class ValidationMixin(object):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-08-06 16:48:29 +02:00
|
|
|
|
Mixin for implementing XML data validators/decoders. A derived class must implement the
|
2017-12-14 13:38:23 +01:00
|
|
|
|
methods `iter_decode` and `iter_encode`.
|
|
|
|
|
"""
|
2018-08-27 15:05:12 +02:00
|
|
|
|
def validate(self, source, use_defaults=True, namespaces=None):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-01-11 10:59:43 +01:00
|
|
|
|
Validates an XML data against the XSD schema/component instance.
|
|
|
|
|
|
2018-06-27 16:29:59 +02:00
|
|
|
|
:param source: the source of XML data. For a schema can be a path \
|
2018-01-11 10:59:43 +01:00
|
|
|
|
to a file or an URI of a resource or an opened file-like object or an Element Tree \
|
|
|
|
|
instance or a string containing XML data. For other XSD components can be a string \
|
|
|
|
|
for an attribute or a simple type validators, or an ElementTree's Element otherwise.
|
|
|
|
|
:param use_defaults: indicates whether to use default values for filling missing data.
|
2018-08-27 15:05:12 +02:00
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2018-01-11 10:59:43 +01:00
|
|
|
|
:raises: :exc:`XMLSchemaValidationError` if XML *data* instance is not a valid.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-08-27 15:05:12 +02:00
|
|
|
|
for error in self.iter_errors(source, use_defaults=use_defaults, namespaces=namespaces):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
raise error
|
2017-07-17 10:40:36 +02:00
|
|
|
|
|
2019-06-05 06:43:53 +02:00
|
|
|
|
def is_valid(self, source, use_defaults=True, namespaces=None):
|
2018-08-27 15:05:12 +02:00
|
|
|
|
"""
|
|
|
|
|
Like :meth:`validate` except that do not raises an exception but returns ``True`` if
|
|
|
|
|
the XML document is valid, ``False`` if it's invalid.
|
|
|
|
|
|
|
|
|
|
:param source: the source of XML data. For a schema can be a path \
|
|
|
|
|
to a file or an URI of a resource or an opened file-like object or an Element Tree \
|
|
|
|
|
instance or a string containing XML data. For other XSD components can be a string \
|
|
|
|
|
for an attribute or a simple type validators, or an ElementTree's Element otherwise.
|
|
|
|
|
:param use_defaults: indicates whether to use default values for filling missing data.
|
2019-06-05 06:43:53 +02:00
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2018-08-27 15:05:12 +02:00
|
|
|
|
"""
|
2019-06-05 06:43:53 +02:00
|
|
|
|
error = next(self.iter_errors(source, use_defaults=use_defaults, namespaces=namespaces), None)
|
2018-08-27 15:05:12 +02:00
|
|
|
|
return error is None
|
|
|
|
|
|
2019-06-05 06:43:53 +02:00
|
|
|
|
def iter_errors(self, source, use_defaults=True, namespaces=None):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-01-11 10:59:43 +01:00
|
|
|
|
Creates an iterator for the errors generated by the validation of an XML data
|
|
|
|
|
against the XSD schema/component instance.
|
|
|
|
|
|
2018-06-27 16:29:59 +02:00
|
|
|
|
:param source: the source of XML data. For a schema can be a path \
|
2018-01-11 10:59:43 +01:00
|
|
|
|
to a file or an URI of a resource or an opened file-like object or an Element Tree \
|
|
|
|
|
instance or a string containing XML data. For other XSD components can be a string \
|
|
|
|
|
for an attribute or a simple type validators, or an ElementTree's Element otherwise.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
:param use_defaults: Use schema's default values for filling missing data.
|
2018-08-27 15:05:12 +02:00
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2019-06-05 06:43:53 +02:00
|
|
|
|
for result in self.iter_decode(source, use_defaults=use_defaults, namespaces=namespaces):
|
2018-06-28 13:52:21 +02:00
|
|
|
|
if isinstance(result, XMLSchemaValidationError):
|
|
|
|
|
yield result
|
|
|
|
|
else:
|
|
|
|
|
del result
|
2017-07-17 10:40:36 +02:00
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
def decode(self, source, validation='strict', **kwargs):
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2019-06-18 17:20:13 +02:00
|
|
|
|
Decodes XML data.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
:param source: the XML data. Can be a string for an attribute or for a simple \
|
|
|
|
|
type components or a dictionary for an attribute group or an ElementTree's \
|
|
|
|
|
Element for other components.
|
|
|
|
|
:param validation: the validation mode. Can be 'lax', 'strict' or 'skip.
|
|
|
|
|
:param kwargs: optional keyword arguments for the method :func:`iter_decode`.
|
2018-07-26 17:47:00 +02:00
|
|
|
|
:return: a dictionary like object if the XSD component is an element, a \
|
2017-12-14 13:38:23 +01:00
|
|
|
|
group or a complex type; a list if the XSD component is an attribute group; \
|
2018-07-26 15:55:47 +02:00
|
|
|
|
a simple data type object otherwise. If *validation* argument is 'lax' a 2-items \
|
|
|
|
|
tuple is returned, where the first item is the decoded object and the second item \
|
|
|
|
|
is a list containing the errors.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
:raises: :exc:`XMLSchemaValidationError` if the object is not decodable by \
|
2018-05-18 12:37:33 +02:00
|
|
|
|
the XSD component, or also if it's invalid when ``validation='strict'`` is provided.
|
2017-12-14 13:38:23 +01:00
|
|
|
|
"""
|
2018-06-07 23:30:41 +02:00
|
|
|
|
if validation not in XSD_VALIDATION_MODES:
|
|
|
|
|
raise XMLSchemaValueError("validation argument can be 'strict', 'lax' or 'skip': %r" % validation)
|
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
errors = []
|
|
|
|
|
for result in self.iter_decode(source, validation, **kwargs):
|
|
|
|
|
if not isinstance(result, XMLSchemaValidationError):
|
|
|
|
|
return (result, errors) if validation == 'lax' else result
|
|
|
|
|
elif validation == 'strict':
|
|
|
|
|
raise result
|
2018-07-26 15:55:47 +02:00
|
|
|
|
elif validation == 'lax':
|
2019-06-18 17:20:13 +02:00
|
|
|
|
errors.append(result)
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
def encode(self, obj, validation='strict', **kwargs):
|
2018-05-18 12:37:33 +02:00
|
|
|
|
"""
|
2019-06-18 17:20:13 +02:00
|
|
|
|
Encodes data to XML.
|
2018-05-18 12:37:33 +02:00
|
|
|
|
|
2018-06-28 13:52:21 +02:00
|
|
|
|
:param obj: the data to be encoded to XML.
|
2019-06-18 17:20:13 +02:00
|
|
|
|
:param validation: the validation mode. Can be 'lax', 'strict' or 'skip.
|
|
|
|
|
:param kwargs: optional keyword arguments for the method :func:`iter_encode`.
|
2018-05-18 12:37:33 +02:00
|
|
|
|
:return: An element tree's Element if the original data is a structured data or \
|
2018-07-26 15:55:47 +02:00
|
|
|
|
a string if it's simple type datum. If *validation* argument is 'lax' a 2-items \
|
|
|
|
|
tuple is returned, where the first item is the encoded object and the second item \
|
|
|
|
|
is a list containing the errors.
|
2019-06-18 17:20:13 +02:00
|
|
|
|
:raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the XSD \
|
|
|
|
|
component, or also if it's invalid when ``validation='strict'`` is provided.
|
2018-05-18 12:37:33 +02:00
|
|
|
|
"""
|
2018-06-07 23:30:41 +02:00
|
|
|
|
if validation not in XSD_VALIDATION_MODES:
|
|
|
|
|
raise XMLSchemaValueError("validation argument can be 'strict', 'lax' or 'skip': %r" % validation)
|
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
errors = []
|
|
|
|
|
for result in self.iter_encode(obj, validation=validation, **kwargs):
|
|
|
|
|
if not isinstance(result, XMLSchemaValidationError):
|
|
|
|
|
return (result, errors) if validation == 'lax' else result
|
|
|
|
|
elif validation == 'strict':
|
|
|
|
|
raise result
|
2018-07-26 15:55:47 +02:00
|
|
|
|
elif validation == 'lax':
|
2019-06-18 17:20:13 +02:00
|
|
|
|
errors.append(result)
|
2017-12-14 13:38:23 +01:00
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
def iter_decode(self, source, validation='lax', **kwargs):
|
2018-07-26 17:47:00 +02:00
|
|
|
|
"""
|
|
|
|
|
Creates an iterator for decoding an XML source to a Python object.
|
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
:param source: the XML data source.
|
2018-07-26 17:47:00 +02:00
|
|
|
|
:param validation: the validation mode. Can be 'lax', 'strict' or 'skip.
|
|
|
|
|
:param kwargs: keyword arguments for the decoder API.
|
|
|
|
|
:return: Yields a decoded object, eventually preceded by a sequence of \
|
|
|
|
|
validation or decoding errors.
|
|
|
|
|
"""
|
2017-12-14 13:38:23 +01:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2019-06-18 17:20:13 +02:00
|
|
|
|
def iter_encode(self, obj, validation='lax', **kwargs):
|
2018-07-26 17:47:00 +02:00
|
|
|
|
"""
|
|
|
|
|
Creates an iterator for Encode data to an Element.
|
|
|
|
|
|
|
|
|
|
:param obj: The data that has to be encoded.
|
|
|
|
|
:param validation: The validation mode. Can be 'lax', 'strict' or 'skip'.
|
|
|
|
|
:param kwargs: keyword arguments for the encoder API.
|
|
|
|
|
:return: Yields an Element, eventually preceded by a sequence of validation \
|
|
|
|
|
or encoding errors.
|
|
|
|
|
"""
|
2017-12-14 13:38:23 +01:00
|
|
|
|
raise NotImplementedError
|
2018-08-07 17:57:35 +02:00
|
|
|
|
|
2018-11-15 10:54:11 +01:00
|
|
|
|
def validation_error(self, validation, error, obj=None, source=None, namespaces=None, **_kwargs):
|
2018-08-07 17:57:35 +02:00
|
|
|
|
"""
|
2018-08-26 12:13:31 +02:00
|
|
|
|
Helper method for generating and updating validation errors. Incompatible with 'skip'
|
|
|
|
|
validation mode. Il validation mode is 'lax' returns the error, otherwise raises the error.
|
2018-08-07 17:57:35 +02:00
|
|
|
|
|
2018-08-11 08:03:18 +02:00
|
|
|
|
:param validation: an error-compatible validation mode: can be 'lax' or 'strict'.
|
2018-08-26 12:13:31 +02:00
|
|
|
|
:param error: an error instance or the detailed reason of failed validation.
|
2018-08-07 17:57:35 +02:00
|
|
|
|
:param obj: the instance related to the error.
|
|
|
|
|
:param source: the XML resource related to the validation process.
|
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2018-11-15 10:54:11 +01:00
|
|
|
|
:param _kwargs: keyword arguments of the validation process that are not used.
|
2018-08-07 17:57:35 +02:00
|
|
|
|
"""
|
2018-08-26 12:13:31 +02:00
|
|
|
|
if not isinstance(error, XMLSchemaValidationError):
|
|
|
|
|
error = XMLSchemaValidationError(self, obj, error, source, namespaces)
|
2018-08-11 08:03:18 +02:00
|
|
|
|
else:
|
2018-08-26 12:13:31 +02:00
|
|
|
|
if error.namespaces is None and namespaces is not None:
|
|
|
|
|
error.namespaces = namespaces
|
|
|
|
|
if error.source is None and source is not None:
|
|
|
|
|
error.source = source
|
2019-06-05 08:15:04 +02:00
|
|
|
|
if error.obj is None and obj is not None:
|
|
|
|
|
error.obj = obj
|
|
|
|
|
if error.elem is None and is_etree_element(obj):
|
|
|
|
|
error.elem = obj
|
2018-08-26 12:13:31 +02:00
|
|
|
|
|
|
|
|
|
if validation == 'lax':
|
2018-08-11 08:03:18 +02:00
|
|
|
|
return error
|
2018-08-26 12:13:31 +02:00
|
|
|
|
elif validation == 'strict':
|
|
|
|
|
raise error
|
|
|
|
|
elif validation == 'skip':
|
|
|
|
|
raise XMLSchemaValueError("validation mode 'skip' incompatible with error generation.")
|
|
|
|
|
else:
|
|
|
|
|
raise XMLSchemaValueError("unknown validation mode %r" % validation)
|
2018-08-11 08:03:18 +02:00
|
|
|
|
|
2018-11-15 10:54:11 +01:00
|
|
|
|
def decode_error(self, validation, obj, decoder, reason=None, source=None, namespaces=None, **_kwargs):
|
2018-08-11 08:03:18 +02:00
|
|
|
|
"""
|
|
|
|
|
Helper method for generating decode errors. Incompatible with 'skip' validation mode.
|
|
|
|
|
Il validation mode is 'lax' returns the error, otherwise raises the error.
|
|
|
|
|
|
|
|
|
|
:param validation: an error-compatible validation mode: can be 'lax' or 'strict'.
|
|
|
|
|
:param obj: the not validated XML data.
|
|
|
|
|
:param decoder: the XML data decoder.
|
|
|
|
|
:param reason: the detailed reason of failed validation.
|
|
|
|
|
:param source: the XML resource that contains the error.
|
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2018-11-15 10:54:11 +01:00
|
|
|
|
:param _kwargs: keyword arguments of the validation process that are not used.
|
2018-08-11 08:03:18 +02:00
|
|
|
|
"""
|
|
|
|
|
error = XMLSchemaDecodeError(self, obj, decoder, reason, source, namespaces)
|
2018-08-26 12:13:31 +02:00
|
|
|
|
if validation == 'lax':
|
|
|
|
|
return error
|
|
|
|
|
elif validation == 'strict':
|
2018-08-11 08:03:18 +02:00
|
|
|
|
raise error
|
2018-08-26 12:13:31 +02:00
|
|
|
|
elif validation == 'skip':
|
|
|
|
|
raise XMLSchemaValueError("validation mode 'skip' incompatible with error generation.")
|
2018-08-07 17:57:35 +02:00
|
|
|
|
else:
|
2018-08-26 12:13:31 +02:00
|
|
|
|
raise XMLSchemaValueError("unknown validation mode %r" % validation)
|
2018-08-11 08:03:18 +02:00
|
|
|
|
|
2018-11-15 10:54:11 +01:00
|
|
|
|
def encode_error(self, validation, obj, encoder, reason=None, source=None, namespaces=None, **_kwargs):
|
2018-08-11 08:03:18 +02:00
|
|
|
|
"""
|
|
|
|
|
Helper method for generating encode errors. Incompatible with 'skip' validation mode.
|
|
|
|
|
Il validation mode is 'lax' returns the error, otherwise raises the error.
|
|
|
|
|
|
|
|
|
|
:param validation: an error-compatible validation mode: can be 'lax' or 'strict'.
|
|
|
|
|
:param obj: the not validated XML data.
|
|
|
|
|
:param encoder: the XML encoder.
|
|
|
|
|
:param reason: the detailed reason of failed validation.
|
|
|
|
|
:param source: the XML resource that contains the error.
|
|
|
|
|
:param namespaces: is an optional mapping from namespace prefix to URI.
|
2018-11-15 10:54:11 +01:00
|
|
|
|
:param _kwargs: keyword arguments of the validation process that are not used.
|
2018-08-11 08:03:18 +02:00
|
|
|
|
"""
|
|
|
|
|
error = XMLSchemaEncodeError(self, obj, encoder, reason, source, namespaces)
|
2018-08-26 12:13:31 +02:00
|
|
|
|
if validation == 'lax':
|
|
|
|
|
return error
|
|
|
|
|
elif validation == 'strict':
|
2018-08-11 08:03:18 +02:00
|
|
|
|
raise error
|
2018-08-26 12:13:31 +02:00
|
|
|
|
elif validation == 'skip':
|
|
|
|
|
raise XMLSchemaValueError("validation mode 'skip' incompatible with error generation.")
|
2018-08-11 08:03:18 +02:00
|
|
|
|
else:
|
2018-08-26 12:13:31 +02:00
|
|
|
|
raise XMLSchemaValueError("unknown validation mode %r" % validation)
|
2018-08-11 08:03:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParticleMixin(object):
|
|
|
|
|
"""
|
|
|
|
|
Mixin for objects related to XSD Particle Schema Components:
|
|
|
|
|
|
|
|
|
|
https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#p
|
|
|
|
|
https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#t
|
|
|
|
|
"""
|
2019-03-21 07:14:58 +01:00
|
|
|
|
min_occurs = 1
|
|
|
|
|
max_occurs = 1
|
|
|
|
|
|
2019-04-15 13:07:31 +02:00
|
|
|
|
@property
|
|
|
|
|
def occurs(self):
|
|
|
|
|
return [self.min_occurs, self.max_occurs]
|
|
|
|
|
|
|
|
|
|
def is_emptiable(self):
|
|
|
|
|
return self.min_occurs == 0
|
|
|
|
|
|
|
|
|
|
def is_empty(self):
|
|
|
|
|
return self.max_occurs == 0
|
|
|
|
|
|
|
|
|
|
def is_single(self):
|
|
|
|
|
return self.max_occurs == 1
|
|
|
|
|
|
|
|
|
|
def is_ambiguous(self):
|
|
|
|
|
return self.min_occurs != self.max_occurs
|
|
|
|
|
|
|
|
|
|
def is_univocal(self):
|
|
|
|
|
return self.min_occurs == self.max_occurs
|
|
|
|
|
|
|
|
|
|
def is_missing(self, occurs):
|
|
|
|
|
return not self.is_emptiable() if occurs == 0 else self.min_occurs > occurs
|
|
|
|
|
|
|
|
|
|
def is_over(self, occurs):
|
|
|
|
|
return self.max_occurs is not None and self.max_occurs <= occurs
|
|
|
|
|
|
|
|
|
|
def has_occurs_restriction(self, other):
|
|
|
|
|
if self.min_occurs == self.max_occurs == 0:
|
|
|
|
|
return True
|
|
|
|
|
elif self.min_occurs < other.min_occurs:
|
|
|
|
|
return False
|
|
|
|
|
elif other.max_occurs is None:
|
|
|
|
|
return True
|
|
|
|
|
elif self.max_occurs is None:
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
return self.max_occurs <= other.max_occurs
|
|
|
|
|
|
2019-08-23 08:43:41 +02:00
|
|
|
|
@property
|
|
|
|
|
def effective_min_occurs(self):
|
|
|
|
|
return self.min_occurs
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def effective_max_occurs(self):
|
|
|
|
|
return self.max_occurs
|
|
|
|
|
|
2019-04-15 13:07:31 +02:00
|
|
|
|
###
|
|
|
|
|
# Methods used by XSD components
|
2018-08-11 08:03:18 +02:00
|
|
|
|
def parse_error(self, *args, **kwargs):
|
2019-04-15 13:07:31 +02:00
|
|
|
|
"""Helper method overridden by XsdValidator.parse_error() in XSD components."""
|
2019-04-09 16:56:28 +02:00
|
|
|
|
raise XMLSchemaParseError(*args)
|
2018-08-11 08:03:18 +02:00
|
|
|
|
|
|
|
|
|
def _parse_particle(self, elem):
|
2019-03-21 07:14:58 +01:00
|
|
|
|
if 'minOccurs' in elem.attrib:
|
|
|
|
|
try:
|
|
|
|
|
min_occurs = int(elem.attrib['minOccurs'])
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
self.parse_error("minOccurs value is not an integer value")
|
|
|
|
|
else:
|
|
|
|
|
if min_occurs < 0:
|
|
|
|
|
self.parse_error("minOccurs value must be a non negative integer")
|
|
|
|
|
else:
|
|
|
|
|
self.min_occurs = min_occurs
|
|
|
|
|
|
|
|
|
|
max_occurs = elem.get('maxOccurs')
|
|
|
|
|
if max_occurs is None:
|
|
|
|
|
if self.min_occurs > 1:
|
|
|
|
|
self.parse_error("minOccurs must be lesser or equal than maxOccurs")
|
|
|
|
|
elif max_occurs == 'unbounded':
|
2018-10-05 07:15:25 +02:00
|
|
|
|
self.max_occurs = None
|
|
|
|
|
else:
|
|
|
|
|
try:
|
2019-03-21 07:14:58 +01:00
|
|
|
|
max_occurs = int(max_occurs)
|
2018-10-05 07:15:25 +02:00
|
|
|
|
except ValueError:
|
2018-08-11 08:03:18 +02:00
|
|
|
|
self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'")
|
2018-10-05 07:15:25 +02:00
|
|
|
|
else:
|
2019-03-21 07:14:58 +01:00
|
|
|
|
if self.min_occurs > max_occurs:
|
|
|
|
|
self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs")
|
|
|
|
|
else:
|
|
|
|
|
self.max_occurs = max_occurs
|