Skip to content

Commit ad22230

Browse files
committed
AST Modification
1 parent 861da08 commit ad22230

File tree

2 files changed

+112
-4
lines changed

2 files changed

+112
-4
lines changed

cppython/plugins/conan/builder.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,97 @@
44
from string import Template
55
from textwrap import dedent
66

7+
import libcst as cst
78
from pydantic import DirectoryPath
89

910
from cppython.plugins.conan.schema import ConanDependency
1011

1112

13+
class RequiresTransformer(cst.CSTTransformer):
14+
"""Transformer to add or update the `requires` attribute in a ConanFile class."""
15+
16+
def __init__(self, dependencies: list[ConanDependency]) -> None:
17+
"""Initialize the transformer with a list of dependencies."""
18+
self.dependencies = dependencies
19+
20+
def _create_requires_assignment(self) -> cst.Assign:
21+
"""Create a `requires` assignment statement."""
22+
return cst.Assign(
23+
targets=[cst.AssignTarget(cst.Name('requires'))],
24+
value=cst.List([cst.Element(cst.SimpleString(f'"{dep.requires()}"')) for dep in self.dependencies]),
25+
)
26+
27+
def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement:
28+
"""Modify the class definition to include or update 'requires'.
29+
30+
Args:
31+
original_node: The original class definition.
32+
updated_node: The updated class definition.
33+
34+
Returns: The modified class definition.
35+
"""
36+
if self._is_conanfile_class(original_node):
37+
updated_node = self._update_requires(updated_node)
38+
return updated_node
39+
40+
@staticmethod
41+
def _is_conanfile_class(class_node: cst.ClassDef) -> bool:
42+
"""Check if the class inherits from ConanFile.
43+
44+
Args:
45+
class_node: The class definition to check.
46+
47+
Returns: True if the class inherits from ConanFile, False otherwise.
48+
"""
49+
return any(
50+
(isinstance(base, cst.Name) and base.value == 'ConanFile')
51+
or (isinstance(base, cst.Attribute) and base.attr.value == 'ConanFile')
52+
for base in class_node.bases
53+
)
54+
55+
def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef:
56+
"""Update or add the 'requires' attribute in the class definition.
57+
58+
Args:
59+
updated_node: The class definition to update.
60+
61+
Returns: The updated class definition.
62+
"""
63+
for body_item in updated_node.body.body:
64+
if isinstance(body_item, cst.SimpleStatementLine):
65+
stmt = body_item.body[0]
66+
if isinstance(stmt, cst.Assign):
67+
for target in stmt.targets:
68+
if isinstance(target.target, cst.Name) and target.target.value == 'requires':
69+
return self._replace_requires(updated_node, body_item, stmt)
70+
71+
new_stmt = self._create_requires_assignment()
72+
return updated_node.with_changes(
73+
body=updated_node.body.with_changes(body=[new_stmt] + list(updated_node.body.body))
74+
)
75+
76+
def _replace_requires(
77+
self, updated_node: cst.ClassDef, body_item: cst.SimpleStatementLine, stmt: cst.Assign
78+
) -> cst.ClassDef:
79+
"""Replace the existing 'requires' assignment with a new one.
80+
81+
Args:
82+
updated_node (cst.ClassDef): The class definition to update.
83+
body_item (cst.SimpleStatementLine): The body item containing the assignment.
84+
stmt (cst.Assign): The existing assignment statement.
85+
86+
Returns:
87+
cst.ClassDef: The updated class definition.
88+
"""
89+
new_value = cst.List([cst.Element(cst.SimpleString(f'"{dep.requires()}"')) for dep in self.dependencies])
90+
new_stmt = stmt.with_changes(value=new_value)
91+
return updated_node.with_changes(
92+
body=updated_node.body.with_changes(
93+
body=[new_stmt if item is body_item else item for item in updated_node.body.body]
94+
)
95+
)
96+
97+
1298
class Builder:
1399
"""Aids in building the information needed for the Conan plugin"""
14100

@@ -57,12 +143,14 @@ def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanD
57143
"""Generate a conanfile.py file for the project."""
58144
conan_file = directory / self._filename
59145

60-
# If the file exists then we need to inject our information into it
61146
if conan_file.exists():
62-
raise NotImplementedError(
63-
'Updating existing conanfile.py is not yet supported. Please remove the file and try again.'
64-
)
147+
source_code = conan_file.read_text(encoding='utf-8')
148+
149+
module = cst.parse_module(source_code)
150+
transformer = RequiresTransformer(dependencies)
151+
modified = module.visit(transformer)
65152

153+
conan_file.write_text(modified.code, encoding='utf-8')
66154
else:
67155
directory.mkdir(parents=True, exist_ok=True)
68156
self._create_conanfile(conan_file, dependencies)

tests/integration/examples/test_conan_cmake.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,23 @@ def test_simple(example_runner: CliRunner) -> None:
3636

3737
# Verify that the build directory contains the expected files
3838
assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found'
39+
40+
@staticmethod
41+
def test_inject(example_runner: CliRunner) -> None:
42+
"""Inject"""
43+
result = example_runner.invoke(
44+
app,
45+
[
46+
'install',
47+
],
48+
)
49+
50+
assert result.exit_code == 0, result.output
51+
52+
# Run the CMake configuration command
53+
cmake_result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False)
54+
55+
assert cmake_result.returncode == 0, f'CMake configuration failed: {cmake_result.stderr}'
56+
57+
# Verify that the build directory contains the expected files
58+
assert (Path('build') / 'CMakeCache.txt').exists(), 'build/CMakeCache.txt not found'

0 commit comments

Comments
 (0)