Skip to content

CG-9706: Support imp.is_dynamic #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/codegen/sdk/codebase/node_classes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class NodeClasses:
function_call_cls: type[FunctionCall]
comment_cls: type[Comment]
bool_conversion: dict[bool, str]
dynamic_import_parent_types: set[str]
symbol_map: dict[str, type[Symbol]] = field(default_factory=dict)
expression_map: dict[str, type[Expression]] = field(default_factory=dict)
type_map: dict[str, type[Type] | dict[str, type[Type]]] = field(default_factory=dict)
Expand Down
13 changes: 13 additions & 0 deletions src/codegen/sdk/codebase/node_classes/py_node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,17 @@ def parse_subscript(node: TSNode, file_node_id, G, parent):
True: "True",
False: "False",
},
dynamic_import_parent_types={
"function_definition",
"if_statement",
"try_statement",
"with_statement",
"else_clause",
"for_statement",
"except_clause",
"while_statement",
"match_statement",
"case_clause",
"finally_clause",
},
)
15 changes: 15 additions & 0 deletions src/codegen/sdk/codebase/node_classes/ts_node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,19 @@ def parse_new(node: TSNode, *args):
True: "true",
False: "false",
},
dynamic_import_parent_types={
"function_declaration",
"method_definition",
"arrow_function",
"if_statement",
"try_statement",
"else_clause",
"catch_clause",
"finally_clause",
"while_statement",
"for_statement",
"do_statement",
"switch_case",
"switch_statement",
},
)
48 changes: 48 additions & 0 deletions src/codegen/sdk/core/import_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,54 @@ def imported_exports(self) -> list[Exportable]:
For symbol imports, contains only the single imported symbol.
"""

@property
@reader
def is_dynamic(self) -> bool:
"""Determines if this import is dynamically loaded based on its parent symbol.

A dynamic import is one that appears within control flow or scope-defining statements, such as:
- Inside function definitions
- Inside class definitions
- Inside if/else blocks
- Inside try/except blocks
- Inside with statements

Dynamic imports are only loaded when their containing block is executed, unlike
top-level imports which are loaded when the module is imported.

Examples:
Dynamic imports:
```python
def my_function():
import foo # Dynamic - only imported when function runs

if condition:
from bar import baz # Dynamic - only imported if condition is True

with context():
import qux # Dynamic - only imported within context
```

Static imports:
```python
import foo # Static - imported when module loads
from bar import baz # Static - imported when module loads
```

Returns:
bool: True if the import is dynamic (within a control flow or scope block),
False if it's a top-level import.
"""
curr = self.ts_node

# always traverses upto the module level
while curr:
if curr.type in self.G.node_classes.dynamic_import_parent_types:
return True
curr = curr.parent

return False

####################################################################################################################
# MANIPULATIONS
####################################################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_api_doc_generation_sanity(codebase, language: ProgrammingLanguage) -> N
other_lang = "TS" if language == ProgrammingLanguage.PYTHON else "Py"
# =====[ Python ]=====
docs = get_codegen_sdk_docs(language=language, codebase=codebase)
assert count_tokens(docs) < 50500
assert count_tokens(docs) < 50700
assert f"{lang}Function" in docs
assert f"{lang}Class" in docs
assert f"{other_lang}Function" not in docs
Expand Down
227 changes: 227 additions & 0 deletions tests/unit/codegen/sdk/python/import_resolution/test_is_dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
from codegen.sdk.codebase.factory.get_session import get_codebase_session
from codegen.sdk.enums import ProgrammingLanguage


def test_py_import_is_dynamic_in_function(tmpdir):
# language=python
content = """
def my_function():
import foo # Dynamic import inside function
from bar import baz # Another dynamic import

import static_import # Static import at module level
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

# Dynamic imports inside function
assert imports[0].is_dynamic # import foo
assert imports[1].is_dynamic # from bar import baz

# Static import at module level
assert not imports[2].is_dynamic # import static_import


def test_py_import_is_dynamic_in_if_block(tmpdir):
# language=python
content = """
import top_level # Static import

if condition:
import conditional # Dynamic import in if block
from x import y # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # top_level import
assert imports[1].is_dynamic # conditional import
assert imports[2].is_dynamic # from x import y


def test_py_import_is_dynamic_in_try_except(tmpdir):
# language=python
content = """
import static_first # Static import

try:
import dynamic_in_try # Dynamic import in try block
from x.y import z # Another dynamic import
except ImportError:
pass
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_first import
assert imports[1].is_dynamic # dynamic_in_try import
assert imports[2].is_dynamic # from x.y import z


def test_py_import_is_dynamic_in_with_block(tmpdir):
# language=python
content = """
import static_import # Static import

with context_manager():
import dynamic_in_with # Dynamic import in with block
from a.b import c # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_with import
assert imports[2].is_dynamic # from a.b import c


def test_py_import_is_dynamic_in_class_method(tmpdir):
# language=python
content = """
import static_import # Static import

class MyClass:
def my_method(self):
import dynamic_in_method # Dynamic import in method
from pkg import module # Another dynamic import

@classmethod
def class_method(cls):
import another_dynamic # Dynamic import in classmethod
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_method import
assert imports[2].is_dynamic # from pkg import module
assert imports[3].is_dynamic # another_dynamic import


def test_py_import_is_dynamic_in_nested_function(tmpdir):
# language=python
content = """
import static_import # Static import

def outer_function():
import dynamic_in_outer # Dynamic import in outer function

def inner_function():
import dynamic_in_inner # Dynamic import in inner function
from x import y # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_outer import
assert imports[2].is_dynamic # dynamic_in_inner import
assert imports[3].is_dynamic # from x import y


def test_py_import_is_dynamic_in_else_clause(tmpdir):
# language=python
content = """
import static_import # Static import

if condition:
pass
else:
import dynamic_in_else # Dynamic import in else clause
from x import y # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_else import
assert imports[2].is_dynamic # from x import y


def test_py_import_is_dynamic_in_except_clause(tmpdir):
# language=python
content = """
import static_import # Static import

try:
pass
except ImportError:
import dynamic_in_except # Dynamic import in except clause
from x import y # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_except import
assert imports[2].is_dynamic # from x import y


def test_py_import_is_dynamic_in_finally_clause(tmpdir):
# language=python
content = """
import static_import # Static import

try:
pass
except ImportError:
pass
finally:
import dynamic_in_finally # Dynamic import in finally clause
from x import y # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_finally import
assert imports[2].is_dynamic # from x import y


def test_py_import_is_dynamic_in_while_statement(tmpdir):
# language=python
content = """
import static_import # Static import

while condition:
import dynamic_in_while # Dynamic import in while loop
from a import b # Another dynamic import
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_while import
assert imports[2].is_dynamic # from a import b


def test_py_import_is_dynamic_in_match_case(tmpdir):
# language=python
content = """
import static_import # Static import

match value:
case 1:
import dynamic_in_case # Dynamic import in case clause
from x import y # Another dynamic import
case _:
import another_dynamic # Dynamic import in default case
"""
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}, programming_language=ProgrammingLanguage.PYTHON) as codebase:
file = codebase.get_file("test.py")
imports = file.imports

assert not imports[0].is_dynamic # static_import
assert imports[1].is_dynamic # dynamic_in_case import
assert imports[2].is_dynamic # from x import y
assert imports[3].is_dynamic # another_dynamic import
Loading