Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/validate.py: 65%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2XSD (XML Schema) and RELAX NG schema validation for parsed XML documents.
4:class:`XMLSchema` and :class:`RelaxNG` mirror lxml's ``etree.XMLSchema`` and
5``etree.RelaxNG``: construct one from schema text (or a parsed schema document), then
6call :meth:`~XMLSchema.validate` on a document parsed with :func:`turbohtml.parse_xml`.
7The schema compiles once in the C core; each validation walks the instance tree there
8and returns a :class:`ValidationResult` -- a ``valid`` flag plus a tuple of
9:class:`ValidationError` records locating every violation. The Python layer only shapes
10the input and wraps the C result; every datatype, facet, content-model, and RELAX NG
11derivative check runs in the extension.
12"""
14from __future__ import annotations
16from itertools import starmap
17from typing import TYPE_CHECKING, Final, NamedTuple
19from ._html import _schema_compile, _schema_validate
21if TYPE_CHECKING:
22 from pathlib import Path
24 from ._html import Document, Node
26__all__ = [
27 "RelaxNG",
28 "SchemaValidationError",
29 "ValidationError",
30 "ValidationResult",
31 "XMLSchema",
32]
35class ValidationError(NamedTuple):
36 """
37 One schema violation.
39 :param message: a human-readable description of what failed.
40 :param path: the ``/root/child`` document-order path of the offending node.
41 :param line: the 1-based source line of the node, or ``0`` when unknown.
42 :param type: the violation category -- ``"structure"``, ``"datatype"``, or ``"facet"``.
43 """
45 message: str
46 path: str
47 line: int
48 type: str
51class ValidationResult(NamedTuple):
52 """
53 The outcome of validating a document: a ``valid`` flag and the errors found.
55 The result is truthy exactly when the document is valid, so ``if schema.validate(doc):``
56 reads naturally.
57 """
59 valid: bool
60 errors: tuple[ValidationError, ...]
62 def __bool__(self) -> bool:
63 """Whether the document is valid, so the result reads as a truth value."""
64 return self.valid
67class SchemaValidationError(Exception):
68 """Raised by :meth:`XMLSchema.assert_valid` / :meth:`RelaxNG.assert_valid` when a document is invalid."""
70 def __init__(self, errors: tuple[ValidationError, ...]) -> None:
71 """Carry the ``errors`` the failed validation produced, the first of which becomes the message."""
72 self.errors = errors
73 super().__init__(errors[0].message if errors else "document is invalid")
76class _Schema:
77 """Shared machinery for the two schema languages; not instantiated directly."""
79 def __init__(self, compiled: object) -> None:
80 self._compiled: Final = compiled
82 def validate(self, document: Document | Node) -> ValidationResult:
83 """Validate a parsed document (or element), returning the full :class:`ValidationResult`."""
84 valid, errors = _schema_validate(self._compiled, document)
85 return ValidationResult(valid, tuple(starmap(ValidationError, errors)))
87 def is_valid(self, document: Document | Node) -> bool:
88 """Whether the document satisfies the schema."""
89 return _schema_validate(self._compiled, document, collect_errors=False)[0]
91 def assert_valid(self, document: Document | Node) -> None:
92 """Validate the document, raising :class:`SchemaValidationError` with the errors when it is invalid."""
93 result = self.validate(document)
94 if not result.valid:
95 raise SchemaValidationError(result.errors)
98class XMLSchema(_Schema):
99 """
100 A compiled XSD 1.0 schema.
102 :param source: the schema as XSD text, or a schema document parsed with :func:`turbohtml.parse_xml`.
103 :raises ValueError: when the schema is malformed, its root is not ``xs:schema``, a ``ref``/``type``/``base``
104 does not resolve, a group, attribute-group, or type-derivation reference forms a cycle or a chain over 100
105 hops, or an ``xs:unique``/``xs:key``/``xs:keyref`` identity constraint is declared.
106 """
108 def __init__(self, source: str | Node) -> None:
109 """Compile the schema once; every validation reuses the result."""
110 super().__init__(_schema_compile(0, _text(source)))
113class RelaxNG(_Schema):
114 """
115 A compiled RELAX NG schema (XML syntax).
117 :param source: the schema as RELAX NG text, or a schema document parsed with :func:`turbohtml.parse_xml`.
118 :param base_url: the schema's path or file URL, against which each ``include`` and ``externalRef`` href resolves
119 (after any ``xml:base``); required only when the schema references other files.
120 :param include_root: when set, referenced files must resolve inside this directory, including through nested
121 references.
122 :raises ValueError: when the schema is malformed, references a file without a ``base_url``, or a reference
123 escapes ``include_root``, carries a fragment, loops, loads the wrong kind of pattern, or overrides a component
124 the included grammar lacks.
125 :raises OSError: when a referenced file cannot be read.
126 """
128 def __init__(
129 self, source: str | Node, *, base_url: str | None = None, include_root: str | Path | None = None
130 ) -> None:
131 """Compile the schema, reading the files it references, once; every validation reuses the result."""
132 super().__init__(_schema_compile(1, _text(source), base_url, include_root))
135def _text(source: str | Node) -> str:
136 return source if isinstance(source, str) else source.serialize()