Coverage for pyTooling/Documentation/Sphinx/XSDSchemaGraph.py: 99%

57 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ _ _ _ __ ___ ___ _ __ | |_ __ _| |_(_) ___ _ __ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \ / __| | | | '_ ` _ \ / _ \ '_ \| __/ _` | __| |/ _ \| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | (_) | (__| |_| | | | | | | __/ | | | || (_| | |_| | (_) | | | |# 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___/ \___|\__,_|_| |_| |_|\___|_| |_|\__\__,_|\__|_|\___/|_| |_|# 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32A Sphinx directive drawing an XML schema as a Graphviz graph. 

33 

34A schema's source says what a document may contain, in a shape that hides the structure: a reader looking for *what 

35contains what* has to follow named types through the file. This directive draws that structure instead: 

36 

37.. code-block:: ReST 

38 

39 .. xsd-graph:: ../../pyTooling/Resources/TestReport-v0.1.xsd 

40 :caption: The types of TestReport-v0.1.xsd. 

41 

42The model comes from :mod:`xmlschema`, so the picture is the schema as a validator sees it rather than as its source 

43text is laid out, and it is drawn at build time from the shipped file, so it cannot drift from it. 

44 

45.. attention:: 

46 

47 :mod:`xmlschema` is imported when the directive **runs**, not when this module is imported, so a project using the 

48 extension for its roles alone doesn't need it installed. It is part of the ``sphinx`` extra. 

49 

50.. seealso:: 

51 

52 :mod:`pyTooling.Documentation.Sphinx.SchemaGraph` 

53 |rarr| The graph and the directive's base-class, shared by every schema language. 

54""" 

55from __future__ import annotations 

56 

57from pathlib import Path 

58from typing import TYPE_CHECKING, Generator 

59 

60from pyTooling.Decorators import export 

61from pyTooling.Exceptions import MissingDependencyError 

62from pyTooling.Documentation.Sphinx.SchemaGraph import DotGraph, SchemaGraph 

63 

64if TYPE_CHECKING: # pragma: no cover 

65 from xmlschema.validators import XsdElement, XsdGroup, XsdType 

66 

67 

68@export 

69class XSDSchemaGraph(SchemaGraph): 

70 """ 

71 The ``xsd-graph`` directive: an XML schema, drawn from the schema itself. 

72 

73 One argument, the path of the schema relative to the document using the directive; ``:caption:`` puts a caption 

74 under the diagram. 

75 """ 

76 

77 directiveName: str = "xsd-graph" #: Name the directive is invoked by. 

78 

79 @staticmethod 

80 def _TypeName(xsdType: XsdType) -> str: 

81 """ 

82 Return a readable name for a type. 

83 

84 :param xsdType: The type to name. 

85 :returns: ``xsd:string`` for a builtin type, the local name for a named one, ``(anonymous)`` otherwise. 

86 """ 

87 if (name := xsdType.name) is None: 

88 return "(anonymous)" 

89 

90 if (localName := name.removeprefix("{http://www.w3.org/2001/XMLSchema}")) != name: 

91 return f"xsd:{localName}" 

92 

93 return name 

94 

95 @staticmethod 

96 def _Cardinality(element: XsdElement) -> str: 

97 """ 

98 Render an element's occurrence. 

99 

100 :param element: The element to render the occurrence of. 

101 :returns: ``lower..upper``, with ``*`` for an unbounded upper limit. 

102 """ 

103 lower, upper = element.occurs 

104 

105 return f"{lower}..{'*' if upper is None else upper}" 

106 

107 @classmethod 

108 def _RenderGraph(cls, schemaFile: Path) -> str: 

109 """ 

110 Render an XML schema as a Graphviz graph. 

111 

112 Every complex type becomes a record of three compartments - its name, its attributes, and its simple-typed child 

113 elements with their cardinality - and every complex-typed child element becomes an edge, so containment and 

114 recursion are visible as edges rather than as repeated type names. A simple type earns a node of its own only 

115 when it is an enumeration, because its values are what a type name cannot say. 

116 

117 :param schemaFile: Path of the schema to render. 

118 :returns: The graph in the DOT language. 

119 :raises MissingDependencyError: If :mod:`xmlschema` isn't installed. 

120 """ 

121 try: 

122 from xmlschema import XMLSchema 

123 from xmlschema.validators import XsdElement, XsdGroup 

124 except ImportError as ex: # pragma: no cover 

125 raise MissingDependencyError(dependency="xmlschema", extra="sphinx") from ex 

126 

127 def childElements(group: XsdGroup) -> Generator[XsdElement, None, None]: 

128 """ 

129 Yield every element of a content model, flattening the sequences and choices in between. 

130 

131 :param group: The content model to walk. 

132 :returns: Generator of the elements it holds, at any depth. 

133 """ 

134 for child in group: 

135 if isinstance(child, XsdGroup): 

136 yield from childElements(child) 

137 elif isinstance(child, XsdElement): 137 ↛ 134line 137 didn't jump to line 134 because the condition on line 137 was always true

138 yield child 

139 

140 schema = XMLSchema(str(schemaFile)) 

141 complexTypes = {name: xsdType for name, xsdType in schema.types.items() if xsdType.is_complex()} 

142 # sorted, because a set's iteration order varies between interpreter runs and a graph that is redrawn 

143 # identically is what lets a rebuilt page be compared to the one before it 

144 enumerations = sorted( 

145 name for name, xsdType in schema.types.items() if xsdType.is_simple() and xsdType.enumeration is not None 

146 ) 

147 

148 graph = DotGraph() 

149 

150 for name, xsdType in complexTypes.items(): 

151 attributes = [ 

152 f"{attribute} : {cls._TypeName(xsdType.attributes[attribute].type)}" for attribute in xsdType.attributes 

153 ] 

154 elements = [ 

155 f"{child.name} : {cls._TypeName(child.type)} [{cls._Cardinality(child)}]" 

156 for child in childElements(xsdType.content) if child.type.is_simple() 

157 ] 

158 graph.AddRecord(name, name, (attributes, elements)) 

159 

160 graph.AddSeparator() 

161 for name, xsdType in complexTypes.items(): 

162 for child in childElements(xsdType.content): 

163 if child.type.is_complex(): 

164 graph.AddEdge(name, cls._TypeName(child.type), label=f"{child.name} [{cls._Cardinality(child)}]") 

165 

166 graph.AddSeparator() 

167 for name in enumerations: 

168 graph.AddRecord(name, name, (schema.types[name].enumeration,), style="filled", fillcolor="#f0f0f0") 

169 

170 for name, xsdType in complexTypes.items(): 

171 used = {xsdType.attributes[attribute].type.name for attribute in xsdType.attributes} 

172 used |= {child.type.name for child in childElements(xsdType.content) if child.type.is_simple()} 

173 for usedType in sorted(usedType for usedType in used if usedType in enumerations): 

174 graph.AddEdge(name, usedType, style="dashed", arrowhead="open", constraint="false") 

175 

176 graph.AddSeparator() 

177 for name, element in schema.elements.items(): 

178 graph.AddNode(f"<{name}>", name, shape="doublecircle", style="filled", fillcolor="#e8e8ff") 

179 graph.AddEdge(f"<{name}>", cls._TypeName(element.type), label="root") 

180 

181 return str(graph)