11import importlib .util
22import os
3+ import shutil
34import sys
45
6+ import pytest
7+
8+ PACKAGE_NAME = "starknet_py"
9+
510
611def _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 } \n class A:\n pass\n " )
57+ with open (os .path .join (module_path , "file_b.py" ), "w" ) as f :
58+ f .write (f"{ import_b } \n class 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