-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d1b4b52
commit 8eba2c7
Showing
4 changed files
with
199 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from __future__ import annotations | ||
|
||
from contextlib import suppress | ||
from functools import lru_cache | ||
from pathlib import Path | ||
from typing import IO, TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
import xmlschema | ||
|
||
XMLSourceType = str | bytes | Path | IO[str] | IO[bytes] | ||
|
||
NS_OME = r"{http://www.openmicroscopy.org/Schemas/OME/2016-06}" | ||
OME_2016_06_XSD = str(Path(__file__).parent / "ome-2016-06.xsd") | ||
|
||
|
||
class ValidationError(ValueError): | ||
... | ||
|
||
|
||
def validate_xml(xml: XMLSourceType, schema: Path | str | None = None) -> None: | ||
"""Validate XML against an XML Schema. | ||
By default, will validate against the OME 2016-06 schema. | ||
""" | ||
with suppress(ImportError): | ||
return validate_xml_with_lxml(xml, schema) | ||
|
||
with suppress(ImportError): | ||
return validate_xml_with_xmlschema(xml, schema) | ||
|
||
raise ImportError( # pragma: no cover | ||
"Validation requires either `lxml` or `xmlschema`. " | ||
"Please pip install one of them." | ||
) from None | ||
|
||
|
||
def validate_xml_with_lxml( | ||
xml: XMLSourceType, schema: Path | str | None = None | ||
) -> None: | ||
"""Validate XML against an XML Schema using lxml.""" | ||
from lxml import etree | ||
|
||
tree = etree.parse(schema or OME_2016_06_XSD) # noqa: S320 | ||
xmlschema = etree.XMLSchema(tree) | ||
|
||
doc = etree.parse(xml) # noqa: S320 | ||
if not xmlschema.validate(doc): | ||
msg = f"Validation of {str(xml)[:20]!r} failed:" | ||
for error in xmlschema.error_log: | ||
print(dir(error)) | ||
msg += f"\n - line {error.line}: {error.message}" | ||
raise ValidationError(msg) | ||
|
||
|
||
def validate_xml_with_xmlschema( | ||
xml: XMLSourceType, schema: Path | str | None = None | ||
) -> None: | ||
"""Validate XML against an XML Schema using xmlschema.""" | ||
from xmlschema.exceptions import XMLSchemaException | ||
|
||
xmlschema = _get_XMLSchema(schema or OME_2016_06_XSD) | ||
try: | ||
xmlschema.validate(xml) | ||
except XMLSchemaException as e: | ||
raise ValidationError(str(e)) from None | ||
|
||
|
||
@lru_cache(maxsize=None) | ||
def _get_XMLSchema(schema: Path | str) -> xmlschema.XMLSchema: | ||
import xmlschema | ||
|
||
xml_schema = xmlschema.XMLSchema(schema) | ||
# FIXME Hack to work around xmlschema poor support for keyrefs to | ||
# substitution groups | ||
ls_sgs = xml_schema.maps.substitution_groups[f"{NS_OME}LightSourceGroup"] | ||
ls_id_maps = xml_schema.maps.identities[f"{NS_OME}LightSourceIDKey"] | ||
ls_id_maps.elements = {e: None for e in ls_sgs} | ||
return xml_schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Callable | ||
|
||
import pytest | ||
|
||
from ome_types import ( | ||
validate_xml, # noqa: F401 ... just to make sure it's importable | ||
validation, | ||
) | ||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
VALIDATORS: dict[str, Callable] = { | ||
"lxml": validation.validate_xml_with_lxml, | ||
"xmlschema": validation.validate_xml_with_xmlschema, | ||
} | ||
|
||
|
||
@pytest.mark.parametrize("backend", ["lxml", "xmlschema"]) | ||
def test_validation_good(valid_xml: Path, backend: str) -> None: | ||
VALIDATORS[backend](valid_xml) | ||
|
||
|
||
@pytest.mark.parametrize("backend", ["lxml", "xmlschema"]) | ||
def test_validation_raises(invalid_xml: Path, backend: str) -> None: | ||
with pytest.raises(validation.ValidationError): | ||
VALIDATORS[backend](invalid_xml) |