Skip to content

Commit 8417587

Browse files
Add AST import-utils module import boundary guard
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 229627e commit 8417587

5 files changed

Lines changed: 73 additions & 1 deletion

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ This runs lint, format checks, compile checks, tests, and package build.
9696
- `tests/test_ast_import_helper_secondary_import_boundary.py` (secondary AST import-helper import boundary enforcement across test modules),
9797
- `tests/test_ast_import_helper_usage.py` (shared AST import-helper usage enforcement across AST import-boundary guard suites),
9898
- `tests/test_ast_import_utils.py` (shared AST import-helper contract validation),
99+
- `tests/test_ast_import_utils_module_import_boundary.py` (shared AST import-helper module import boundary enforcement across test modules),
99100
- `tests/test_binary_file_open_helper_usage.py` (shared binary file open helper usage enforcement),
100101
- `tests/test_browser_use_payload_helper_usage.py` (browser-use payload helper usage enforcement),
101102
- `tests/test_ci_workflow_quality_gates.py` (CI guard-stage + make-target enforcement),

tests/ast_import_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import ast
22

33

4+
def imports_from_module(module_text: str, module: str) -> bool:
5+
module_ast = ast.parse(module_text)
6+
for node in module_ast.body:
7+
if isinstance(node, ast.ImportFrom) and node.module == module:
8+
return True
9+
if not isinstance(node, ast.Import):
10+
continue
11+
if any(alias.name == module for alias in node.names):
12+
return True
13+
return False
14+
15+
416
def imports_symbol_from_module(module_text: str, module: str, symbol: str) -> bool:
517
module_ast = ast.parse(module_text)
618
for node in module_ast.body:

tests/test_architecture_marker_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"tests/test_ast_import_helper_secondary_import_boundary.py",
2626
"tests/test_ast_import_helper_usage.py",
2727
"tests/test_ast_import_utils.py",
28+
"tests/test_ast_import_utils_module_import_boundary.py",
2829
"tests/test_guardrail_ast_utils.py",
2930
"tests/test_helper_transport_usage_boundary.py",
3031
"tests/test_manager_model_dump_usage.py",

tests/test_ast_import_utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from tests.ast_import_utils import (
44
calls_symbol,
5-
imports_symbol_from_module,
65
imports_collect_function_sources,
6+
imports_from_module,
77
imports_imports_collect_function_sources,
8+
imports_symbol_from_module,
89
)
910

1011
pytestmark = pytest.mark.architecture
@@ -28,6 +29,33 @@ def test_imports_collect_function_sources_ignores_non_matching_imports():
2829
assert imports_collect_function_sources(module_text) is False
2930

3031

32+
def test_imports_from_module_detects_expected_from_import():
33+
module_text = (
34+
"from tests.ast_import_utils import imports_collect_function_sources\n"
35+
"imports_collect_function_sources('dummy')\n"
36+
)
37+
38+
assert imports_from_module(module_text, module="tests.ast_import_utils") is True
39+
40+
41+
def test_imports_from_module_detects_expected_direct_import():
42+
module_text = (
43+
"import tests.ast_import_utils as import_utils\n"
44+
"import_utils.imports_collect_function_sources('dummy')\n"
45+
)
46+
47+
assert imports_from_module(module_text, module="tests.ast_import_utils") is True
48+
49+
50+
def test_imports_from_module_ignores_unrelated_module_imports():
51+
module_text = (
52+
"from tests.ast_function_source_utils import collect_function_sources\n"
53+
"collect_function_sources('dummy')\n"
54+
)
55+
56+
assert imports_from_module(module_text, module="tests.ast_import_utils") is False
57+
58+
3159
def test_imports_symbol_from_module_detects_expected_symbol():
3260
module_text = (
3361
"from tests.ast_import_utils import imports_collect_function_sources\n"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from tests.ast_import_utils import imports_from_module
6+
7+
pytestmark = pytest.mark.architecture
8+
9+
10+
EXPECTED_AST_IMPORT_UTILS_IMPORTERS = (
11+
"tests/test_ast_call_symbol_helper_import_boundary.py",
12+
"tests/test_ast_function_source_helper_usage.py",
13+
"tests/test_ast_function_source_import_boundary.py",
14+
"tests/test_ast_import_helper_import_boundary.py",
15+
"tests/test_ast_import_helper_secondary_import_boundary.py",
16+
"tests/test_ast_import_helper_usage.py",
17+
"tests/test_ast_import_utils.py",
18+
"tests/test_ast_import_utils_module_import_boundary.py",
19+
)
20+
21+
22+
def test_ast_import_utils_imports_are_centralized():
23+
discovered_modules: list[str] = []
24+
for module_path in sorted(Path("tests").glob("test_*.py")):
25+
module_text = module_path.read_text(encoding="utf-8")
26+
if not imports_from_module(module_text, module="tests.ast_import_utils"):
27+
continue
28+
discovered_modules.append(module_path.as_posix())
29+
30+
assert discovered_modules == list(EXPECTED_AST_IMPORT_UTILS_IMPORTERS)

0 commit comments

Comments
 (0)