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
« 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."""
3from __future__ import annotations
5import json
6import re
7from typing import TYPE_CHECKING, Any
9import bs4
10import pytest
11from griffe import LinesCollection, ModulesCollection, TmpPackage, temporary_pypackage
12from inline_snapshot import external_file, register_format_alias
14if TYPE_CHECKING:
15 from collections.abc import Iterator
17 from mkdocstrings_handlers.python import PythonHandler
20register_format_alias(".html", ".txt")
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
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()
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)
43 options = handler.get_options(handler_options)
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 = []
55 html = handler.render(data, options)
56 return _normalize_html(html)
59def _render_options(options: dict[str, Any]) -> str:
60 return f"<!--\n{json.dumps(options, indent=2, sort_keys=True)}\n-->\n\n"
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"
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`.'''
74 def _private_function(a: int, b: str) -> None:
75 '''Docstring for `_private_function`.'''
77 class Class:
78 '''Docstring for `Class`.'''
80 def __init__(self, a: int, b: str) -> None:
81 '''Docstring for `Class.__init__.'''
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
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.
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")
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
121 @overload
122 def foo(a: int, b: str) -> float: ...
124 @overload
125 def foo(a: str, b: int) -> None: ...
127 def foo(a: str | int, b: int | str) -> float | None:
128 '''Docstring for `foo`.'''
130 def bar(a: str, b: int | str) -> float | None:
131 '''Docstring for `bar`.'''
133 class Class:
134 '''Docstring for `Class`.'''
136 @overload
137 def foo(self, a: int, b: str) -> float: ...
139 @overload
140 def foo(self, a: str, b: int) -> None: ...
142 def foo(self, a: str | int, b: int | str) -> float | None:
143 '''Docstring for `Class.foo`.'''
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
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.
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")
177# Member tests.
178@pytest.fixture(name="members_package", scope="session")
179def _members_package() -> Iterator[TmpPackage]:
180 code = """
181 '''Docstring for the package.'''
183 def module_function(a: int, b: str) -> None:
184 '''Docstring for `module_function`.'''
186 class Class:
187 '''Docstring for `Class`.'''
189 class NestedClass:
190 '''Docstring for `NestedClass`.'''
192 class_attribute: int = 42
193 '''Docstring for `Class.class_attribute`.'''
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`.'''
200 def method1(self, a: int, b: str) -> None:
201 '''Docstring for `Class.method1`.'''
203 def method2(self, a: int, b: str) -> None:
204 '''Docstring for `Class.method2`.'''
206 module_attribute: int = 42
207 '''Docstring for `module_attribute`.'''
209 class Subclass(Class):
210 '''Docstring for `Subclass`.'''
211 """
212 with temporary_pypackage("members_package", {"__init__.py": code}) as tmppkg:
213 yield tmppkg
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.
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")
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
248 class Class:
249 class_attribute: int = 42
251 def __init__(self, a: int, b: str) -> None:
252 self.instance_attribute = a + b
254 def method1(self, a: int, b: str) -> None:
255 pass
257 module_attribute: int = 42
258 """
259 with temporary_pypackage("headings_package", {"__init__.py": code}) as tmppkg:
260 yield tmppkg
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.
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")