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

43 statements  

1""" 

2XSD (XML Schema) and RELAX NG schema validation for parsed XML documents. 

3 

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""" 

13 

14from __future__ import annotations 

15 

16from itertools import starmap 

17from typing import TYPE_CHECKING, Final, NamedTuple 

18 

19from ._html import _schema_compile, _schema_validate 

20 

21if TYPE_CHECKING: 

22 from pathlib import Path 

23 

24 from ._html import Document, Node 

25 

26__all__ = [ 

27 "RelaxNG", 

28 "SchemaValidationError", 

29 "ValidationError", 

30 "ValidationResult", 

31 "XMLSchema", 

32] 

33 

34 

35class ValidationError(NamedTuple): 

36 """ 

37 One schema violation. 

38 

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 """ 

44 

45 message: str 

46 path: str 

47 line: int 

48 type: str 

49 

50 

51class ValidationResult(NamedTuple): 

52 """ 

53 The outcome of validating a document: a ``valid`` flag and the errors found. 

54 

55 The result is truthy exactly when the document is valid, so ``if schema.validate(doc):`` 

56 reads naturally. 

57 """ 

58 

59 valid: bool 

60 errors: tuple[ValidationError, ...] 

61 

62 def __bool__(self) -> bool: 

63 """Whether the document is valid, so the result reads as a truth value.""" 

64 return self.valid 

65 

66 

67class SchemaValidationError(Exception): 

68 """Raised by :meth:`XMLSchema.assert_valid` / :meth:`RelaxNG.assert_valid` when a document is invalid.""" 

69 

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") 

74 

75 

76class _Schema: 

77 """Shared machinery for the two schema languages; not instantiated directly.""" 

78 

79 def __init__(self, compiled: object) -> None: 

80 self._compiled: Final = compiled 

81 

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))) 

86 

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] 

90 

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) 

96 

97 

98class XMLSchema(_Schema): 

99 """ 

100 A compiled XSD 1.0 schema. 

101 

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 """ 

107 

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))) 

111 

112 

113class RelaxNG(_Schema): 

114 """ 

115 A compiled RELAX NG schema (XML syntax). 

116 

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 """ 

127 

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)) 

133 

134 

135def _text(source: str | Node) -> str: 

136 return source if isinstance(source, str) else source.serialize()