diff --git a/edi_xml_oca/__manifest__.py b/edi_xml_oca/__manifest__.py index 3fcbf80e4..1595c3735 100644 --- a/edi_xml_oca/__manifest__.py +++ b/edi_xml_oca/__manifest__.py @@ -14,5 +14,5 @@ "maintainers": ["simahawk"], "website": "https://github.com/OCA/edi-framework", "depends": ["edi_oca", "component"], - "external_dependencies": {"python": ["xmlschema"]}, + "external_dependencies": {"python": ["xmltodict", "lxml"]}, } diff --git a/edi_xml_oca/components/xml_handler.py b/edi_xml_oca/components/xml_handler.py index e853edb36..2b92f1f11 100644 --- a/edi_xml_oca/components/xml_handler.py +++ b/edi_xml_oca/components/xml_handler.py @@ -2,13 +2,12 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import io -from contextlib import closing - -import xmlschema +import xmltodict +from lxml import etree from odoo import modules -from odoo.tools import DotDict +from odoo.exceptions import UserError +from odoo.tools import validate_xml_from_attachment from odoo.addons.component.core import Component @@ -28,35 +27,39 @@ def __init__(self, work_context): if not hasattr(work_context, key): raise AttributeError(f"`{key}` is required for this component!") - self.schema = xmlschema.XMLSchema(self._get_xsd_schema_path()) + self.schema_path, self.schema = self._get_xsd_schema() - def _get_xsd_schema_path(self): - """Lookup for XSD schema.""" + def _get_xsd_schema(self): + """Lookup and parse the XSD schema.""" try: mod_name, path = self.work.schema_path.split(":") except ValueError as exc: raise ValueError("Path must be in the form `module:path`") from exc - return modules.get_resource_path(mod_name, path) - def _parse_xml(self, file_obj, **kw): + schema_path = modules.get_resource_path(mod_name, path) + with open(schema_path, "rb") as schema_file: + return schema_path, etree.XMLSchema(etree.parse(schema_file)) + + def _xml_string_to_dict(self, xml_string): """Read xml_content and return a data dict. - :param file_obj: file obj of XML file + :param xml_string: str of XML file """ - return DotDict(self.schema.to_dict(file_obj, **kw)) + return xmltodict.parse(xml_string)["xs:element"] - def parse_xml(self, file_content, **kw): + def parse_xml(self, file_content): """Read XML content. - :param file_content: str of XML file + :param file_content: unicode str of XML file :return: dict with final data """ - with closing(io.StringIO(file_content)) as fd: - return self._parse_xml(fd) + tree = etree.XML(file_content.encode("utf-8")) + xml_string = etree.tostring(tree).decode("utf-8") + return self._xml_string_to_dict(xml_string) def validate(self, xml_content, raise_on_fail=False): """Validate XML content against XSD schema. - Raises `XMLSchemaValidationError` if `raise_on_fail` is True. + Raises `etree.DocumentInvalid` if `raise_on_fail` is True. :param xml_content: str containing xml data to validate :raise_on_fail: turn on/off validation error exception on fail @@ -65,15 +68,13 @@ def validate(self, xml_content, raise_on_fail=False): * None if validation is ok * error string if `raise_on_fail` is False """ + xsd_name = self.schema_path + xml_content = ( + xml_content.encode("utf-8") if isinstance(xml_content, str) else xml_content + ) try: - return self.schema.validate(xml_content) - except self._validate_swallable_exceptions() as err: + validate_xml_from_attachment(self.env, xml_content, xsd_name) + except UserError as exc: if raise_on_fail: - raise - return str(err) - - def _validate_swallable_exceptions(self): - return ( - xmlschema.exceptions.XMLSchemaValueError, - xmlschema.validators.exceptions.XMLSchemaValidationError, - ) + raise exc + return str(exc) diff --git a/edi_xml_oca/static/description/index.html b/edi_xml_oca/static/description/index.html index d305b6d97..d86587be5 100644 --- a/edi_xml_oca/static/description/index.html +++ b/edi_xml_oca/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -423,7 +424,9 @@

Other credits

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/edi_xml_oca/tests/test_xml.py b/edi_xml_oca/tests/test_xml.py index 628820a64..4abf571e8 100644 --- a/edi_xml_oca/tests/test_xml.py +++ b/edi_xml_oca/tests/test_xml.py @@ -2,6 +2,8 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo.tools import file_open + from odoo.addons.component.tests.common import TransactionComponentCase from .common import XMLTestCaseMixin @@ -36,15 +38,39 @@ def test_xml_schema_fail(self): self.backend._name, ["edi.xml"], work_ctx={"no_schema": "Nothing"} ) + def test_xml_schema_validation(self): + self.assertIsNone(self.handler.validate(TEST_XML)) + XML_FAULTY = """ + + + + """ + + schema_path = self.handler.schema_path + with file_open(schema_path, "rb") as f: + schema_xml = f.read() + # store the schema in ir.attachment as it is searched + # inside _check_with_xsd method defined in odoo.tools + self.env["ir.attachment"].create( + { + "name": schema_path, + "datas": schema_xml, + "type": "binary", + "res_model": "edi.exchange.template.output", + "res_id": 1, + "raw": schema_xml, + } + ) + + self.assertIsNotNone(self.handler.validate(XML_FAULTY)) + def test_xml(self): data = self.handler.parse_xml(TEST_XML) self.assertEqual( data, { - "@abstract": False, + "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@name": "shoesize", - "@nillable": False, "@type": "shoetype", - "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", }, ) diff --git a/requirements.txt b/requirements.txt index 06cbe13b9..2746bdcbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # generated from manifests external_dependencies +lxml PyYAML -xmlschema +xmltodict