Skip to content

Commit ad848cb

Browse files
tawsifkamalcodegen-bot
andauthored
CG-9706: Support imp.is_dynamic (#149)
# Motivation <!-- Why is this change necessary? --> - Checks if the import is dynamic by checking if it's in the body of a function, method, if statement, etc - Supports TSImport and PyImport --------- Co-authored-by: codegen-bot <[email protected]>
1 parent 105da0e commit ad848cb

File tree

7 files changed

+545
-1
lines changed

7 files changed

+545
-1
lines changed

src/codegen/sdk/codebase/node_classes/node_classes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class NodeClasses:
3333
function_call_cls: type[FunctionCall]
3434
comment_cls: type[Comment]
3535
bool_conversion: dict[bool, str]
36+
dynamic_import_parent_types: set[str]
3637
symbol_map: dict[str, type[Symbol]] = field(default_factory=dict)
3738
expression_map: dict[str, type[Expression]] = field(default_factory=dict)
3839
type_map: dict[str, type[Type] | dict[str, type[Type]]] = field(default_factory=dict)

src/codegen/sdk/codebase/node_classes/py_node_classes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,17 @@ def parse_subscript(node: TSNode, file_node_id, G, parent):
109109
True: "True",
110110
False: "False",
111111
},
112+
dynamic_import_parent_types={
113+
"function_definition",
114+
"if_statement",
115+
"try_statement",
116+
"with_statement",
117+
"else_clause",
118+
"for_statement",
119+
"except_clause",
120+
"while_statement",
121+
"match_statement",
122+
"case_clause",
123+
"finally_clause",
124+
},
112125
)

src/codegen/sdk/codebase/node_classes/ts_node_classes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,19 @@ def parse_new(node: TSNode, *args):
163163
True: "true",
164164
False: "false",
165165
},
166+
dynamic_import_parent_types={
167+
"function_declaration",
168+
"method_definition",
169+
"arrow_function",
170+
"if_statement",
171+
"try_statement",
172+
"else_clause",
173+
"catch_clause",
174+
"finally_clause",
175+
"while_statement",
176+
"for_statement",
177+
"do_statement",
178+
"switch_case",
179+
"switch_statement",
180+
},
166181
)

src/codegen/sdk/core/import_resolution.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,54 @@ def imported_exports(self) -> list[Exportable]:
381381
For symbol imports, contains only the single imported symbol.
382382
"""
383383

384+
@property
385+
@reader
386+
def is_dynamic(self) -> bool:
387+
"""Determines if this import is dynamically loaded based on its parent symbol.
388+
389+
A dynamic import is one that appears within control flow or scope-defining statements, such as:
390+
- Inside function definitions
391+
- Inside class definitions
392+
- Inside if/else blocks
393+
- Inside try/except blocks
394+
- Inside with statements
395+
396+
Dynamic imports are only loaded when their containing block is executed, unlike
397+
top-level imports which are loaded when the module is imported.
398+
399+
Examples:
400+
Dynamic imports:
401+
```python
402+
def my_function():
403+
import foo # Dynamic - only imported when function runs
404+
405+
if condition:
406+
from bar import baz # Dynamic - only imported if condition is True
407+
408+
with context():
409+
import qux # Dynamic - only imported within context
410+
```
411+
412+
Static imports:
413+
```python
414+
import foo # Static - imported when module loads
415+
from bar import baz # Static - imported when module loads
416+
```
417+
418+
Returns:
419+
bool: True if the import is dynamic (within a control flow or scope block),
420+
False if it's a top-level import.
421+
"""
422+
curr = self.ts_node
423+
424+
# always traverses upto the module level
425+
while curr:
426+
if curr.type in self.G.node_classes.dynamic_import_parent_types:
427+
return True
428+
curr = curr.parent
429+
430+
return False
431+
384432
####################################################################################################################
385433
# MANIPULATIONS
386434
####################################################################################################################

tests/unit/codegen/sdk/code_generation/test_api_doc_generation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_api_doc_generation_sanity(codebase, language: ProgrammingLanguage) -> N
4040
other_lang = "TS" if language == ProgrammingLanguage.PYTHON else "Py"
4141
# =====[ Python ]=====
4242
docs = get_codegen_sdk_docs(language=language, codebase=codebase)
43-
assert count_tokens(docs) < 50500
43+
assert count_tokens(docs) < 50700
4444
assert f"{lang}Function" in docs
4545
assert f"{lang}Class" in docs
4646
assert f"{other_lang}Function" not in docs
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from codegen.sdk.codebase.factory.get_session import get_codebase_session
2+
from codegen.sdk.enums import ProgrammingLanguage
3+
4+
5+
def test_py_import_is_dynamic_in_function(tmpdir):
6+
# language=python
7+
content = """
8+
def my_function():
9+
import foo # Dynamic import inside function
10+
from bar import baz # Another dynamic import
11+
12+
import static_import # Static import at module level
13+
"""
14+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
15+
file = codebase.get_file("test.py")
16+
imports = file.imports
17+
18+
# Dynamic imports inside function
19+
assert imports[0].is_dynamic # import foo
20+
assert imports[1].is_dynamic # from bar import baz
21+
22+
# Static import at module level
23+
assert not imports[2].is_dynamic # import static_import
24+
25+
26+
def test_py_import_is_dynamic_in_if_block(tmpdir):
27+
# language=python
28+
content = """
29+
import top_level # Static import
30+
31+
if condition:
32+
import conditional # Dynamic import in if block
33+
from x import y # Another dynamic import
34+
"""
35+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
36+
file = codebase.get_file("test.py")
37+
imports = file.imports
38+
39+
assert not imports[0].is_dynamic # top_level import
40+
assert imports[1].is_dynamic # conditional import
41+
assert imports[2].is_dynamic # from x import y
42+
43+
44+
def test_py_import_is_dynamic_in_try_except(tmpdir):
45+
# language=python
46+
content = """
47+
import static_first # Static import
48+
49+
try:
50+
import dynamic_in_try # Dynamic import in try block
51+
from x.y import z # Another dynamic import
52+
except ImportError:
53+
pass
54+
"""
55+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
56+
file = codebase.get_file("test.py")
57+
imports = file.imports
58+
59+
assert not imports[0].is_dynamic # static_first import
60+
assert imports[1].is_dynamic # dynamic_in_try import
61+
assert imports[2].is_dynamic # from x.y import z
62+
63+
64+
def test_py_import_is_dynamic_in_with_block(tmpdir):
65+
# language=python
66+
content = """
67+
import static_import # Static import
68+
69+
with context_manager():
70+
import dynamic_in_with # Dynamic import in with block
71+
from a.b import c # Another dynamic import
72+
"""
73+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
74+
file = codebase.get_file("test.py")
75+
imports = file.imports
76+
77+
assert not imports[0].is_dynamic # static_import
78+
assert imports[1].is_dynamic # dynamic_in_with import
79+
assert imports[2].is_dynamic # from a.b import c
80+
81+
82+
def test_py_import_is_dynamic_in_class_method(tmpdir):
83+
# language=python
84+
content = """
85+
import static_import # Static import
86+
87+
class MyClass:
88+
def my_method(self):
89+
import dynamic_in_method # Dynamic import in method
90+
from pkg import module # Another dynamic import
91+
92+
@classmethod
93+
def class_method(cls):
94+
import another_dynamic # Dynamic import in classmethod
95+
"""
96+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
97+
file = codebase.get_file("test.py")
98+
imports = file.imports
99+
100+
assert not imports[0].is_dynamic # static_import
101+
assert imports[1].is_dynamic # dynamic_in_method import
102+
assert imports[2].is_dynamic # from pkg import module
103+
assert imports[3].is_dynamic # another_dynamic import
104+
105+
106+
def test_py_import_is_dynamic_in_nested_function(tmpdir):
107+
# language=python
108+
content = """
109+
import static_import # Static import
110+
111+
def outer_function():
112+
import dynamic_in_outer # Dynamic import in outer function
113+
114+
def inner_function():
115+
import dynamic_in_inner # Dynamic import in inner function
116+
from x import y # Another dynamic import
117+
"""
118+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
119+
file = codebase.get_file("test.py")
120+
imports = file.imports
121+
122+
assert not imports[0].is_dynamic # static_import
123+
assert imports[1].is_dynamic # dynamic_in_outer import
124+
assert imports[2].is_dynamic # dynamic_in_inner import
125+
assert imports[3].is_dynamic # from x import y
126+
127+
128+
def test_py_import_is_dynamic_in_else_clause(tmpdir):
129+
# language=python
130+
content = """
131+
import static_import # Static import
132+
133+
if condition:
134+
pass
135+
else:
136+
import dynamic_in_else # Dynamic import in else clause
137+
from x import y # Another dynamic import
138+
"""
139+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
140+
file = codebase.get_file("test.py")
141+
imports = file.imports
142+
143+
assert not imports[0].is_dynamic # static_import
144+
assert imports[1].is_dynamic # dynamic_in_else import
145+
assert imports[2].is_dynamic # from x import y
146+
147+
148+
def test_py_import_is_dynamic_in_except_clause(tmpdir):
149+
# language=python
150+
content = """
151+
import static_import # Static import
152+
153+
try:
154+
pass
155+
except ImportError:
156+
import dynamic_in_except # Dynamic import in except clause
157+
from x import y # Another dynamic import
158+
"""
159+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
160+
file = codebase.get_file("test.py")
161+
imports = file.imports
162+
163+
assert not imports[0].is_dynamic # static_import
164+
assert imports[1].is_dynamic # dynamic_in_except import
165+
assert imports[2].is_dynamic # from x import y
166+
167+
168+
def test_py_import_is_dynamic_in_finally_clause(tmpdir):
169+
# language=python
170+
content = """
171+
import static_import # Static import
172+
173+
try:
174+
pass
175+
except ImportError:
176+
pass
177+
finally:
178+
import dynamic_in_finally # Dynamic import in finally clause
179+
from x import y # Another dynamic import
180+
"""
181+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
182+
file = codebase.get_file("test.py")
183+
imports = file.imports
184+
185+
assert not imports[0].is_dynamic # static_import
186+
assert imports[1].is_dynamic # dynamic_in_finally import
187+
assert imports[2].is_dynamic # from x import y
188+
189+
190+
def test_py_import_is_dynamic_in_while_statement(tmpdir):
191+
# language=python
192+
content = """
193+
import static_import # Static import
194+
195+
while condition:
196+
import dynamic_in_while # Dynamic import in while loop
197+
from a import b # Another dynamic import
198+
"""
199+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
200+
file = codebase.get_file("test.py")
201+
imports = file.imports
202+
203+
assert not imports[0].is_dynamic # static_import
204+
assert imports[1].is_dynamic # dynamic_in_while import
205+
assert imports[2].is_dynamic # from a import b
206+
207+
208+
def test_py_import_is_dynamic_in_match_case(tmpdir):
209+
# language=python
210+
content = """
211+
import static_import # Static import
212+
213+
match value:
214+
case 1:
215+
import dynamic_in_case # Dynamic import in case clause
216+
from x import y # Another dynamic import
217+
case _:
218+
import another_dynamic # Dynamic import in default case
219+
"""
220+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
221+
file = codebase.get_file("test.py")
222+
imports = file.imports
223+
224+
assert not imports[0].is_dynamic # static_import
225+
assert imports[1].is_dynamic # dynamic_in_case import
226+
assert imports[2].is_dynamic # from x import y
227+
assert imports[3].is_dynamic # another_dynamic import

0 commit comments

Comments
 (0)