Merge branch 'develop' for updating to release v1.0.11

This commit is contained in:
Davide Brunato 2019-05-05 15:26:41 +02:00
commit d0ddbc4d6d
78 changed files with 4700 additions and 1896 deletions

1
.gitignore vendored
View File

@ -13,4 +13,3 @@ build/
development/
test_cases/
!xmlschema/tests/test_cases/
!xmlschema/unicode_categories.json

View File

@ -2,6 +2,18 @@
CHANGELOG
*********
`v1.0.11`_ (2019-05-05)
=======================
* Added a script for running the W3C XSD test suite.
* Check restrictions and model groups UPA violations
* Model groups splitted between two modules for more focusing on models basics
* Added two new exceptions for model group errors
* More control on imported namespaces
* Added *use_meta* argument to schema classes
* Added *includes* list and *imports* dict to schema classes
* Many fixes for passing the W3C's tests for XSD 1.0 schemas
* Added a test for issue #105 and a fix for issue #103
`v1.0.10`_ (2019-02-25)
=======================
* Fixed Element type mismatch issue when apply *SafeXMLParser* to schema resources
@ -226,3 +238,4 @@ v0.9.6 (2017-05-05)
.. _v1.0.8: https://github.com/brunato/xmlschema/compare/v1.0.7...v1.0.8
.. _v1.0.9: https://github.com/brunato/xmlschema/compare/v1.0.8...v1.0.9
.. _v1.0.10: https://github.com/brunato/xmlschema/compare/v1.0.9...v1.0.10
.. _v1.0.11: https://github.com/brunato/xmlschema/compare/v1.0.10...v1.0.11

View File

@ -33,25 +33,26 @@ Schema level API
.. autoattribute:: tag
.. autoattribute:: id
.. autoattribute:: version
.. autoattribute:: attribute_form_default
.. autoattribute:: element_form_default
.. autoattribute:: block_default
.. autoattribute:: final_default
.. autoattribute:: schema_location
.. autoattribute:: no_namespace_schema_location
.. autoattribute:: target_prefix
.. autoattribute:: default_namespace
.. autoattribute:: base_url
.. autoattribute:: root_elements
.. autoattribute:: builtin_types
.. autoattribute:: root_elements
.. automethod:: create_meta_schema
.. automethod:: create_schema
.. automethod:: create_any_content_group
.. automethod:: create_any_attribute_group
.. automethod:: get_locations
.. automethod:: include_schema
.. automethod:: import_schema
.. automethod:: create_schema
.. automethod:: create_any_content_group
.. automethod:: create_any_attribute_group
.. automethod:: resolve_qname
.. automethod:: iter_globals
.. automethod:: iter_components
.. automethod:: check_schema
.. automethod:: build
@ -59,8 +60,6 @@ Schema level API
.. autoattribute:: validation_attempted
.. autoattribute:: validity
.. autoattribute:: all_errors
.. automethod:: iter_components
.. automethod:: iter_globals
.. automethod:: get_converter
.. automethod:: validate
@ -180,6 +179,8 @@ Errors and exceptions
.. autoexception:: xmlschema.XMLSchemaValidatorError
.. autoexception:: xmlschema.XMLSchemaNotBuiltError
.. autoexception:: xmlschema.XMLSchemaParseError
.. autoexception:: xmlschema.XMLSchemaModelError
.. autoexception:: xmlschema.XMLSchemaModelDepthError
.. autoexception:: xmlschema.XMLSchemaValidationError
.. autoexception:: xmlschema.XMLSchemaDecodeError
.. autoexception:: xmlschema.XMLSchemaEncodeError

View File

@ -62,7 +62,7 @@ author = 'Davide Brunato'
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0.10'
release = '1.0.11'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -136,3 +136,17 @@ or:
.. code-block:: text
tox -- -x
Testing with the W3C XML Schema 1.1 test suite
----------------------------------------------
From release v1.0.11, using the script *test_w3c_suite.py*, you can run also tests based on the
`W3C XML Schema 1.1 test suite <https://github.com/w3c/xsdtests>`_. To run these tests, actually
limited to XSD 1.0 schema tests, clone the W3C repo on the project's parent directory and than
run the script:
.. code-block:: text
git clone https://github.com/w3c/xsdtests.git
python xmlschema/xmlschema/tests/test_w3c_suite.py

View File

@ -115,9 +115,9 @@ The global maps can be accessed through :attr:`XMLSchema.maps` attribute:
'{http://www.w3.org/1999/xlink}locator',
'{http://www.w3.org/1999/xlink}resource',
'{http://www.w3.org/1999/xlink}title',
'{http://www.w3.org/2001/XMLSchema-hasFacetAndProperty}hasFacet',
'{http://www.w3.org/2001/XMLSchema-hasFacetAndProperty}hasProperty',
'{http://www.w3.org/2001/XMLSchema}all']
'{http://www.w3.org/2001/XMLSchema}all',
'{http://www.w3.org/2001/XMLSchema}annotation',
'{http://www.w3.org/2001/XMLSchema}any']
Schema objects include methods for finding XSD elements and attributes in the schema.
Those are methods ot the ElementTree's API, so you can use an XPath expression for
@ -528,3 +528,19 @@ For default this argument has value *'remote'* that means the protection on XML
applied only to data loaded from remote. Other values for this argument can be *'always'*
and *'never'*.
Limit on model groups checking
------------------------------
From release v1.0.11 the model groups of the schemas are checked against restriction violations
and *Unique Particle Attribution* violations.
To avoids XSD model recursion attacks a limit of ``MAX_MODEL_DEPTH = 15`` is set. If this limit
is exceeded an ``XMLSchemaModelDepthError`` is raised, the error is caught and a warning is generated.
If you need to set an higher limit for checking all your groups you can import the library and change
the value in the specific module that processes the model checks:
.. doctest::
>>> import xmlschema
>>> xmlschema.validators.models.MAX_MODEL_DEPTH = 20

View File

@ -1,7 +1,7 @@
# Requirements for setup a development environment for the xmlschema package.
setuptools
tox
elementpath~=1.1.5
elementpath~=1.1.7
lxml
memory_profiler
pathlib2 # For Py27 tests on resources

View File

@ -9,17 +9,43 @@
#
# @author Davide Brunato <brunato@sissa.it>
#
import importlib
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
with open("README.rst") as readme:
long_description = readme.read()
class DevelopCommand(develop):
def run(self):
develop.run(self)
print("Post-develop: create Unicode categories JSON file")
codepoints_module = importlib.import_module('xmlschema.codepoints')
codepoints_module.save_unicode_categories()
class InstallCommand(install):
def run(self):
install.run(self)
print("Post-install: create Unicode categories JSON file")
codepoints_module = importlib.import_module('xmlschema.codepoints')
codepoints_module.save_unicode_categories()
setup(
name='xmlschema',
version='1.0.10',
install_requires=['elementpath~=1.1.5'],
version='1.0.11',
install_requires=['elementpath~=1.1.7'],
packages=['xmlschema'],
include_package_data=True,
cmdclass={
'develop': DevelopCommand,
'install': InstallCommand
},
author='Davide Brunato',
author_email='brunato@sissa.it',
url='https://github.com/brunato/xmlschema',

View File

@ -10,12 +10,12 @@ toxworkdir = {homedir}/.tox/xmlschema
[testenv]
deps =
lxml
elementpath~=1.1.5
elementpath~=1.1.7
commands = python xmlschema/tests/test_all.py {posargs}
[testenv:py27]
deps =
lxml
elementpath~=1.1.5
elementpath~=1.1.7
pathlib2
commands = python xmlschema/tests/test_all.py {posargs}

View File

@ -20,12 +20,13 @@ from .converters import (
from .documents import validate, to_dict, to_json, from_json
from .validators import (
XMLSchemaValidatorError, XMLSchemaParseError, XMLSchemaNotBuiltError, XMLSchemaValidationError,
XMLSchemaDecodeError, XMLSchemaEncodeError, XMLSchemaChildrenValidationError, XMLSchemaIncludeWarning,
XMLSchemaImportWarning, XsdGlobals, XMLSchemaBase, XMLSchema, XMLSchema10
XMLSchemaValidatorError, XMLSchemaParseError, XMLSchemaNotBuiltError, XMLSchemaModelError,
XMLSchemaModelDepthError, XMLSchemaValidationError, XMLSchemaDecodeError, XMLSchemaEncodeError,
XMLSchemaChildrenValidationError, XMLSchemaIncludeWarning, XMLSchemaImportWarning, XsdGlobals,
XMLSchemaBase, XMLSchema, XMLSchema10
)
__version__ = '1.0.10'
__version__ = '1.0.11'
__author__ = "Davide Brunato"
__contact__ = "brunato@sissa.it"
__copyright__ = "Copyright 2016-2019, SISSA"
@ -40,6 +41,7 @@ def XMLSchema_v1_0(*args, **kwargs):
"and will be removed in 1.1 version", DeprecationWarning, stacklevel=2)
return XMLSchema10(*args, **kwargs)
def etree_get_namespaces(*args, **kwargs):
import warnings
warnings.warn("etree_get_namespaces() function name has been replaced by fetch_namespaces() "

View File

@ -16,7 +16,6 @@ from __future__ import unicode_literals
import json
import os
from sys import maxunicode
from collections import defaultdict
from .compat import PY3, unicode_chr, string_base_type, Iterable, MutableSet
from .exceptions import XMLSchemaValueError, XMLSchemaTypeError, XMLSchemaRegexError
@ -135,6 +134,7 @@ def iterparse_character_group(s, expand_ranges=False):
:return: yields integers or couples of integers.
"""
escaped = False
on_range = False
char = None
length = len(s)
string_iter = iter(range(len(s)))
@ -154,19 +154,29 @@ def iterparse_character_group(s, expand_ranges=False):
char = s[k]
yield ord(char)
escaped = False
elif on_range:
char = s[k]
yield ord(char)
on_range = False
else:
# Parse character range
on_range = True
try:
k = next(string_iter)
end_char = s[k]
if end_char == '\\' and (k < length - 1) and s[k + 1] in r'-|.^?*+{}()[]':
k = next(string_iter)
end_char = s[k]
if end_char == '\\' and (k < length - 1):
if s[k+1] in r'-|.^?*+{}()[]':
k = next(string_iter)
end_char = s[k]
elif s[k+1] in r'sSdDiIcCwWpP':
msg = "bad character range '%s-\\%s' at position %d: %r" % (char, s[k+1], k-2, s)
raise XMLSchemaRegexError(msg)
except StopIteration:
msg = "bad character range %r-%r at position %d: %r" % (char, s[-1], k - 2, s)
msg = "bad character range '%s-%s' at position %d: %r" % (char, s[-1], k-2, s)
raise XMLSchemaRegexError(msg)
if ord(char) > ord(end_char):
msg = "bad character range %r-%r at position %d: %r" % (char, end_char, k - 2, s)
msg = "bad character range '%s-%s' at position %d: %r" % (char, end_char, k-2, s)
raise XMLSchemaRegexError(msg)
elif expand_ranges:
for cp in range(ord(char) + 1, ord(end_char) + 1):
@ -176,17 +186,19 @@ def iterparse_character_group(s, expand_ranges=False):
elif s[k] in r'|.^?*+{}()':
if escaped:
escaped = False
on_range = False
char = s[k]
yield ord(char)
elif s[k] in r'[]':
if not escaped and length > 1:
raise XMLSchemaRegexError("bad character %r at position %d" % (s[k], k))
escaped = False
escaped = on_range = False
char = s[k]
yield ord(char)
if k >= length-1 or s[k+1] != '-':
yield ord(char)
elif s[k] == '\\':
if escaped:
escaped = False
escaped = on_range = False
char = '\\'
yield ord(char)
else:
@ -195,8 +207,10 @@ def iterparse_character_group(s, expand_ranges=False):
if escaped:
escaped = False
yield ord('\\')
on_range = False
char = s[k]
yield ord(char)
if k >= length-1 or s[k+1] != '-':
yield ord(char)
if escaped:
yield ord('\\')
@ -226,6 +240,12 @@ class UnicodeSubset(MutableSet):
self._code_points = list()
self.update(args[0])
@classmethod
def fromlist(cls, code_points):
subset = cls()
subset._code_points = sorted(code_points, key=code_point_order)
return subset
@property
def code_points(self):
return self._code_points
@ -470,33 +490,42 @@ class UnicodeSubset(MutableSet):
def get_unicodedata_categories():
"""
Extracts Unicode categories information from unicodedata library. Each category is
represented with a list containing distinct code points and code points intervals.
represented with an ordered list containing code points and code point ranges.
:return: a `defaultdict` dictionary with category names as keys and lists as values.
:return: a dictionary with category names as keys and lists as values.
"""
import unicodedata
from unicodedata import category
def append_code_points():
diff = cp - start_cp
if diff == 1:
categories[last_category].append(start_cp)
elif diff == 2:
categories[last_category].append(start_cp)
categories[last_category].append(start_cp + 1)
else:
categories[last_category].append((start_cp, cp))
categories = {k: [] for k in (
'C', 'Cc', 'Cf', 'Cs', 'Co', 'Cn',
'L', 'Lu', 'Ll', 'Lt', 'Lm', 'Lo',
'M', 'Mn', 'Mc', 'Me',
'N', 'Nd', 'Nl', 'No',
'P', 'Pc', 'Pd', 'Ps', 'Pe', 'Pi', 'Pf', 'Po',
'S', 'Sm', 'Sc', 'Sk', 'So',
'Z', 'Zs', 'Zl', 'Zp'
)}
categories = defaultdict(list)
last_category = 'Cc'
start_cp = cp = 0
for cp, category in enumerate(map(unicodedata.category, map(unicode_chr, range(maxunicode + 1)))):
if category != last_category:
append_code_points()
last_category = category
start_cp = cp
minor_category = 'Cc'
start_cp, next_cp = 0, 1
for cp in range(maxunicode + 1):
if category(unicode_chr(cp)) != minor_category:
if cp > next_cp:
categories[minor_category].append((start_cp, cp))
categories[minor_category[0]].append(categories[minor_category][-1])
else:
categories[minor_category].append(start_cp)
categories[minor_category[0]].append(start_cp)
minor_category = category(unicode_chr(cp))
start_cp, next_cp = cp, cp + 1
else:
cp += 1
append_code_points()
if next_cp == maxunicode + 1:
categories[minor_category].append(start_cp)
categories[minor_category[0]].append(start_cp)
else:
categories[minor_category].append((start_cp, maxunicode + 1))
categories[minor_category[0]].append(categories[minor_category][-1])
return categories
@ -511,6 +540,7 @@ def save_unicode_categories(filename=None):
if filename is None:
filename = os.path.join(os.path.dirname(__file__), 'unicode_categories.json')
print("Saving Unicode categories to %r" % filename)
with open(filename, 'w') as fp:
json.dump(get_unicodedata_categories(), fp)
@ -529,28 +559,22 @@ def build_unicode_categories(filename=None):
categories = get_unicodedata_categories() # for Python 2.7
else:
if filename is None:
filename = os.path.join(os.path.dirname(__file__), 'unicode_categoriesxxx.json')
filename = os.path.join(os.path.dirname(__file__), 'unicode_categories.json')
try:
with open(filename, 'r') as fp:
categories = json.load(fp)
except (IOError, SystemError, ValueError):
categories = get_unicodedata_categories()
else:
if any(not v for v in categories):
categories = get_unicodedata_categories()
# Add general categories code points
categories['C'] = categories['Cc'] + categories['Cf'] + categories['Cs'] + categories['Co'] + categories['Cn']
categories['L'] = categories['Lu'] + categories['Ll'] + categories['Lt'] + categories['Lm'] + categories['Lo']
categories['M'] = categories['Mn'] + categories['Mc'] + categories['Me']
categories['N'] = categories['Nd'] + categories['Nl'] + categories['No']
categories['P'] = categories['Pc'] + categories['Pd'] + categories['Ps'] + \
categories['Pe'] + categories['Pi'] + categories['Pf'] + categories['Po']
categories['S'] = categories['Sm'] + categories['Sc'] + categories['Sk'] + categories['So']
categories['Z'] = categories['Zs'] + categories['Zl'] + categories['Zp']
return {k: UnicodeSubset(v) for k, v in categories.items()}
return {k: UnicodeSubset.fromlist(v) for k, v in categories.items()}
UNICODE_CATEGORIES = build_unicode_categories()
UNICODE_BLOCKS = {
'IsBasicLatin': UnicodeSubset('\u0000-\u007F'),
'IsLatin-1Supplement': UnicodeSubset('\u0080-\u00FF'),

View File

@ -21,6 +21,7 @@ try:
from urllib.error import URLError
from io import StringIO, BytesIO
from collections.abc import Iterable, MutableSet, Sequence, MutableSequence, Mapping, MutableMapping
from functools import lru_cache
except ImportError:
# Python 2.7 imports
from urllib import pathname2url
@ -29,6 +30,18 @@ except ImportError:
from StringIO import StringIO # the io.StringIO accepts only unicode type
from io import BytesIO
from collections import Iterable, MutableSet, Sequence, MutableSequence, Mapping, MutableMapping
from functools import wraps
def lru_cache(maxsize=128, typed=False):
"""
A fake lru_cache decorator function for Python 2.7 compatibility until support ends.
"""
def lru_cache_decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
return lru_cache_decorator
PY3 = sys.version_info[0] == 3

View File

@ -16,7 +16,6 @@ from .compat import URLError
class XMLSchemaException(Exception):
"""The base exception that let you catch all the errors generated by the library."""
pass
class XMLSchemaOSError(XMLSchemaException, OSError):
@ -53,9 +52,8 @@ class XMLSchemaURLError(XMLSchemaException, URLError):
class XMLSchemaRegexError(XMLSchemaException, ValueError):
"""Raised when an error is found when parsing an XML Schema regular expression."""
pass
class XMLSchemaWarning(Warning):
"""Base warning class for the XMLSchema package."""
pass

View File

@ -9,14 +9,14 @@
# @author Davide Brunato <brunato@sissa.it>
#
"""
This module contains various helper functions for XML/XSD processing and parsing.
This module contains various helper functions and classes.
"""
import re
from .exceptions import XMLSchemaValueError, XMLSchemaTypeError, XMLSchemaKeyError
from .qnames import XSD_ANNOTATION
XSD_FINAL_ATTRIBUTE_VALUES = {'restriction', 'extension', 'list', 'union'}
NAMESPACE_PATTERN = re.compile(r'{([^}]*)}')
@ -29,8 +29,8 @@ def get_namespace(name):
def get_qname(uri, name):
"""
Returns a fully qualified name from URI and local part. If any argument has boolean value
`False` or if the name is already a fully qualified name, returns the *name* argument.
Returns an expanded QName from URI and local part. If any argument has boolean value
`False` or if the name is already an expanded QName, returns the *name* argument.
:param uri: namespace URI
:param name: local or qualified name
@ -44,10 +44,10 @@ def get_qname(uri, name):
def local_name(qname):
"""
Return the local part of a qualified name. If the name is `None` or empty
Return the local part of an expanded QName. If the name is `None` or empty
returns the *name* argument.
:param qname: QName or universal name formatted string, or `None`.
:param qname: an expanded QName or a local name.
"""
try:
if qname[0] != '{':
@ -63,40 +63,6 @@ def local_name(qname):
raise XMLSchemaTypeError("required a string-like object or None! %r" % qname)
def prefixed_to_qname(name, namespaces):
"""
Transforms a prefixed name into a fully qualified name using a namespace map. Returns
the *name* argument if it's not a prefixed name or if it has boolean value `False`.
:param name: a local name or a prefixed name or a fully qualified name or `None`.
:param namespaces: a map from prefixes to namespace URIs.
:return: string with a FQN or a local name or the name argument.
"""
if not name or name[0] == '{':
return name
try:
prefix, name = name.split(':')
except ValueError:
if ':' in name:
raise XMLSchemaValueError("wrong format for reference name %r" % name)
try:
uri = namespaces['']
except KeyError:
return name
else:
return '{%s}%s' % (uri, name) if uri else name
else:
if not prefix or not name:
raise XMLSchemaValueError("wrong format for reference name %r" % name)
try:
uri = namespaces[prefix]
except KeyError:
raise XMLSchemaValueError("prefix %r not found in namespace map" % prefix)
else:
return '{%s}%s' % (uri, name) if uri else name
def qname_to_prefixed(qname, namespaces):
"""
Transforms a fully qualified name into a prefixed name using a namespace map. Returns the
@ -212,7 +178,7 @@ def get_xml_bool_attribute(elem, attribute, default=None):
raise XMLSchemaTypeError("an XML boolean value is required for attribute %r" % attribute)
def get_xsd_derivation_attribute(elem, attribute, values):
def get_xsd_derivation_attribute(elem, attribute, values=None):
"""
Get a derivation attribute (maybe 'block', 'blockDefault', 'final' or 'finalDefault')
checking the items with the values arguments. Returns a string.
@ -222,10 +188,69 @@ def get_xsd_derivation_attribute(elem, attribute, values):
:param values: sequence of admitted values when the attribute value is not '#all'.
:return: a string.
"""
value = elem.get(attribute, '')
value = elem.get(attribute)
if value is None:
return ''
if values is None:
values = XSD_FINAL_ATTRIBUTE_VALUES
items = value.split()
if len(items) == 1 and items[0] == '#all':
return ' '.join(values)
elif not all([s in values for s in items]):
raise XMLSchemaValueError("wrong value %r for attribute %r." % (value, attribute))
return value
def get_xsd_form_attribute(elem, attribute):
"""
Get an XSD form attribute, checking the value. If the attribute is missing returns `None`
:param elem: the Element instance.
:param attribute: the attribute name (maybe 'form', or 'elementFormDefault' or 'attributeFormDefault').
:return: a string.
"""
value = elem.get(attribute)
if value is None:
return
elif value not in ('qualified', 'unqualified'):
raise XMLSchemaValueError(
"wrong value %r for attribute %r, it must be 'qualified' or 'unqualified'." % (value, attribute)
)
return value
class ParticleCounter(object):
"""
An helper class for counting total min/max occurrences of XSD particles.
"""
def __init__(self):
self.min_occurs = self.max_occurs = 0
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.min_occurs, self.max_occurs)
def __add__(self, other):
self.min_occurs += other.min_occurs
if self.max_occurs is not None:
if other.max_occurs is None:
self.max_occurs = None
else:
self.max_occurs += other.max_occurs
return self
def __mul__(self, other):
self.min_occurs *= other.min_occurs
if self.max_occurs is None:
if other.max_occurs == 0:
self.max_occurs = 0
elif other.max_occurs is None:
if self.max_occurs != 0:
self.max_occurs = None
else:
self.max_occurs *= other.max_occurs
return self
def reset(self):
self.min_occurs = self.max_occurs = 0

View File

@ -57,7 +57,7 @@ class NamespaceResourcesMap(MutableMapping):
def __setitem__(self, uri, value):
if isinstance(value, list):
self._store[uri] = value
self._store[uri] = value[:]
else:
try:
self._store[uri].append(value)

View File

@ -99,10 +99,11 @@ XSD_TOTAL_DIGITS = xsd_qname('totalDigits')
XSD_FRACTION_DIGITS = xsd_qname('fractionDigits')
# XSD 1.1 elements
XSD_OPEN_CONTENT = xsd_qname('openContent') # open content model
XSD_ALTERNATIVE = xsd_qname('alternative') # conditional type assignment
XSD_ASSERT = xsd_qname('assert') # complex type assertions
XSD_ASSERTION = xsd_qname('assertion') # facets
XSD_OPEN_CONTENT = xsd_qname('openContent') # open content model
XSD_DEFAULT_OPEN_CONTENT = xsd_qname('defaultOpenContent') # default open content model (schema level)
XSD_ALTERNATIVE = xsd_qname('alternative') # conditional type assignment
XSD_ASSERT = xsd_qname('assert') # complex type assertions
XSD_ASSERTION = xsd_qname('assertion') # facets
XSD_EXPLICIT_TIMEZONE = xsd_qname('explicitTimezone')
# Identity constraints
@ -195,5 +196,5 @@ __all__ = [
'XSD_NON_NEGATIVE_INTEGER', 'XSD_POSITIVE_INTEGER', 'XSD_UNSIGNED_LONG', 'XSD_UNSIGNED_INT',
'XSD_UNSIGNED_SHORT', 'XSD_UNSIGNED_BYTE', 'XSD_NON_POSITIVE_INTEGER', 'XSD_NEGATIVE_INTEGER',
'XSD_IDREFS', 'XSD_ENTITIES', 'XSD_NMTOKENS', 'XSD_DATE_TIME_STAMP', 'XSD_DAY_TIME_DURATION',
'XSD_YEAR_MONTH_DURATION',
'XSD_YEAR_MONTH_DURATION', 'XSD_DEFAULT_OPEN_CONTENT', 'XSD_OVERRIDE',
]

View File

@ -19,6 +19,11 @@ from .compat import PY3, unicode_type, string_base_type, MutableSet
from .exceptions import XMLSchemaValueError, XMLSchemaRegexError
from .codepoints import UNICODE_CATEGORIES, UNICODE_BLOCKS, UnicodeSubset
_RE_QUANTIFIER = re.compile(r'{\d+(,(\d+)?)?}')
_RE_FORBIDDEN_ESCAPES = re.compile(
r'(?<!\\)\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|o{\d+}|\d+|A|Z|z|B|b|o)'
)
_UNICODE_SUBSETS = UNICODE_CATEGORIES.copy()
_UNICODE_SUBSETS.update(UNICODE_BLOCKS)
@ -36,7 +41,7 @@ I_SHORTCUT_REPLACE = (
)
C_SHORTCUT_REPLACE = (
"\-.0-9:A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-"
"-.0-9:A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-"
"\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"
)
@ -47,7 +52,7 @@ C_SHORTCUT_SET = UnicodeSubset(C_SHORTCUT_REPLACE)
W_SHORTCUT_SET = UnicodeSubset()
W_SHORTCUT_SET._code_points = sorted(
UNICODE_CATEGORIES['P'].code_points + UNICODE_CATEGORIES['Z'].code_points +
UNICODE_CATEGORIES['C'].code_points, key=lambda x: x[0] if isinstance(x, tuple) else x
UNICODE_CATEGORIES['C'].code_points, key=lambda x: x if isinstance(x, int) else x[0]
)
# Single and Multi character escapes
@ -89,8 +94,8 @@ class XsdRegexCharGroup(MutableSet):
"""
A set subclass to represent XML Schema regex character groups.
"""
_re_char_group = re.compile(r'(\\[nrt\\|.\-^?*+{}()\[\]sSdDiIcCwW]|\\[pP]{[a-zA-Z\-0-9]+})')
_re_unicode_ref = re.compile(r'\\([pP]){([a-zA-Z\-0-9]+)}')
_re_char_group = re.compile(r'(?<!.-)(\\[nrt|.\-^?*+{}()\]sSdDiIcCwW]|\\[pP]{[a-zA-Z\-0-9]+})')
_re_unicode_ref = re.compile(r'\\([pP]){([\w\d-]+)}')
def __init__(self, *args):
self.positive = UnicodeSubset()
@ -154,11 +159,14 @@ class XsdRegexCharGroup(MutableSet):
self.positive |= value
else:
self.negative |= value
elif self._re_unicode_ref.search(part) is not None:
if part.startswith('\\p'):
self.positive |= get_unicode_subset(part[3:-1])
else:
self.negative |= get_unicode_subset(part[3:-1])
elif part.startswith('\\p'):
if self._re_unicode_ref.search(part) is None:
raise XMLSchemaValueError("wrong Unicode subset specification %r" % part)
self.positive |= get_unicode_subset(part[3:-1])
elif part.startswith('\\P'):
if self._re_unicode_ref.search(part) is None:
raise XMLSchemaValueError("wrong Unicode subset specification %r" % part)
self.negative |= get_unicode_subset(part[3:-1])
else:
self.positive.update(part)
@ -172,11 +180,14 @@ class XsdRegexCharGroup(MutableSet):
self.positive -= value
else:
self.negative -= value
elif self._re_unicode_ref.search(part) is not None:
if part.startswith('\\p'):
self.positive -= get_unicode_subset(part[3:-1])
else:
self.negative -= get_unicode_subset(part[3:-1])
elif part.startswith('\\p'):
if self._re_unicode_ref.search(part) is None:
raise XMLSchemaValueError("wrong Unicode subset specification %r" % part)
self.positive -= get_unicode_subset(part[3:-1])
elif part.startswith('\\P'):
if self._re_unicode_ref.search(part) is None:
raise XMLSchemaValueError("wrong Unicode subset specification %r" % part)
self.negative -= get_unicode_subset(part[3:-1])
else:
self.positive.difference_update(part)
@ -209,7 +220,9 @@ def parse_character_class(xml_regex, class_pos):
group_pos = pos
while True:
if xml_regex[pos] == '\\':
if xml_regex[pos] == '[':
raise XMLSchemaRegexError("'[' is invalid in a character class: %r" % xml_regex)
elif xml_regex[pos] == '\\':
pos += 2
elif xml_regex[pos] == ']' or xml_regex[pos:pos + 2] == '-[':
if pos == group_pos:
@ -239,7 +252,16 @@ def get_python_regex(xml_regex):
"""
regex = ['^(']
pos = 0
while pos < len(xml_regex):
xml_regex_len = len(xml_regex)
nested_groups = 0
match = _RE_FORBIDDEN_ESCAPES.search(xml_regex)
if match:
raise XMLSchemaRegexError(
"not allowed escape sequence %r at position %d: %r" % (match.group(), match.span()[0], xml_regex)
)
while pos < xml_regex_len:
ch = xml_regex[pos]
if ch == '.':
regex.append('[^\r\n]')
@ -248,14 +270,56 @@ def get_python_regex(xml_regex):
elif ch == '[':
try:
char_group, pos = parse_character_class(xml_regex, pos)
regex.append(unicode_type(char_group))
except IndexError:
raise XMLSchemaRegexError(
"unterminated character group at position %d: %r" % (pos, xml_regex)
)
else:
char_group_repr = unicode_type(char_group)
if char_group_repr == '[^]':
regex.append(r'[\w\W]')
elif char_group_repr == '[]':
regex.append(r'[^\w\W]')
else:
regex.append(char_group_repr)
elif ch == '{':
if pos == 0:
raise XMLSchemaRegexError("unexpected quantifier %r at position %d: %r" % (ch, pos, xml_regex))
match = _RE_QUANTIFIER.match(xml_regex[pos:])
if match is None:
raise XMLSchemaRegexError("invalid quantifier %r at position %d: %r" % (ch, pos, xml_regex))
regex.append(match.group())
pos += len(match.group())
if pos < xml_regex_len and xml_regex[pos] in ('?', '+', '*'):
raise XMLSchemaRegexError(
"unexpected meta character %r at position %d: %r" % (xml_regex[pos], pos, xml_regex)
)
continue
elif ch == '(':
if xml_regex[pos:pos + 2] == '(?':
raise XMLSchemaRegexError("'(?...)' extension notation is not allowed: %r" % xml_regex)
nested_groups += 1
regex.append(ch)
elif ch == ']':
raise XMLSchemaRegexError("unexpected meta character %r at position %d: %r" % (ch, pos, xml_regex))
elif ch == ')':
if nested_groups == 0:
raise XMLSchemaRegexError("unbalanced parenthesis ')' at position %d: %r" % (pos, xml_regex))
nested_groups -= 1
regex.append(ch)
elif ch in ('?', '+', '*'):
if pos == 0:
raise XMLSchemaRegexError("unexpected quantifier %r at position %d: %r" % (ch, pos, xml_regex))
elif pos < xml_regex_len - 1 and xml_regex[pos+1] in ('?', '+', '*', '{'):
raise XMLSchemaRegexError(
"unexpected meta character %r at position %d: %r" % (xml_regex[pos+1], pos+1, xml_regex)
)
regex.append(ch)
elif ch == '\\':
pos += 1
if pos >= len(xml_regex):
if pos >= xml_regex_len:
regex.append('\\')
elif xml_regex[pos] == 'i':
regex.append('[%s]' % I_SHORTCUT_REPLACE)
@ -287,5 +351,7 @@ def get_python_regex(xml_regex):
regex.append(ch)
pos += 1
if nested_groups > 0:
raise XMLSchemaRegexError("unterminated subpattern in expression: %r" % xml_regex)
regex.append(r')$')
return ''.join(regex)

View File

@ -75,9 +75,13 @@ class XMLSchemaTestCase(unittest.TestCase):
def setUpClass(cls):
cls.errors = []
cls.xsd_types = cls.schema_class.builtin_types()
cls.content_pattern = re.compile(r'(xs:sequence|xs:choice|xs:all)')
cls.content_pattern = re.compile(r'(<|<xs:)(sequence|choice|all)')
cls.default_namespaces = {'ns': 'ns', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
cls.default_namespaces = {
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'tns': 'http://xmlschema.test/ns',
'ns': 'ns',
}
cls.vh_dir = cls.casepath('examples/vehicles')
cls.vh_xsd_file = cls.casepath('examples/vehicles/vehicles.xsd')
@ -179,7 +183,7 @@ class XMLSchemaTestCase(unittest.TestCase):
self.check_namespace_prefixes(error_string)
if not self.errors and expected:
raise ValueError("found no errors when %d expected." % expected)
raise ValueError("{!r}: found no errors when {} expected.".format(path, expected))
elif len(self.errors) != expected:
num_errors = len(self.errors)
if num_errors == 1:

View File

@ -1,5 +1,6 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/vehicles"
targetNamespace="http://example.com/vehicles">
<xs:include schemaLocation="cars.xsd" />

View File

@ -124,4 +124,24 @@
<xs:pattern value="(word|other)|(alternative)"/>
</xs:restriction>
</xs:simpleType>
<!-- Issue #103 -->
<xs:simpleType name="myType">
<xs:union memberTypes="typeA typeB"/>
</xs:simpleType>
<xs:simpleType name="typeA">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0.0"/>
<xs:maxInclusive value="100.0"/>
<xs:fractionDigits value="3"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="typeB">
<xs:restriction base="xs:string">
<xs:pattern value="NaN"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Schema test for invalid restricted models: UPA violation restricting a substitution group head. -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element type="xs:string" name="elem1"/>
<xs:element type="xs:string" name="elem2" substitutionGroup="elem1" />
<xs:complexType name="basicType1">
<xs:sequence>
<xs:element ref="elem1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- UPA violation minOccurs < maxOccurs -->
<xs:complexType name="restrictedType1">
<xs:complexContent>
<xs:restriction base="basicType1">
<xs:sequence>
<xs:element ref="elem2" maxOccurs="unbounded"/>
</xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<!-- No UPA violation if the restricted element is empty -->
<xs:complexType name="restrictedType2">
<xs:complexContent>
<xs:restriction base="basicType1">
<xs:sequence>
<xs:element ref="elem2" minOccurs="0" maxOccurs="0"/>
</xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<xs:element type="xs:string" name="elem3" abstract="true"/>
<xs:element type="xs:string" name="elem4" substitutionGroup="elem3" />
<xs:complexType name="basicType3">
<xs:sequence>
<xs:element ref="elem3" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- No UPA violation if the head element is abstract (it cannot be used in an instance). -->
<xs:complexType name="restrictedType3">
<xs:complexContent>
<xs:restriction base="basicType3">
<xs:sequence>
<xs:element ref="elem4" maxOccurs="unbounded"/>
</xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Schema test for invalid models: UPA violation restricting a substitution group head. -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="baseType1">
<xs:sequence>
<xs:element name="elem1" />
<xs:element minOccurs="0" name="elem2" />
<xs:choice>
<xs:element name="elem3" type="xs:string" />
<xs:element name="elem4" type="xs:string" />
</xs:choice>
<xs:element minOccurs="0" name="elem5" />
<xs:element minOccurs="0" name="elem6" type="xs:string" />
<xs:element minOccurs="0" name="elem7" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="restrictedType1">
<xs:complexContent>
<xs:restriction base="baseType1">
<xs:sequence>
<xs:sequence>
<xs:element name="elem1" />
<xs:element minOccurs="0" name="elem2" />
<xs:choice>
<xs:element name="elem3" type="xs:token" />
<xs:element name="elem4" type="xs:string" />
</xs:choice>
<xs:sequence>
<xs:element minOccurs="0" name="elem6" type="xs:string" />
</xs:sequence>
</xs:sequence>
<xs:sequence>
<xs:element minOccurs="0" name="elem7" />
</xs:sequence>
</xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Schema test for invalid models: UPA violations with a substitution group head and element. -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element type="xs:int" name="a"/>
<xs:element type="xs:int" name="b" substitutionGroup="a" />
<xs:complexType name="wrong_type1">
<xs:all>
<xs:element ref="a"/>
<xs:element ref="b"/>
</xs:all>
</xs:complexType>
<xs:complexType name="wrong_type2">
<xs:all>
<xs:element ref="b"/>
<xs:element ref="a"/>
</xs:all>
</xs:complexType>
<xs:complexType name="wrong_type3">
<xs:choice>
<xs:element ref="a"/>
<xs:element ref="b"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="good_type1">
<xs:sequence>
<xs:element ref="a"/>
<xs:element ref="b"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="wrong_type4">
<xs:sequence>
<xs:element ref="a" maxOccurs="unbounded"/>
<xs:element ref="b"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Schema test for invalid XSD 1.0 models: UPA violations that involve wildcards and elements. -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://xmlschema.test/ns"
xmlns="http://xmlschema.test/ns"
xmlns:other-ns="http://xmlschema.test/other-ns">
<xs:import namespace='http://xmlschema.test/other-ns' schemaLocation='other-ns.xsd'/>
<xs:element name="elem0" type="xs:string"/>
<!-- UPA violation (any with minOccurs < maxOccurs and ambiguity with elem0 validation) -->
<xs:element name='elem1'>
<xs:complexType>
<xs:sequence>
<xs:any namespace='##targetNamespace' minOccurs='0'/>
<xs:element ref='elem0'/>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- UPA violation (any and elem0 overlap and choice model) -->
<xs:element name='elem2'>
<xs:complexType>
<xs:choice>
<xs:any namespace='##targetNamespace'/>
<xs:element ref='elem0'/>
</xs:choice>
</xs:complexType>
</xs:element>
<!-- UPA violation (any with minOccurs < maxOccurs and ambiguity with other-ns:elem0) -->
<xs:element name='elem3'>
<xs:complexType>
<xs:sequence>
<xs:any namespace='##other' minOccurs='0'/>
<xs:element ref='other-ns:elem0'/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -14,15 +14,15 @@
<!-- Groups for model validation tests -->
<xs:group name="group1">
<xs:sequence>
<xs:element name="elem1" minOccurs="0" />
<xs:element name="elem2" minOccurs="0" />
<xs:element name="elem3" minOccurs="0" />
<xs:element name="elem1" type="xs:string" minOccurs="0" />
<xs:element name="elem2" type="xs:string" minOccurs="0" />
<xs:element name="elem3" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:group>
<xs:group name="group2">
<xs:sequence>
<xs:element name="elem1" type="xs:string" minOccurs="0"/>
<xs:element name="elem1" type="xs:string" minOccurs="0" />
<xs:choice minOccurs="0">
<xs:group ref="group1"/>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xmlschema.test/other-ns">
<xs:element name="elem0" type="xs:string"/>
</xs:schema>

View File

@ -0,0 +1,8 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
</xs:schema>

View File

@ -0,0 +1,7 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:ns="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="ns:rootType"/>
</xs:schema>

View File

@ -0,0 +1,8 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:ns="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:import namespace="http://example.com/xmlschema/namespaces"/>
<xs:element name="root" type="ns:rootType"/>
</xs:schema>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An invalid usage of the default namespace: it isn't mapped to targetNamespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid usage of the default namespace: mapped by an explicit declaration.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid usage of the default namespace: for default is mapped to no namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An invalid import case: the default namespace is mapped to targetNamespace but
the imported chameleon schema still maps default namespace to no namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:import schemaLocation="chameleon1.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An invalid import case: the default namespace is mapped to targetNamespace but
the imported chameleon schema still maps default namespace to no namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:import schemaLocation="chameleon2.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid import case: imported chameleon schema imports the target namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:import schemaLocation="chameleon3.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid include of a chameleon schema using an explicit declaration of the default namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="chameleon1.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid include using the default namespace, including a chameleon schema.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="chameleon1.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid include of a chameleon schema using an explicit declaration of the default namespace.
In this case the chameleon schema uses another prefix for the same target namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="chameleon2.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A valid include of a chameleon schema using an implicit declaration of the default namespace.
In this case the chameleon schema uses another prefix for the same target namespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="chameleon2.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An valid include from a schema with a targetNamespace, with the default namespace explicitly mapped.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="included3-valid.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An invalid include: including a schema with a different targetNamespace.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="included4-invalid.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An valid include from a schema with a targetNamespace, with the default namespace explicitly mapped.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:nsx="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="included5-valid.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
An invalid include from a schema with a targetNamespace not mapped to any prefix.
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:include schemaLocation="included6-invalid.xsd"/>
<xs:simpleType name="rootType">
<xs:restriction base="xs:string"/>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,10 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
</xs:schema>

View File

@ -0,0 +1,10 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://example.com/xmlschema/elements"
targetNamespace="http://example.com/xmlschema/elements"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
</xs:schema>

View File

@ -0,0 +1,10 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:nsy="http://example.com/xmlschema/namespaces"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="nsy:rootType"/>
</xs:schema>

View File

@ -0,0 +1,9 @@
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/xmlschema/namespaces"
elementFormDefault="qualified">
<xs:element name="root" type="rootType"/>
</xs:schema>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="project" type="TProject">
<xs:key name="widgetId">
<xs:selector xpath=".//widgets/widget | .//logic/widget"/>
<xs:field xpath="@id"/>
</xs:key>
</xs:element>
<xs:complexType name="TProject">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="protocols"/>
</xs:choice>
</xs:complexType>
</xs:schema>

View File

@ -36,6 +36,7 @@ features/derivations/complex-extensions.xsd --errors=1
features/derivations/complex-with-simple-content-restriction.xsd
features/derivations/list_types.xsd --errors=1
features/derivations/list_types.xml --errors=2
features/derivations/invalid_restrictions1.xsd --errors=1 # UPA violations with substitution group head element
features/elements/type_alternatives.xsd --errors=3
features/elements/type_alternatives.xsd --version=1.1
@ -47,10 +48,27 @@ features/models/circular_model.xsd --errors=1
features/models/illegal-attributes.xsd --errors=1
features/models/illegal-declarations.xsd --errors=3
features/models/illegal-occurs.xsd --errors=2
features/models/invalid_models1.xsd --errors=4
features/models/invalid_models2.xsd --errors=3
features/models/model1.xml --errors=1
features/models/models.xsd
features/models/recursive-groups.xsd --errors=2 # Two circular definitions
features/namespaces/default_ns_invalid.xsd --errors=1
features/namespaces/default_ns_valid1.xsd
features/namespaces/default_ns_valid2.xsd
features/namespaces/import-case1.xsd --errors=1 # Unknown type
features/namespaces/import-case2.xsd --errors=1 # Missing namespace import in imported chameleon schema
features/namespaces/import-case3.xsd
features/namespaces/include-case1.xsd
features/namespaces/include-case1bis.xsd
features/namespaces/include-case2.xsd
features/namespaces/include-case2bis.xsd
features/namespaces/include-case3.xsd
features/namespaces/include-case4.xsd --errors=2
features/namespaces/include-case5.xsd
features/namespaces/include-case6.xsd --errors=1
features/patterns/patterns.xsd
features/patterns/patterns.xml --errors=5
@ -81,4 +99,5 @@ issues/issue_051/issue_051.xml
issues/issue_073/issue_073-1.xml
issues/issue_073/issue_073-2.xml --errors=1
issues/issue_086/issue_086-1.xml
issues/issue_086/issue_086-2.xml
issues/issue_086/issue_086-2.xml
issues/issue_105/issue_105.xsd

View File

@ -36,6 +36,8 @@ TEST_FACTORY_OPTIONS = {
}
"""Command line options for test factory."""
RUN_W3C_TEST_SUITE = '-w' in sys.argv or '--w3c' in sys.argv
sys.argv = [a for a in sys.argv if a not in {'-x', '--extra', '-l', '--lxml'}] # Clean sys.argv for unittest

View File

@ -19,7 +19,7 @@ import unittest
from xmlschema.etree import etree_element
from xmlschema.namespaces import XSD_NAMESPACE, XSI_NAMESPACE
from xmlschema.helpers import get_xsd_annotation, iter_xsd_components, get_namespace, get_qname, \
local_name, prefixed_to_qname, qname_to_prefixed, has_xsd_components, get_xsd_component, \
local_name, qname_to_prefixed, has_xsd_components, get_xsd_component, \
get_xml_bool_attribute, get_xsd_derivation_attribute
from xmlschema.qnames import XSI_TYPE, XSD_SCHEMA, XSD_ELEMENT, XSD_SIMPLE_TYPE, XSD_ANNOTATION
from xmlschema.tests import XMLSchemaTestCase
@ -57,19 +57,6 @@ class TestHelpers(XMLSchemaTestCase):
self.assertRaises(TypeError, local_name, 1.0)
self.assertRaises(TypeError, local_name, 0)
def test_prefixed_to_qname_functions(self):
namespaces = {'xs': XSD_NAMESPACE, 'xsi': XSI_NAMESPACE}
self.assertEqual(prefixed_to_qname('xs:element', namespaces), XSD_ELEMENT)
self.assertEqual(prefixed_to_qname('xsi:type', namespaces), XSI_TYPE)
self.assertEqual(prefixed_to_qname(XSI_TYPE, namespaces), XSI_TYPE)
self.assertEqual(prefixed_to_qname('element', namespaces), 'element')
self.assertEqual(prefixed_to_qname('', namespaces), '')
self.assertEqual(prefixed_to_qname(None, namespaces), None)
self.assertRaises(ValueError, prefixed_to_qname, 'xsi:type', {})
self.assertRaises(ValueError, prefixed_to_qname, 'xml:lang', namespaces)
def test_qname_to_prefixed_functions(self):
namespaces = {'xs': XSD_NAMESPACE, 'xsi': XSI_NAMESPACE}
self.assertEqual(qname_to_prefixed(XSD_ELEMENT, namespaces), 'xs:element')

View File

@ -265,12 +265,12 @@ class TestGlobalMaps(unittest.TestCase):
def test_xsd_10_globals(self):
self.assertEqual(len(xsd_10_meta_schema.maps.notations), 2)
self.assertEqual(len(xsd_10_meta_schema.maps.types), 105)
self.assertEqual(len(xsd_10_meta_schema.maps.types), 108)
self.assertEqual(len(xsd_10_meta_schema.maps.attributes), 18)
self.assertEqual(len(xsd_10_meta_schema.maps.attribute_groups), 9)
self.assertEqual(len(xsd_10_meta_schema.maps.groups), 18)
self.assertEqual(len(xsd_10_meta_schema.maps.elements), 47)
self.assertEqual(len([e.is_global for e in xsd_10_meta_schema.maps.iter_globals()]), 199)
self.assertEqual(len(xsd_10_meta_schema.maps.elements), 45)
self.assertEqual(len([e.is_global for e in xsd_10_meta_schema.maps.iter_globals()]), 200)
self.assertEqual(len(xsd_10_meta_schema.maps.substitution_groups), 0)
def test_xsd_11_globals(self):
@ -279,13 +279,13 @@ class TestGlobalMaps(unittest.TestCase):
self.assertEqual(len(xsd_11_meta_schema.maps.attributes), 18)
self.assertEqual(len(xsd_11_meta_schema.maps.attribute_groups), 10)
self.assertEqual(len(xsd_11_meta_schema.maps.groups), 19)
self.assertEqual(len(xsd_11_meta_schema.maps.elements), 53)
self.assertEqual(len([e.is_global for e in xsd_11_meta_schema.maps.iter_globals()]), 220)
self.assertEqual(len(xsd_11_meta_schema.maps.elements), 51)
self.assertEqual(len([e.is_global for e in xsd_11_meta_schema.maps.iter_globals()]), 218)
self.assertEqual(len(xsd_11_meta_schema.maps.substitution_groups), 1)
def test_xsd_10_build(self):
xsd_10_meta_schema.maps.build()
self.assertEqual(len([e for e in xsd_10_meta_schema.maps.iter_globals()]), 199)
self.assertEqual(len([e for e in xsd_10_meta_schema.maps.iter_globals()]), 200)
self.assertTrue(xsd_10_meta_schema.maps.built)
xsd_10_meta_schema.maps.clear()
xsd_10_meta_schema.maps.build()
@ -293,7 +293,7 @@ class TestGlobalMaps(unittest.TestCase):
def test_xsd_11_build(self):
xsd_11_meta_schema.maps.build()
self.assertEqual(len([e for e in xsd_11_meta_schema.maps.iter_globals()]), 220)
self.assertEqual(len([e for e in xsd_11_meta_schema.maps.iter_globals()]), 218)
self.assertTrue(xsd_11_meta_schema.maps.built)
xsd_11_meta_schema.maps.clear()
xsd_11_meta_schema.maps.build()
@ -307,8 +307,8 @@ class TestGlobalMaps(unittest.TestCase):
total_counter += 1
if c.is_global:
global_counter += 1
self.assertEqual(global_counter, 199)
self.assertEqual(total_counter, 894)
self.assertEqual(global_counter, 200)
self.assertEqual(total_counter, 901)
def test_xsd_11_components(self):
total_counter = 0
@ -318,8 +318,8 @@ class TestGlobalMaps(unittest.TestCase):
total_counter += 1
if c.is_global:
global_counter += 1
self.assertEqual(global_counter, 220)
self.assertEqual(total_counter, 1031)
self.assertEqual(global_counter, 218)
self.assertEqual(total_counter, 1018)
if __name__ == '__main__':

View File

@ -14,7 +14,7 @@ This module runs tests concerning model groups validation.
"""
import unittest
from xmlschema.validators import XsdModelVisitor
from xmlschema.validators import ModelVisitor
from xmlschema.tests import XMLSchemaTestCase
@ -26,7 +26,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
Advances a model with a match condition and checks the expected error list or exception.
:param model: an XsdModelVisitor instance.
:param model: an ModelGroupVisitor instance.
:param expected: can be an exception class or a list. Leaving `None` means that an empty \
list is expected.
"""
@ -39,7 +39,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
Advances a model with a no-match condition and checks the expected error list or or exception.
:param model: an XsdModelVisitor instance.
:param model: an ModelGroupVisitor instance.
:param expected: can be an exception class or a list. Leaving `None` means that an empty \
list is expected.
"""
@ -52,7 +52,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
Advances a model with an argument match condition and checks the expected error list.
:param model: an XsdModelVisitor instance.
:param model: an ModelGroupVisitor instance.
:param match: the matching boolean condition.
:param expected: can be an exception class or a list. Leaving `None` means that an empty \
list is expected.
@ -66,7 +66,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
Stops a model and checks the expected errors list.
:param model: an XsdModelVisitor instance.
:param model: an ModelGroupVisitor instance.
:param expected: can be an exception class or a list. Leaving `None` means that an empty \
list is expected.
"""
@ -81,12 +81,12 @@ class TestModelValidation(XMLSchemaTestCase):
# Sequence with two not-emptiable single-occurs elements
group = self.vh_schema.elements['vehicles'].type.content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <cars>
self.check_advance_true(model) # <bikes>
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <cars>
self.check_advance_true(model) # <bikes>
self.check_advance_true(model, ValueError) # <bikes>
@ -96,14 +96,14 @@ class TestModelValidation(XMLSchemaTestCase):
# Emptiable 1:1 sequence with one emptiable and unlimited element.
group = self.vh_schema.elements['cars'].type.content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <car>
self.check_advance_true(model) # <car>
self.check_advance_true(model) # <car>
self.check_advance_false(model) # (end)
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model) # <not-a-car>
self.assertIsNone(model.element)
@ -113,7 +113,7 @@ class TestModelValidation(XMLSchemaTestCase):
# Sequence with one not-emptiable and unlimited element.
group = self.col_schema.elements['collection'].type.content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <car>
self.check_advance_true(model) # <car>
self.check_advance_true(model) # <car>
@ -121,7 +121,7 @@ class TestModelValidation(XMLSchemaTestCase):
self.check_advance_false(model) # (end)
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model, [(group[0], 0, [group[0]])]) # <not-a-car>
self.assertIsNone(model.element)
@ -129,20 +129,20 @@ class TestModelValidation(XMLSchemaTestCase):
# Sequence with four single elements, last two are also emptiable.
group = self.col_schema.types['personType'].content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <name>
self.check_advance_true(model) # <born>
self.check_advance_true(model) # <dead>
self.check_advance_true(model) # <qualification>
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <name>
self.check_advance_true(model) # <born>
self.check_stop(model)
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <name> match
self.check_advance_false(model, [(group[1], 0, [group[1]])]) # <born> missing!
self.check_advance_true(model) # <dead> match
@ -163,22 +163,22 @@ class TestModelValidation(XMLSchemaTestCase):
"""
group = self.schema_class.meta_schema.groups['simpleDerivation']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <restriction> match
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model) # <list> not match with <restriction>
self.check_advance_true(model) # <list> match
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model) # <union> not match with <restriction>
self.check_advance_false(model) # <union> not match with <list>
self.check_advance_true(model) # <union> match
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model) # <other> not match with <restriction>
self.check_advance_false(model) # <other> not match with <list>
self.check_advance_false(model, [(group, 0, group[:])]) # <other> not match with <union>
@ -213,7 +213,7 @@ class TestModelValidation(XMLSchemaTestCase):
# Sequence with an optional single element and an optional unlimited choice.
group = self.schema_class.meta_schema.groups['simpleRestrictionModel']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
self.check_advance_true(model) # <simpleType> match
self.assertEqual(model.element, group[1][0][0])
@ -249,7 +249,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
group = self.schema_class.meta_schema.groups['schemaTop']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0][0])
self.check_advance_false(model) # <simpleType> don't match
self.assertEqual(model.element, group[0][0][1])
@ -301,22 +301,22 @@ class TestModelValidation(XMLSchemaTestCase):
"""
group = self.schema_class.meta_schema.groups['attrDecls']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
for match in [False, False, True]:
self.check_advance(model, match)
self.assertIsNone(model.element)
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_false(model)
self.check_advance_true(model)
self.assertEqual(model.element, group[0][0])
model = XsdModelVisitor(group)
model = ModelVisitor(group)
for match in [False, True, False, False]:
self.check_advance(model, match)
self.assertEqual(model.element, group[1])
model = XsdModelVisitor(group)
model = ModelVisitor(group)
for match in [False, True, True, False, True, False, False]:
self.check_advance(model, match)
self.assertEqual(model.element, group[1])
@ -345,7 +345,7 @@ class TestModelValidation(XMLSchemaTestCase):
"""
group = self.schema_class.meta_schema.groups['complexTypeModel']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
self.check_advance_true(model) # <simpleContent> match
self.assertIsNone(model.element)
@ -373,7 +373,7 @@ class TestModelValidation(XMLSchemaTestCase):
group = self.schema_class.meta_schema.elements['schema'].type.content_type
# A schema model with a wrong tag
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0])
self.check_advance_false(model) # eg. anyAttribute
self.check_stop(model)
@ -383,7 +383,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group1(self):
group = self.models_schema.groups['group1']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
self.check_stop(model)
@ -401,7 +401,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group2(self):
group = self.models_schema.groups['group2']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
for _ in range(3):
self.check_advance_false(model) # group1 do not match
@ -417,7 +417,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group3(self):
group = self.models_schema.groups['group3']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
for match in [True, False, True]:
self.check_advance(model, match)
@ -426,7 +426,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group4(self):
group = self.models_schema.groups['group4']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
for match in [True, False, True]:
self.check_advance(model, match)
@ -435,7 +435,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group5(self):
group = self.models_schema.groups['group5']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0])
for _ in range(5): # match [<elem1> .. <elem5>]
self.check_advance_true(model)
@ -446,7 +446,7 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group6(self):
group = self.models_schema.groups['group6']
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0])
self.check_advance_true(model) # match choice with <elem1>
self.check_advance_true(model) # match choice with <elem2>
@ -455,13 +455,13 @@ class TestModelValidation(XMLSchemaTestCase):
def test_model_group7(self):
group = self.models_schema.types['complexType7'].content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0])
self.check_stop(model, [(group[0][0], 0, [group[0][0]])])
group = self.models_schema.types['complexType7_emptiable'].content_type
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0][0])
self.check_stop(model)
@ -473,7 +473,7 @@ class TestModelValidation(XMLSchemaTestCase):
group = schema.types['Foo'].content_type
# issue_086-1.xml sequence simulation
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.assertEqual(model.element, group[0])
self.check_advance_true(model) # <header> matching
self.assertEqual(model.element, group[1][0][0]) # 'a' element
@ -499,7 +499,7 @@ class TestModelValidation(XMLSchemaTestCase):
self.check_stop(model)
# issue_086-2.xml sequence simulation
model = XsdModelVisitor(group)
model = ModelVisitor(group)
self.check_advance_true(model) # <header> matching
self.assertEqual(model.element, group[1][0][0]) # 'a' element
self.check_advance_false(model)

View File

@ -12,9 +12,11 @@
import unittest
import glob
import fileinput
import json
import os
import re
import importlib
import platform
import sys
import subprocess
@ -113,6 +115,7 @@ class TestPackaging(unittest.TestCase):
# Exclude explicit debug statements written in the code
exclude = {
'regex.py': [240, 241],
'codepoints.py': [543],
}
message = "\nFound a debug missing statement at line %d or file %r: %r"
@ -160,12 +163,28 @@ class TestPackaging(unittest.TestCase):
message % (lineno, filename, match.group(1).strip('\'\"'), version)
)
def test_json_unicode_categories(self):
filename = os.path.join(self.source_dir, 'unicode_categories.json')
self.assertTrue(os.path.isfile(filename), msg="file %r is missing!" % filename)
with open(filename, 'r') as fp:
self.assertIsInstance(json.load(fp), dict, msg="file %r is not encoded in JSON format!" % filename)
# TODO: Add tests for checking base schemas files and other package files.
def test_base_schema_files(self):
et = importlib.import_module('xml.etree.ElementTree')
schemas_dir = os.path.join(self.source_dir, 'validators/schemas')
base_schemas = [
'XSD_1.0/XMLSchema.xsd', 'XSD_1.1/XMLSchema.xsd', 'xhtml1-strict.xsd', 'xlink.xsd',
'xml_minimal.xsd', 'XMLSchema-hasFacetAndProperty_minimal.xsd', 'XMLSchema-instance_minimal.xsd'
]
for rel_path in base_schemas:
filename = os.path.join(schemas_dir, rel_path)
self.assertTrue(os.path.isfile(filename), msg="schema file %r is missing!" % filename)
self.assertIsInstance(et.parse(filename), et.ElementTree)
if __name__ == '__main__':
from xmlschema.tests import print_test_header
header1 = "Test package %r installation" % os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
header2 = "with Python {} on platform {}".format(platform.python_version(), platform.platform())
print('{0}\n{1}\n{2}\n{0}'.format("*" * max(len(header1), len(header2)), header1, header2))
print_test_header()
unittest.main()

View File

@ -18,10 +18,11 @@ import sys
import re
from unicodedata import category
from xmlschema.exceptions import XMLSchemaValueError
from xmlschema.exceptions import XMLSchemaValueError, XMLSchemaRegexError
from xmlschema.compat import unicode_chr
from xmlschema.codepoints import iter_code_points, UnicodeSubset, build_unicode_categories, UNICODE_CATEGORIES
from xmlschema.regex import get_python_regex
from xmlschema.codepoints import code_point_repr, iterparse_character_group, iter_code_points, \
UnicodeSubset, build_unicode_categories, UNICODE_CATEGORIES
from xmlschema.regex import get_python_regex, XsdRegexCharGroup
class TestCodePoints(unittest.TestCase):
@ -111,6 +112,15 @@ class TestUnicodeSubset(unittest.TestCase):
cds = UnicodeSubset([0, 2, (80, 200), 10000])
self.assertEqual(cds - {2, 120, 121, (150, 260)}, [0, (80, 120), (122, 150), 10000])
def test_code_point_repr_function(self):
self.assertEqual(code_point_repr((ord('2'), ord('\\') + 1)), r'2-\\')
class TestXsdRegexCharGroup(unittest.TestCase):
def test_char_group_split(self):
self.assertListEqual(XsdRegexCharGroup._re_char_group.split(r'2-\\'), [r'2-\\'])
class TestUnicodeCategories(unittest.TestCase):
"""
@ -131,7 +141,7 @@ class TestUnicodeCategories(unittest.TestCase):
base_sets = [set(v) for k, v in UNICODE_CATEGORIES.items() if len(k) > 1]
self.assertFalse(any([s.intersection(t) for s in base_sets for t in base_sets if s != t]))
@unittest.skipIf(not ((3, 6) <= sys.version_info < (3, 7)), "Test only for Python 3.6")
@unittest.skipIf(not ((3, 7) <= sys.version_info < (3, 8)), "Test only for Python 3.7")
def test_unicodedata_category(self):
for key in UNICODE_CATEGORIES:
for cp in UNICODE_CATEGORIES[key]:
@ -273,6 +283,12 @@ class TestPatterns(unittest.TestCase):
self.assertEqual(pattern.search('zk:xy-9s').group(0), 'zk:xy-9s')
self.assertIsNone(pattern.search('xx:y'))
def test_iterparse_character_group(self):
self.assertListEqual(list(iterparse_character_group('a-c-1-4x-z-7-9')),
[(ord('a'), ord('c') + 1), ord('-'), (ord('1'), ord('4') + 1),
(ord('x'), ord('z') + 1), ord('-'), (55, 58)])
self.assertListEqual(list(iterparse_character_group('2-\\')), [(ord('2'), ord('\\') + 1)])
def test_occurrences_qualifiers(self):
regex = get_python_regex('#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?')
self.assertEqual(regex, '^(#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?)$')
@ -321,6 +337,11 @@ class TestPatterns(unittest.TestCase):
self.assertEqual(pattern.search('x11').group(0), 'x11')
self.assertIsNone(pattern.search('3a'))
def test_empty_character_group_repr(self):
regex = get_python_regex('[a-[a-f]]')
self.assertEqual(regex, r'^([^\w\W])$')
self.assertRaises(XMLSchemaRegexError, get_python_regex, '[]')
if __name__ == '__main__':
from xmlschema.tests import print_test_header

View File

@ -21,10 +21,11 @@ import time
import warnings
import xmlschema
from xmlschema import XMLSchemaBase, XMLSchemaParseError, XMLSchemaIncludeWarning, XMLSchemaImportWarning
from xmlschema import XMLSchemaBase, XMLSchemaParseError, XMLSchemaModelError, \
XMLSchemaIncludeWarning, XMLSchemaImportWarning
from xmlschema.compat import PY3, unicode_type
from xmlschema.etree import lxml_etree, etree_element, py_etree_element
from xmlschema.qnames import XSD_LIST, XSD_UNION
from xmlschema.qnames import XSD_LIST, XSD_UNION, XSD_ELEMENT, XSI_TYPE
from xmlschema.tests import tests_factory, SchemaObserver, XMLSchemaTestCase
from xmlschema.validators import XsdValidator, XMLSchema11
from xmlschema.xpath import ElementPathContext
@ -57,7 +58,7 @@ class TestXMLSchema10(XMLSchemaTestCase):
</complexType>
<complexType name="restrictedType">
<{1}Content>
<restriction base="targetType">
<restriction base="ns:targetType">
{2}
</restriction>
</{1}Content>
@ -65,6 +66,28 @@ class TestXMLSchema10(XMLSchemaTestCase):
""".format(base.strip(), content, restriction.strip())
self.check_schema(source, expected, **kwargs)
def test_schema_copy(self):
schema = self.vh_schema.copy()
self.assertNotEqual(id(self.vh_schema), id(schema))
self.assertNotEqual(id(self.vh_schema.namespaces), id(schema.namespaces))
self.assertNotEqual(id(self.vh_schema.maps), id(schema.maps))
def test_resolve_qname(self):
schema = self.schema_class("""<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xs:element name="root" />
</xs:schema>""")
self.assertEqual(schema.resolve_qname('xs:element'), XSD_ELEMENT)
self.assertEqual(schema.resolve_qname('xsi:type'), XSI_TYPE)
self.assertEqual(schema.resolve_qname(XSI_TYPE), XSI_TYPE)
self.assertEqual(schema.resolve_qname('element'), 'element')
self.assertRaises(ValueError, schema.resolve_qname, '')
self.assertRaises(ValueError, schema.resolve_qname, 'xsi:a type ')
self.assertRaises(ValueError, schema.resolve_qname, 'xml::lang')
def test_simple_types(self):
# Issue #54: set list or union schema element.
xs = self.check_schema("""
@ -94,16 +117,13 @@ class TestXMLSchema10(XMLSchemaTestCase):
<import namespace="http://missing.example.test/" />
<import/>
""")
self.assertEqual(len(context), 4, "Wrong number of include/import warnings")
self.assertEqual(len(context), 3, "Wrong number of include/import warnings")
self.assertEqual(context[0].category, XMLSchemaIncludeWarning)
self.assertEqual(context[1].category, XMLSchemaIncludeWarning)
self.assertEqual(context[2].category, XMLSchemaImportWarning)
self.assertEqual(context[3].category, XMLSchemaImportWarning)
self.assertTrue(str(context[0].message).startswith("Include"))
self.assertTrue(str(context[1].message).startswith("Redefine"))
self.assertTrue(str(context[2].message).startswith("Namespace import"))
self.assertTrue(str(context[3].message).startswith("Namespace import"))
self.assertTrue(str(context[3].message).endswith("no schema location provided."))
def test_wrong_references(self):
# Wrong namespace for element type's reference
@ -167,7 +187,6 @@ class TestXMLSchema10(XMLSchemaTestCase):
</simpleType>
""")
@unittest.skip("The feature is still under development")
def test_element_restrictions(self):
base = """
<sequence>
@ -221,7 +240,6 @@ class TestXMLSchema10(XMLSchemaTestCase):
</sequence>
""", expected=XMLSchemaParseError)
@unittest.skip("The feature is still under development")
def test_sequence_group_restriction(self):
# Meaningless sequence group
base = """
@ -263,7 +281,6 @@ class TestXMLSchema10(XMLSchemaTestCase):
XMLSchemaParseError
)
@unittest.skip("The feature is still under development")
def test_all_group_restriction(self):
base = """
<all>
@ -274,22 +291,29 @@ class TestXMLSchema10(XMLSchemaTestCase):
"""
self.check_complex_restriction(base, '<all><element name="A"/><element name="C"/></all>')
self.check_complex_restriction(
base, '<all><element name="C" minOccurs="0"/><element name="A"/></all>',
XMLSchemaParseError
base, '<all><element name="C" minOccurs="0"/><element name="A"/></all>', XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="C"/></sequence>'
)
self.check_complex_restriction(
base, '<sequence><element name="C" minOccurs="0"/><element name="A"/></sequence>',
)
self.check_complex_restriction(
base, '<sequence><element name="C" minOccurs="0"/><element name="A" minOccurs="0"/></sequence>',
XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="X"/></sequence>',
XMLSchemaParseError
base, '<sequence><element name="A"/><element name="X"/></sequence>', XMLSchemaParseError
)
@unittest.skip("The feature is still under development")
base = """
<all>
<element name="A" minOccurs="0" maxOccurs="0"/>
</all>
"""
self.check_complex_restriction(base, '<all><element name="A"/></all>', XMLSchemaParseError)
def test_choice_group_restriction(self):
base = """
<choice maxOccurs="2">
@ -308,7 +332,6 @@ class TestXMLSchema10(XMLSchemaTestCase):
base, '<choice maxOccurs="2"><element name="A"/><element name="C"/></choice>',
)
@unittest.skip("The feature is still under development")
def test_occurs_restriction(self):
base = """
<sequence minOccurs="3" maxOccurs="10">
@ -330,7 +353,7 @@ class TestXMLSchema10(XMLSchemaTestCase):
def test_union_restrictions(self):
# Wrong union restriction (not admitted facets, see issue #67)
self.check_schema("""
self.check_schema(r"""
<simpleType name="Percentage">
<restriction base="ns:Integer">
<minInclusive value="0"/>
@ -388,7 +411,7 @@ class TestXMLSchema10(XMLSchemaTestCase):
</restriction>
</simpleType>""")
schema = self.check_schema("""
self.check_schema("""
<simpleType name="restricted_year">
<restriction base="gYear">
<minInclusive value="1900"/>
@ -396,6 +419,45 @@ class TestXMLSchema10(XMLSchemaTestCase):
</restriction>
</simpleType>""")
def test_base_schemas(self):
from xmlschema.validators.schema import XML_SCHEMA_FILE
schema = self.schema_class(XML_SCHEMA_FILE)
def test_recursive_complex_type(self):
schema = self.schema_class("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elemA" type="typeA"/>
<xs:complexType name="typeA">
<xs:sequence>
<xs:element ref="elemA" minOccurs="0" maxOccurs="5"/>
</xs:sequence>
</xs:complexType>
</xs:schema>""")
self.assertEqual(schema.elements['elemA'].type, schema.types['typeA'])
def test_upa_violations(self):
self.check_schema("""
<complexType name="typeA">
<sequence>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="A"/>
<element name="B"/>
</sequence>
<element name="A" minOccurs="0"/>
</sequence>
</complexType>""", XMLSchemaModelError)
self.check_schema("""
<complexType name="typeA">
<sequence>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="B"/>
<element name="A"/>
</sequence>
<element name="A" minOccurs="0"/>
</sequence>
</complexType>""")
class TestXMLSchema11(TestXMLSchema10):
@ -522,10 +584,10 @@ def make_schema_test_class(test_file, test_args, test_num, schema_class, check_w
xs = schema_class(xsd_file, validation='lax', locations=locations, defuse=defuse)
else:
xs = schema_class(xsd_file, locations=locations, defuse=defuse)
self.errors.extend(xs.all_errors)
self.errors.extend(xs.maps.all_errors)
if inspect:
components_ids = set([id(c) for c in xs.iter_components()])
components_ids = set([id(c) for c in xs.maps.iter_components()])
missing = [c for c in SchemaObserver.components if id(c) not in components_ids]
if any([c for c in missing]):
raise ValueError("schema missing %d components: %r" % (len(missing), missing))
@ -589,7 +651,8 @@ def make_schema_test_class(test_file, test_args, test_num, schema_class, check_w
with warnings.catch_warnings(record=True) as ctx:
warnings.simplefilter("always")
self.check_schema()
self.assertEqual(len(ctx), expected_warnings, "Wrong number of include/import warnings")
self.assertEqual(len(ctx), expected_warnings,
"%r: Wrong number of include/import warnings" % xsd_file)
else:
self.check_schema()

View File

@ -906,6 +906,12 @@ class TestDecoding(XMLSchemaTestCase):
self.assertFalse(xs.is_valid('<value>alpha</value>'))
self.assertEqual(xs.decode('<value>10</value>'), 10)
def test_union_types(self):
# For testing issue #103
decimal_or_nan = self.st_schema.types['myType']
self.check_decode(decimal_or_nan, '95.0', Decimal('95.0'))
self.check_decode(decimal_or_nan, 'NaN', u'NaN')
class TestDecoding11(TestDecoding):
schema_class = XMLSchema11

View File

@ -0,0 +1,187 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2016-2019, SISSA (International School for Advanced Studies).
# 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 runs tests concerning the W3C XML Schema 1.1 test suite.
"""
from __future__ import print_function, unicode_literals
import unittest
import os.path
import xml.etree.ElementTree as ElementTree
import xmlschema
from xmlschema import XMLSchemaException
TEST_SUITE_NAMESPACE = "http://www.w3.org/XML/2004/xml-schema-test-suite/"
XLINK_NAMESPACE = "http://www.w3.org/1999/xlink"
ADMITTED_VALIDITY = {'valid', 'invalid', 'indeterminate'}
####
# Tests that are incompatible with XSD meta-schema validation or that are postponed
SKIPPED_TESTS = {
# Signed as valid that have to be checked
'../msData/additional/addB194.xsd', # 4826: invalid xml:lang='enu'
'../msData/particles/particlesZ001.xsd', # 10957: Invalid in XSD 1.0
'../msData/simpleType/stE110.xsd', # 13892: Circular xs:union declaration
'../saxonData/Missing/missing001.xsd', # 14405: missing type (this may be valid in 'lax' mode?)
'../saxonData/Missing/missing002.xsd', # 14406: missing substitution group
'../saxonData/Missing/missing003.xsd', # 14406: missing type and substitution group
'../saxonData/Missing/missing006.xsd', # 14410: missing list item type
'../saxonData/VC/vc001.xsd', # 14411: VC namespace required
'../saxonData/VC/vc002.xsd', # 14412: VC namespace required
'../saxonData/VC/vc014.xsd', # 14413: VC namespace required
'../saxonData/VC/vc024.xsd', # 14414: VC 1.1? required
'../saxonData/XmlVersions/xv004.xsd', # 14419: non-BMP chars allowed in names in XML 1.1+
# Invalid that may be valid
'../sunData/combined/xsd003b/xsd003b.e.xsd', # 3981: Redefinition that may be valid
'../msData/additional/adhocAddC002.xsd', # 4642: Lack of the processor on XML namespace knowledge
'../msData/additional/test65026.xsd', # 4712: Lack of the processor on XML namespace knowledge
'../msData/annotations/annotF001.xsd', # 4989: Annotation contains xml:lang="" ?? (but xml.xsd allows '')
'../msData/datatypes/Facets/base64Binary/base64Binary_enumeration003.xsd', # 7277: check base64 invalid values
'../msData/datatypes/Facets/anyURI/anyURI_a001.xsd', # 7292: XSD 1.0 limited URI (see RFC 2396 + RFC 2732)
'../msData/datatypes/Facets/anyURI/anyURI_a003.xsd', # 7294: XSD 1.0 limited URI (see RFC 2396 + RFC 2732)
'../msData/datatypes/Facets/anyURI/anyURI_b004.xsd', # 7310: XSD 1.0 limited URI (see RFC 2396 + RFC 2732)
'../msData/datatypes/Facets/anyURI/anyURI_b006.xsd', # 7312: XSD 1.0 limited URI (see RFC 2396 + RFC 2732)
'../msData/element/elemZ026.xsd', # 8541: This is good because the head element is abstract
'../msData/element/elemZ031.xsd', # 8557: Valid in Python that has arbitrary large integers
'../msData/errata10/errC005.xsd', # 8558: Typo: abstract attribute must be set to "true" to fail
'../msData/group/groupH021.xsd', # 8679: TODO: wrong in XSD 1.0, good in XSD 1.1
'../msData/identityConstraint/idC019.xsd', # 8936: TODO: is it an error?
'../msData/identityConstraint/idI148.xsd', # 9291: FIXME attribute::* in a selector (restrict XPath parser)
'../msData/identityConstraint/idJ016.xsd', # 9311: FIXME xpath="xpns: *" not allowed??
'../msData/modelGroups/mgE006.xsd', # 9712: Is valid (is mg007.xsd invalid for the same reason)
# Invalid that are valid because depend by implementation choices
'../msData/schema/schG6_a.xsd', # 13639: Schema is valid because the ns import is done once, validation fails.
'../msData/schema/schG11_a.xsd', # 13544: Schema is valid because the ns import is done once, validation fails.
}
def fetch_xsd_test_suite():
parent = os.path.dirname
xmlschema_test_dir = parent(os.path.abspath(__file__))
xmlschema_base_dir = parent(parent(xmlschema_test_dir))
suite_file = os.path.join(parent(xmlschema_base_dir), 'xsdtests/suite.xml')
if os.path.isfile(suite_file):
return suite_file
else:
raise FileNotFoundError("can't find the XSD suite index file suite.xml ...")
def create_w3c_test_group_case(filename, group_elem, group_number, xsd_version='1.0'):
"""
Creates a test class for a W3C test group.
:param filename: the filename of the testSet that owns the testGroup.
:param group_elem: the Element instance of the test group.
:param group_number: a positive integer to distinguish and order test groups.
:param xsd_version: if '1.1' uses XSD 1.1 validator class, otherwise uses the XSD 1.0 validator.
"""
name = group_elem.attrib['name']
if xsd_version == '1.1':
schema_class = xmlschema.validators.XMLSchema11
if group_elem.get('version') == '1.0':
raise ValueError("testGroup %r is not suited for XSD 1.1" % name)
elif group_elem.get('version') == '1.1':
pass # raise ValueError("testGroup %r is not suited for XSD 1.0" % name)
else:
schema_class = xmlschema.XMLSchema
schema_elem = group_elem.find('{%s}schemaTest' % TEST_SUITE_NAMESPACE)
if schema_elem is not None:
schema_document = schema_elem.find('{%s}schemaDocument' % TEST_SUITE_NAMESPACE)
schema_path = schema_document.get('{%s}href' % XLINK_NAMESPACE)
if schema_path in SKIPPED_TESTS:
return
schema_path = os.path.normpath(os.path.join(os.path.dirname(filename), schema_path))
if not os.path.isfile(schema_path):
raise ValueError("Schema file %r not found!" % schema_path)
expected = elem = None
for elem in schema_elem.findall('{%s}expected' % TEST_SUITE_NAMESPACE):
if 'version' not in elem.attrib:
expected = elem.attrib['validity']
elif elem.attrib['version'] in (xsd_version, 'full-xpath-in-CTA'):
expected = elem.attrib['validity']
break
if expected is None:
raise ValueError("Missing expected validity for XSD %s" % xsd_version)
elif expected not in ADMITTED_VALIDITY:
raise ValueError("Wrong validity=%r attribute for %r" % (expected, elem))
else:
schema_path = expected = None
if expected == 'invalid':
class TestGroupCase(unittest.TestCase):
def test_invalid_schema(self):
with self.assertRaises(XMLSchemaException, msg="Schema %r may be invalid" % schema_path) as _:
schema_class(schema_path, use_meta=False)
elif expected == 'valid':
class TestGroupCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
try:
cls.schema = schema_class(schema_path, use_meta=False) if schema_path else None
except TypeError:
cls.schema = None
def test_valid_schema(self):
if schema_path:
self.assertIsInstance(schema_class(schema_path, use_meta=False), schema_class)
else:
return # expected is None or 'indeterminate'
TestGroupCase.__name__ = TestGroupCase.__qualname__ = str(
'TestGroupCase{0:05}_{1}'.format(group_number, name.replace('-', '_'))
)
return TestGroupCase
if __name__ == '__main__':
index_path = fetch_xsd_test_suite()
index_dir = os.path.dirname(index_path)
suite_xml = ElementTree.parse(index_path)
HREF_ATTRIBUTE = "{%s}href" % XLINK_NAMESPACE
test_classes = {}
testgroup_num = 1
for testset_elem in suite_xml.iter("{%s}testSetRef" % TEST_SUITE_NAMESPACE):
testset_file = os.path.join(index_dir, testset_elem.attrib.get(HREF_ATTRIBUTE, ''))
testset_xml = ElementTree.parse(testset_file)
testset_version = testset_xml.getroot().get('version')
if testset_version is not None and '1.0' not in testset_version:
continue
# print("*** {} ***".format(testset_file))
for testgroup_elem in testset_xml.iter("{%s}testGroup" % TEST_SUITE_NAMESPACE):
if testgroup_elem.get('version') == '1.1':
continue
cls = create_w3c_test_group_case(testset_file, testgroup_elem, testgroup_num)
if cls is not None:
test_classes[cls.__name__] = cls
testgroup_num += 1
globals().update(test_classes)
# print_test_header()
unittest.main()

File diff suppressed because one or more lines are too long

View File

@ -11,11 +11,11 @@
"""
XML Schema validators subpackage.
"""
from .exceptions import XMLSchemaValidatorError, XMLSchemaParseError, XMLSchemaValidationError, \
XMLSchemaDecodeError, XMLSchemaEncodeError, XMLSchemaNotBuiltError, XMLSchemaChildrenValidationError, \
XMLSchemaIncludeWarning, XMLSchemaImportWarning
from .exceptions import XMLSchemaValidatorError, XMLSchemaParseError, XMLSchemaModelError, \
XMLSchemaModelDepthError, XMLSchemaValidationError, XMLSchemaDecodeError, XMLSchemaEncodeError, \
XMLSchemaNotBuiltError, XMLSchemaChildrenValidationError, XMLSchemaIncludeWarning, XMLSchemaImportWarning
from .xsdbase import XsdValidator, XsdComponent, XsdAnnotation, XsdType, ParticleMixin, ValidationMixin
from .xsdbase import XsdValidator, XsdComponent, XsdAnnotation, XsdType, ValidationMixin, ParticleMixin
from .assertions import XsdAssert
from .notations import XsdNotation
@ -26,7 +26,8 @@ from .attributes import XsdAttribute, Xsd11Attribute, XsdAttributeGroup
from .simple_types import xsd_simple_type_factory, XsdSimpleType, XsdAtomic, XsdAtomicBuiltin, \
XsdAtomicRestriction, Xsd11AtomicRestriction, XsdList, XsdUnion
from .complex_types import XsdComplexType, Xsd11ComplexType
from .groups import XsdModelVisitor, XsdGroup, Xsd11Group
from .models import ModelGroup, ModelVisitor
from .groups import XsdGroup, Xsd11Group
from .elements import XsdElement, Xsd11Element
from .globals_ import XsdGlobals

View File

@ -30,7 +30,7 @@ class XsdAssert(XsdComponent, ElementPathMixin):
Content: (annotation?)
</assert>
"""
admitted_tags = {XSD_ASSERT}
_admitted_tags = {XSD_ASSERT}
token = None
def __init__(self, elem, schema, parent, base_type):

View File

@ -15,11 +15,11 @@ from __future__ import unicode_literals
from decimal import Decimal
from elementpath.datatypes import AbstractDateTime, Duration
from ..compat import MutableMapping
from ..exceptions import XMLSchemaAttributeError, XMLSchemaValueError
from ..compat import MutableMapping, ordered_dict_class
from ..exceptions import XMLSchemaAttributeError, XMLSchemaTypeError, XMLSchemaValueError
from ..qnames import XSD_ANY_SIMPLE_TYPE, XSD_SIMPLE_TYPE, XSD_ATTRIBUTE_GROUP, XSD_COMPLEX_TYPE, \
XSD_RESTRICTION, XSD_EXTENSION, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, XSD_ATTRIBUTE, XSD_ANY_ATTRIBUTE
from ..helpers import get_namespace, get_qname, local_name, prefixed_to_qname
from ..helpers import get_namespace, get_qname, get_xsd_form_attribute
from ..namespaces import XSI_NAMESPACE
from .exceptions import XMLSchemaValidationError
@ -45,7 +45,8 @@ class XsdAttribute(XsdComponent, ValidationMixin):
Content: (annotation?, simpleType?)
</attribute>
"""
admitted_tags = {XSD_ATTRIBUTE}
_admitted_tags = {XSD_ATTRIBUTE}
qualified = False
def __init__(self, elem, schema, parent, name=None, xsd_type=None):
if xsd_type is not None:
@ -63,50 +64,91 @@ class XsdAttribute(XsdComponent, ValidationMixin):
def __setattr__(self, name, value):
if name == "type":
assert isinstance(value, XsdSimpleType), "An XSD attribute's type must be a simpleType."
if not isinstance(value, XsdSimpleType):
raise XMLSchemaTypeError("An XSD attribute's type must be a simpleType.")
super(XsdAttribute, self).__setattr__(name, value)
def _parse(self):
super(XsdAttribute, self)._parse()
elem = self.elem
self.qualified = elem.attrib.get('form', self.schema.attribute_form_default) == 'qualified'
if 'default' in elem.attrib and 'fixed' in elem.attrib:
self.parse_error("'default' and 'fixed' attributes are mutually exclusive")
self._parse_properties('form', 'use')
try:
if self.is_global or self.qualified:
self.name = get_qname(self.target_namespace, elem.attrib['name'])
form = self.form
except ValueError as err:
self.parse_error(err)
else:
if form is None:
self.qualified = self.schema.attribute_form_default == 'qualified'
elif self.parent is None:
self.parse_error("attribute 'form' not allowed in a global attribute.")
else:
self.name = elem.attrib['name']
except KeyError:
# No 'name' attribute, must be a reference
self.qualified = form == 'qualified'
self.use = elem.get('use')
if self.use is None:
self.use = 'optional'
elif self.parent is None:
self.parse_error("attribute 'use' not allowed in a global attribute.")
elif self.use not in {'optional', 'prohibited', 'required'}:
self.parse_error("wrong value %r for 'use' attribute." % self.use)
self.use = 'optional'
name = elem.get('name')
if name is not None:
if 'ref' in elem.attrib:
self.parse_error("both 'name' and 'ref' in attribute declaration")
elif name == 'xmlns':
self.parse_error("an attribute name must be different from 'xmlns'")
if self.parent is None or self.qualified:
if self.target_namespace == XSI_NAMESPACE and \
name not in {'nil', 'type', 'schemaLocation', 'noNamespaceSchemaLocation'}:
self.parse_error("Cannot add attributes in %r namespace" % XSI_NAMESPACE)
self.name = get_qname(self.target_namespace, name)
else:
self.name = name
elif self.parent is None:
self.parse_error("missing 'name' in global attribute declaration")
else:
try:
attribute_name = prefixed_to_qname(elem.attrib['ref'], self.namespaces)
attribute_qname = self.schema.resolve_qname(elem.attrib['ref'])
except KeyError:
# Missing also the 'ref' attribute
self.parse_error("missing both 'name' and 'ref' in attribute declaration")
self.xsd_type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
return
except ValueError as err:
self.parse_error(err)
self.xsd_type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
return
else:
try:
xsd_attribute = self.maps.lookup_attribute(attribute_name)
xsd_attribute = self.maps.lookup_attribute(attribute_qname)
except LookupError:
self.parse_error("unknown attribute %r" % elem.attrib['ref'])
self.type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
else:
self.type = xsd_attribute.type
self.qualified = xsd_attribute.qualified
if xsd_attribute.fixed is not None and 'fixed' in elem.attrib and \
elem.get('fixed') != xsd_attribute.fixed:
self.parse_error("referenced attribute has a different fixed value %r" % xsd_attribute.fixed)
self.name = attribute_name
self.name = attribute_qname
for attribute in ('form', 'type'):
if attribute in self.elem.attrib:
self.parse_error("attribute %r is not allowed when attribute reference is used." % attribute)
xsd_declaration = self._parse_component(elem, required=False)
if xsd_declaration is not None and xsd_declaration.tag == XSD_SIMPLE_TYPE:
self.parse_error("not allowed type declaration for XSD attribute reference")
return
xsd_declaration = self._parse_component(elem, required=False)
try:
type_qname = prefixed_to_qname(elem.attrib['type'], self.namespaces)
type_qname = self.schema.resolve_qname(elem.attrib['type'])
except ValueError as err:
self.parse_error(err, elem)
xsd_type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
except KeyError:
if xsd_declaration is not None:
# No 'type' attribute in declaration, parse for child local simpleType
@ -117,19 +159,41 @@ class XsdAttribute(XsdComponent, ValidationMixin):
else:
try:
xsd_type = self.maps.lookup_type(type_qname)
except LookupError:
self.parse_error("unknown type %r." % elem.attrib['type'], elem)
except LookupError as err:
self.parse_error(err, elem)
xsd_type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
if xsd_declaration is not None and xsd_declaration.tag == XSD_SIMPLE_TYPE:
self.parse_error("ambiguous type declaration for XSD attribute")
elif xsd_declaration:
self.parse_error("not allowed element in XSD attribute declaration: %r" % xsd_declaration[0])
self.type = xsd_type
try:
self.type = xsd_type
except TypeError as err:
self.parse_error(err)
# Check value constraints
if 'default' in elem.attrib:
if 'fixed' in elem.attrib:
self.parse_error("'default' and 'fixed' attributes are mutually exclusive")
if self.use != 'optional':
self.parse_error("the attribute 'use' must be 'optional' if the attribute 'default' is present")
if not self.type.is_valid(elem.attrib['default']):
msg = "'default' value {!r} is not compatible with the type {!r}"
self.parse_error(msg.format(elem.attrib['default'], self.type))
elif self.type.is_key():
self.parse_error("'xs:ID' or a type derived from 'xs:ID' cannot has a 'default'")
elif 'fixed' in elem.attrib:
if not self.type.is_valid(elem.attrib['fixed']):
msg = "'fixed' value {!r} is not compatible with the type {!r}"
self.parse_error(msg.format(elem.attrib['fixed'], self.type))
elif self.type.is_key():
self.parse_error("'xs:ID' or a type derived from 'xs:ID' cannot has a 'default'")
@property
def built(self):
return self.type.is_global or self.type.built
return self.type.parent is None or self.type.built
@property
def validation_attempted(self):
@ -153,17 +217,7 @@ class XsdAttribute(XsdComponent, ValidationMixin):
@property
def form(self):
value = self.elem.get('form')
if value not in {None, 'qualified', 'unqualified'}:
raise XMLSchemaValueError("wrong value %r for 'form' attribute." % value)
return value
@property
def use(self):
value = self.elem.get('use', 'optional')
if value not in {'optional', 'prohibited', 'required'}:
raise XMLSchemaValueError("wrong value %r for 'use' attribute." % value)
return value
return get_xsd_form_attribute(self.elem, 'form')
def is_optional(self):
return self.use == 'optional'
@ -171,12 +225,12 @@ class XsdAttribute(XsdComponent, ValidationMixin):
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
yield self
if self.ref is None and not self.type.is_global:
if self.ref is None and self.type.parent is not None:
for obj in self.type.iter_components(xsd_classes):
yield obj
def iter_decode(self, text, validation='lax', **kwargs):
if not text and kwargs.get('use_defaults', True):
if not text and kwargs.get('use_defaults', True) and self.default is not None:
text = self.default
if self.fixed is not None and text != self.fixed and validation != 'skip':
yield self.validation_error(validation, "value differs from fixed value", text, **kwargs)
@ -252,14 +306,15 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
Content: (annotation?, ((attribute | attributeGroup)*, anyAttribute?))
</attributeGroup>
"""
admitted_tags = {
redefine = None
_admitted_tags = {
XSD_ATTRIBUTE_GROUP, XSD_COMPLEX_TYPE, XSD_RESTRICTION, XSD_EXTENSION,
XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, XSD_ATTRIBUTE, XSD_ANY_ATTRIBUTE
}
def __init__(self, elem, schema, parent, name=None, derivation=None, base_attributes=None):
self.derivation = derivation
self._attribute_group = dict()
self._attribute_group = ordered_dict_class()
self.base_attributes = base_attributes
XsdComponent.__init__(self, elem, schema, parent, name)
@ -274,20 +329,9 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
else:
return '%s()' % self.__class__.__name__
# Implements the abstract methods of MutableMapping
# Implementation of abstract methods
def __getitem__(self, key):
try:
return self._attribute_group[key]
except KeyError:
if key is None or key[:1] != '{' or get_namespace(key) != self.target_namespace:
raise
else:
# Unqualified form lookup if key is in targetNamespace
try:
return self._attribute_group[local_name(key)]
except KeyError:
pass
raise
return self._attribute_group[key]
def __setitem__(self, key, value):
if key is None:
@ -305,12 +349,9 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
raise XMLSchemaValueError("%r name and key %r mismatch." % (value.name, key))
self._attribute_group[key] = value
if value.use == 'required':
self.required.add(key)
def __delitem__(self, key):
del self._attribute_group[key]
self.required.discard(key)
def __iter__(self):
if None in self._attribute_group:
@ -332,17 +373,12 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
assert isinstance(value, XsdAnyAttribute), 'An XsdAnyAttribute instance is required.'
else:
assert isinstance(value, XsdAttribute), 'An XsdAttribute instance is required.'
self.required = {
k for k, v in self.items() if k is not None and v.use == 'required'
}
def _parse(self):
super(XsdAttributeGroup, self)._parse()
elem = self.elem
any_attribute = False
self.clear()
if self.base_attributes is not None:
self._attribute_group.update(self.base_attributes.items())
attribute_group_refs = []
if elem.tag == XSD_ATTRIBUTE_GROUP:
if self.parent is not None:
@ -353,6 +389,7 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
self.parse_error("an attribute group declaration requires a 'name' attribute.")
return
attributes = ordered_dict_class()
for child in self._iterparse_components(elem):
if any_attribute:
if child.tag == XSD_ANY_ATTRIBUTE:
@ -362,30 +399,131 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
elif child.tag == XSD_ANY_ATTRIBUTE:
any_attribute = True
self.update([(None, XsdAnyAttribute(child, self.schema, self))])
attributes.update([(None, XsdAnyAttribute(child, self.schema, self))])
elif child.tag == XSD_ATTRIBUTE:
attribute = self.schema.BUILDERS.attribute_class(child, self.schema, self)
self[attribute.name] = attribute
if attribute.name in attributes:
self.parse_error("multiple declaration for attribute {!r}".format(attribute.name))
else:
attributes[attribute.name] = attribute
elif child.tag == XSD_ATTRIBUTE_GROUP:
try:
name = child.attrib['ref']
except KeyError as err:
self.parse_error(str(err), elem)
ref = child.attrib['ref']
attribute_group_qname = self.schema.resolve_qname(ref)
except ValueError as err:
self.parse_error(err, elem)
except KeyError:
self.parse_error("the attribute 'ref' is required in a local attributeGroup", elem)
else:
qname = prefixed_to_qname(name, self.namespaces)
if attribute_group_qname in attribute_group_refs:
self.parse_error("duplicated attributeGroup %r" % ref)
elif self.redefine is not None:
if attribute_group_qname == self.name:
if attribute_group_refs:
self.parse_error("in a redefinition the reference to itself must be the first")
attribute_group_refs.append(attribute_group_qname)
attributes.update(self._attribute_group.items())
continue
elif not attribute_group_refs:
# May be an attributeGroup restriction with a ref to another group
if not any(e.tag == XSD_ATTRIBUTE_GROUP and ref == e.get('ref')
for e in self.redefine.elem):
self.parse_error("attributeGroup ref=%r is not in the redefined group" % ref)
elif attribute_group_qname == self.name and self.schema.XSD_VERSION == '1.0':
self.parse_error("Circular attribute groups not allowed in XSD 1.0")
attribute_group_refs.append(attribute_group_qname)
try:
attribute_group = self.maps.lookup_attribute_group(qname)
base_attributes = self.maps.lookup_attribute_group(attribute_group_qname)
except LookupError:
self.parse_error("unknown attribute group %r" % name, elem)
self.parse_error("unknown attribute group %r" % child.attrib['ref'], elem)
else:
self.update(attribute_group.items())
if isinstance(base_attributes, tuple):
self.parse_error("Circular reference found between attribute groups "
"{!r} and {!r}".format(self.name, attribute_group_qname))
for name, attr in base_attributes.items():
if name is not None and name in attributes:
self.parse_error("multiple declaration for attribute {!r}".format(name))
else:
attributes[name] = attr
elif self.name is not None:
self.parse_error("(attribute | attributeGroup) expected, found %r." % child)
if self.parent is None and getattr(self.schema, 'default_attributes', None) == self.name:
# Check and copy base attributes
if self.base_attributes is not None:
wildcard = self.base_attributes.get(None)
for name, attr in attributes.items():
if name not in self.base_attributes:
if self.derivation != 'restriction':
continue
elif wildcard is None or not wildcard.is_matching(name, self.default_namespace):
self.parse_error("Unexpected attribute %r in restriction" % name)
continue
base_attr = self.base_attributes[name]
if name is None:
if self.derivation == 'extension':
try:
attr.extend_namespace(base_attr)
except ValueError as err:
self.parse_error(err)
elif not attr.is_restriction(base_attr):
self.parse_error("Attribute wildcard is not a restriction of the base wildcard")
continue
if self.derivation == 'restriction' and attr.type.name != XSD_ANY_SIMPLE_TYPE and \
not attr.type.is_derived(base_attr.type, 'restriction'):
self.parse_error("Attribute type is not a restriction of the base attribute type")
if base_attr.use != 'optional' and attr.use == 'optional' or \
base_attr.use == 'required' and attr.use != 'required':
self.parse_error("Attribute %r: unmatched attribute use in restriction" % name)
if base_attr.fixed is not None and \
attr.type.normalize(attr.fixed) != base_attr.type.normalize(base_attr.fixed):
self.parse_error("Attribute %r: derived attribute has a different fixed value" % name)
self._attribute_group.update(self.base_attributes.items())
elif self.redefine is not None and not attribute_group_refs:
for name, attr in self._attribute_group.items():
if name is None:
continue
elif name not in attributes:
if attr.use == 'required':
self.parse_error("Missing required attribute %r in redefinition restriction" % name)
continue
if attr.use != 'optional' and attributes[name].use != attr.use:
self.parse_error("Attribute %r: unmatched attribute use in redefinition" % name)
if attr.fixed is not None and attributes[name].fixed is None:
self.parse_error("Attribute %r: redefinition remove fixed constraint" % name)
pos = 0
keys = list(self._attribute_group.keys())
for name in attributes:
try:
next_pos = keys.index(name)
except ValueError:
self.parse_error("Redefinition restriction contains additional attribute %r" % name)
else:
if next_pos < pos:
self.parse_error("Wrong attribute order in redefinition restriction")
break
pos = next_pos
self.clear()
self._attribute_group.update(attributes)
if self.schema.XSD_VERSION == '1.0':
has_key = False
for attr in self._attribute_group.values():
if attr.name is not None and attr.type.is_key():
if has_key:
self.parse_error("multiple key attributes in a group not allowed in XSD 1.0")
has_key = True
elif self.parent is None and self.schema.default_attributes == self.name:
self.schema.default_attributes = self
@property
@ -405,12 +543,17 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
def ref(self):
return self.elem.get('ref')
def iter_required(self):
for k, v in self._attribute_group.items():
if k is not None and v.use == 'required':
yield k
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
yield self
if self.ref is None:
for attr in self.values():
if not attr.is_global:
if attr.parent is not None:
for obj in attr.iter_components(xsd_classes):
yield obj
@ -419,7 +562,7 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
return
result_list = []
required_attributes = self.required.copy()
required_attributes = {a for a in self.iter_required()}
for name, value in attrs.items():
try:
xsd_attribute = self[name]
@ -459,7 +602,7 @@ class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin):
def iter_encode(self, attrs, validation='lax', **kwargs):
result_list = []
required_attributes = self.required.copy()
required_attributes = {a for a in self.iter_required()}
try:
attrs = attrs.items()
except AttributeError:

View File

@ -19,6 +19,7 @@ from __future__ import unicode_literals
import re
import base64
from decimal import Decimal
from math import isinf, isnan
from elementpath import datatypes
@ -61,7 +62,20 @@ DATETIME_FACETS = (
#
# XSD numerical built-in types validator functions
# XSD built-in types validator functions
def finite_number_validator(x):
try:
if isinf(x) or isnan(x):
yield XMLSchemaValidationError(finite_number_validator, x, "value {!r} is not an xs:decimal".format(x))
except TypeError:
pass
def qname_validator(x):
if datatypes.QNAME_PATTERN.match(x) is None:
yield XMLSchemaValidationError(qname_validator, x, "value {!r} is not an xs:QName".format(x))
def byte_validator(x):
if not (-2**7 <= x < 2**7):
yield XMLSchemaValidationError(int_validator, x, "value must be -128 <= x < 128.")
@ -123,11 +137,13 @@ def non_negative_int_validator(x):
def hex_binary_validator(x):
if len(x) % 2 or HEX_BINARY_PATTERN.match(x) is None:
if x and (len(x) % 2 or HEX_BINARY_PATTERN.match(x) is None):
yield XMLSchemaValidationError(hex_binary_validator, x, "not an hexadecimal number.")
def base64_binary_validator(x):
if not x:
return
match = NOT_BASE64_BINARY_PATTERN.search(x)
if match is not None:
reason = "not a base64 encoding: illegal character %r at position %d." % (match.group(0), match.span()[0])
@ -156,9 +172,9 @@ def python_to_boolean(obj):
#
# Element facets instances for builtin types.
PRESERVE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, attrib={'value': 'preserve'})
COLLAPSE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, attrib={'value': 'collapse'})
REPLACE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, attrib={'value': 'replace'})
PRESERVE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='preserve')
COLLAPSE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='collapse')
REPLACE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='replace')
XSD_COMMON_BUILTIN_TYPES = (
@ -179,7 +195,7 @@ XSD_COMMON_BUILTIN_TYPES = (
'name': XSD_DECIMAL,
'python_type': (Decimal, str, unicode_type, int, float),
'admitted_facets': DECIMAL_FACETS,
'facets': [COLLAPSE_WHITE_SPACE_ELEMENT],
'facets': [finite_number_validator, COLLAPSE_WHITE_SPACE_ELEMENT],
}, # decimal number
{
'name': XSD_DOUBLE,
@ -236,8 +252,8 @@ XSD_COMMON_BUILTIN_TYPES = (
'name': XSD_QNAME,
'python_type': (unicode_type, str),
'admitted_facets': STRING_FACETS,
'facets': [COLLAPSE_WHITE_SPACE_ELEMENT],
}, # prf:name (the prefix needs to be qualified with an in scope namespace)
'facets': [COLLAPSE_WHITE_SPACE_ELEMENT, qname_validator],
}, # prf:name (the prefix needs to be qualified with an in-scope namespace)
{
'name': XSD_NOTATION_TYPE,
'python_type': (unicode_type, str),
@ -293,22 +309,20 @@ XSD_COMMON_BUILTIN_TYPES = (
'python_type': (unicode_type, str),
'base_type': XSD_TOKEN,
'facets': [
etree_element(XSD_PATTERN, attrib={
'value': r"([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*"
})
etree_element(XSD_PATTERN, value=r"([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*")
]
}, # language codes
{
'name': XSD_NAME,
'python_type': (unicode_type, str),
'base_type': XSD_TOKEN,
'facets': [etree_element(XSD_PATTERN, attrib={'value': r"\i\c*"})]
'facets': [etree_element(XSD_PATTERN, value=r"\i\c*")]
}, # not starting with a digit
{
'name': XSD_NCNAME,
'python_type': (unicode_type, str),
'base_type': XSD_NAME,
'facets': [etree_element(XSD_PATTERN, attrib={'value': r"[\i-[:]][\c-[:]]*"})]
'facets': [etree_element(XSD_PATTERN, value=r"[\i-[:]][\c-[:]]*")]
}, # cannot contain colons
{
'name': XSD_ID,
@ -329,7 +343,7 @@ XSD_COMMON_BUILTIN_TYPES = (
'name': XSD_NMTOKEN,
'python_type': (unicode_type, str),
'base_type': XSD_TOKEN,
'facets': [etree_element(XSD_PATTERN, attrib={'value': r"\c+"})]
'facets': [etree_element(XSD_PATTERN, value=r"\c+")]
}, # should not contain whitespace (attribute only)
# --- Numerical derived types ---
@ -342,73 +356,81 @@ XSD_COMMON_BUILTIN_TYPES = (
'name': XSD_LONG,
'python_type': int if PY3 else (long_type, int),
'base_type': XSD_INTEGER,
'facets': [long_validator]
'facets': [long_validator,
etree_element(XSD_MIN_INCLUSIVE, value='-9223372036854775808'),
etree_element(XSD_MAX_INCLUSIVE, value='9223372036854775807')]
}, # signed 128 bit value
{
'name': XSD_INT,
'python_type': int,
'base_type': XSD_LONG,
'facets': [int_validator]
'facets': [int_validator,
etree_element(XSD_MIN_INCLUSIVE, value='-2147483648'),
etree_element(XSD_MAX_INCLUSIVE, value='2147483647')]
}, # signed 64 bit value
{
'name': XSD_SHORT,
'python_type': int,
'base_type': XSD_INT,
'facets': [short_validator]
'facets': [short_validator,
etree_element(XSD_MIN_INCLUSIVE, value='-32768'),
etree_element(XSD_MAX_INCLUSIVE, value='32767')]
}, # signed 32 bit value
{
'name': XSD_BYTE,
'python_type': int,
'base_type': XSD_SHORT,
'facets': [byte_validator]
'facets': [byte_validator,
etree_element(XSD_MIN_INCLUSIVE, value='-128'),
etree_element(XSD_MAX_INCLUSIVE, value='127')]
}, # signed 8 bit value
{
'name': XSD_NON_NEGATIVE_INTEGER,
'python_type': int if PY3 else (long_type, int),
'base_type': XSD_INTEGER,
'facets': [non_negative_int_validator]
'facets': [non_negative_int_validator, etree_element(XSD_MIN_INCLUSIVE, value='0')]
}, # only zero and more value allowed [>= 0]
{
'name': XSD_POSITIVE_INTEGER,
'python_type': int if PY3 else (long_type, int),
'base_type': XSD_NON_NEGATIVE_INTEGER,
'facets': [positive_int_validator]
'facets': [positive_int_validator, etree_element(XSD_MIN_INCLUSIVE, value='1')]
}, # only positive value allowed [> 0]
{
'name': XSD_UNSIGNED_LONG,
'python_type': int if PY3 else (long_type, int),
'base_type': XSD_NON_NEGATIVE_INTEGER,
'facets': [unsigned_long_validator]
'facets': [unsigned_long_validator, etree_element(XSD_MAX_INCLUSIVE, value='18446744073709551615')]
}, # unsigned 128 bit value
{
'name': XSD_UNSIGNED_INT,
'python_type': int,
'base_type': XSD_UNSIGNED_LONG,
'facets': [unsigned_int_validator]
'facets': [unsigned_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='4294967295')]
}, # unsigned 64 bit value
{
'name': XSD_UNSIGNED_SHORT,
'python_type': int,
'base_type': XSD_UNSIGNED_INT,
'facets': [unsigned_short_validator]
'facets': [unsigned_short_validator, etree_element(XSD_MAX_INCLUSIVE, value='65535')]
}, # unsigned 32 bit value
{
'name': XSD_UNSIGNED_BYTE,
'python_type': int,
'base_type': XSD_UNSIGNED_SHORT,
'facets': [unsigned_byte_validator]
'facets': [unsigned_byte_validator, etree_element(XSD_MAX_INCLUSIVE, value='255')]
}, # unsigned 8 bit value
{
'name': XSD_NON_POSITIVE_INTEGER,
'python_type': (long_type, int),
'base_type': XSD_INTEGER,
'facets': [non_positive_int_validator]
'facets': [non_positive_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='0')]
}, # only zero and smaller value allowed [<= 0]
{
'name': XSD_NEGATIVE_INTEGER,
'python_type': (long_type, int),
'base_type': XSD_NON_POSITIVE_INTEGER,
'facets': [negative_int_validator]
'facets': [negative_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='-1')]
}, # only negative value allowed [< 0]
)
@ -480,7 +502,7 @@ XSD_11_BUILTIN_TYPES = XSD_COMMON_BUILTIN_TYPES + (
'python_type': (unicode_type, str),
'base_type': XSD_DATETIME,
'to_python': datatypes.DateTime.fromstring,
'facets': [etree_element(XSD_EXPLICIT_TIMEZONE, attrib={'value': 'required'})],
'facets': [etree_element(XSD_EXPLICIT_TIMEZONE, value='required')],
}, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] with required timezone
{
'name': XSD_DAY_TIME_DURATION,
@ -515,7 +537,7 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, atomic_builtin_class=None)
# xs:anyType
# Ref: https://www.w3.org/TR/xmlschema11-1/#builtin-ctd
any_type = meta_schema.BUILDERS.complex_type_class(
elem=etree_element(XSD_COMPLEX_TYPE, attrib={'name': XSD_ANY_TYPE}),
elem=etree_element(XSD_COMPLEX_TYPE, name=XSD_ANY_TYPE),
schema=meta_schema,
parent=None,
mixed=True
@ -527,7 +549,7 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, atomic_builtin_class=None)
# xs:anySimpleType
# Ref: https://www.w3.org/TR/xmlschema11-2/#builtin-stds
xsd_types[XSD_ANY_SIMPLE_TYPE] = XsdSimpleType(
elem=etree_element(XSD_SIMPLE_TYPE, attrib={'name': XSD_ANY_SIMPLE_TYPE}),
elem=etree_element(XSD_SIMPLE_TYPE, name=XSD_ANY_SIMPLE_TYPE),
schema=meta_schema,
parent=None,
name=XSD_ANY_SIMPLE_TYPE
@ -536,7 +558,7 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, atomic_builtin_class=None)
# xs:anyAtomicType
# Ref: https://www.w3.org/TR/xmlschema11-2/#builtin-stds
xsd_types[XSD_ANY_ATOMIC_TYPE] = meta_schema.BUILDERS.restriction_class(
elem=etree_element(XSD_SIMPLE_TYPE, attrib={'name': XSD_ANY_ATOMIC_TYPE}),
elem=etree_element(XSD_SIMPLE_TYPE, name=XSD_ANY_ATOMIC_TYPE),
schema=meta_schema,
parent=None,
name=XSD_ANY_ATOMIC_TYPE,
@ -551,14 +573,13 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, atomic_builtin_class=None)
except KeyError:
# If builtin type element is missing create a dummy element. Necessary for the
# meta-schema XMLSchema.xsd of XSD 1.1, that not includes builtins declarations.
elem = etree_element(XSD_SIMPLE_TYPE, attrib={'name': name, 'id': name})
elem = etree_element(XSD_SIMPLE_TYPE, name=name, id=name)
else:
if schema is not meta_schema:
raise XMLSchemaValueError("loaded entry schema doesn't match meta_schema!")
if 'base_type' in item:
base_type = item.get('base_type')
item['base_type'] = xsd_types[base_type]
base_type = item['base_type'] = xsd_types[item['base_type']]
else:
base_type = None

View File

@ -10,10 +10,11 @@
#
from __future__ import unicode_literals
from ..exceptions import XMLSchemaValueError
from ..qnames import XSD_GROUP, XSD_ATTRIBUTE_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, \
XSD_ANY_ATTRIBUTE, XSD_ATTRIBUTE, XSD_COMPLEX_CONTENT, XSD_RESTRICTION, XSD_COMPLEX_TYPE, \
XSD_EXTENSION, XSD_ANY_TYPE, XSD_SIMPLE_CONTENT, XSD_ANY_SIMPLE_TYPE, XSD_OPEN_CONTENT, XSD_ASSERT
from ..helpers import get_qname, local_name, prefixed_to_qname, get_xml_bool_attribute, get_xsd_derivation_attribute
from ..helpers import get_qname, local_name, get_xml_bool_attribute, get_xsd_derivation_attribute
from ..etree import etree_element
from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError
@ -44,32 +45,47 @@ class XsdComplexType(XsdType, ValidationMixin):
((group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?))))
</complexType>
"""
admitted_tags = {XSD_COMPLEX_TYPE, XSD_RESTRICTION}
_admitted_tags = {XSD_COMPLEX_TYPE, XSD_RESTRICTION}
assertions = ()
mixed = False
_block = None
_derivation = None
def __init__(self, elem, schema, parent, name=None, content_type=None, attributes=None, mixed=None):
self.base_type = None
self.content_type = content_type
self.attributes = attributes
self.mixed = mixed
self._derivation = None
@staticmethod
def normalize(text):
return text.decode('utf-8') if isinstance(text, bytes) else text
def __init__(self, elem, schema, parent, name=None, **kwargs):
if kwargs:
if 'content_type' in kwargs:
self.content_type = kwargs['content_type']
if 'attributes' in kwargs:
self.attributes = kwargs['attributes']
if 'mixed' in kwargs:
self.mixed = kwargs['mixed']
if 'block' in kwargs:
self._block = kwargs['block']
if 'final' in kwargs:
self._final = kwargs['final']
super(XsdComplexType, self).__init__(elem, schema, parent, name)
def __repr__(self):
if self.name is None:
if self.name is not None:
return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name)
elif not hasattr(self, 'content_type'):
return '%s(id=%r)' % (self.__class__.__name__, id(self))
else:
return '%s(content=%r, attributes=%r)' % (
self.__class__.__name__, self.content_type_label,
[a if a.name is None else a.prefixed_name for a in self.attributes.values()]
)
else:
return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name)
def __setattr__(self, name, value):
if name == 'content_type':
assert value is None or isinstance(value, (XsdSimpleType, XsdGroup)), \
assert isinstance(value, (XsdSimpleType, XsdGroup)), \
"The attribute 'content_type' must be a XsdSimpleType or an XsdGroup instance."
elif name == 'attributes':
assert value is None or isinstance(value, XsdAttributeGroup), \
assert isinstance(value, XsdAttributeGroup), \
"The attribute 'attributes' must be an XsdAttributeGroup."
super(XsdComplexType, self).__setattr__(name, value)
@ -79,15 +95,36 @@ class XsdComplexType(XsdType, ValidationMixin):
if elem.tag == XSD_RESTRICTION:
return # a local restriction is already parsed by the caller
self.mixed = get_xml_bool_attribute(elem, 'mixed', default=False)
self._parse_properties('abstract', 'block', 'final')
if 'abstract' in elem.attrib:
try:
self.abstract = get_xml_bool_attribute(elem, 'abstract')
except ValueError as err:
self.parse_error(err, elem)
if 'block' in elem.attrib:
try:
self._block = get_xsd_derivation_attribute(elem, 'block', ('extension', 'restriction'))
except ValueError as err:
self.parse_error(err, elem)
if 'final' in elem.attrib:
try:
self._final = get_xsd_derivation_attribute(elem, 'final', ('extension', 'restriction'))
except ValueError as err:
self.parse_error(err, elem)
if 'mixed' in elem.attrib:
try:
self.mixed = get_xml_bool_attribute(elem, 'mixed')
except ValueError as err:
self.parse_error(err, elem)
try:
self.name = get_qname(self.target_namespace, elem.attrib['name'])
except KeyError:
self.name = None
else:
if not self.is_global:
if self.parent is not None:
self.parse_error("attribute 'name' not allowed for a local complexType", elem)
content_elem = self._parse_component(elem, required=False, strict=False)
@ -141,9 +178,25 @@ class XsdComplexType(XsdType, ValidationMixin):
if content_elem is not elem[-1]:
k = 2 if content_elem is not elem[0] else 1
self.parse_error("unexpected tag %r after complexContent declaration:" % elem[k].tag, elem)
if base_type is not self:
if self.redefine or base_type is not self:
self.base_type = base_type
elif content_elem.tag == XSD_OPEN_CONTENT and self.schema.XSD_VERSION != '1.0':
self.open_content = None
if content_elem is elem[-1]:
self.content_type = self.schema.BUILDERS.group_class(SEQUENCE_ELEMENT, self.schema, self)
else:
for child, index in enumerate(elem):
if content_elem is not child:
continue
elif elem[index + 1].tag in {XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}:
self.content_type = self.schema.BUILDERS.group_class(elem[index + 1], self.schema, self)
else:
self.content_type = self.schema.BUILDERS.group_class(SEQUENCE_ELEMENT, self.schema, self)
break
self._parse_content_tail(elem)
else:
if self.schema.validation == 'skip':
# Also generated by meta-schema validation for 'lax' and 'strict' modes
@ -151,6 +204,12 @@ class XsdComplexType(XsdType, ValidationMixin):
self.content_type = self.schema.create_any_content_group(self)
self.attributes = self.schema.create_any_attribute_group(self)
if self.redefine is None:
if self.base_type is not None and self.base_type.name == self.name:
self.parse_error("wrong definition with self-reference", elem)
elif self.base_type is None or self.base_type.name != self.name:
self.parse_error("wrong redefinition without self-reference", elem)
def _parse_content_tail(self, elem, **kwargs):
self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self, **kwargs)
@ -163,33 +222,41 @@ class XsdComplexType(XsdType, ValidationMixin):
return
derivation = local_name(derivation_elem.tag)
self._derivation = derivation == 'extension'
if self._derivation is None:
self._derivation = derivation == 'extension'
elif self.redefine is None:
raise XMLSchemaValueError("%r is expected to have a redefined/overridden component" % self)
if self.base_type is not None and derivation in self.base_type.final:
self.parse_error("%r derivation not allowed for %r." % (derivation, self))
return derivation_elem
def _parse_base_type(self, elem, complex_content=False):
try:
content_base = elem.attrib['base']
base_qname = self.schema.resolve_qname(elem.attrib['base'])
except KeyError:
self.parse_error("'base' attribute required", elem)
return self.maps.lookup_type(XSD_ANY_TYPE)
return self.maps.types[XSD_ANY_TYPE]
except ValueError as err:
self.parse_error(err, elem)
return self.maps.types[XSD_ANY_TYPE]
base_qname = prefixed_to_qname(content_base, self.namespaces)
try:
base_type = self.maps.lookup_type(base_qname)
except KeyError:
self.parse_error("missing base type %r" % base_qname, elem)
if complex_content:
return self.maps.lookup_type(XSD_ANY_TYPE)
return self.maps.types[XSD_ANY_TYPE]
else:
return self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
return self.maps.types[XSD_ANY_SIMPLE_TYPE]
else:
if complex_content and base_type.is_simple():
if isinstance(base_type, tuple):
self.parse_error("circularity definition found between %r and %r" % (self, base_qname), elem)
return self.maps.types[XSD_ANY_TYPE]
elif complex_content and base_type.is_simple():
self.parse_error("a complexType ancestor required: %r" % base_type, elem)
return self.maps.lookup_type(XSD_ANY_TYPE)
else:
return base_type
return self.maps.types[XSD_ANY_TYPE]
return base_type
def _parse_simple_content_restriction(self, elem, base_type):
# simpleContent restriction: the base type must be a complexType with a simple
@ -199,7 +266,12 @@ class XsdComplexType(XsdType, ValidationMixin):
self.content_type = self.schema.create_any_content_group(self)
self._parse_content_tail(elem)
else:
if base_type.has_simple_content() or base_type.mixed and base_type.is_emptiable():
if base_type.has_simple_content():
self.content_type = self.schema.BUILDERS.restriction_class(elem, self.schema, self)
if not self.content_type.is_derived(base_type.content_type, 'restriction'):
self.parse_error("Content type is not a restriction of base content type", elem)
elif base_type.mixed and base_type.is_emptiable():
self.content_type = self.schema.BUILDERS.restriction_class(elem, self.schema, self)
else:
self.parse_error("with simple content cannot restrict an empty or "
@ -226,18 +298,19 @@ class XsdComplexType(XsdType, ValidationMixin):
self.parse_error("base type %r has not simple content." % base_type, elem)
self.content_type = self.schema.create_any_content_group(self)
self._parse_content_tail(elem, derivation='restriction', base_attributes=base_type.attributes)
self._parse_content_tail(elem, derivation='extension', base_attributes=base_type.attributes)
def _parse_complex_content_restriction(self, elem, base_type):
if 'restriction' in base_type.final:
self.parse_error("the base type is not derivable by restriction")
if base_type.is_simple() or base_type.has_simple_content():
self.parse_error("base %r is simple or has a simple content." % base_type, elem)
base_type = self.maps.types[XSD_ANY_TYPE]
# complexContent restriction: the base type must be a complexType with a complex content.
group_elem = self._parse_component(elem, required=False, strict=False)
if group_elem is not None and group_elem.tag in XSD_MODEL_GROUP_TAGS:
content_type = self.schema.BUILDERS.group_class(group_elem, self.schema, self)
model = content_type.model
if model != 'sequence' and model != base_type.content_type.model:
self.parse_error(
"cannot restrict a %r model to %r." % (base_type.content_type.model, model), elem
)
else:
# Empty content model
content_type = self.schema.BUILDERS.group_class(elem, self.schema, self)
@ -252,17 +325,15 @@ class XsdComplexType(XsdType, ValidationMixin):
)
if base_type.name != XSD_ANY_TYPE and not base_type.is_empty() and False:
if not content_type.is_restriction(base_type.content_type):
if not content_type.has_occurs_restriction(base_type.content_type):
self.parse_error("The derived group %r is not a restriction of the base group." % elem, elem)
self.content_type = content_type
self._parse_content_tail(elem, derivation='restriction', base_attributes=base_type.attributes)
def _parse_complex_content_extension(self, elem, base_type):
# complexContent extension: base type must be a complex type with complex content.
# A dummy sequence group is added if the base type has not empty content model.
if getattr(base_type.content_type, 'model', None) == 'all' and self.schema.XSD_VERSION == '1.0':
self.parse_error("XSD 1.0 does not allow extension of an 'ALL' model group.", elem)
if 'extension' in base_type.final:
self.parse_error("the base type is not derivable by extension")
group_elem = self._parse_component(elem, required=False, strict=False)
if base_type.is_empty():
@ -284,21 +355,33 @@ class XsdComplexType(XsdType, ValidationMixin):
# https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#sec-cos-ct-extends
if base_type.is_simple() or base_type.has_simple_content():
self.parse_error("base %r is simple or has a simple content." % base_type, elem)
base_type = self.maps.lookup_type(XSD_ANY_TYPE)
base_type = self.maps.types[XSD_ANY_TYPE]
group = self.schema.BUILDERS.group_class(group_elem, self.schema, self)
if group.model == 'all':
self.parse_error("Cannot extend a complex content with an all model")
content_type.append(base_type.content_type)
content_type.append(group)
sequence_elem.append(base_type.content_type.elem)
sequence_elem.append(group.elem)
if base_type.mixed != self.mixed and not group.is_empty():
# complexContent extension: base type must be a complex type with complex content.
# A dummy sequence group is added if the base type has not empty content model.
if base_type.content_type.model == 'all' and base_type.content_type and group \
and self.schema.XSD_VERSION == '1.0':
self.parse_error("XSD 1.0 does not allow extension of a not empty 'ALL' model group.", elem)
if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE:
self.parse_error("base has a different content type (mixed=%r) and the "
"extension group is not empty." % base_type.mixed, elem)
self.mixed = base_type.mixed
elif not base_type.is_simple() and not base_type.has_simple_content():
content_type.append(base_type.content_type)
sequence_elem.append(base_type.content_type.elem)
if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE and self.mixed:
self.parse_error("extended type has a mixed content but the base is element-only", elem)
self.content_type = content_type
self._parse_content_tail(elem, derivation='extension', base_attributes=base_type.attributes)
@ -321,6 +404,10 @@ class XsdComplexType(XsdType, ValidationMixin):
else:
return 'none'
@property
def block(self):
return self.schema.block_default if self._block is None else self._block
@staticmethod
def is_simple():
return False
@ -363,17 +450,33 @@ class XsdComplexType(XsdType, ValidationMixin):
def is_list(self):
return self.has_simple_content() and self.content_type.is_list()
@property
def abstract(self):
return get_xml_bool_attribute(self.elem, 'abstract', default=False)
def is_valid(self, source, use_defaults=True):
if hasattr(source, 'tag'):
return super(XsdComplexType, self).is_valid(source, use_defaults)
elif isinstance(self.content_type, XsdSimpleType):
return self.content_type.is_valid(source)
else:
return self.base_type is not None and self.base_type.is_valid(source) or self.mixed
@property
def block(self):
return get_xsd_derivation_attribute(self.elem, 'block', ('extension', 'restriction'))
@property
def final(self):
return get_xsd_derivation_attribute(self.elem, 'final', ('extension', 'restriction'))
def is_derived(self, other, derivation=None):
if self is other:
return True
elif derivation and self.derivation and derivation != self.derivation and other.is_complex():
return False
elif other.name == XSD_ANY_TYPE:
return True
elif self.base_type is other:
return True
elif hasattr(other, 'member_types'):
return any(self.is_derived(m, derivation) for m in other.member_types)
elif self.base_type is None:
if not self.has_simple_content():
return False
return self.content_type.is_derived(other, derivation)
elif self.has_simple_content():
return self.content_type.is_derived(other, derivation) or self.base_type.is_derived(other, derivation)
else:
return self.base_type.is_derived(other, derivation)
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
@ -387,7 +490,7 @@ class XsdComplexType(XsdType, ValidationMixin):
for obj in self.assertions:
if xsd_classes is None or isinstance(obj, xsd_classes):
yield obj
yield obj
@staticmethod
def get_facet(*_args, **_kwargs):
@ -409,17 +512,6 @@ class XsdComplexType(XsdType, ValidationMixin):
def has_extension(self):
return self._derivation is True
def check_restriction(self):
if self._derivation is not False:
return
elif isinstance(self.content_type, XsdGroup):
base_type = self.base_type
if base_type.name != XSD_ANY_TYPE and base_type.is_complex() and base_type:
if not self.content_type.is_restriction(base_type.content_type):
self.parse_error(
"The derived group is an illegal restriction of the base type group.", self.elem
)
def decode(self, data, *args, **kwargs):
if hasattr(data, 'attrib') or self.is_simple():
return super(XsdComplexType, self).decode(data, *args, **kwargs)

View File

@ -17,17 +17,18 @@ from elementpath import XPath2Parser, ElementPathSyntaxError, XPathContext
from elementpath.xpath_helpers import boolean_value
from elementpath.datatypes import AbstractDateTime, Duration
from ..exceptions import XMLSchemaAttributeError, XMLSchemaValueError
from ..exceptions import XMLSchemaAttributeError
from ..qnames import XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, XSD_ATTRIBUTE_GROUP, \
XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE, XSD_ALTERNATIVE, XSD_ELEMENT, XSD_ANY_TYPE, XSD_UNIQUE, \
XSD_KEY, XSD_KEYREF, XSI_NIL, XSI_TYPE
from ..helpers import get_qname, prefixed_to_qname, get_xml_bool_attribute, get_xsd_derivation_attribute
XSD_KEY, XSD_KEYREF, XSI_NIL, XSI_TYPE, XSD_ID
from ..helpers import get_qname, get_xml_bool_attribute, get_xsd_derivation_attribute, \
get_xsd_form_attribute, ParticleCounter
from ..etree import etree_element
from ..converters import ElementData, raw_xml_encode, XMLSchemaConverter
from ..xpath import ElementPathMixin
from .exceptions import XMLSchemaValidationError
from .xsdbase import XsdComponent, XsdType, ParticleMixin, ValidationMixin
from .xsdbase import XsdComponent, XsdType, ValidationMixin, ParticleMixin
from .identities import XsdUnique, XsdKey, XsdKeyref
from .wildcards import XsdAnyElement
@ -59,7 +60,13 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
Content: (annotation?, ((simpleType | complexType)?, (unique | key | keyref)*))
</element>
"""
admitted_tags = {XSD_ELEMENT}
_admitted_tags = {XSD_ELEMENT}
qualified = False
_ref = None
_abstract = False
_block = None
_final = None
_substitution_group = None
def __init__(self, elem, schema, parent, name=None):
super(XsdElement, self).__init__(elem, schema, parent, name)
@ -96,66 +103,120 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
self._parse_attributes()
index = self._parse_type()
self._parse_identity_constraints(index)
self._parse_substitution_group()
if self.parent is None:
self._parse_substitution_group()
def _parse_attributes(self):
self._parse_particle(self.elem)
self.name = None
self._ref = None
self.qualified = self.elem.get('form', self.schema.element_form_default) == 'qualified'
elem = self.elem
attrib = elem.attrib
self._parse_particle(elem)
if self.default is not None and self.fixed is not None:
self.parse_error("'default' and 'fixed' attributes are mutually exclusive.")
self._parse_properties('abstract', 'block', 'final', 'form', 'nillable')
# Parse element attributes
try:
element_name = prefixed_to_qname(self.elem.attrib['ref'], self.namespaces)
except KeyError:
# No 'ref' attribute ==> 'name' attribute required.
try:
if self.is_global or self.qualified:
self.name = get_qname(self.target_namespace, self.elem.attrib['name'])
else:
self.name = self.elem.attrib['name']
except KeyError:
self.parse_error("missing both 'name' and 'ref' attributes.")
self.qualified = (self.form or self.schema.element_form_default) == 'qualified'
except ValueError as err:
self.parse_error(err)
if self.is_global:
if 'minOccurs' in self.elem.attrib:
self.parse_error("attribute 'minOccurs' not allowed for a global element.")
if 'maxOccurs' in self.elem.attrib:
self.parse_error("attribute 'maxOccurs' not allowed for a global element.")
else:
# Reference to a global element
if self.is_global:
self.parse_error("an element reference can't be global.")
for attribute in ('name', 'type', 'nillable', 'default', 'fixed', 'form', 'block'):
if attribute in self.elem.attrib:
self.parse_error("attribute %r is not allowed when element reference is used." % attribute)
try:
xsd_element = self.maps.lookup_element(element_name)
except KeyError:
self.parse_error('unknown element %r' % element_name)
self.name = element_name
self.type = self.maps.lookup_type(XSD_ANY_TYPE)
name = elem.get('name')
if name is not None:
if self.parent is None or self.qualified:
self.name = get_qname(self.target_namespace, attrib['name'])
else:
self._ref = xsd_element
self.name = xsd_element.name
self.type = xsd_element.type
self.qualified = xsd_element.qualified
self.name = attrib['name']
elif self.parent is None:
self.parse_error("missing 'name' in a global element declaration")
self.name = elem.get('ref', 'nameless_%s' % str(id(self)))
elif 'ref' not in attrib:
self.parse_error("missing both 'name' and 'ref' attributes")
self.name = elem.get('nameless_%s' % str(id(self)))
else:
try:
element_name = self.schema.resolve_qname(attrib['ref'])
except ValueError as err:
self.parse_error(err)
self.type = self.maps.types[XSD_ANY_TYPE]
self.name = elem.get('nameless_%s' % str(id(self)))
else:
if not element_name:
self.parse_error("empty 'ref' attribute")
self.type = self.maps.types[XSD_ANY_TYPE]
self.name = elem.get('nameless_%s' % str(id(self)))
else:
try:
xsd_element = self.maps.lookup_element(element_name)
except KeyError:
self.parse_error('unknown element %r' % element_name)
self.name = element_name
self.type = self.maps.types[XSD_ANY_TYPE]
else:
self._ref = xsd_element
self.name = xsd_element.name
self.type = xsd_element.type
self.qualified = xsd_element.qualified
for attr_name in ('name', 'type', 'nillable', 'default', 'fixed', 'form',
'block', 'abstract', 'final', 'substitutionGroup'):
if attr_name in attrib:
self.parse_error("attribute %r is not allowed when element reference is used." % attr_name)
return
if 'default' in attrib and 'fixed' in attrib:
self.parse_error("'default' and 'fixed' attributes are mutually exclusive.")
if 'abstract' in elem.attrib:
try:
self._abstract = get_xml_bool_attribute(elem, 'abstract')
except ValueError as err:
self.parse_error(err, elem)
else:
if self.parent is not None:
self.parse_error("local scope elements cannot have abstract attribute")
if 'block' in elem.attrib:
try:
self._block = get_xsd_derivation_attribute(
elem, 'block', ('extension', 'restriction', 'substitution')
)
except ValueError as err:
self.parse_error(err, elem)
if self.parent is None:
self._parse_properties('nillable')
if 'final' in elem.attrib:
try:
self._final = get_xsd_derivation_attribute(elem, 'final', ('extension', 'restriction'))
except ValueError as err:
self.parse_error(err, elem)
for attr_name in ('ref', 'form', 'minOccurs', 'maxOccurs'):
if attr_name in attrib:
self.parse_error("attribute %r not allowed in a global element declaration" % attr_name)
else:
self._parse_properties('form', 'nillable')
for attr_name in ('final', 'substitutionGroup'):
if attr_name in attrib:
self.parse_error("attribute %r not allowed in a local element declaration" % attr_name)
def _parse_type(self):
attrib = self.elem.attrib
if self.ref:
if self._parse_component(self.elem, required=False, strict=False) is not None:
self.parse_error("element reference declaration can't has children.")
elif 'type' in self.elem.attrib:
type_qname = prefixed_to_qname(self.elem.attrib['type'], self.namespaces)
elif 'type' in attrib:
try:
self.type = self.maps.lookup_type(type_qname)
self.type = self.maps.lookup_type(self.schema.resolve_qname(attrib['type']))
except KeyError:
self.parse_error('unknown type %r' % self.elem.attrib['type'])
self.type = self.maps.lookup_type(XSD_ANY_TYPE)
self.parse_error('unknown type %r' % attrib['type'])
self.type = self.maps.types[XSD_ANY_TYPE]
except ValueError as err:
self.parse_error(err)
self.type = self.maps.types[XSD_ANY_TYPE]
finally:
child = self._parse_component(self.elem, required=False, strict=False)
if child is not None and child.tag in (XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE):
msg = "the attribute 'type' and the <%s> local declaration are mutually exclusive"
self.parse_error(msg % child.tag.split('}')[-1])
else:
child = self._parse_component(self.elem, required=False, strict=False)
if child is not None:
@ -163,9 +224,39 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
self.type = self.schema.BUILDERS.complex_type_class(child, self.schema, self)
elif child.tag == XSD_SIMPLE_TYPE:
self.type = self.schema.BUILDERS.simple_type_factory(child, self.schema, self)
else:
self.type = self.maps.lookup_type(XSD_ANY_TYPE)
return 0
# Check value constraints
if 'default' in attrib and not self.type.is_valid(attrib['default']):
msg = "'default' value %r is not compatible with the type of the element"
self.parse_error(msg % attrib['default'])
elif 'fixed' in attrib and not self.type.is_valid(attrib['fixed']):
msg = "'fixed' value %r is not compatible with the type of the element"
self.parse_error(msg % attrib['fixed'])
return 1
else:
self.type = self.maps.lookup_type(XSD_ANY_TYPE)
return 0
# Check value constraints
if 'default' in attrib:
if not self.type.is_valid(attrib['default']):
msg = "'default' value {!r} is not compatible with the type {!r}"
self.parse_error(msg.format(attrib['default'], self.type))
elif self.schema.XSD_VERSION == '1.0' and (
self.type.name == XSD_ID or self.type.is_derived(self.schema.meta_schema.types['ID'])):
self.parse_error("'xs:ID' or a type derived from 'xs:ID' cannot has a 'default'")
elif 'fixed' in attrib:
if not self.type.is_valid(attrib['fixed']):
msg = "'fixed' value {!r} is not compatible with the type {!r}"
self.parse_error(msg.format(attrib['fixed'], self.type))
elif self.schema.XSD_VERSION == '1.0' and (
self.type.name == XSD_ID or self.type.is_derived(self.schema.meta_schema.types['ID'])):
self.parse_error("'xs:ID' or a type derived from 'xs:ID' cannot has a 'default'")
return 0
def _parse_identity_constraints(self, index=0):
@ -189,44 +280,56 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
self.constraints[constraint.name] = constraint
def _parse_substitution_group(self):
substitution_group = self.substitution_group
substitution_group = self.elem.get('substitutionGroup')
if substitution_group is None:
return
if not self.is_global:
self.parse_error("'substitutionGroup' attribute in a local element declaration")
qname = prefixed_to_qname(substitution_group, self.namespaces)
if qname[0] != '{':
qname = get_qname(self.target_namespace, qname)
try:
head_element = self.maps.lookup_element(qname)
substitution_group_qname = self.schema.resolve_qname(substitution_group)
except ValueError as err:
self.parse_error(err)
return
else:
if substitution_group_qname[0] != '{':
substitution_group_qname = get_qname(self.target_namespace, substitution_group_qname)
try:
head_element = self.maps.lookup_element(substitution_group_qname)
except KeyError:
self.parse_error("unknown substitutionGroup %r" % substitution_group)
return
else:
final = head_element.final
if final is None:
final = self.schema.final_default
if isinstance(head_element, tuple):
self.parse_error("circularity found for substitutionGroup %r" % substitution_group)
return
elif 'substitution' in head_element.block:
return
if final == '#all' or 'extension' in final and 'restriction' in final:
self.parse_error("head element %r cannot be substituted." % head_element)
elif self.type == head_element.type or self.type.name == XSD_ANY_TYPE:
pass
elif 'extension' in final and not self.type.is_derived(head_element.type, 'extension'):
self.parse_error(
"%r type is not of the same or an extension of the head element %r type."
% (self, head_element)
)
elif 'restriction' in final and not self.type.is_derived(head_element.type, 'restriction'):
self.parse_error(
"%r type is not of the same or a restriction of the head element %r type."
% (self, head_element)
)
elif not self.type.is_derived(head_element.type):
self.parse_error(
"%r type is not of the same or a derivation of the head element %r type."
% (self, head_element)
)
final = head_element.final
if self.type == head_element.type or self.type.name == XSD_ANY_TYPE:
pass
elif not self.type.is_derived(head_element.type):
msg = "%r type is not of the same or a derivation of the head element %r type."
self.parse_error(msg % (self, head_element))
elif final == '#all' or 'extension' in final and 'restriction' in final:
msg = "head element %r can't be substituted by an element that has a derivation of its type"
self.parse_error(msg % head_element)
elif 'extension' in final and self.type.is_derived(head_element.type, 'extension'):
msg = "head element %r can't be substituted by an element that has an extension of its type"
self.parse_error(msg % head_element)
elif 'restriction' in final and self.type.is_derived(head_element.type, 'restriction'):
msg = "head element %r can't be substituted by an element that has a restriction of its type"
self.parse_error(msg % head_element)
if self.type.name == XSD_ANY_TYPE and 'type' not in self.elem.attrib:
self.type = self.maps.elements[substitution_group_qname].type
try:
self.maps.substitution_groups[substitution_group_qname].add(self)
except KeyError:
self.maps.substitution_groups[substitution_group_qname] = {self}
finally:
self._substitution_group = substitution_group_qname
@property
def built(self):
@ -245,24 +348,21 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
return self.elem.get('ref')
# Global element's exclusive properties
@property
def abstract(self):
return self._abstract if self._ref is None else self._ref.abstract
@property
def final(self):
return get_xsd_derivation_attribute(self.elem, 'final', ('extension', 'restriction'))
return self._final or self.schema.final_default if self._ref is None else self._ref.final
@property
def block(self):
return get_xsd_derivation_attribute(self.elem, 'block', ('extension', 'restriction', 'substitution'))
return self._block or self.schema.block_default if self._ref is None else self._ref.block
@property
def substitution_group(self):
return self.elem.get('substitutionGroup')
# Properties inherited by references
@property
def abstract(self):
if self._ref is not None:
return self._ref.abstract
return get_xml_bool_attribute(self.elem, 'abstract', default=False)
return self._substitution_group if self._ref is None else self._ref.substitution_group
@property
def default(self):
@ -274,12 +374,7 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
@property
def form(self):
if self._ref is not None:
return self._ref.form
value = self.elem.get('form')
if value not in (None, 'qualified', 'unqualified'):
raise XMLSchemaValueError("wrong value %r for 'form' attribute." % value)
return value
return get_xsd_form_attribute(self.elem, 'form') if self._ref is None else self._ref.form
@property
def nillable(self):
@ -295,6 +390,27 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
def get_type(self, elem):
return self.type
def get_path(self, ancestor=None, reverse=False):
"""
Returns the XPath expression of the element. The path is relative to the schema instance
in which the element is contained or is relative to a specific ancestor passed as argument.
In the latter case returns `None` if the argument is not an ancestor.
:param ancestor: optional XSD component of the same schema, that may be an ancestor of the element.
:param reverse: if set to `True` returns the reverse path, from the element to ancestor.
"""
path = []
xsd_component = self
while xsd_component is not None:
if xsd_component is ancestor:
return '/'.join(reversed(path)) or '.'
elif hasattr(xsd_component, 'tag'):
path.append('..' if reverse else xsd_component.name)
xsd_component = xsd_component.parent
else:
if ancestor is None:
return '/'.join(reversed(path)) or '.'
def iter_components(self, xsd_classes=None):
if xsd_classes is None:
yield self
@ -307,7 +423,7 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
if isinstance(obj, xsd_classes):
yield obj
if self.ref is None and not self.type.is_global:
if self.ref is None and self.type.parent is not None:
for obj in self.type.iter_components(xsd_classes):
yield obj
@ -527,36 +643,63 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
yield elem
del element_data
def is_restriction(self, other, check_particle=True):
def is_restriction(self, other, check_occurs=True):
if isinstance(other, XsdAnyElement):
return True # TODO
if self.min_occurs == self.max_occurs == 0:
return True
if check_occurs and not self.has_occurs_restriction(other):
return False
return other.is_matching(self.name, self.default_namespace)
elif isinstance(other, XsdElement):
if self.name != other.name:
if other.name not in self.maps.substitution_groups:
substitution_group = self.substitution_group
if other.name == self.substitution_group and other.min_occurs != other.max_occurs \
and self.max_occurs != 0 and not other.abstract:
# Base is the head element, it's not abstract and has non deterministic occurs: this
# is less restrictive than W3C test group (elemZ026), marked as invalid despite it's
# based on an abstract declaration.
return False
else:
return any(self.is_restriction(e) for e in self.maps.substitution_groups[other.name])
elif check_particle and not ParticleMixin.is_restriction(self, other):
elif self.substitution_group is None:
return False
elif not any(e.name == self.name for e in self.maps.substitution_groups[substitution_group]):
return False
if check_occurs and not self.has_occurs_restriction(other):
return False
elif self.type is not other.type and self.type.elem is not other.type.elem and \
not self.type.is_derived(other.type):
not self.type.is_derived(other.type, 'restriction') and not other.type.abstract:
return False
elif self.fixed != other.fixed:
elif self.fixed != other.fixed and self.type.normalize(self.fixed) != other.type.normalize(other.fixed):
return False
elif other.nillable is False and self.nillable:
return False
elif not all(value in other.block for value in self.block):
elif any(value not in self.block for value in other.block.split()):
return False
elif not all(k in other.constraints for k in self.constraints):
return False
elif other.model == 'choice':
if ParticleMixin.is_restriction(self, other):
return any(self.is_restriction(e, False) for e in other.iter_group())
else:
return any(self.is_restriction(e) for e in other.iter_group())
return True
elif other.model == 'choice':
if other.is_empty() and self.max_occurs != 0:
return False
check_group_items_occurs = self.schema.XSD_VERSION == '1.0'
counter = ParticleCounter()
for e in other.iter_model():
if not isinstance(e, (XsdElement, XsdAnyElement)):
return False
elif not self.is_restriction(e, check_group_items_occurs):
continue
counter += e
counter *= other
if self.has_occurs_restriction(counter):
return True
counter.reset()
return False
else:
match_restriction = False
for e in other.iter_group():
for e in other.iter_model():
if match_restriction:
if not e.is_emptiable():
return False
@ -564,7 +707,21 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)
match_restriction = True
elif not e.is_emptiable():
return False
return True
return True
def overlap(self, other):
if isinstance(other, XsdElement):
if self.name == other.name:
return True
elif other.substitution_group == self.name or other.name == self.substitution_group:
return True
elif isinstance(other, XsdAnyElement):
if other.is_matching(self.name, self.default_namespace):
return True
for e in self.maps.substitution_groups.get(self.name, ()):
if other.is_matching(e.name, self.default_namespace):
return True
return False
class Xsd11Element(XsdElement):
@ -597,7 +754,8 @@ class Xsd11Element(XsdElement):
index = self._parse_type()
index = self._parse_alternatives(index)
self._parse_identity_constraints(index)
self._parse_substitution_group()
if self.parent is None:
self._parse_substitution_group()
self._parse_target_namespace()
def _parse_alternatives(self, index=0):
@ -636,6 +794,14 @@ class Xsd11Element(XsdElement):
return alt.type
return self.type
def overlap(self, other):
if isinstance(other, XsdElement):
if self.name == other.name:
return True
elif other.substitution_group == self.name or other.name == self.substitution_group:
return True
return False
class XsdAlternative(XsdComponent):
"""
@ -648,21 +814,22 @@ class XsdAlternative(XsdComponent):
Content: (annotation?, (simpleType | complexType)?)
</alternative>
"""
admitted_tags = {XSD_ALTERNATIVE}
_admitted_tags = {XSD_ALTERNATIVE}
type = None
def __repr__(self):
return '%s(type=%r, test=%r)' % (self.__class__.__name__, self.elem.get('type'), self.elem.get('test'))
def _parse(self):
XsdComponent._parse(self)
elem = self.elem
attrib = self.elem.attrib
try:
self.path = elem.attrib['test']
self.path = attrib['test']
except KeyError as err:
self.path = 'true()'
self.parse_error(err, elem=elem)
self.parse_error(err)
if 'xpathDefaultNamespace' in self.elem.attrib:
if 'xpathDefaultNamespace' in attrib:
self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem)
else:
self.xpath_default_namespace = self.schema.xpath_default_namespace
@ -671,23 +838,24 @@ class XsdAlternative(XsdComponent):
try:
self.token = parser.parse(self.path)
except ElementPathSyntaxError as err:
self.parse_error(err, elem=elem)
self.parse_error(err)
self.token = parser.parse('true()')
self.path = 'true()'
try:
type_attrib = elem.attrib['type']
type_qname = self.schema.resolve_qname(attrib['type'])
except KeyError:
self.parse_error("missing 'type' attribute from %r" % elem)
self.type = None
self.parse_error("missing 'type' attribute")
except ValueError as err:
self.parse_error(err)
else:
try:
self.type = self.maps.lookup_type(prefixed_to_qname(type_attrib, self.namespaces))
self.type = self.maps.lookup_type(type_qname)
except KeyError:
self.parse_error("unknown type %r" % type_attrib, elem)
self.parse_error("unknown type %r" % attrib['type'])
else:
if not self.type.is_derived(self.parent.type):
self.parse_error("type %r ir not derived from %r" % (type_attrib, self.parent.type), elem)
self.parse_error("type %r ir not derived from %r" % (attrib['type'], self.parent.type))
@property
def built(self):

View File

@ -50,21 +50,28 @@ class XMLSchemaValidatorError(XMLSchemaException):
if self.elem is None:
return '%s.' % self.message
else:
elem, path = self.elem, self.path
msg = ['%s:\n' % self.message]
if elem is not None:
if self.elem is not None:
elem_as_string = etree_tostring(self.elem, self.namespaces, ' ', 20)
if hasattr(elem, 'sourceline'):
msg.append("Schema (line %r):\n\n%s\n" % (elem.sourceline, elem_as_string))
if hasattr(self.elem, 'sourceline'):
msg.append("Schema (line %r):\n\n%s\n" % (self.elem.sourceline, elem_as_string))
else:
msg.append("Schema:\n\n%s\n" % elem_as_string)
if path is not None:
msg.append("Path: %s\n" % path)
if self.path is not None:
msg.append("Path: %s\n" % self.path)
if self.schema_url is not None:
msg.append("Schema URL: %s\n" % self.schema_url)
if self.origin_url not in (None, self.schema_url):
msg.append("Origin URL: %s\n" % self.origin_url)
return '\n'.join(msg)
if PY3:
__str__ = __unicode__
@property
def msg(self):
return self.__unicode__()
def __setattr__(self, name, value):
if name == 'elem' and value is not None and not is_etree_element(value):
raise XMLSchemaValueError("'elem' attribute requires an Element, not %r." % type(value))
@ -88,7 +95,21 @@ class XMLSchemaValidatorError(XMLSchemaException):
try:
return self.source.root
except AttributeError:
return None
return
@property
def schema_url(self):
try:
return self.validator.schema.source.url
except AttributeError:
return
@property
def origin_url(self):
try:
return self.validator.maps.validator.source.url
except AttributeError:
return
class XMLSchemaNotBuiltError(XMLSchemaValidatorError, RuntimeError):
@ -131,6 +152,32 @@ class XMLSchemaParseError(XMLSchemaValidatorError, SyntaxError):
)
class XMLSchemaModelError(XMLSchemaValidatorError, ValueError):
"""
Raised when a model error is found during the checking of a model group.
:param group: the XSD model group.
:type group: XsdGroup
:param message: the error message.
:type message: str or unicode
"""
def __init__(self, group, message):
super(XMLSchemaModelError, self).__init__(
validator=group,
message=message,
elem=group.elem,
source=getattr(group, 'source', None),
namespaces=group.namespaces
)
class XMLSchemaModelDepthError(XMLSchemaModelError):
"""Raised when recursion depth is exceeded while iterating a model group."""
def __init__(self, group):
msg = "maximum model recursion depth exceeded while iterating group %r" % group
super(XMLSchemaModelDepthError, self).__init__(group, message=msg)
class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError):
"""
Raised when the XML data is not validated with the XSD component or schema.
@ -165,20 +212,19 @@ class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError):
return unicode(self).encode("utf-8")
def __unicode__(self):
elem, path = self.elem, self.path
msg = ['%s:\n' % self.message]
if self.reason is not None:
msg.append('Reason: %s\n' % self.reason)
if hasattr(self.validator, 'tostring'):
msg.append("Schema:\n\n%s\n" % self.validator.tostring(' ', 20))
if elem is not None:
elem_as_string = etree_tostring(elem, self.namespaces, ' ', 20)
if hasattr(elem, 'sourceline'):
msg.append("Instance (line %r):\n\n%s\n" % (elem.sourceline, elem_as_string))
if self.elem is not None:
elem_as_string = etree_tostring(self.elem, self.namespaces, ' ', 20)
if hasattr(self.elem, 'sourceline'):
msg.append("Instance (line %r):\n\n%s\n" % (self.elem.sourceline, elem_as_string))
else:
msg.append("Instance:\n\n%s\n" % elem_as_string)
if path is not None:
msg.append("Path: %s\n" % path)
if self.path is not None:
msg.append("Path: %s\n" % self.path)
return '\n'.join(msg)
if PY3:
@ -298,9 +344,7 @@ class XMLSchemaChildrenValidationError(XMLSchemaValidationError):
class XMLSchemaIncludeWarning(XMLSchemaWarning):
"""A schema include fails."""
pass
class XMLSchemaImportWarning(XMLSchemaWarning):
"""A schema namespace import fails."""
pass

View File

@ -19,7 +19,7 @@ from ..compat import unicode_type, MutableSequence
from ..qnames import XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_ENUMERATION, XSD_WHITE_SPACE, \
XSD_PATTERN, XSD_MAX_INCLUSIVE, XSD_MAX_EXCLUSIVE, XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, \
XSD_TOTAL_DIGITS, XSD_FRACTION_DIGITS, XSD_ASSERTION, XSD_EXPLICIT_TIMEZONE, XSD_NOTATION_TYPE, \
XSD_DECIMAL, XSD_INTEGER, XSD_BASE64_BINARY, XSD_HEX_BINARY
XSD_BASE64_BINARY, XSD_HEX_BINARY
from ..regex import get_python_regex
from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError
@ -105,7 +105,7 @@ class XsdWhiteSpaceFacet(XsdFacet):
Content: (annotation?)
</whiteSpace>
"""
admitted_tags = XSD_WHITE_SPACE,
_admitted_tags = XSD_WHITE_SPACE,
def _parse_value(self, elem):
self.value = value = elem.attrib['value']
@ -141,7 +141,7 @@ class XsdLengthFacet(XsdFacet):
Content: (annotation?)
</length>
"""
admitted_tags = XSD_LENGTH,
_admitted_tags = XSD_LENGTH,
def _parse_value(self, elem):
self.value = int(elem.attrib['value'])
@ -184,7 +184,7 @@ class XsdMinLengthFacet(XsdFacet):
Content: (annotation?)
</minLength>
"""
admitted_tags = XSD_MIN_LENGTH,
_admitted_tags = XSD_MIN_LENGTH,
def _parse_value(self, elem):
self.value = int(elem.attrib['value'])
@ -227,7 +227,7 @@ class XsdMaxLengthFacet(XsdFacet):
Content: (annotation?)
</maxLength>
"""
admitted_tags = XSD_MAX_LENGTH,
_admitted_tags = XSD_MAX_LENGTH,
def _parse_value(self, elem):
self.value = int(elem.attrib['value'])
@ -270,13 +270,25 @@ class XsdMinInclusiveFacet(XsdFacet):
Content: (annotation?)
</minInclusive>
"""
admitted_tags = XSD_MIN_INCLUSIVE,
_admitted_tags = XSD_MIN_INCLUSIVE,
def _parse_value(self, elem):
self.value = self.base_type.decode(elem.attrib['value'])
self.validator = self.min_inclusive_validator
def min_inclusive_validator(self, x):
facet = self.base_type.get_facet(XSD_MIN_EXCLUSIVE)
if facet is not None and facet.value >= self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MIN_INCLUSIVE)
if facet is not None and facet.value > self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MAX_EXCLUSIVE)
if facet is not None and facet.value <= self.value:
self.parse_error("maximum value of base_type is lesser")
facet = self.base_type.get_facet(XSD_MAX_INCLUSIVE)
if facet is not None and facet.value < self.value:
self.parse_error("maximum value of base_type is lesser")
def validator(self, x):
if x < self.value:
yield XMLSchemaValidationError(self, x, "value has to be greater or equal than %r." % self.value)
@ -293,13 +305,25 @@ class XsdMinExclusiveFacet(XsdFacet):
Content: (annotation?)
</minExclusive>
"""
admitted_tags = XSD_MIN_EXCLUSIVE,
_admitted_tags = XSD_MIN_EXCLUSIVE,
def _parse_value(self, elem):
self.value = self.base_type.decode(elem.attrib['value'])
self.validator = self.min_exclusive_validator
def min_exclusive_validator(self, x):
facet = self.base_type.get_facet(XSD_MIN_EXCLUSIVE)
if facet is not None and facet.value > self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MIN_INCLUSIVE)
if facet is not None and facet.value > self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MAX_EXCLUSIVE)
if facet is not None and facet.value <= self.value:
self.parse_error("maximum value of base_type is lesser")
facet = self.base_type.get_facet(XSD_MAX_INCLUSIVE)
if facet is not None and facet.value <= self.value:
self.parse_error("maximum value of base_type is lesser")
def validator(self, x):
if x <= self.value:
yield XMLSchemaValidationError(self, x, "value has to be greater than %r." % self.value)
@ -316,13 +340,25 @@ class XsdMaxInclusiveFacet(XsdFacet):
Content: (annotation?)
</maxInclusive>
"""
admitted_tags = XSD_MAX_INCLUSIVE,
_admitted_tags = XSD_MAX_INCLUSIVE,
def _parse_value(self, elem):
self.value = self.base_type.decode(elem.attrib['value'])
self.validator = self.max_inclusive_validator
def max_inclusive_validator(self, x):
facet = self.base_type.get_facet(XSD_MIN_EXCLUSIVE)
if facet is not None and facet.value >= self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MIN_INCLUSIVE)
if facet is not None and facet.value > self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MAX_EXCLUSIVE)
if facet is not None and facet.value <= self.value:
self.parse_error("maximum value of base_type is lesser")
facet = self.base_type.get_facet(XSD_MAX_INCLUSIVE)
if facet is not None and facet.value < self.value:
self.parse_error("maximum value of base_type is lesser")
def validator(self, x):
if x > self.value:
yield XMLSchemaValidationError(self, x, "value has to be lesser or equal than %r." % self.value)
@ -339,15 +375,27 @@ class XsdMaxExclusiveFacet(XsdFacet):
Content: (annotation?)
</maxExclusive>
"""
admitted_tags = XSD_MAX_EXCLUSIVE,
_admitted_tags = XSD_MAX_EXCLUSIVE,
def _parse_value(self, elem):
self.value = self.base_type.decode(elem.attrib['value'])
self.validator = self.max_exclusive_validator
def max_exclusive_validator(self, x):
facet = self.base_type.get_facet(XSD_MIN_EXCLUSIVE)
if facet is not None and facet.value >= self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MIN_INCLUSIVE)
if facet is not None and facet.value >= self.value:
self.parse_error("minimum value of base_type is greater")
facet = self.base_type.get_facet(XSD_MAX_EXCLUSIVE)
if facet is not None and facet.value < self.value:
self.parse_error("maximum value of base_type is lesser")
facet = self.base_type.get_facet(XSD_MAX_INCLUSIVE)
if facet is not None and facet.value < self.value:
self.parse_error("maximum value of base_type is lesser")
def validator(self, x):
if x >= self.value:
yield XMLSchemaValidationError(self, x, "value has to be lesser than %r." % self.value)
yield XMLSchemaValidationError(self, x, "value has to be lesser than %r" % self.value)
class XsdTotalDigitsFacet(XsdFacet):
@ -362,7 +410,7 @@ class XsdTotalDigitsFacet(XsdFacet):
Content: (annotation?)
</totalDigits>
"""
admitted_tags = XSD_TOTAL_DIGITS,
_admitted_tags = XSD_TOTAL_DIGITS,
def _parse_value(self, elem):
self.value = int(elem.attrib['value'])
@ -387,18 +435,18 @@ class XsdFractionDigitsFacet(XsdFacet):
Content: (annotation?)
</fractionDigits>
"""
admitted_tags = XSD_FRACTION_DIGITS,
_admitted_tags = XSD_FRACTION_DIGITS,
def __init__(self, elem, schema, parent, base_type):
super(XsdFractionDigitsFacet, self).__init__(elem, schema, parent, base_type)
if not base_type.is_subtype(XSD_DECIMAL):
if not base_type.is_derived(self.schema.builtin_types()['decimal']):
self.parse_error("fractionDigits facet can be applied only to types derived from xs:decimal")
def _parse_value(self, elem):
self.value = int(elem.attrib['value'])
if self.value < 0:
raise ValueError("'value' must be greater or equal than 0")
elif self.value > 0 and self.base_type.is_subtype(XSD_INTEGER):
elif self.value > 0 and self.base_type.is_derived(self.schema.builtin_types()['integer']):
raise ValueError("fractionDigits facet value has to be 0 for types derived from xs:integer.")
self.validator = self.fraction_digits_validator
@ -419,7 +467,7 @@ class XsdExplicitTimezoneFacet(XsdFacet):
Content: (annotation?)
</explicitTimezone>
"""
admitted_tags = XSD_EXPLICIT_TIMEZONE,
_admitted_tags = XSD_EXPLICIT_TIMEZONE,
def _parse_value(self, elem):
self.value = value = elem.attrib['value']
@ -450,7 +498,7 @@ class XsdEnumerationFacets(MutableSequence, XsdFacet):
Content: (annotation?)
</enumeration>
"""
admitted_tags = {XSD_ENUMERATION}
_admitted_tags = {XSD_ENUMERATION}
def __init__(self, elem, schema, parent, base_type):
XsdFacet.__init__(self, elem, schema, parent, base_type)
@ -468,8 +516,15 @@ class XsdEnumerationFacets(MutableSequence, XsdFacet):
except XMLSchemaDecodeError as err:
self.parse_error(err, elem)
else:
if self.base_type.name == XSD_NOTATION_TYPE and value not in self.schema.notations:
self.parse_error("value must match a notation global declaration", elem)
if self.base_type.name == XSD_NOTATION_TYPE:
try:
notation_qname = self.schema.resolve_qname(value)
except ValueError as err:
self.parse_error(err, elem)
else:
if notation_qname not in self.maps.notations:
self.parse_error("value {} must match a notation global declaration".format(value), elem)
return value
# Implements the abstract methods of MutableSequence
@ -517,7 +572,7 @@ class XsdPatternFacets(MutableSequence, XsdFacet):
Content: (annotation?)
</pattern>
"""
admitted_tags = {XSD_PATTERN}
_admitted_tags = {XSD_PATTERN}
def __init__(self, elem, schema, parent, base_type):
XsdFacet.__init__(self, elem, schema, parent, base_type)
@ -533,7 +588,7 @@ class XsdPatternFacets(MutableSequence, XsdFacet):
except KeyError:
self.parse_error("missing 'value' attribute", elem)
return re.compile(r'^$')
except XMLSchemaDecodeError as err:
except (re.error, XMLSchemaDecodeError) as err:
self.parse_error(err, elem)
return re.compile(r'^$')
@ -585,7 +640,7 @@ class XsdAssertionFacet(XsdFacet):
Content: (annotation?)
</assertion>
"""
admitted_tags = {XSD_ASSERTION}
_admitted_tags = {XSD_ASSERTION}
def __repr__(self):
return '%s(test=%r)' % (self.__class__.__name__, self.path)

View File

@ -14,16 +14,19 @@ XSD declarations/definitions.
"""
from __future__ import unicode_literals
import re
import warnings
from collections import Counter
from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, XMLSchemaValueError
from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, XMLSchemaValueError, XMLSchemaWarning
from ..namespaces import XSD_NAMESPACE
from ..qnames import XSD_INCLUDE, XSD_IMPORT, XSD_REDEFINE, XSD_OVERRIDE, XSD_NOTATION, XSD_SIMPLE_TYPE, \
XSD_COMPLEX_TYPE, XSD_GROUP, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ELEMENT, XSD_ANY_TYPE
from ..helpers import get_qname, local_name, prefixed_to_qname
from ..qnames import XSD_INCLUDE, XSD_IMPORT, XSD_REDEFINE, XSD_OVERRIDE, XSD_NOTATION, XSD_ANY_TYPE, \
XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE, XSD_GROUP, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ELEMENT
from ..helpers import get_qname, local_name
from ..namespaces import NamespaceResourcesMap
from . import XMLSchemaNotBuiltError, XsdValidator, XsdKeyref, XsdComponent, XsdAttribute, \
XsdSimpleType, XsdComplexType, XsdElement, XsdAttributeGroup, XsdGroup, XsdNotation, XsdAssert
from . import XMLSchemaNotBuiltError, XMLSchemaModelError, XMLSchemaModelDepthError, XsdValidator, \
XsdKeyref, XsdComponent, XsdAttribute, XsdSimpleType, XsdComplexType, XsdElement, XsdAttributeGroup, \
XsdGroup, XsdNotation, XsdAssert
from .builtins import xsd_builtin_types_factory
@ -61,9 +64,12 @@ def create_load_function(filter_function):
for schema in schemas:
target_namespace = schema.target_namespace
for elem in iterchildren_xsd_redefine(schema.root):
location = elem.get('schemaLocation')
if location is None:
continue
for child in filter_function(elem):
qname = get_qname(target_namespace, child.attrib['name'])
redefinitions.append((qname, (child, schema)))
redefinitions.append((qname, child, schema, schema.includes[location]))
for elem in filter_function(schema.root):
qname = get_qname(target_namespace, elem.attrib['name'])
@ -74,17 +80,41 @@ def create_load_function(filter_function):
except AttributeError:
xsd_globals[qname] = [xsd_globals[qname], (elem, schema)]
for qname, obj in redefinitions:
if qname not in xsd_globals:
elem, schema = obj
tags = Counter([x[0] for x in redefinitions])
for qname, elem, schema, redefined_schema in redefinitions:
# Checks multiple redefinitions
if tags[qname] > 1:
tags[qname] = 1
redefined_schemas = [x[3] for x in redefinitions if x[0] == qname]
if any(redefined_schemas.count(x) > 1 for x in redefined_schemas):
schema.parse_error(
"multiple redefinition for {} {!r}".format(local_name(elem.tag), qname), elem
)
else:
redefined_schemas = {x[3]: x[2] for x in redefinitions if x[0] == qname}
for rs, s in redefined_schemas.items():
while True:
try:
s = redefined_schemas[s]
except KeyError:
break
if s is rs:
schema.parse_error(
"circular redefinition for {} {!r}".format(local_name(elem.tag), qname), elem
)
break
# Append redefinition
try:
xsd_globals[qname].append((elem, schema))
except KeyError:
schema.parse_error("not a redefinition!", elem)
else:
try:
xsd_globals[qname].append(obj)
except KeyError:
xsd_globals[qname] = obj
except AttributeError:
xsd_globals[qname] = [xsd_globals[qname], obj]
# xsd_globals[qname] = elem, schema
except AttributeError:
xsd_globals[qname] = [xsd_globals[qname], (elem, schema)]
return load_xsd_globals
@ -108,7 +138,10 @@ def create_lookup_function(xsd_classes):
try:
obj = global_map[qname]
except KeyError:
raise XMLSchemaKeyError("missing a %s object for %r!" % (types_desc, qname))
if '{' in qname:
raise XMLSchemaKeyError("missing a %s component for %r!" % (types_desc, qname))
raise XMLSchemaKeyError("missing a %s component for %r! As the name has no namespace "
"maybe a missing default namespace declaration." % (types_desc, qname))
else:
if isinstance(obj, xsd_classes):
return obj
@ -130,28 +163,26 @@ def create_lookup_function(xsd_classes):
return global_map[qname]
elif isinstance(obj, list):
if not isinstance(obj[0], xsd_classes):
# Not built XSD global component with redefinitions
try:
elem, schema = obj[0]
except ValueError:
return obj[0][0] # Circular build, simply return (elem, schema) couple
# Not built XSD global component with redefinitions
try:
elem, schema = obj[0]
except ValueError:
return obj[0][0] # Circular build, simply return (elem, schema) couple
try:
factory_or_class = tag_map[elem.tag]
except KeyError:
raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map))
try:
factory_or_class = tag_map[elem.tag]
except KeyError:
raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map))
global_map[qname] = obj[0], # To catch circular builds
global_map[qname] = factory_or_class(elem, schema, parent=None)
else:
# Built-in type
global_map[qname] = obj[0]
global_map[qname] = obj[0], # To catch circular builds
global_map[qname] = component = factory_or_class(elem, schema, parent=None)
# Apply redefinitions (changing elem involve a re-parsing of the component)
for elem, schema in obj[1:]:
global_map[qname].schema = schema
global_map[qname].elem = elem
component.redefine = component.copy()
component.schema = schema
component.elem = elem
return global_map[qname]
else:
@ -176,13 +207,15 @@ class XsdGlobals(XsdValidator):
declarations defined in the registered schemas. Register a schema to
add it's declarations to the global maps.
:param validator: the XMLSchema class to use for global maps.
:param validator: the origin schema class/instance used for creating the global maps.
:param validation: the XSD validation mode to use, can be 'strict', 'lax' or 'skip'.
"""
def __init__(self, validator, validation='strict'):
super(XsdGlobals, self).__init__(validation)
self.validator = validator
if not all(hasattr(validator, a) for a in ('meta_schema', 'BUILDERS_MAP')):
raise XMLSchemaValueError("The argument {!r} is not an XSD schema validator".format(validator))
self.validator = validator
self.namespaces = NamespaceResourcesMap() # Registered schemas by namespace URI
self.types = {} # Global types (both complex and simple)
@ -234,20 +267,28 @@ class XsdGlobals(XsdValidator):
def lookup_element(self, qname):
return lookup_element(self.elements, qname, self.validator.BUILDERS_MAP)
def lookup(self, tag, qname):
if tag in (XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE):
return self.lookup_type(qname)
elif tag == XSD_ELEMENT:
return self.lookup_element(qname)
elif tag == XSD_GROUP:
return self.lookup_group(qname)
elif tag == XSD_ATTRIBUTE:
return self.lookup_attribute(qname)
elif tag == XSD_ATTRIBUTE_GROUP:
return self.lookup_attribute_group(qname)
elif tag == XSD_NOTATION:
return self.lookup_notation(qname)
else:
raise XMLSchemaValueError("wrong tag {!r} for an XSD global definition/declaration".format(tag))
@property
def built(self):
if not self.namespaces:
return False
xsd_global = None
for xsd_global in self.iter_globals():
if not isinstance(xsd_global, XsdComponent):
for schema in self.iter_schemas():
if not schema.built:
return False
if not xsd_global.built:
return False
if xsd_global is not None:
return True
else:
return False
return True
@property
def validation_attempted(self):
@ -273,6 +314,13 @@ class XsdGlobals(XsdValidator):
def resources(self):
return [(schema.url, schema) for schemas in self.namespaces.values() for schema in schemas]
@property
def all_errors(self):
errors = []
for schema in self.iter_schemas():
errors.extend(schema.all_errors)
return errors
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
yield self
@ -305,7 +353,7 @@ class XsdGlobals(XsdValidator):
else:
if schema in ns_schemas:
return
if not any([schema.url == obj.url for obj in ns_schemas]):
elif not any([schema.url == obj.url and schema.__class__ == obj.__class__ for obj in ns_schemas]):
ns_schemas.append(schema)
def clear(self, remove_schemas=False, only_unbuilt=False):
@ -355,7 +403,30 @@ class XsdGlobals(XsdValidator):
try:
meta_schema = self.namespaces[XSD_NAMESPACE][0]
except KeyError:
raise XMLSchemaValueError("%r: %r namespace is not registered." % (self, XSD_NAMESPACE))
# Meta-schemas are not registered. If any of base namespaces is already registered
# create a new meta-schema, otherwise register the meta-schemas.
meta_schema = self.validator.meta_schema
if meta_schema is None:
raise XMLSchemaValueError("{!r} has not a meta-schema".format(self.validator))
if any(ns in self.namespaces for ns in meta_schema.BASE_SCHEMAS):
base_schemas = {k: v for k, v in meta_schema.BASE_SCHEMAS.items() if k not in self.namespaces}
meta_schema = self.validator.create_meta_schema(meta_schema.url, base_schemas, self)
for schema in self.iter_schemas():
if schema.meta_schema is not None:
schema.meta_schema = meta_schema
else:
for schema in meta_schema.maps.iter_schemas():
self.register(schema)
self.types.update(meta_schema.maps.types)
self.attributes.update(meta_schema.maps.attributes)
self.attribute_groups.update(meta_schema.maps.attribute_groups)
self.groups.update(meta_schema.maps.groups)
self.notations.update(meta_schema.maps.notations)
self.elements.update(meta_schema.maps.elements)
self.substitution_groups.update(meta_schema.maps.substitution_groups)
self.constraints.update(meta_schema.maps.constraints)
not_built_schemas = [schema for schema in self.iter_schemas() if not schema.built]
for schema in not_built_schemas:
@ -387,58 +458,62 @@ class XsdGlobals(XsdValidator):
self.lookup_group(qname)
# Builds element declarations inside model groups.
element_class = meta_schema.BUILDERS.element_class
for schema in not_built_schemas:
for group in schema.iter_components(XsdGroup):
for k in range(len(group)):
if isinstance(group[k], tuple):
elem, schema = group[k]
group[k] = element_class(elem, schema, group)
group.build()
for schema in not_built_schemas:
# Build substitution groups from global element declarations
for xsd_element in schema.elements.values():
if xsd_element.substitution_group:
qname = prefixed_to_qname(xsd_element.substitution_group, xsd_element.schema.namespaces)
if xsd_element.type.name == XSD_ANY_TYPE and 'type' not in xsd_element.elem.attrib:
xsd_element.type = self.elements[qname].type
try:
self.substitution_groups[qname].add(xsd_element)
except KeyError:
self.substitution_groups[qname] = {xsd_element}
# Checks substitution groups
for qname in self.substitution_groups:
try:
xsd_element = self.elements[qname]
except KeyError:
raise XMLSchemaKeyError("missing global element %r in %r." % (qname, schema))
else:
for e in xsd_element.iter_substitutes():
if e is xsd_element:
raise XMLSchemaValueError(
"circularity found for element %r in substitution groups of %r" % (e, schema)
)
if schema.meta_schema is not None:
# Set referenced key/unique constraints for keyrefs
for constraint in schema.iter_components(XsdKeyref):
constraint.parse_refer()
for assertion in schema.iter_components(XsdAssert):
assertion.parse()
# Check for illegal restrictions
# TODO: Fix for XsdGroup.is_restriction() method is needed before enabling this check
# if schema.validation != 'skip':
# for xsd_type in schema.iter_components(XsdComplexType):
# xsd_type.check_restriction()
if schema.XSD_VERSION > '1.0' and schema.default_attributes is not None:
if not isinstance(schema.default_attributes, XsdAttributeGroup):
schema.default_attributes = None
schema.parse_error("defaultAttributes={!r} doesn't match an attribute group of {!r}"
.format(schema.root.get('defaultAttributes'), schema), schema.root)
for schema in filter(lambda x: x.meta_schema is not None, not_built_schemas):
# Build key references and assertions (XSD meta-schema doesn't have any of them)
for constraint in schema.iter_components(XsdKeyref):
constraint.parse_refer()
for assertion in schema.iter_components(XsdAssert):
assertion.parse()
self._check_schema(schema)
if self.validation == 'strict' and not self.built:
raise XMLSchemaNotBuiltError(self, "global map %r not built!" % self)
def _check_schema(self, schema):
# Checks substitution groups circularities
for qname in self.substitution_groups:
xsd_element = self.elements[qname]
for e in xsd_element.iter_substitutes():
if e is xsd_element:
schema.parse_error("circularity found for substitution group with head element %r" % xsd_element)
if schema.XSD_VERSION > '1.0' and schema.default_attributes is not None:
if not isinstance(schema.default_attributes, XsdAttributeGroup):
schema.default_attributes = None
schema.parse_error("defaultAttributes={!r} doesn't match an attribute group of {!r}"
.format(schema.root.get('defaultAttributes'), schema), schema.root)
if schema.validation == 'skip':
return
# Check redefined global groups
for group in filter(lambda x: x.schema is schema and x.redefine is not None, self.groups.values()):
if not any(isinstance(e, XsdGroup) and e.name == group.name for e in group) \
and not group.is_restriction(group.redefine):
group.parse_error("The redefined group is an illegal restriction of the original group.")
# Check complex content types models
for xsd_type in schema.iter_components(XsdComplexType):
if not isinstance(xsd_type.content_type, XsdGroup):
continue
base_type = xsd_type.base_type
if xsd_type.derivation == 'restriction':
if base_type and base_type.name != XSD_ANY_TYPE and base_type.is_complex():
if not xsd_type.content_type.is_restriction(base_type.content_type):
xsd_type.parse_error("The derived group is an illegal restriction of the base type group.")
try:
xsd_type.content_type.check_model()
except XMLSchemaModelDepthError:
msg = "cannot verify the content model of %r due to maximum recursion depth exceeded" % xsd_type
schema.warnings.append(msg)
warnings.warn(msg, XMLSchemaWarning, stacklevel=4)
except XMLSchemaModelError as err:
if self.validation == 'strict':
raise
xsd_type.errors.append(err)

View File

@ -12,22 +12,20 @@
This module contains classes for XML Schema model groups.
"""
from __future__ import unicode_literals
from collections import Counter
from ..compat import PY3, unicode_type, MutableSequence
from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError
from ..compat import unicode_type
from ..exceptions import XMLSchemaValueError
from ..etree import etree_element
from ..qnames import XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, XSD_COMPLEX_TYPE, \
XSD_ELEMENT, XSD_ANY, XSD_RESTRICTION, XSD_EXTENSION
from xmlschema.helpers import get_qname, local_name, prefixed_to_qname
from xmlschema.helpers import get_qname, local_name
from ..converters import XMLSchemaConverter
from .exceptions import XMLSchemaValidationError
from .xsdbase import ValidationMixin, XsdComponent, ParticleMixin, XsdType
from .exceptions import XMLSchemaValidationError, XMLSchemaChildrenValidationError
from .xsdbase import ValidationMixin, XsdComponent, XsdType
from .elements import XsdElement
from .wildcards import XsdAnyElement
XSD_GROUP_MODELS = {'sequence', 'choice', 'all'}
from .models import MAX_MODEL_DEPTH, ParticleMixin, ModelGroup, ModelVisitor
ANY_ELEMENT = etree_element(
XSD_ANY,
@ -39,198 +37,7 @@ ANY_ELEMENT = etree_element(
})
class XsdModelVisitor(MutableSequence):
"""
A visitor design pattern class that can be used for validating XML data related to an
XSD model group. The visit of the model is done using an external match information,
counting the occurrences and yielding tuples in case of model's item occurrence errors.
Ends setting the current element to `None`.
:param root: the root XsdGroup instance of the model.
:ivar occurs: the Counter instance for keeping track of occurrences of XSD elements and groups.
:ivar element: the current XSD element, initialized to the first element of the model.
:ivar broken: a boolean value that records if the model is still usable.
:ivar group: the current XSD group, initialized to *root* argument.
:ivar iterator: the current XSD group iterator.
:ivar items: the current XSD group unmatched items.
:ivar match: if the XSD group has an effective item match.
"""
def __init__(self, root):
self.root = root
self.occurs = Counter()
self._subgroups = []
self.element = None
self.broken = False
self.group, self.iterator, self.items, self.match = root, iter(root), root[::-1], False
self._start()
def __str__(self):
# noinspection PyCompatibility,PyUnresolvedReferences
return unicode(self).encode("utf-8")
def __unicode__(self):
return self.__repr__()
if PY3:
__str__ = __unicode__
def __repr__(self):
return '%s(root=%r)' % (self.__class__.__name__, self.root)
# Implements the abstract methods of MutableSequence
def __getitem__(self, i):
return self._subgroups[i]
def __setitem__(self, i, item):
self._subgroups[i] = item
def __delitem__(self, i):
del self._subgroups[i]
def __len__(self):
return len(self._subgroups)
def insert(self, i, item):
self._subgroups.insert(i, item)
def clear(self):
del self._subgroups[:]
self.occurs.clear()
self.element = None
self.broken = False
self.group, self.iterator, self.items, self.match = self.root, iter(self.root), self.root[::-1], False
def _start(self):
while True:
item = next(self.iterator, None)
if item is None or not isinstance(item, XsdGroup):
self.element = item
break
elif item:
self.append((self.group, self.iterator, self.items, self.match))
self.group, self.iterator, self.items, self.match = item, iter(item), item[::-1], False
@property
def expected(self):
"""
Returns the expected elements of the current and descendant groups.
"""
expected = []
for item in reversed(self.items):
if isinstance(item, XsdGroup):
expected.extend(item.iter_elements())
else:
expected.append(item)
expected.extend(item.maps.substitution_groups.get(item.name, ()))
return expected
def restart(self):
self.clear()
self._start()
def stop(self):
while self.element is not None:
for e in self.advance():
yield e
def advance(self, match=False):
"""
Generator function for advance to the next element. Yields tuples with
particles information when occurrence violation is found.
:param match: provides current element match.
"""
def stop_item(item):
"""
Stops element or group matching, incrementing current group counter.
:return: `True` if the item has violated the minimum occurrences for itself \
or for the current group, `False` otherwise.
"""
if isinstance(item, XsdGroup):
self.group, self.iterator, self.items, self.match = self.pop()
item_occurs = occurs[item]
model = self.group.model
if item_occurs:
self.match = True
if model == 'choice':
occurs[item] = 0
occurs[self.group] += 1
self.iterator, self.match = iter(self.group), False
else:
if model == 'all':
self.items.remove(item)
else:
self.items.pop()
if not self.items:
self.occurs[self.group] += 1
return item.is_missing(item_occurs)
elif model == 'sequence':
if self.match:
self.items.pop()
if not self.items:
occurs[self.group] += 1
return not item.is_emptiable()
elif item.is_emptiable():
self.items.pop()
return False
elif self.group.min_occurs <= occurs[self.group] or self:
return stop_item(self.group)
else:
self.items.pop()
return True
element, occurs = self.element, self.occurs
if element is None:
raise XMLSchemaValueError("cannot advance, %r is ended!" % self)
if match:
occurs[element] += 1
self.match = True
if not element.is_over(occurs[element]):
return
try:
if stop_item(element):
yield element, occurs[element], [element]
while True:
while self.group.is_over(occurs[self.group]):
stop_item(self.group)
obj = next(self.iterator, None)
if obj is None:
if not self.match:
if self.group.model == 'all' and all(e.min_occurs == 0 for e in self.items):
occurs[self.group] += 1
group, expected = self.group, self.items
if stop_item(group) and expected:
yield group, occurs[group], self.expected
elif not self.items:
self.iterator, self.items, self.match = iter(self.group), self.group[::-1], False
elif self.group.model == 'all':
self.iterator, self.match = iter(self.items), False
elif all(e.min_occurs == 0 for e in self.items):
self.iterator, self.items, self.match = iter(self.group), self.group[::-1], False
occurs[self.group] += 1
elif not isinstance(obj, XsdGroup): # XsdElement or XsdAnyElement
self.element, occurs[obj] = obj, 0
return
elif obj:
self.append((self.group, self.iterator, self.items, self.match))
self.group, self.iterator, self.items, self.match = obj, iter(obj), obj[::-1], False
occurs[obj] = 0
except IndexError:
self.element = None
if self.group.is_missing(occurs[self.group]) and self.items:
yield self.group, occurs[self.group], self.expected
class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
class XsdGroup(XsdComponent, ModelGroup, ValidationMixin):
"""
A class for XSD 1.0 model group definitions.
@ -268,23 +75,18 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
Content: (annotation?, (element | group | choice | sequence | any)*)
</sequence>
"""
admitted_tags = {
XSD_COMPLEX_TYPE, XSD_EXTENSION, XSD_RESTRICTION,
XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE
mixed = False
model = None
redefine = None
_admitted_tags = {
XSD_COMPLEX_TYPE, XSD_EXTENSION, XSD_RESTRICTION, XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE
}
def __init__(self, elem, schema, parent, name=None, initlist=None):
self.mixed = False if parent is None else parent.mixed
self.model = None
def __init__(self, elem, schema, parent, name=None):
self._group = []
if initlist is not None:
if isinstance(initlist, type(self._group)):
self._group[:] = initlist
elif isinstance(initlist, XsdGroup):
self._group[:] = initlist._group[:]
else:
self._group = list(initlist)
XsdComponent.__init__(self, elem, schema, parent, name)
if parent is not None and parent.mixed:
self.mixed = parent.mixed
super(XsdGroup, self).__init__(elem, schema, parent, name)
def __repr__(self):
if self.name is None:
@ -298,41 +100,18 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
self.__class__.__name__, self.prefixed_name, self.model, self.occurs
)
# Implements the abstract methods of MutableSequence
def __getitem__(self, i):
return self._group[i]
def copy(self):
group = object.__new__(self.__class__)
group.__dict__.update(self.__dict__)
group.errors = self.errors[:]
group._group = self._group[:]
return group
def __setitem__(self, i, item):
assert isinstance(item, (tuple, ParticleMixin)), \
"XsdGroup's items must be tuples or ParticleMixin instances."
self._group[i] = item
def __delitem__(self, i):
del self._group[i]
def __len__(self):
return len(self._group)
def insert(self, i, item):
assert isinstance(item, (tuple, ParticleMixin)), \
"XsdGroup's items must be tuples or ParticleMixin instances."
self._group.insert(i, item)
def __setattr__(self, name, value):
if name == 'model' and value is not None:
if value not in XSD_GROUP_MODELS:
raise XMLSchemaValueError("invalid model group %r." % value)
if self.model is not None and value != self.model:
raise XMLSchemaValueError("cannot change a valid group model: %r" % value)
elif name == '_group':
if not all(isinstance(item, (tuple, ParticleMixin)) for item in value):
raise XMLSchemaValueError("XsdGroup's items must be tuples or ParticleMixin instances.")
super(XsdGroup, self).__setattr__(name, value)
__copy__ = copy
def _parse(self):
super(XsdGroup, self)._parse()
if self and not hasattr(self, '_elem'):
self.clear()
self.clear()
elem = self.elem
self._parse_particle(elem)
@ -343,14 +122,19 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
if name is None:
if ref is not None:
# Reference to a global group
if self.is_global:
self.parse_error("a group reference cannot be global", elem)
self.name = prefixed_to_qname(ref, self.namespaces)
if self.parent is None:
self.parse_error("a group reference cannot be global")
try:
self.name = self.schema.resolve_qname(ref)
except ValueError as err:
self.parse_error(err, elem)
return
try:
xsd_group = self.schema.maps.lookup_group(self.name)
except KeyError:
self.parse_error("missing group %r" % self.prefixed_name, elem)
self.parse_error("missing group %r" % self.prefixed_name)
xsd_group = self.schema.create_any_content_group(self, self.name)
if isinstance(xsd_group, tuple):
@ -361,33 +145,47 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
self.append(XsdAnyElement(ANY_ELEMENT, self.schema, self))
else:
self.model = xsd_group.model
if self.model == 'all':
if self.max_occurs != 1:
self.parse_error("maxOccurs must be 1 for 'all' model groups")
if self.min_occurs not in (0, 1):
self.parse_error("minOccurs must be (0 | 1) for 'all' model groups")
if self.schema.XSD_VERSION == '1.0' and isinstance(self.parent, XsdGroup):
self.parse_error("in XSD 1.0 the 'all' model group cannot be nested")
self.append(xsd_group)
else:
self.parse_error("missing both attributes 'name' and 'ref'", elem)
self.parse_error("missing both attributes 'name' and 'ref'")
return
elif ref is None:
# Global group
self.name = get_qname(self.target_namespace, name)
content_model = self._parse_component(elem)
if not self.is_global:
self.parse_error("attribute 'name' not allowed for a local group", self)
if self.parent is not None:
self.parse_error("attribute 'name' not allowed for a local group")
else:
if 'minOccurs' in elem.attrib:
self.parse_error(
"attribute 'minOccurs' not allowed for a global group", self
)
self.parse_error("attribute 'minOccurs' not allowed for a global group")
if 'maxOccurs' in elem.attrib:
self.parse_error("attribute 'maxOccurs' not allowed for a global group")
if 'minOccurs' in content_model.attrib:
self.parse_error(
"attribute 'maxOccurs' not allowed for a global group", self
"attribute 'minOccurs' not allowed for the model of a global group", content_model
)
if content_model.tag not in {XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}:
self.parse_error('unexpected tag %r' % content_model.tag, content_model)
return
if 'maxOccurs' in content_model.attrib:
self.parse_error(
"attribute 'maxOccurs' not allowed for the model of a global group", content_model
)
if content_model.tag not in {XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}:
self.parse_error('unexpected tag %r' % content_model.tag, content_model)
return
else:
self.parse_error("found both attributes 'name' and 'ref'", elem)
self.parse_error("found both attributes 'name' and 'ref'")
return
elif elem.tag in {XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}:
# Local group (sequence|all|choice)
if 'name' in elem.attrib:
self.parse_error("attribute 'name' not allowed for a local group")
content_model = elem
self.name = None
elif elem.tag in {XSD_COMPLEX_TYPE, XSD_EXTENSION, XSD_RESTRICTION}:
@ -418,37 +216,85 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
elif child.tag in (XSD_SEQUENCE, XSD_CHOICE):
self.append(XsdGroup(child, self.schema, self))
elif child.tag == XSD_GROUP:
xsd_group = XsdGroup(child, self.schema, self)
if xsd_group.name != self.name:
self.append(xsd_group)
elif not hasattr(self, '_elem'):
self.parse_error("Circular definitions detected for group %r:" % self.ref, elem)
try:
ref = self.schema.resolve_qname(child.attrib['ref'])
except KeyError:
self.parse_error("missing attribute 'ref' in local group", child)
continue
if ref != self.name:
xsd_group = XsdGroup(child, self.schema, self)
if xsd_group.model == 'all':
self.parse_error("'all' model can appears only at 1st level of a model group")
else:
self.append(xsd_group)
elif self.redefine is None:
self.parse_error("Circular definition detected for group %r:" % self.ref, elem)
else:
if child.get('minOccurs', '1') != '1' or child.get('maxOccurs', '1') != '1':
self.parse_error(
"Redefined group reference cannot have minOccurs/maxOccurs other than 1:", elem
)
self.append(self.redefine)
else:
continue # Error already caught by validation against the meta-schema
@property
def schema_elem(self):
return self.elem if self.name else self.parent.elem
def children_validation_error(self, validation, elem, index, particle, occurs=0, expected=None,
source=None, namespaces=None, **_kwargs):
"""
Helper method for generating model validation errors. Incompatible with 'skip' validation mode.
Il validation mode is 'lax' returns the error, otherwise raise the error.
:param validation: the validation mode. Can be 'lax' or 'strict'.
:param elem: the instance Element.
:param index: the child index.
:param particle: the XSD component (subgroup or element) associated to the child.
:param occurs: the child tag occurs.
:param expected: the expected element tags/object names.
:param source: the XML resource related to the validation process.
:param namespaces: is an optional mapping from namespace prefix to URI.
:param _kwargs: keyword arguments of the validation process that are not used.
"""
if validation == 'skip':
raise XMLSchemaValueError("validation mode 'skip' incompatible with error generation.")
error = XMLSchemaChildrenValidationError(self, elem, index, particle, occurs, expected, source, namespaces)
if validation == 'strict':
raise error
else:
return error
def build(self):
element_class = self.schema.BUILDERS.element_class
for k in range(len(self._group)):
if isinstance(self._group[k], tuple):
elem, schema = self._group[k]
self._group[k] = element_class(elem, schema, self)
if self.redefine is not None:
for group in self.redefine.iter_components(XsdGroup):
group.build()
@property
def built(self):
for item in self:
if isinstance(item, XsdAnyElement):
if not isinstance(item, ParticleMixin):
return False
elif isinstance(item, XsdAnyElement):
if not item.built:
return False
elif isinstance(item, tuple):
return False
elif not isinstance(item, (XsdElement, XsdGroup)):
raise XMLSchemaTypeError("wrong type for item %r of %r." % (item, self))
elif item.parent is None:
continue
elif item.parent is not self.parent and isinstance(item.parent, XsdType) and item.parent.parent is None:
continue
elif not item.ref and not item.built:
return False
return True
@property
def schema_elem(self):
return self.elem if self.name else self.parent.elem
@property
def validation_attempted(self):
if self.built:
@ -473,111 +319,192 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
for obj in item.iter_components(xsd_classes):
yield obj
def clear(self):
del self._group[:]
def admitted_restriction(self, model):
if self.model == model:
return True
elif self.model == 'all' and model == 'choice' and len(self) > 1:
return False
elif model == 'all' and self.model == 'choice' and len(self) > 1:
return False
if model == 'sequence' and self.model != 'sequence' and len(self) > 1:
return False
def is_empty(self):
return not self.mixed and not self
def is_emptiable(self):
if self.model == 'choice':
return self.min_occurs == 0 or not self or any([item.is_emptiable() for item in self])
else:
return self.min_occurs == 0 or not self or all([item.is_emptiable() for item in self])
def is_meaningless(self, parent_group=None):
"""
A group that may be eliminated. A group is meaningless if one of those conditions is verified:
- the group is empty
- minOccurs == maxOccurs == 1 and the group has one child
- minOccurs == maxOccurs == 1 and the group and its parent have a sequence model
- minOccurs == maxOccurs == 1 and the group and its parent have a choice model
"""
def is_restriction(self, other, check_occurs=True):
if not self:
return True
elif self.min_occurs != 1 or self.max_occurs != 1:
return False
elif len(self) == 1:
return True
elif parent_group is None:
return False
elif self.model == 'sequence' and parent_group.model != 'sequence':
return False
elif self.model == 'choice' and parent_group.model != 'choice':
return False
else:
return True
def is_restriction(self, other, check_particle=True):
if not isinstance(other, XsdGroup):
return False
elif not self:
return True
elif self.ref is not None:
return self[0].is_restriction(other, check_occurs)
elif not isinstance(other, ParticleMixin):
raise XMLSchemaValueError("the argument 'base' must be a %r instance" % ParticleMixin)
elif not isinstance(other, XsdGroup):
return self.is_element_restriction(other)
elif not other:
return False
elif other.model == 'sequence' and self.model != 'sequence':
return False
elif other.model == 'choice' and self.model == 'all':
return False
elif other.model == 'all' and self.model == 'choice':
return False
elif check_particle and not super(XsdGroup, self).is_restriction(other):
return False
elif other.ref:
return self.is_restriction(other[0], check_occurs)
elif len(other) == other.min_occurs == other.max_occurs == 1:
if len(self) > 1:
return self.is_restriction(other[0], check_occurs)
elif isinstance(self[0], XsdGroup) and self[0].is_pointless(parent=self):
return self[0].is_restriction(other[0], check_occurs)
other_iterator = iter(other.iter_group())
for item in self.iter_group():
# Compare model with model
if self.model != other.model and self.model != 'sequence' and len(self) > 1:
return False
elif self.model == other.model or other.model == 'sequence':
return self.is_sequence_restriction(other)
elif other.model == 'all':
return self.is_all_restriction(other)
elif other.model == 'choice':
return self.is_choice_restriction(other)
def is_element_restriction(self, other):
if self.schema.XSD_VERSION == '1.0' and isinstance(other, XsdElement) and \
not other.ref and other.name not in self.schema.substitution_groups:
return False
elif not self.has_occurs_restriction(other):
return False
elif self.model == 'choice':
if other.name in self.maps.substitution_groups and all(
isinstance(e, XsdElement) and e.substitution_group == other.name for e in self):
return True
return any(e.is_restriction(other, False) for e in self)
else:
min_occurs = max_occurs = 0
for item in self.iter_model():
if isinstance(item, XsdGroup):
return False
elif item.min_occurs == 0 or item.is_restriction(other, False):
min_occurs += item.min_occurs
if max_occurs is not None:
if item.max_occurs is None:
max_occurs = None
else:
max_occurs += item.max_occurs
continue
return False
if min_occurs < other.min_occurs:
return False
elif max_occurs is None:
return other.max_occurs is None
elif other.max_occurs is None:
return True
else:
return max_occurs <= other.max_occurs
def is_sequence_restriction(self, other):
if not self.has_occurs_restriction(other):
return False
check_occurs = other.max_occurs != 0
check_emptiable = other.model != 'choice' # or self.schema.XSD_VERSION == '1.0'
# Same model: declarations must simply preserve order
other_iterator = iter(other.iter_model())
for item in self.iter_model():
while True:
try:
other_item = next(other_iterator)
except StopIteration:
return False
if other_item is item:
if other_item is item or item.is_restriction(other_item, check_occurs):
break
elif item.is_restriction(other_item):
break
elif other.model == 'choice':
continue
elif other_item.is_emptiable():
continue
elif isinstance(other_item, XsdGroup) and other_item.model == 'choice' and \
other_item.max_occurs == 1:
if any(item.is_restriction(s) for s in other_item.iter_group()):
break
else:
elif check_emptiable and not other_item.is_emptiable():
return False
return True
if not check_emptiable:
return True
def iter_group(self):
"""Creates an iterator for sub elements and groups. Skips meaningless groups."""
for item in self:
if not isinstance(item, XsdGroup):
yield item
elif item.is_global or not item.is_meaningless(self):
yield item
while True:
try:
other_item = next(other_iterator)
except StopIteration:
return True
else:
for obj in item.iter_group():
yield obj
if not other_item.is_emptiable():
return False
def iter_subelements(self):
for item in self:
if isinstance(item, XsdGroup):
for e in item.iter_subelements():
yield e
else:
yield item
def is_all_restriction(self, other):
if not self.has_occurs_restriction(other):
return False
def iter_elements(self):
for item in self:
if isinstance(item, XsdGroup):
for e in item.iter_elements():
yield e
check_occurs = other.max_occurs != 0
restriction_items = list(self)
for other_item in other.iter_model():
for item in restriction_items:
if other_item is item or item.is_restriction(other_item, check_occurs):
break
else:
yield item
for e in self.maps.substitution_groups.get(item.name, ()):
yield e
if not other_item.is_emptiable():
return False
continue
restriction_items.remove(item)
return not bool(restriction_items)
def is_choice_restriction(self, other):
if self.parent is None and other.parent is not None and self.schema.XSD_VERSION == '1.0':
return False
check_occurs = other.max_occurs != 0
restriction_items = list(self)
max_occurs = 0
other_max_occurs = 0
for other_item in other.iter_model():
for item in restriction_items:
if other_item is item or item.is_restriction(other_item, check_occurs):
if max_occurs is not None:
if item.max_occurs is None:
max_occurs = None
else:
max_occurs += item.max_occurs
if other_max_occurs is not None:
if other_item.max_occurs is None:
other_max_occurs = None
else:
other_max_occurs = max(other_max_occurs, other_item.max_occurs)
break
else:
continue
restriction_items.remove(item)
if restriction_items:
return False
elif other_max_occurs is None:
if other.max_occurs:
return True
other_max_occurs = 0
elif other.max_occurs is None:
if other_max_occurs:
return True
other_max_occurs = 0
else:
other_max_occurs *= other.max_occurs
if max_occurs is None:
return self.max_occurs == 0
elif self.max_occurs is None:
return max_occurs == 0
else:
return other_max_occurs >= max_occurs * self.max_occurs
def iter_elements(self, depth=0):
if depth <= MAX_MODEL_DEPTH:
for item in self:
if isinstance(item, XsdGroup):
for e in item.iter_elements(depth+1):
yield e
else:
yield item
for e in self.maps.substitution_groups.get(item.name, ()):
yield e
def sort_children(self, elements, default_namespace=None):
"""
@ -626,7 +553,7 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
result_list.append((cdata_index, text, None))
cdata_index += 1
model = XsdModelVisitor(self)
model = ModelVisitor(self)
errors = []
if not isinstance(converter, XMLSchemaConverter):
@ -734,7 +661,7 @@ class XsdGroup(MutableSequence, XsdComponent, ValidationMixin, ParticleMixin):
default_namespace = converter.get('')
losslessly = converter.losslessly
model = XsdModelVisitor(self)
model = ModelVisitor(self)
cdata_index = 0
for index, (name, value) in enumerate(element_data.content):
@ -875,10 +802,21 @@ class Xsd11Group(XsdGroup):
elif child.tag in (XSD_SEQUENCE, XSD_CHOICE, XSD_ALL):
self.append(XsdGroup(child, self.schema, self))
elif child.tag == XSD_GROUP:
xsd_group = XsdGroup(child, self.schema, self)
if xsd_group.name != self.name:
self.append(xsd_group)
elif not hasattr(self, '_elem'):
self.parse_error("Circular definitions detected for group %r:" % self.ref, elem)
try:
ref = self.schema.resolve_qname(child.attrib['ref'])
except KeyError:
self.parse_error("missing attribute 'ref' in local group", child)
continue
if ref != self.name:
self.append(XsdGroup(child, self.schema, self))
elif self.redefine is None:
self.parse_error("Circular definition detected for group %r:" % self.ref, elem)
else:
if child.get('minOccurs', '1') != '1' or child.get('maxOccurs', '1') != '1':
self.parse_error(
"Redefined group reference cannot have minOccurs/maxOccurs other than 1:", elem
)
self.append(self.redefine)
else:
continue # Error already caught by validation against the meta-schema

View File

@ -12,24 +12,26 @@
This module contains classes for other XML Schema identity constraints.
"""
from __future__ import unicode_literals
import re
from collections import Counter
from elementpath import Selector, XPath1Parser, ElementPathSyntaxError
from elementpath import Selector, XPath1Parser, ElementPathSyntaxError, ElementPathKeyError
from ..exceptions import XMLSchemaValueError
from ..qnames import XSD_UNIQUE, XSD_KEY, XSD_KEYREF, XSD_SELECTOR, XSD_FIELD
from ..helpers import get_qname, prefixed_to_qname, qname_to_prefixed
from ..helpers import get_qname, qname_to_prefixed
from ..etree import etree_getpath
from ..regex import get_python_regex
from .exceptions import XMLSchemaValidationError
from .xsdbase import XsdComponent
XSD_IDENTITY_XPATH_SYMBOLS = {
'processing-instruction', 'descendant-or-self', 'following-sibling', 'preceding-sibling',
'ancestor-or-self', 'descendant', 'attribute', 'following', 'namespace', 'preceding',
'ancestor', 'position', 'comment', 'parent', 'child', 'self', 'false', 'text', 'node',
'processing-instruction', 'following-sibling', 'preceding-sibling',
'ancestor-or-self', 'attribute', 'following', 'namespace', 'preceding',
'ancestor', 'position', 'comment', 'parent', 'child', 'false', 'text', 'node',
'true', 'last', 'not', 'and', 'mod', 'div', 'or', '..', '//', '!=', '<=', '>=', '(', ')',
'[', ']', '.', '@', ',', '/', '|', '*', '-', '=', '+', '<', '>', ':', '(end)', '(name)',
'(string)', '(float)', '(decimal)', '(integer)'
'(string)', '(float)', '(decimal)', '(integer)', '::'
}
@ -42,7 +44,11 @@ XsdIdentityXPathParser.build_tokenizer()
class XsdSelector(XsdComponent):
admitted_tags = {XSD_SELECTOR}
_admitted_tags = {XSD_SELECTOR}
pattern = re.compile(get_python_regex(
r"(\.//)?(((child::)?((\i\c*:)?(\i\c*|\*)))|\.)(/(((child::)?((\i\c*:)?(\i\c*|\*)))|\.))*(\|"
r"(\.//)?(((child::)?((\i\c*:)?(\i\c*|\*)))|\.)(/(((child::)?((\i\c*:)?(\i\c*|\*)))|\.))*)*"
))
def __init__(self, elem, schema, parent):
super(XsdSelector, self).__init__(elem, schema, parent)
@ -54,10 +60,13 @@ class XsdSelector(XsdComponent):
except KeyError:
self.parse_error("'xpath' attribute required:", self.elem)
self.path = "*"
else:
if not self.pattern.match(self.path.replace(' ', '')):
self.parse_error("Wrong XPath expression for an xs:selector")
try:
self.xpath_selector = Selector(self.path, self.namespaces, parser=XsdIdentityXPathParser)
except ElementPathSyntaxError as err:
except (ElementPathSyntaxError, ElementPathKeyError) as err:
self.parse_error(err)
self.xpath_selector = Selector('*', self.namespaces, parser=XsdIdentityXPathParser)
@ -77,7 +86,12 @@ class XsdSelector(XsdComponent):
class XsdFieldSelector(XsdSelector):
admitted_tags = {XSD_FIELD}
_admitted_tags = {XSD_FIELD}
pattern = re.compile(get_python_regex(
r"(\.//)?((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)/)*((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)|"
r"((attribute::|@)((\i\c*:)?(\i\c*|\*))))(\|(\.//)?((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)/)*"
r"((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)|((attribute::|@)((\i\c*:)?(\i\c*|\*)))))*"
))
class XsdIdentity(XsdComponent):
@ -107,6 +121,10 @@ class XsdIdentity(XsdComponent):
else:
self.parse_error("element %r not allowed here:" % child.tag, elem)
def iter_elements(self):
for xsd_element in self.selector.xpath_selector.iter_select(self.parent):
yield xsd_element
def get_fields(self, context, decoders=None):
"""
Get fields for a schema or instance context element.
@ -191,11 +209,11 @@ class XsdIdentity(XsdComponent):
class XsdUnique(XsdIdentity):
admitted_tags = {XSD_UNIQUE}
_admitted_tags = {XSD_UNIQUE}
class XsdKey(XsdIdentity):
admitted_tags = {XSD_KEY}
_admitted_tags = {XSD_KEY}
class XsdKeyref(XsdIdentity):
@ -205,12 +223,9 @@ class XsdKeyref(XsdIdentity):
:ivar refer: reference to a *xs:key* declaration that must be in the same element \
or in a descendant element.
"""
admitted_tags = {XSD_KEYREF}
def __init__(self, elem, schema, parent):
self.refer = None
self.refer_path = '.'
super(XsdKeyref, self).__init__(elem, schema, parent)
_admitted_tags = {XSD_KEYREF}
refer = None
refer_path = '.'
def __repr__(self):
return '%s(name=%r, refer=%r)' % (
@ -220,14 +235,16 @@ class XsdKeyref(XsdIdentity):
def _parse(self):
super(XsdKeyref, self)._parse()
try:
self.refer = prefixed_to_qname(self.elem.attrib['refer'], self.namespaces)
self.refer = self.schema.resolve_qname(self.elem.attrib['refer'])
except KeyError:
self.parse_error("missing required attribute 'refer'", self.elem)
self.parse_error("missing required attribute 'refer'")
except ValueError as err:
self.parse_error(err)
def parse_refer(self):
if self.refer is None:
return # attribute or key/unique identity constraint missing
elif isinstance(self.refer, XsdIdentity):
elif isinstance(self.refer, (XsdKey, XsdUnique)):
return # referenced key/unique identity constraint already set
try:
@ -236,23 +253,26 @@ class XsdKeyref(XsdIdentity):
try:
self.refer = self.maps.constraints[self.refer]
except KeyError:
self.parse_error("refer=%r must reference to a key/unique identity "
"constraint." % self.elem.get('refer'))
self.refer = None
else:
refer_path = []
xsd_component = self.refer.parent
while xsd_component is not None:
if xsd_component is self.parent:
refer_path.reverse()
self.refer_path = '/'.join(refer_path)
break
elif hasattr(xsd_component, 'tag'):
refer_path.append(xsd_component.name)
xsd_component = xsd_component.parent
else:
self.parse_error("%r is not defined in a descendant element." % self.elem.get('refer'))
self.refer = None
self.parse_error("key/unique identity constraint %r is missing" % self.refer)
return
if not isinstance(self.refer, (XsdKey, XsdUnique)):
self.parse_error("reference to a non key/unique identity constraint %r" % self.refer)
elif len(self.refer.fields) != len(self.fields):
self.parse_error("field cardinality mismatch between %r and %r" % (self, self.refer))
elif self.parent is not self.refer.parent:
refer_path = self.refer.parent.get_path(ancestor=self.parent)
if refer_path is None:
# From a note in par. 3.11.5 Part 1 of XSD 1.0 spec: "keyref identity-constraints may be
# defined on domains distinct from the embedded domain of the identity-constraint they
# reference, or the domains may be the same but self-embedding at some depth. In either
# case the node table for the referenced identity-constraint needs to propagate upwards,
# with conflict resolution."
refer_path = self.parent.get_path(ancestor=self.refer.parent, reverse=True)
if refer_path is None:
refer_path = self.parent.get_path(reverse=True) + '/' + self.refer.parent.get_path()
self.refer_path = refer_path
def get_refer_values(self, elem):
values = set()

View File

@ -0,0 +1,469 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2016-2019, SISSA (International School for Advanced Studies).
# 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 classes and functions for processing XSD content models.
"""
from __future__ import unicode_literals
from collections import Counter
from ..compat import PY3, MutableSequence
from ..exceptions import XMLSchemaValueError
from .exceptions import XMLSchemaModelError, XMLSchemaModelDepthError
from .xsdbase import ParticleMixin
MAX_MODEL_DEPTH = 15
"""Limit depth for safe visiting of models"""
XSD_GROUP_MODELS = {'sequence', 'choice', 'all'}
class ModelGroup(MutableSequence, ParticleMixin):
"""
Class for XSD model group particles. This class implements only model related methods,
schema element parsing and validation methods are implemented in derived classes.
"""
def __init__(self, model):
assert model in XSD_GROUP_MODELS, "Not a valid value for 'model'"
self._group = []
self.model = model
def __repr__(self):
return '%s(model=%r, occurs=%r)' % (self.__class__.__name__, self.model, self.occurs)
# Implements the abstract methods of MutableSequence
def __getitem__(self, i):
return self._group[i]
def __setitem__(self, i, item):
assert isinstance(item, (tuple, ParticleMixin)), "Items must be tuples or XSD particles"
self._group[i] = item
def __delitem__(self, i):
del self._group[i]
def __len__(self):
return len(self._group)
def insert(self, i, item):
assert isinstance(item, (tuple, ParticleMixin)), "Items must be tuples or XSD particles"
self._group.insert(i, item)
def __setattr__(self, name, value):
if name == 'model' and value is not None:
if value not in XSD_GROUP_MODELS:
raise XMLSchemaValueError("invalid model group %r." % value)
if self.model is not None and value != self.model and self.model != 'all':
raise XMLSchemaValueError("cannot change group model from %r to %r" % (self.model, value))
elif name == '_group':
if not all(isinstance(item, (tuple, ParticleMixin)) for item in value):
raise XMLSchemaValueError("XsdGroup's items must be tuples or ParticleMixin instances.")
super(ModelGroup, self).__setattr__(name, value)
def clear(self):
del self._group[:]
def is_emptiable(self):
if self.model == 'choice':
return self.min_occurs == 0 or not self or any([item.is_emptiable() for item in self])
else:
return self.min_occurs == 0 or not self or all([item.is_emptiable() for item in self])
def is_empty(self):
return not self._group or self.max_occurs == 0
def is_pointless(self, parent):
"""
Returns `True` if the group may be eliminated without affecting the model, `False` otherwise.
A group is pointless if one of those conditions is verified:
- the group is empty
- minOccurs == maxOccurs == 1 and the group has one child
- minOccurs == maxOccurs == 1 and the group and its parent have a sequence model
- minOccurs == maxOccurs == 1 and the group and its parent have a choice model
Ref: https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/#coss-particle
:param parent: effective parent of the model group.
"""
if not self:
return True
elif self.min_occurs != 1 or self.max_occurs != 1:
return False
elif len(self) == 1:
return True
elif not isinstance(parent, ModelGroup):
return False
elif self.model == 'sequence' and parent.model != 'sequence':
return False
elif self.model == 'choice' and parent.model != 'choice':
return False
else:
return True
def has_occurs_restriction(self, other):
if not self:
return True
elif isinstance(other, ModelGroup):
return super(ModelGroup, self).has_occurs_restriction(other)
# Group particle compared to element particle
if self.max_occurs is None or any(e.max_occurs is None for e in self):
if other.max_occurs is not None:
return False
elif self.model == 'choice':
return self.min_occurs * min(e.min_occurs for e in self) >= other.min_occurs
else:
return self.min_occurs * sum(e.min_occurs for e in self) >= other.min_occurs
elif self.model == 'choice':
if self.min_occurs * min(e.min_occurs for e in self) < other.min_occurs:
return False
elif other.max_occurs is None:
return True
else:
return self.max_occurs * max(e.max_occurs for e in self) <= other.max_occurs
else:
if self.min_occurs * sum(e.min_occurs for e in self) < other.min_occurs:
return False
elif other.max_occurs is None:
return True
else:
return self.max_occurs * sum(e.max_occurs for e in self) <= other.max_occurs
def iter_model(self, depth=0):
"""
A generator function iterating elements and groups of a model group. Skips pointless groups,
iterating deeper through them. Raises `XMLSchemaModelDepthError` if the argument *depth* is
over `MAX_MODEL_DEPTH` value.
:param depth: guard for protect model nesting bombs, incremented at each deepest recursion.
"""
if depth > MAX_MODEL_DEPTH:
raise XMLSchemaModelDepthError(self)
for item in self:
if not isinstance(item, ModelGroup):
yield item
elif not item.is_pointless(parent=self):
yield item
else:
for obj in item.iter_model(depth+1):
yield obj
def iter_elements(self, depth=0):
"""
A generator function iterating model's elements. Raises `XMLSchemaModelDepthError` if the
argument *depth* is over `MAX_MODEL_DEPTH` value.
:param depth: guard for protect model nesting bombs, incremented at each deepest recursion.
"""
if depth > MAX_MODEL_DEPTH:
raise XMLSchemaModelDepthError(self)
for item in self:
if isinstance(item, ModelGroup):
for e in item.iter_elements(depth+1):
yield e
else:
yield item
def iter_subelements(self, depth=0):
if depth <= MAX_MODEL_DEPTH:
for item in self:
if isinstance(item, ModelGroup):
for e in item.iter_subelements(depth+1):
yield e
else:
yield item
def check_model(self):
"""
Checks if the model group is deterministic. Types matching of same elements and Unique Particle
Attribution Constraint are checked. Raises an `XMLSchemaModelError` at first violated constraint.
"""
def safe_iter_path(group, depth):
if depth > MAX_MODEL_DEPTH:
raise XMLSchemaModelDepthError(group)
for item in group:
if isinstance(item, ModelGroup):
current_path.append(item)
for _item in safe_iter_path(item, depth + 1):
yield _item
current_path.pop()
else:
yield item
paths = {}
current_path = [self]
for e in safe_iter_path(self, 0):
for pe, previous_path in paths.values():
if pe.name == e.name and pe.name is not None and pe.type is not e.type:
raise XMLSchemaModelError(
self, "The model has elements with the same name %r but a different type" % e.name
)
elif not pe.overlap(e):
continue
elif pe is not e and pe.parent is e.parent:
if pe.parent.model in {'all', 'choice'}:
msg = "{!r} and {!r} overlap and are in the same {!r} group"
raise XMLSchemaModelError(self, msg.format(pe, e, pe.parent.model))
elif pe.min_occurs == pe.max_occurs:
continue
if not distinguishable_paths(previous_path + [pe], current_path + [e]):
raise XMLSchemaModelError(
self, "Unique Particle Attribution violation between {!r} and {!r}".format(pe, e)
)
paths[e.name] = e, current_path[:]
def distinguishable_paths(path1, path2):
"""
Checks if two model paths are distinguishable in a deterministic way, without looking forward
or backtracking. The arguments are lists containing paths from the base group of the model to
a couple of leaf elements. Returns `True` if there is a deterministic separation between paths,
`False` if the paths are ambiguous.
"""
e1, e2 = path1[-1], path2[-1]
for k, e in enumerate(path1):
if e not in path2:
depth = k - 1
break
else:
depth = 0
if path1[depth].max_occurs == 0:
return True
univocal1 = univocal2 = True
if path1[depth].model == 'sequence':
idx1 = path1[depth].index(path1[depth + 1])
idx2 = path2[depth].index(path2[depth + 1])
before1 = any(not e.is_emptiable() for e in path1[depth][:idx1])
after1 = before2 = any(not e.is_emptiable() for e in path1[depth][idx1 + 1:idx2])
after2 = any(not e.is_emptiable() for e in path1[depth][idx2 + 1:])
else:
before1 = after1 = before2 = after2 = False
for k in range(depth + 1, len(path1) - 1):
univocal1 &= path1[k].is_univocal()
if path1[k].model == 'sequence':
idx = path1[k].index(path1[k + 1])
before1 |= any(not e.is_emptiable() for e in path1[k][:idx])
after1 |= any(not e.is_emptiable() for e in path1[k][idx + 1:])
for k in range(depth + 1, len(path2) - 1):
univocal2 &= path2[k].is_univocal()
if path2[k].model == 'sequence':
idx = path2[k].index(path2[k + 1])
before2 |= any(not e.is_emptiable() for e in path2[k][:idx])
after2 |= any(not e.is_emptiable() for e in path2[k][idx + 1:])
if path1[depth].model != 'sequence':
return before1 and before2 or \
(before1 and (univocal1 and e1.is_univocal() or after1 or path1[depth].max_occurs == 1)) or \
(before2 and (univocal2 and e2.is_univocal() or after2 or path2[depth].max_occurs == 1))
elif path1[depth].max_occurs == 1:
return before2 or (before1 or univocal1) and (e1.is_univocal() or after1)
else:
return (before2 or (before1 or univocal1) and (e1.is_univocal() or after1)) and \
(before1 or (before2 or univocal2) and (e2.is_univocal() or after2))
class ModelVisitor(MutableSequence):
"""
A visitor design pattern class that can be used for validating XML data related to an XSD
model group. The visit of the model is done using an external match information,
counting the occurrences and yielding tuples in case of model's item occurrence errors.
Ends setting the current element to `None`.
:param root: the root ModelGroup instance of the model.
:ivar occurs: the Counter instance for keeping track of occurrences of XSD elements and groups.
:ivar element: the current XSD element, initialized to the first element of the model.
:ivar broken: a boolean value that records if the model is still usable.
:ivar group: the current XSD model group, initialized to *root* argument.
:ivar iterator: the current XSD group iterator.
:ivar items: the current XSD group unmatched items.
:ivar match: if the XSD group has an effective item match.
"""
def __init__(self, root):
self.root = root
self.occurs = Counter()
self._subgroups = []
self.element = None
self.broken = False
self.group, self.iterator, self.items, self.match = root, iter(root), root[::-1], False
self._start()
def __str__(self):
# noinspection PyCompatibility,PyUnresolvedReferences
return unicode(self).encode("utf-8")
def __unicode__(self):
return self.__repr__()
if PY3:
__str__ = __unicode__
def __repr__(self):
return '%s(root=%r)' % (self.__class__.__name__, self.root)
# Implements the abstract methods of MutableSequence
def __getitem__(self, i):
return self._subgroups[i]
def __setitem__(self, i, item):
self._subgroups[i] = item
def __delitem__(self, i):
del self._subgroups[i]
def __len__(self):
return len(self._subgroups)
def insert(self, i, item):
self._subgroups.insert(i, item)
def clear(self):
del self._subgroups[:]
self.occurs.clear()
self.element = None
self.broken = False
self.group, self.iterator, self.items, self.match = self.root, iter(self.root), self.root[::-1], False
def _start(self):
while True:
item = next(self.iterator, None)
if item is None or not isinstance(item, ModelGroup):
self.element = item
break
elif item:
self.append((self.group, self.iterator, self.items, self.match))
self.group, self.iterator, self.items, self.match = item, iter(item), item[::-1], False
@property
def expected(self):
"""
Returns the expected elements of the current and descendant groups.
"""
expected = []
for item in reversed(self.items):
if isinstance(item, ModelGroup):
expected.extend(item.iter_elements())
else:
expected.append(item)
expected.extend(item.maps.substitution_groups.get(item.name, ()))
return expected
def restart(self):
self.clear()
self._start()
def stop(self):
while self.element is not None:
for e in self.advance():
yield e
def advance(self, match=False):
"""
Generator function for advance to the next element. Yields tuples with
particles information when occurrence violation is found.
:param match: provides current element match.
"""
def stop_item(item):
"""
Stops element or group matching, incrementing current group counter.
:return: `True` if the item has violated the minimum occurrences for itself \
or for the current group, `False` otherwise.
"""
if isinstance(item, ModelGroup):
self.group, self.iterator, self.items, self.match = self.pop()
item_occurs = occurs[item]
model = self.group.model
if item_occurs:
self.match = True
if model == 'choice':
occurs[item] = 0
occurs[self.group] += 1
self.iterator, self.match = iter(self.group), False
else:
if model == 'all':
self.items.remove(item)
else:
self.items.pop()
if not self.items:
self.occurs[self.group] += 1
return item.is_missing(item_occurs)
elif model == 'sequence':
if self.match:
self.items.pop()
if not self.items:
occurs[self.group] += 1
return not item.is_emptiable()
elif item.is_emptiable():
self.items.pop()
return False
elif self.group.min_occurs <= occurs[self.group] or self:
return stop_item(self.group)
else:
self.items.pop()
return True
element, occurs = self.element, self.occurs
if element is None:
raise XMLSchemaValueError("cannot advance, %r is ended!" % self)
if match:
occurs[element] += 1
self.match = True
if not element.is_over(occurs[element]):
return
try:
if stop_item(element):
yield element, occurs[element], [element]
while True:
while self.group.is_over(occurs[self.group]):
stop_item(self.group)
obj = next(self.iterator, None)
if obj is None:
if not self.match:
if self.group.model == 'all' and all(e.min_occurs == 0 for e in self.items):
occurs[self.group] += 1
group, expected = self.group, self.items
if stop_item(group) and expected:
yield group, occurs[group], self.expected
elif not self.items:
self.iterator, self.items, self.match = iter(self.group), self.group[::-1], False
elif self.group.model == 'all':
self.iterator, self.match = iter(self.items), False
elif all(e.min_occurs == 0 for e in self.items):
self.iterator, self.items, self.match = iter(self.group), self.group[::-1], False
occurs[self.group] += 1
elif not isinstance(obj, ModelGroup): # XsdElement or XsdAnyElement
self.element, occurs[obj] = obj, 0
return
elif obj:
self.append((self.group, self.iterator, self.items, self.match))
self.group, self.iterator, self.items, self.match = obj, iter(obj), obj[::-1], False
occurs[obj] = 0
except IndexError:
self.element = None
if self.group.is_missing(occurs[self.group]) and self.items:
yield self.group, occurs[self.group], self.expected

View File

@ -30,7 +30,7 @@ class XsdNotation(XsdComponent):
Content: (annotation?)
</notation>
"""
admitted_tags = {XSD_NOTATION}
_admitted_tags = {XSD_NOTATION}
def __init__(self, elem, schema, parent):
if parent is not None:
@ -50,11 +50,8 @@ class XsdNotation(XsdComponent):
except KeyError:
self.parse_error("a notation must have a 'name'.", self.elem)
for key in self.elem.attrib:
if key not in {'id', 'name', 'public', 'system'}:
self.parse_error("wrong attribute %r for notation definition." % key, self.elem)
if 'public' not in self.elem.attrib and 'system' not in self.elem.attrib:
self.parse_error("a notation may have 'public' or 'system' attribute.", self.elem)
if 'public' not in self.elem.attrib and 'system' not in self.elem.attrib:
self.parse_error("a notation must has a 'public' or a 'system' attribute.", self.elem)
@property
def public(self):

View File

@ -28,33 +28,37 @@ Those are the differences between XSD 1.0 and XSD 1.1 and their current developm
* schema overrides
"""
import os
from collections import namedtuple
from collections import namedtuple, Counter
from abc import ABCMeta
import warnings
import elementpath
from ..compat import add_metaclass
from ..exceptions import XMLSchemaTypeError, XMLSchemaURLError, XMLSchemaValueError, XMLSchemaOSError
from ..qnames import XSD_SCHEMA, XSD_NOTATION, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_SIMPLE_TYPE, \
XSD_COMPLEX_TYPE, XSD_GROUP, XSD_ELEMENT, XSD_SEQUENCE, XSD_ANY, XSD_ANY_ATTRIBUTE
from ..helpers import prefixed_to_qname, has_xsd_components, get_xsd_derivation_attribute
from ..namespaces import XSD_NAMESPACE, XML_NAMESPACE, HFP_NAMESPACE, XSI_NAMESPACE, XHTML_NAMESPACE, \
from ..qnames import XSD_SCHEMA, XSD_ANNOTATION, XSD_NOTATION, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, \
XSD_GROUP, XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE, XSD_ELEMENT, XSD_SEQUENCE, XSD_ANY, \
XSD_ANY_ATTRIBUTE, XSD_REDEFINE, XSD_OVERRIDE
from ..helpers import has_xsd_components, get_xsd_derivation_attribute, get_xsd_form_attribute
from ..namespaces import XSD_NAMESPACE, XML_NAMESPACE, XSI_NAMESPACE, XHTML_NAMESPACE, \
XLINK_NAMESPACE, NamespaceResourcesMap, NamespaceView
from ..etree import etree_element, etree_tostring
from ..etree import etree_element, etree_tostring, ParseError
from ..resources import is_remote_url, url_path_is_file, fetch_resource, XMLResource
from ..converters import XMLSchemaConverter
from ..xpath import ElementPathMixin
from . import (
XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaNotBuiltError,
XMLSchemaIncludeWarning, XMLSchemaImportWarning, XsdValidator, ValidationMixin, XsdComponent,
XsdNotation, XsdComplexType, XsdAttribute, XsdElement, XsdAttributeGroup, XsdGroup, Xsd11Group,
XsdAnyElement, XsdAnyAttribute, Xsd11Attribute, Xsd11Element, Xsd11AnyElement, XsdGlobals,
Xsd11AnyAttribute, Xsd11ComplexType, xsd_simple_type_factory,
XsdAtomicRestriction, Xsd11AtomicRestriction
)
from .exceptions import XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaEncodeError, \
XMLSchemaNotBuiltError, XMLSchemaIncludeWarning, XMLSchemaImportWarning
from .xsdbase import XsdValidator, ValidationMixin, XsdComponent
from .notations import XsdNotation
from .simple_types import xsd_simple_type_factory, XsdUnion, XsdAtomicRestriction, \
Xsd11AtomicRestriction, Xsd11Union
from .attributes import XsdAttribute, XsdAttributeGroup, Xsd11Attribute
from .complex_types import XsdComplexType, Xsd11ComplexType
from .groups import XsdGroup, Xsd11Group
from .elements import XsdElement, Xsd11Element
from .wildcards import XsdAnyElement, XsdAnyAttribute, Xsd11AnyElement, Xsd11AnyAttribute
from .globals_ import iterchildren_xsd_import, iterchildren_xsd_include, \
iterchildren_xsd_redefine, iterchildren_xsd_override
iterchildren_xsd_redefine, iterchildren_xsd_override, XsdGlobals
# Elements for building dummy groups
@ -90,7 +94,9 @@ class XMLSchemaMeta(ABCMeta):
meta_schema = dict_.get('meta_schema') or get_attribute('meta_schema', *bases)
if meta_schema is None:
# Defining a subclass without a meta-schema (eg. XMLSchemaBase)
return super(XMLSchemaMeta, mcs).__new__(mcs, name, bases, dict_)
dict_['meta_schema'] = None
xsd_version = dict_.get('XSD_VERSION') or get_attribute('XSD_VERSION', *bases)
if xsd_version not in ('1.0', '1.1'):
@ -113,23 +119,15 @@ class XMLSchemaMeta(ABCMeta):
elif get_attribute('BUILDERS_MAP', *bases) is None:
raise XMLSchemaValueError("Validator class doesn't have a builder map for XSD globals.")
dict_['meta_schema'] = None
if isinstance(meta_schema, XMLSchemaBase):
meta_schema = meta_schema.url
# Build the meta-schema class
# Build the new meta-schema class
meta_schema_class_name = 'Meta' + name
meta_schema_class = super(XMLSchemaMeta, mcs).__new__(mcs, meta_schema_class_name, bases, dict_)
meta_schema_class.__qualname__ = meta_schema_class_name
meta_schema = meta_schema_class(meta_schema, defuse='never', build=False)
globals()[meta_schema_class_name] = meta_schema_class
base_schemas = dict_.get('BASE_SCHEMAS') or get_attribute('BASE_SCHEMAS', *bases)
for uri, pathname in list(base_schemas.items()):
if uri == XSD_NAMESPACE:
meta_schema.include_schema(location=pathname)
else:
meta_schema.import_schema(namespace=uri, location=pathname)
# Build the new meta-schema instance
schema_location = meta_schema.url if isinstance(meta_schema, XMLSchemaBase) else meta_schema
meta_schema = meta_schema_class.create_meta_schema(schema_location)
meta_schema.maps.build()
dict_['meta_schema'] = meta_schema
@ -173,6 +171,10 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:type timeout: int
:param build: defines whether build the schema maps. Default is `True`.
:type build: bool
:param use_meta: if `True` the schema processor uses the package meta-schema, otherwise the \
meta-schema is added at the end. In the latter case the meta-schema is rebuilt if any base \
namespace has been overridden by an import. Ignored if the argument *global_maps* is provided.
:type use_meta: bool
:cvar XSD_VERSION: store the XSD version (1.0 or 1.1).
:vartype XSD_VERSION: str
@ -186,6 +188,16 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:vartype BASE_SCHEMAS: dict
:cvar meta_schema: the XSD meta-schema instance.
:vartype meta_schema: XMLSchema
:cvar attribute_form_default: the schema's *attributeFormDefault* attribute, defaults to 'unqualified'.
:vartype attribute_form_default: str
:cvar element_form_default: the schema's *elementFormDefault* attribute, defaults to 'unqualified'
:vartype element_form_default: str
:cvar block_default: the schema's *blockDefault* attribute, defaults to ''.
:vartype block_default: str
:cvar final_default: the schema's *finalDefault* attribute, defaults to ''.
:vartype final_default: str
:cvar default_attributes: the XSD 1.1 schema's *defaultAttributes* attribute, defaults to ``None``.
:vartype default_attributes: XsdAttributeGroup
:ivar target_namespace: is the *targetNamespace* of the schema, the namespace to which \
belong the declarations/definitions of the schema. If it's empty no namespace is associated \
@ -203,8 +215,14 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:vartype locations: NamespaceResourcesMap
:ivar namespaces: a dictionary that maps from the prefixes used by the schema into namespace URI.
:vartype namespaces: dict
:ivar imports: a dictionary of namespace imports of the schema, that maps namespace URI to imported schema \
object, or `None` in case of unsuccessful import.
:vartype imports: dict
:ivar includes: a dictionary of included schemas, that maps a schema location to an included schema. \
It also comprehend schemas included by "xs:redefine" or "xs:override" statements.
:vartype warnings: dict
:ivar warnings: warning messages about failure of import and include elements.
:vartype namespaces: list
:vartype warnings: list
:ivar notations: `xsd:notation` declarations.
:vartype notations: NamespaceView
@ -225,70 +243,105 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
BASE_SCHEMAS = None
meta_schema = None
def __init__(self, source, namespace=None, validation='strict', global_maps=None, converter=None,
locations=None, base_url=None, defuse='remote', timeout=300, build=True):
super(XMLSchemaBase, self).__init__(validation)
try:
self.source = XMLResource(source, base_url, defuse, timeout, lazy=False)
except (XMLSchemaTypeError, OSError, IOError) as err:
raise type(err)('cannot create schema: %s' % err)
# Schema defaults
target_namespace = ''
attribute_form_default = 'unqualified'
element_form_default = 'unqualified'
block_default = ''
final_default = ''
default_attributes = None # for XSD 1.1
def __init__(self, source, namespace=None, validation='strict', global_maps=None, converter=None,
locations=None, base_url=None, defuse='remote', timeout=300, build=True, use_meta=True):
super(XMLSchemaBase, self).__init__(validation)
self.source = XMLResource(source, base_url, defuse, timeout, lazy=False)
self.imports = {}
self.includes = {}
self.warnings = []
self._root_elements = None
root = self.source.root
# Set and check target namespace
self.target_namespace = root.get('targetNamespace', '')
if self.target_namespace == XSD_NAMESPACE and self.meta_schema is not None:
raise XMLSchemaValueError("The %r cannot be used as target namespace!" % XSD_NAMESPACE)
# Parse namespaces and targetNamespace
self.namespaces = {'xml': XML_NAMESPACE} # the XML namespace is implicit
self.namespaces.update(self.source.get_namespaces())
try:
self.target_namespace = root.attrib['targetNamespace']
except KeyError:
pass
else:
if self.target_namespace == '':
# Ref: https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/structures.html#element-schema
self.parse_error("The attribute 'targetNamespace' cannot be an empty string.", root)
if namespace is not None and self.target_namespace != namespace:
if self.target_namespace:
msg = u"wrong namespace (%r instead of %r) for XSD resource %r."
self.parse_error(msg % (self.target_namespace, namespace, self.url), root)
else:
self.target_namespace = namespace # Chameleon schema
# Chameleon schema case: set the target namespace and the default namespace
self.target_namespace = namespace
if '' not in self.namespaces:
self.namespaces[''] = namespace
# Parses the schema defaults
if 'attributeFormDefault' in root.attrib:
try:
self.attribute_form_default = get_xsd_form_attribute(root, 'attributeFormDefault')
except ValueError as err:
self.parse_error(err, root)
if 'elementFormDefault' in root.attrib:
try:
self.element_form_default = get_xsd_form_attribute(root, 'elementFormDefault')
except ValueError as err:
self.parse_error(err, root)
if 'blockDefault' in root.attrib:
try:
self.block_default = get_xsd_derivation_attribute(
root, 'blockDefault', {'extension', 'restriction', 'substitution'}
)
except ValueError as err:
self.parse_error(err, root)
if 'finalDefault' in root.attrib:
try:
self.final_default = get_xsd_derivation_attribute(root, 'finalDefault')
except ValueError as err:
self.parse_error(err, root)
if self.XSD_VERSION > '1.0':
# XSD 1.1: "defaultAttributes" and "xpathDefaultNamespace"
self.xpath_default_namespace = self._parse_xpath_default_namespace(root)
if 'defaultAttributes' in root.attrib:
try:
self.default_attributes = self.resolve_qname(root.attrib['defaultAttributes'])
except XMLSchemaValueError as error:
self.parse_error(str(error), root)
# Set locations hints map and converter
self.locations = NamespaceResourcesMap(self.source.get_locations(locations))
if self.meta_schema is not None:
# Add fallback schema location hint for XHTML
self.locations[XHTML_NAMESPACE] = os.path.join(SCHEMAS_DIR, 'xhtml1-strict.xsd')
self.namespaces = {'xml': XML_NAMESPACE} # the XML namespace is implicit
self.namespaces.update(self.source.get_namespaces())
if '' not in self.namespaces:
# For default local names are mapped to targetNamespace
self.namespaces[''] = self.target_namespace
self.converter = self.get_converter(converter)
# XSD 1.1 attributes "defaultAttributes" and "xpathDefaultNamespace"
if self.XSD_VERSION > '1.0':
self.xpath_default_namespace = self._parse_xpath_default_namespace(root)
try:
self.default_attributes = prefixed_to_qname(root.attrib['defaultAttributes'], self.namespaces)
except KeyError:
self.default_attributes = None
except XMLSchemaValueError as error:
self.parse_error(str(error), root)
self.default_attributes = None
# Create or set the XSD global maps instance
if global_maps is None:
if self.meta_schema is None:
self.maps = XsdGlobals(self.__class__)
elif self.target_namespace in self.BASE_SCHEMAS:
# Change the meta-schema instance
meta_schema_class = self.meta_schema.__class__
meta_schema = meta_schema_class(self.meta_schema.url, build=False)
for uri, pathname in list(self.BASE_SCHEMAS.items()):
if uri == self.target_namespace:
meta_schema.import_schema(namespace=uri, location=self.url)
else:
meta_schema.import_schema(namespace=uri, location=pathname)
self.meta_schema = meta_schema
self.maps = self.meta_schema.maps
else:
if self.meta_schema is None:
self.maps = global_maps or XsdGlobals(self)
return # Meta-schemas don't need to be checked or built and don't process include/imports
elif global_maps is None:
if use_meta is False:
self.maps = XsdGlobals(self, validation)
self.locations.update(self.BASE_SCHEMAS)
elif self.target_namespace not in self.BASE_SCHEMAS:
self.maps = self.meta_schema.maps.copy(self, validation=validation)
else:
base_schemas = {k: v for k, v in self.BASE_SCHEMAS.items() if k != self.target_namespace}
meta_schema = self.create_meta_schema(base_schemas=base_schemas)
self.maps = meta_schema.maps
self.meta_schema = meta_schema
elif isinstance(global_maps, XsdGlobals):
self.maps = global_maps
@ -296,10 +349,7 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
raise XMLSchemaTypeError("'global_maps' argument must be a %r instance." % XsdGlobals)
# Validate the schema document
if self.meta_schema is None:
# Base schemas use single file and don't have to be checked
return
elif validation == 'strict':
if validation == 'strict':
self.check_schema(root, self.namespaces)
elif validation == 'lax':
self.errors.extend([e for e in self.meta_schema.iter_errors(root, namespaces=self.namespaces)])
@ -308,6 +358,9 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
self._include_schemas()
self._import_namespaces()
if '' not in self.namespaces:
self.namespaces[''] = '' # For default local names are mapped to no namespace
if build:
self.maps.build()
@ -321,11 +374,10 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
def __setattr__(self, name, value):
if name == 'root' and value.tag not in (XSD_SCHEMA, 'schema'):
raise XMLSchemaValueError("schema root element must has %r tag." % XSD_SCHEMA)
elif name == 'validation':
if value not in ('strict', 'lax', 'skip'):
raise XMLSchemaValueError("Wrong value %r for attribute 'validation'." % value)
elif name == 'maps':
value.register(self)
if self.meta_schema is None and hasattr(self, 'maps'):
raise XMLSchemaValueError("cannot change the global maps instance of a meta-schema")
super(XMLSchemaBase, self).__setattr__(name, value)
self.notations = NamespaceView(value.notations, self.target_namespace)
self.types = NamespaceView(value.types, self.target_namespace)
self.attributes = NamespaceView(value.attributes, self.target_namespace)
@ -336,7 +388,11 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
self.constraints = NamespaceView(value.constraints, self.target_namespace)
self.global_maps = (self.notations, self.types, self.attributes,
self.attribute_groups, self.groups, self.elements)
super(XMLSchemaBase, self).__setattr__(name, value)
value.register(self)
elif name == 'validation' and value not in ('strict', 'lax', 'skip'):
raise XMLSchemaValueError("Wrong value %r for attribute 'validation'." % value)
else:
super(XMLSchemaBase, self).__setattr__(name, value)
def __iter__(self):
for xsd_element in sorted(self.elements.values(), key=lambda x: x.name):
@ -390,6 +446,11 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
"""Timeout in seconds for fetching resources."""
return self.source.timeout
@property
def use_meta(self):
"""Returns `True` if the meta-schema is imported."""
return self.meta_schema is not None and XSD_NAMESPACE in self.maps.namespaces
# Schema root attributes
@property
def tag(self):
@ -406,30 +467,6 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
"""The schema's *version* attribute, defaults to ``None``."""
return self.root.get('version')
@property
def attribute_form_default(self):
"""The schema's *attributeFormDefault* attribute, defaults to ``'unqualified'``"""
return self.root.get('attributeFormDefault', 'unqualified')
@property
def element_form_default(self):
"""The schema's *elementFormDefault* attribute, defaults to ``'unqualified'``."""
return self.root.get('elementFormDefault', 'unqualified')
@property
def block_default(self):
"""The schema's *blockDefault* attribute, defaults to ``None``."""
return get_xsd_derivation_attribute(
self.root, 'blockDefault', ('extension', 'restriction', 'substitution')
)
@property
def final_default(self):
"""The schema's *finalDefault* attribute, defaults to ``None``."""
return get_xsd_derivation_attribute(
self.root, 'finalDefault', ('extension', 'restriction', 'list', 'union')
)
@property
def schema_location(self):
"""A list of location hints extracted from the *xsi:schemaLocation* attribute of the schema."""
@ -461,7 +498,9 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
try:
return cls.meta_schema.maps.namespaces[XSD_NAMESPACE][0].types
except KeyError:
raise XMLSchemaNotBuiltError(cls.meta_schema, "missing XSD namespace in meta-schema.")
raise XMLSchemaNotBuiltError(cls.meta_schema, "missing XSD namespace in meta-schema")
except AttributeError:
raise XMLSchemaNotBuiltError(cls.meta_schema, "meta-schema unavailable for %r" % cls)
@property
def root_elements(self):
@ -489,6 +528,46 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
return [e for e in self.elements.values() if e.name in self._root_elements]
@classmethod
def create_meta_schema(cls, source=None, base_schemas=None, global_maps=None):
"""
Creates a new meta-schema instance.
:param source: an optional argument referencing to or containing the XSD meta-schema \
resource. Required if the schema class doesn't already have a meta-schema.
:param base_schemas: an optional dictionary that contains namespace URIs and schema locations. \
If provided it's used as substitute for class 's BASE_SCHEMAS. Also a sequence of (namespace, \
location) items can be provided if there are more schema documents for one or more namespaces.
:param global_maps: is an optional argument containing an :class:`XsdGlobals` \
instance for the new meta schema. If not provided a new map is created.
"""
if source is None:
try:
source = cls.meta_schema.url
except AttributeError:
raise XMLSchemaValueError(
"The argument 'source' is required when the class doesn't already have a meta-schema"
)
if base_schemas is None:
base_schemas = cls.BASE_SCHEMAS.items()
elif isinstance(base_schemas, dict):
base_schemas = base_schemas.items()
else:
try:
base_schemas = [(n, l) for n, l in base_schemas]
except ValueError:
raise ValueError("The argument 'base_schemas' is not a dictionary nor a sequence of items")
meta_schema_class = cls if cls.meta_schema is None else cls.meta_schema.__class__
meta_schema = meta_schema_class(source, XSD_NAMESPACE, global_maps=global_maps, defuse='never', build=False)
for ns, location in base_schemas:
if ns == XSD_NAMESPACE:
meta_schema.include_schema(location=location)
else:
meta_schema.import_schema(namespace=ns, location=location)
return meta_schema
@classmethod
def create_schema(cls, *args, **kwargs):
"""Creates a new schema instance of the same class of the caller."""
@ -506,6 +585,22 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
attribute_group[None] = XsdAnyAttribute(ANY_ATTRIBUTE_ELEMENT, self, attribute_group)
return attribute_group
def copy(self):
"""Makes a copy of the schema instance. The new instance has independent maps of shared XSD components."""
schema = object.__new__(self.__class__)
schema.__dict__.update(self.__dict__)
schema.source = self.source.copy()
schema.errors = self.errors[:]
schema.warnings = self.warnings[:]
schema.namespaces = self.namespaces.copy()
schema.locations = NamespaceResourcesMap(self.locations)
schema.imports = dict(self.imports)
schema.includes = dict(self.includes)
schema.maps = self.maps.copy(validator=schema)
return schema
__copy__ = copy
@classmethod
def check_schema(cls, schema, namespaces=None):
"""
@ -526,25 +621,38 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
@property
def built(self):
xsd_global = None
for xsd_global in self.iter_globals():
for xsd_global in self.iter_globals(self):
if not isinstance(xsd_global, XsdComponent):
return False
if not xsd_global.built:
return False
if xsd_global is not None:
return True
else:
return False
prefix = '{%s}' % self.target_namespace if self.target_namespace else ''
for child in filter(lambda x: x.tag != XSD_ANNOTATION, self.root):
if child.tag in (XSD_REDEFINE, XSD_OVERRIDE):
for e in filter(lambda x: x.tag in self.BUILDERS_MAP, child):
name = e.get('name')
if name is not None:
try:
if not self.maps.lookup(e.tag, prefix + name if prefix else name).built:
return False
except KeyError:
return False
elif child.tag in self.BUILDERS_MAP:
name = child.get('name')
if name is not None:
try:
if not self.maps.lookup(child.tag, prefix + name if prefix else name).built:
return False
except KeyError:
return False
return True
@property
def validation_attempted(self):
"""
Property that returns the XSD component validation status. It can be
'full', 'partial' or 'none'.
| https://www.w3.org/TR/xmlschema-1/#e-validation_attempted
| https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#e-validation_attempted
"""
if self.built:
return 'full'
elif any([comp.validation_attempted == 'partial' for comp in self.iter_globals()]):
@ -554,9 +662,9 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
def iter_globals(self, schema=None):
"""
Creates an iterator for XSD global definitions/declarations.
Creates an iterator for XSD global definitions/declarations related to schema namespace.
:param schema: Optional schema instance.
:param schema: Optional argument for filtering only globals related to a schema instance.
"""
if schema is None:
for global_map in self.global_maps:
@ -574,7 +682,7 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
yield self
for xsd_global in self.iter_globals():
for xsd_global in self.iter_globals(self):
for obj in xsd_global.iter_components(xsd_classes):
yield obj
@ -622,6 +730,14 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
# https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#src-include
self.warnings.append("Include schema failed: %s." % str(err))
warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3)
except (XMLSchemaURLError, XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err:
msg = 'cannot include schema %r: %s' % (child.attrib['schemaLocation'], err)
if isinstance(err, (XMLSchemaParseError, ParseError)):
self.parse_error(msg)
elif self.validation == 'strict':
raise type(err)(msg)
else:
self.errors.append(type(err)(msg))
for child in iterchildren_xsd_redefine(self.root):
try:
@ -635,6 +751,14 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3)
if has_xsd_components(child):
self.parse_error(str(err), child)
except (XMLSchemaURLError, XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err:
msg = 'cannot redefine schema %r: %s' % (child.attrib['schemaLocation'], err)
if isinstance(err, (XMLSchemaParseError, ParseError)):
self.parse_error(msg)
elif self.validation == 'strict':
raise type(err)(msg)
else:
self.errors.append(type(err)(msg))
def include_schema(self, location, base_url=None):
"""
@ -644,40 +768,56 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:param base_url: is an optional base URL for fetching the schema resource.
:return: the included :class:`XMLSchema` instance.
"""
try:
schema_url = fetch_resource(location, base_url)
except XMLSchemaURLError as err:
raise XMLSchemaOSError("cannot include schema from %r: %s." % (location, err))
schema_url = fetch_resource(location, base_url)
for schema in self.maps.namespaces[self.target_namespace]:
if schema_url == schema.url:
break
else:
for schema in self.maps.namespaces[self.target_namespace]:
if schema_url == schema.url:
return schema
try:
return self.create_schema(
schema = self.create_schema(
schema_url, self.target_namespace, self.validation, self.maps, self.converter,
self.locations, self.base_url, self.defuse, self.timeout, False
)
except XMLSchemaParseError as err:
err.message = 'cannot include %r: %s' % (schema_url, err.message)
raise err
except (XMLSchemaTypeError, OSError, IOError) as err:
raise type(err)('cannot include %r: %s' % (schema_url, err))
if location not in self.includes:
self.includes[location] = schema
elif self.includes[location] != schema:
self.includes[schema_url] = schema
return schema
def _import_namespaces(self):
"""Processes namespace imports. Return a list of exceptions."""
"""
Processes namespace imports. Imports are done on namespace basis not on resource: this
is the standard and also avoids import loops that sometimes are hard to detect.
"""
namespace_imports = NamespaceResourcesMap(map(
lambda x: (x.get('namespace', '').strip(), x.get('schemaLocation')),
lambda x: (x.get('namespace'), x.get('schemaLocation')),
iterchildren_xsd_import(self.root)
))
for namespace, locations in namespace_imports.items():
if namespace in self.maps.namespaces:
# Imports are done on namespace basis not on resource: this is the standard
# and also avoids import loops that sometimes are hard to detect.
# Checks the namespace against the targetNamespace of the schema
if namespace is None:
namespace = ''
if namespace == self.target_namespace:
self.parse_error("if the 'namespace' attribute is not present on the import statement "
"then the importing schema must has a 'targetNamespace'")
continue
elif namespace == self.target_namespace:
self.parse_error("the attribute 'namespace' must be different from schema's 'targetNamespace'")
continue
# Skip import of already imported namespaces
if self.imports.get(namespace) is not None:
continue
elif namespace in self.maps.namespaces:
self.imports[namespace] = self.maps.namespaces[namespace][0]
continue
locations = [url for url in locations if url]
if not locations:
if not namespace:
pass
elif not locations:
locations = self.get_locations(namespace)
elif all(is_remote_url(url) for url in locations):
# If all import schema locations are remote URLs and there are local hints
@ -698,14 +838,26 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
# https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#composition-schemaImport
if import_error is None:
import_error = err
except (XMLSchemaURLError, XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err:
if namespace:
msg = "cannot import namespace %r: %s." % (namespace, err)
else:
msg = "cannot import chameleon schema: %s." % err
if isinstance(err, (XMLSchemaParseError, ParseError)):
self.parse_error(msg)
elif self.validation == 'strict':
raise type(err)(msg)
else:
self.errors.append(type(err)(msg))
except XMLSchemaValueError as err:
self.parse_error(err)
else:
break
else:
if import_error is None:
self.warnings.append("Namespace import failed: no schema location provided.")
else:
if import_error is not None:
self.warnings.append("Namespace import failed: %s." % str(import_error))
warnings.warn(self.warnings[-1], XMLSchemaImportWarning, stacklevel=3)
warnings.warn(self.warnings[-1], XMLSchemaImportWarning, stacklevel=3)
self.imports[namespace] = None
def import_schema(self, namespace, location, base_url=None, force=False):
"""
@ -717,33 +869,72 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:param force: is set to `True` imports the schema also if the namespace is already imported.
:return: the imported :class:`XMLSchema` instance.
"""
if namespace in self.maps.namespaces and not force:
return
if not force:
if self.imports.get(namespace) is not None:
return self.imports[namespace]
elif namespace in self.maps.namespaces:
self.imports[namespace] = self.maps.namespaces[namespace][0]
return self.imports[namespace]
try:
schema_url = fetch_resource(location, base_url)
except XMLSchemaURLError as err:
if namespace:
raise XMLSchemaOSError("cannot import namespace %r from %r: %s." % (namespace, location, err))
schema_url = fetch_resource(location, base_url)
if self.imports.get(namespace) is not None and self.imports[namespace].url == schema_url:
return self.imports[namespace]
elif namespace in self.maps.namespaces:
for schema in self.maps.namespaces[namespace]:
if schema_url == schema.url:
self.imports[namespace] = schema
return schema
schema = self.create_schema(
schema_url, None, self.validation, self.maps, self.converter,
self.locations, self.base_url, self.defuse, self.timeout, False
)
if schema.target_namespace != namespace:
raise XMLSchemaValueError('imported schema %r has an unmatched namespace %r' % (location, namespace))
self.imports[namespace] = schema
return schema
def resolve_qname(self, qname):
"""
QName resolution for a schema instance.
:param qname: a string in xs:QName format.
:returns: an expanded QName in the format "{*namespace-URI*}*local-name*".
:raises: `XMLSchemaValueError` for an invalid xs:QName or if the namespace prefix is not \
declared in the schema instance or if the namespace is not the *targetNamespace* and \
the namespace is not imported by the schema.
"""
qname = qname.strip()
if not qname or ' ' in qname or '\t' in qname or '\n' in qname:
raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname))
if qname[0] == '{':
try:
namespace, local_name = qname[1:].split('}')
except ValueError:
raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname))
elif ':' in qname:
try:
prefix, local_name = qname.split(':')
except ValueError:
raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname))
else:
raise XMLSchemaOSError("cannot import chameleon schema from %r: %s." % (location, err))
try:
namespace = self.namespaces[prefix]
except KeyError:
raise XMLSchemaValueError("prefix %r not found in namespace map" % prefix)
else:
if namespace in self.maps.namespaces:
for schema in self.maps.namespaces[namespace]:
if schema_url == schema.url:
return schema
namespace, local_name = self.namespaces.get('', ''), qname
try:
namespace = namespace or self.target_namespace
return self.create_schema(
schema_url, namespace, self.validation, self.maps, self.converter,
self.locations, self.base_url, self.defuse, self.timeout, False
if not namespace:
return local_name
elif self.meta_schema is not None and namespace != self.target_namespace and \
namespace not in {XSD_NAMESPACE, XSI_NAMESPACE} and namespace not in self.imports:
raise XMLSchemaValueError(
"the QName {!r} is mapped to the namespace {!r}, but this namespace has "
"not an xs:import statement in the schema.".format(qname, namespace)
)
except XMLSchemaParseError as err:
err.message = 'cannot import namespace %r: %s' % (namespace, err.message)
raise err
except (XMLSchemaTypeError, OSError, IOError) as err:
raise type(err)('cannot import namespace %r: %s' % (namespace, err))
return '{%s}%s' % (namespace, local_name)
def iter_decode(self, source, path=None, validation='lax', process_namespaces=True,
namespaces=None, use_defaults=True, decimal_type=None, datetime_types=False,
@ -793,6 +984,7 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
namespaces = {}
converter = self.get_converter(converter, namespaces, **kwargs)
id_map = Counter()
if path is None:
xsd_element = self.find(source.root.tag, namespaces=namespaces)
@ -803,7 +995,7 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
for obj in xsd_element.iter_decode(
source.root, validation, converter, source=source, namespaces=namespaces,
use_defaults=use_defaults, decimal_type=decimal_type,
datetime_types=datetime_types, **kwargs):
datetime_types=datetime_types, id_map=id_map, **kwargs):
yield obj
else:
xsd_element = self.find(path, namespaces=namespaces)
@ -816,9 +1008,13 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
for obj in xsd_element.iter_decode(
elem, validation, converter, source=source, namespaces=namespaces,
use_defaults=use_defaults, decimal_type=decimal_type,
datetime_types=datetime_types, **kwargs):
datetime_types=datetime_types, id_map=id_map, **kwargs):
yield obj
for k, v in id_map.items():
if v != 1:
self.parse_error("Duplicated xsd:ID value {!r}".format(k), self.root)
def iter_encode(self, obj, path=None, validation='lax', namespaces=None, converter=None, **kwargs):
"""
Creates an iterator for encoding a data structure to an ElementTree's Element.
@ -892,12 +1088,13 @@ class XMLSchema10(XMLSchemaBase):
'element_class': XsdElement,
'any_element_class': XsdAnyElement,
'restriction_class': XsdAtomicRestriction,
'union_class': XsdUnion,
'simple_type_factory': xsd_simple_type_factory
}
meta_schema = os.path.join(SCHEMAS_DIR, 'XSD_1.0/XMLSchema.xsd')
BASE_SCHEMAS = {
XML_NAMESPACE: XML_SCHEMA_FILE,
HFP_NAMESPACE: HFP_SCHEMA_FILE,
# HFP_NAMESPACE: HFP_SCHEMA_FILE,
XSI_NAMESPACE: XSI_SCHEMA_FILE,
XLINK_NAMESPACE: XLINK_SCHEMA_FILE,
}
@ -949,13 +1146,14 @@ class XMLSchema11(XMLSchemaBase):
'element_class': Xsd11Element,
'any_element_class': Xsd11AnyElement,
'restriction_class': Xsd11AtomicRestriction,
'union_class': Xsd11Union,
'simple_type_factory': xsd_simple_type_factory
}
meta_schema = os.path.join(SCHEMAS_DIR, 'XSD_1.1/XMLSchema.xsd')
BASE_SCHEMAS = {
XSD_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XSD_1.1/list_builtins.xsd'),
XML_NAMESPACE: XML_SCHEMA_FILE,
HFP_NAMESPACE: HFP_SCHEMA_FILE,
# HFP_NAMESPACE: HFP_SCHEMA_FILE,
XSI_NAMESPACE: XSI_SCHEMA_FILE,
XLINK_NAMESPACE: XLINK_SCHEMA_FILE,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version='1.0'?>
<!DOCTYPE xs:schema PUBLIC "-//W3C//DTD XSD 1.1//EN" "XMLSchema.dtd" [
<!-- provide ID type information even for parsers which only read the
@ -51,7 +52,11 @@
<!ATTLIST xs:list id ID #IMPLIED>
<!ATTLIST xs:union id ID #IMPLIED>
]>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xml:lang="EN" targetNamespace="http://www.w3.org/2001/XMLSchema" version="1.0">
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified" xml:lang="EN"
targetNamespace="http://www.w3.org/2001/XMLSchema"
version="1.0">
<xs:annotation>
<xs:documentation>
Part 1 version: structures.xsd (rec-20120405)
@ -60,7 +65,7 @@
</xs:annotation>
<xs:annotation>
<xs:documentation source="../structures/structures.html">
<xs:documentation source="../structures/structures.html">
The schema corresponding to this document is normative,
with respect to the syntactic constraints it expresses in the
XML Schema Definition Language. The documentation (within 'documentation' elements)
@ -78,7 +83,8 @@
The simpleType element and all of its members are defined
towards the end of this schema document.</xs:documentation>
</xs:annotation>
<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd">
<xs:import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/xml.xsd">
<xs:annotation>
<xs:documentation>
Get access to the xml: attribute groups for xml:lang
@ -104,7 +110,7 @@
<xs:annotation>
<xs:documentation>
This type is extended by all types which allow annotation
other than &lt;schema&gt; itself
other than &lt;schema> itself
</xs:documentation>
</xs:annotation>
<xs:complexContent>
@ -143,7 +149,7 @@
<xs:annotation>
<xs:documentation>
This group is for the
elements which can self-redefine (see &lt;redefine&gt; below).</xs:documentation>
elements which can self-redefine (see &lt;redefine> below).</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element ref="xs:simpleType"/>
@ -222,7 +228,8 @@
</xs:simpleType>
<xs:element name="schema" id="schema">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-schema"/>
<xs:documentation
source="../structures/structures.html#element-schema"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -231,21 +238,28 @@
<xs:group ref="xs:composition" minOccurs="0" maxOccurs="unbounded"/>
<xs:sequence minOccurs="0">
<xs:element ref="xs:defaultOpenContent"/>
<xs:element ref="xs:annotation" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="xs:annotation" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:group ref="xs:schemaTop"/>
<xs:element ref="xs:annotation" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="xs:annotation" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>
</xs:sequence>
<xs:attribute name="targetNamespace" type="xs:anyURI"/>
<xs:attribute name="version" type="xs:token"/>
<xs:attribute name="finalDefault" type="xs:fullDerivationSet" default="" use="optional"/>
<xs:attribute name="blockDefault" type="xs:blockSet" default="" use="optional"/>
<xs:attribute name="attributeFormDefault" type="xs:formChoice" default="unqualified" use="optional"/>
<xs:attribute name="elementFormDefault" type="xs:formChoice" default="unqualified" use="optional"/>
<xs:attribute name="finalDefault" type="xs:fullDerivationSet"
default="" use="optional"/>
<xs:attribute name="blockDefault" type="xs:blockSet" default=""
use="optional"/>
<xs:attribute name="attributeFormDefault" type="xs:formChoice"
default="unqualified" use="optional"/>
<xs:attribute name="elementFormDefault" type="xs:formChoice"
default="unqualified" use="optional"/>
<xs:attribute name="defaultAttributes" type="xs:QName"/>
<xs:attribute name="xpathDefaultNamespace" type="xs:xpathDefaultNamespace" default="##local" use="optional"/>
<xs:attribute name="xpathDefaultNamespace" type="xs:xpathDefaultNamespace"
default="##local" use="optional"/>
<xs:attribute name="id" type="xs:ID"/>
<xs:attribute ref="xml:lang"/>
</xs:extension>
@ -298,7 +312,8 @@
<xs:documentation>
for all particles</xs:documentation>
</xs:annotation>
<xs:attribute name="minOccurs" type="xs:nonNegativeInteger" default="1" use="optional"/>
<xs:attribute name="minOccurs" type="xs:nonNegativeInteger" default="1"
use="optional"/>
<xs:attribute name="maxOccurs" type="xs:allNNI" default="1" use="optional"/>
</xs:attributeGroup>
<xs:attributeGroup name="defRef">
@ -394,21 +409,24 @@
<xs:element ref="xs:anyAttribute" minOccurs="0"/>
</xs:sequence>
</xs:group>
<xs:element name="anyAttribute" id="anyAttribute">
<xs:element name="anyAttribute" id="anyAttribute">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-anyAttribute"/>
<xs:documentation
source="../structures/structures.html#element-anyAttribute"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
<xs:extension base="xs:wildcard">
<xs:attribute name="notQName" type="xs:qnameListA" use="optional"/>
<xs:attribute name="notQName" type="xs:qnameListA"
use="optional"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:group name="assertions">
<xs:sequence>
<xs:element name="assert" type="xs:assertion" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="assert" type="xs:assertion"
minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:group>
<xs:complexType name="assertion">
@ -427,11 +445,11 @@
<xs:annotation>
<xs:documentation>
This branch is short for
&lt;complexContent&gt;
&lt;restriction base="xs:anyType"&gt;
&lt;complexContent>
&lt;restriction base="xs:anyType">
...
&lt;/restriction&gt;
&lt;/complexContent&gt;</xs:documentation>
&lt;/restriction>
&lt;/complexContent></xs:documentation>
</xs:annotation>
<xs:element ref="xs:openContent" minOccurs="0"/>
<xs:group ref="xs:typeDefParticle" minOccurs="0"/>
@ -457,10 +475,12 @@
May be overridden by setting on complexContent child.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="abstract" type="xs:boolean" default="false" use="optional"/>
<xs:attribute name="abstract" type="xs:boolean" default="false"
use="optional"/>
<xs:attribute name="final" type="xs:derivationSet"/>
<xs:attribute name="block" type="xs:derivationSet"/>
<xs:attribute name="defaultAttributesApply" type="xs:boolean" default="true" use="optional"/>
<xs:attribute name="defaultAttributesApply" type="xs:boolean"
default="true" use="optional"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
@ -549,7 +569,8 @@
</xs:complexType>
<xs:element name="complexContent" id="complexContent">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-complexContent"/>
<xs:documentation
source="../structures/structures.html#element-complexContent"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -570,7 +591,8 @@
</xs:element>
<xs:element name="openContent" id="openContent">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-openContent"/>
<xs:documentation
source="../structures/structures.html#element-openContent"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -594,7 +616,8 @@
</xs:element>
<xs:element name="defaultOpenContent" id="defaultOpenContent">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-defaultOpenContent"/>
<xs:documentation
source="../structures/structures.html#element-defaultOpenContent"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -602,7 +625,8 @@
<xs:sequence>
<xs:element name="any" type="xs:wildcard"/>
</xs:sequence>
<xs:attribute name="appliesToEmpty" type="xs:boolean" default="false" use="optional"/>
<xs:attribute name="appliesToEmpty" type="xs:boolean"
default="false" use="optional"/>
<xs:attribute name="mode" default="interleave" use="optional">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
@ -653,7 +677,8 @@
</xs:complexType>
<xs:element name="simpleContent" id="simpleContent">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-simpleContent"/>
<xs:documentation
source="../structures/structures.html#element-simpleContent"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -668,7 +693,8 @@
</xs:element>
<xs:element name="complexType" type="xs:topLevelComplexType" id="complexType">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-complexType"/>
<xs:documentation
source="../structures/structures.html#element-complexType"/>
</xs:annotation>
</xs:element>
<xs:simpleType name="blockSet">
@ -714,8 +740,10 @@
<xs:element name="simpleType" type="xs:localSimpleType"/>
<xs:element name="complexType" type="xs:localComplexType"/>
</xs:choice>
<xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="alternative" type="xs:altType"
minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:attributeGroup ref="xs:defRef"/>
<xs:attribute name="type" type="xs:QName"/>
@ -729,7 +757,8 @@
<xs:attribute name="default" type="xs:string"/>
<xs:attribute name="fixed" type="xs:string"/>
<xs:attribute name="nillable" type="xs:boolean" use="optional"/>
<xs:attribute name="abstract" type="xs:boolean" default="false" use="optional"/>
<xs:attribute name="abstract" type="xs:boolean" default="false"
use="optional"/>
<xs:attribute name="final" type="xs:derivationSet"/>
<xs:attribute name="block" type="xs:blockSet"/>
<xs:attribute name="form" type="xs:formChoice"/>
@ -746,8 +775,10 @@
<xs:element name="simpleType" type="xs:localSimpleType"/>
<xs:element name="complexType" type="xs:localComplexType"/>
</xs:choice>
<xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="alternative" type="xs:altType"
minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ref" use="prohibited"/>
<xs:attribute name="form" use="prohibited"/>
@ -768,8 +799,10 @@
<xs:element name="simpleType" type="xs:localSimpleType"/>
<xs:element name="complexType" type="xs:localComplexType"/>
</xs:choice>
<xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="alternative" type="xs:altType"
minOccurs="0" maxOccurs="unbounded"/>
<xs:group ref="xs:identityConstraint" minOccurs="0"
maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="substitutionGroup" use="prohibited"/>
<xs:attribute name="final" use="prohibited"/>
@ -780,7 +813,8 @@
</xs:complexType>
<xs:element name="element" type="xs:topLevelElement" id="element">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-element"/>
<xs:documentation
source="../structures/structures.html#element-element"/>
</xs:annotation>
</xs:element>
<xs:complexType name="altType">
@ -967,12 +1001,14 @@
</xs:element>
<xs:element name="choice" type="xs:explicitGroup" id="choice">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-choice"/>
<xs:documentation
source="../structures/structures.html#element-choice"/>
</xs:annotation>
</xs:element>
<xs:element name="sequence" type="xs:explicitGroup" id="sequence">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-sequence"/>
<xs:documentation
source="../structures/structures.html#element-sequence"/>
</xs:annotation>
</xs:element>
<xs:element name="group" type="xs:namedGroup" id="group">
@ -981,7 +1017,8 @@
</xs:annotation>
</xs:element>
<xs:attributeGroup name="anyAttrGroup">
<xs:attribute name="namespace" type="xs:namespaceList" use="optional"/>
<xs:attribute name="namespace" type="xs:namespaceList"
use="optional"/>
<xs:attribute name="notNamespace" use="optional">
<xs:simpleType>
<xs:restriction base="xs:basicNamespaceList">
@ -1014,7 +1051,8 @@
<xs:complexType>
<xs:complexContent>
<xs:extension base="xs:wildcard">
<xs:attribute name="notQName" type="xs:qnameList" use="optional"/>
<xs:attribute name="notQName" type="xs:qnameList"
use="optional"/>
<xs:attributeGroup ref="xs:occurs"/>
</xs:extension>
</xs:complexContent>
@ -1050,7 +1088,7 @@
A utility type, not for public use</xs:documentation>
</xs:annotation>
<xs:union memberTypes="xs:specialNamespaceList xs:basicNamespaceList"/>
<xs:union memberTypes="xs:specialNamespaceList xs:basicNamespaceList" />
</xs:simpleType>
<xs:simpleType name="basicNamespaceList">
<xs:annotation>
@ -1130,7 +1168,8 @@
</xs:simpleType>
<xs:element name="attribute" type="xs:topLevelAttribute" id="attribute">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-attribute"/>
<xs:documentation
source="../structures/structures.html#element-attribute"/>
</xs:annotation>
</xs:element>
<xs:complexType name="attributeGroup" abstract="true">
@ -1169,14 +1208,17 @@
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<xs:element name="attributeGroup" type="xs:namedAttributeGroup" id="attributeGroup">
<xs:element name="attributeGroup" type="xs:namedAttributeGroup"
id="attributeGroup">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-attributeGroup"/>
<xs:documentation
source="../structures/structures.html#element-attributeGroup"/>
</xs:annotation>
</xs:element>
<xs:element name="include" id="include">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-include"/>
<xs:documentation
source="../structures/structures.html#element-include"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1188,7 +1230,8 @@
</xs:element>
<xs:element name="redefine" id="redefine">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-redefine"/>
<xs:documentation
source="../structures/structures.html#element-redefine"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1206,7 +1249,8 @@
<xs:element name="override" id="override">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-override"/>
<xs:documentation
source="../structures/structures.html#element-override"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1223,7 +1267,8 @@
</xs:element>
<xs:element name="import" id="import">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-import"/>
<xs:documentation
source="../structures/structures.html#element-import"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1236,7 +1281,8 @@
</xs:element>
<xs:element name="selector" id="selector">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-selector"/>
<xs:documentation
source="../structures/structures.html#element-selector"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1308,7 +1354,8 @@ use</xs:documentation>
</xs:group>
<xs:element name="unique" type="xs:keybase" id="unique">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-unique"/>
<xs:documentation
source="../structures/structures.html#element-unique"/>
</xs:annotation>
</xs:element>
<xs:element name="key" type="xs:keybase" id="key">
@ -1318,7 +1365,8 @@ use</xs:documentation>
</xs:element>
<xs:element name="keyref" id="keyref">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-keyref"/>
<xs:documentation
source="../structures/structures.html#element-keyref"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1330,7 +1378,8 @@ use</xs:documentation>
</xs:element>
<xs:element name="notation" id="notation">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-notation"/>
<xs:documentation
source="../structures/structures.html#element-notation"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1353,7 +1402,8 @@ use</xs:documentation>
</xs:simpleType>
<xs:element name="appinfo" id="appinfo">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-appinfo"/>
<xs:documentation
source="../structures/structures.html#element-appinfo"/>
</xs:annotation>
<xs:complexType mixed="true">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
@ -1365,7 +1415,8 @@ use</xs:documentation>
</xs:element>
<xs:element name="documentation" id="documentation">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-documentation"/>
<xs:documentation
source="../structures/structures.html#element-documentation"/>
</xs:annotation>
<xs:complexType mixed="true">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
@ -1378,7 +1429,8 @@ use</xs:documentation>
</xs:element>
<xs:element name="annotation" id="annotation">
<xs:annotation>
<xs:documentation source="../structures/structures.html#element-annotation"/>
<xs:documentation
source="../structures/structures.html#element-annotation"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1396,8 +1448,10 @@ use</xs:documentation>
<xs:documentation>
notations for use within schema documents</xs:documentation>
</xs:annotation>
<xs:notation name="XMLSchemaStructures" public="structures" system="http://www.w3.org/2000/08/XMLSchema.xsd"/>
<xs:notation name="XML" public="REC-xml-19980210" system="http://www.w3.org/TR/1998/REC-xml-19980210"/>
<xs:notation name="XMLSchemaStructures" public="structures"
system="http://www.w3.org/2000/08/XMLSchema.xsd"/>
<xs:notation name="XML" public="REC-xml-19980210"
system="http://www.w3.org/TR/1998/REC-xml-19980210"/>
<xs:complexType name="anyType" mixed="true">
<xs:annotation>
<xs:documentation>
@ -1553,7 +1607,8 @@ use</xs:documentation>
</xs:complexType>
<xs:element name="simpleType" type="xs:topLevelSimpleType" id="simpleType">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-simpleType"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-simpleType"/>
</xs:annotation>
</xs:element>
<xs:element name="facet" abstract="true">
@ -1569,16 +1624,19 @@ use</xs:documentation>
<xs:group name="simpleRestrictionModel">
<xs:sequence>
<xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:choice minOccurs="0"
maxOccurs="unbounded">
<xs:element ref="xs:facet"/>
<xs:any processContents="lax" namespace="##other"/>
<xs:any processContents="lax"
namespace="##other"/>
</xs:choice>
</xs:sequence>
</xs:group>
<xs:element name="restriction" id="restriction">
<xs:complexType>
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-restriction">
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-restriction">
base attribute and simpleType child are mutually
exclusive, but one or other is required
</xs:documentation>
@ -1594,7 +1652,8 @@ use</xs:documentation>
<xs:element name="list" id="list">
<xs:complexType>
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-list">
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-list">
itemType attribute and simpleType child are mutually
exclusive, but one or other is required
</xs:documentation>
@ -1602,7 +1661,8 @@ use</xs:documentation>
<xs:complexContent>
<xs:extension base="xs:annotated">
<xs:sequence>
<xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0"/>
<xs:element name="simpleType" type="xs:localSimpleType"
minOccurs="0"/>
</xs:sequence>
<xs:attribute name="itemType" type="xs:QName" use="optional"/>
</xs:extension>
@ -1612,7 +1672,8 @@ use</xs:documentation>
<xs:element name="union" id="union">
<xs:complexType>
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-union">
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-union">
memberTypes attribute must be non-empty or there must be
at least one simpleType child
</xs:documentation>
@ -1620,7 +1681,8 @@ use</xs:documentation>
<xs:complexContent>
<xs:extension base="xs:annotated">
<xs:sequence>
<xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="simpleType" type="xs:localSimpleType"
minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="memberTypes" use="optional">
<xs:simpleType>
@ -1635,7 +1697,8 @@ use</xs:documentation>
<xs:complexContent>
<xs:extension base="xs:annotated">
<xs:attribute name="value" use="required"/>
<xs:attribute name="fixed" type="xs:boolean" default="false" use="optional"/>
<xs:attribute name="fixed" type="xs:boolean" default="false"
use="optional"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
@ -1650,24 +1713,36 @@ use</xs:documentation>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<xs:element name="minExclusive" type="xs:facet" id="minExclusive" substitutionGroup="xs:facet">
<xs:element name="minExclusive" type="xs:facet"
id="minExclusive"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minExclusive"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-minExclusive"/>
</xs:annotation>
</xs:element>
<xs:element name="minInclusive" type="xs:facet" id="minInclusive" substitutionGroup="xs:facet">
<xs:element name="minInclusive" type="xs:facet"
id="minInclusive"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minInclusive"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-minInclusive"/>
</xs:annotation>
</xs:element>
<xs:element name="maxExclusive" type="xs:facet" id="maxExclusive" substitutionGroup="xs:facet">
<xs:element name="maxExclusive" type="xs:facet"
id="maxExclusive"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxExclusive"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-maxExclusive"/>
</xs:annotation>
</xs:element>
<xs:element name="maxInclusive" type="xs:facet" id="maxInclusive" substitutionGroup="xs:facet">
<xs:element name="maxInclusive" type="xs:facet"
id="maxInclusive"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxInclusive"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-maxInclusive"/>
</xs:annotation>
</xs:element>
<xs:complexType name="numFacet">
@ -1676,7 +1751,8 @@ use</xs:documentation>
<xs:sequence>
<xs:element ref="xs:annotation" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="value" type="xs:nonNegativeInteger" use="required"/>
<xs:attribute name="value"
type="xs:nonNegativeInteger" use="required"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:restriction>
</xs:complexContent>
@ -1694,9 +1770,11 @@ use</xs:documentation>
</xs:complexContent>
</xs:complexType>
<xs:element name="totalDigits" id="totalDigits" substitutionGroup="xs:facet">
<xs:element name="totalDigits" id="totalDigits"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-totalDigits"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-totalDigits"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1710,35 +1788,51 @@ use</xs:documentation>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="fractionDigits" type="xs:numFacet" id="fractionDigits" substitutionGroup="xs:facet">
<xs:element name="fractionDigits" type="xs:numFacet"
id="fractionDigits"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-fractionDigits"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-fractionDigits"/>
</xs:annotation>
</xs:element>
<xs:element name="length" type="xs:numFacet" id="length" substitutionGroup="xs:facet">
<xs:element name="length" type="xs:numFacet" id="length"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-length"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-length"/>
</xs:annotation>
</xs:element>
<xs:element name="minLength" type="xs:numFacet" id="minLength" substitutionGroup="xs:facet">
<xs:element name="minLength" type="xs:numFacet"
id="minLength"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minLength"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-minLength"/>
</xs:annotation>
</xs:element>
<xs:element name="maxLength" type="xs:numFacet" id="maxLength" substitutionGroup="xs:facet">
<xs:element name="maxLength" type="xs:numFacet"
id="maxLength"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxLength"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-maxLength"/>
</xs:annotation>
</xs:element>
<xs:element name="enumeration" type="xs:noFixedFacet" id="enumeration" substitutionGroup="xs:facet">
<xs:element name="enumeration" type="xs:noFixedFacet"
id="enumeration"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-enumeration"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-enumeration"/>
</xs:annotation>
</xs:element>
<xs:element name="whiteSpace" id="whiteSpace" substitutionGroup="xs:facet">
<xs:element name="whiteSpace" id="whiteSpace"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-whiteSpace"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-whiteSpace"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1760,9 +1854,11 @@ use</xs:documentation>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="pattern" id="pattern" substitutionGroup="xs:facet">
<xs:element name="pattern" id="pattern"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-pattern"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-pattern"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1770,20 +1866,26 @@ use</xs:documentation>
<xs:sequence>
<xs:element ref="xs:annotation" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other" processContents="lax"/>
<xs:attribute name="value" type="xs:string"
use="required"/>
<xs:anyAttribute namespace="##other"
processContents="lax"/>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="assertion" type="xs:assertion" id="assertion" substitutionGroup="xs:facet">
<xs:element name="assertion" type="xs:assertion"
id="assertion" substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-assertion"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-assertion"/>
</xs:annotation>
</xs:element>
<xs:element name="explicitTimezone" id="explicitTimezone" substitutionGroup="xs:facet">
<xs:element name="explicitTimezone" id="explicitTimezone"
substitutionGroup="xs:facet">
<xs:annotation>
<xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-explicitTimezone"/>
<xs:documentation
source="http://www.w3.org/TR/xmlschema11-2/#element-explicitTimezone"/>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
@ -1845,4 +1947,4 @@ use</xs:documentation>
</xs:schema>
</xs:schema>

View File

@ -15,19 +15,21 @@ from __future__ import unicode_literals
from decimal import DecimalException
from ..compat import string_base_type, unicode_type
from ..etree import etree_element
from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError
from ..qnames import (
XSD_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ANY_ATTRIBUTE,
XSD_PATTERN, XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, XSD_MAX_INCLUSIVE, XSD_MAX_EXCLUSIVE,
XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_WHITE_SPACE, XSD_LIST, XSD_ANY_SIMPLE_TYPE,
XSD_UNION, XSD_RESTRICTION, XSD_ANNOTATION, XSD_ASSERTION
XSD_ANY_TYPE, XSD_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP,
XSD_ANY_ATTRIBUTE, XSD_PATTERN, XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, XSD_MAX_INCLUSIVE,
XSD_MAX_EXCLUSIVE, XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_WHITE_SPACE, XSD_LIST,
XSD_ANY_SIMPLE_TYPE, XSD_UNION, XSD_RESTRICTION, XSD_ANNOTATION, XSD_ASSERTION, XSD_ID,
XSD_FRACTION_DIGITS, XSD_TOTAL_DIGITS
)
from ..helpers import get_qname, local_name, prefixed_to_qname, get_xsd_derivation_attribute
from ..helpers import get_qname, local_name, get_xsd_derivation_attribute
from .exceptions import XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaDecodeError, XMLSchemaParseError
from .xsdbase import XsdAnnotation, XsdType, ValidationMixin
from .facets import XsdFacet, XSD_10_FACETS_BUILDERS, XSD_11_FACETS_BUILDERS, XSD_10_FACETS, XSD_11_FACETS, \
XSD_10_LIST_FACETS, XSD_11_LIST_FACETS, XSD_10_UNION_FACETS, XSD_11_UNION_FACETS, MULTIPLE_FACETS
from .facets import XsdFacet, XsdWhiteSpaceFacet, XSD_10_FACETS_BUILDERS, XSD_11_FACETS_BUILDERS, XSD_10_FACETS, \
XSD_11_FACETS, XSD_10_LIST_FACETS, XSD_11_LIST_FACETS, XSD_10_UNION_FACETS, XSD_11_UNION_FACETS, MULTIPLE_FACETS
def xsd_simple_type_factory(elem, schema, parent):
@ -38,29 +40,38 @@ def xsd_simple_type_factory(elem, schema, parent):
else:
if name == XSD_ANY_SIMPLE_TYPE:
return
annotation = None
try:
child = elem[0]
except IndexError:
return schema.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
return schema.maps.types[XSD_ANY_SIMPLE_TYPE]
else:
if child.tag == XSD_ANNOTATION:
try:
child = elem[1]
annotation = XsdAnnotation(elem[0], schema, child)
except IndexError:
return schema.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
return schema.maps.types[XSD_ANY_SIMPLE_TYPE]
if child.tag == XSD_RESTRICTION:
result = schema.BUILDERS.restriction_class(child, schema, parent, name=name)
elif child.tag == XSD_LIST:
result = XsdList(child, schema, parent, name=name)
elif child.tag == XSD_UNION:
result = XsdUnion(child, schema, parent, name=name)
result = schema.BUILDERS.union_class(child, schema, parent, name=name)
else:
result = schema.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
result = schema.maps.types[XSD_ANY_SIMPLE_TYPE]
if annotation is not None:
result.annotation = annotation
if 'final' in elem.attrib:
try:
result._final = get_xsd_derivation_attribute(elem, 'final')
except ValueError as err:
result.parse_error(err, elem)
return result
@ -70,14 +81,21 @@ class XsdSimpleType(XsdType, ValidationMixin):
instances of xs:anySimpleType.
<simpleType
final = (#all | List of (list | union | restriction))
final = (#all | List of (list | union | restriction | extension))
id = ID
name = NCName
{any attributes with non-schema namespace . . .}>
Content: (annotation?, (restriction | list | union))
</simpleType>
"""
admitted_tags = {XSD_SIMPLE_TYPE}
_special_types = {XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE}
_admitted_tags = {XSD_SIMPLE_TYPE}
min_length = None
max_length = None
white_space = None
patterns = None
validators = ()
def __init__(self, elem, schema, parent, name=None, facets=None):
super(XsdSimpleType, self).__init__(elem, schema, parent, name)
@ -85,31 +103,156 @@ class XsdSimpleType(XsdType, ValidationMixin):
self.facets = facets or {}
def __setattr__(self, name, value):
super(XsdSimpleType, self).__setattr__(name, value)
if name == 'facets':
super(XsdSimpleType, self).__setattr__(name, value)
try:
self.min_length, self.max_length, self.min_value, self.max_value = self.check_facets(value)
except XMLSchemaValueError as err:
self.parse_error(unicode_type(err))
self.min_length = self.max_length = self.min_value = self.max_value = None
self.white_space = None
self.patterns = None
self.validators = []
else:
self.white_space = getattr(self.get_facet(XSD_WHITE_SPACE), 'value', None)
self.patterns = self.get_facet(XSD_PATTERN)
self.validators = [
v for k, v in value.items()
if k not in (XSD_WHITE_SPACE, XSD_PATTERN, XSD_ASSERTION) and callable(v)
]
if not isinstance(self, XsdAtomicBuiltin):
self._parse_facets(value)
white_space = getattr(self.get_facet(XSD_WHITE_SPACE), 'value', None)
if white_space is not None:
self.white_space = white_space
patterns = self.get_facet(XSD_PATTERN)
if patterns is not None:
self.patterns = patterns
if value:
if None in value:
validators = [value[None]] # Use only the validator function!
else:
validators = [v for k, v in value.items()
if k not in {XSD_WHITE_SPACE, XSD_PATTERN, XSD_ASSERTION}]
if XSD_ASSERTION in value:
assertions = value[XSD_ASSERTION]
if isinstance(assertions, list):
self.validators.extend(assertions)
validators.extend(assertions)
else:
self.validators.append(assertions)
validators.append(assertions)
if validators:
self.validators = validators
def _parse_facets(self, facets):
if facets and self.base_type is not None:
if self.base_type.is_simple():
if self.base_type.name == XSD_ANY_SIMPLE_TYPE:
self.parse_error("facets not allowed for a direct derivation of xs:anySimpleType")
elif self.base_type.has_simple_content():
if self.base_type.content_type.name == XSD_ANY_SIMPLE_TYPE:
self.parse_error("facets not allowed for a direct content derivation of xs:anySimpleType")
# Checks the applicability of the facets
if any(k not in self.admitted_facets for k in facets if k is not None):
reason = "one or more facets are not applicable, admitted set is %r:"
self.parse_error(reason % {local_name(e) for e in self.admitted_facets if e})
# Check group base_type
base_type = {t.base_type for t in facets.values() if isinstance(t, XsdFacet)}
if len(base_type) > 1:
self.parse_error("facet group must have the same base_type: %r" % base_type)
base_type = base_type.pop() if base_type else None
# Checks length based facets
length = getattr(facets.get(XSD_LENGTH), 'value', None)
min_length = getattr(facets.get(XSD_MIN_LENGTH), 'value', None)
max_length = getattr(facets.get(XSD_MAX_LENGTH), 'value', None)
if length is not None:
if length < 0:
self.parse_error("'length' value must be non negative integer")
if min_length is not None:
if min_length > length:
self.parse_error("'minLength' value must be less or equal to 'length'")
min_length_facet = base_type.get_facet(XSD_MIN_LENGTH)
length_facet = base_type.get_facet(XSD_LENGTH)
if min_length_facet is None or \
(length_facet is not None and length_facet.base_type == min_length_facet.base_type):
self.parse_error("cannot specify both 'length' and 'minLength'")
if max_length is not None:
if max_length < length:
self.parse_error("'maxLength' value must be greater or equal to 'length'")
max_length_facet = base_type.get_facet(XSD_MAX_LENGTH)
length_facet = base_type.get_facet(XSD_LENGTH)
if max_length_facet is None or \
(length_facet is not None and length_facet.base_type == max_length_facet.base_type):
self.parse_error("cannot specify both 'length' and 'maxLength'")
min_length = max_length = length
elif min_length is not None or max_length is not None:
min_length_facet = base_type.get_facet(XSD_MIN_LENGTH)
max_length_facet = base_type.get_facet(XSD_MAX_LENGTH)
if min_length is not None:
if min_length < 0:
self.parse_error("'minLength' value must be non negative integer")
if max_length is not None and max_length < min_length:
self.parse_error("'maxLength' value is lesser than 'minLength'")
if min_length_facet is not None and min_length_facet.value > min_length:
self.parse_error("'minLength' has a lesser value than parent")
if max_length_facet is not None and min_length > max_length_facet.value:
self.parse_error("'minLength' has a greater value than parent 'maxLength'")
if max_length is not None:
if max_length < 0:
self.parse_error("'maxLength' value mu st be non negative integer")
if min_length_facet is not None and min_length_facet.value > max_length:
self.parse_error("'maxLength' has a lesser value than parent 'minLength'")
if max_length_facet is not None and max_length > max_length_facet.value:
self.parse_error("'maxLength' has a greater value than parent")
# Checks min/max values
min_inclusive = getattr(facets.get(XSD_MIN_INCLUSIVE), 'value', None)
min_exclusive = getattr(facets.get(XSD_MIN_EXCLUSIVE), 'value', None)
max_inclusive = getattr(facets.get(XSD_MAX_INCLUSIVE), 'value', None)
max_exclusive = getattr(facets.get(XSD_MAX_EXCLUSIVE), 'value', None)
if min_inclusive is not None:
if min_exclusive is not None:
self.parse_error("cannot specify both 'minInclusive' and 'minExclusive")
if max_inclusive is not None and min_inclusive > max_inclusive:
self.parse_error("'minInclusive' must be less or equal to 'maxInclusive'")
elif max_exclusive is not None and min_inclusive >= max_exclusive:
self.parse_error("'minInclusive' must be lesser than 'maxExclusive'")
elif min_exclusive is not None:
if max_inclusive is not None and min_exclusive >= max_inclusive:
self.parse_error("'minExclusive' must be lesser than 'maxInclusive'")
elif max_exclusive is not None and min_exclusive > max_exclusive:
self.parse_error("'minExclusive' must be less or equal to 'maxExclusive'")
if max_inclusive is not None and max_exclusive is not None:
self.parse_error("cannot specify both 'maxInclusive' and 'maxExclusive")
# Checks fraction digits
if XSD_TOTAL_DIGITS in facets:
if XSD_FRACTION_DIGITS in facets and facets[XSD_TOTAL_DIGITS].value < facets[XSD_FRACTION_DIGITS].value:
self.parse_error("fractionDigits facet value cannot be lesser than the value of totalDigits")
total_digits = base_type.get_facet(XSD_TOTAL_DIGITS)
if total_digits is not None and total_digits.value < facets[XSD_TOTAL_DIGITS].value:
self.parse_error("totalDigits facet value cannot be greater than those on the base type")
self.min_length = min_length
self.max_length = max_length
@property
def min_value(self):
min_exclusive_facet = self.get_facet(XSD_MIN_EXCLUSIVE)
if min_exclusive_facet is None:
return getattr(self.get_facet(XSD_MIN_INCLUSIVE), 'value', None)
min_inclusive_facet = self.get_facet(XSD_MIN_INCLUSIVE)
if min_inclusive_facet is None or min_inclusive_facet.value <= min_exclusive_facet.value:
return min_exclusive_facet.value
else:
super(XsdSimpleType, self).__setattr__(name, value)
return min_inclusive_facet.value
@property
def max_value(self):
max_exclusive_facet = self.get_facet(XSD_MAX_EXCLUSIVE)
if max_exclusive_facet is None:
return getattr(self.get_facet(XSD_MAX_INCLUSIVE), 'value', None)
max_inclusive_facet = self.get_facet(XSD_MAX_INCLUSIVE)
if max_inclusive_facet is None or max_inclusive_facet.value >= max_exclusive_facet.value:
return max_exclusive_facet.value
else:
return max_inclusive_facet.value
@property
def admitted_facets(self):
@ -119,10 +262,6 @@ class XsdSimpleType(XsdType, ValidationMixin):
def built(self):
return True
@property
def final(self):
return get_xsd_derivation_attribute(self.elem, 'final', ('list', 'union', 'restriction'))
@staticmethod
def is_simple():
return True
@ -146,104 +285,27 @@ class XsdSimpleType(XsdType, ValidationMixin):
def is_element_only(self):
return False
def check_facets(self, facets):
"""
Verifies the applicability and the mutual incompatibility of a group of facets.
Raises a ValueError if the facets group is invalid.
:param facets: Dictionary with XSD facets.
:returns Min and max values, a `None` value means no min/max limit.
"""
# Checks the applicability of the facets
if any(k not in self.admitted_facets for k in facets if k is not None):
reason = "one or more facets are not applicable, admitted set is %r:"
raise XMLSchemaValueError(reason % {local_name(e) for e in self.admitted_facets if e})
# Check group base_type
base_type = {t.base_type for t in facets.values() if isinstance(t, XsdFacet)}
if len(base_type) > 1:
raise XMLSchemaValueError("facet group must have the same base_type: %r" % base_type)
base_type = base_type.pop() if base_type else None
# Checks length based facets
length = getattr(facets.get(XSD_LENGTH), 'value', None)
min_length = getattr(facets.get(XSD_MIN_LENGTH), 'value', None)
max_length = getattr(facets.get(XSD_MAX_LENGTH), 'value', None)
if length is not None:
if length < 0:
raise XMLSchemaValueError("'length' value must be non negative integer.")
if min_length is not None:
if min_length > length:
raise XMLSchemaValueError("'minLength' value must be less or equal to 'length'.")
min_length_facet = base_type.get_facet(XSD_MIN_LENGTH)
length_facet = base_type.get_facet(XSD_LENGTH)
if min_length_facet is None or \
(length_facet is not None and length_facet.base_type == min_length_facet.base_type):
raise XMLSchemaValueError("cannot specify both 'length' and 'minLength'.")
if max_length is not None:
if max_length < length:
raise XMLSchemaValueError("'maxLength' value must be greater or equal to 'length'.")
max_length_facet = base_type.get_facet(XSD_MAX_LENGTH)
length_facet = base_type.get_facet(XSD_LENGTH)
if max_length_facet is None or \
(length_facet is not None and length_facet.base_type == max_length_facet.base_type):
raise XMLSchemaValueError("cannot specify both 'length' and 'maxLength'.")
min_length = max_length = length
elif min_length is not None:
if min_length < 0:
raise XMLSchemaValueError("'minLength' value must be non negative integer.")
if max_length is not None and max_length < min_length:
raise XMLSchemaValueError("'maxLength' value is lesser than 'minLength'.")
min_length_facet = base_type.get_facet(XSD_MIN_LENGTH)
if min_length_facet is not None and min_length_facet.value > min_length:
raise XMLSchemaValueError("child 'minLength' has a lesser value than parent")
elif max_length is not None:
if max_length < 0:
raise XMLSchemaValueError("'maxLength' value must be non negative integer.")
max_length_facet = base_type.get_facet(XSD_MAX_LENGTH)
if max_length_facet is not None and max_length > max_length_facet.value:
raise XMLSchemaValueError("child 'maxLength' has a greater value than parent.")
# Checks max/min
min_inclusive = getattr(facets.get(XSD_MIN_INCLUSIVE), 'value', None)
min_exclusive = getattr(facets.get(XSD_MIN_EXCLUSIVE), 'value', None)
max_inclusive = getattr(facets.get(XSD_MAX_INCLUSIVE), 'value', None)
max_exclusive = getattr(facets.get(XSD_MAX_EXCLUSIVE), 'value', None)
if min_inclusive is not None and min_exclusive is not None:
raise XMLSchemaValueError("cannot specify both 'minInclusive' and 'minExclusive.")
if max_inclusive is not None and max_exclusive is not None:
raise XMLSchemaValueError("cannot specify both 'maxInclusive' and 'maxExclusive.")
if min_inclusive is not None:
if max_inclusive is not None and min_inclusive > max_inclusive:
raise XMLSchemaValueError("'minInclusive' must be less or equal to 'maxInclusive'.")
elif max_exclusive is not None and min_inclusive >= max_exclusive:
raise XMLSchemaValueError("'minInclusive' must be lesser than 'maxExclusive'.")
min_value = min_inclusive
elif min_exclusive is not None:
if max_inclusive is not None and min_exclusive >= max_inclusive:
raise XMLSchemaValueError("'minExclusive' must be lesser than 'maxInclusive'.")
elif max_exclusive is not None and min_exclusive > max_exclusive:
raise XMLSchemaValueError("'minExclusive' must be less or equal to 'maxExclusive'.")
min_value = min_exclusive + 1
def is_derived(self, other, derivation=None):
if self is other:
return True
elif derivation and self.derivation and derivation != self.derivation:
return False
elif other.name in self._special_types:
return True
elif self.base_type is other:
return True
elif self.base_type is None:
if hasattr(other, 'member_types'):
return any(self.is_derived(m, derivation) for m in other.member_types)
return False
elif self.base_type.is_complex():
if not self.base_type.has_simple_content():
return False
return self.base_type.content_type.is_derived(other, derivation)
elif hasattr(other, 'member_types'):
return any(self.is_derived(m, derivation) for m in other.member_types)
else:
min_value = None
if max_inclusive is not None:
max_value = max_inclusive
elif max_exclusive is not None:
max_value = max_exclusive - 1
else:
max_value = None
base_min_value = getattr(base_type, 'min_value', None)
base_max_value = getattr(base_type, 'max_value', None)
if base_min_value is not None and min_value is not None and base_min_value > min_value:
raise XMLSchemaValueError("minimum value of base_type is greater.")
if base_max_value is not None and max_value is not None and base_max_value < max_value:
raise XMLSchemaValueError("maximum value of base_type is lesser.")
return min_length, max_length, min_value, max_value
return self.base_type.is_derived(other, derivation)
def normalize(self, text):
"""
@ -316,7 +378,8 @@ class XsdAtomic(XsdSimpleType):
a base_type attribute that refers to primitive or derived atomic
built-in type or another derived simpleType.
"""
admitted_tags = {XSD_RESTRICTION, XSD_SIMPLE_TYPE}
_special_types = {XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE}
_admitted_tags = {XSD_RESTRICTION, XSD_SIMPLE_TYPE}
def __init__(self, elem, schema, parent, name=None, facets=None, base_type=None):
self.base_type = base_type
@ -367,12 +430,14 @@ class XsdAtomic(XsdSimpleType):
def primitive_type(self):
if self.base_type is None:
return self
else:
try:
try:
if self.base_type.is_simple():
return self.base_type.primitive_type
except AttributeError:
# The base_type is XsdList or XsdUnion.
return self.base_type
else:
return self.base_type.content_type.primitive_type
except AttributeError:
# The base_type is XsdList or XsdUnion.
return self.base_type
def get_facet(self, tag):
try:
@ -448,6 +513,10 @@ class XsdAtomicBuiltin(XsdAtomic):
yield self.decode_error(validation, obj, self.to_python,
reason="value is not an instance of {!r}".format(self.instance_types))
if 'id_map' in kwargs:
if self.name == XSD_ID:
kwargs['id_map'][obj] += 1
if validation == 'skip':
try:
yield self.to_python(obj)
@ -540,12 +609,12 @@ class XsdList(XsdSimpleType):
Content: (annotation?, simpleType?)
</list>
"""
admitted_tags = {XSD_LIST}
_admitted_tags = {XSD_LIST}
_white_space_elem = etree_element(XSD_WHITE_SPACE, attrib={'value': 'collapse', 'fixed': 'true'})
def __init__(self, elem, schema, parent, name=None, facets=None, base_type=None):
def __init__(self, elem, schema, parent, name=None):
facets = {XSD_WHITE_SPACE: XsdWhiteSpaceFacet(self._white_space_elem, schema, self, self)}
super(XsdList, self).__init__(elem, schema, parent, name, facets)
if not hasattr(self, 'base_type'):
self.base_type = base_type
def __repr__(self):
if self.name is None:
@ -579,28 +648,36 @@ class XsdList(XsdSimpleType):
base_type = xsd_simple_type_factory(child, self.schema, self)
except XMLSchemaParseError as err:
self.parse_error(err, elem)
base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
if 'itemType' in elem.attrib:
self.parse_error("ambiguous list type declaration", self)
elif 'itemType' in elem.attrib:
# List tag with itemType attribute that refers to a global type
item_qname = prefixed_to_qname(elem.attrib['itemType'], self.namespaces)
try:
base_type = self.maps.lookup_type(item_qname)
except LookupError:
self.parse_error("unknown itemType %r" % elem.attrib['itemType'], elem)
base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
else:
self.parse_error("missing list type declaration", elem)
base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
# List tag with itemType attribute that refers to a global type
try:
item_qname = self.schema.resolve_qname(elem.attrib['itemType'])
except KeyError:
self.parse_error("missing list type declaration", elem)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
except ValueError as err:
self.parse_error(err, elem)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
else:
try:
base_type = self.maps.lookup_type(item_qname)
except LookupError:
self.parse_error("unknown itemType %r" % elem.attrib['itemType'], elem)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
if base_type.final == '#all' or 'list' in base_type.final:
self.parse_error("'final' value of the itemType %r forbids derivation by list" % base_type)
try:
self.base_type = base_type
except XMLSchemaValueError as err:
self.parse_error(str(err), elem)
self.base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
self.base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
@property
def admitted_facets(self):
@ -629,6 +706,18 @@ class XsdList(XsdSimpleType):
def is_list():
return True
def is_derived(self, other, derivation=None):
if self is other:
return True
elif derivation and self.derivation and derivation != self.derivation:
return False
elif other.name in self._special_types:
return True
elif self.base_type is other:
return True
else:
return False
def iter_components(self, xsd_classes=None):
if xsd_classes is None or isinstance(self, xsd_classes):
yield self
@ -697,12 +786,13 @@ class XsdUnion(XsdSimpleType):
Content: (annotation?, simpleType*)
</union>
"""
admitted_tags = {XSD_UNION}
_admitted_types = XsdSimpleType
_admitted_tags = {XSD_UNION}
def __init__(self, elem, schema, parent, name=None, facets=None, member_types=None):
super(XsdUnion, self).__init__(elem, schema, parent, name, facets)
if not hasattr(self, 'member_types'):
self.member_types = member_types
member_types = None
def __init__(self, elem, schema, parent, name=None):
super(XsdUnion, self).__init__(elem, schema, parent, name, facets=None)
def __repr__(self):
if self.name is None:
@ -719,13 +809,6 @@ class XsdUnion(XsdSimpleType):
return
raise XMLSchemaValueError("a %r definition required for %r." % (XSD_UNION, self))
elif name == "member_types":
if not value:
raise XMLSchemaValueError("%r attribute cannot be empty or None." % name)
elif not all(isinstance(mt, (XsdAtomic, XsdList, XsdUnion)) for mt in value):
raise XMLSchemaValueError("%r: member types must be all atomic or list types." % self)
# FIXME: Union only for XSD 1.1
elif name == 'white_space':
if not (value is None or value == 'collapse'):
raise XMLSchemaValueError("Wrong value % for attribute 'white_space'." % value)
@ -746,29 +829,37 @@ class XsdUnion(XsdSimpleType):
if 'memberTypes' in elem.attrib:
for name in elem.attrib['memberTypes'].split():
type_qname = prefixed_to_qname(name, self.namespaces)
try:
type_qname = self.schema.resolve_qname(name)
except ValueError as err:
self.parse_error(err)
continue
try:
mt = self.maps.lookup_type(type_qname)
except LookupError:
self.parse_error("unknown member type %r" % type_qname)
mt = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
mt = self.maps.types[XSD_ANY_ATOMIC_TYPE]
except XMLSchemaParseError as err:
self.parse_error(err)
mt = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
mt = self.maps.types[XSD_ANY_ATOMIC_TYPE]
if not isinstance(mt, XsdSimpleType):
self.parse_error("a simpleType required", mt)
else:
member_types.append(mt)
if isinstance(mt, tuple):
self.parse_error("circular definition found on xs:union type {!r}".format(self.name))
continue
elif not isinstance(mt, self._admitted_types):
self.parse_error("a {!r} required, not {!r}".format(self._admitted_types, mt))
continue
elif mt.final == '#all' or 'union' in mt.final:
self.parse_error("'final' value of the memberTypes %r forbids derivation by union" % member_types)
if not member_types:
self.parse_error("missing union type declarations", elem)
member_types.append(mt)
try:
if member_types:
self.member_types = member_types
except XMLSchemaValueError as err:
self.parse_error(str(err), elem)
self.member_types = [self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)]
else:
self.parse_error("missing xs:union type declarations", elem)
self.member_types = [self.maps.types[XSD_ANY_ATOMIC_TYPE]]
@property
def admitted_facets(self):
@ -917,6 +1008,11 @@ class XsdUnion(XsdSimpleType):
yield unicode_type(obj)
class Xsd11Union(XsdUnion):
_admitted_types = XsdAtomic, XsdList, XsdUnion
class XsdAtomicRestriction(XsdAtomic):
"""
Class for XSD 1.0 atomic simpleType and complexType's simpleContent restrictions.
@ -931,14 +1027,13 @@ class XsdAtomicRestriction(XsdAtomic):
</restriction>
"""
FACETS_BUILDERS = XSD_10_FACETS_BUILDERS
derivation = 'restriction'
def __setattr__(self, name, value):
if name == 'elem' and value is not None:
if self.name != XSD_ANY_ATOMIC_TYPE and value.tag != XSD_RESTRICTION:
if not (value.tag == XSD_SIMPLE_TYPE and value.get('name') is not None):
raise XMLSchemaValueError(
"a %r definition required for %r." % (XSD_RESTRICTION, self)
)
raise XMLSchemaValueError("an xs:restriction definition required for %r." % self)
super(XsdAtomicRestriction, self).__setattr__(name, value)
def _parse(self):
@ -949,30 +1044,57 @@ class XsdAtomicRestriction(XsdAtomic):
elif elem.tag == XSD_SIMPLE_TYPE and elem.get('name') is not None:
elem = self._parse_component(elem) # Global simpleType with internal restriction
if self.name is not None and self.parent is not None:
self.parse_error("'name' attribute in a local simpleType definition", elem)
base_type = None
facets = {}
has_attributes = False
has_simple_type_child = False
if 'base' in elem.attrib:
base_qname = prefixed_to_qname(elem.attrib['base'], self.namespaces)
try:
base_type = self.maps.lookup_type(base_qname)
except LookupError:
self.parse_error("unknown type %r." % elem.attrib['base'])
base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
except XMLSchemaParseError as err:
self.parse_error(err)
base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)
base_qname = self.schema.resolve_qname(elem.attrib['base'])
except ValueError as err:
self.parse_error(err, elem)
base_type = self.maps.type[XSD_ANY_ATOMIC_TYPE]
else:
if base_qname == self.name:
if self.redefine is None:
self.parse_error("wrong definition with self-reference", elem)
base_type = self.maps.type[XSD_ANY_ATOMIC_TYPE]
else:
base_type = self.base_type
else:
if self.redefine is not None:
self.parse_error("wrong redefinition without self-reference", elem)
if base_type.is_complex() and base_type.mixed and base_type.is_emptiable():
if self._parse_component(elem, strict=False).tag != XSD_SIMPLE_TYPE:
# See: "http://www.w3.org/TR/xmlschema-2/#element-restriction"
self.parse_error(
"when a complexType with simpleContent restricts a complexType "
"with mixed and with emptiable content then a simpleType child "
"declaration is required.", elem
)
try:
base_type = self.maps.lookup_type(base_qname)
except LookupError:
self.parse_error("unknown type %r." % elem.attrib['base'])
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
except XMLSchemaParseError as err:
self.parse_error(err)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
else:
if isinstance(base_type, tuple):
self.parse_error("circularity definition between %r and %r" % (self, base_qname), elem)
base_type = self.maps.types[XSD_ANY_ATOMIC_TYPE]
if base_type.is_simple() and base_type.name == XSD_ANY_SIMPLE_TYPE:
self.parse_error("wrong base type {!r}, an atomic type required")
elif base_type.is_complex():
if base_type.mixed and base_type.is_emptiable():
if self._parse_component(elem, strict=False).tag != XSD_SIMPLE_TYPE:
# See: "http://www.w3.org/TR/xmlschema-2/#element-restriction"
self.parse_error(
"when a complexType with simpleContent restricts a complexType "
"with mixed and with emptiable content then a simpleType child "
"declaration is required.", elem
)
elif self.parent is None or self.parent.is_simple():
self.parse_error("simpleType restriction of %r is not allowed" % base_type, elem)
for child in self._iterparse_components(elem):
if child.tag in {XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ANY_ATTRIBUTE}:
@ -983,22 +1105,28 @@ class XsdAtomicRestriction(XsdAtomic):
# Case of simpleType declaration inside a restriction
if has_simple_type_child:
self.parse_error("duplicated simpleType declaration", child)
elif base_type is None:
if base_type is None:
try:
base_type = xsd_simple_type_factory(child, self.schema, self)
except XMLSchemaParseError as err:
self.parse_error(err)
base_type = self.maps.lookup_type(XSD_ANY_SIMPLE_TYPE)
base_type = self.maps.types[XSD_ANY_SIMPLE_TYPE]
elif base_type.is_complex():
if base_type.admit_simple_restriction():
base_type = self.schema.BUILDERS.complex_type_class(
elem=elem,
schema=self.schema,
parent=self,
content_type=xsd_simple_type_factory(child, self.schema, self),
attributes=base_type.attributes,
mixed=base_type.mixed,
block=base_type.block,
final=base_type.final,
)
elif 'base' in elem.attrib:
self.parse_error("restriction with 'base' attribute and simpleType declaration", child)
elif base_type.is_complex() and base_type.admit_simple_restriction():
base_type = self.schema.BUILDERS.complex_type_class(
elem=elem,
schema=self.schema,
parent=self,
content_type=xsd_simple_type_factory(child, self.schema, self),
attributes=base_type.attributes,
mixed=base_type.mixed
)
has_simple_type_child = True
else:
try:
@ -1022,6 +1150,9 @@ class XsdAtomicRestriction(XsdAtomic):
if base_type is None:
self.parse_error("missing base type in restriction:", self)
elif base_type.final == '#all' or 'restriction' in base_type.final:
self.parse_error("'final' value of the baseType %r forbids derivation by restriction" % base_type)
self.base_type = base_type
self.facets = facets

View File

@ -14,7 +14,7 @@ This module contains classes for XML Schema wildcards.
from __future__ import unicode_literals
from ..exceptions import XMLSchemaValueError
from ..qnames import XSD_ANY, XSD_ANY_ATTRIBUTE
from ..qnames import XSD_ANY, XSD_ANY_ATTRIBUTE, XSD_OPEN_CONTENT, XSD_DEFAULT_OPEN_CONTENT
from ..helpers import get_namespace
from ..namespaces import XSI_NAMESPACE
from ..xpath import ElementPathMixin
@ -42,16 +42,16 @@ class XsdWildcard(XsdComponent, ValidationMixin):
# Parse namespace and processContents
namespace = self.elem.get('namespace', '##any')
items = namespace.strip().split()
if len(items) == 1 and items[0] in ('##any', '##all', '##other', '##local', '##targetNamespace'):
if len(items) == 1 and items[0] in ('##any', '##other', '##local', '##targetNamespace'):
self.namespace = namespace.strip()
elif not all([s not in ('##any', '##other') for s in items]):
elif not all(not s.startswith('##') or s in {'##local', '##targetNamespace'} for s in items):
self.parse_error("wrong value %r for 'namespace' attribute." % namespace)
self.namespace = '##any'
else:
self.namespace = namespace.strip()
self.process_contents = self.elem.get('processContents', 'strict')
if self.process_contents not in ('lax', 'skip', 'strict'):
if self.process_contents not in {'lax', 'skip', 'strict'}:
self.parse_error("wrong value %r for 'processContents' attribute." % self.process_contents)
def _load_namespace(self, namespace):
@ -77,6 +77,17 @@ class XsdWildcard(XsdComponent, ValidationMixin):
def built(self):
return True
def iter_namespaces(self):
if self.namespace in ('##any', '##other'):
return
for ns in self.namespace.split():
if ns == '##local':
yield ''
elif ns == '##targetNamespace':
yield self.target_namespace
else:
yield ns
def is_matching(self, name, default_namespace=None):
if name is None:
return False
@ -104,6 +115,34 @@ class XsdWildcard(XsdComponent, ValidationMixin):
else:
return namespace in any_namespaces
def is_restriction(self, other, check_occurs=True):
if check_occurs and isinstance(self, ParticleMixin) and not self.has_occurs_restriction(other):
return False
elif not isinstance(other, type(self)):
return False
elif other.process_contents == 'strict' and self.process_contents != 'strict':
return False
elif other.process_contents == 'lax' and self.process_contents == 'skip':
return False
elif self.namespace == other.namespace:
return True
elif other.namespace == '##any':
return True
elif self.namespace == '##any':
return False
other_namespaces = other.namespace.split()
for ns in self.namespace.split():
if ns in other_namespaces:
continue
elif ns == self.target_namespace:
if '##targetNamespace' in other_namespaces:
continue
elif not ns.startswith('##') and '##other' in other_namespaces:
continue
return False
return True
def iter_decode(self, source, validation='lax', *args, **kwargs):
raise NotImplementedError
@ -125,7 +164,7 @@ class XsdAnyElement(XsdWildcard, ParticleMixin, ElementPathMixin):
Content: (annotation?)
</any>
"""
admitted_tags = {XSD_ANY}
_admitted_tags = {XSD_ANY}
def __repr__(self):
return '%s(namespace=%r, process_contents=%r, occurs=%r)' % (
@ -158,7 +197,8 @@ class XsdAnyElement(XsdWildcard, ParticleMixin, ElementPathMixin):
def iterchildren(self, tag=None):
return iter(())
def iter_substitutes(self, tag=None):
@staticmethod
def iter_substitutes():
return iter(())
def iter_decode(self, elem, validation='lax', converter=None, **kwargs):
@ -202,10 +242,20 @@ class XsdAnyElement(XsdWildcard, ParticleMixin, ElementPathMixin):
reason = "element %r not allowed here." % name
yield self.validation_error(validation, reason, value, **kwargs)
def is_restriction(self, other):
if not ParticleMixin.is_restriction(self, other):
return False
return True
def overlap(self, other):
if not isinstance(other, XsdAnyElement):
return other.overlap(self)
elif self.namespace == other.namespace:
return True
elif self.namespace == '##any' or other.namespace == '##any':
return True
elif self.namespace == '##other':
return any(not ns.startswith('##') and ns != self.target_namespace for ns in other.namespace.split())
elif other.namespace == '##other':
return any(not ns.startswith('##') and ns != other.target_namespace for ns in self.namespace.split())
any_namespaces = self.namespace.split()
return any(ns in any_namespaces for ns in other.namespace.split())
class XsdAnyAttribute(XsdWildcard):
@ -220,7 +270,33 @@ class XsdAnyAttribute(XsdWildcard):
Content: (annotation?)
</anyAttribute>
"""
admitted_tags = {XSD_ANY_ATTRIBUTE}
_admitted_tags = {XSD_ANY_ATTRIBUTE}
def extend_namespace(self, other):
if self.namespace == '##any' or self.namespace == other.namespace:
return
elif other.namespace == '##any':
self.namespace = other.namespace
return
elif other.namespace == '##other':
w1, w2 = other, self
elif self.namespace == '##other':
w1, w2 = self, other
elif self.target_namespace == other.target_namespace:
self.namespace = ' '.join(set(other.namespace.split() + self.namespace.split()))
return
else:
self.namespace = ' '.join(set(list(other.iter_namespaces()) + self.namespace.split()))
return
namespaces = set(w2.iter_namespaces())
if w1.target_namespace in namespaces and '' in namespaces:
self.namespace = '##any'
elif '' not in namespaces and w1.target_namespace == w2.target_namespace:
self.namespace = '##other'
else:
msg = "not expressible wildcard namespace union: {!r} V {!r}:"
raise XMLSchemaValueError(msg.format(other.namespace, self.namespace))
def match(self, name, default_namespace=None):
if self.is_matching(name, default_namespace):
@ -370,9 +446,26 @@ class XsdOpenContent(XsdComponent):
Content: (annotation?), (any?)
</openContent>
"""
_admitted_tags = {XSD_OPEN_CONTENT, XSD_DEFAULT_OPEN_CONTENT}
def __init__(self, elem, schema, parent):
super(XsdOpenContent, self).__init__(elem, schema, parent)
self.mode = self.elem.get('mode', 'interleave')
if self.mode not in ('none', 'interleave', 'suffix'):
self.parse_error("wrong value %r for 'mode' attribute." % self.mode)
def _parse(self):
super(XsdOpenContent, self)._parse()
child = self._parse_component(self.elem)
if child is None:
if self.elem.tag == XSD_DEFAULT_OPEN_CONTENT:
self.parse_error("a %r declaration cannot be empty:" % self.elem.tag, self.elem)
self.any_element = None
elif child.tag == XSD_ANY:
self.any_element = Xsd11AnyElement(child, self.schema, self)
else:
self.any_element = None
if self.schema.validation == 'skip':
# Also generated by meta-schema validation for 'lax' and 'strict' modes
self.parse_error("unexpected tag %r for openContent child:" % child.tag, self.elem)

View File

@ -16,14 +16,10 @@ import re
from ..compat import PY3, string_base_type, unicode_type
from ..exceptions import XMLSchemaValueError, XMLSchemaTypeError
from ..qnames import XSD_ANNOTATION, XSD_APPINFO, XSD_DOCUMENTATION, XML_LANG, XSD_ANY_TYPE
from ..qnames import XSD_ANNOTATION, XSD_APPINFO, XSD_DOCUMENTATION, XML_LANG, XSD_ANY_TYPE, XSD_ID
from ..helpers import get_qname, local_name, qname_to_prefixed, iter_xsd_components, get_xsd_component
from ..etree import etree_tostring, is_etree_element
from .exceptions import (
XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaDecodeError,
XMLSchemaEncodeError, XMLSchemaChildrenValidationError
)
from .exceptions import XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaDecodeError, XMLSchemaEncodeError
XSD_VALIDATION_MODES = {'strict', 'lax', 'skip'}
"""
@ -73,7 +69,7 @@ class XsdValidator(object):
@property
def validation_attempted(self):
"""
Property that returns the XSD validator's validation status. It can be 'full', 'partial' or 'none'.
Property that returns the *validation status* of the XSD validator. It can be 'full', 'partial' or 'none'.
| https://www.w3.org/TR/xmlschema-1/#e-validation_attempted
| https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#e-validation_attempted
@ -115,6 +111,14 @@ class XsdValidator(object):
errors.extend(comp.errors)
return errors
def copy(self):
validator = object.__new__(self.__class__)
validator.__dict__.update(self.__dict__)
validator.errors = self.errors[:]
return validator
__copy__ = copy
def parse_error(self, error, elem=None):
"""
Helper method for registering parse errors. Does nothing if validation mode is 'skip'.
@ -140,7 +144,7 @@ class XsdValidator(object):
error.elem = elem
error.source = getattr(self, 'source', None)
elif isinstance(error, Exception):
error = XMLSchemaParseError(self, unicode_type(error), elem)
error = XMLSchemaParseError(self, unicode_type(error).strip('\'" '), elem)
elif isinstance(error, string_base_type):
error = XMLSchemaParseError(self, error, elem)
else:
@ -187,26 +191,25 @@ class XsdComponent(XsdValidator):
:param parent: the XSD parent, `None` means that is a global component that has the schema as parent.
:param name: name of the component, maybe overwritten by the parse of the `elem` argument.
:cvar admitted_tags: the set of admitted element tags for component type.
:vartype admitted_tags: tuple or set
:cvar qualified: for name matching, unqualified matching may be admitted only for elements and attributes..
:cvar qualified: for name matching, unqualified matching may be admitted only for elements and attributes.
:vartype qualified: bool
"""
_REGEX_SPACE = re.compile(r'\s')
_REGEX_SPACES = re.compile(r'\s+')
_admitted_tags = ()
admitted_tags = ()
parent = None
name = None
qualified = True
def __init__(self, elem, schema, parent, name=None):
def __init__(self, elem, schema, parent=None, name=None):
super(XsdComponent, self).__init__(schema.validation)
if name == '':
raise XMLSchemaValueError("'name' cannot be an empty string!")
assert name is None or name[0] == '{' or not schema.target_namespace, \
"name=%r argument: can be None or a qualified name of the target namespace." % name
self.name = name
self.parent = parent
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
if parent is not None:
self.parent = parent
self.schema = schema
self.elem = elem
@ -214,15 +217,13 @@ class XsdComponent(XsdValidator):
if name == "elem":
if not is_etree_element(value):
raise XMLSchemaTypeError("%r attribute must be an Etree Element: %r" % (name, value))
elif value.tag not in self.admitted_tags:
elif value.tag not in self._admitted_tags:
raise XMLSchemaValueError(
"wrong XSD element %r for %r, must be one of %r." % (
local_name(value.tag), self,
[local_name(tag) for tag in self.admitted_tags]
[local_name(tag) for tag in self._admitted_tags]
)
)
elif hasattr(self, 'elem'):
self._elem = self.elem # redefinition cases
super(XsdComponent, self).__setattr__(name, value)
self._parse()
return
@ -298,6 +299,13 @@ class XsdComponent(XsdValidator):
except XMLSchemaValueError as err:
self.parse_error(err, elem)
def _parse_attribute(self, elem, name, values, default=None):
value = elem.get(name, default)
if value not in values:
self.parse_error("wrong value {} for {} attribute.".format(value, name))
return default
return value
def _parse_properties(self, *properties):
for name in properties:
try:
@ -376,6 +384,16 @@ class XsdComponent(XsdValidator):
"""Returns the component if its name is matching the name provided as argument, `None` otherwise."""
return self if self.is_matching(name, default_namespace) else None
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
def iter_components(self, xsd_classes=None):
"""
Creates an iterator for XSD subcomponents.
@ -436,7 +454,7 @@ class XsdAnnotation(XsdComponent):
Content: ({any})*
</documentation>
"""
admitted_tags = {XSD_ANNOTATION}
_admitted_tags = {XSD_ANNOTATION}
@property
def built(self):
@ -460,9 +478,15 @@ class XsdAnnotation(XsdComponent):
class XsdType(XsdComponent):
abstract = False
base_type = None
derivation = None
redefine = None
_final = None
@property
def final(self):
return self.schema.final_default if self._final is None else self._final
@property
def built(self):
@ -509,20 +533,10 @@ class XsdType(XsdComponent):
return 'unknown'
def is_derived(self, other, derivation=None):
if other.name == XSD_ANY_TYPE or self.base_type == other:
return True if derivation is None else derivation == self.derivation
elif self.base_type is not None:
return self.base_type.is_derived(other, derivation)
else:
return False
raise NotImplementedError
def is_subtype(self, qname):
if qname == XSD_ANY_TYPE or self.name == qname:
return True
elif self.base_type is not None:
return self.base_type.is_subtype(qname)
else:
return False
def is_key(self):
return self.name == XSD_ID or self.is_derived(self.maps.types[XSD_ID])
class ValidationMixin(object):
@ -761,54 +775,27 @@ class ParticleMixin(object):
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
"""
def parse_error(self, *args, **kwargs):
# Implemented by XsdValidator
raise NotImplementedError
min_occurs = 1
max_occurs = 1
def _parse_particle(self, elem):
try:
self.min_occurs = int(elem.attrib['minOccurs'])
if self.min_occurs < 0:
raise ValueError()
except KeyError:
self.min_occurs = 1
except (TypeError, ValueError):
self.parse_error("minOccurs value must be a non negative integer")
self.min_occurs = 1
if 'maxOccurs' not in elem.attrib:
self.max_occurs = 1
elif elem.attrib['maxOccurs'] == 'unbounded':
self.max_occurs = None
else:
try:
self.max_occurs = int(elem.attrib['maxOccurs'])
except ValueError:
self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'")
self.max_occurs = 1
else:
if self.min_occurs > self.max_occurs:
self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs:")
self.occurs = [self.min_occurs, self.max_occurs]
@property
def occurs(self):
return [self.min_occurs, self.max_occurs]
def is_emptiable(self):
return self.min_occurs == 0
is_optional = is_emptiable
def is_empty(self):
return self.max_occurs == 0
def is_single(self):
return self.max_occurs == 1
def is_restriction(self, other):
if self.min_occurs < other.min_occurs:
return False
if other.max_occurs is not None:
if self.max_occurs is None:
return False
elif self.max_occurs > other.max_occurs:
return False
return True
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
@ -816,27 +803,49 @@ class ParticleMixin(object):
def is_over(self, occurs):
return self.max_occurs is not None and self.max_occurs <= occurs
def children_validation_error(self, validation, elem, index, particle, occurs=0, expected=None,
source=None, namespaces=None, **_kwargs):
"""
Helper method for generating model validation errors. Incompatible with 'skip' validation mode.
Il validation mode is 'lax' returns the error, otherwise raise the error.
:param validation: the validation mode. Can be 'lax' or 'strict'.
:param elem: the instance Element.
:param index: the child index.
:param particle: the XSD component (subgroup or element) associated to the child.
:param occurs: the child tag occurs.
:param expected: the expected element tags/object names.
:param source: the XML resource related to the validation process.
:param namespaces: is an optional mapping from namespace prefix to URI.
:param _kwargs: keyword arguments of the validation process that are not used.
"""
if validation == 'skip':
raise XMLSchemaValueError("validation mode 'skip' incompatible with error generation.")
error = XMLSchemaChildrenValidationError(self, elem, index, particle, occurs, expected, source, namespaces)
if validation == 'strict':
raise error
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 error
return self.max_occurs <= other.max_occurs
###
# Methods used by XSD components
def parse_error(self, *args, **kwargs):
"""Helper method overridden by XsdValidator.parse_error() in XSD components."""
raise XMLSchemaParseError(*args)
def _parse_particle(self, elem):
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':
self.max_occurs = None
else:
try:
max_occurs = int(max_occurs)
except ValueError:
self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'")
else:
if self.min_occurs > max_occurs:
self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs")
else:
self.max_occurs = max_occurs