Skip to content

Commit f579fbf

Browse files
committed
Add Test + Fix Ast Rewrite
1 parent e19476f commit f579fbf

File tree

2 files changed

+106
-29
lines changed

2 files changed

+106
-29
lines changed

cppython/plugins/conan/builder.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ def __init__(self, dependencies: list[ConanDependency]) -> None:
2020
def _create_requires_assignment(self) -> cst.Assign:
2121
"""Create a `requires` assignment statement."""
2222
return cst.Assign(
23-
targets=[cst.AssignTarget(cst.Name('requires'))],
24-
value=cst.List([
25-
cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies
26-
]),
23+
targets=[cst.AssignTarget(cst.Name(value='requires'))],
24+
value=cst.List(
25+
[cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies]
26+
),
2727
)
2828

2929
def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement:
@@ -56,24 +56,23 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef:
5656
for body_statement_line in updated_node.body.body:
5757
if not isinstance(body_statement_line, cst.SimpleStatementLine):
5858
continue
59-
60-
assignment_statement = body_statement_line.body[0]
61-
if not isinstance(assignment_statement, cst.Assign):
62-
continue
63-
64-
for target in assignment_statement.targets:
65-
if not isinstance(target.target, cst.Name) or target.target.value != 'requires':
59+
for assignment_statement in body_statement_line.body:
60+
if not isinstance(assignment_statement, cst.Assign):
6661
continue
67-
68-
return self._replace_requires(updated_node, body_statement_line, assignment_statement)
62+
for target in assignment_statement.targets:
63+
if not isinstance(target.target, cst.Name) or target.target.value != 'requires':
64+
continue
65+
# Replace only the assignment within the SimpleStatementLine
66+
return self._replace_requires(updated_node, body_statement_line, assignment_statement)
6967

7068
# Find the last attribute assignment before methods
7169
last_attribute = None
7270
for body_statement_line in updated_node.body.body:
7371
if not isinstance(body_statement_line, cst.SimpleStatementLine):
7472
break
75-
assignment_statement = body_statement_line.body[0]
76-
if not isinstance(assignment_statement, cst.Assign):
73+
if not body_statement_line.body:
74+
break
75+
if not isinstance(body_statement_line.body[0], cst.Assign):
7776
break
7877
last_attribute = body_statement_line
7978

@@ -89,29 +88,27 @@ def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef:
8988
new_body.insert(index + 1, new_statement)
9089
else:
9190
new_body = [new_statement] + list(updated_node.body.body)
92-
9391
return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body))
9492

9593
def _replace_requires(
9694
self, updated_node: cst.ClassDef, body_statement_line: cst.SimpleStatementLine, assignment_statement: cst.Assign
9795
) -> cst.ClassDef:
98-
"""Replace the existing 'requires' assignment with a new one.
96+
"""Replace the existing 'requires' assignment with a new one, preserving other statements on the same line."""
97+
new_value = cst.List(
98+
[cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies]
99+
)
100+
new_assignment = assignment_statement.with_changes(value=new_value)
99101

100-
Args:
101-
updated_node (cst.ClassDef): The class definition to update.
102-
body_statement_line (cst.SimpleStatementLine): The body item containing the assignment.
103-
assignment_statement (cst.Assign): The existing assignment statement.
102+
# Replace only the relevant assignment in the SimpleStatementLine
103+
new_body = [
104+
new_assignment if statement is assignment_statement else statement for statement in body_statement_line.body
105+
]
106+
new_statement_line = body_statement_line.with_changes(body=new_body)
104107

105-
Returns:
106-
cst.ClassDef: The updated class definition.
107-
"""
108-
new_value = cst.List([
109-
cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies
110-
])
111-
new_assignment = assignment_statement.with_changes(value=new_value)
108+
# Replace the statement line in the class body
112109
return updated_node.with_changes(
113110
body=updated_node.body.with_changes(
114-
body=[new_assignment if item is body_statement_line else item for item in updated_node.body.body]
111+
body=[new_statement_line if item is body_statement_line else item for item in updated_node.body.body]
115112
)
116113
)
117114

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Tests for the AST transformer that modifies ConanFile classes."""
2+
3+
import ast
4+
from textwrap import dedent
5+
6+
import libcst as cst
7+
8+
from cppython.plugins.conan.builder import RequiresTransformer
9+
from cppython.plugins.conan.schema import ConanDependency
10+
11+
12+
class TestTransformer:
13+
"""Unit tests for the RequiresTransformer."""
14+
15+
class MockDependency(ConanDependency):
16+
"""A dummy dependency class for testing."""
17+
18+
@staticmethod
19+
def requires() -> str:
20+
"""Return a dummy requires string."""
21+
return 'test/1.2.3'
22+
23+
@staticmethod
24+
def test_add_requires_when_missing() -> None:
25+
"""Test that the transformer adds requires when missing."""
26+
dependency = TestTransformer.MockDependency(name='test')
27+
28+
code = """
29+
class MyFile(ConanFile):
30+
name = "test"
31+
version = "1.0"
32+
"""
33+
34+
module = cst.parse_module(dedent(code))
35+
transformer = RequiresTransformer([dependency])
36+
modified = module.visit(transformer)
37+
assert 'requires = ["test/1.2.3"]' in modified.code
38+
39+
# Verify the resulting code is valid Python syntax
40+
ast.parse(modified.code)
41+
42+
@staticmethod
43+
def test_replace_existing_requires() -> None:
44+
"""Test that the transformer replaces existing requires."""
45+
dependency = TestTransformer.MockDependency(name='test')
46+
47+
code = """
48+
class MyFile(ConanFile):
49+
name = "test"
50+
requires = ["old/0.1"]
51+
version = "1.0"
52+
"""
53+
54+
module = cst.parse_module(dedent(code))
55+
transformer = RequiresTransformer([dependency])
56+
modified = module.visit(transformer)
57+
assert 'requires = ["test/1.2.3"]' in modified.code
58+
assert 'old/0.1' not in modified.code
59+
60+
# Verify the resulting code is valid Python syntax
61+
ast.parse(modified.code)
62+
63+
@staticmethod
64+
def test_no_conanfile_class() -> None:
65+
"""Test that the transformer does not modify non-ConanFile classes."""
66+
dependency = TestTransformer.MockDependency(name='test')
67+
68+
code = """
69+
class NotConan:
70+
pass
71+
"""
72+
73+
module = cst.parse_module(dedent(code))
74+
transformer = RequiresTransformer([dependency])
75+
modified = module.visit(transformer)
76+
# Should not add requires to non-ConanFile classes
77+
assert 'requires' not in modified.code
78+
79+
# Verify the resulting code is valid Python syntax
80+
ast.parse(modified.code)

0 commit comments

Comments
 (0)