Coverage for tests/test_end_to_end.py: 100.00%

85 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 23:22 +0200

1"""End-to-end tests for every combination of options.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import re 

7from typing import TYPE_CHECKING, Any 

8 

9import bs4 

10import pytest 

11from griffe import LinesCollection, ModulesCollection, TmpPackage, temporary_pypackage 

12from inline_snapshot import external_file, register_format_alias 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Iterator 

16 

17 from mkdocstrings_handlers.python import PythonHandler 

18 

19 

20register_format_alias(".html", ".txt") 

21 

22 

23def _normalize_html(html: str) -> str: 

24 soup = bs4.BeautifulSoup(html, features="html.parser") 

25 html = soup.prettify() # type: ignore[assignment] 

26 html = re.sub(r"\b(0x)[a-f0-9]+\b", r"\1...", html) 

27 html = re.sub(r"^(Build Date UTC ?:).+", r"\1...", html, flags=re.MULTILINE) 

28 html = re.sub(r"\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b", r"...", html) 

29 html = re.sub(r'(?<=id="cell-id=)\w+(?=")', r"...", html) 

30 return html # noqa: RET504 

31 

32 

33def _render(handler: PythonHandler, package: TmpPackage, final_options: dict[str, Any]) -> str: 

34 final_options.pop("handler", None) 

35 final_options.pop("session_handler", None) 

36 handler_options = final_options.copy() 

37 

38 # Some default options to make snapshots easier to review. 

39 handler_options.setdefault("heading_level", 1) 

40 handler_options.setdefault("show_root_heading", True) 

41 handler_options.setdefault("show_source", False) 

42 

43 options = handler.get_options(handler_options) 

44 

45 handler._paths = [str(package.tmpdir)] 

46 try: 

47 data = handler.collect(package.name, options) 

48 finally: 

49 # We're using a session handler, so we need to reset its state after each call. 

50 # This is not thread-safe, but pytest-xdist uses subprocesses, so it's fine. 

51 handler._modules_collection = ModulesCollection() 

52 handler._lines_collection = LinesCollection() 

53 handler._paths = [] 

54 

55 html = handler.render(data, options) 

56 return _normalize_html(html) 

57 

58 

59def _render_options(options: dict[str, Any]) -> str: 

60 return f"<!--\n{json.dumps(options, indent=2, sort_keys=True)}\n-->\n\n" 

61 

62 

63def _snapshot_file(group: str, options: dict[str, Any]) -> str: 

64 return f"snapshots/{group}/" + ",".join(f"{k}={v}" for k, v in sorted(options.items())) + ".html" 

65 

66 

67# Signature tests. 

68@pytest.fixture(name="signature_package", scope="session") 

69def _signature_package() -> Iterator[TmpPackage]: 

70 code = """ 

71 def module_function(a: int, b: str) -> None: 

72 '''Docstring for `module_function`.''' 

73 

74 def _private_function(a: int, b: str) -> None: 

75 '''Docstring for `_private_function`.''' 

76 

77 class Class: 

78 '''Docstring for `Class`.''' 

79 

80 def __init__(self, a: int, b: str) -> None: 

81 '''Docstring for `Class.__init__.''' 

82 

83 def method1(self, a: int, b: str) -> None: 

84 '''Docstring for `Class.method1`.''' 

85 """ 

86 with temporary_pypackage("signature_package", {"__init__.py": code}) as tmppkg: 

87 yield tmppkg 

88 

89 

90@pytest.mark.parametrize("show_signature_annotations", [True, False]) 

91@pytest.mark.parametrize("signature_crossrefs", [True, False]) 

92@pytest.mark.parametrize("separate_signature", [True, False]) 

93def test_end_to_end_for_signatures( 

94 session_handler: PythonHandler, 

95 signature_package: TmpPackage, 

96 show_signature_annotations: bool, 

97 signature_crossrefs: bool, 

98 separate_signature: bool, 

99) -> None: 

100 """Test rendering of a given theme's templates. 

101 

102 Parameters: 

103 identifier: Parametrized identifier. 

104 session_handler: Python handler (fixture). 

105 """ 

106 options = { 

107 "show_signature_annotations": show_signature_annotations, 

108 "signature_crossrefs": signature_crossrefs, 

109 "separate_signature": separate_signature, 

110 } 

111 html = _render_options(options) + _render(session_handler, signature_package, options) 

112 assert html == external_file(_snapshot_file("signatures", options), format=".txt") 

113 

114 

115# Signature overloads tests. 

116@pytest.fixture(name="overloads_package", scope="session") 

117def _overloads_package() -> Iterator[TmpPackage]: 

118 code = """ 

119 from typing_extensions import overload 

120 

121 @overload 

122 def foo(a: int, b: str) -> float: ... 

123 

124 @overload 

125 def foo(a: str, b: int) -> None: ... 

126 

127 def foo(a: str | int, b: int | str) -> float | None: 

128 '''Docstring for `foo`.''' 

129 

130 def bar(a: str, b: int | str) -> float | None: 

131 '''Docstring for `bar`.''' 

132 

133 class Class: 

134 '''Docstring for `Class`.''' 

135 

136 @overload 

137 def foo(self, a: int, b: str) -> float: ... 

138 

139 @overload 

140 def foo(self, a: str, b: int) -> None: ... 

141 

142 def foo(self, a: str | int, b: int | str) -> float | None: 

143 '''Docstring for `Class.foo`.''' 

144 

145 def bar(self, a: str, b: int | str) -> float | None: 

146 '''Docstring for `Class.bar`.''' 

147 """ 

148 with temporary_pypackage("overloads_package", {"__init__.py": code}) as tmppkg: 

149 yield tmppkg 

150 

151 

152@pytest.mark.parametrize("separate_signature", [True, False]) 

153@pytest.mark.parametrize("show_overloads", [True, False]) 

154@pytest.mark.parametrize("overloads_only", [True, False]) 

155def test_end_to_end_for_overloads( 

156 session_handler: PythonHandler, 

157 overloads_package: TmpPackage, 

158 separate_signature: bool, 

159 show_overloads: bool, 

160 overloads_only: bool, 

161) -> None: 

162 """Test rendering of a given theme's templates. 

163 

164 Parameters: 

165 identifier: Parametrized identifier. 

166 session_handler: Python handler (fixture). 

167 """ 

168 options = { 

169 "separate_signature": separate_signature, 

170 "show_overloads": show_overloads, 

171 "overloads_only": overloads_only, 

172 } 

173 html = _render_options(options) + _render(session_handler, overloads_package, options) 

174 assert html == external_file(_snapshot_file("overloads", options), format=".txt") 

175 

176 

177# Member tests. 

178@pytest.fixture(name="members_package", scope="session") 

179def _members_package() -> Iterator[TmpPackage]: 

180 code = """ 

181 '''Docstring for the package.''' 

182 

183 def module_function(a: int, b: str) -> None: 

184 '''Docstring for `module_function`.''' 

185 

186 class Class: 

187 '''Docstring for `Class`.''' 

188 

189 class NestedClass: 

190 '''Docstring for `NestedClass`.''' 

191 

192 class_attribute: int = 42 

193 '''Docstring for `Class.class_attribute`.''' 

194 

195 def __init__(self, a: int, b: str) -> None: 

196 '''Docstring for `Class.__init__`.''' 

197 self.instance_attribute = a + b 

198 '''Docstring for `Class.instance_attribute`.''' 

199 

200 def method1(self, a: int, b: str) -> None: 

201 '''Docstring for `Class.method1`.''' 

202 

203 def method2(self, a: int, b: str) -> None: 

204 '''Docstring for `Class.method2`.''' 

205 

206 module_attribute: int = 42 

207 '''Docstring for `module_attribute`.''' 

208 

209 class Subclass(Class): 

210 '''Docstring for `Subclass`.''' 

211 """ 

212 with temporary_pypackage("members_package", {"__init__.py": code}) as tmppkg: 

213 yield tmppkg 

214 

215 

216@pytest.mark.parametrize("inherited_members", [(), ("method1",), True, False]) 

217@pytest.mark.parametrize("members", [(), ("module_attribute",), True, False, None]) 

218@pytest.mark.parametrize("filters", [(), ("!module_attribute",), ("module_attribute",), "public", None]) 

219def test_end_to_end_for_members( 

220 session_handler: PythonHandler, 

221 members_package: TmpPackage, 

222 inherited_members: list[str] | bool | None, 

223 members: list[str] | bool | None, 

224 filters: list[str] | None, 

225) -> None: 

226 """Test rendering of a given theme's templates. 

227 

228 Parameters: 

229 identifier: Parametrized identifier. 

230 session_handler: Python handler (fixture). 

231 """ 

232 options = { 

233 "inherited_members": inherited_members, 

234 "members": members, 

235 "filters": filters, 

236 } 

237 html = _render_options(options) + _render(session_handler, members_package, options) 

238 assert html == external_file(_snapshot_file("members", options), format=".txt") 

239 

240 

241# Heading tests. 

242@pytest.fixture(name="headings_package", scope="session") 

243def _headings_package() -> Iterator[TmpPackage]: 

244 code = """ 

245 def module_function(a: int, b: str) -> None: 

246 pass 

247 

248 class Class: 

249 class_attribute: int = 42 

250 

251 def __init__(self, a: int, b: str) -> None: 

252 self.instance_attribute = a + b 

253 

254 def method1(self, a: int, b: str) -> None: 

255 pass 

256 

257 module_attribute: int = 42 

258 """ 

259 with temporary_pypackage("headings_package", {"__init__.py": code}) as tmppkg: 

260 yield tmppkg 

261 

262 

263@pytest.mark.parametrize("separate_signature", [True, False]) 

264@pytest.mark.parametrize("heading", ["", "Some heading"]) 

265def test_end_to_end_for_headings( 

266 session_handler: PythonHandler, 

267 headings_package: TmpPackage, 

268 separate_signature: bool, 

269 heading: str, 

270) -> None: 

271 """Test rendering of a given theme's templates. 

272 

273 Parameters: 

274 identifier: Parametrized identifier. 

275 session_handler: Python handler (fixture). 

276 """ 

277 options = { 

278 "separate_signature": separate_signature, 

279 "heading": heading, 

280 } 

281 extra = {"show_if_no_docstring": True, "members": False} 

282 html = _render_options(options) + _render(session_handler, headings_package, {**options, **extra}) 

283 assert html == external_file(_snapshot_file("headings", options), format=".txt")