9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
149 lines
4.8 KiB
Python
149 lines
4.8 KiB
Python
"""Regression tests for docs/detection/annotators.md tab structure."""
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent
|
|
while not (_REPO_ROOT / "pyproject.toml").exists():
|
|
_REPO_ROOT = _REPO_ROOT.parent
|
|
|
|
REPO_ROOT = _REPO_ROOT
|
|
|
|
EXPECTED_ANNOTATOR_TAB_GROUPS: dict[str, list[str]] = {
|
|
"Outlines": [
|
|
"Box",
|
|
"RoundBox",
|
|
"BoxCorner",
|
|
"Circle",
|
|
"Ellipse",
|
|
"Polygon",
|
|
],
|
|
"Shading": ["Color", "Halo", "Mask"],
|
|
"Markers": ["Dot", "Triangle"],
|
|
"Labels": ["Label", "RichLabel"],
|
|
"Transformative": ["Blur", "Pixelate"],
|
|
"Tracking & Aggregation": ["Trace", "HeatMap"],
|
|
"Others": [
|
|
"PercentageBar",
|
|
"Icon",
|
|
"Background Color",
|
|
"Comparison",
|
|
],
|
|
}
|
|
|
|
|
|
def _extract_annotator_tab_groups() -> dict[str, list[str]]:
|
|
"""Parse annotator tab structure from docs/detection/annotators.md.
|
|
|
|
Returns:
|
|
Mapping of category name to list of annotator tab labels within that
|
|
category.
|
|
"""
|
|
docs_path = REPO_ROOT / "docs" / "detection" / "annotators.md"
|
|
content = docs_path.read_text(encoding="utf-8")
|
|
|
|
start_marker = '=== "Outlines"'
|
|
end_marker = "Try Supervision Annotators on your own image"
|
|
|
|
start = content.find(start_marker)
|
|
if start == -1:
|
|
pytest.fail(
|
|
f"Could not find start marker {start_marker!r} in {docs_path} while "
|
|
"parsing annotator example tab groups."
|
|
)
|
|
|
|
end = content.find(end_marker, start)
|
|
if end == -1:
|
|
pytest.fail(
|
|
f"Could not find end marker {end_marker!r} in {docs_path} while "
|
|
"parsing annotator example tab groups."
|
|
)
|
|
if end <= start:
|
|
pytest.fail(
|
|
f"End marker {end_marker!r} must appear after start marker "
|
|
f"{start_marker!r} in {docs_path}."
|
|
)
|
|
|
|
example_section = content[start:end]
|
|
|
|
groups: dict[str, list[str]] = {}
|
|
current_group = None
|
|
|
|
for line in example_section.splitlines():
|
|
if match := re.match(r'^(?P<indent>\s*)=== "(?P<label>[^"]+)"\s*$', line):
|
|
indent = 1 if match.group("indent") else 0
|
|
label = match.group("label")
|
|
|
|
if indent == 0:
|
|
current_group = label
|
|
groups[current_group] = []
|
|
elif indent > 0 and current_group:
|
|
groups[current_group].append(label)
|
|
return groups
|
|
|
|
|
|
def test_all_expected_annotators_have_tab_entries() -> None:
|
|
"""Assert every annotator in EXPECTED_ANNOTATOR_TAB_GROUPS has a tab in the docs.
|
|
|
|
Tests flat membership only — does not enforce which category each annotator
|
|
belongs to. Update EXPECTED_ANNOTATOR_TAB_GROUPS when adding or removing
|
|
annotator tab entries.
|
|
"""
|
|
tab_groups = _extract_annotator_tab_groups()
|
|
actual_tabs = {tab for group in tab_groups.values() for tab in group}
|
|
expected_tabs = {
|
|
tab for group in EXPECTED_ANNOTATOR_TAB_GROUPS.values() for tab in group
|
|
}
|
|
assert actual_tabs == expected_tabs, (
|
|
f"Tab set mismatch. "
|
|
f"Missing from docs: {expected_tabs - actual_tabs!r}. "
|
|
f"Extra in docs (add to EXPECTED_ANNOTATOR_TAB_GROUPS): "
|
|
f"{actual_tabs - expected_tabs!r}."
|
|
)
|
|
|
|
|
|
def test_annotator_example_tab_groups_stay_within_material_limit() -> None:
|
|
"""Assert no tab group exceeds MkDocs Material's 20-tab rendering limit."""
|
|
tab_groups = _extract_annotator_tab_groups()
|
|
|
|
# Outer-tab guard is defense-in-depth; the original failure was strictly an
|
|
# inner-tab overflow (22 inner tabs in a single flat group).
|
|
assert len(tab_groups) <= 20
|
|
assert all(len(group) <= 20 for group in tab_groups.values())
|
|
|
|
|
|
def test_annotator_code_examples_have_no_tuple_assignment() -> None:
|
|
"""Assert no annotated_frame assignment wraps the call in a tuple.
|
|
|
|
Guards against the paren-comma typo: ``annotated_frame = (call(),)``
|
|
which creates a 1-tuple instead of the annotated image.
|
|
"""
|
|
docs_path = REPO_ROOT / "docs" / "detection" / "annotators.md"
|
|
content = docs_path.read_text(encoding="utf-8")
|
|
|
|
fenced_blocks = re.findall(r"```python\n(.*?)```", content, re.DOTALL)
|
|
|
|
for block_idx, block in enumerate(fenced_blocks, 1):
|
|
try:
|
|
tree = ast.parse(block)
|
|
except SyntaxError:
|
|
continue
|
|
|
|
for node in ast.walk(tree):
|
|
if (
|
|
isinstance(node, ast.Assign)
|
|
and any(
|
|
isinstance(t, ast.Name) and t.id == "annotated_frame"
|
|
for t in node.targets
|
|
)
|
|
and isinstance(node.value, ast.Tuple)
|
|
):
|
|
pytest.fail(
|
|
f"Block {block_idx}: 'annotated_frame' assigned a tuple "
|
|
"(paren-comma typo). "
|
|
"Use 'annotated_frame = call()' not 'annotated_frame = (call(),)'."
|
|
)
|