Coverage for src/lexigram/graphql/schema/documentation.py: 36%

73 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""Automatic GraphQL documentation generation. 

2 

3This module provides utilities for generating documentation 

4from GraphQL schemas. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from typing import Any 

11 

12 

13@dataclass 

14class FieldDoc: 

15 """Documentation for a GraphQL field. 

16 

17 Attributes: 

18 name: Field name. 

19 description: Field description. 

20 type: Field type. 

21 args: Field arguments. 

22 """ 

23 

24 name: str 

25 description: str | None = None 

26 type: str = "" 

27 args: list[dict[str, Any]] = field(default_factory=list) 

28 

29 

30@dataclass 

31class TypeDoc: 

32 """Documentation for a GraphQL type. 

33 

34 Attributes: 

35 name: Type name. 

36 description: Type description. 

37 fields: Type fields. 

38 """ 

39 

40 name: str 

41 description: str | None = None 

42 fields: list[FieldDoc] = field(default_factory=list) 

43 

44 

45@dataclass 

46class SchemaDoc: 

47 """Complete schema documentation. 

48 

49 Attributes: 

50 query_type: Query type name. 

51 mutation_type: Mutation type name. 

52 subscription_type: Subscription type name. 

53 types: All type documentations. 

54 """ 

55 

56 query_type: str | None = None 

57 mutation_type: str | None = None 

58 subscription_type: str | None = None 

59 types: list[TypeDoc] = field(default_factory=list) 

60 

61 def to_markdown(self) -> str: 

62 """Generate Markdown documentation. 

63 

64 Returns: 

65 Markdown-formatted documentation. 

66 """ 

67 lines = ["# GraphQL Schema\n"] 

68 

69 # Root types 

70 if self.query_type: 

71 lines.append(f"**Query Type**: `{self.query_type}`") 

72 if self.mutation_type: 

73 lines.append(f"**Mutation Type**: `{self.mutation_type}`") 

74 if self.subscription_type: 

75 lines.append(f"**Subscription Type**: `{self.subscription_type}`") 

76 

77 lines.append("\n## Types\n") 

78 

79 # Types 

80 for type_doc in self.types: 

81 lines.append(f"### {type_doc.name}\n") 

82 

83 if type_doc.description: 

84 lines.append(f"{type_doc.description}\n") 

85 

86 if type_doc.fields: 

87 lines.append("| Field | Type | Description |") 

88 lines.append("|-------|------|-------------|") 

89 

90 for field_doc in type_doc.fields: 

91 desc = field_doc.description or "" 

92 lines.append(f"| {field_doc.name} | `{field_doc.type}` | {desc} |") 

93 

94 lines.append("") 

95 

96 return "\n".join(lines) 

97 

98 

99class SchemaDocumentationGenerator: 

100 """Generate documentation from GraphQL schemas. 

101 

102 Example: 

103 from lexigram.logging import get_logger 

104 

105 logger = get_logger(__name__) 

106 generator = SchemaDocumentationGenerator() 

107 doc = generator.generate(schema) 

108 

109 # Output as Markdown 

110 markdown = doc.to_markdown() 

111 logger.info("schema_documentation", doc_length=len(markdown)) 

112 """ 

113 

114 def generate(self, schema: Any) -> SchemaDoc: 

115 """Generate documentation from a schema. 

116 

117 Args: 

118 schema: GraphQL schema. 

119 

120 Returns: 

121 SchemaDoc with all documentation. 

122 """ 

123 doc = SchemaDoc() 

124 

125 # Get root types 

126 if hasattr(schema, "query"): 

127 doc.query_type = "Query" 

128 

129 if hasattr(schema, "mutation"): 

130 doc.mutation_type = "Mutation" 

131 

132 if hasattr(schema, "subscription"): 

133 doc.subscription_type = "Subscription" 

134 

135 # Get types 

136 if hasattr(schema, "types"): 

137 for type_obj in schema.types: 

138 type_doc = self._document_type(type_obj) 

139 if type_doc: 

140 doc.types.append(type_doc) 

141 

142 return doc 

143 

144 def _document_type(self, type_obj: Any) -> TypeDoc | None: 

145 """Document a single type. 

146 

147 Args: 

148 type_obj: Type object. 

149 

150 Returns: 

151 TypeDoc or None. 

152 """ 

153 if not hasattr(type_obj, "name"): 

154 return None 

155 

156 name = type_obj.name 

157 

158 # Skip internal types 

159 if name.startswith("__"): 

160 return None 

161 

162 type_doc = TypeDoc( 

163 name=name, 

164 description=getattr(type_obj, "description", None), 

165 ) 

166 

167 # Get fields 

168 if hasattr(type_obj, "fields"): 

169 for field_obj in type_obj.fields: 

170 field_doc = FieldDoc( 

171 name=getattr(field_obj, "name", ""), 

172 description=getattr(field_obj, "description", None), 

173 type=str(getattr(field_obj, "type", "")), 

174 ) 

175 type_doc.fields.append(field_doc) 

176 

177 return type_doc 

178 

179 

180def generate_schema_docs(schema: Any) -> str: 

181 """Convenience function to generate schema documentation. 

182 

183 Args: 

184 schema: GraphQL schema. 

185 

186 Returns: 

187 Markdown-formatted documentation. 

188 """ 

189 generator = SchemaDocumentationGenerator() 

190 doc = generator.generate(schema) 

191 return doc.to_markdown() 

192 

193 

194__all__ = [ 

195 "FieldDoc", 

196 "SchemaDoc", 

197 "SchemaDocumentationGenerator", 

198 "TypeDoc", 

199 "generate_schema_docs", 

200]