Skip to content

Commit 2fec18c

Browse files
committed
fix: build full dotted name in _expr_name for module-qualified decorators/bases
_expr_name now recurses into ast.Attribute to produce the full dotted path (e.g. "dataclasses.dataclass", "typing.NamedTuple"). Callers use .endswith() so both bare and module-qualified forms are matched. Adds test for typing.NamedTuple base class.
1 parent 2b8bef5 commit 2fec18c

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

codeflash/verification/instrument_codeflash_capture.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
179179
# _orig_init = ClassName.__init__; then calling _orig_init(self, *args, **kwargs) in the wrapper
180180
for dec in node.decorator_list:
181181
dec_name = self._expr_name(dec)
182-
if dec_name == "dataclass":
182+
if dec_name is not None and dec_name.endswith("dataclass"):
183183
return node
184184

185185
# Skip NamedTuples — their __init__ is synthesized and cannot be overwritten.
186186
for base in node.bases:
187187
base_name = self._expr_name(base)
188-
if base_name == "NamedTuple":
188+
if base_name is not None and base_name.endswith("NamedTuple"):
189189
return node
190190

191191
# Create super().__init__(*args, **kwargs) call (use prebuilt AST fragments)
@@ -208,5 +208,6 @@ def _expr_name(self, node: ast.AST) -> str | None:
208208
if isinstance(node, ast.Call):
209209
return self._expr_name(node.func)
210210
if isinstance(node, ast.Attribute):
211-
return node.attr
211+
parent = self._expr_name(node.value)
212+
return f"{parent}.{node.attr}" if parent else node.attr
212213
return None

tests/test_instrument_codeflash_capture.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,34 @@ def magnitude(self):
471471
test_path.unlink(missing_ok=True)
472472

473473

474+
def test_module_qualified_namedtuple_skipped():
475+
"""typing.NamedTuple — module-qualified base class — should be skipped."""
476+
original_code = """
477+
import typing
478+
479+
class MyTuple(typing.NamedTuple):
480+
x: int
481+
y: str
482+
483+
def display(self):
484+
return f"{self.x}: {self.y}"
485+
"""
486+
test_path = (Path(__file__).parent.resolve() / "../code_to_optimize/tests/pytest/test_file.py").resolve()
487+
test_path.write_text(original_code)
488+
489+
function = FunctionToOptimize(
490+
function_name="display", file_path=test_path, parents=[FunctionParent(type="ClassDef", name="MyTuple")]
491+
)
492+
493+
try:
494+
instrument_codeflash_capture(function, {}, test_path.parent)
495+
modified_code = test_path.read_text()
496+
assert "super().__init__" not in modified_code
497+
assert "codeflash_capture" not in modified_code
498+
finally:
499+
test_path.unlink(missing_ok=True)
500+
501+
474502
def test_dataclass_with_explicit_init_still_instrumented():
475503
"""A dataclass that defines its own __init__ should still be instrumented normally."""
476504
original_code = """

0 commit comments

Comments
 (0)