Source code for fastkml.validator

# Copyright (C) 2024 Christian Ledermann
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# This library is distributed 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.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
"""Validate KML files against the XML schema."""

import logging
import pathlib
from functools import lru_cache
from typing import TYPE_CHECKING
from typing import Final
from typing import cast

from fastkml import config
from fastkml.types import Element

if TYPE_CHECKING:
    from collections.abc import Iterable

    from lxml import etree
    from typing_extensions import Protocol

    class _LogEntry(Protocol):
        """A single entry of an lxml `_ErrorLog`, which lxml-stubs omits."""

        message: str
        path: str


__all__ = [
    "get_schema_parser",
    "validate",
]


logger = logging.getLogger(__name__)

MUTUAL_EXCLUSIVE: Final = "Only one of element and file_to_validate can be provided."
REQUIRE_ONE_OF: Final = "Either element or file_to_validate must be provided."


[docs] @lru_cache(maxsize=16) def get_schema_parser( schema: pathlib.Path | None = None, ) -> "etree.XMLSchema": """ Parse the XML schema. Args: ---- schema: The path to the XML schema file. Returns: ------- The parsed XML schema. To clear the cache call get_schema_parser.cache_clear(). """ if schema is None: schema = pathlib.Path(__file__).parent / "schema" / "ogckml22.xsd" return config.etree.XMLSchema(config.etree.parse(schema))
def handle_validation_error( schema_parser: "etree.XMLSchema", element: Element, ) -> None: """ Log the validation error in its XML context. Args: ---- schema_parser: The parsed XML schema. element: The element to validate. """ # lxml-stubs' `_ErrorLog` is an empty stub with no `__iter__` or entry # attributes, even though the real lxml class supports both. log = cast("Iterable[_LogEntry]", schema_parser.error_log) for error_entry in log: try: matches = cast("list[Element]", element.xpath(error_entry.path)) parent = matches[0].getparent() except (config.etree.XPathEvalError, IndexError): parent = element if parent is None: parent = element error_in_xml = config.etree.tostring( parent, encoding="UTF-8", pretty_print=True, ).decode( "UTF-8", ) logger.error( "Error <%s> in XML:\n %s", error_entry.message, error_in_xml, )
[docs] def validate( *, schema: pathlib.Path | None = None, element: Element | None = None, file_to_validate: pathlib.Path | None = None, ) -> bool | None: """ Validate a KML file against the XML schema. Args: ---- schema: The path to the XML schema file. element: The element to validate. file_to_validate: The file to validate. Returns: ------- True if the file or element is valid. Raises an AssertionError if validation fails. Returns None if the schema parser is unavailable. """ if element is None and file_to_validate is None: raise ValueError(REQUIRE_ONE_OF) if element is not None and file_to_validate is not None: raise ValueError(MUTUAL_EXCLUSIVE) try: schema_parser = get_schema_parser(schema) except AttributeError: return None if file_to_validate is not None: element = config.etree.parse(file_to_validate).getroot() assert element is not None # noqa: S101 try: schema_parser.assert_(element) # noqa: PT009 except AssertionError: handle_validation_error(schema_parser, element) raise return True