Skip to content

Commit af0c3be

Browse files
Add architecture guard for polling loop centralization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 17b2fc5 commit af0c3be

File tree

5 files changed

+52
-1
lines changed

5 files changed

+52
-1
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ This runs lint, format checks, compile checks, tests, and package build.
8888
- `tests/test_architecture_marker_usage.py` (architecture marker coverage across guard modules),
8989
- `tests/test_readme_examples_listing.py` (README example-listing consistency enforcement),
9090
- `tests/test_plain_type_guard_usage.py` (`str`/`int` guardrail enforcement via plain-type checks),
91-
- `tests/test_type_utils_usage.py` (type `__mro__` boundary centralization in `hyperbrowser/type_utils.py`).
91+
- `tests/test_type_utils_usage.py` (type `__mro__` boundary centralization in `hyperbrowser/type_utils.py`),
92+
- `tests/test_polling_loop_usage.py` (`while True` polling-loop centralization in `hyperbrowser/client/polling.py`).
9293

9394
## Code quality conventions
9495

tests/guardrail_ast_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,13 @@ def collect_list_keys_call_lines(module: ast.AST) -> list[int]:
5353
continue
5454
lines.append(node.lineno)
5555
return lines
56+
57+
58+
def collect_while_true_lines(module: ast.AST) -> list[int]:
59+
lines: list[int] = []
60+
for node in ast.walk(module):
61+
if not isinstance(node, ast.While):
62+
continue
63+
if isinstance(node.test, ast.Constant) and node.test.value is True:
64+
lines.append(node.lineno)
65+
return lines

tests/test_architecture_marker_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"tests/test_architecture_marker_usage.py",
1919
"tests/test_plain_type_guard_usage.py",
2020
"tests/test_type_utils_usage.py",
21+
"tests/test_polling_loop_usage.py",
2122
)
2223

2324

tests/test_guardrail_ast_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
collect_attribute_call_lines,
77
collect_list_keys_call_lines,
88
collect_name_call_lines,
9+
collect_while_true_lines,
910
)
1011

1112
pytestmark = pytest.mark.architecture
@@ -16,6 +17,8 @@
1617
values = list(mapping.keys())
1718
result = helper()
1819
other = obj.method()
20+
while True:
21+
break
1922
"""
2023
)
2124

@@ -30,3 +33,7 @@ def test_collect_attribute_call_lines_returns_attribute_calls():
3033

3134
def test_collect_list_keys_call_lines_returns_list_key_calls():
3235
assert collect_list_keys_call_lines(SAMPLE_MODULE) == [2]
36+
37+
38+
def test_collect_while_true_lines_returns_while_true_statements():
39+
assert collect_while_true_lines(SAMPLE_MODULE) == [5]

tests/test_polling_loop_usage.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from tests.guardrail_ast_utils import collect_while_true_lines, read_module_ast
6+
7+
pytestmark = pytest.mark.architecture
8+
9+
10+
ALLOWED_WHILE_TRUE_MODULES = {
11+
"hyperbrowser/client/polling.py",
12+
}
13+
14+
15+
def test_while_true_loops_are_centralized_to_polling_module():
16+
violations: list[str] = []
17+
polling_while_true_lines: list[int] = []
18+
19+
for module_path in sorted(Path("hyperbrowser").rglob("*.py")):
20+
module_ast = read_module_ast(module_path)
21+
while_true_lines = collect_while_true_lines(module_ast)
22+
if not while_true_lines:
23+
continue
24+
path_text = module_path.as_posix()
25+
if path_text in ALLOWED_WHILE_TRUE_MODULES:
26+
polling_while_true_lines.extend(while_true_lines)
27+
continue
28+
for line in while_true_lines:
29+
violations.append(f"{path_text}:{line}")
30+
31+
assert violations == []
32+
assert polling_while_true_lines != []

0 commit comments

Comments
 (0)