Skip to content

Commit d18a671

Browse files
Fix checking circular imports (#1668)
1 parent f86f6cb commit d18a671

File tree

1 file changed

+82
-5
lines changed

1 file changed

+82
-5
lines changed

circular.py

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import importlib.util
22
import os
3+
import shutil
34
import sys
45

6+
import pytest
7+
8+
PACKAGE_NAME = "starknet_py"
9+
510

611
def _import_from_path(module_name, file_path):
712
spec = importlib.util.spec_from_file_location(module_name, file_path)
@@ -10,9 +15,81 @@ def _import_from_path(module_name, file_path):
1015
spec.loader.exec_module(module)
1116

1217

13-
def test_circular_imports():
14-
for path, _, files in os.walk("starknet_py"):
18+
def assert_no_circular_imports():
19+
for path, _, files in os.walk(PACKAGE_NAME):
1520
py_files = [f for f in files if f.endswith(".py")]
16-
for file_ in py_files:
17-
file_path = os.path.join(path, file_)
18-
_import_from_path(file_, file_path)
21+
for file in py_files:
22+
file_path = os.path.join(path, file)
23+
relative_path = os.path.relpath(file_path, PACKAGE_NAME)
24+
module_path_no_ext = relative_path.removesuffix(".py")
25+
26+
# Handle __init__.py files specially
27+
if module_path_no_ext.endswith("__init__"):
28+
module_path_no_init = module_path_no_ext.removesuffix(
29+
"__init__"
30+
).rstrip(os.sep)
31+
32+
# Top-level __init__.py gives empty module path
33+
if not module_path_no_init:
34+
module_name = PACKAGE_NAME
35+
else:
36+
dotted_module_path = module_path_no_init.replace(os.sep, ".")
37+
module_name = f"{PACKAGE_NAME}.{dotted_module_path}"
38+
else:
39+
dotted_module_path = module_path_no_ext.replace(os.sep, ".")
40+
module_name = f"{PACKAGE_NAME}.{dotted_module_path}"
41+
42+
_import_from_path(module_name, file_path)
43+
44+
45+
def test_circular_imports_absent():
46+
assert_no_circular_imports()
47+
48+
49+
def _run_circular_import_test(module_name, import_a, import_b):
50+
module_path = os.path.join(PACKAGE_NAME, module_name)
51+
os.makedirs(module_path, exist_ok=True)
52+
try:
53+
with open(os.path.join(module_path, "__init__.py"), "w") as f:
54+
f.write("")
55+
with open(os.path.join(module_path, "file_a.py"), "w") as f:
56+
f.write(f"{import_a}\nclass A:\n pass\n")
57+
with open(os.path.join(module_path, "file_b.py"), "w") as f:
58+
f.write(f"{import_b}\nclass B:\n pass\n")
59+
error_regex = (
60+
rf"(?:"
61+
rf"cannot import name 'A' from '{PACKAGE_NAME}.{module_name}.file_a' \(.*{PACKAGE_NAME}[\\/]+{module_name}[\\/]+file_a\.py\)"
62+
rf"|"
63+
rf"cannot import name 'B' from '{PACKAGE_NAME}.{module_name}.file_b' \(.*{PACKAGE_NAME}[\\/]+{module_name}[\\/]+file_b\.py\)"
64+
rf")"
65+
)
66+
with pytest.raises(ImportError, match=error_regex):
67+
assert_no_circular_imports()
68+
finally:
69+
# Clean up temporary files
70+
if os.path.exists(module_path):
71+
shutil.rmtree(module_path)
72+
sys.modules.pop(f"{PACKAGE_NAME}.{module_name}.file_a", None)
73+
sys.modules.pop(f"{PACKAGE_NAME}.{module_name}.file_b", None)
74+
sys.modules.pop(f"{PACKAGE_NAME}.{module_name}", None)
75+
76+
77+
def test_circular_imports_present():
78+
_run_circular_import_test(
79+
"module_x",
80+
f"from {PACKAGE_NAME}.module_x.file_b import B",
81+
f"from {PACKAGE_NAME}.module_x.file_a import A",
82+
)
83+
84+
85+
def test_circular_imports_present_with_relative_imports():
86+
# This test verifies that circular import detection works correctly when the problematic modules use
87+
# relative imports (e.g., `from .file_b import B`) rather than
88+
# absolute imports (e.g., `from starknet_py.module.file_b import B`),
89+
# which was tested in the previous test case.
90+
91+
_run_circular_import_test(
92+
"module_y",
93+
"from .file_b import B",
94+
"from .file_a import A",
95+
)

0 commit comments

Comments
 (0)