Skip to content

Commit c4c2b48

Browse files
committed
Refactor controller documentation test logic
Replaces the ControllerDocumentationChecker class with standalone functions for extracting controller classes and documented controllers. Simplifies the test structure and error reporting, improving readability and maintainability.
1 parent f5c74d5 commit c4c2b48

File tree

1 file changed

+68
-117
lines changed

1 file changed

+68
-117
lines changed

tests/pytest/test_controller_documentation.py

Lines changed: 68 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -5,143 +5,94 @@
55
import pytest
66
import yaml
77

8-
9-
class ControllerDocumentationChecker:
10-
"""Validates that all controller classes are documented."""
11-
12-
CONTROLLER_DIR = Path("shiny/playwright/controller")
13-
DOCS_CONFIG = Path("docs/_quartodoc-testing.yml")
14-
15-
SKIP_PATTERNS = {"Base", "Container", "Label", "StyleM"}
16-
CONTROLLER_BASE_PATTERNS = {
17-
"Base",
18-
"Container",
19-
"Label",
20-
"StyleM",
21-
"WidthLocM",
22-
"InputActionButton",
23-
"UiBase",
24-
"UiWithLabel",
25-
"UiWithContainer",
26-
}
27-
28-
def get_controller_classes(self) -> Set[str]:
29-
"""Extract public controller class names from the controller directory."""
30-
controller_classes: Set[str] = set()
31-
32-
for py_file in self.CONTROLLER_DIR.glob("*.py"):
33-
if py_file.name == "__init__.py":
34-
continue
35-
36-
try:
37-
controller_classes.update(self._extract_classes_from_file(py_file))
38-
except Exception as e:
39-
pytest.fail(f"Failed to parse {py_file}: {e}")
40-
41-
return controller_classes
42-
43-
def _extract_classes_from_file(self, py_file: Path) -> Set[str]:
44-
"""Extract controller classes from a Python file."""
45-
with open(py_file, encoding="utf-8") as f:
46-
tree = ast.parse(f.read())
47-
48-
classes: Set[str] = set()
49-
for node in ast.walk(tree):
50-
if isinstance(node, ast.ClassDef):
51-
if self._is_controller_class(node):
52-
classes.add(node.name)
53-
54-
return classes
55-
56-
def _is_controller_class(self, node: ast.ClassDef) -> bool:
57-
"""Check if a class definition represents a controller."""
58-
class_name = node.name
59-
60-
if class_name.startswith("_"):
61-
return False
62-
63-
if any(pattern in class_name for pattern in self.SKIP_PATTERNS):
64-
return False
65-
66-
if self._is_protocol_class(node):
67-
return False
68-
69-
return self._has_controller_base(node)
70-
71-
def _is_protocol_class(self, node: ast.ClassDef) -> bool:
72-
"""Check if class inherits from a Protocol."""
73-
return any(
74-
isinstance(base, ast.Name) and base.id.endswith("P") for base in node.bases
8+
CONTROLLER_DIR = Path("shiny/playwright/controller")
9+
DOCS_CONFIG = Path("docs/_quartodoc-testing.yml")
10+
SKIP_PATTERNS = {"Base", "Container", "Label", "StyleM"}
11+
CONTROLLER_BASE_PATTERNS = {
12+
"Base",
13+
"Container",
14+
"Label",
15+
"StyleM",
16+
"WidthLocM",
17+
"InputActionButton",
18+
"UiBase",
19+
"UiWithLabel",
20+
"UiWithContainer",
21+
}
22+
23+
24+
def _is_valid_controller_class(node: ast.ClassDef) -> bool:
25+
class_name = node.name
26+
base_names = {ast.unparse(base) for base in node.bases}
27+
28+
return (
29+
not class_name.startswith("_")
30+
and not any(pattern in class_name for pattern in SKIP_PATTERNS)
31+
and not any(base.endswith("P") for base in base_names if isinstance(base, str))
32+
and any(
33+
base.startswith("_") or any(p in base for p in CONTROLLER_BASE_PATTERNS)
34+
for base in base_names
7535
)
36+
)
7637

77-
def _has_controller_base(self, node: ast.ClassDef) -> bool:
78-
"""Check if class has controller base classes."""
79-
for base in node.bases:
80-
base_str = ast.unparse(base)
81-
82-
if base_str.startswith("_"):
83-
return True
8438

85-
if any(pattern in base_str for pattern in self.CONTROLLER_BASE_PATTERNS):
86-
return True
87-
88-
return False
89-
90-
def get_documented_controllers(self) -> Set[str]:
91-
"""Extract controller class names from documentation config."""
39+
def get_controller_classes() -> Set[str]:
40+
classes: Set[str] = set()
41+
for py_file in CONTROLLER_DIR.glob("*.py"):
42+
if py_file.name == "__init__.py":
43+
continue
9244
try:
93-
with open(self.DOCS_CONFIG, encoding="utf-8") as f:
94-
config = yaml.safe_load(f)
95-
except FileNotFoundError:
96-
pytest.fail(f"Documentation config not found: {self.DOCS_CONFIG}")
45+
tree = ast.parse(py_file.read_text(encoding="utf-8"))
46+
classes.update(
47+
node.name
48+
for node in ast.walk(tree)
49+
if isinstance(node, ast.ClassDef) and _is_valid_controller_class(node)
50+
)
9751
except Exception as e:
98-
pytest.fail(f"Failed to parse {self.DOCS_CONFIG}: {e}")
99-
100-
documented_controllers: Set[str] = set()
52+
pytest.fail(f"Failed to parse {py_file}: {e}")
53+
return classes
10154

102-
for section in config.get("quartodoc", {}).get("sections", []):
103-
for content in section.get("contents", []):
104-
if isinstance(content, str) and content.startswith(
105-
"playwright.controller."
106-
):
107-
class_name = content.split(".")[-1]
108-
documented_controllers.add(class_name)
10955

110-
return documented_controllers
56+
def get_documented_controllers() -> Set[str]:
57+
try:
58+
config = yaml.safe_load(DOCS_CONFIG.read_text(encoding="utf-8"))
59+
except Exception as e:
60+
pytest.fail(f"Failed to load or parse {DOCS_CONFIG}: {e}")
11161

112-
113-
@pytest.fixture
114-
def checker():
115-
"""Provide a ControllerDocumentationChecker instance."""
116-
return ControllerDocumentationChecker()
62+
return {
63+
content.split(".")[-1]
64+
for section in config.get("quartodoc", {}).get("sections", [])
65+
for content in section.get("contents", [])
66+
if isinstance(content, str) and content.startswith("playwright.controller.")
67+
}
11768

11869

119-
def test_all_controllers_are_documented(checker: ControllerDocumentationChecker):
120-
"""Verify all controller classes have documentation entries."""
121-
controller_classes = checker.get_controller_classes()
122-
documented_controllers = checker.get_documented_controllers()
70+
def test_all_controllers_are_documented():
71+
controller_classes = get_controller_classes()
72+
documented_controllers = get_documented_controllers()
12373

124-
missing_controllers = controller_classes - documented_controllers
125-
extra_documented = documented_controllers - controller_classes
74+
missing_from_docs = controller_classes - documented_controllers
75+
extra_in_docs = documented_controllers - controller_classes
12676

127-
error_messages: list[str] = []
77+
from typing import List
12878

129-
if missing_controllers:
79+
error_messages: List[str] = []
80+
if missing_from_docs:
13081
missing_list = "\n".join(
131-
f" - playwright.controller.{cls}" for cls in sorted(missing_controllers)
82+
sorted(f" - playwright.controller.{c}" for c in missing_from_docs)
13283
)
13384
error_messages.append(
134-
f"Controller classes missing from {checker.DOCS_CONFIG}:\n{missing_list}"
85+
f"Controllers missing from {DOCS_CONFIG}:\n{missing_list}"
13586
)
13687

137-
if extra_documented:
88+
if extra_in_docs:
13889
extra_list = "\n".join(
139-
f" - playwright.controller.{cls}" for cls in sorted(extra_documented)
90+
sorted(f" - playwright.controller.{c}" for c in extra_in_docs)
14091
)
141-
error_messages.append(f"Classes documented but not found:\n{extra_list}")
92+
error_messages.append(f"Extraneous classes in {DOCS_CONFIG}:\n{extra_list}")
14293

14394
if error_messages:
144-
pytest.fail("\n\n".join(error_messages))
95+
pytest.fail("\n\n".join(error_messages), pytrace=False)
14596

146-
assert controller_classes, "No controller classes found"
147-
assert documented_controllers, "No documented controllers found"
97+
assert controller_classes, "No controller classes were found."
98+
assert documented_controllers, "No documented controllers were found."

0 commit comments

Comments
 (0)