From 13a5f5371ca431144d68fadb783a42802f32527a Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Thu, 7 Feb 2019 23:10:31 +0100 Subject: [PATCH 01/12] Parse of XSD 1.1 targetNamespace of elements and attributes - Added XsdComponent._parse_target_namespace() --- CHANGELOG.rst | 5 +++++ doc/conf.py | 2 +- setup.py | 2 +- xmlschema/__init__.py | 2 +- xmlschema/validators/attributes.py | 14 ++++++++++++-- xmlschema/validators/elements.py | 1 + xmlschema/validators/xsdbase.py | 24 ++++++++++++++++++++++++ 7 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f82a385..5269a284 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ CHANGELOG ********* +`v1.0.10`_ (TBD) +================ +* (XSD 1.1 features implementation completed) + `v1.0.9`_ (2019-02-03) ====================== * Programmatic import of ElementTree for avoid module mismatches @@ -220,3 +224,4 @@ v0.9.6 (2017-05-05) .. _v1.0.7: https://github.com/brunato/xmlschema/compare/v1.0.6...v1.0.7 .. _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 diff --git a/doc/conf.py b/doc/conf.py index 7eddde2d..657449be 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -62,7 +62,7 @@ # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. -release = '1.0.9' +release = '1.0.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index b9f995ea..f576e0cc 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name='xmlschema', - version='1.0.9', + version='1.0.10', install_requires=['elementpath>=1.1.2'], packages=['xmlschema'], include_package_data=True, diff --git a/xmlschema/__init__.py b/xmlschema/__init__.py index 5de1c1eb..cc4fb98a 100644 --- a/xmlschema/__init__.py +++ b/xmlschema/__init__.py @@ -25,7 +25,7 @@ XMLSchemaImportWarning, XsdGlobals, XMLSchemaBase, XMLSchema, XMLSchema10 ) -__version__ = '1.0.9' +__version__ = '1.0.10' __author__ = "Davide Brunato" __contact__ = "brunato@sissa.it" __copyright__ = "Copyright 2016-2019, SISSA" diff --git a/xmlschema/validators/attributes.py b/xmlschema/validators/attributes.py index 9b1c0cff..df20d90e 100644 --- a/xmlschema/validators/attributes.py +++ b/xmlschema/validators/attributes.py @@ -17,13 +17,13 @@ from ..compat import MutableMapping from ..exceptions import XMLSchemaAttributeError, XMLSchemaValueError -from ..qnames import XSD_ANY_SIMPLE_TYPE, XSD_SIMPLE_TYPE, XSD_ATTRIBUTE_GROUP, XSD_COMPLEX_TYPE, \ +from ..qnames import XSD_ANY_SIMPLE_TYPE, XSD_SIMPLE_TYPE, XSD_ATTRIBUTE_GROUP, XSD_COMPLEX_TYPE, XSD_ANY_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 ..namespaces import XSI_NAMESPACE from .exceptions import XMLSchemaValidationError -from .xsdbase import XsdComponent, ValidationMixin +from .xsdbase import XsdComponent, ValidationMixin, XsdType from .simple_types import XsdSimpleType from .wildcards import XsdAnyAttribute @@ -229,6 +229,16 @@ class Xsd11Attribute(XsdAttribute): def inheritable(self): return self.elem.get('inheritable') in ('0', 'true') + @property + def target_namespace(self): + return self.elem.get('targetNamespace', self.schema.target_namespace) + + def _parse(self): + super(Xsd11Attribute, self)._parse() + if not self.elem.get('inheritable') not in {'0', '1', 'false', 'true'}: + self.parse_error("an XML boolean value is required for attribute 'inheritable'") + self._parse_target_namespace() + class XsdAttributeGroup(MutableMapping, XsdComponent, ValidationMixin): """ diff --git a/xmlschema/validators/elements.py b/xmlschema/validators/elements.py index 9b7c418e..b3bfab80 100644 --- a/xmlschema/validators/elements.py +++ b/xmlschema/validators/elements.py @@ -599,6 +599,7 @@ def _parse(self): index = self._parse_alternatives(index) self._parse_constraints(index) self._parse_substitution_group() + self._parse_target_namespace() def _parse_alternatives(self, index=0): if self._ref is not None: diff --git a/xmlschema/validators/xsdbase.py b/xmlschema/validators/xsdbase.py index 73af418f..c1a05b28 100644 --- a/xmlschema/validators/xsdbase.py +++ b/xmlschema/validators/xsdbase.py @@ -286,6 +286,30 @@ def _parse_properties(self, *properties): except (ValueError, TypeError) as err: self.parse_error(str(err)) + def _parse_target_namespace(self): + """ + XSD 1.1 targetNamespace attribute in elements and attributes declarations. + """ + self._target_namespace = self.elem.get('targetNamespace') + if self._target_namespace is not None: + if 'name' not in self.elem.attrib: + self.parse_error("attribute 'name' must be present when 'targetNamespace' attribute is provided") + if 'form' in self.elem.attrib: + self.parse_error("attribute 'form' must be absent when 'targetNamespace' attribute is provided") + if self.elem.attrib['targetNamespace'].strip() != self.schema.target_namespace: + parent = self.parent + if parent is None: + self.parse_error("a global attribute must has the same namespace as its parent schema") + elif not isinstance(parent, XsdType) or not parent.is_complex() or parent.derivation != 'restriction': + self.parse_error("a complexType restriction required for parent, found %r" % self.parent) + elif self.parent.base_type.name == XSD_ANY_TYPE: + pass + + elif self.qualified: + self._target_namespace = self.schema.target_namespace + else: + self._target_namespace = '' + @property def local_name(self): return local_name(self.name) From bbbbbe0873f094e022ca0f6626bf4d8975bd42bb Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Fri, 8 Feb 2019 23:28:49 +0100 Subject: [PATCH 02/12] Add override tag and XMLSchema11._include_schema() --- xmlschema/qnames.py | 1 + xmlschema/tests/__init__.py | 28 ++++++++- xmlschema/tests/test_schemas.py | 99 +++++++++++++----------------- xmlschema/tests/test_validators.py | 18 +----- xmlschema/validators/exceptions.py | 2 +- xmlschema/validators/globals_.py | 3 +- xmlschema/validators/schema.py | 44 +++++++------ 7 files changed, 103 insertions(+), 92 deletions(-) diff --git a/xmlschema/qnames.py b/xmlschema/qnames.py index 41a56b42..092f54cc 100644 --- a/xmlschema/qnames.py +++ b/xmlschema/qnames.py @@ -55,6 +55,7 @@ def xsi_qname(name): XSD_INCLUDE = xsd_qname('include') XSD_IMPORT = xsd_qname('import') XSD_REDEFINE = xsd_qname('redefine') +XSD_OVERRIDE = xsd_qname('override') # Structures XSD_SIMPLE_TYPE = xsd_qname('simpleType') diff --git a/xmlschema/tests/__init__.py b/xmlschema/tests/__init__.py index bde4b165..e9b1a2f4 100644 --- a/xmlschema/tests/__init__.py +++ b/xmlschema/tests/__init__.py @@ -18,7 +18,7 @@ import xmlschema from xmlschema import XMLSchema -from xmlschema.compat import urlopen, URLError +from xmlschema.compat import urlopen, URLError, unicode_type from xmlschema.exceptions import XMLSchemaValueError from xmlschema.etree import ( is_etree_element, etree_element, etree_register_namespace, etree_elements_assert_equal @@ -75,6 +75,7 @@ class XMLSchemaTestCase(unittest.TestCase): @classmethod 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)') @@ -164,3 +165,28 @@ def check_namespace_prefixes(self, s): if match: msg = "Protected prefix {!r} found:\n {}".format(match.group(0), s) self.assertIsNone(match, msg) + + def check_errors(self, expected): + """ + Checks schema or validation errors, checking information completeness of the + instances and those number against expected. + """ + for e in self.errors: + error_string = unicode_type(e) + self.assertTrue(e.path, "Missing path for: %s" % error_string) + self.assertTrue(e.namespaces, "Missing namespaces for: %s" % error_string) + self.check_namespace_prefixes(error_string) + + if not self.errors and expected: + raise ValueError("found no errors when %d expected." % expected) + elif len(self.errors) != expected: + num_errors = len(self.errors) + if num_errors == 1: + msg = "n.{} errors expected, found {}:\n\n{}" + elif num_errors <= 5: + msg = "n.{} errors expected, found {}. Errors follow:\n\n{}" + else: + msg = "n.{} errors expected, found {}. First five errors follow:\n\n{}" + + error_string = '\n++++++++++\n\n'.join([unicode_type(e) for e in self.errors[:5]]) + raise ValueError(msg.format(expected, len(self.errors), error_string)) diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index 98dda874..2578aab0 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -14,6 +14,7 @@ """ from __future__ import print_function, unicode_literals import unittest +import pdb import os import pickle import time @@ -482,17 +483,24 @@ def make_schema_test_class(test_file, test_args, test_num=0, schema_class=None, defuse = test_args.defuse debug_mode = test_args.debug - def test_schema(self): - if inspect: - SchemaObserver.clear() + class TestSchema(XMLSchemaTestCase): - def check_schema(): + @classmethod + def setUpClass(cls): + cls.rel_path = os.path.relpath(test_file) + cls.errors = [] + cls.longMessage = True + + if debug_mode: + print("\n##\n## Testing %s schema in debug mode.\n##" % cls.rel_path) + pdb.set_trace() + + def check_schema(self): if expected_errors > 0: xs = schema_class(xsd_file, validation='lax', locations=locations, defuse=defuse) else: xs = schema_class(xsd_file, locations=locations, defuse=defuse) - - errors_ = xs.all_errors + self.errors.extend(xs.all_errors) if inspect: components_ids = set([id(c) for c in xs.iter_components()]) @@ -519,78 +527,57 @@ def check_schema(): self.assertEqual(xs.built, deserialized_schema.built) # XPath API tests - if not inspect and not errors_: + if not inspect and not self.errors: context = ElementPathContext(xs) elements = [x for x in xs.iter()] context_elements = [x for x in context.iter() if isinstance(x, XsdValidator)] self.assertEqual(context_elements, [x for x in context.iter_descendants()]) self.assertEqual(context_elements, elements) - return errors_ - - if debug_mode: - print("\n##\n## Testing schema %s in debug mode.\n##" % rel_path) - import pdb - pdb.set_trace() - - start_time = time.time() - if expected_warnings > 0: - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter("always") - errors = check_schema() - self.assertEqual(len(ctx), expected_warnings, "Wrong number of include/import warnings") - else: - errors = check_schema() - - # Check with lxml.etree.XMLSchema class - if check_with_lxml and lxml_etree is not None: - schema_time = time.time() - start_time + def check_lxml_schema(self, xmlschema_time): start_time = time.time() lxs = lxml_etree.parse(xsd_file) try: lxml_etree.XMLSchema(lxs.getroot()) except lxml_etree.XMLSchemaParseError as err: - if not errors: + if not self.errors: print("\nSchema error with lxml.etree.XMLSchema for file {!r} ({}): {}".format( - rel_path, class_name, unicode_type(err) + self.rel_path, self.__class__.__name__, unicode_type(err) )) else: - if errors: + if self.errors: print("\nUnrecognized errors with lxml.etree.XMLSchema for file {!r} ({}): {}".format( - rel_path, class_name, '\n++++++\n'.join([unicode_type(e) for e in errors]) + self.rel_path, self.__class__.__name__, + '\n++++++\n'.join([unicode_type(e) for e in self.errors]) )) lxml_schema_time = time.time() - start_time - if lxml_schema_time >= schema_time: + if lxml_schema_time >= xmlschema_time: print( "\nSlower lxml.etree.XMLSchema ({:.3f}s VS {:.3f}s) with file {!r} ({})".format( - lxml_schema_time, schema_time, rel_path, class_name + lxml_schema_time, xmlschema_time, self.rel_path, self.__class__.__name__ )) - # Check errors completeness - for e in errors: - error_string = unicode_type(e) - self.assertTrue(e.path, "Missing path for: %s" % error_string) - self.assertTrue(e.namespaces, "Missing namespaces for: %s" % error_string) - self.check_namespace_prefixes(error_string) - - num_errors = len(errors) - if num_errors != expected_errors: - print("\n%s: %r errors, %r expected." % (self.id()[13:], num_errors, expected_errors)) - if num_errors == 0: - raise ValueError("found no errors when %d expected." % expected_errors) + def test_xsd_schema(self): + if inspect: + SchemaObserver.clear() + self.errors.clear() + + start_time = time.time() + if expected_warnings > 0: + 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") else: - raise ValueError("n.%d errors expected, found %d: %s" % ( - expected_errors, num_errors, '\n++++++\n'.join([str(e) for e in errors]) - )) - else: - self.assertTrue(True, "Successfully created schema for {}".format(xsd_file)) - - rel_path = os.path.relpath(test_file) - class_name = 'Test{}_{:03}'.format(schema_class.__name__, test_num) - return type( - class_name if PY3 else str(class_name), (XMLSchemaTestCase,), { - 'test_schema_{0:03}_{1}'.format(test_num, rel_path): test_schema - }) + self.check_schema() + + # Check with lxml.etree.XMLSchema class + if check_with_lxml and lxml_etree is not None: + self.check_lxml_schema(xmlschema_time=time.time()-start_time) + self.check_errors(expected_errors) + + TestSchema.__name__ = TestSchema.__qualname__ = 'TestSchema{0:03}'.format(test_num) + return TestSchema # Creates schema tests from XSD files diff --git a/xmlschema/tests/test_validators.py b/xmlschema/tests/test_validators.py index a7d8a49c..6d53ded8 100644 --- a/xmlschema/tests/test_validators.py +++ b/xmlschema/tests/test_validators.py @@ -314,7 +314,7 @@ class TestValidator(XMLSchemaTestCase): @classmethod def setUpClass(cls): if debug_mode: - print("\n##\n## Testing schema %s in debug mode.\n##" % rel_path) + print("\n##\n## Testing %s validation in debug mode.\n##" % rel_path) pdb.set_trace() # Builds schema instance using 'lax' validation mode to accepts also schemas with not crashing errors. @@ -431,19 +431,7 @@ def do_decoding(): do_decoding() self.assertEqual(len(ctx), expected_warnings, "Wrong number of include/import warnings") - if len(self.errors) != expected_errors: - raise ValueError( - "file %r: n.%d errors expected, found %d: %s" % ( - rel_path, expected_errors, len(self.errors), '\n++++++\n'.join([str(e) for e in self.errors]) - ) - ) - - # Checks errors correctness - for e in self.errors: - error_string = unicode_type(e) - self.assertTrue(e.path, "Missing path for: %s" % error_string) - self.assertTrue(e.namespaces, "Missing namespaces for: %s" % error_string) - self.check_namespace_prefixes(error_string) + self.check_errors(expected_errors) if not self.chunks: raise ValueError("No decoded object returned!!") @@ -561,7 +549,7 @@ def check_lxml_validation(self): else: self.assertTrue(schema.validate(xml_tree)) - def test_decoding_and_encoding(self): + def test_xml_document_validation(self): self.check_decoding_with_element_tree() if not inspect and sys.version_info >= (3,): diff --git a/xmlschema/validators/exceptions.py b/xmlschema/validators/exceptions.py index 9a9b1c57..a3d02041 100644 --- a/xmlschema/validators/exceptions.py +++ b/xmlschema/validators/exceptions.py @@ -152,7 +152,7 @@ class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError): def __init__(self, validator, obj, reason=None, source=None, namespaces=None): super(XMLSchemaValidationError, self).__init__( validator=validator, - message="failed validating {!r} with {!r}.\n".format(obj, validator), + message="failed validating {!r} with {!r}".format(obj, validator), elem=obj if is_etree_element(obj) else None, source=source, namespaces=namespaces, diff --git a/xmlschema/validators/globals_.py b/xmlschema/validators/globals_.py index 65a4cbdc..54fa7b2c 100644 --- a/xmlschema/validators/globals_.py +++ b/xmlschema/validators/globals_.py @@ -17,7 +17,7 @@ from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, XMLSchemaValueError from ..namespaces import XSD_NAMESPACE -from ..qnames import XSD_INCLUDE, XSD_IMPORT, XSD_REDEFINE, XSD_NOTATION, XSD_SIMPLE_TYPE, \ +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 ..namespaces import NamespaceResourcesMap @@ -49,6 +49,7 @@ def iterfind_function(elem): iterchildren_xsd_import = iterchildren_by_tag(XSD_IMPORT) iterchildren_xsd_include = iterchildren_by_tag(XSD_INCLUDE) iterchildren_xsd_redefine = iterchildren_by_tag(XSD_REDEFINE) +iterchildren_xsd_override = iterchildren_by_tag(XSD_OVERRIDE) # diff --git a/xmlschema/validators/schema.py b/xmlschema/validators/schema.py index 6ceb6eac..b1335c02 100644 --- a/xmlschema/validators/schema.py +++ b/xmlschema/validators/schema.py @@ -54,7 +54,8 @@ XsdAnyAttribute, xsd_simple_type_factory, Xsd11Attribute, Xsd11Element, Xsd11AnyElement, Xsd11AnyAttribute, Xsd11AtomicRestriction, Xsd11ComplexType, Xsd11Group, XsdGlobals ) -from .globals_ import iterchildren_xsd_import, iterchildren_xsd_include, iterchildren_xsd_redefine +from .globals_ import iterchildren_xsd_import, iterchildren_xsd_include, \ + iterchildren_xsd_redefine, iterchildren_xsd_override # Elements for building dummy groups @@ -315,8 +316,8 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None self.errors.extend([e for e in self.meta_schema.iter_errors(root, namespaces=self.namespaces)]) # Includes and imports schemas (errors are treated as warnings) - self.warnings.extend(self._include_schemas()) - self.warnings.extend(self._import_namespaces()) + self._include_schemas() + self._import_namespaces() if build: self.maps.build() @@ -631,8 +632,6 @@ def get_converter(self, converter=None, namespaces=None, **kwargs): def _include_schemas(self): """Processes schema document inclusions and redefinitions.""" - include_warnings = [] - for child in iterchildren_xsd_include(self.root): try: self.include_schema(child.attrib['schemaLocation'], self.base_url) @@ -643,8 +642,8 @@ def _include_schemas(self): # It is not an error if the location fail to resolve: # https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#compound-schema # https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#src-include - include_warnings.append("Include schema failed: %s." % str(err)) - warnings.warn(include_warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) + self.warnings.append("Include schema failed: %s." % str(err)) + warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) for child in iterchildren_xsd_redefine(self.root): try: @@ -654,13 +653,11 @@ def _include_schemas(self): except (OSError, IOError) as err: # If the redefine doesn't contain components (annotation excluded) the statement # is equivalent to an include, so no error is generated. Otherwise fails. - include_warnings.append("Redefine schema failed: %s." % str(err)) - warnings.warn(include_warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) + self.warnings.append("Redefine schema failed: %s." % str(err)) + warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) if has_xsd_components(child): self.parse_error(str(err), child) - return include_warnings - def include_schema(self, location, base_url=None): """ Includes a schema for the same namespace, from a specific URL. @@ -669,7 +666,6 @@ def include_schema(self, location, base_url=None): :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: @@ -691,7 +687,6 @@ def include_schema(self, location, base_url=None): def _import_namespaces(self): """Processes namespace imports. Return a list of exceptions.""" - import_warnings = [] namespace_imports = NamespaceResourcesMap(map( lambda x: (x.get('namespace', '').strip(), x.get('schemaLocation')), iterchildren_xsd_import(self.root) @@ -729,12 +724,10 @@ def _import_namespaces(self): break else: if import_error is None: - import_warnings.append("Namespace import failed: no schema location provided.") + self.warnings.append("Namespace import failed: no schema location provided.") else: - import_warnings.append("Namespace import failed: %s." % str(import_error)) - warnings.warn(import_warnings[-1], XMLSchemaImportWarning, stacklevel=3) - - return import_warnings + self.warnings.append("Namespace import failed: %s." % str(import_error)) + warnings.warn(self.warnings[-1], XMLSchemaImportWarning, stacklevel=3) def import_schema(self, namespace, location, base_url=None, force=False): """ @@ -991,6 +984,21 @@ class XMLSchema11(XMLSchemaBase): XLINK_NAMESPACE: XLINK_SCHEMA_FILE, } + def _include_schemas(self): + super(XMLSchema11, self)._include_schemas() + for child in iterchildren_xsd_override(self.root): + try: + self.include_schema(child.attrib['schemaLocation'], self.base_url) + except KeyError: + pass # Attribute missing error already found by validation against meta-schema + except (OSError, IOError) as err: + # If the override doesn't contain components (annotation excluded) the statement + # is equivalent to an include, so no error is generated. Otherwise fails. + self.warnings.append("Override schema failed: %s." % str(err)) + warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) + if has_xsd_components(child): + self.parse_error(str(err), child) + XMLSchema = XMLSchema10 """The default class for schema instances.""" From ebe368166ddbf52970845775a6ec6a766c487965 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Sat, 9 Feb 2019 09:09:28 +0100 Subject: [PATCH 03/12] Code cleaning on XMLSchemaTestCase classes --- xmlschema/tests/__init__.py | 23 ++++++------ xmlschema/tests/test_schemas.py | 18 ++++----- xmlschema/tests/test_validators.py | 53 +++++++++++++-------------- xmlschema/validators/complex_types.py | 3 +- xmlschema/validators/schema.py | 2 +- 5 files changed, 48 insertions(+), 51 deletions(-) diff --git a/xmlschema/tests/__init__.py b/xmlschema/tests/__init__.py index e9b1a2f4..c0a562fe 100644 --- a/xmlschema/tests/__init__.py +++ b/xmlschema/tests/__init__.py @@ -60,9 +60,7 @@ class XMLSchemaTestCase(unittest.TestCase): Setup tests common environment. The tests parts have to use empty prefix for XSD namespace names and 'ns' prefix for XMLSchema test namespace names. """ - - test_dir = os.path.dirname(__file__) - test_cases_dir = os.path.join(test_dir, 'test_cases/') + test_cases_dir = os.path.join(os.path.dirname(__file__), 'test_cases/') etree_register_namespace(prefix='', uri=XSD_NAMESPACE) etree_register_namespace(prefix='ns', uri="ns") SCHEMA_TEMPLATE = """ @@ -104,9 +102,9 @@ def setUpClass(cls): @classmethod def casepath(cls, path): """ - Returns the absolute path for a test case file. + Returns the absolute path of a test case file. - :param path: the relative path of the case file from base dir ``test_cases/``. + :param path: the relative path of the case file from base dir ``xmlschema/tests/test_cases/``. """ return os.path.join(cls.test_cases_dir, path) @@ -138,7 +136,7 @@ def retrieve_schema_source(self, source): else: source = source.strip() if not source.startswith('<'): - return os.path.join(self.test_dir, source) + return self.casepath(source) else: return self.SCHEMA_TEMPLATE.format(self.schema_class.XSD_VERSION, source) @@ -166,10 +164,13 @@ def check_namespace_prefixes(self, s): msg = "Protected prefix {!r} found:\n {}".format(match.group(0), s) self.assertIsNone(match, msg) - def check_errors(self, expected): + def check_errors(self, path, expected): """ Checks schema or validation errors, checking information completeness of the instances and those number against expected. + + :param path: the path of the test case. + :param expected: the number of expected errors. """ for e in self.errors: error_string = unicode_type(e) @@ -182,11 +183,11 @@ def check_errors(self, expected): elif len(self.errors) != expected: num_errors = len(self.errors) if num_errors == 1: - msg = "n.{} errors expected, found {}:\n\n{}" + msg = "{!r}: n.{} errors expected, found {}:\n\n{}" elif num_errors <= 5: - msg = "n.{} errors expected, found {}. Errors follow:\n\n{}" + msg = "{!r}: n.{} errors expected, found {}. Errors follow:\n\n{}" else: - msg = "n.{} errors expected, found {}. First five errors follow:\n\n{}" + msg = "{!r}: n.{} errors expected, found {}. First five errors follow:\n\n{}" error_string = '\n++++++++++\n\n'.join([unicode_type(e) for e in self.errors[:5]]) - raise ValueError(msg.format(expected, len(self.errors), error_string)) + raise ValueError(msg.format(path, expected, len(self.errors), error_string)) diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index 2578aab0..18cc5dda 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -460,7 +460,7 @@ def test_assertion_facet(self): self.assertFalse(schema.types['Percentage'].is_valid('90.1')) -def make_schema_test_class(test_file, test_args, test_num=0, schema_class=None, check_with_lxml=True): +def make_schema_test_class(test_file, test_args, test_num, schema_class, check_with_lxml): """ Creates a schema test class. @@ -471,9 +471,7 @@ def make_schema_test_class(test_file, test_args, test_num=0, schema_class=None, :param check_with_lxml: if `True` compare with lxml XMLSchema class, reporting anomalies. \ Works only for XSD 1.0 tests. """ - xsd_file = test_file - if schema_class is None: - schema_class = XMLSchema + xsd_file = os.path.relpath(test_file) # Extract schema test arguments expected_errors = test_args.errors @@ -487,12 +485,12 @@ class TestSchema(XMLSchemaTestCase): @classmethod def setUpClass(cls): - cls.rel_path = os.path.relpath(test_file) + cls.schema_class = schema_class cls.errors = [] cls.longMessage = True if debug_mode: - print("\n##\n## Testing %s schema in debug mode.\n##" % cls.rel_path) + print("\n##\n## Testing %r schema in debug mode.\n##" % xsd_file) pdb.set_trace() def check_schema(self): @@ -542,19 +540,19 @@ def check_lxml_schema(self, xmlschema_time): except lxml_etree.XMLSchemaParseError as err: if not self.errors: print("\nSchema error with lxml.etree.XMLSchema for file {!r} ({}): {}".format( - self.rel_path, self.__class__.__name__, unicode_type(err) + xsd_file, self.__class__.__name__, unicode_type(err) )) else: if self.errors: print("\nUnrecognized errors with lxml.etree.XMLSchema for file {!r} ({}): {}".format( - self.rel_path, self.__class__.__name__, + xsd_file, self.__class__.__name__, '\n++++++\n'.join([unicode_type(e) for e in self.errors]) )) lxml_schema_time = time.time() - start_time if lxml_schema_time >= xmlschema_time: print( "\nSlower lxml.etree.XMLSchema ({:.3f}s VS {:.3f}s) with file {!r} ({})".format( - lxml_schema_time, xmlschema_time, self.rel_path, self.__class__.__name__ + lxml_schema_time, xmlschema_time, xsd_file, self.__class__.__name__ )) def test_xsd_schema(self): @@ -574,7 +572,7 @@ def test_xsd_schema(self): # Check with lxml.etree.XMLSchema class if check_with_lxml and lxml_etree is not None: self.check_lxml_schema(xmlschema_time=time.time()-start_time) - self.check_errors(expected_errors) + self.check_errors(xsd_file, expected_errors) TestSchema.__name__ = TestSchema.__qualname__ = 'TestSchema{0:03}'.format(test_num) return TestSchema diff --git a/xmlschema/tests/test_validators.py b/xmlschema/tests/test_validators.py index 6d53ded8..11367138 100644 --- a/xmlschema/tests/test_validators.py +++ b/xmlschema/tests/test_validators.py @@ -282,7 +282,7 @@ def iter_nested_items(items, dict_class=dict, list_class=list): yield items -def make_validator_test_class(test_file, test_args, test_num=0, schema_class=None, check_with_lxml=False): +def make_validator_test_class(test_file, test_args, test_num, schema_class, check_with_lxml): """ Creates a validator test class. @@ -293,8 +293,8 @@ def make_validator_test_class(test_file, test_args, test_num=0, schema_class=Non :param check_with_lxml: if `True` compare with lxml XMLSchema class, reporting anomalies. \ Works only for XSD 1.0 tests. """ - if schema_class is None: - schema_class = XMLSchema + xml_file = os.path.relpath(test_file) + msg_tmpl = "\n\n{}: %s.".format(xml_file) # Extract schema test arguments expected_errors = test_args.errors @@ -305,19 +305,12 @@ def make_validator_test_class(test_file, test_args, test_num=0, schema_class=Non skip_strict = test_args.skip debug_mode = test_args.debug - xml_file = test_file - rel_path = os.path.relpath(test_file) - msg_template = "\n\n{}: %s.".format(rel_path) - class TestValidator(XMLSchemaTestCase): @classmethod def setUpClass(cls): - if debug_mode: - print("\n##\n## Testing %s validation in debug mode.\n##" % rel_path) - pdb.set_trace() - # 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: @@ -327,6 +320,10 @@ def setUpClass(cls): 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): @@ -357,7 +354,7 @@ def check_etree_encode(self, root, converter=None, **kwargs): if converter not in (ParkerConverter, AbderaConverter, JsonMLConverter) and not skip_strict: if debug_mode: pdb.set_trace() - raise AssertionError(str(err) + msg_template % "encoded tree differs from original") + 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: @@ -369,7 +366,7 @@ def check_etree_encode(self, root, converter=None, **kwargs): if sys.version_info >= (3, 6): # For Python < 3.6 cannot ensure attribute decoding order try: - self.assertEqual(data1, data2, msg_template % "re decoded data changed") + self.assertEqual(data1, data2, msg_tmpl % "re decoded data changed") except AssertionError: if debug_mode: pdb.set_trace() @@ -384,7 +381,7 @@ def check_etree_encode(self, root, converter=None, **kwargs): except AssertionError as err: if debug_mode: pdb.set_trace() - raise AssertionError(str(err) + msg_template % "encoded tree differs after second pass") + 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) @@ -402,7 +399,7 @@ def check_json_serialization(self, root, converter=None, **kwargs): 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_template % "serialized data changed at second pass") + 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): @@ -431,7 +428,7 @@ def do_decoding(): do_decoding() self.assertEqual(len(ctx), expected_warnings, "Wrong number of include/import warnings") - self.check_errors(expected_errors) + self.check_errors(xml_file, expected_errors) if not self.chunks: raise ValueError("No decoded object returned!!") @@ -454,17 +451,17 @@ def check_schema_serialization(self): else: chunks.append(obj) - self.assertEqual(len(errors), len(self.errors), msg_template % "wrong number errors") - self.assertEqual(chunks, self.chunks, msg_template % "decoded data differ") + 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_template % "decode() API has a different result") - self.assertEqual(lax_data[0], self.chunks[0], msg_template % "'lax' validation has a different result") - self.assertEqual(skip_data, self.chunks[0], msg_template % "'skip' validation has a different result") + 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() @@ -497,8 +494,8 @@ def check_decoding_and_encoding_with_lxml(self): else: chunks.append(obj) - self.assertEqual(chunks, self.chunks, msg_template % "decode data change with lxml") - self.assertEqual(len(errors), len(self.errors), msg_template % "errors number change with lxml") + 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() @@ -525,23 +522,23 @@ def check_decoding_and_encoding_with_lxml(self): def check_validate_and_is_valid_api(self): if expected_errors: - self.assertFalse(self.schema.is_valid(xml_file), msg_template % "file with errors is valid") + 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_template % "file without errors is not valid") + 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_template % "file without errors not validated") + 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_template % "wrong number of errors (%d expected)" % 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(rel_path, TestValidator.__name__, )) + format(xml_file, TestValidator.__name__, )) else: xml_tree = lxml_etree.parse(xml_file) if self.errors: diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index d80532ca..6039d214 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -12,7 +12,7 @@ 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_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 ..etree import etree_element @@ -144,6 +144,7 @@ def _parse(self): else: if self.schema.validation == 'skip': + # Also generated by meta-schema validation for 'lax' and 'strict' modes self.parse_error("unexpected tag %r for complexType content:" % content_elem.tag, elem) self.content_type = self.schema.create_any_content_group(self) self.attributes = self.schema.create_any_attribute_group(self) diff --git a/xmlschema/validators/schema.py b/xmlschema/validators/schema.py index b1335c02..39af3293 100644 --- a/xmlschema/validators/schema.py +++ b/xmlschema/validators/schema.py @@ -25,7 +25,7 @@ * targetNamespace for restricted element and attributes * TODO: Assert for complex types * TODO: OpenContent and XSD 1.1 wildcards for complex types - * TODO: schema overrides + * schema overrides """ import os from collections import namedtuple From d46f17d08c915c7be1838fd996b860b00ec76b2b Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Sat, 9 Feb 2019 10:26:56 +0100 Subject: [PATCH 04/12] Add _parse_content_tail() to XsdComplexType --- xmlschema/tests/test_schemas.py | 4 ++-- xmlschema/validators/complex_types.py | 27 +++++++++++---------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index 18cc5dda..0cd3777e 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -558,7 +558,7 @@ def check_lxml_schema(self, xmlschema_time): def test_xsd_schema(self): if inspect: SchemaObserver.clear() - self.errors.clear() + del self.errors[:] start_time = time.time() if expected_warnings > 0: @@ -574,7 +574,7 @@ def test_xsd_schema(self): self.check_lxml_schema(xmlschema_time=time.time()-start_time) self.check_errors(xsd_file, expected_errors) - TestSchema.__name__ = TestSchema.__qualname__ = 'TestSchema{0:03}'.format(test_num) + TestSchema.__name__ = TestSchema.__qualname__ = str('TestSchema{0:03}'.format(test_num)) return TestSchema diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index 6039d214..694480eb 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -94,13 +94,13 @@ def _parse(self): # # complexType with empty content self.content_type = self.schema.BUILDERS.group_class(SEQUENCE_ELEMENT, self.schema, self) - self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self) + self._parse_content_tail(elem) elif content_elem.tag in {XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}: # # complexType with child elements self.content_type = self.schema.BUILDERS.group_class(content_elem, self.schema, self) - self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self) + self._parse_content_tail(elem) elif content_elem.tag == XSD_SIMPLE_CONTENT: if 'mixed' in content_elem.attrib: @@ -149,6 +149,9 @@ def _parse(self): self.content_type = self.schema.create_any_content_group(self) self.attributes = self.schema.create_any_attribute_group(self) + def _parse_content_tail(self, elem, **kwargs): + self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self, **kwargs) + def _parse_derivation_elem(self, elem): derivation_elem = self._parse_component(elem, required=False) if getattr(derivation_elem, 'tag', None) not in (XSD_RESTRICTION, XSD_EXTENSION): @@ -192,7 +195,7 @@ def _parse_simple_content_restriction(self, elem, base_type): if base_type.is_simple(): self.parse_error("a complexType ancestor required: %r" % base_type, elem) self.content_type = self.schema.create_any_content_group(self) - self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self) + self._parse_content_tail(elem) else: if base_type.has_simple_content() or base_type.mixed and base_type.is_emptiable(): self.content_type = self.schema.BUILDERS.restriction_class(elem, self.schema, self) @@ -201,9 +204,7 @@ def _parse_simple_content_restriction(self, elem, base_type): "an element-only content type ", base_type.elem) self.content_type = self.schema.create_any_content_group(self) - self.attributes = self.schema.BUILDERS.attribute_group_class( - elem, self.schema, self, derivation='restriction', base_attributes=base_type.attributes - ) + self._parse_content_tail(elem, derivation='restriction', base_attributes=base_type.attributes) def _parse_simple_content_extension(self, elem, base_type): # simpleContent extension: the base type must be a simpleType or a complexType @@ -215,7 +216,7 @@ def _parse_simple_content_extension(self, elem, base_type): if base_type.is_simple(): self.content_type = base_type - self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self) + self._parse_content_tail(elem) else: if base_type.has_simple_content(): self.content_type = base_type.content_type @@ -223,9 +224,7 @@ def _parse_simple_content_extension(self, elem, base_type): self.parse_error("base type %r has not simple content." % base_type, elem) self.content_type = self.schema.create_any_content_group(self) - self.attributes = self.schema.BUILDERS.attribute_group_class( - elem, self.schema, self, derivation='extension', base_attributes=base_type.attributes - ) + self._parse_content_tail(elem, derivation='restriction', base_attributes=base_type.attributes) def _parse_complex_content_restriction(self, elem, base_type): # complexContent restriction: the base type must be a complexType with a complex content. @@ -255,9 +254,7 @@ def _parse_complex_content_restriction(self, elem, base_type): self.parse_error("The derived group %r is not a restriction of the base group." % elem, elem) self.content_type = content_type - self.attributes = self.schema.BUILDERS.attribute_group_class( - elem, self.schema, self, derivation='restriction', base_attributes=base_type.attributes - ) + 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. @@ -303,9 +300,7 @@ def _parse_complex_content_extension(self, elem, base_type): self.content_type = content_type - self.attributes = self.schema.BUILDERS.attribute_group_class( - elem, self.schema, self, derivation='extension', base_attributes=base_type.attributes - ) + self._parse_content_tail(elem, derivation='extension', base_attributes=base_type.attributes) @property def built(self): From d746e4918afb7198d9469fa9cd97e61fa74c6361 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Mon, 11 Feb 2019 17:15:17 +0100 Subject: [PATCH 05/12] Clean and improve facets usage - Removed FACETS, LIST_FACETS, UNION_FACETS from schema classes - Added admitted_facets argument to XsdAtomicBuiltin class --- xmlschema/validators/__init__.py | 5 +- xmlschema/validators/builtins.py | 163 ++++++---- xmlschema/validators/complex_types.py | 4 +- xmlschema/validators/elements.py | 8 +- xmlschema/validators/facets.py | 307 ++++++++++++------ xmlschema/validators/globals_.py | 12 +- .../{constraints.py => identities.py} | 37 ++- xmlschema/validators/schema.py | 54 ++- xmlschema/validators/simple_types.py | 77 ++--- 9 files changed, 399 insertions(+), 268 deletions(-) rename xmlschema/validators/{constraints.py => identities.py} (91%) diff --git a/xmlschema/validators/__init__.py b/xmlschema/validators/__init__.py index b92a142f..b72b722d 100644 --- a/xmlschema/validators/__init__.py +++ b/xmlschema/validators/__init__.py @@ -18,9 +18,8 @@ from .xsdbase import XsdValidator, XsdComponent, XsdAnnotation, XsdType, ParticleMixin, ValidationMixin from .notations import XsdNotation -from .constraints import XsdSelector, XsdFieldSelector, XsdConstraint, XsdKeyref, XsdKey, XsdUnique -from .facets import XSD_10_FACETS, XSD_11_FACETS, STRING_FACETS, BOOLEAN_FACETS, FLOAT_FACETS, DECIMAL_FACETS, \ - DATETIME_FACETS, LIST_FACETS, UNION_FACETS, XsdSingleFacet, XsdPatternsFacet, XsdEnumerationFacet +from .identities import XsdSelector, XsdFieldSelector, XsdIdentity, XsdKeyref, XsdKey, XsdUnique +from .facets import XsdPatternFacets, XsdEnumerationFacets from .wildcards import XsdAnyElement, Xsd11AnyElement, XsdAnyAttribute, Xsd11AnyAttribute from .attributes import XsdAttribute, Xsd11Attribute, XsdAttributeGroup from .simple_types import xsd_simple_type_factory, XsdSimpleType, XsdAtomic, XsdAtomicBuiltin, \ diff --git a/xmlschema/validators/builtins.py b/xmlschema/validators/builtins.py index a6e5a24d..76312a50 100644 --- a/xmlschema/validators/builtins.py +++ b/xmlschema/validators/builtins.py @@ -27,12 +27,38 @@ from ..qnames import * from ..etree import etree_element, is_etree_element from .exceptions import XMLSchemaValidationError -from .facets import XSD_10_FACETS, STRING_FACETS, BOOLEAN_FACETS, FLOAT_FACETS, DECIMAL_FACETS, DATETIME_FACETS +from .facets import XSD_10_FACETS_BUILDERS, XSD_11_FACETS_BUILDERS from .simple_types import XsdSimpleType, XsdAtomicBuiltin HEX_BINARY_PATTERN = re.compile(r'^[0-9a-fA-F]+$') NOT_BASE64_BINARY_PATTERN = re.compile(r'[^0-9a-zA-z+/= \t\n]') +# +# Admitted facets sets for XSD atomic types +STRING_FACETS = ( + XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_PATTERN, + XSD_ENUMERATION, XSD_WHITE_SPACE, XSD_ASSERTION +) + +BOOLEAN_FACETS = (XSD_PATTERN, XSD_WHITE_SPACE, XSD_ASSERTION) + +FLOAT_FACETS = ( + XSD_PATTERN, XSD_ENUMERATION, XSD_WHITE_SPACE, XSD_MAX_INCLUSIVE, + XSD_MAX_EXCLUSIVE, XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, XSD_ASSERTION +) + +DECIMAL_FACETS = ( + XSD_TOTAL_DIGITS, XSD_FRACTION_DIGITS, XSD_PATTERN, XSD_ENUMERATION, + XSD_WHITE_SPACE, XSD_MAX_INCLUSIVE, XSD_MAX_EXCLUSIVE, XSD_MIN_INCLUSIVE, + XSD_MIN_EXCLUSIVE, XSD_ASSERTION +) + +DATETIME_FACETS = ( + XSD_PATTERN, XSD_ENUMERATION, XSD_WHITE_SPACE, + XSD_MAX_INCLUSIVE, XSD_MAX_EXCLUSIVE, XSD_MIN_INCLUSIVE, + XSD_MIN_EXCLUSIVE, XSD_ASSERTION, XSD_EXPLICIT_TIMEZONE +) + # # XSD numerical built-in types validator functions @@ -144,7 +170,8 @@ def python_to_boolean(obj): { 'name': XSD_STRING, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, PRESERVE_WHITE_SPACE_ELEMENT), + 'admitted_facets': STRING_FACETS, + 'facets': [PRESERVE_WHITE_SPACE_ELEMENT], 'value': 'alpha', }, # character string @@ -152,19 +179,22 @@ def python_to_boolean(obj): { 'name': XSD_DECIMAL, 'python_type': (Decimal, str, unicode_type, int, float), - 'facets': (DECIMAL_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DECIMAL_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': Decimal('1.0'), }, # decimal number { 'name': XSD_DOUBLE, 'python_type': float, - 'facets': (FLOAT_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': FLOAT_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': 1.0, }, # 64 bit floating point { 'name': XSD_FLOAT, 'python_type': float, - 'facets': (FLOAT_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': FLOAT_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': 1.0, }, # 32 bit floating point @@ -172,35 +202,40 @@ def python_to_boolean(obj): { 'name': XSD_GDAY, 'python_type': (unicode_type, str, datatypes.GregorianDay), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianDay.fromstring, 'value': datatypes.GregorianDay.fromstring('---31'), }, # DD { 'name': XSD_GMONTH, 'python_type': (unicode_type, str, datatypes.GregorianMonth), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianMonth.fromstring, 'value': datatypes.GregorianMonth.fromstring('--12'), }, # MM { 'name': XSD_GMONTH_DAY, 'python_type': (unicode_type, str, datatypes.GregorianMonthDay), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianMonthDay.fromstring, 'value': datatypes.GregorianMonthDay.fromstring('--12-01'), }, # MM-DD { 'name': XSD_TIME, 'python_type': (unicode_type, str, datatypes.Time), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Time.fromstring, 'value': datatypes.Time.fromstring('09:26:54'), }, # hh:mm:ss { 'name': XSD_DURATION, 'python_type': (unicode_type, str, datatypes.Duration), - 'facets': (FLOAT_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': FLOAT_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Duration.fromstring, 'value': datatypes.Duration.fromstring('P1MT1S'), }, # PnYnMnDTnHnMnS @@ -209,25 +244,29 @@ def python_to_boolean(obj): { 'name': XSD_QNAME, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': STRING_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': 'xs:element', }, # prf:name (the prefix needs to be qualified with an in scope namespace) { 'name': XSD_NOTATION_TYPE, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': STRING_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': 'alpha', }, # type for NOTATION attributes: QNames of xs:notation declarations as value space. { 'name': XSD_ANY_URI, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': STRING_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'value': 'https://example.com', }, # absolute or relative uri (RFC 2396) { 'name': XSD_BOOLEAN, 'python_type': bool, - 'facets': (BOOLEAN_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': BOOLEAN_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': boolean_to_python, 'from_python': python_to_boolean, 'value': True, @@ -235,13 +274,15 @@ def python_to_boolean(obj): { 'name': XSD_BASE64_BINARY, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT, base64_binary_validator), + 'admitted_facets': STRING_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT, base64_binary_validator], 'value': b'YWxwaGE=', }, # base64 encoded binary value { 'name': XSD_HEX_BINARY, 'python_type': (unicode_type, str), - 'facets': (STRING_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT, hex_binary_validator), + 'admitted_facets': STRING_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT, hex_binary_validator], 'value': b'31', }, # hexadecimal encoded binary value @@ -391,28 +432,32 @@ def python_to_boolean(obj): { 'name': XSD_DATETIME, 'python_type': (unicode_type, str, datatypes.DateTime), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.DateTime10.fromstring, 'value': datatypes.DateTime10.fromstring('2000-01-01T12:00:00'), }, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] { 'name': XSD_DATE, 'python_type': (unicode_type, str, datatypes.Date), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Date10.fromstring, 'value': datatypes.Date10.fromstring('2000-01-01'), }, # [-][Y*]YYYY-MM-DD { 'name': XSD_GYEAR, 'python_type': (unicode_type, str, datatypes.GregorianYear), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYear10.fromstring, 'value': datatypes.GregorianYear10.fromstring('1999'), }, # [-][Y*]YYYY { 'name': XSD_GYEAR_MONTH, 'python_type': (unicode_type, str, datatypes.GregorianYearMonth), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYearMonth10.fromstring, 'value': datatypes.GregorianYearMonth10.fromstring('1999-09'), }, # [-][Y*]YYYY-MM @@ -423,28 +468,32 @@ def python_to_boolean(obj): { 'name': XSD_DATETIME, 'python_type': (unicode_type, str, datatypes.DateTime), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.DateTime.fromstring, 'value': datatypes.DateTime.fromstring('2000-01-01T12:00:00'), }, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] { 'name': XSD_DATE, 'python_type': (unicode_type, str, datatypes.Date), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Date.fromstring, 'value': datatypes.Date.fromstring('2000-01-01'), }, # [-][Y*]YYYY-MM-DD { 'name': XSD_GYEAR, 'python_type': (unicode_type, str, datatypes.GregorianYear), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYear10.fromstring, 'value': datatypes.GregorianYear10.fromstring('1999'), }, # [-][Y*]YYYY { 'name': XSD_GYEAR_MONTH, 'python_type': (unicode_type, str, datatypes.GregorianYearMonth), - 'facets': (DATETIME_FACETS, COLLAPSE_WHITE_SPACE_ELEMENT), + 'admitted_facets': DATETIME_FACETS, + 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYearMonth10.fromstring, 'value': datatypes.GregorianYearMonth10.fromstring('1999-09'), }, # [-][Y*]YYYY-MM @@ -473,27 +522,18 @@ def python_to_boolean(obj): ) -def xsd_build_facets(schema, parent, base_type, items): - facets = {} - for obj in items: - if isinstance(obj, (list, tuple, set)): - facets.update([(k, None) for k in obj if k in schema.FACETS]) - elif is_etree_element(obj): - if obj.tag in schema.FACETS: - facets[obj.tag] = schema.FACETS[obj.tag](obj, schema, parent, base_type) - elif callable(obj): - if None in facets: - raise XMLSchemaValueError("Almost one callable for facet group!!") - facets[None] = obj - else: - raise XMLSchemaValueError("Wrong type for item %r" % obj) - return facets - - -def xsd_builtin_types_factory(meta_schema, xsd_types, xsd_class=None): +def xsd_builtin_types_factory(meta_schema, xsd_types, atomic_builtin_class=None): """ Builds the dictionary for XML Schema built-in types mapping. """ + atomic_builtin_class = atomic_builtin_class or XsdAtomicBuiltin + if meta_schema.XSD_VERSION == '1.1': + builtin_types = XSD_11_BUILTIN_TYPES + facets_map = XSD_11_FACETS_BUILDERS + else: + builtin_types = XSD_10_BUILTIN_TYPES + facets_map = XSD_10_FACETS_BUILDERS + # # Special builtin types. # @@ -515,8 +555,7 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, xsd_class=None): elem=etree_element(XSD_SIMPLE_TYPE, attrib={'name': XSD_ANY_SIMPLE_TYPE}), schema=meta_schema, parent=None, - name=XSD_ANY_SIMPLE_TYPE, - facets={k: None for k in XSD_10_FACETS} + name=XSD_ANY_SIMPLE_TYPE ) # xs:anyAtomicType @@ -529,12 +568,6 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, xsd_class=None): base_type=xsd_types[XSD_ANY_SIMPLE_TYPE] ) - xsd_class = xsd_class or XsdAtomicBuiltin - if meta_schema.XSD_VERSION == '1.1': - builtin_types = XSD_11_BUILTIN_TYPES - else: - builtin_types = XSD_10_BUILTIN_TYPES - for item in builtin_types: item = item.copy() name = item['name'] @@ -548,20 +581,26 @@ def xsd_builtin_types_factory(meta_schema, xsd_types, xsd_class=None): if schema is not meta_schema: raise XMLSchemaValueError("loaded entry schema doesn't match meta_schema!") - if item.get('base_type'): + if 'base_type' in item: base_type = item.get('base_type') item['base_type'] = xsd_types[base_type] - elif item.get('item_type'): - base_type = item.get('item_type') - item['item_type'] = xsd_types[base_type] else: base_type = None - if 'facets' in item: - facets = item.pop('facets') - builtin_type = xsd_class(elem, meta_schema, **item) - builtin_type.facets = xsd_build_facets(meta_schema, builtin_type, base_type, facets) - else: - builtin_type = xsd_class(elem, meta_schema, **item) - - xsd_types[item['name']] = builtin_type + facets = item.pop('facets', None) + builtin_type = atomic_builtin_class(elem, meta_schema, **item) + if isinstance(facets, (list, tuple)): + built_facets = builtin_type.facets + for e in facets: + if is_etree_element(e): + cls = facets_map[e.tag] + built_facets[e.tag] = cls(e, meta_schema, builtin_type, base_type) + elif callable(e): + if None in facets: + raise XMLSchemaValueError("Almost one callable for facet group!!") + built_facets[None] = e + else: + raise XMLSchemaValueError("Wrong type for item %r" % e) + builtin_type.facets = built_facets + + xsd_types[name] = builtin_type diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index 694480eb..6eb9210f 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -297,7 +297,6 @@ def _parse_complex_content_extension(self, elem, base_type): 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) - self.content_type = content_type self._parse_content_tail(elem, derivation='extension', base_attributes=base_type.attributes) @@ -538,6 +537,9 @@ def _parse(self): (k, v) for k, v in self.schema.default_attributes.items() if k not in self.attributes ) + def _parse_content_tail(self, elem, **kwargs): + self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self, **kwargs) + @property def default_attributes_apply(self): return get_xml_bool_attribute(self.elem, 'defaultAttributesApply', default=True) diff --git a/xmlschema/validators/elements.py b/xmlschema/validators/elements.py index b3bfab80..b7f5268d 100644 --- a/xmlschema/validators/elements.py +++ b/xmlschema/validators/elements.py @@ -29,7 +29,7 @@ from .exceptions import XMLSchemaValidationError from .xsdbase import XsdComponent, XsdType, ParticleMixin, ValidationMixin -from .constraints import XsdUnique, XsdKey, XsdKeyref +from .identities import XsdUnique, XsdKey, XsdKeyref from .wildcards import XsdAnyElement @@ -96,7 +96,7 @@ def _parse(self): XsdComponent._parse(self) self._parse_attributes() index = self._parse_type() - self._parse_constraints(index) + self._parse_identity_constraints(index) self._parse_substitution_group() def _parse_attributes(self): @@ -169,7 +169,7 @@ def _parse_type(self): self.type = self.maps.lookup_type(XSD_ANY_TYPE) return 0 - def _parse_constraints(self, index=0): + def _parse_identity_constraints(self, index=0): self.constraints = {} for child in self._iterparse_components(self.elem, start=index): if child.tag == XSD_UNIQUE: @@ -597,7 +597,7 @@ def _parse(self): self._parse_attributes() index = self._parse_type() index = self._parse_alternatives(index) - self._parse_constraints(index) + self._parse_identity_constraints(index) self._parse_substitution_group() self._parse_target_namespace() diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index 14fda1bd..b3b8a850 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -36,6 +36,33 @@ def __init__(self, elem, schema, parent, base_type): self.base_type = base_type super(XsdFacet, self).__init__(elem, schema, parent) + def __repr__(self): + return '%s(value=%r, fixed=%r)' % (self.__class__.__name__, self.value, self.fixed) + + def __call__(self, value): + for error in self.validator(value): + yield error + + def _parse(self): + super(XsdFacet, self)._parse() + elem = self.elem + self.fixed = elem.get('fixed', False) + base_facet = self.base_facet + self.base_value = None if base_facet is None else base_facet.value + + try: + self._parse_value(elem) + except (KeyError, ValueError, XMLSchemaDecodeError) as err: + self.value = None + self.parse_error(unicode_type(err)) + else: + if base_facet is not None and base_facet.fixed and \ + base_facet.value is not None and self.value != base_facet.value: + self.parse_error("%r facet value is fixed to %r" % (elem.tag, base_facet.value)) + + def _parse_value(self, elem): + self.value = elem.attrib['value'] + @property def built(self): return self.base_type.is_global or self.base_type.built @@ -47,14 +74,6 @@ def validation_attempted(self): else: return self.base_type.validation_attempted - def __call__(self, value): - for error in self.validator(value): - yield error - - @staticmethod - def validator(_): - return () - @property def base_facet(self): """ @@ -71,40 +90,23 @@ def base_facet(self): else: return None + @staticmethod + def validator(_): + return () -class XsdSingleFacet(XsdFacet): - """ - Class for XSD facets that are singular for each restriction, - the facets for whom the repetition is an error. - The facets of this group are: whiteSpace, length, minLength, - maxLength, minInclusive, minExclusive, maxInclusive, maxExclusive, - totalDigits, fractionDigits. - """ - def _parse(self): - super(XsdFacet, self)._parse() - elem = self.elem - self.fixed = elem.get('fixed', False) - base_facet = self.base_facet - self.base_value = None if base_facet is None else base_facet.value - - try: - self._parse_value(elem) - except (KeyError, ValueError, XMLSchemaDecodeError) as err: - self.value = None - self.parse_error(unicode_type(err)) - else: - if base_facet is not None and base_facet.fixed and \ - base_facet.value is not None and self.value != base_facet.value: - self.parse_error("%r facet value is fixed to %r" % (elem.tag, base_facet.value)) - - def _parse_value(self, elem): - self.value = elem.attrib['value'] - - def __repr__(self): - return '%s(value=%r, fixed=%r)' % (self.__class__.__name__, self.value, self.fixed) +class XsdWhiteSpaceFacet(XsdFacet): + """ + XSD whiteSpace facet. -class XsdWhiteSpaceFacet(XsdSingleFacet): + + Content: (annotation?) + + """ admitted_tags = XSD_WHITE_SPACE, def _parse_value(self, elem): @@ -129,7 +131,18 @@ def collapse_white_space_validator(self, x): yield XMLSchemaValidationError(self, x) -class XsdLengthFacet(XsdSingleFacet): +class XsdLengthFacet(XsdFacet): + """ + XSD length facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_LENGTH, def _parse_value(self, elem): @@ -161,7 +174,18 @@ def base64_length_validator(self, x): yield XMLSchemaValidationError(self, x, "binary length has to be %r." % self.value) -class XsdMinLengthFacet(XsdSingleFacet): +class XsdMinLengthFacet(XsdFacet): + """ + XSD minLength facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MIN_LENGTH, def _parse_value(self, elem): @@ -193,7 +217,18 @@ def base64_min_length_validator(self, x): yield XMLSchemaValidationError(self, x, "binary length cannot be lesser than %r." % self.value) -class XsdMaxLengthFacet(XsdSingleFacet): +class XsdMaxLengthFacet(XsdFacet): + """ + XSD maxLength facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MAX_LENGTH, def _parse_value(self, elem): @@ -225,7 +260,18 @@ def base64_max_length_validator(self, x): yield XMLSchemaValidationError(self, x, "binary length cannot be greater than %r." % self.value) -class XsdMinInclusiveFacet(XsdSingleFacet): +class XsdMinInclusiveFacet(XsdFacet): + """ + XSD minInclusive facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MIN_INCLUSIVE, def _parse_value(self, elem): @@ -237,7 +283,18 @@ def min_inclusive_validator(self, x): yield XMLSchemaValidationError(self, x, "value has to be greater or equal than %r." % self.value) -class XsdMinExclusiveFacet(XsdSingleFacet): +class XsdMinExclusiveFacet(XsdFacet): + """ + XSD minExclusive facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MIN_EXCLUSIVE, def _parse_value(self, elem): @@ -249,7 +306,18 @@ def min_exclusive_validator(self, x): yield XMLSchemaValidationError(self, x, "value has to be greater than %r." % self.value) -class XsdMaxInclusiveFacet(XsdSingleFacet): +class XsdMaxInclusiveFacet(XsdFacet): + """ + XSD maxInclusive facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MAX_INCLUSIVE, def _parse_value(self, elem): @@ -261,7 +329,18 @@ def max_inclusive_validator(self, x): yield XMLSchemaValidationError(self, x, "value has to be lesser or equal than %r." % self.value) -class XsdMaxExclusiveFacet(XsdSingleFacet): +class XsdMaxExclusiveFacet(XsdFacet): + """ + XSD maxExclusive facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_MAX_EXCLUSIVE, def _parse_value(self, elem): @@ -273,7 +352,18 @@ def max_exclusive_validator(self, x): yield XMLSchemaValidationError(self, x, "value has to be lesser than %r." % self.value) -class XsdTotalDigitsFacet(XsdSingleFacet): +class XsdTotalDigitsFacet(XsdFacet): + """ + XSD totalDigits facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_TOTAL_DIGITS, def _parse_value(self, elem): @@ -287,7 +377,18 @@ def total_digits_validator(self, x): yield XMLSchemaValidationError(self, x, "the number of digits is greater than %r." % self.value) -class XsdFractionDigitsFacet(XsdSingleFacet): +class XsdFractionDigitsFacet(XsdFacet): + """ + XSD fractionDigits facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_FRACTION_DIGITS, def __init__(self, elem, schema, parent, base_type): @@ -308,7 +409,18 @@ def fraction_digits_validator(self, x): yield XMLSchemaValidationError(self, x, "the number of fraction digits is greater than %r." % self.value) -class XsdExplicitTimezoneFacet(XsdSingleFacet): +class XsdExplicitTimezoneFacet(XsdFacet): + """ + XSD 1.1 explicitTimezone facet. + + + Content: (annotation?) + + """ admitted_tags = XSD_EXPLICIT_TIMEZONE, def _parse_value(self, elem): @@ -329,15 +441,27 @@ def prohibited_timezone_validator(self, x): yield XMLSchemaValidationError(self, x, "time zone prohibited for value %r." % self.value) -class XsdEnumerationFacet(MutableSequence, XsdFacet): +class XsdEnumerationFacets(MutableSequence, XsdFacet): + """ + Sequence of XSD enumeration facets. Values are validates if match any of enumeration values. + + Content: (annotation?) + + """ admitted_tags = {XSD_ENUMERATION} def __init__(self, elem, schema, parent, base_type): XsdFacet.__init__(self, elem, schema, parent, base_type) + + def _parse(self): + super(XsdFacet, self)._parse() self._elements = [] self.enumeration = [] - self.append(elem) + self.append(self.elem) # Implements the abstract methods of MutableSequence def __getitem__(self, i): @@ -379,14 +503,26 @@ def __call__(self, value): ) -class XsdPatternsFacet(MutableSequence, XsdFacet): +class XsdPatternFacets(MutableSequence, XsdFacet): + """ + Sequence of XSD pattern facets. Values are validates if match any of patterns. + + Content: (annotation?) + + """ admitted_tags = {XSD_PATTERN} def __init__(self, elem, schema, parent, base_type): XsdFacet.__init__(self, elem, schema, parent, base_type) - self._elements = [elem] - value = elem.attrib['value'] + + def _parse(self): + super(XsdFacet, self)._parse() + self._elements = [self.elem] + value = self.elem.attrib['value'] self.regexps = [value] self.patterns = [re.compile(get_python_regex(value))] @@ -423,9 +559,9 @@ def __call__(self, text): yield XMLSchemaValidationError(self, text, reason=msg % self.regexps) -class XsdAssertionsFacet(MutableSequence, XsdFacet): +class XsdAssertionFacets(MutableSequence, XsdFacet): """ - Sequence of XSD simpleType assertions. + Sequence of XSD assertion facets. Values are validates if are `True` with all XPath tests of assertions. # """ -This module contains classes for other XML Schema constraints. +This module contains classes for other XML Schema identity constraints. """ from __future__ import unicode_literals from collections import Counter @@ -23,7 +23,7 @@ from .exceptions import XMLSchemaValidationError from .xsdbase import XsdComponent -XSD_CONSTRAINTS_XPATH_SYMBOLS = { +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', @@ -33,12 +33,12 @@ } -class XsdConstraintXPathParser(XPath1Parser): - symbol_table = {k: v for k, v in XPath1Parser.symbol_table.items() if k in XSD_CONSTRAINTS_XPATH_SYMBOLS} - SYMBOLS = XSD_CONSTRAINTS_XPATH_SYMBOLS +class XsdIdentityXPathParser(XPath1Parser): + symbol_table = {k: v for k, v in XPath1Parser.symbol_table.items() if k in XSD_IDENTITY_XPATH_SYMBOLS} + SYMBOLS = XSD_IDENTITY_XPATH_SYMBOLS -XsdConstraintXPathParser.build_tokenizer() +XsdIdentityXPathParser.build_tokenizer() class XsdSelector(XsdComponent): @@ -56,10 +56,10 @@ def _parse(self): self.path = "*" try: - self.xpath_selector = Selector(self.path, self.namespaces, parser=XsdConstraintXPathParser) + self.xpath_selector = Selector(self.path, self.namespaces, parser=XsdIdentityXPathParser) except ElementPathSyntaxError as err: self.parse_error(err) - self.xpath_selector = Selector('*', self.namespaces, parser=XsdConstraintXPathParser) + self.xpath_selector = Selector('*', self.namespaces, parser=XsdIdentityXPathParser) # XSD 1.1 xpathDefaultNamespace attribute if self.schema.XSD_VERSION > '1.0': @@ -83,12 +83,12 @@ class XsdFieldSelector(XsdSelector): admitted_tags = {XSD_FIELD} -class XsdConstraint(XsdComponent): +class XsdIdentity(XsdComponent): def __init__(self, elem, schema, parent): - super(XsdConstraint, self).__init__(elem, schema, parent) + super(XsdIdentity, self).__init__(elem, schema, parent) def _parse(self): - super(XsdConstraint, self)._parse() + super(XsdIdentity, self)._parse() elem = self.elem try: self.name = get_qname(self.target_namespace, elem.attrib['name']) @@ -193,15 +193,15 @@ def validator(self, elem): yield XMLSchemaValidationError(self, elem, reason="duplicated value %r." % value) -class XsdUnique(XsdConstraint): +class XsdUnique(XsdIdentity): admitted_tags = {XSD_UNIQUE} -class XsdKey(XsdConstraint): +class XsdKey(XsdIdentity): admitted_tags = {XSD_KEY} -class XsdKeyref(XsdConstraint): +class XsdKeyref(XsdIdentity): """ Implementation of xs:keyref. @@ -229,9 +229,9 @@ def _parse(self): def parse_refer(self): if self.refer is None: - return # attribute or key/unique constraint missing - elif isinstance(self.refer, XsdConstraint): - return # referenced key/unique constraint already set + return # attribute or key/unique identity constraint missing + elif isinstance(self.refer, XsdIdentity): + return # referenced key/unique identity constraint already set try: self.refer = self.parent.constraints[self.refer] @@ -239,7 +239,8 @@ def parse_refer(self): try: self.refer = self.maps.constraints[self.refer] except KeyError: - self.parse_error("refer=%r must reference to a key/unique constraint." % self.elem.get('refer')) + self.parse_error("refer=%r must reference to a key/unique identity " + "constraint." % self.elem.get('refer')) self.refer = None else: refer_path = [] diff --git a/xmlschema/validators/schema.py b/xmlschema/validators/schema.py index 39af3293..68c2e1bf 100644 --- a/xmlschema/validators/schema.py +++ b/xmlschema/validators/schema.py @@ -49,10 +49,10 @@ from . import ( XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaNotBuiltError, XMLSchemaIncludeWarning, XMLSchemaImportWarning, XsdValidator, ValidationMixin, XsdComponent, - XsdNotation, XSD_10_FACETS, XSD_11_FACETS, UNION_FACETS, LIST_FACETS, XsdComplexType, - XsdAttribute, XsdElement, XsdAttributeGroup, XsdGroup, XsdAtomicRestriction, XsdAnyElement, - XsdAnyAttribute, xsd_simple_type_factory, Xsd11Attribute, Xsd11Element, Xsd11AnyElement, - Xsd11AnyAttribute, Xsd11AtomicRestriction, Xsd11ComplexType, Xsd11Group, XsdGlobals + XsdNotation, XsdComplexType, XsdAttribute, XsdElement, XsdAttributeGroup, XsdGroup, Xsd11Group, + XsdAnyElement, XsdAnyAttribute, Xsd11Attribute, Xsd11Element, Xsd11AnyElement, XsdGlobals, + Xsd11AnyAttribute, Xsd11ComplexType, xsd_simple_type_factory, + XsdAtomicRestriction, Xsd11AtomicRestriction ) from .globals_ import iterchildren_xsd_import, iterchildren_xsd_include, \ iterchildren_xsd_redefine, iterchildren_xsd_override @@ -97,16 +97,10 @@ def get_attribute(attr, *args): if xsd_version not in ('1.0', '1.1'): raise XMLSchemaValueError("Validator class XSD version must be '1.0' or '1.1', not %r." % xsd_version) - facets = dict_.get('FACETS') or get_attribute('FACETS', *bases) - if not isinstance(facets, dict): - raise XMLSchemaValueError("Validator class FACETS must be a dict(), not %r." % type(facets)) - dict_['LIST_FACETS'] = set(facets).intersection(LIST_FACETS) - dict_['UNION_FACETS'] = set(facets).intersection(UNION_FACETS) - builders = dict_.get('BUILDERS') or get_attribute('BUILDERS', *bases) if isinstance(builders, dict): dict_['BUILDERS'] = namedtuple('Builders', builders)(**builders) - dict_['TAG_MAP'] = { + dict_['BUILDERS_MAP'] = { XSD_NOTATION: builders['notation_class'], XSD_SIMPLE_TYPE: builders['simple_type_factory'], XSD_COMPLEX_TYPE: builders['complex_type_class'], @@ -116,9 +110,9 @@ def get_attribute(attr, *args): XSD_ELEMENT: builders['element_class'], } elif builders is None: - raise XMLSchemaValueError("Validator class doesn't have defined builders.") - elif get_attribute('TAG_MAP', *bases) is None: - raise XMLSchemaValueError("Validator class doesn't have a defined tag map.") + raise XMLSchemaValueError("Validator class doesn't have defined XSD builders.") + 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): @@ -183,8 +177,17 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin): :cvar XSD_VERSION: store the XSD version (1.0 or 1.1). :vartype XSD_VERSION: str + :cvar BUILDERS: a namedtuple with attributes related to schema components classes. \ + Used for build local components within parsing methods. + :vartype BUILDERS: namedtuple + :cvar BUILDERS_MAP: a dictionary that maps from tag to class for XSD global components. \ + Used for build global components within lookup functions. + :vartype BUILDERS_MAP: dict + :cvar BASE_SCHEMAS: a dictionary from namespace to schema resource for meta-schema bases. + :vartype BASE_SCHEMAS: dict :cvar meta_schema: the XSD meta-schema instance. :vartype meta_schema: XMLSchema + :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 \ with the schema. In this case the schema declarations can be reused from other namespaces as \ @@ -218,16 +221,10 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin): :vartype elements: NamespaceView """ XSD_VERSION = None - - FACETS = None - LIST_FACETS = None - UNION_FACETS = None BUILDERS = None - TAG_MAP = None - - meta_schema = None + BUILDERS_MAP = None BASE_SCHEMAS = None - _parent_map = 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): @@ -466,17 +463,6 @@ def target_prefix(self): return prefix return '' - @property - def parent_map(self): - warnings.warn( - "This property will be removed in future versions. " - "Use the 'parent' attribute of the element instead.", - DeprecationWarning, stacklevel=2 - ) - if self._parent_map is None: - self._parent_map = {e: p for p in self.iter() for e in p.iterchildren()} - return self._parent_map - @classmethod def builtin_types(cls): """An accessor for XSD built-in types.""" @@ -904,7 +890,6 @@ class XMLSchema10(XMLSchemaBase): """ XSD_VERSION = '1.0' - FACETS = XSD_10_FACETS BUILDERS = { 'notation_class': XsdNotation, 'complex_type_class': XsdComplexType, @@ -962,7 +947,6 @@ class XMLSchema11(XMLSchemaBase): """ XSD_VERSION = '1.1' - FACETS = XSD_11_FACETS BUILDERS = { 'notation_class': XsdNotation, 'complex_type_class': Xsd11ComplexType, diff --git a/xmlschema/validators/simple_types.py b/xmlschema/validators/simple_types.py index c76017c3..91a1053b 100644 --- a/xmlschema/validators/simple_types.py +++ b/xmlschema/validators/simple_types.py @@ -26,7 +26,8 @@ from .exceptions import XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaDecodeError, XMLSchemaParseError from .xsdbase import XsdAnnotation, XsdType, ValidationMixin -from .facets import XsdFacet, XSD_10_FACETS +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 def xsd_simple_type_factory(elem, schema, parent): @@ -51,7 +52,7 @@ def xsd_simple_type_factory(elem, schema, parent): return schema.maps.lookup_type(XSD_ANY_SIMPLE_TYPE) if child.tag == XSD_RESTRICTION: - result = XsdAtomicRestriction(child, schema, parent, name=name) + 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: @@ -82,15 +83,9 @@ def __init__(self, elem, schema, parent, name=None, facets=None): super(XsdSimpleType, self).__init__(elem, schema, parent, name) if not hasattr(self, 'facets'): self.facets = facets or {} - elif facets: - for k, v in self.facets: - if k not in facets: - facets[k] = v - self.facets = facets def __setattr__(self, name, value): if name == 'facets': - assert isinstance(value, dict), "A dictionary is required for attribute 'facets'." super(XsdSimpleType, self).__setattr__(name, value) try: self.min_length, self.max_length, self.min_value, self.max_value = self.check_facets(value) @@ -111,12 +106,12 @@ def __setattr__(self, name, value): super(XsdSimpleType, self).__setattr__(name, value) @property - def built(self): - return True + def admitted_facets(self): + return XSD_10_FACETS if self.schema.XSD_VERSION == '1.0' else XSD_11_FACETS @property - def admitted_facets(self): - return set(self.schema.FACETS) + def built(self): + return True @property def final(self): @@ -154,11 +149,9 @@ def check_facets(self, facets): :returns Min and max values, a `None` value means no min/max limit. """ # Checks the applicability of the facets - admitted_facets = self.admitted_facets - if not admitted_facets.issuperset(set([k for k in facets if k is not None])): - admitted_facets = {local_name(e) for e in admitted_facets if e} + 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 % admitted_facets) + 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)} @@ -360,17 +353,9 @@ def validation_attempted(self): @property def admitted_facets(self): primitive_type = self.primitive_type - if isinstance(primitive_type, (XsdList, XsdUnion)): - return primitive_type.admitted_facets - try: - facets = set(primitive_type.facets.keys()) - except AttributeError: - return set(XSD_10_FACETS).union({None}) - else: - try: - return set(self.schema.FACETS).intersection(facets) - except AttributeError: - return set(primitive_type.facets.keys()).union({None}) + if primitive_type is None or primitive_type.is_complex(): + return XSD_10_FACETS if self.schema.XSD_VERSION == '1.0' else XSD_11_FACETS + return primitive_type.admitted_facets @property def primitive_type(self): @@ -412,13 +397,14 @@ class XsdAtomicBuiltin(XsdAtomic): - to_python(value): Decoding from XML - from_python(value): Encoding to XML """ - def __init__(self, elem, schema, name, python_type, base_type=None, facets=None, + def __init__(self, elem, schema, name, python_type, base_type=None, admitted_facets=None, facets=None, to_python=None, from_python=None, value=None): """ :param name: the XSD type's qualified name. :param python_type: the correspondent Python's type. If a tuple or list of types \ is provided uses the first and consider the others as compatible types. :param base_type: the reference base type, None if it's a primitive type. + :param admitted_facets: admitted facets tags for type (required for primitive types). :param facets: optional facets validators. :param to_python: optional decode function. :param from_python: optional encode function. @@ -431,6 +417,10 @@ def __init__(self, elem, schema, name, python_type, base_type=None, facets=None, if not callable(python_type): raise XMLSchemaTypeError("%r object is not callable" % python_type.__class__) + if base_type is None and not admitted_facets: + raise XMLSchemaValueError("argument 'admitted_facets' must be a not empty set of a primitive type") + self._admitted_facets = admitted_facets + super(XsdAtomicBuiltin, self).__init__(elem, schema, None, name, facets, base_type) self.python_type = python_type self.to_python = to_python or python_type @@ -441,7 +431,11 @@ def __repr__(self): return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name) def _parse(self): - return + pass + + @property + def admitted_facets(self): + return self._admitted_facets or self.primitive_type.admitted_facets def iter_decode(self, obj, validation='lax', **kwargs): if isinstance(obj, (string_base_type, bytes)): @@ -453,7 +447,7 @@ def iter_decode(self, obj, validation='lax', **kwargs): if validation == 'skip': try: yield self.to_python(obj) - except (ValueError, DecimalException) as err: + except (ValueError, DecimalException): yield unicode_type(obj) return @@ -573,7 +567,6 @@ def __setattr__(self, name, value): def _parse(self): super(XsdList, self)._parse() elem = self.elem - base_type = None child = self._parse_component(elem, required=False) if child is not None: @@ -605,6 +598,10 @@ def _parse(self): self.parse_error(str(err), elem) self.base_type = self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE) + @property + def admitted_facets(self): + return XSD_10_LIST_FACETS if self.schema.XSD_VERSION == '1.0' else XSD_11_LIST_FACETS + @property def item_type(self): return self.base_type @@ -620,10 +617,6 @@ def validation_attempted(self): else: return self.base_type.validation_attempted - @property - def admitted_facets(self): - return self.schema.LIST_FACETS - @staticmethod def is_atomic(): return False @@ -773,6 +766,10 @@ def _parse(self): self.parse_error(str(err), elem) self.member_types = [self.maps.lookup_type(XSD_ANY_ATOMIC_TYPE)] + @property + def admitted_facets(self): + return XSD_10_UNION_FACETS if self.schema.XSD_VERSION == '1.0' else XSD_11_UNION_FACETS + @property def built(self): return all([mt.is_global or mt.built for mt in self.member_types]) @@ -786,10 +783,6 @@ def validation_attempted(self): else: return 'none' - @property - def admitted_facets(self): - return self.schema.UNION_FACETS - def is_atomic(self): return all(mt.is_atomic() for mt in self.member_types) @@ -933,6 +926,8 @@ class XsdAtomicRestriction(XsdAtomic): enumeration | whiteSpace | pattern)*)) """ + FACETS_BUILDERS = XSD_10_FACETS_BUILDERS + 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: @@ -1003,7 +998,7 @@ def _parse(self): has_simple_type_child = True else: try: - facet_class = self.schema.FACETS[child.tag] + facet_class = self.FACETS_BUILDERS[child.tag] except KeyError: self.parse_error("unexpected tag %r in restriction:" % child.tag) continue @@ -1139,4 +1134,4 @@ class Xsd11AtomicRestriction(XsdAtomicRestriction): {any with namespace: ##other})*)) """ - pass + FACETS_BUILDERS = XSD_11_FACETS_BUILDERS From 7d71d27011dad958509de24c331f86c213e09029 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Mon, 11 Feb 2019 17:50:55 +0100 Subject: [PATCH 06/12] Add XsdAssertion class in new module assertions.py --- xmlschema/validators/assertions.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 xmlschema/validators/assertions.py diff --git a/xmlschema/validators/assertions.py b/xmlschema/validators/assertions.py new file mode 100644 index 00000000..8bd7c49a --- /dev/null +++ b/xmlschema/validators/assertions.py @@ -0,0 +1,84 @@ +# -*- 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 +# +from __future__ import unicode_literals +from elementpath import XPath2Parser, XPathContext, XMLSchemaProxy, ElementPathSyntaxError + +from ..etree import etree_element +from ..qnames import XSD_ASSERT +from ..helpers import get_xpath_default_namespace + +from .exceptions import XMLSchemaValidationError +from .xsdbase import XsdComponent + + +class XsdAssertion(XsdComponent): + """ + Class for XSD 'assert' constraint declaration. + + + Content: (annotation?) + + """ + admitted_tags = {XSD_ASSERT} + + def __init__(self, elem, schema, parent, base_type): + self.base_type = base_type + super(XsdAssertion, self).__init__(self, elem, schema, parent) + + @property + def built(self): + return self.base_type.is_global or self.base_type.built + + def _parse(self): + super(XsdAssertion, self)._parse() + self.path, self.root = self._parse_assertion(self.elem) + + def _parse_assertion(self, elem): + try: + path = elem.attrib['test'] + except KeyError as err: + self.parse_error(str(err), elem=elem) + path = 'true()' + + try: + default_namespace = get_xpath_default_namespace(elem, self.namespaces[''], self.target_namespace) + except ValueError as err: + self.parse_error(str(err), elem=elem) + parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema)) + else: + parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema), + default_namespace=default_namespace) + + try: + root_token = parser.parse(path) + except ElementPathSyntaxError as err: + self.parse_error(err, elem=elem) + return path, parser.parse('true()') + + primitive_type = self.base_type.primitive_type + context = XPathContext(root=etree_element('root'), variables={'value': primitive_type.value}) + try: + root_token.evaluate(context) + except (TypeError, ValueError) as err: + self.parse_error(err, elem=elem) + return path, parser.parse('true()') + else: + return path, root_token + + def __call__(self, elem): + context = XPathContext(root=elem) + if not self.root.evaluate(context): + msg = "expression is not true with test path %r." + yield XMLSchemaValidationError(self, obj=elem, reason=msg % self.path) From 9cc61e8788e0d62cd06dbbd8f889b8e2d2e508ec Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Fri, 22 Feb 2019 22:50:05 +0100 Subject: [PATCH 07/12] Rewrite assertions implementation --- requirements-dev.txt | 2 +- setup.py | 2 +- tox.ini | 4 +- xmlschema/tests/test_schemas.py | 17 +++- xmlschema/validators/__init__.py | 1 + xmlschema/validators/assertions.py | 123 +++++++++++++++++++------- xmlschema/validators/builtins.py | 8 +- xmlschema/validators/complex_types.py | 18 +++- xmlschema/validators/facets.py | 70 ++++----------- xmlschema/validators/globals_.py | 5 +- xmlschema/validators/simple_types.py | 1 + 11 files changed, 155 insertions(+), 96 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fc7888ff..c8d83a2c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ # Requirements for setup a development environment for the xmlschema package. setuptools tox -elementpath>=1.1.2 +elementpath>=1.1.4 lxml memory_profiler pathlib2 # For Py27 tests on resources diff --git a/setup.py b/setup.py index f576e0cc..2db95eab 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='xmlschema', version='1.0.10', - install_requires=['elementpath>=1.1.2'], + install_requires=['elementpath==1.1.4'], packages=['xmlschema'], include_package_data=True, author='Davide Brunato', diff --git a/tox.ini b/tox.ini index 45929fc9..155e7f77 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,12 @@ toxworkdir = {homedir}/.tox/xmlschema [testenv] deps = lxml - elementpath>=1.1.2 + elementpath>=1.1.4 commands = python xmlschema/tests/test_all.py {posargs} [testenv:py27] deps = lxml - elementpath>=1.1.2 + elementpath>=1.1.4 pathlib2 commands = python xmlschema/tests/test_all.py {posargs} diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index 0cd3777e..f8cbdba2 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -23,7 +23,7 @@ import xmlschema from xmlschema import XMLSchemaBase, XMLSchema, XMLSchemaParseError, XMLSchemaIncludeWarning, XMLSchemaImportWarning from xmlschema.compat import PY3, unicode_type -from xmlschema.etree import lxml_etree, py_etree_element +from xmlschema.etree import lxml_etree, etree_element, py_etree_element from xmlschema.qnames import XSD_LIST, XSD_UNION from xmlschema.tests import tests_factory, SKIP_REMOTE_TESTS, SchemaObserver, XMLSchemaTestCase from xmlschema.validators import XsdValidator, XMLSchema11 @@ -459,6 +459,21 @@ def test_assertion_facet(self): self.assertFalse(schema.types['Percentage'].is_valid('101')) self.assertFalse(schema.types['Percentage'].is_valid('90.1')) + def test_complex_type_assertion(self): + schema = self.check_schema(""" + + + + + """) + + xsd_type = schema.types['intRange'] + self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '10', 'max': '19'}))) + self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '19', 'max': '19'}))) + import pdb + pdb.set_trace() + self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '19'}))) + def make_schema_test_class(test_file, test_args, test_num, schema_class, check_with_lxml): """ diff --git a/xmlschema/validators/__init__.py b/xmlschema/validators/__init__.py index b72b722d..9d78bd5b 100644 --- a/xmlschema/validators/__init__.py +++ b/xmlschema/validators/__init__.py @@ -17,6 +17,7 @@ from .xsdbase import XsdValidator, XsdComponent, XsdAnnotation, XsdType, ParticleMixin, ValidationMixin +from .assertions import XsdAssert from .notations import XsdNotation from .identities import XsdSelector, XsdFieldSelector, XsdIdentity, XsdKeyref, XsdKey, XsdUnique from .facets import XsdPatternFacets, XsdEnumerationFacets diff --git a/xmlschema/validators/assertions.py b/xmlschema/validators/assertions.py index 8bd7c49a..2641ce0c 100644 --- a/xmlschema/validators/assertions.py +++ b/xmlschema/validators/assertions.py @@ -11,9 +11,9 @@ from __future__ import unicode_literals from elementpath import XPath2Parser, XPathContext, XMLSchemaProxy, ElementPathSyntaxError -from ..etree import etree_element -from ..qnames import XSD_ASSERT +from ..qnames import XSD_ASSERTION, XSD_ASSERT from ..helpers import get_xpath_default_namespace +from ..xpath import ElementPathMixin from .exceptions import XMLSchemaValidationError from .xsdbase import XsdComponent @@ -21,64 +21,125 @@ class XsdAssertion(XsdComponent): """ - Class for XSD 'assert' constraint declaration. + XSD 1.1 assertion facet for simpleType definitions. - Content: (annotation?) - + """ - admitted_tags = {XSD_ASSERT} + admitted_tags = {XSD_ASSERTION} def __init__(self, elem, schema, parent, base_type): self.base_type = base_type - super(XsdAssertion, self).__init__(self, elem, schema, parent) + super(XsdAssertion, self).__init__(elem, schema, parent) + if not self.base_type.is_simple() or not self.base_type.is_atomic(): + self.parse_error("base_type={!r} is not a simpleType restriction", elem=self.elem) + self.path = 'true()' @property def built(self): - return self.base_type.is_global or self.base_type.built + return self.token is not None and (self.base_type.is_global or self.base_type.built) def _parse(self): super(XsdAssertion, self)._parse() - self.path, self.root = self._parse_assertion(self.elem) - - def _parse_assertion(self, elem): try: - path = elem.attrib['test'] + self.path = self.elem.attrib['test'] except KeyError as err: - self.parse_error(str(err), elem=elem) - path = 'true()' + self.parse_error(str(err), elem=self.elem) + self.path = 'true()' + + variables= {'value': self.base_type.primitive_type.value} try: - default_namespace = get_xpath_default_namespace(elem, self.namespaces[''], self.target_namespace) + default_namespace = get_xpath_default_namespace(self.elem, self.namespaces[''], self.target_namespace) except ValueError as err: - self.parse_error(str(err), elem=elem) - parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema)) + self.parse_error(str(err), elem=self.elem) + self.parser = XPath2Parser(self.namespaces, strict=False, variables=variables) else: - parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema), - default_namespace=default_namespace) + self.parser = XPath2Parser( + self.namespaces, strict=False, default_namespace=default_namespace, variables=variables + ) try: - root_token = parser.parse(path) - except ElementPathSyntaxError as err: - self.parse_error(err, elem=elem) - return path, parser.parse('true()') + self.token = self.parser.parse(self.path) + except (ElementPathSyntaxError, TypeError) as err: + self.parse_error(err, elem=self.elem) + self.token = self.parser.parse('true()') + + def __call__(self, value): + self.parser.variables['value'] = value + if not self.token.evaluate(): + msg = "value is not true with test path %r." + yield XMLSchemaValidationError(self, value, reason=msg % self.path) + + +class XsdAssert(XsdComponent, ElementPathMixin): + """ + Class for XSD 'assert' constraint declaration. - primitive_type = self.base_type.primitive_type - context = XPathContext(root=etree_element('root'), variables={'value': primitive_type.value}) + + Content: (annotation?) + + """ + admitted_tags = {XSD_ASSERT} + token = None + + def __init__(self, elem, schema, parent, base_type): + self.base_type = base_type + super(XsdAssert, self).__init__(elem, schema, parent) + if not self.base_type.is_complex(): + self.parse_error("base_type={!r} is not a complexType definition", elem=self.elem) + self.path = 'true()' + + def _parse(self): + super(XsdAssert, self)._parse() try: - root_token.evaluate(context) - except (TypeError, ValueError) as err: - self.parse_error(err, elem=elem) - return path, parser.parse('true()') + self.path = self.elem.attrib['test'] + except KeyError as err: + self.parse_error(str(err), elem=self.elem) + self.path = 'true()' + + try: + default_namespace = get_xpath_default_namespace(self.elem, self.namespaces[''], self.target_namespace) + except ValueError as err: + self.parse_error(str(err), elem=self.elem) + self.parser = XPath2Parser(self.namespaces, strict=False) else: - return path, root_token + self.parser = XPath2Parser(self.namespaces, strict=False, default_namespace=default_namespace) + + @property + def built(self): + return self.token is not None and (self.base_type.is_global or self.base_type.built) + + def parse(self): + self.parser.schema = XMLSchemaProxy(self.schema, self) + try: + self.token = self.parser.parse(self.path) + except ElementPathSyntaxError as err: + self.parse_error(err, elem=self.elem) + self.token = self.parser.parse('true()') def __call__(self, elem): + # TODO: protect from dynamic errors ... context = XPathContext(root=elem) - if not self.root.evaluate(context): + if not self.token.evaluate(context): msg = "expression is not true with test path %r." yield XMLSchemaValidationError(self, obj=elem, reason=msg % self.path) + + # For implementing ElementPathMixin + def __iter__(self): + if not self.parent.has_simple_content(): + for e in self.parent.content_type.iter_subelements(): + yield e + + @property + def attrib(self): + return self.parent.attributes diff --git a/xmlschema/validators/builtins.py b/xmlschema/validators/builtins.py index 76312a50..6f62c451 100644 --- a/xmlschema/validators/builtins.py +++ b/xmlschema/validators/builtins.py @@ -486,16 +486,16 @@ def python_to_boolean(obj): 'python_type': (unicode_type, str, datatypes.GregorianYear), 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'to_python': datatypes.GregorianYear10.fromstring, - 'value': datatypes.GregorianYear10.fromstring('1999'), + 'to_python': datatypes.GregorianYear.fromstring, + 'value': datatypes.GregorianYear.fromstring('1999'), }, # [-][Y*]YYYY { 'name': XSD_GYEAR_MONTH, 'python_type': (unicode_type, str, datatypes.GregorianYearMonth), 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'to_python': datatypes.GregorianYearMonth10.fromstring, - 'value': datatypes.GregorianYearMonth10.fromstring('1999-09'), + 'to_python': datatypes.GregorianYearMonth.fromstring, + 'value': datatypes.GregorianYearMonth.fromstring('1999-09'), }, # [-][Y*]YYYY-MM # --- Datetime derived types (XSD 1.1) --- { diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index 6eb9210f..162e616e 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -15,9 +15,11 @@ 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 ..etree import etree_element +from ..xpath import ElementPathMixin from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError from .xsdbase import XsdType, ValidationMixin +from .assertions import XsdAssert from .attributes import XsdAttributeGroup from .simple_types import XsdSimpleType from .groups import XsdGroup @@ -44,6 +46,7 @@ class XsdComplexType(XsdType, ValidationMixin): """ admitted_tags = {XSD_COMPLEX_TYPE, XSD_RESTRICTION} + assertions = () def __init__(self, elem, schema, parent, name=None, content_type=None, attributes=None, mixed=None): self.base_type = None @@ -259,7 +262,7 @@ def _parse_complex_content_restriction(self, elem, base_type): 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': + 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) group_elem = self._parse_component(elem, required=False, strict=False) @@ -383,6 +386,10 @@ def iter_components(self, xsd_classes=None): for obj in self.content_type.iter_components(xsd_classes): yield obj + for obj in self.assertions: + if xsd_classes is None or isinstance(obj, xsd_classes): + yield obj + @staticmethod def get_facet(*_args, **_kwargs): return None @@ -433,6 +440,11 @@ def iter_decode(self, elem, validation='lax', converter=None, **kwargs): :return: yields a 3-tuple (simple content, complex content, attributes) containing \ the decoded parts, eventually preceded by a sequence of validation or decoding errors. """ + # XSD 1.1 assertions + for assertion in self.assertions: + for error in assertion(elem): + yield self.validation_error(validation, error, **kwargs) + for result in self.attributes.iter_decode(elem.attrib, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield result @@ -539,6 +551,10 @@ def _parse(self): def _parse_content_tail(self, elem, **kwargs): self.attributes = self.schema.BUILDERS.attribute_group_class(elem, self.schema, self, **kwargs) + self.assertions = [] + for child in self._iterparse_components(elem): + if child.tag == XSD_ASSERT: + self.assertions.append(XsdAssert(child, self.schema, self, self)) @property def default_attributes_apply(self): diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index b3b8a850..8f2c6ebd 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -20,12 +20,12 @@ 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 -from ..helpers import get_xpath_default_namespace from ..etree import etree_element from ..regex import get_python_regex from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError from .xsdbase import XsdComponent +from .assertions import XsdAssertion class XsdFacet(XsdComponent): @@ -554,7 +554,7 @@ def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.regexps) def __call__(self, text): - if all(pattern.search(text) is None for pattern in self.patterns): + if all(pattern.match(text) is None for pattern in self.patterns): msg = "value doesn't match any pattern of %r." yield XMLSchemaValidationError(self, text, reason=msg % self.regexps) @@ -575,76 +575,38 @@ class XsdAssertionFacets(MutableSequence, XsdFacet): def __init__(self, elem, schema, parent, base_type): XsdFacet.__init__(self, elem, schema, parent, base_type) + self._assertions = [XsdAssertion(elem, schema, parent, base_type)] def _parse(self): - super(XsdFacet, self)._parse() - self._elements = [self.elem] - path, root = self._parse_assertion(self.elem) - self.paths = [path] - self.tokens = [root] - - def _parse_assertion(self, elem): - try: - path = elem.attrib['test'] - except KeyError as err: - self.parse_error(str(err), elem=elem) - path = 'true()' - - try: - default_namespace = get_xpath_default_namespace(elem, self.namespaces[''], self.target_namespace) - except ValueError as err: - self.parse_error(str(err), elem=elem) - parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema)) - else: - parser = XPath2Parser(self.namespaces, strict=False, schema=XMLSchemaProxy(self.schema.meta_schema), - default_namespace=default_namespace) - - try: - root_token = parser.parse(path) - except ElementPathSyntaxError as err: - self.parse_error(err, elem=elem) - return path, parser.parse('true()') + return - primitive_type = self.base_type.primitive_type - context = XPathContext(root=etree_element('root'), variables={'value': primitive_type.value}) - try: - root_token.evaluate(context) - except (TypeError, ValueError) as err: - self.parse_error(err, elem=elem) - return path, parser.parse('true()') - else: - return path, root_token + @property + def paths(self): + return [assertion.path for assertion in self._assertions] # Implements the abstract methods of MutableSequence def __getitem__(self, i): - return self._elements[i] + return self._assertions[i] def __setitem__(self, i, elem): - self._elements[i] = elem - self.paths[i], self.tokens[i] = self._parse_assertion(elem) + self._assertions[i] = XsdAssertion(elem, self.schema, self.parent, self.base_type) def __delitem__(self, i): - del self._elements[i] - del self.paths[i] - del self.tokens[i] + del self._assertions[i] def __len__(self): - return len(self._elements) + return len(self._assertions) def insert(self, i, elem): - self._elements.insert(i, elem) - path, root = self._parse_assertion(elem) - self.paths.insert(i, path) - self.tokens.insert(i, root) + self._assertions.insert(i, XsdAssertion(elem, self.schema, self.parent, self.base_type)) def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.paths) + return '%s(paths=%r)' % (self.__class__.__name__, self.paths) def __call__(self, value): - context = XPathContext(root=etree_element('root'), variables={'value': value}) - if not all(token.evaluate(context) for token in self.tokens): - msg = "value is not true with all expressions of %r." - yield XMLSchemaValidationError(self, value, reason=msg % self.paths) + for assertion in self: + for error in assertion(value): + yield error XSD_10_FACETS_BUILDERS = { diff --git a/xmlschema/validators/globals_.py b/xmlschema/validators/globals_.py index a88f6ec2..937e0efd 100644 --- a/xmlschema/validators/globals_.py +++ b/xmlschema/validators/globals_.py @@ -23,7 +23,7 @@ from ..namespaces import NamespaceResourcesMap from . import XMLSchemaNotBuiltError, XsdValidator, XsdKeyref, XsdComponent, XsdAttribute, \ - XsdSimpleType, XsdComplexType, XsdElement, XsdAttributeGroup, XsdGroup, XsdNotation + XsdSimpleType, XsdComplexType, XsdElement, XsdAttributeGroup, XsdGroup, XsdNotation, XsdAssert from .builtins import xsd_builtin_types_factory @@ -425,6 +425,9 @@ def build(self): 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': diff --git a/xmlschema/validators/simple_types.py b/xmlschema/validators/simple_types.py index 91a1053b..bc67de53 100644 --- a/xmlschema/validators/simple_types.py +++ b/xmlschema/validators/simple_types.py @@ -23,6 +23,7 @@ XSD_RESTRICTION, XSD_ANNOTATION, XSD_ASSERTION ) from ..helpers import get_qname, local_name, prefixed_to_qname, get_xsd_component, get_xsd_derivation_attribute +from ..xpath import ElementPathMixin from .exceptions import XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaDecodeError, XMLSchemaParseError from .xsdbase import XsdAnnotation, XsdType, ValidationMixin From a341721d59f178c6e071b1d31534e0439912f88d Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Sat, 23 Feb 2019 09:10:06 +0100 Subject: [PATCH 08/12] Modify assertion facets - Move XsdAssertion to XsdAssertionFacet - Removed XsdAssertionFacets - Refactoring of XSD facets parsing --- xmlschema/validators/assertions.py | 60 +----------- xmlschema/validators/facets.py | 135 +++++++++++++++------------ xmlschema/validators/simple_types.py | 26 ++++-- 3 files changed, 95 insertions(+), 126 deletions(-) diff --git a/xmlschema/validators/assertions.py b/xmlschema/validators/assertions.py index 2641ce0c..660467b3 100644 --- a/xmlschema/validators/assertions.py +++ b/xmlschema/validators/assertions.py @@ -11,7 +11,7 @@ from __future__ import unicode_literals from elementpath import XPath2Parser, XPathContext, XMLSchemaProxy, ElementPathSyntaxError -from ..qnames import XSD_ASSERTION, XSD_ASSERT +from ..qnames import XSD_ASSERT from ..helpers import get_xpath_default_namespace from ..xpath import ElementPathMixin @@ -19,64 +19,6 @@ from .xsdbase import XsdComponent -class XsdAssertion(XsdComponent): - """ - XSD 1.1 assertion facet for simpleType definitions. - - - Content: (annotation?) - - """ - admitted_tags = {XSD_ASSERTION} - - def __init__(self, elem, schema, parent, base_type): - self.base_type = base_type - super(XsdAssertion, self).__init__(elem, schema, parent) - if not self.base_type.is_simple() or not self.base_type.is_atomic(): - self.parse_error("base_type={!r} is not a simpleType restriction", elem=self.elem) - self.path = 'true()' - - @property - def built(self): - return self.token is not None and (self.base_type.is_global or self.base_type.built) - - def _parse(self): - super(XsdAssertion, self)._parse() - try: - self.path = self.elem.attrib['test'] - except KeyError as err: - self.parse_error(str(err), elem=self.elem) - self.path = 'true()' - - variables= {'value': self.base_type.primitive_type.value} - - try: - default_namespace = get_xpath_default_namespace(self.elem, self.namespaces[''], self.target_namespace) - except ValueError as err: - self.parse_error(str(err), elem=self.elem) - self.parser = XPath2Parser(self.namespaces, strict=False, variables=variables) - else: - self.parser = XPath2Parser( - self.namespaces, strict=False, default_namespace=default_namespace, variables=variables - ) - - try: - self.token = self.parser.parse(self.path) - except (ElementPathSyntaxError, TypeError) as err: - self.parse_error(err, elem=self.elem) - self.token = self.parser.parse('true()') - - def __call__(self, value): - self.parser.variables['value'] = value - if not self.token.evaluate(): - msg = "value is not true with test path %r." - yield XMLSchemaValidationError(self, value, reason=msg % self.path) - - class XsdAssert(XsdComponent, ElementPathMixin): """ Class for XSD 'assert' constraint declaration. diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index 8f2c6ebd..d301bb97 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -13,19 +13,18 @@ """ from __future__ import unicode_literals import re -from elementpath import XMLSchemaProxy, XPath2Parser, XPathContext, ElementPathSyntaxError +from elementpath import XPath2Parser, ElementPathSyntaxError, ElementPathTypeError from ..compat import unicode_type, MutableSequence +from ..helpers import get_xpath_default_namespace 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 -from ..etree import etree_element from ..regex import get_python_regex from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError from .xsdbase import XsdComponent -from .assertions import XsdAssertion class XsdFacet(XsdComponent): @@ -459,20 +458,28 @@ def __init__(self, elem, schema, parent, base_type): def _parse(self): super(XsdFacet, self)._parse() - self._elements = [] - self.enumeration = [] - self.append(self.elem) + self._elements = [self.elem] + self.enumeration = [self._parse_value(self.elem)] + + def _parse_value(self, elem): + try: + value = self.base_type.decode(elem.attrib['value']) + except KeyError: + self.parse_error("missing 'value' attribute", elem) + 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) + return value # Implements the abstract methods of MutableSequence def __getitem__(self, i): return self._elements[i] - def __setitem__(self, i, item): - value = self.base_type.decode(item.attrib['value']) - 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", item) - self._elements[i] = item - self.enumeration[i] = value + def __setitem__(self, i, elem): + self._elements[i] = elem + self.enumeration[i] = self._parse_value(elem) def __delitem__(self, i): del self._elements[i] @@ -481,12 +488,9 @@ def __delitem__(self, i): def __len__(self): return len(self._elements) - def insert(self, i, item): - self._elements.insert(i, item) - value = self.base_type.decode(item.attrib['value']) - 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", item) - self.enumeration.insert(i, value) + def insert(self, i, elem): + self._elements.insert(i, elem) + self.enumeration.insert(i, self._parse_value(elem)) def __repr__(self): if len(self.enumeration) > 5: @@ -522,9 +526,17 @@ def __init__(self, elem, schema, parent, base_type): def _parse(self): super(XsdFacet, self)._parse() self._elements = [self.elem] - value = self.elem.attrib['value'] - self.regexps = [value] - self.patterns = [re.compile(get_python_regex(value))] + self.patterns = [self._parse_value(self.elem)] + + def _parse_value(self, elem): + try: + return re.compile(get_python_regex(elem.attrib['value'])) + except KeyError: + self.parse_error("missing 'value' attribute", elem) + return re.compile(r'^$') + except XMLSchemaDecodeError as err: + self.parse_error(err, elem) + return re.compile(r'^$') # Implements the abstract methods of MutableSequence def __getitem__(self, i): @@ -532,13 +544,10 @@ def __getitem__(self, i): def __setitem__(self, i, elem): self._elements[i] = elem - value = elem.attrib['value'] - self.regexps[i] = value - self.patterns[i] = re.compile(get_python_regex(value)) + self.patterns[i] = self._parse_value(elem) def __delitem__(self, i): del self._elements[i] - del self.regexps[i] del self.patterns[i] def __len__(self): @@ -546,22 +555,28 @@ def __len__(self): def insert(self, i, elem): self._elements.insert(i, elem) - value = elem.attrib['value'] - self.regexps.insert(i, value) - self.patterns.insert(i, re.compile(get_python_regex(value))) + self.patterns.insert(i, self._parse_value(elem)) def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.regexps) + s = repr(self.regexps) + if len(s) < 70: + return '%s(%s)' % (self.__class__.__name__, s) + else: + return '%s(%s...\'])' % (self.__class__.__name__, s[:70]) def __call__(self, text): if all(pattern.match(text) is None for pattern in self.patterns): msg = "value doesn't match any pattern of %r." yield XMLSchemaValidationError(self, text, reason=msg % self.regexps) + @property + def regexps(self): + return [e.get('value', '') for e in self._elements] -class XsdAssertionFacets(MutableSequence, XsdFacet): + +class XsdAssertionFacet(XsdFacet): """ - Sequence of XSD assertion facets. Values are validates if are `True` with all XPath tests of assertions. + XSD 1.1 assertion facet for simpleType definitions. Date: Sat, 23 Feb 2019 10:40:55 +0100 Subject: [PATCH 09/12] Add _parse_xpath_default_namespace() to XsdComponent - Removed get_xpath_default_namespace() helper and its tests - Removed xpath_default_namespace property from XsdComponent --- xmlschema/helpers.py | 24 ---------------------- xmlschema/tests/test_helpers.py | 17 +-------------- xmlschema/tests/test_schemas.py | 6 +++--- xmlschema/validators/assertions.py | 15 +++++--------- xmlschema/validators/elements.py | 17 ++++++--------- xmlschema/validators/facets.py | 14 +++++-------- xmlschema/validators/identities.py | 13 +++++------- xmlschema/validators/schema.py | 14 +++---------- xmlschema/validators/xsdbase.py | 33 +++++++++++++++++++++++------- 9 files changed, 54 insertions(+), 99 deletions(-) diff --git a/xmlschema/helpers.py b/xmlschema/helpers.py index c6bdf13c..1e235947 100644 --- a/xmlschema/helpers.py +++ b/xmlschema/helpers.py @@ -229,27 +229,3 @@ def get_xsd_derivation_attribute(elem, attribute, 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_xpath_default_namespace(elem, default_namespace, target_namespace, default=None): - """ - Get the xpathDefaultNamespace attribute value for alternative, assert, assertion, selector - and field XSD 1.1 declarations, checking if the value is conforming to the specification. - """ - value = elem.get('xpathDefaultNamespace') - if value is None: - return default - - value = value.strip() - if value == '##local': - return '' - elif value == '##defaultNamespace': - return default_namespace - elif value == '##targetNamespace': - return target_namespace - elif len(value.split()) == 1: - return value - else: - admitted_values = ('##defaultNamespace', '##targetNamespace', '##local') - msg = "wrong value %r for 'xpathDefaultNamespace' attribute, can be (anyURI | %s)." - raise XMLSchemaValueError(msg % (value, ' | '.join(admitted_values))) diff --git a/xmlschema/tests/test_helpers.py b/xmlschema/tests/test_helpers.py index f008277a..1de31955 100644 --- a/xmlschema/tests/test_helpers.py +++ b/xmlschema/tests/test_helpers.py @@ -20,7 +20,7 @@ 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, \ - get_xml_bool_attribute, get_xsd_derivation_attribute, get_xpath_default_namespace + 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 @@ -190,21 +190,6 @@ def test_get_xsd_derivation_attribute(self): self.assertRaises(ValueError, get_xsd_derivation_attribute, elem, 'a6', values) self.assertEqual(get_xsd_derivation_attribute(elem, 'a7', values), '') - def test_get_xpath_default_namespace(self): - elem = etree_element(XSD_ELEMENT, attrib={'xpathDefaultNamespace': '##local '}) - self.assertEqual(get_xpath_default_namespace(elem, 'ns0', 'ns1', 'ns2'), '') - elem = etree_element(XSD_ELEMENT, attrib={'xpathDefaultNamespace': ' ##defaultNamespace'}) - self.assertEqual(get_xpath_default_namespace(elem, 'ns0', 'ns1', 'ns2'), 'ns0') - elem = etree_element(XSD_ELEMENT, attrib={'xpathDefaultNamespace': ' ##targetNamespace'}) - self.assertEqual(get_xpath_default_namespace(elem, 'ns0', 'ns1', 'ns2'), 'ns1') - elem = etree_element(XSD_ELEMENT) - self.assertIsNone(get_xpath_default_namespace(elem, 'ns0', 'ns1')) - self.assertEqual(get_xpath_default_namespace(elem, 'ns0', 'ns1', 'ns2'), 'ns2') - elem = etree_element(XSD_ELEMENT, attrib={'xpathDefaultNamespace': 'ns3'}) - self.assertEqual(get_xpath_default_namespace(elem, 'ns0', 'ns1', 'ns2'), 'ns3') - elem = etree_element(XSD_ELEMENT, attrib={'xpathDefaultNamespace': 'ns3 ns4'}) - self.assertRaises(ValueError, get_xpath_default_namespace, elem, 'ns0', 'ns1', 'ns2') - if __name__ == '__main__': from xmlschema.tests import print_test_header diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index f8cbdba2..92575d8c 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -468,11 +468,11 @@ def test_complex_type_assertion(self): """) xsd_type = schema.types['intRange'] + xsd_type.decode(etree_element('a', attrib={'min': '10', 'max': '19'})) self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '10', 'max': '19'}))) self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '19', 'max': '19'}))) - import pdb - pdb.set_trace() - self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '19'}))) + self.assertFalse(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '19'}))) + self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '100'}))) def make_schema_test_class(test_file, test_args, test_num, schema_class, check_with_lxml): diff --git a/xmlschema/validators/assertions.py b/xmlschema/validators/assertions.py index 660467b3..1a27292b 100644 --- a/xmlschema/validators/assertions.py +++ b/xmlschema/validators/assertions.py @@ -12,7 +12,6 @@ from elementpath import XPath2Parser, XPathContext, XMLSchemaProxy, ElementPathSyntaxError from ..qnames import XSD_ASSERT -from ..helpers import get_xpath_default_namespace from ..xpath import ElementPathMixin from .exceptions import XMLSchemaValidationError @@ -49,13 +48,11 @@ def _parse(self): self.parse_error(str(err), elem=self.elem) self.path = 'true()' - try: - default_namespace = get_xpath_default_namespace(self.elem, self.namespaces[''], self.target_namespace) - except ValueError as err: - self.parse_error(str(err), elem=self.elem) - self.parser = XPath2Parser(self.namespaces, strict=False) + if 'xpathDefaultNamespace' in self.elem.attrib: + self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem) else: - self.parser = XPath2Parser(self.namespaces, strict=False, default_namespace=default_namespace) + self.xpath_default_namespace = self.schema.xpath_default_namespace + self.parser = XPath2Parser(self.namespaces, strict=False, default_namespace=self.xpath_default_namespace) @property def built(self): @@ -70,9 +67,7 @@ def parse(self): self.token = self.parser.parse('true()') def __call__(self, elem): - # TODO: protect from dynamic errors ... - context = XPathContext(root=elem) - if not self.token.evaluate(context): + if not self.token.evaluate(XPathContext(root=elem)): msg = "expression is not true with test path %r." yield XMLSchemaValidationError(self, obj=elem, reason=msg % self.path) diff --git a/xmlschema/validators/elements.py b/xmlschema/validators/elements.py index b7f5268d..e574fdd6 100644 --- a/xmlschema/validators/elements.py +++ b/xmlschema/validators/elements.py @@ -21,8 +21,7 @@ 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, get_xpath_default_namespace +from ..helpers import get_qname, prefixed_to_qname, get_xml_bool_attribute, get_xsd_derivation_attribute from ..etree import etree_element from ..converters import ElementData, raw_xml_encode, XMLSchemaConverter from ..xpath import ElementPathMixin @@ -661,17 +660,13 @@ def _parse(self): self.path = elem.attrib['test'] except KeyError as err: self.path = 'true()' - - try: - default_namespace = get_xpath_default_namespace(elem, self.namespaces[''], self.target_namespace) - except ValueError as err: self.parse_error(err, elem=elem) - default_namespace = self.schema.xpath_default_namespace - else: - if default_namespace is None: - default_namespace = self.schema.xpath_default_namespace - parser = XPath2Parser(self.namespaces, strict=False, default_namespace=default_namespace) + if 'xpathDefaultNamespace' in self.elem.attrib: + self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem) + else: + self.xpath_default_namespace = self.schema.xpath_default_namespace + parser = XPath2Parser(self.namespaces, strict=False, default_namespace=self.xpath_default_namespace) try: self.token = parser.parse(self.path) diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index d301bb97..05eb0717 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -16,7 +16,6 @@ from elementpath import XPath2Parser, ElementPathSyntaxError, ElementPathTypeError from ..compat import unicode_type, MutableSequence -from ..helpers import get_xpath_default_namespace 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, \ @@ -601,15 +600,12 @@ def _parse(self): variables = {'value': self.base_type.primitive_type.value} - try: - default_namespace = get_xpath_default_namespace(self.elem, self.namespaces[''], self.target_namespace) - except ValueError as err: - self.parse_error(str(err), elem=self.elem) - self.parser = XPath2Parser(self.namespaces, strict=False, variables=variables) + if 'xpathDefaultNamespace' in self.elem.attrib: + self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem) else: - self.parser = XPath2Parser( - self.namespaces, strict=False, default_namespace=default_namespace, variables=variables - ) + self.xpath_default_namespace = self.schema.xpath_default_namespace + self.parser = XPath2Parser(self.namespaces, strict=False, variables=variables, + default_namespace=self.xpath_default_namespace) try: self.token = self.parser.parse(self.path) diff --git a/xmlschema/validators/identities.py b/xmlschema/validators/identities.py index e6b1beb8..6f512043 100644 --- a/xmlschema/validators/identities.py +++ b/xmlschema/validators/identities.py @@ -17,7 +17,7 @@ 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, get_xpath_default_namespace +from ..helpers import get_qname, prefixed_to_qname, qname_to_prefixed from ..etree import etree_getpath from .exceptions import XMLSchemaValidationError @@ -63,13 +63,10 @@ def _parse(self): # XSD 1.1 xpathDefaultNamespace attribute if self.schema.XSD_VERSION > '1.0': - try: - self._xpath_default_namespace = get_xpath_default_namespace( - self.elem, self.namespaces[''], self.target_namespace - ) - except XMLSchemaValueError as error: - self.parse_error(str(error)) - self._xpath_default_namespace = self.namespaces[''] + if 'xpathDefaultNamespace' in self.elem.attrib: + self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem) + else: + self.xpath_default_namespace = self.schema.xpath_default_namespace def __repr__(self): return '%s(path=%r)' % (self.__class__.__name__, self.path) diff --git a/xmlschema/validators/schema.py b/xmlschema/validators/schema.py index 68c2e1bf..d75d2459 100644 --- a/xmlschema/validators/schema.py +++ b/xmlschema/validators/schema.py @@ -23,7 +23,7 @@ * Alternative type for elements * Inheritable attributes * targetNamespace for restricted element and attributes - * TODO: Assert for complex types + * Assert for complex types * TODO: OpenContent and XSD 1.1 wildcards for complex types * schema overrides """ @@ -37,8 +37,7 @@ 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, \ - get_xpath_default_namespace +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, \ XLINK_NAMESPACE, NamespaceResourcesMap, NamespaceView from ..etree import etree_element, etree_tostring @@ -264,6 +263,7 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None # 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: @@ -272,14 +272,6 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None self.parse_error(str(error), root) self.default_attributes = None - try: - self.xpath_default_namespace = get_xpath_default_namespace( - root, self.namespaces[''], self.target_namespace, default='' - ) - except XMLSchemaValueError as error: - self.parse_error(str(error), root) - self.xpath_default_namespace = '' # self.namespaces[''] - # Create or set the XSD global maps instance if global_maps is None: if self.meta_schema is None: diff --git a/xmlschema/validators/xsdbase.py b/xmlschema/validators/xsdbase.py index c1a05b28..487ff595 100644 --- a/xmlschema/validators/xsdbase.py +++ b/xmlschema/validators/xsdbase.py @@ -151,6 +151,32 @@ def parse_error(self, error, elem=None): else: raise error + def _parse_xpath_default_namespace(self, elem): + """ + Parse XSD 1.1 xpathDefaultNamespace attribute for schema, alternative, assert, assertion + and selector declarations, checking if the value is conforming to the specification. In + case the attribute is missing or for wrong attribute values defaults to ''. + """ + try: + value = elem.attrib['xpathDefaultNamespace'] + except KeyError: + return '' + + value = value.strip() + if value == '##local': + return '' + elif value == '##defaultNamespace': + return getattr(self, 'default_namespace') + elif value == '##targetNamespace': + return getattr(self, 'target_namespace') + elif len(value.split()) == 1: + return value + else: + admitted_values = ('##defaultNamespace', '##targetNamespace', '##local') + msg = "wrong value %r for 'xpathDefaultNamespace' attribute, can be (anyURI | %s)." + self.parse_error(msg % (value, ' | '.join(admitted_values)), elem) + return '' + class XsdComponent(XsdValidator): """ @@ -238,13 +264,6 @@ def namespaces(self): """Property that references to schema's namespace mapping.""" return self.schema.namespaces - @property - def xpath_default_namespace(self): - try: - return getattr(self, '_xpath_default_namespace') - except AttributeError: - getattr(self.schema, '_xpath_default_namespace', None) - @property def maps(self): """Property that references to schema's global maps.""" From ccfaab9479097c55f739a35191b5cdcb2df40f78 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Mon, 25 Feb 2019 13:21:46 +0100 Subject: [PATCH 10/12] Fix XML resource defusing - Add XMLResource.defusing() for checking XML data - Now in XMLResource.parse() and fromsource() the SafeXMLParser is used only for defusing data --- setup.py | 2 +- tox.ini | 4 +- xmlschema/compat.py | 3 +- xmlschema/resources.py | 73 +++++++++++++++++++-------- xmlschema/tests/test_resources.py | 33 +++++++++++- xmlschema/tests/test_schemas.py | 27 ++++++---- xmlschema/validators/attributes.py | 4 +- xmlschema/validators/complex_types.py | 1 - xmlschema/validators/simple_types.py | 5 +- xmlschema/validators/xsdbase.py | 4 +- 10 files changed, 111 insertions(+), 45 deletions(-) diff --git a/setup.py b/setup.py index 2db95eab..98ede226 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='xmlschema', version='1.0.10', - install_requires=['elementpath==1.1.4'], + install_requires=['elementpath==1.1.5'], packages=['xmlschema'], include_package_data=True, author='Davide Brunato', diff --git a/tox.ini b/tox.ini index 155e7f77..95447aec 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,12 @@ toxworkdir = {homedir}/.tox/xmlschema [testenv] deps = lxml - elementpath>=1.1.4 + elementpath>=1.1.5 commands = python xmlschema/tests/test_all.py {posargs} [testenv:py27] deps = lxml - elementpath>=1.1.4 + elementpath>=1.1.5 pathlib2 commands = python xmlschema/tests/test_all.py {posargs} diff --git a/xmlschema/compat.py b/xmlschema/compat.py index f4fdb55d..1fb151a4 100644 --- a/xmlschema/compat.py +++ b/xmlschema/compat.py @@ -19,7 +19,7 @@ from urllib.request import urlopen, urljoin, urlsplit, pathname2url from urllib.parse import uses_relative, urlparse, urlunsplit from urllib.error import URLError - from io import StringIO + from io import StringIO, BytesIO from collections.abc import Iterable, MutableSet, Sequence, MutableSequence, Mapping, MutableMapping except ImportError: # Python 2.7 imports @@ -27,6 +27,7 @@ from urllib2 import urlopen, URLError from urlparse import urlsplit, urljoin, uses_relative, urlparse, urlunsplit from StringIO import StringIO # the io.StringIO accepts only unicode type + from io import BytesIO from collections import Iterable, MutableSet, Sequence, MutableSequence, Mapping, MutableMapping diff --git a/xmlschema/resources.py b/xmlschema/resources.py index 106fd237..ccd4e23c 100644 --- a/xmlschema/resources.py +++ b/xmlschema/resources.py @@ -13,7 +13,7 @@ import codecs from .compat import ( - PY3, StringIO, string_base_type, urlopen, urlsplit, urljoin, urlunsplit, + PY3, StringIO, BytesIO, string_base_type, urlopen, urlsplit, urljoin, urlunsplit, pathname2url, URLError, uses_relative ) from .exceptions import XMLSchemaTypeError, XMLSchemaValueError, XMLSchemaURLError, XMLSchemaOSError @@ -220,8 +220,8 @@ class XMLResource(object): object or an ElementTree or an Element. :param base_url: is an optional base URL, used for the normalization of relative paths when \ the URL of the resource can't be obtained from the source argument. - :param defuse: set the usage of defusedxml library for parsing XML data. Can be 'always', \ - 'remote' or 'never'. Default is 'remote' that uses the defusedxml only when loading remote data. + :param defuse: set the usage of SafeXMLParser for XML data. Can be 'always', 'remote' or 'never'. \ + Default is 'remote' that uses the defusedxml only when loading remote data. :param timeout: the timeout in seconds for the connection attempt in case of remote data. :param lazy: if set to `False` the source is fully loaded into and processed from memory. Default is `True`. """ @@ -270,7 +270,7 @@ def __setattr__(self, name, value): def _fromsource(self, source): url, lazy = None, self._lazy if is_etree_element(source): - return source, None, None, None + return source, None, None, None # Source is already an Element --> nothing to load elif isinstance(source, string_base_type): _url, self._url = self._url, None try: @@ -280,7 +280,7 @@ def _fromsource(self, source): return root, None, source, None else: return self.fromstring(source), None, source, None - except (ElementTree.ParseError, UnicodeEncodeError): + except (ElementTree.ParseError, PyElementTree.ParseError, UnicodeEncodeError): if '\n' in source: raise finally: @@ -383,19 +383,48 @@ def namespace(self): """The namespace of the XML document.""" return get_namespace(self._root.tag) if self._root is not None else None + @staticmethod + def defusing(source): + """ + Defuse an XML source, raising an `ElementTree.ParseError` if the source contains entity + definitions or remote entity loading. + + :param source: a filename or file object containing XML data. + """ + parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) + try: + for _, _ in PyElementTree.iterparse(source, ('start',), parser): + break + except PyElementTree.ParseError as err: + raise ElementTree.ParseError(str(err)) + def parse(self, source): - """The ElementTree parse method, depends from 'defuse' and 'url' attributes.""" + """ + An equivalent of *ElementTree.parse()* that can protect from XML entities attacks. When + protection is applied XML data are loaded and defused before building the ElementTree instance. + + :param source: a filename or file object containing XML data. + :returns: an ElementTree instance. + """ if self.defuse == 'always' or self.defuse == 'remote' and is_remote_url(self._url): - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - try: - return PyElementTree.parse(source, parser) - except PyElementTree.ParseError as err: - raise ElementTree.ParseError(str(err)) + text = source.read() + if isinstance(text, bytes): + self.defusing(BytesIO(text)) + return ElementTree.parse(BytesIO(text)) + else: + self.defusing(StringIO(text)) + return ElementTree.parse(StringIO(text)) else: return ElementTree.parse(source) def iterparse(self, source, events=None): - """The ElementTree iterparse method, depends from 'defuse' and 'url' attributes.""" + """ + An equivalent of *ElementTree.iterparse()* that can protect from XML entities attacks. + When protection is applied the iterator yields pure-Python Element instances. + + :param source: a filename or file object containing XML data. + :param events: a list of events to report back. If omitted, only “end” events are reported. + """ if self.defuse == 'always' or self.defuse == 'remote' and is_remote_url(self._url): parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) try: @@ -406,15 +435,15 @@ def iterparse(self, source, events=None): return ElementTree.iterparse(source, events) def fromstring(self, text): - """The ElementTree fromstring method, depends from 'defuse' and 'url' attributes.""" + """ + An equivalent of *ElementTree.fromstring()* that can protect from XML entities attacks. + + :param text: a string containing XML data. + :returns: the root Element instance. + """ if self.defuse == 'always' or self.defuse == 'remote' and is_remote_url(self._url): - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - try: - return PyElementTree.fromstring(text, parser) - except PyElementTree.ParseError as err: - raise ElementTree.ParseError(str(err)) - else: - return ElementTree.fromstring(text) + self.defusing(StringIO(text)) + return ElementTree.fromstring(text) def tostring(self, indent='', max_lines=None, spaces_for_tab=4, xml_declaration=False): """Generates a string representation of the XML resource.""" @@ -550,7 +579,7 @@ def update_nsmap(prefix, uri): try: for event, node in self.iterparse(resource, events=('start-ns',)): update_nsmap(*node) - except ElementTree.ParseError: + except (ElementTree.ParseError, PyElementTree.ParseError, UnicodeEncodeError): pass finally: resource.close() @@ -558,7 +587,7 @@ def update_nsmap(prefix, uri): try: for event, node in self.iterparse(StringIO(self._text), events=('start-ns',)): update_nsmap(*node) - except ElementTree.ParseError: + except (ElementTree.ParseError, PyElementTree.ParseError, UnicodeEncodeError): pass else: # Warning: can extracts namespace information only from lxml etree structures diff --git a/xmlschema/tests/test_resources.py b/xmlschema/tests/test_resources.py index 53005555..fd8ce7bc 100644 --- a/xmlschema/tests/test_resources.py +++ b/xmlschema/tests/test_resources.py @@ -24,9 +24,9 @@ fetch_namespaces, fetch_resource, normalize_url, fetch_schema, fetch_schema_locations, load_xml_resource, XMLResource, XMLSchemaURLError ) -from xmlschema.tests import XMLSchemaTestCase +from xmlschema.tests import XMLSchemaTestCase, SKIP_REMOTE_TESTS from xmlschema.compat import urlopen, urlsplit, uses_relative, StringIO -from xmlschema.etree import ElementTree, lxml_etree, is_etree_element +from xmlschema.etree import ElementTree, PyElementTree, lxml_etree, is_etree_element, etree_element, py_etree_element def is_windows_path(path): @@ -264,6 +264,21 @@ def test_xml_resource_defuse(self): self.assertEqual(resource.defuse, 'never') self.assertRaises(ValueError, XMLResource, self.vh_xml_file, defuse='all') self.assertRaises(ValueError, XMLResource, self.vh_xml_file, defuse=None) + self.assertIsInstance(resource.root, etree_element) + resource = XMLResource(self.vh_xml_file, defuse='always') + self.assertIsInstance(resource.root, py_etree_element) + + xml_file = self.casepath('resources/with_entity.xml') + self.assertIsInstance(XMLResource(xml_file), XMLResource) + self.assertRaises(PyElementTree.ParseError, XMLResource, xml_file, defuse='always') + + xml_file = self.casepath('resources/unused_external_entity.xml') + self.assertIsInstance(XMLResource(xml_file), XMLResource) + self.assertRaises(PyElementTree.ParseError, XMLResource, xml_file, defuse='always') + + xml_file = self.casepath('resources/external_entity.xml') + self.assertIsInstance(XMLResource(xml_file), XMLResource) + self.assertRaises(PyElementTree.ParseError, XMLResource, xml_file, defuse='always') def test_xml_resource_timeout(self): resource = XMLResource(self.vh_xml_file, timeout=30) @@ -338,6 +353,20 @@ def test_xml_resource_get_locations(self): self.assertEqual(len(locations), 2) self.check_url(locations[0][1], os.path.join(self.col_dir, 'other.xsd')) + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_remote_schemas_loading(self): + # Tests with Dublin Core schemas that also use imports + dc_schema = self.schema_class("http://dublincore.org/schemas/xmls/qdc/2008/02/11/dc.xsd") + self.assertTrue(isinstance(dc_schema, self.schema_class)) + dcterms_schema = self.schema_class("http://dublincore.org/schemas/xmls/qdc/2008/02/11/dcterms.xsd") + self.assertTrue(isinstance(dcterms_schema, self.schema_class)) + + def test_schema_defuse(self): + vh_schema = self.schema_class(self.vh_xsd_file, defuse='always') + self.assertIsInstance(vh_schema.root, etree_element) + for schema in vh_schema.maps.iter_schemas(): + self.assertIsInstance(schema.root, etree_element) + if __name__ == '__main__': from xmlschema.tests import print_test_header diff --git a/xmlschema/tests/test_schemas.py b/xmlschema/tests/test_schemas.py index 92575d8c..c674e9f9 100644 --- a/xmlschema/tests/test_schemas.py +++ b/xmlschema/tests/test_schemas.py @@ -21,11 +21,11 @@ import warnings import xmlschema -from xmlschema import XMLSchemaBase, XMLSchema, XMLSchemaParseError, XMLSchemaIncludeWarning, XMLSchemaImportWarning +from xmlschema import XMLSchemaBase, XMLSchemaParseError, 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.tests import tests_factory, SKIP_REMOTE_TESTS, SchemaObserver, XMLSchemaTestCase +from xmlschema.tests import tests_factory, SchemaObserver, XMLSchemaTestCase from xmlschema.validators import XsdValidator, XMLSchema11 from xmlschema.xpath import ElementPathContext @@ -379,13 +379,22 @@ def test_wrong_attribute_group(self): """, validation='lax') self.assertTrue(isinstance(schema.all_errors[1], XMLSchemaParseError)) - @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") - def test_remote_schemas(self): - # Tests with Dublin Core schemas that also use imports - dc_schema = self.schema_class("http://dublincore.org/schemas/xmls/qdc/2008/02/11/dc.xsd") - self.assertTrue(isinstance(dc_schema, self.schema_class)) - dcterms_schema = self.schema_class("http://dublincore.org/schemas/xmls/qdc/2008/02/11/dcterms.xsd") - self.assertTrue(isinstance(dcterms_schema, self.schema_class)) + def test_date_time_facets(self): + self.check_schema(""" + + + + + + """) + + schema = self.check_schema(""" + + + + + + """) class TestXMLSchema11(TestXMLSchema10): diff --git a/xmlschema/validators/attributes.py b/xmlschema/validators/attributes.py index df20d90e..78c1b6f6 100644 --- a/xmlschema/validators/attributes.py +++ b/xmlschema/validators/attributes.py @@ -17,13 +17,13 @@ from ..compat import MutableMapping from ..exceptions import XMLSchemaAttributeError, XMLSchemaValueError -from ..qnames import XSD_ANY_SIMPLE_TYPE, XSD_SIMPLE_TYPE, XSD_ATTRIBUTE_GROUP, XSD_COMPLEX_TYPE, XSD_ANY_TYPE, \ +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 ..namespaces import XSI_NAMESPACE from .exceptions import XMLSchemaValidationError -from .xsdbase import XsdComponent, ValidationMixin, XsdType +from .xsdbase import XsdComponent, ValidationMixin from .simple_types import XsdSimpleType from .wildcards import XsdAnyAttribute diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index 162e616e..87065eda 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -15,7 +15,6 @@ 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 ..etree import etree_element -from ..xpath import ElementPathMixin from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError from .xsdbase import XsdType, ValidationMixin diff --git a/xmlschema/validators/simple_types.py b/xmlschema/validators/simple_types.py index 21e97a97..d96a184e 100644 --- a/xmlschema/validators/simple_types.py +++ b/xmlschema/validators/simple_types.py @@ -22,8 +22,7 @@ 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 ) -from ..helpers import get_qname, local_name, prefixed_to_qname, get_xsd_component, get_xsd_derivation_attribute -from ..xpath import ElementPathMixin +from ..helpers import get_qname, local_name, prefixed_to_qname, get_xsd_derivation_attribute from .exceptions import XMLSchemaValidationError, XMLSchemaEncodeError, XMLSchemaDecodeError, XMLSchemaParseError from .xsdbase import XsdAnnotation, XsdType, ValidationMixin @@ -950,7 +949,7 @@ def _parse(self): if elem.get('name') == XSD_ANY_ATOMIC_TYPE: return # skip special type xs:anyAtomicType elif elem.tag == XSD_SIMPLE_TYPE and elem.get('name') is not None: - elem = get_xsd_component(elem) # Global simpleType with internal restriction + elem = self._parse_component(elem) # Global simpleType with internal restriction base_type = None facets = {} diff --git a/xmlschema/validators/xsdbase.py b/xmlschema/validators/xsdbase.py index 487ff595..961cf368 100644 --- a/xmlschema/validators/xsdbase.py +++ b/xmlschema/validators/xsdbase.py @@ -289,14 +289,14 @@ def _parse_component(self, elem, required=True, strict=True): try: return get_xsd_component(elem, required, strict) except XMLSchemaValueError as err: - self.parse_error(str(err), elem) + self.parse_error(err, elem) def _iterparse_components(self, elem, start=0): try: for obj in iter_xsd_components(elem, start): yield obj except XMLSchemaValueError as err: - self.parse_error(str(err), elem) + self.parse_error(err, elem) def _parse_properties(self, *properties): for name in properties: From 1ae3ebd131fa703b1d0d38b74a91ac7a6ff872b5 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Mon, 25 Feb 2019 13:47:10 +0100 Subject: [PATCH 11/12] Remove sample value from XSD primitive builtin types - No more necessary, now provided by elementpath v1.1.4+. --- xmlschema/validators/builtins.py | 25 ------------------------- xmlschema/validators/facets.py | 5 +++-- xmlschema/validators/simple_types.py | 4 +--- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/xmlschema/validators/builtins.py b/xmlschema/validators/builtins.py index 6f62c451..216f7512 100644 --- a/xmlschema/validators/builtins.py +++ b/xmlschema/validators/builtins.py @@ -172,7 +172,6 @@ def python_to_boolean(obj): 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [PRESERVE_WHITE_SPACE_ELEMENT], - 'value': 'alpha', }, # character string # --- Numerical Types --- @@ -181,21 +180,18 @@ def python_to_boolean(obj): 'python_type': (Decimal, str, unicode_type, int, float), 'admitted_facets': DECIMAL_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': Decimal('1.0'), }, # decimal number { 'name': XSD_DOUBLE, 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': 1.0, }, # 64 bit floating point { 'name': XSD_FLOAT, 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': 1.0, }, # 32 bit floating point # --- Dates and Times (not year related) --- @@ -205,7 +201,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianDay.fromstring, - 'value': datatypes.GregorianDay.fromstring('---31'), }, # DD { 'name': XSD_GMONTH, @@ -213,7 +208,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianMonth.fromstring, - 'value': datatypes.GregorianMonth.fromstring('--12'), }, # MM { 'name': XSD_GMONTH_DAY, @@ -221,7 +215,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianMonthDay.fromstring, - 'value': datatypes.GregorianMonthDay.fromstring('--12-01'), }, # MM-DD { 'name': XSD_TIME, @@ -229,7 +222,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Time.fromstring, - 'value': datatypes.Time.fromstring('09:26:54'), }, # hh:mm:ss { 'name': XSD_DURATION, @@ -237,7 +229,6 @@ def python_to_boolean(obj): 'admitted_facets': FLOAT_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Duration.fromstring, - 'value': datatypes.Duration.fromstring('P1MT1S'), }, # PnYnMnDTnHnMnS # Other primitive types @@ -246,21 +237,18 @@ def python_to_boolean(obj): 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': 'xs:element', }, # prf:name (the prefix needs to be qualified with an in scope namespace) { 'name': XSD_NOTATION_TYPE, 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': 'alpha', }, # type for NOTATION attributes: QNames of xs:notation declarations as value space. { 'name': XSD_ANY_URI, 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], - 'value': 'https://example.com', }, # absolute or relative uri (RFC 2396) { 'name': XSD_BOOLEAN, @@ -269,21 +257,18 @@ def python_to_boolean(obj): 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': boolean_to_python, 'from_python': python_to_boolean, - 'value': True, }, # true/false or 1/0 { 'name': XSD_BASE64_BINARY, 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT, base64_binary_validator], - 'value': b'YWxwaGE=', }, # base64 encoded binary value { 'name': XSD_HEX_BINARY, 'python_type': (unicode_type, str), 'admitted_facets': STRING_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT, hex_binary_validator], - 'value': b'31', }, # hexadecimal encoded binary value # ********************* @@ -435,7 +420,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.DateTime10.fromstring, - 'value': datatypes.DateTime10.fromstring('2000-01-01T12:00:00'), }, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] { 'name': XSD_DATE, @@ -443,7 +427,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Date10.fromstring, - 'value': datatypes.Date10.fromstring('2000-01-01'), }, # [-][Y*]YYYY-MM-DD { 'name': XSD_GYEAR, @@ -451,7 +434,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYear10.fromstring, - 'value': datatypes.GregorianYear10.fromstring('1999'), }, # [-][Y*]YYYY { 'name': XSD_GYEAR_MONTH, @@ -459,7 +441,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYearMonth10.fromstring, - 'value': datatypes.GregorianYearMonth10.fromstring('1999-09'), }, # [-][Y*]YYYY-MM ) @@ -471,7 +452,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.DateTime.fromstring, - 'value': datatypes.DateTime.fromstring('2000-01-01T12:00:00'), }, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] { 'name': XSD_DATE, @@ -479,7 +459,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.Date.fromstring, - 'value': datatypes.Date.fromstring('2000-01-01'), }, # [-][Y*]YYYY-MM-DD { 'name': XSD_GYEAR, @@ -487,7 +466,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYear.fromstring, - 'value': datatypes.GregorianYear.fromstring('1999'), }, # [-][Y*]YYYY { 'name': XSD_GYEAR_MONTH, @@ -495,7 +473,6 @@ def python_to_boolean(obj): 'admitted_facets': DATETIME_FACETS, 'facets': [COLLAPSE_WHITE_SPACE_ELEMENT], 'to_python': datatypes.GregorianYearMonth.fromstring, - 'value': datatypes.GregorianYearMonth.fromstring('1999-09'), }, # [-][Y*]YYYY-MM # --- Datetime derived types (XSD 1.1) --- { @@ -510,14 +487,12 @@ def python_to_boolean(obj): 'python_type': (unicode_type, str), 'base_type': XSD_DURATION, 'to_python': datatypes.DayTimeDuration.fromstring, - 'value': datatypes.DayTimeDuration.fromstring('P1DT1S'), }, # PnYnMnDTnHnMnS with month an year equal to 0 { 'name': XSD_YEAR_MONTH_DURATION, 'python_type': (unicode_type, str), 'base_type': XSD_DURATION, 'to_python': datatypes.YearMonthDuration.fromstring, - 'value': datatypes.YearMonthDuration.fromstring('P1Y1M'), }, # PnYnMnDTnHnMnS with day and time equals to 0 ) diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index 05eb0717..e4bca57e 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -13,7 +13,7 @@ """ from __future__ import unicode_literals import re -from elementpath import XPath2Parser, ElementPathSyntaxError, ElementPathTypeError +from elementpath import XPath2Parser, ElementPathSyntaxError, ElementPathTypeError, datatypes from ..compat import unicode_type, MutableSequence from ..qnames import XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_ENUMERATION, XSD_WHITE_SPACE, \ @@ -598,7 +598,8 @@ def _parse(self): self.parse_error(str(err), elem=self.elem) self.path = 'true()' - variables = {'value': self.base_type.primitive_type.value} + builtin_type_name = self.base_type.primitive_type.local_name + variables = {'value': datatypes.XSD_BUILTIN_TYPES[builtin_type_name].value} if 'xpathDefaultNamespace' in self.elem.attrib: self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem) diff --git a/xmlschema/validators/simple_types.py b/xmlschema/validators/simple_types.py index d96a184e..8d77dd90 100644 --- a/xmlschema/validators/simple_types.py +++ b/xmlschema/validators/simple_types.py @@ -404,7 +404,7 @@ class XsdAtomicBuiltin(XsdAtomic): - from_python(value): Encoding to XML """ def __init__(self, elem, schema, name, python_type, base_type=None, admitted_facets=None, facets=None, - to_python=None, from_python=None, value=None): + to_python=None, from_python=None): """ :param name: the XSD type's qualified name. :param python_type: the correspondent Python's type. If a tuple or list of types \ @@ -414,7 +414,6 @@ def __init__(self, elem, schema, name, python_type, base_type=None, admitted_fac :param facets: optional facets validators. :param to_python: optional decode function. :param from_python: optional encode function. - :param value: optional decoded sample value, included in the value-space of the type. """ if isinstance(python_type, (tuple, list)): self.instance_types, python_type = python_type, python_type[0] @@ -431,7 +430,6 @@ def __init__(self, elem, schema, name, python_type, base_type=None, admitted_fac self.python_type = python_type self.to_python = to_python or python_type self.from_python = from_python or unicode_type - self.value = value def __repr__(self): return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name) From 4dc7714a18a07e10845f9c63dc923370d365868a Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Mon, 25 Feb 2019 14:44:28 +0100 Subject: [PATCH 12/12] Update documentation and requirements --- CHANGELOG.rst | 7 ++++--- doc/api.rst | 8 +++++--- doc/usage.rst | 5 ----- requirements-dev.txt | 2 +- setup.py | 2 +- tox.ini | 4 ++-- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5269a284..80ecca1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,9 +2,10 @@ CHANGELOG ********* -`v1.0.10`_ (TBD) -================ -* (XSD 1.1 features implementation completed) +`v1.0.10`_ (2019-02-25) +======================= +* Fixed Element type mismatch issue when apply *SafeXMLParser* to schema resources +* More XSD 1.1 features implemented (open content and versioning namespace are missing) `v1.0.9`_ (2019-02-03) ====================== diff --git a/doc/api.rst b/doc/api.rst index b403bfa5..face8a3e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -144,9 +144,6 @@ Resource access API .. autoattribute:: url .. autoattribute:: base_url .. autoattribute:: namespace - .. autoattribute:: parse - .. autoattribute:: iterparse - .. autoattribute:: fromstring .. automethod:: copy .. automethod:: tostring @@ -159,6 +156,11 @@ Resource access API .. automethod:: get_namespaces .. automethod:: get_locations + .. automethod:: defusing + .. automethod:: parse + .. automethod:: iterparse + .. automethod:: fromstring + .. autofunction:: xmlschema.fetch_resource .. autofunction:: xmlschema.fetch_schema diff --git a/doc/usage.rst b/doc/usage.rst index 2ec9c813..3e1ca80a 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -522,14 +522,9 @@ XML entity-based attacks protection The XML data resource loading is protected using the `SafeXMLParser` class, a subclass of the pure Python version of XMLParser that forbids the use of entities. - The protection is applied both to XSD schemas and to XML data. The usage of this feature is regulated by the XMLSchema's argument *defuse*. For default this argument has value *'remote'* that means the protection on XML data is applied only to data loaded from remote. Other values for this argument can be *'always'* and *'never'*. -The `SafeXMLParser` requires the usage of the pure Python module of ElementTree and this -involves the penalty that trees loaded by this parser can't be serialized with pickle, -that in Python 3 works with the C implementation of ElementTree. - diff --git a/requirements-dev.txt b/requirements-dev.txt index c8d83a2c..8f231dd6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ # Requirements for setup a development environment for the xmlschema package. setuptools tox -elementpath>=1.1.4 +elementpath~=1.1.5 lxml memory_profiler pathlib2 # For Py27 tests on resources diff --git a/setup.py b/setup.py index 98ede226..b4b90877 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='xmlschema', version='1.0.10', - install_requires=['elementpath==1.1.5'], + install_requires=['elementpath~=1.1.5'], packages=['xmlschema'], include_package_data=True, author='Davide Brunato', diff --git a/tox.ini b/tox.ini index 95447aec..3a9ea2e7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,12 @@ toxworkdir = {homedir}/.tox/xmlschema [testenv] deps = lxml - elementpath>=1.1.5 + elementpath~=1.1.5 commands = python xmlschema/tests/test_all.py {posargs} [testenv:py27] deps = lxml - elementpath>=1.1.5 + elementpath~=1.1.5 pathlib2 commands = python xmlschema/tests/test_all.py {posargs}