Skip to content

feat: Implementation for AttributeExpr and MethodCall #272

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions libs/astx-transpilers/src/astx_transpilers/python_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
42 changes: 42 additions & 0 deletions libs/astx-transpilers/tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions libs/astx/src/astx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AST,
ASTKind,
ASTNodes,
AttributeExpr,
DataType,
Expr,
ExprType,
Expand All @@ -42,6 +43,7 @@
FunctionPrototype,
FunctionReturn,
LambdaExpr,
MethodCall,
YieldExpr,
YieldFromExpr,
YieldStmt,
Expand Down Expand Up @@ -219,6 +221,7 @@ def get_version() -> str:
"AssignmentExpr",
"AsyncForRangeLoopExpr",
"AsyncForRangeLoopStmt",
"AttributeExpr",
"AugAssign",
"AwaitExpr",
"BinaryOp",
Expand Down Expand Up @@ -313,6 +316,7 @@ def get_version() -> str:
"LiteralUInt128",
"LiteralUTF8Char",
"LiteralUTF8String",
"MethodCall",
"Module",
"MutabilityKind",
"NamedExpr",
Expand Down
41 changes: 41 additions & 0 deletions libs/astx/src/astx/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ class ASTKind(Enum):
NorOpKind = -1204
XnorOpKind = -1205
NotOpKind = -1206
AttributeExprKind = -1300
MethodCallKind = -1301


class ASTMeta(type):
Expand Down Expand Up @@ -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)
63 changes: 63 additions & 0 deletions libs/astx/src/astx/callables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
21 changes: 21 additions & 0 deletions libs/astx/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
35 changes: 35 additions & 0 deletions libs/astx/tests/test_callables.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
FunctionPrototype,
FunctionReturn,
LambdaExpr,
MethodCall,
YieldExpr,
YieldFromExpr,
YieldStmt,
Expand Down Expand Up @@ -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())
Loading