1264 lines
60 KiB
Python
1264 lines
60 KiB
Python
#!/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 validation/decoding/encoding of XML files.
|
|
"""
|
|
import unittest
|
|
import pdb
|
|
import os
|
|
import sys
|
|
import pickle
|
|
from decimal import Decimal
|
|
import base64
|
|
import warnings
|
|
from elementpath import datatypes
|
|
|
|
import xmlschema
|
|
from xmlschema import (
|
|
XMLSchemaEncodeError, XMLSchemaValidationError, ParkerConverter,
|
|
BadgerFishConverter, AbderaConverter, JsonMLConverter
|
|
)
|
|
from xmlschema.compat import unicode_type, ordered_dict_class
|
|
from xmlschema.etree import etree_element, etree_tostring, is_etree_element, ElementTree, \
|
|
etree_elements_assert_equal, lxml_etree, lxml_etree_element
|
|
from xmlschema.helpers import local_name
|
|
from xmlschema.qnames import XSI_TYPE
|
|
from xmlschema.resources import fetch_namespaces
|
|
from xmlschema.tests import XMLSchemaTestCase, tests_factory
|
|
from xmlschema.validators import XMLSchema11
|
|
|
|
_VEHICLES_DICT = {
|
|
'@xmlns:vh': 'http://example.com/vehicles',
|
|
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd',
|
|
'vh:cars': {
|
|
'vh:car': [
|
|
{'@make': 'Porsche', '@model': '911'},
|
|
{'@make': 'Porsche', '@model': '911'}
|
|
]},
|
|
'vh:bikes': {
|
|
'vh:bike': [
|
|
{'@make': 'Harley-Davidson', '@model': 'WL'},
|
|
{'@make': 'Yamaha', '@model': 'XS650'}
|
|
]}
|
|
}
|
|
|
|
_VEHICLES_DICT_ALT = [
|
|
{'vh:cars': [
|
|
{'vh:car': None, '@make': 'Porsche', '@model': '911'},
|
|
{'vh:car': None, '@make': 'Porsche', '@model': '911'}
|
|
]},
|
|
{'vh:bikes': [
|
|
{'vh:bike': None, '@make': 'Harley-Davidson', '@model': 'WL'},
|
|
{'vh:bike': None, '@make': 'Yamaha', '@model': 'XS650'}
|
|
]},
|
|
{'@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd'}
|
|
]
|
|
|
|
_COLLECTION_DICT = {
|
|
'@xmlns:col': 'http://example.com/ns/collection',
|
|
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
|
|
'object': [{
|
|
'@available': True,
|
|
'@id': 'b0836217462',
|
|
'author': {
|
|
'@id': 'PAR',
|
|
'born': '1841-02-25',
|
|
'dead': '1919-12-03',
|
|
'name': 'Pierre-Auguste Renoir',
|
|
'qualification': 'painter'
|
|
},
|
|
'estimation': Decimal('10000.00'),
|
|
'position': 1,
|
|
'title': 'The Umbrellas',
|
|
'year': '1886'},
|
|
{
|
|
'@available': True,
|
|
'@id': 'b0836217463',
|
|
'author': {
|
|
'@id': 'JM',
|
|
'born': '1893-04-20',
|
|
'dead': '1983-12-25',
|
|
'name': u'Joan Miró',
|
|
'qualification': 'painter, sculptor and ceramicist'
|
|
},
|
|
'position': 2,
|
|
'title': None,
|
|
'year': '1925'
|
|
}]
|
|
}
|
|
|
|
_COLLECTION_PARKER = {
|
|
'object': [{'author': {'born': '1841-02-25',
|
|
'dead': '1919-12-03',
|
|
'name': 'Pierre-Auguste Renoir',
|
|
'qualification': 'painter'},
|
|
'estimation': 10000.0,
|
|
'position': 1,
|
|
'title': 'The Umbrellas',
|
|
'year': '1886'},
|
|
{'author': {'born': '1893-04-20',
|
|
'dead': '1983-12-25',
|
|
'name': u'Joan Miró',
|
|
'qualification': 'painter, sculptor and ceramicist'},
|
|
'position': 2,
|
|
'title': None,
|
|
'year': '1925'}]}
|
|
|
|
_COLLECTION_PARKER_ROOT = {
|
|
'col:collection': {'object': [{'author': {'born': '1841-02-25',
|
|
'dead': '1919-12-03',
|
|
'name': 'Pierre-Auguste Renoir',
|
|
'qualification': 'painter'},
|
|
'estimation': 10000.0,
|
|
'position': 1,
|
|
'title': 'The Umbrellas',
|
|
'year': '1886'},
|
|
{'author': {'born': '1893-04-20',
|
|
'dead': '1983-12-25',
|
|
'name': u'Joan Miró',
|
|
'qualification': 'painter, sculptor and ceramicist'},
|
|
'position': 2,
|
|
'title': None,
|
|
'year': '1925'}]}}
|
|
|
|
_COLLECTION_BADGERFISH = {
|
|
'@xmlns': {
|
|
'col': 'http://example.com/ns/collection',
|
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'},
|
|
'col:collection': {
|
|
'@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
|
|
'object': [{
|
|
'@available': True,
|
|
'@id': 'b0836217462',
|
|
'author': {
|
|
'@id': 'PAR',
|
|
'born': {'$': '1841-02-25'},
|
|
'dead': {'$': '1919-12-03'},
|
|
'name': {'$': 'Pierre-Auguste Renoir'},
|
|
'qualification': {'$': 'painter'}},
|
|
'estimation': {'$': 10000.0},
|
|
'position': {'$': 1},
|
|
'title': {'$': 'The Umbrellas'},
|
|
'year': {'$': '1886'}},
|
|
{
|
|
'@available': True,
|
|
'@id': 'b0836217463',
|
|
'author': {
|
|
'@id': 'JM',
|
|
'born': {'$': '1893-04-20'},
|
|
'dead': {'$': '1983-12-25'},
|
|
'name': {'$': u'Joan Miró'},
|
|
'qualification': {
|
|
'$': 'painter, sculptor and ceramicist'}
|
|
},
|
|
'position': {'$': 2},
|
|
'title': {},
|
|
'year': {'$': '1925'}
|
|
}]
|
|
}
|
|
}
|
|
|
|
_COLLECTION_ABDERA = {
|
|
'attributes': {
|
|
'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd'
|
|
},
|
|
'children': [
|
|
{
|
|
'object': [
|
|
{
|
|
'attributes': {'available': True, 'id': 'b0836217462'},
|
|
'children': [{
|
|
'author': {
|
|
'attributes': {'id': 'PAR'},
|
|
'children': [{
|
|
'born': '1841-02-25',
|
|
'dead': '1919-12-03',
|
|
'name': 'Pierre-Auguste Renoir',
|
|
'qualification': 'painter'}
|
|
]},
|
|
'estimation': 10000.0,
|
|
'position': 1,
|
|
'title': 'The Umbrellas',
|
|
'year': '1886'}
|
|
]},
|
|
{
|
|
'attributes': {'available': True, 'id': 'b0836217463'},
|
|
'children': [{
|
|
'author': {
|
|
'attributes': {'id': 'JM'},
|
|
'children': [{
|
|
'born': '1893-04-20',
|
|
'dead': '1983-12-25',
|
|
'name': u'Joan Miró',
|
|
'qualification': 'painter, sculptor and ceramicist'}
|
|
]},
|
|
'position': 2,
|
|
'title': [],
|
|
'year': '1925'
|
|
}]
|
|
}]
|
|
}
|
|
]}
|
|
|
|
_COLLECTION_JSON_ML = [
|
|
'col:collection',
|
|
{'xmlns:col': 'http://example.com/ns/collection',
|
|
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd'},
|
|
['object',
|
|
{'available': True, 'id': 'b0836217462'},
|
|
['position', 1],
|
|
['title', 'The Umbrellas'],
|
|
['year', '1886'],
|
|
[
|
|
'author',
|
|
{'id': 'PAR'},
|
|
['name', 'Pierre-Auguste Renoir'],
|
|
['born', '1841-02-25'],
|
|
['dead', '1919-12-03'],
|
|
['qualification', 'painter']
|
|
],
|
|
[
|
|
'estimation',
|
|
Decimal('10000.00')
|
|
]],
|
|
['object',
|
|
{'available': True, 'id': 'b0836217463'},
|
|
['position', 2],
|
|
['title'],
|
|
['year', '1925'],
|
|
[
|
|
'author',
|
|
{'id': 'JM'},
|
|
['name', u'Joan Miró'],
|
|
['born', '1893-04-20'],
|
|
['dead', '1983-12-25'],
|
|
['qualification', 'painter, sculptor and ceramicist']
|
|
]]
|
|
]
|
|
|
|
_DATA_DICT = {
|
|
'@xmlns:ns': 'ns',
|
|
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'@xsi:schemaLocation': 'ns ./simple-types.xsd',
|
|
'certification': [
|
|
{'$': 'ISO-9001', '@Year': 1999},
|
|
{'$': 'ISO-27001', '@Year': 2009}
|
|
],
|
|
'decimal_value': [Decimal('1')],
|
|
u'menù': u'baccalà mantecato',
|
|
u'complex_boolean': [
|
|
{'$': True, '@Type': 2}, {'$': False, '@Type': 1}, True, False
|
|
],
|
|
u'simple_boolean': [True, False]
|
|
}
|
|
|
|
|
|
def iter_nested_items(items, dict_class=dict, list_class=list):
|
|
if isinstance(items, dict_class):
|
|
for k, v in items.items():
|
|
for value in iter_nested_items(v, dict_class, list_class):
|
|
yield value
|
|
elif isinstance(items, list_class):
|
|
for item in items:
|
|
for value in iter_nested_items(item, dict_class, list_class):
|
|
yield value
|
|
elif isinstance(items, dict):
|
|
raise TypeError("%r: is a dict() instead of %r." % (items, dict_class))
|
|
elif isinstance(items, list):
|
|
raise TypeError("%r: is a list() instead of %r." % (items, list_class))
|
|
else:
|
|
yield items
|
|
|
|
|
|
def make_validator_test_class(test_file, test_args, test_num, schema_class, check_with_lxml):
|
|
"""
|
|
Creates a validator test class.
|
|
|
|
:param test_file: the XML test file path.
|
|
:param test_args: line arguments for test case.
|
|
:param test_num: a positive integer number associated with the test case.
|
|
:param schema_class: the schema class to use.
|
|
:param check_with_lxml: if `True` compare with lxml XMLSchema class, reporting anomalies. \
|
|
Works only for XSD 1.0 tests.
|
|
"""
|
|
xml_file = os.path.relpath(test_file)
|
|
msg_tmpl = "\n\n{}: %s.".format(xml_file)
|
|
|
|
# Extract schema test arguments
|
|
expected_errors = test_args.errors
|
|
expected_warnings = test_args.warnings
|
|
inspect = test_args.inspect
|
|
locations = test_args.locations
|
|
defuse = test_args.defuse
|
|
skip_strict = test_args.skip
|
|
debug_mode = test_args.debug
|
|
|
|
class TestValidator(XMLSchemaTestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# Builds schema instance using 'lax' validation mode to accepts also schemas with not crashing errors.
|
|
cls.schema_class = schema_class
|
|
source, _locations = xmlschema.fetch_schema_locations(xml_file, locations)
|
|
cls.schema = schema_class(source, validation='lax', locations=_locations, defuse=defuse)
|
|
if check_with_lxml and lxml_etree is not None:
|
|
cls.lxml_schema = lxml_etree.parse(source)
|
|
|
|
cls.errors = []
|
|
cls.chunks = []
|
|
cls.longMessage = True
|
|
|
|
if debug_mode:
|
|
print("\n##\n## Testing %r validation in debug mode.\n##" % xml_file)
|
|
pdb.set_trace()
|
|
|
|
def check_etree_encode(self, root, converter=None, **kwargs):
|
|
data1 = self.schema.decode(root, converter=converter, **kwargs)
|
|
if isinstance(data1, tuple):
|
|
data1 = data1[0] # When validation='lax'
|
|
|
|
for _ in iter_nested_items(data1, dict_class=ordered_dict_class):
|
|
pass
|
|
|
|
elem1 = self.schema.encode(data1, path=root.tag, converter=converter, **kwargs)
|
|
if isinstance(elem1, tuple):
|
|
# When validation='lax'
|
|
if converter is not ParkerConverter:
|
|
for e in elem1[1]:
|
|
self.check_namespace_prefixes(unicode_type(e))
|
|
elem1 = elem1[0]
|
|
|
|
# Checks the encoded element to not contains reserved namespace prefixes
|
|
if 'namespaces' in kwargs and all('ns%d' % k not in kwargs['namespaces'] for k in range(10)):
|
|
self.check_namespace_prefixes(etree_tostring(elem1, namespaces=kwargs['namespaces']))
|
|
|
|
# Main check: compare original a re encoded tree
|
|
try:
|
|
etree_elements_assert_equal(root, elem1, strict=False)
|
|
except AssertionError as err:
|
|
# If the check fails retry only if the converter is lossy (eg. ParkerConverter)
|
|
# or if the XML case has defaults taken from the schema or some part of data
|
|
# decoding is skipped by schema wildcards (set the specific argument in testfiles).
|
|
if converter not in (ParkerConverter, AbderaConverter, JsonMLConverter) and not skip_strict:
|
|
if debug_mode:
|
|
pdb.set_trace()
|
|
raise AssertionError(str(err) + msg_tmpl % "encoded tree differs from original")
|
|
elif converter is ParkerConverter and any(XSI_TYPE in e.attrib for e in root.iter()):
|
|
return # can't check encode equivalence if xsi:type is provided
|
|
else:
|
|
# Lossy or augmenting cases are checked after a re decoding-encoding pass
|
|
data2 = self.schema.decode(elem1, converter=converter, **kwargs)
|
|
if isinstance(data2, tuple):
|
|
data2 = data2[0]
|
|
|
|
if sys.version_info >= (3, 6):
|
|
# For Python < 3.6 cannot ensure attribute decoding order
|
|
try:
|
|
self.assertEqual(data1, data2, msg_tmpl % "re decoded data changed")
|
|
except AssertionError:
|
|
if debug_mode:
|
|
pdb.set_trace()
|
|
raise
|
|
|
|
elem2 = self.schema.encode(data2, path=root.tag, converter=converter, **kwargs)
|
|
if isinstance(elem2, tuple):
|
|
elem2 = elem2[0]
|
|
|
|
try:
|
|
etree_elements_assert_equal(elem1, elem2, strict=False)
|
|
except AssertionError as err:
|
|
if debug_mode:
|
|
pdb.set_trace()
|
|
raise AssertionError(str(err) + msg_tmpl % "encoded tree differs after second pass")
|
|
|
|
def check_json_serialization(self, root, converter=None, **kwargs):
|
|
data1 = xmlschema.to_json(root, schema=self.schema, converter=converter, **kwargs)
|
|
if isinstance(data1, tuple):
|
|
data1 = data1[0]
|
|
|
|
elem1 = xmlschema.from_json(data1, schema=self.schema, path=root.tag, converter=converter, **kwargs)
|
|
if isinstance(elem1, tuple):
|
|
elem1 = elem1[0]
|
|
|
|
data2 = xmlschema.to_json(elem1, schema=self.schema, converter=converter, **kwargs)
|
|
if isinstance(data2, tuple):
|
|
data2 = data2[0]
|
|
|
|
if converter is ParkerConverter and any(XSI_TYPE in e.attrib for e in root.iter()):
|
|
return # can't check encode equivalence if xsi:type is provided
|
|
elif sys.version_info >= (3, 6):
|
|
self.assertEqual(data2, data1, msg_tmpl % "serialized data changed at second pass")
|
|
else:
|
|
elem2 = xmlschema.from_json(data2, schema=self.schema, path=root.tag, converter=converter, **kwargs)
|
|
if isinstance(elem2, tuple):
|
|
elem2 = elem2[0]
|
|
try:
|
|
self.assertIsNone(etree_elements_assert_equal(elem1, elem2, strict=False, skip_comments=True))
|
|
except AssertionError as err:
|
|
self.assertIsNone(err, None)
|
|
|
|
def check_decoding_with_element_tree(self):
|
|
del self.errors[:]
|
|
del self.chunks[:]
|
|
|
|
def do_decoding():
|
|
for obj in self.schema.iter_decode(xml_file):
|
|
if isinstance(obj, (xmlschema.XMLSchemaDecodeError, xmlschema.XMLSchemaValidationError)):
|
|
self.errors.append(obj)
|
|
else:
|
|
self.chunks.append(obj)
|
|
|
|
if expected_warnings == 0:
|
|
do_decoding()
|
|
else:
|
|
with warnings.catch_warnings(record=True) as ctx:
|
|
warnings.simplefilter("always")
|
|
do_decoding()
|
|
self.assertEqual(len(ctx), expected_warnings, "Wrong number of include/import warnings")
|
|
|
|
self.check_errors(xml_file, expected_errors)
|
|
|
|
if not self.chunks:
|
|
raise ValueError("No decoded object returned!!")
|
|
elif len(self.chunks) > 1:
|
|
raise ValueError("Too many ({}) decoded objects returned: {}".format(len(self.chunks), self.chunks))
|
|
elif not isinstance(self.chunks[0], dict):
|
|
raise ValueError("Decoded object is not a dictionary: {}".format(self.chunks))
|
|
else:
|
|
self.assertTrue(True, "Successfully test decoding for {}".format(xml_file))
|
|
|
|
def check_schema_serialization(self):
|
|
# Repeat with serialized-deserialized schema (only for Python 3)
|
|
serialized_schema = pickle.dumps(self.schema)
|
|
deserialized_schema = pickle.loads(serialized_schema)
|
|
errors = []
|
|
chunks = []
|
|
for obj in deserialized_schema.iter_decode(xml_file):
|
|
if isinstance(obj, xmlschema.XMLSchemaValidationError):
|
|
errors.append(obj)
|
|
else:
|
|
chunks.append(obj)
|
|
|
|
self.assertEqual(len(errors), len(self.errors), msg_tmpl % "wrong number errors")
|
|
self.assertEqual(chunks, self.chunks, msg_tmpl % "decoded data differ")
|
|
|
|
def check_decode_api(self):
|
|
# Compare with the decode API and other validation modes
|
|
strict_data = self.schema.decode(xml_file)
|
|
lax_data = self.schema.decode(xml_file, validation='lax')
|
|
skip_data = self.schema.decode(xml_file, validation='skip')
|
|
self.assertEqual(strict_data, self.chunks[0], msg_tmpl % "decode() API has a different result")
|
|
self.assertEqual(lax_data[0], self.chunks[0], msg_tmpl % "'lax' validation has a different result")
|
|
self.assertEqual(skip_data, self.chunks[0], msg_tmpl % "'skip' validation has a different result")
|
|
|
|
def check_encoding_with_element_tree(self):
|
|
root = ElementTree.parse(xml_file).getroot()
|
|
namespaces = fetch_namespaces(xml_file)
|
|
options = {'namespaces': namespaces, 'dict_class': ordered_dict_class}
|
|
|
|
self.check_etree_encode(root, cdata_prefix='#', **options) # Default converter
|
|
self.check_etree_encode(root, ParkerConverter, validation='lax', **options)
|
|
self.check_etree_encode(root, ParkerConverter, validation='skip', **options)
|
|
self.check_etree_encode(root, BadgerFishConverter, **options)
|
|
self.check_etree_encode(root, AbderaConverter, **options)
|
|
self.check_etree_encode(root, JsonMLConverter, **options)
|
|
|
|
options.pop('dict_class')
|
|
self.check_json_serialization(root, cdata_prefix='#', **options)
|
|
self.check_json_serialization(root, ParkerConverter, validation='lax', **options)
|
|
self.check_json_serialization(root, ParkerConverter, validation='skip', **options)
|
|
self.check_json_serialization(root, BadgerFishConverter, **options)
|
|
self.check_json_serialization(root, AbderaConverter, **options)
|
|
self.check_json_serialization(root, JsonMLConverter, **options)
|
|
|
|
def check_decoding_and_encoding_with_lxml(self):
|
|
xml_tree = lxml_etree.parse(xml_file)
|
|
namespaces = fetch_namespaces(xml_file)
|
|
errors = []
|
|
chunks = []
|
|
for obj in self.schema.iter_decode(xml_tree, namespaces=namespaces):
|
|
if isinstance(obj, xmlschema.XMLSchemaValidationError):
|
|
errors.append(obj)
|
|
else:
|
|
chunks.append(obj)
|
|
|
|
self.assertEqual(chunks, self.chunks, msg_tmpl % "decode data change with lxml")
|
|
self.assertEqual(len(errors), len(self.errors), msg_tmpl % "errors number change with lxml")
|
|
|
|
if not errors:
|
|
root = xml_tree.getroot()
|
|
options = {
|
|
'etree_element_class': lxml_etree_element,
|
|
'namespaces': namespaces,
|
|
'dict_class': ordered_dict_class,
|
|
}
|
|
|
|
self.check_etree_encode(root, cdata_prefix='#', **options) # Default converter
|
|
self.check_etree_encode(root, ParkerConverter, validation='lax', **options)
|
|
self.check_etree_encode(root, ParkerConverter, validation='skip', **options)
|
|
self.check_etree_encode(root, BadgerFishConverter, **options)
|
|
self.check_etree_encode(root, AbderaConverter, **options)
|
|
self.check_etree_encode(root, JsonMLConverter, **options)
|
|
|
|
options.pop('dict_class')
|
|
self.check_json_serialization(root, cdata_prefix='#', **options)
|
|
self.check_json_serialization(root, ParkerConverter, validation='lax', **options)
|
|
self.check_json_serialization(root, ParkerConverter, validation='skip', **options)
|
|
self.check_json_serialization(root, BadgerFishConverter, **options)
|
|
self.check_json_serialization(root, AbderaConverter, **options)
|
|
self.check_json_serialization(root, JsonMLConverter, **options)
|
|
|
|
def check_validate_and_is_valid_api(self):
|
|
if expected_errors:
|
|
self.assertFalse(self.schema.is_valid(xml_file), msg_tmpl % "file with errors is valid")
|
|
self.assertRaises(XMLSchemaValidationError, self.schema.validate, xml_file)
|
|
else:
|
|
self.assertTrue(self.schema.is_valid(xml_file), msg_tmpl % "file without errors is not valid")
|
|
self.assertEqual(self.schema.validate(xml_file), None,
|
|
msg_tmpl % "file without errors not validated")
|
|
|
|
def check_iter_errors(self):
|
|
self.assertEqual(len(list(self.schema.iter_errors(xml_file))), expected_errors,
|
|
msg_tmpl % "wrong number of errors (%d expected)" % expected_errors)
|
|
|
|
def check_lxml_validation(self):
|
|
try:
|
|
schema = lxml_etree.XMLSchema(self.lxml_schema.getroot())
|
|
except lxml_etree.XMLSchemaParseError:
|
|
print("\nSkip lxml.etree.XMLSchema validation test for {!r} ({})".
|
|
format(xml_file, TestValidator.__name__, ))
|
|
else:
|
|
xml_tree = lxml_etree.parse(xml_file)
|
|
if self.errors:
|
|
self.assertFalse(schema.validate(xml_tree))
|
|
else:
|
|
self.assertTrue(schema.validate(xml_tree))
|
|
|
|
def test_xml_document_validation(self):
|
|
self.check_decoding_with_element_tree()
|
|
|
|
if not inspect and sys.version_info >= (3,):
|
|
self.check_schema_serialization()
|
|
|
|
if not self.errors:
|
|
self.check_encoding_with_element_tree()
|
|
|
|
if lxml_etree is not None:
|
|
self.check_decoding_and_encoding_with_lxml()
|
|
|
|
self.check_iter_errors()
|
|
self.check_validate_and_is_valid_api()
|
|
if check_with_lxml and lxml_etree is not None:
|
|
self.check_lxml_validation()
|
|
|
|
TestValidator.__name__ = TestValidator.__qualname__ = 'TestValidator{0:03}'.format(test_num)
|
|
return TestValidator
|
|
|
|
|
|
class TestValidation(XMLSchemaTestCase):
|
|
|
|
def check_validity(self, xsd_component, data, expected, use_defaults=True):
|
|
if isinstance(expected, type) and issubclass(expected, Exception):
|
|
self.assertRaises(expected, xsd_component.is_valid, data, use_defaults)
|
|
elif expected:
|
|
self.assertTrue(xsd_component.is_valid(data, use_defaults))
|
|
else:
|
|
self.assertFalse(xsd_component.is_valid(data, use_defaults))
|
|
|
|
@unittest.skipIf(lxml_etree is None, "The lxml library is not available.")
|
|
def test_lxml(self):
|
|
xs = xmlschema.XMLSchema(self.casepath('examples/vehicles/vehicles.xsd'))
|
|
xt1 = lxml_etree.parse(self.casepath('examples/vehicles/vehicles.xml'))
|
|
xt2 = lxml_etree.parse(self.casepath('examples/vehicles/vehicles-1_error.xml'))
|
|
self.assertTrue(xs.is_valid(xt1))
|
|
self.assertFalse(xs.is_valid(xt2))
|
|
self.assertTrue(xs.validate(xt1) is None)
|
|
self.assertRaises(xmlschema.XMLSchemaValidationError, xs.validate, xt2)
|
|
|
|
def test_issue_064(self):
|
|
self.check_validity(self.st_schema, '<name xmlns="ns"></name>', False)
|
|
|
|
def test_document_validate_api(self):
|
|
self.assertIsNone(xmlschema.validate(self.vh_xml_file))
|
|
self.assertIsNone(xmlschema.validate(self.vh_xml_file, use_defaults=False))
|
|
|
|
vh_2_file = self.casepath('examples/vehicles/vehicles-2_errors.xml')
|
|
self.assertRaises(XMLSchemaValidationError, xmlschema.validate, vh_2_file)
|
|
|
|
try:
|
|
xmlschema.validate(vh_2_file, namespaces={'vhx': "http://example.com/vehicles"})
|
|
except XMLSchemaValidationError as err:
|
|
path_line = str(err).splitlines()[-1]
|
|
else:
|
|
path_line = ''
|
|
self.assertEqual('Path: /vhx:vehicles/vhx:cars', path_line)
|
|
|
|
# Issue #80
|
|
vh_2_xt = ElementTree.parse(vh_2_file)
|
|
self.assertRaises(XMLSchemaValidationError, xmlschema.validate, vh_2_xt, self.vh_xsd_file)
|
|
|
|
|
|
class TestValidation11(TestValidation):
|
|
schema_class = XMLSchema11
|
|
|
|
def test_default_attributes(self):
|
|
"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<ns:node xmlns:ns="ns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="ns ./default_attributes.xsd" colour="red">Root Node</ns:node>
|
|
"""
|
|
xs = self.schema_class(self.casepath('features/attributes/default_attributes.xsd'))
|
|
self.assertTrue(xs.is_valid("<tree xmlns='ns'>"
|
|
" <node node-id='1'>alpha</node>"
|
|
" <node node-id='2' colour='red'>beta</node>"
|
|
"</tree>"))
|
|
self.assertFalse(xs.is_valid("<tree xmlns='ns'>"
|
|
" <node>alpha</node>" # Misses required attribute
|
|
" <node node-id='2' colour='red'>beta</node>"
|
|
"</tree>"))
|
|
|
|
|
|
class TestDecoding(XMLSchemaTestCase):
|
|
|
|
def check_decode(self, xsd_component, data, expected, **kwargs):
|
|
if isinstance(expected, type) and issubclass(expected, Exception):
|
|
self.assertRaises(expected, xsd_component.decode, data, **kwargs)
|
|
else:
|
|
obj = xsd_component.decode(data, **kwargs)
|
|
if isinstance(obj, tuple) and len(obj) == 2 and isinstance(obj[1], list) \
|
|
and isinstance(obj[1][0], Exception):
|
|
self.assertEqual(expected, obj[0])
|
|
self.assertTrue(isinstance(obj[0], type(expected)))
|
|
else:
|
|
self.assertEqual(expected, obj)
|
|
self.assertTrue(isinstance(obj, type(expected)))
|
|
|
|
@unittest.skipIf(lxml_etree is None, "The lxml library is not available.")
|
|
def test_lxml(self):
|
|
vh_xml_tree = lxml_etree.parse(self.vh_xml_file)
|
|
self.assertEqual(self.vh_schema.to_dict(vh_xml_tree), _VEHICLES_DICT)
|
|
self.assertEqual(xmlschema.to_dict(vh_xml_tree, self.vh_schema.url), _VEHICLES_DICT)
|
|
|
|
def test_to_dict_from_etree(self):
|
|
vh_xml_tree = ElementTree.parse(self.vh_xml_file)
|
|
col_xml_tree = ElementTree.parse(self.col_xml_file)
|
|
|
|
xml_dict = self.vh_schema.to_dict(vh_xml_tree)
|
|
self.assertNotEqual(xml_dict, _VEHICLES_DICT)
|
|
|
|
xml_dict = self.vh_schema.to_dict(vh_xml_tree, namespaces=self.vh_namespaces)
|
|
self.assertEqual(xml_dict, _VEHICLES_DICT)
|
|
|
|
xml_dict = xmlschema.to_dict(vh_xml_tree, self.vh_schema.url, namespaces=self.vh_namespaces)
|
|
self.assertEqual(xml_dict, _VEHICLES_DICT)
|
|
|
|
xml_dict = self.col_schema.to_dict(col_xml_tree)
|
|
self.assertNotEqual(xml_dict, _COLLECTION_DICT)
|
|
|
|
xml_dict = self.col_schema.to_dict(col_xml_tree, namespaces=self.col_namespaces)
|
|
self.assertEqual(xml_dict, _COLLECTION_DICT)
|
|
|
|
xml_dict = xmlschema.to_dict(col_xml_tree, self.col_schema.url, namespaces=self.col_namespaces)
|
|
self.assertEqual(xml_dict, _COLLECTION_DICT)
|
|
|
|
def test_to_dict_from_string(self):
|
|
with open(self.vh_xml_file) as f:
|
|
vh_xml_string = f.read()
|
|
|
|
with open(self.col_xml_file) as f:
|
|
col_xml_string = f.read()
|
|
|
|
xml_dict = self.vh_schema.to_dict(vh_xml_string, namespaces=self.vh_namespaces)
|
|
self.assertEqual(xml_dict, _VEHICLES_DICT)
|
|
|
|
xml_dict = xmlschema.to_dict(vh_xml_string, self.vh_schema.url, namespaces=self.vh_namespaces)
|
|
self.assertEqual(xml_dict, _VEHICLES_DICT)
|
|
|
|
xml_dict = self.col_schema.to_dict(col_xml_string, namespaces=self.col_namespaces)
|
|
self.assertTrue(xml_dict, _COLLECTION_DICT)
|
|
|
|
xml_dict = xmlschema.to_dict(col_xml_string, self.col_schema.url, namespaces=self.col_namespaces)
|
|
self.assertTrue(xml_dict, _COLLECTION_DICT)
|
|
|
|
def test_json_dump_and_load(self):
|
|
vh_xml_tree = ElementTree.parse(self.vh_xml_file)
|
|
col_xml_tree = ElementTree.parse(self.col_xml_file)
|
|
with open(self.vh_json_file, 'w') as f:
|
|
xmlschema.to_json(self.vh_xml_file, f)
|
|
|
|
with open(self.vh_json_file) as f:
|
|
root = xmlschema.from_json(f, self.vh_schema)
|
|
|
|
os.remove(self.vh_json_file)
|
|
self.check_etree_elements(vh_xml_tree, root)
|
|
|
|
with open(self.col_json_file, 'w') as f:
|
|
xmlschema.to_json(self.col_xml_file, f)
|
|
|
|
with open(self.col_json_file) as f:
|
|
root = xmlschema.from_json(f, self.col_schema)
|
|
|
|
os.remove(self.col_json_file)
|
|
self.check_etree_elements(col_xml_tree, root)
|
|
|
|
def test_path(self):
|
|
xt = ElementTree.parse(self.vh_xml_file)
|
|
xd = self.vh_schema.to_dict(xt, '/vh:vehicles/vh:cars', namespaces=self.vh_namespaces)
|
|
self.assertEqual(xd['vh:car'], _VEHICLES_DICT['vh:cars']['vh:car'])
|
|
xd = self.vh_schema.to_dict(xt, '/vh:vehicles/vh:bikes', namespaces=self.vh_namespaces)
|
|
self.assertEqual(xd['vh:bike'], _VEHICLES_DICT['vh:bikes']['vh:bike'])
|
|
|
|
def test_validation_strict(self):
|
|
self.assertRaises(
|
|
xmlschema.XMLSchemaValidationError,
|
|
self.vh_schema.to_dict,
|
|
ElementTree.parse(self.casepath('examples/vehicles/vehicles-2_errors.xml')),
|
|
validation='strict',
|
|
namespaces=self.vh_namespaces
|
|
)
|
|
|
|
def test_validation_skip(self):
|
|
xt = ElementTree.parse(self.casepath('features/decoder/data3.xml'))
|
|
xd = self.st_schema.decode(xt, validation='skip', namespaces={'ns': 'ns'})
|
|
self.assertEqual(xd['decimal_value'], ['abc'])
|
|
|
|
def test_datatypes(self):
|
|
xt = ElementTree.parse(self.casepath('features/decoder/data.xml'))
|
|
xd = self.st_schema.to_dict(xt, namespaces=self.default_namespaces)
|
|
self.assertEqual(xd, _DATA_DICT)
|
|
|
|
def test_datetime_types(self):
|
|
xs = self.get_schema('<element name="dt" type="dateTime"/>')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>'), '2019-01-01T13:40:00')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>', datetime_types=True),
|
|
datatypes.DateTime10.fromstring('2019-01-01T13:40:00'))
|
|
|
|
xs = self.get_schema('<element name="dt" type="date"/>')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>'), '2001-04-15')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>', datetime_types=True),
|
|
datatypes.Date10.fromstring('2001-04-15'))
|
|
|
|
def test_duration_type(self):
|
|
xs = self.get_schema('<element name="td" type="duration"/>')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P5Y3MT60H30.001S</ns:td>'), 'P5Y3MT60H30.001S')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P5Y3MT60H30.001S</ns:td>', datetime_types=True),
|
|
datatypes.Duration.fromstring('P5Y3M2DT12H30.001S'))
|
|
|
|
def test_converters(self):
|
|
filename = self.col_xml_file
|
|
|
|
parker_dict = self.col_schema.to_dict(self.col_xml_file, converter=xmlschema.ParkerConverter)
|
|
self.assertTrue(parker_dict == _COLLECTION_PARKER)
|
|
|
|
parker_dict_root = self.col_schema.to_dict(
|
|
filename, converter=xmlschema.ParkerConverter(preserve_root=True), decimal_type=float)
|
|
self.assertTrue(parker_dict_root == _COLLECTION_PARKER_ROOT)
|
|
|
|
badgerfish_dict = self.col_schema.to_dict(
|
|
filename, converter=xmlschema.BadgerFishConverter, decimal_type=float)
|
|
self.assertTrue(badgerfish_dict == _COLLECTION_BADGERFISH)
|
|
|
|
abdera_dict = self.col_schema.to_dict(
|
|
filename, converter=xmlschema.AbderaConverter, decimal_type=float, dict_class=dict)
|
|
self.assertTrue(abdera_dict == _COLLECTION_ABDERA)
|
|
|
|
json_ml_dict = self.col_schema.to_dict(filename, converter=xmlschema.JsonMLConverter)
|
|
self.assertTrue(json_ml_dict == _COLLECTION_JSON_ML)
|
|
|
|
def test_dict_granularity(self):
|
|
"""Based on Issue #22, test to make sure an xsd indicating list with
|
|
dictionaries, returns just that even when it has a single dict. """
|
|
xsd_string = self.casepath('issues/issue_022/xsd_string.xsd')
|
|
xml_string_1 = self.casepath('issues/issue_022/xml_string_1.xml')
|
|
xml_string_2 = self.casepath('issues/issue_022/xml_string_2.xml')
|
|
xsd_schema = xmlschema.XMLSchema(xsd_string)
|
|
xml_data_1 = xsd_schema.to_dict(xml_string_1)
|
|
xml_data_2 = xsd_schema.to_dict(xml_string_2)
|
|
self.assertTrue(isinstance(xml_data_1['bar'], type(xml_data_2['bar'])),
|
|
msg="XSD with an array that return a single element from xml must still yield a list.")
|
|
|
|
def test_any_type(self):
|
|
any_type = xmlschema.XMLSchema.meta_schema.types['anyType']
|
|
xml_data_1 = ElementTree.Element('dummy')
|
|
self.assertEqual(any_type.decode(xml_data_1), (None, [], []))
|
|
xml_data_2 = ElementTree.fromstring('<root>\n <child_1/>\n <child_2/>\n</root>')
|
|
self.assertEqual(any_type.decode(xml_data_2), (None, [], [])) # Currently no decoding yet
|
|
|
|
def test_choice_model_decoding(self):
|
|
schema = xmlschema.XMLSchema(self.casepath('issues/issue_041/issue_041.xsd'))
|
|
data = schema.to_dict(self.casepath('issues/issue_041/issue_041.xml'))
|
|
self.assertEqual(data, {
|
|
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'@xsi:noNamespaceSchemaLocation': 'issue_041.xsd',
|
|
'Name': 'SomeNameValueThingy',
|
|
'Value': {'Integer': 0}
|
|
})
|
|
|
|
def test_cdata_decoding(self):
|
|
schema = xmlschema.XMLSchema(self.casepath('issues/issue_046/issue_046.xsd'))
|
|
xml_file = self.casepath('issues/issue_046/issue_046.xml')
|
|
self.assertEqual(
|
|
schema.decode(xml_file, dict_class=ordered_dict_class, cdata_prefix='#'),
|
|
ordered_dict_class(
|
|
[('@xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'),
|
|
('@xsi:noNamespaceSchemaLocation', 'issue_046.xsd'),
|
|
('#1', 'Dear Mr.'), ('name', 'John Smith'),
|
|
('#2', '.\n Your order'), ('orderid', 1032),
|
|
('#3', 'will be shipped on'), ('shipdate', '2001-07-13'), ('#4', '.')]
|
|
))
|
|
|
|
def test_string_facets(self):
|
|
none_empty_string_type = self.st_schema.types['none_empty_string']
|
|
self.check_decode(none_empty_string_type, '', XMLSchemaValidationError)
|
|
|
|
def test_binary_data_facets(self):
|
|
hex_code_type = self.st_schema.types['hexCode']
|
|
self.check_decode(hex_code_type, u'00D7310A', u'00D7310A')
|
|
|
|
base64_code_type = self.st_schema.types['base64Code']
|
|
self.check_decode(base64_code_type, base64.b64encode(b'ok'), XMLSchemaValidationError)
|
|
base64_value = base64.b64encode(b'hello')
|
|
self.check_decode(base64_code_type, base64_value, base64_value.decode('utf-8'))
|
|
self.check_decode(base64_code_type, base64.b64encode(b'abcefgh'), u'YWJjZWZnaA==')
|
|
self.check_decode(base64_code_type, b' Y W J j ZWZ\t\tn\na A= =', u'Y W J j ZWZ n a A= =')
|
|
self.check_decode(base64_code_type, u' Y W J j ZWZ\t\tn\na A= =', u'Y W J j ZWZ n a A= =')
|
|
self.check_decode(base64_code_type, base64.b64encode(b'abcefghi'), u'YWJjZWZnaGk=')
|
|
|
|
self.check_decode(base64_code_type, u'YWJjZWZnaA=', XMLSchemaValidationError)
|
|
self.check_decode(base64_code_type, u'YWJjZWZna$==', XMLSchemaValidationError)
|
|
|
|
base64_length4_type = self.st_schema.types['base64Length4']
|
|
self.check_decode(base64_length4_type, base64.b64encode(b'abc'), XMLSchemaValidationError)
|
|
self.check_decode(base64_length4_type, base64.b64encode(b'abce'), u'YWJjZQ==')
|
|
self.check_decode(base64_length4_type, base64.b64encode(b'abcef'), XMLSchemaValidationError)
|
|
|
|
base64_length5_type = self.st_schema.types['base64Length5']
|
|
self.check_decode(base64_length5_type, base64.b64encode(b'1234'), XMLSchemaValidationError)
|
|
self.check_decode(base64_length5_type, base64.b64encode(b'12345'), u'MTIzNDU=')
|
|
self.check_decode(base64_length5_type, base64.b64encode(b'123456'), XMLSchemaValidationError)
|
|
|
|
def test_decimal_type(self):
|
|
schema = self.get_schema("""
|
|
<element name="A" type="ns:A_type" />
|
|
<simpleType name="A_type">
|
|
<restriction base="decimal">
|
|
<minInclusive value="100.50"/>
|
|
</restriction>
|
|
</simpleType>
|
|
""")
|
|
|
|
self.check_decode(schema, '<A xmlns="ns">120.48</A>', Decimal('120.48'))
|
|
self.check_decode(schema, '<A xmlns="ns">100.50</A>', Decimal('100.50'), process_namespaces=False)
|
|
self.check_decode(schema, '<A xmlns="ns">100.49</A>', XMLSchemaValidationError)
|
|
self.check_decode(schema, '<A xmlns="ns">120.48</A>', 120.48, decimal_type=float)
|
|
# Issue #66
|
|
self.check_decode(schema, '<A xmlns="ns">120.48</A>', '120.48', decimal_type=str)
|
|
|
|
def test_nillable(self):
|
|
# Issue #76
|
|
xsd_string = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
|
<xs:element name="foo" type="Foo" />
|
|
<xs:complexType name="Foo">
|
|
<xs:sequence minOccurs="1" maxOccurs="1">
|
|
<xs:element name="bar" type="xs:integer" nillable="true" />
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:schema>
|
|
"""
|
|
xsd_schema = xmlschema.XMLSchema(xsd_string)
|
|
xml_string_1 = "<foo><bar>0</bar></foo>"
|
|
xml_string_2 = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
<bar xsi:nil="true"></bar>
|
|
</foo>
|
|
"""
|
|
self.assertTrue(xsd_schema.is_valid(source=xml_string_1, use_defaults=False))
|
|
self.assertTrue(xsd_schema.is_valid(source=xml_string_2, use_defaults=False))
|
|
obj = xsd_schema.decode(xml_string_2, use_defaults=False)
|
|
self.check_etree_elements(ElementTree.fromstring(xml_string_2), xsd_schema.encode(obj))
|
|
|
|
def test_default_namespace(self):
|
|
# Issue #77
|
|
xs = xmlschema.XMLSchema("""<?xml version="1.0" encoding="UTF-8"?>
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/foo">
|
|
<xs:element name="foo" type="xs:string" />
|
|
</xs:schema>""")
|
|
self.assertEqual(xs.to_dict("""<foo xmlns="http://example.com/foo">bar</foo>""",
|
|
path='/foo', namespaces={'': 'http://example.com/foo'}), 'bar')
|
|
self.assertEqual(xs.to_dict("""<foo>bar</foo>""",
|
|
path='/foo', namespaces={'': 'http://example.com/foo'}), None)
|
|
|
|
def test_complex_with_simple_content_restriction(self):
|
|
xs = self.schema_class(self.casepath('features/derivations/complex-with-simple-content-restriction.xsd'))
|
|
self.assertTrue(xs.is_valid('<value>10</value>'))
|
|
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')
|
|
|
|
def test_default_values(self):
|
|
# From issue #108
|
|
xsd_text = """<?xml version="1.0" encoding="utf-8"?>
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="root" type="root" default="default_value"/>
|
|
<xs:complexType name="root">
|
|
<xs:simpleContent>
|
|
<xs:extension base="xs:string">
|
|
<xs:attribute name="attr" type="xs:string"/>
|
|
<xs:attribute name="attrWithDefault" type="xs:string" default="default_value"/>
|
|
<xs:attribute name="attrWithFixed" type="xs:string" fixed="fixed_value"/>
|
|
</xs:extension>
|
|
</xs:simpleContent>
|
|
</xs:complexType>
|
|
<xs:element name="simple_root" type="xs:string" default="default_value"/>
|
|
</xs:schema>"""
|
|
|
|
schema = self.schema_class(xsd_text)
|
|
self.assertEqual(schema.to_dict("<root>text</root>"),
|
|
{'@attrWithDefault': 'default_value',
|
|
'@attrWithFixed': 'fixed_value',
|
|
'$': 'text'})
|
|
self.assertEqual(schema.to_dict("<root/>"),
|
|
{'@attrWithDefault': 'default_value',
|
|
'@attrWithFixed': 'fixed_value',
|
|
'$': 'default_value'})
|
|
self.assertEqual(schema.to_dict("""<root attr="attr_value">text</root>"""),
|
|
{'$': 'text',
|
|
'@attr': 'attr_value',
|
|
'@attrWithDefault': 'default_value',
|
|
'@attrWithFixed': 'fixed_value'})
|
|
|
|
self.assertEqual(schema.to_dict("<root>text</root>", use_defaults=False),
|
|
{'@attrWithFixed': 'fixed_value', '$': 'text'})
|
|
self.assertEqual(schema.to_dict("""<root attr="attr_value">text</root>""", use_defaults=False),
|
|
{'$': 'text', '@attr': 'attr_value', '@attrWithFixed': 'fixed_value'})
|
|
self.assertEqual(schema.to_dict("<root/>", use_defaults=False), {'@attrWithFixed': 'fixed_value'})
|
|
|
|
self.assertEqual(schema.to_dict("<simple_root/>"), 'default_value')
|
|
self.assertIsNone(schema.to_dict("<simple_root/>", use_defaults=False))
|
|
|
|
|
|
class TestDecoding11(TestDecoding):
|
|
schema_class = XMLSchema11
|
|
|
|
def test_datetime_types(self):
|
|
xs = self.get_schema('<element name="dt" type="dateTime"/>')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>'), '2019-01-01T13:40:00')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>', datetime_types=True),
|
|
datatypes.DateTime.fromstring('2019-01-01T13:40:00'))
|
|
|
|
xs = self.get_schema('<element name="dt" type="date"/>')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>'), '2001-04-15')
|
|
self.assertEqual(xs.decode('<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>', datetime_types=True),
|
|
datatypes.Date.fromstring('2001-04-15'))
|
|
|
|
def test_derived_duration_types(self):
|
|
xs = self.get_schema('<element name="td" type="yearMonthDuration"/>')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P0Y4M</ns:td>'), 'P0Y4M')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P2Y10M</ns:td>', datetime_types=True),
|
|
datatypes.Duration.fromstring('P2Y10M'))
|
|
|
|
xs = self.get_schema('<element name="td" type="dayTimeDuration"/>')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P2DT6H30M30.001S</ns:td>'), 'P2DT6H30M30.001S')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P2DT26H</ns:td>'), 'P2DT26H')
|
|
self.assertEqual(xs.decode('<ns:td xmlns:ns="ns">P2DT6H30M30.001S</ns:td>', datetime_types=True),
|
|
datatypes.Duration.fromstring('P2DT6H30M30.001S'))
|
|
|
|
def test_type_alternatives(self):
|
|
xs = self.schema_class(self.casepath('features/elements/type_alternatives-no-ns.xsd'))
|
|
self.assertTrue(xs.is_valid('<value choice="int">10</value>'))
|
|
self.assertFalse(xs.is_valid('<value choice="int">10.1</value>'))
|
|
self.assertTrue(xs.is_valid('<value choice="float">10.1</value>'))
|
|
self.assertFalse(xs.is_valid('<value choice="float">alpha</value>'))
|
|
self.assertFalse(xs.is_valid('<value choice="bool">alpha</value>'))
|
|
self.assertTrue(xs.is_valid('<value choice="bool">0</value>'))
|
|
self.assertTrue(xs.is_valid('<value choice="bool">true</value>'))
|
|
|
|
xs = self.schema_class(self.casepath('features/elements/type_alternatives.xsd'))
|
|
self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="int">10</ns:value>'))
|
|
self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="int">10.1</ns:value>'))
|
|
self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="float">10.1</ns:value>'))
|
|
self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="float">alpha</ns:value>'))
|
|
self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">alpha</ns:value>'))
|
|
self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">0</ns:value>'))
|
|
self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">true</ns:value>'))
|
|
|
|
|
|
class TestEncoding(XMLSchemaTestCase):
|
|
|
|
def check_encode(self, xsd_component, data, expected, **kwargs):
|
|
if isinstance(expected, type) and issubclass(expected, Exception):
|
|
self.assertRaises(expected, xsd_component.encode, data, **kwargs)
|
|
elif is_etree_element(expected):
|
|
elem = xsd_component.encode(data, **kwargs)
|
|
self.check_etree_elements(expected, elem)
|
|
else:
|
|
obj = xsd_component.encode(data, **kwargs)
|
|
if isinstance(obj, tuple) and len(obj) == 2 and isinstance(obj[1], list):
|
|
self.assertEqual(expected, obj[0])
|
|
self.assertTrue(isinstance(obj[0], type(expected)))
|
|
elif is_etree_element(obj):
|
|
namespaces = kwargs.pop('namespaces', self.default_namespaces)
|
|
self.assertEqual(expected, etree_tostring(obj, namespaces=namespaces).strip())
|
|
else:
|
|
self.assertEqual(expected, obj)
|
|
self.assertTrue(isinstance(obj, type(expected)))
|
|
|
|
def test_decode_encode(self):
|
|
filename = self.casepath('examples/collection/collection.xml')
|
|
xt = ElementTree.parse(filename)
|
|
xd = self.col_schema.to_dict(filename, dict_class=ordered_dict_class)
|
|
elem = self.col_schema.encode(xd, path='./col:collection', namespaces=self.col_namespaces)
|
|
|
|
self.assertEqual(
|
|
len([e for e in elem.iter()]), 20,
|
|
msg="The encoded tree must have 20 elements as the origin."
|
|
)
|
|
self.assertTrue(all([
|
|
local_name(e1.tag) == local_name(e2.tag)
|
|
for e1, e2 in zip(elem.iter(), xt.getroot().iter())
|
|
]))
|
|
|
|
def test_builtin_string_based_types(self):
|
|
self.check_encode(self.xsd_types['string'], 'sample string ', u'sample string ')
|
|
self.check_encode(self.xsd_types['normalizedString'], ' sample string ', u' sample string ')
|
|
self.check_encode(self.xsd_types['normalizedString'], '\n\r sample\tstring\n', u' sample string ')
|
|
self.check_encode(self.xsd_types['token'], '\n\r sample\t\tstring\n ', u'sample string')
|
|
self.check_encode(self.xsd_types['language'], 'sample string', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['language'], ' en ', u'en')
|
|
self.check_encode(self.xsd_types['Name'], 'first_name', u'first_name')
|
|
self.check_encode(self.xsd_types['Name'], ' first_name ', u'first_name')
|
|
self.check_encode(self.xsd_types['Name'], 'first name', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['Name'], '1st_name', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['Name'], 'first_name1', u'first_name1')
|
|
self.check_encode(self.xsd_types['Name'], 'first:name', u'first:name')
|
|
self.check_encode(self.xsd_types['NCName'], 'first_name', u'first_name')
|
|
self.check_encode(self.xsd_types['NCName'], 'first:name', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['ENTITY'], 'first:name', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['ID'], 'first:name', XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['IDREF'], 'first:name', XMLSchemaValidationError)
|
|
|
|
def test_builtin_decimal_based_types(self):
|
|
self.check_encode(self.xsd_types['decimal'], -99.09, u'-99.09')
|
|
self.check_encode(self.xsd_types['decimal'], '-99.09', u'-99.09')
|
|
self.check_encode(self.xsd_types['integer'], 1000, u'1000')
|
|
self.check_encode(self.xsd_types['integer'], 100.0, XMLSchemaEncodeError)
|
|
self.check_encode(self.xsd_types['integer'], 100.0, u'100', validation='lax')
|
|
self.check_encode(self.xsd_types['short'], 1999, u'1999')
|
|
self.check_encode(self.xsd_types['short'], 10000000, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['float'], 100.0, u'100.0')
|
|
self.check_encode(self.xsd_types['float'], 'hello', XMLSchemaEncodeError)
|
|
self.check_encode(self.xsd_types['double'], -4531.7, u'-4531.7')
|
|
self.check_encode(self.xsd_types['positiveInteger'], -1, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['positiveInteger'], 0, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['nonNegativeInteger'], 0, u'0')
|
|
self.check_encode(self.xsd_types['nonNegativeInteger'], -1, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['negativeInteger'], -100, u'-100')
|
|
self.check_encode(self.xsd_types['nonPositiveInteger'], 7, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['unsignedLong'], 101, u'101')
|
|
self.check_encode(self.xsd_types['unsignedLong'], -101, XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['nonPositiveInteger'], 7, XMLSchemaValidationError)
|
|
|
|
def test_builtin_list_types(self):
|
|
self.check_encode(self.xsd_types['IDREFS'], ['first_name'], u'first_name')
|
|
self.check_encode(self.xsd_types['IDREFS'], 'first_name', u'first_name') # Transform data to list
|
|
self.check_encode(self.xsd_types['IDREFS'], ['one', 'two', 'three'], u'one two three')
|
|
self.check_encode(self.xsd_types['IDREFS'], [1, 'two', 'three'], XMLSchemaValidationError)
|
|
self.check_encode(self.xsd_types['NMTOKENS'], ['one', 'two', 'three'], u'one two three')
|
|
self.check_encode(self.xsd_types['ENTITIES'], ('mouse', 'cat', 'dog'), u'mouse cat dog')
|
|
|
|
def test_list_types(self):
|
|
list_of_strings = self.st_schema.types['list_of_strings']
|
|
self.check_encode(list_of_strings, (10, 25, 40), u'', validation='lax')
|
|
self.check_encode(list_of_strings, (10, 25, 40), u'10 25 40', validation='skip')
|
|
self.check_encode(list_of_strings, ['a', 'b', 'c'], u'a b c', validation='skip')
|
|
|
|
list_of_integers = self.st_schema.types['list_of_integers']
|
|
self.check_encode(list_of_integers, (10, 25, 40), u'10 25 40')
|
|
self.check_encode(list_of_integers, (10, 25.0, 40), XMLSchemaValidationError)
|
|
self.check_encode(list_of_integers, (10, 25.0, 40), u'10 25 40', validation='lax')
|
|
|
|
list_of_floats = self.st_schema.types['list_of_floats']
|
|
self.check_encode(list_of_floats, [10.1, 25.0, 40.0], u'10.1 25.0 40.0')
|
|
self.check_encode(list_of_floats, [10.1, 25, 40.0], u'10.1 25.0 40.0', validation='lax')
|
|
self.check_encode(list_of_floats, [10.1, False, 40.0], u'10.1 0.0 40.0', validation='lax')
|
|
|
|
list_of_booleans = self.st_schema.types['list_of_booleans']
|
|
self.check_encode(list_of_booleans, [True, False, True], u'true false true')
|
|
self.check_encode(list_of_booleans, [10, False, True], XMLSchemaEncodeError)
|
|
self.check_encode(list_of_booleans, [True, False, 40.0], u'true false', validation='lax')
|
|
self.check_encode(list_of_booleans, [True, False, 40.0], u'true false 40.0', validation='skip')
|
|
|
|
def test_union_types(self):
|
|
integer_or_float = self.st_schema.types['integer_or_float']
|
|
self.check_encode(integer_or_float, -95, u'-95')
|
|
self.check_encode(integer_or_float, -95.0, u'-95.0')
|
|
self.check_encode(integer_or_float, True, XMLSchemaEncodeError)
|
|
self.check_encode(integer_or_float, True, u'1', validation='lax')
|
|
|
|
integer_or_string = self.st_schema.types['integer_or_string']
|
|
self.check_encode(integer_or_string, 89, u'89')
|
|
self.check_encode(integer_or_string, 89.0, u'89', validation='lax')
|
|
self.check_encode(integer_or_string, 89.0, XMLSchemaEncodeError)
|
|
self.check_encode(integer_or_string, False, XMLSchemaEncodeError)
|
|
self.check_encode(integer_or_string, "Venice ", u'Venice ')
|
|
|
|
boolean_or_integer_or_string = self.st_schema.types['boolean_or_integer_or_string']
|
|
self.check_encode(boolean_or_integer_or_string, 89, u'89')
|
|
self.check_encode(boolean_or_integer_or_string, 89.0, u'89', validation='lax')
|
|
self.check_encode(boolean_or_integer_or_string, 89.0, XMLSchemaEncodeError)
|
|
self.check_encode(boolean_or_integer_or_string, False, u'false')
|
|
self.check_encode(boolean_or_integer_or_string, "Venice ", u'Venice ')
|
|
|
|
def test_simple_elements(self):
|
|
elem = etree_element('{ns}A')
|
|
elem.text = '89'
|
|
self.check_encode(self.get_element('A', type='string'), '89', elem)
|
|
self.check_encode(self.get_element('A', type='integer'), 89, elem)
|
|
elem.text = '-10.4'
|
|
self.check_encode(self.get_element('A', type='float'), -10.4, elem)
|
|
elem.text = 'false'
|
|
self.check_encode(self.get_element('A', type='boolean'), False, elem)
|
|
elem.text = 'true'
|
|
self.check_encode(self.get_element('A', type='boolean'), True, elem)
|
|
|
|
self.check_encode(self.get_element('A', type='short'), 128000, XMLSchemaValidationError)
|
|
elem.text = '0'
|
|
self.check_encode(self.get_element('A', type='nonNegativeInteger'), 0, elem)
|
|
self.check_encode(self.get_element('A', type='nonNegativeInteger'), '0', XMLSchemaValidationError)
|
|
self.check_encode(self.get_element('A', type='positiveInteger'), 0, XMLSchemaValidationError)
|
|
elem.text = '-1'
|
|
self.check_encode(self.get_element('A', type='negativeInteger'), -1, elem)
|
|
self.check_encode(self.get_element('A', type='nonNegativeInteger'), -1, XMLSchemaValidationError)
|
|
|
|
def test_complex_elements(self):
|
|
schema = self.get_schema("""
|
|
<element name="A" type="ns:A_type" />
|
|
<complexType name="A_type" mixed="true">
|
|
<simpleContent>
|
|
<extension base="string">
|
|
<attribute name="a1" type="short" use="required"/>
|
|
<attribute name="a2" type="negativeInteger"/>
|
|
</extension>
|
|
</simpleContent>
|
|
</complexType>
|
|
""")
|
|
self.check_encode(
|
|
schema.elements['A'], data={'@a1': 10, '@a2': -1, '$': 'simple '},
|
|
expected='<ns:A xmlns:ns="ns" a1="10" a2="-1">simple </ns:A>',
|
|
)
|
|
self.check_encode(
|
|
schema.elements['A'], {'@a1': 10, '@a2': -1, '$': 'simple '},
|
|
ElementTree.fromstring('<A xmlns="ns" a1="10" a2="-1">simple </A>'),
|
|
)
|
|
self.check_encode(
|
|
schema.elements['A'], {'@a1': 10, '@a2': -1},
|
|
ElementTree.fromstring('<A xmlns="ns" a1="10" a2="-1"/>')
|
|
)
|
|
self.check_encode(
|
|
schema.elements['A'], {'@a1': 10, '$': 'simple '},
|
|
ElementTree.fromstring('<A xmlns="ns" a1="10">simple </A>')
|
|
)
|
|
self.check_encode(schema.elements['A'], {'@a2': -1, '$': 'simple '}, XMLSchemaValidationError)
|
|
|
|
schema = self.get_schema("""
|
|
<element name="A" type="ns:A_type" />
|
|
<complexType name="A_type">
|
|
<sequence>
|
|
<element name="B1" type="string"/>
|
|
<element name="B2" type="integer"/>
|
|
<element name="B3" type="boolean"/>
|
|
</sequence>
|
|
</complexType>
|
|
""")
|
|
self.check_encode(
|
|
xsd_component=schema.elements['A'],
|
|
data=ordered_dict_class([('B1', 'abc'), ('B2', 10), ('B3', False)]),
|
|
expected=u'<ns:A xmlns:ns="ns">\n<B1>abc</B1>\n<B2>10</B2>\n<B3>false</B3>\n</ns:A>',
|
|
indent=0,
|
|
)
|
|
self.check_encode(schema.elements['A'], {'B1': 'abc', 'B2': 10, 'B4': False}, XMLSchemaValidationError)
|
|
self.check_encode(
|
|
xsd_component=schema.elements['A'],
|
|
data=ordered_dict_class([('B1', 'abc'), ('B2', 10), ('#1', 'hello'), ('B3', True)]),
|
|
expected=u'<ns:A xmlns:ns="ns">\n<B1>abc</B1>\n<B2>10</B2>\nhello\n<B3>true</B3>\n</ns:A>',
|
|
indent=0, cdata_prefix='#'
|
|
)
|
|
self.check_encode(
|
|
xsd_component=schema.elements['A'],
|
|
data=ordered_dict_class([('B1', 'abc'), ('B2', 10), ('#1', 'hello')]),
|
|
expected=XMLSchemaValidationError, indent=0, cdata_prefix='#'
|
|
)
|
|
|
|
def test_encode_datetime(self):
|
|
xs = self.get_schema('<element name="dt" type="dateTime"/>')
|
|
|
|
dt = xs.decode('<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>', datetime_types=True)
|
|
self.assertEqual(
|
|
etree_tostring(xs.encode(dt)),
|
|
'<ns:dt xmlns:ns="ns">2019-01-01T13:40:00</ns:dt>'
|
|
)
|
|
|
|
def test_encode_date(self):
|
|
xs = self.get_schema('<element name="dt" type="date"/>')
|
|
date = xs.decode('<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>', datetime_types=True)
|
|
self.assertEqual(
|
|
etree_tostring(xs.encode(date)),
|
|
'<ns:dt xmlns:ns="ns">2001-04-15</ns:dt>'
|
|
)
|
|
|
|
def test_duration(self):
|
|
xs = self.get_schema('<element name="td" type="duration"/>')
|
|
duration = xs.decode('<ns:td xmlns:ns="ns">P5Y3MT60H30.001S</ns:td>', datetime_types=True)
|
|
self.assertEqual(
|
|
etree_tostring(xs.encode(duration)),
|
|
'<ns:td xmlns:ns="ns">P5Y3M2DT12H30.001S</ns:td>'
|
|
)
|
|
|
|
def test_gregorian_year(self):
|
|
xs = self.get_schema('<element name="td" type="gYear"/>')
|
|
gyear = xs.decode('<ns:td xmlns:ns="ns">2000</ns:td>', datetime_types=True)
|
|
self.assertEqual(
|
|
etree_tostring(xs.encode(gyear)),
|
|
'<ns:td xmlns:ns="ns">2000</ns:td>'
|
|
)
|
|
|
|
def test_gregorian_yearmonth(self):
|
|
xs = self.get_schema('<element name="td" type="gYearMonth"/>')
|
|
gyear_month = xs.decode('<ns:td xmlns:ns="ns">2000-12</ns:td>', datetime_types=True)
|
|
self.assertEqual(
|
|
etree_tostring(xs.encode(gyear_month)),
|
|
'<ns:td xmlns:ns="ns">2000-12</ns:td>'
|
|
)
|
|
|
|
|
|
class TestEncoding11(TestEncoding):
|
|
schema_class = XMLSchema11
|
|
|
|
|
|
# Creates decoding/encoding tests classes from XML files
|
|
globals().update(tests_factory(make_validator_test_class, 'xml'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
from xmlschema.tests import print_test_header
|
|
|
|
print_test_header()
|
|
unittest.main()
|