diff --git a/libs/astx-transpilers/src/astx_transpilers/python_string.py b/libs/astx-transpilers/src/astx_transpilers/python_string.py index c6c0128..08f33de 100644 --- a/libs/astx-transpilers/src/astx_transpilers/python_string.py +++ b/libs/astx-transpilers/src/astx_transpilers/python_string.py @@ -72,6 +72,19 @@ def visit(self, node: astx.ASTNodes) -> str: lines = [self.visit(node) for node in node.nodes] return " ".join(lines) + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.AttributeExpr) -> str: + """Handle AttributeExpr nodes.""" + value = self.visit(node.value) + return f"{value}.{node.attr}" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.MethodCall) -> str: + """Handle MethodCall nodes.""" + obj = self.visit(node.obj) + args = ", ".join(self.visit(arg) for arg in node.args) + return f"{obj}.{node.method}({args})" + @dispatch # type: ignore[no-redef] def visit(self, node: astx.AsyncForRangeLoopExpr) -> str: """Handle AsyncForRangeLoopExpr nodes.""" diff --git a/libs/astx-transpilers/tests/test_python.py b/libs/astx-transpilers/tests/test_python.py index 91d82d3..ac3f9fe 100644 --- a/libs/astx-transpilers/tests/test_python.py +++ b/libs/astx-transpilers/tests/test_python.py @@ -1596,6 +1596,48 @@ def test_transpiler_do_while_expr() -> None: ) +def test_transpiler_attribute_expr() -> None: + """Test transpiling attribute expressions.""" + # Simple attribute access + obj = astx.Variable(name="obj") + attr_expr = astx.AttributeExpr(value=obj, attr="method") + + generated_code = translate(attr_expr) + expected_code = "obj.method" + + assert generated_code == expected_code, ( + f"Expected '{expected_code}', but got '{generated_code}'" + ) + + # Nested attribute access + attr1 = astx.AttributeExpr(value=obj, attr="attr1") + attr2 = astx.AttributeExpr(value=attr1, attr="attr2") + + generated_code = translate(attr2) + expected_code = "obj.attr1.attr2" + + assert generated_code == expected_code, ( + f"Expected '{expected_code}', but got '{generated_code}'" + ) + + +def test_transpiler_method_call() -> None: + """Test transpiling method calls.""" + obj = astx.Variable(name="obj") + + # Method call with arguments + method_call = astx.MethodCall( + obj=obj, method="method", args=[astx.LiteralInt32(42)] + ) + + generated_code = translate(method_call) + expected_code = "obj.method(42)" + + assert generated_code == expected_code, ( + f"Expected '{expected_code}', but got '{generated_code}'" + ) + + def test_transpiler_generator_expr() -> None: """Test astx.GeneratorExpr.""" comp_1 = astx.ComprehensionClause( diff --git a/libs/astx/src/astx/__init__.py b/libs/astx/src/astx/__init__.py index 8e1c2ea..a8c9cfd 100644 --- a/libs/astx/src/astx/__init__.py +++ b/libs/astx/src/astx/__init__.py @@ -19,6 +19,7 @@ AST, ASTKind, ASTNodes, + AttributeExpr, DataType, Expr, ExprType, @@ -42,6 +43,7 @@ FunctionPrototype, FunctionReturn, LambdaExpr, + MethodCall, YieldExpr, YieldFromExpr, YieldStmt, @@ -219,6 +221,7 @@ def get_version() -> str: "AssignmentExpr", "AsyncForRangeLoopExpr", "AsyncForRangeLoopStmt", + "AttributeExpr", "AugAssign", "AwaitExpr", "BinaryOp", @@ -313,6 +316,7 @@ def get_version() -> str: "LiteralUInt128", "LiteralUTF8Char", "LiteralUTF8String", + "MethodCall", "Module", "MutabilityKind", "NamedExpr", diff --git a/libs/astx/src/astx/base.py b/libs/astx/src/astx/base.py index d79e106..8de2786 100644 --- a/libs/astx/src/astx/base.py +++ b/libs/astx/src/astx/base.py @@ -205,6 +205,8 @@ class ASTKind(Enum): NorOpKind = -1204 XnorOpKind = -1205 NotOpKind = -1206 + AttributeExprKind = -1300 + MethodCallKind = -1301 class ASTMeta(type): @@ -519,3 +521,42 @@ def get_struct(self, simplified: bool = False) -> ReprStruct: key = "PARENTHESIZED-EXPR" value = self.value.get_struct(simplified) return self._prepare_struct(key, value, simplified) + + +@public +@typechecked +class AttributeExpr(Expr): + """ + Represents accessing a member of an object (e.g., obj.member). + + Equivalent to Python's ast.Attribute, this node represents attribute access + in object-oriented programming (accessing fields/methods of objects). + """ + + value: Expr + attr: str + + def __init__( + self, + value: Expr, + attr: str, + loc: SourceLocation = NO_SOURCE_LOCATION, + parent: Optional[ASTNodes] = None, + ) -> None: + """Initialize the AttributeExpr instance.""" + super().__init__(loc=loc, parent=parent) + self.value = value + self.attr = attr + self.kind = ASTKind.AttributeExprKind + + def __str__(self) -> str: + """Return a string representation of the attribute expression.""" + if hasattr(self.value, "name"): + return f"{self.value.name}.{self.attr}" + return f"{self.value}.{self.attr}" + + def get_struct(self, simplified: bool = False) -> ReprStruct: + """Return the AST structure of the attribute expression.""" + key = f"ATTRIBUTE[{self.attr}]" + value = self.value.get_struct(simplified) + return self._prepare_struct(key, value, simplified) diff --git a/libs/astx/src/astx/callables.py b/libs/astx/src/astx/callables.py index 7564981..c85077d 100644 --- a/libs/astx/src/astx/callables.py +++ b/libs/astx/src/astx/callables.py @@ -140,6 +140,69 @@ def get_struct(self, simplified: bool = False) -> ReprStruct: return self._prepare_struct(key, value, simplified) +@public +@typechecked +class MethodCall(Expr): + """ + Represents a method call on an object (e.g., obj.method(args)). + + This is a specialized form of function call for methods on objects. + """ + + obj: Expr + method: str + args: list[Expr] + + def __init__( + self, + obj: Expr, + method: str, + args: list[Expr] = [], + loc: SourceLocation = NO_SOURCE_LOCATION, + parent: Optional[ASTNodes] = None, + ) -> None: + """Initialize the MethodCall instance.""" + super().__init__(loc=loc, parent=parent) + self.obj = obj + self.method = method + self.args = args + self.kind = ASTKind.MethodCallKind + + def __str__(self) -> str: + """Return a string representation of the method call.""" + if hasattr(self.obj, "name"): + obj_str = self.obj.name + else: + obj_str = str(self.obj) + + args_str = [] + for arg in self.args: + if hasattr(arg, "value") and isinstance( + getattr(arg, "value"), (int, float, str, bool) + ): + args_str.append(str(arg.value)) + elif hasattr(arg, "name"): + args_str.append(arg.name) + else: + args_str.append(str(arg)) + + return f"{obj_str}.{self.method}({', '.join(args_str)})" + + def get_struct(self, simplified: bool = False) -> ReprStruct: + """Return the AST structure of the method call.""" + args_struct = [arg.get_struct(simplified) for arg in self.args] + obj_struct = self.obj.get_struct(simplified) + + key = f"METHOD-CALL[{self.method}]" + value_dict = { + "object": obj_struct, + "args": cast(ReprStruct, args_struct), + } + value = cast(ReprStruct, value_dict) + + return self._prepare_struct(key, value, simplified) + + @public @typechecked class FunctionPrototype(StatementType): diff --git a/libs/astx/tests/test_base.py b/libs/astx/tests/test_base.py index 40f62f1..43db3dd 100644 --- a/libs/astx/tests/test_base.py +++ b/libs/astx/tests/test_base.py @@ -103,6 +103,27 @@ def test_struct_representation() -> None: assert struct == {"IDENTIFIER[test_var]": "test_var"} +def test_attribute_expr_basic() -> None: + """Test basic attribute access.""" + obj = astx.Variable(name="obj") + attr_expr = astx.AttributeExpr(value=obj, attr="method") + + assert str(attr_expr) == "obj.method" + struct = attr_expr.get_struct(simplified=True) + assert isinstance(struct, dict) + key = next(iter(struct.keys())) + assert key == "ATTRIBUTE[method]" + + +def test_attribute_expr_nested() -> None: + """Test nested attribute access (obj.attr1.attr2).""" + obj = astx.Variable(name="obj") + attr1 = astx.AttributeExpr(value=obj, attr="attr1") + attr2 = astx.AttributeExpr(value=attr1, attr="attr2") + + assert str(attr2) == "obj.attr1.attr2" + + def test_parenthesized_expr_1() -> None: """Test ParenthesizedExpr 1.""" node = astx.ParenthesizedExpr( diff --git a/libs/astx/tests/test_callables.py b/libs/astx/tests/test_callables.py index 94564a6..ec07474 100644 --- a/libs/astx/tests/test_callables.py +++ b/libs/astx/tests/test_callables.py @@ -13,6 +13,7 @@ FunctionPrototype, FunctionReturn, LambdaExpr, + MethodCall, YieldExpr, YieldFromExpr, YieldStmt, @@ -239,3 +240,37 @@ def test_yield_stmt_in_generator_block() -> None: assert isinstance(gen_block[1], WhileStmt) assert str(gen_block[1].body[0]) visualize(gen_block.get_struct()) + + +def test_method_call() -> None: + """Test the MethodCall class.""" + # Setup an object + obj = Variable(name="obj") + + # Method call with no arguments + method_call_empty = MethodCall(obj=obj, method="empty_method") + + assert str(method_call_empty) == "obj.empty_method()" + assert method_call_empty.get_struct() + assert method_call_empty.get_struct(simplified=True) + + # Method call with arguments + method_call_with_args = MethodCall( + obj=obj, + method="method_with_args", + args=[LiteralInt32(42), Variable(name="x")], + ) + + assert str(method_call_with_args) == "obj.method_with_args(42, x)" + assert method_call_with_args.get_struct() + assert method_call_with_args.get_struct(simplified=True) + + # Nested method calls (chained methods) + inner_method = MethodCall(obj=obj, method="inner") + outer_method = MethodCall(obj=inner_method, method="outer") + + assert "outer()" in str(outer_method) + assert outer_method.get_struct() + assert outer_method.get_struct(simplified=True) + + visualize(method_call_with_args.get_struct())