|
4 | 4 | from string import Template
|
5 | 5 | from textwrap import dedent
|
6 | 6 |
|
| 7 | +import libcst as cst |
7 | 8 | from pydantic import DirectoryPath
|
8 | 9 |
|
9 | 10 | from cppython.plugins.conan.schema import ConanDependency
|
10 | 11 |
|
11 | 12 |
|
| 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([ |
| 25 | + cst.Element(cst.SimpleString(f'"{dependency.requires()}"')) for dependency in self.dependencies |
| 26 | + ]), |
| 27 | + ) |
| 28 | + |
| 29 | + def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.BaseStatement: |
| 30 | + """Modify the class definition to include or update 'requires'. |
| 31 | +
|
| 32 | + Args: |
| 33 | + original_node: The original class definition. |
| 34 | + updated_node: The updated class definition. |
| 35 | +
|
| 36 | + Returns: The modified class definition. |
| 37 | + """ |
| 38 | + if self._is_conanfile_class(original_node): |
| 39 | + updated_node = self._update_requires(updated_node) |
| 40 | + return updated_node |
| 41 | + |
| 42 | + @staticmethod |
| 43 | + def _is_conanfile_class(class_node: cst.ClassDef) -> bool: |
| 44 | + """Check if the class inherits from ConanFile. |
| 45 | +
|
| 46 | + Args: |
| 47 | + class_node: The class definition to check. |
| 48 | +
|
| 49 | + Returns: True if the class inherits from ConanFile, False otherwise. |
| 50 | + """ |
| 51 | + return any((isinstance(base.value, cst.Name) and base.value.value == 'ConanFile') for base in class_node.bases) |
| 52 | + |
| 53 | + def _update_requires(self, updated_node: cst.ClassDef) -> cst.ClassDef: |
| 54 | + """Update or add a 'requires' assignment in a ConanFile class definition.""" |
| 55 | + # Check if 'requires' is already defined |
| 56 | + for body_statement_line in updated_node.body.body: |
| 57 | + if not isinstance(body_statement_line, cst.SimpleStatementLine): |
| 58 | + 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': |
| 66 | + continue |
| 67 | + |
| 68 | + return self._replace_requires(updated_node, body_statement_line, assignment_statement) |
| 69 | + |
| 70 | + # Find the last attribute assignment before methods |
| 71 | + last_attribute = None |
| 72 | + for body_statement_line in updated_node.body.body: |
| 73 | + if not isinstance(body_statement_line, cst.SimpleStatementLine): |
| 74 | + break |
| 75 | + assignment_statement = body_statement_line.body[0] |
| 76 | + if not isinstance(assignment_statement, cst.Assign): |
| 77 | + break |
| 78 | + last_attribute = body_statement_line |
| 79 | + |
| 80 | + # Construct a new statement for the 'requires' attribute |
| 81 | + new_statement = cst.SimpleStatementLine( |
| 82 | + body=[self._create_requires_assignment()], |
| 83 | + ) |
| 84 | + |
| 85 | + # Insert the new statement after the last attribute assignment |
| 86 | + if last_attribute is not None: |
| 87 | + new_body = list(updated_node.body.body) |
| 88 | + index = new_body.index(last_attribute) |
| 89 | + new_body.insert(index + 1, new_statement) |
| 90 | + else: |
| 91 | + new_body = [new_statement] + list(updated_node.body.body) |
| 92 | + |
| 93 | + return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body)) |
| 94 | + |
| 95 | + def _replace_requires( |
| 96 | + self, updated_node: cst.ClassDef, body_statement_line: cst.SimpleStatementLine, assignment_statement: cst.Assign |
| 97 | + ) -> cst.ClassDef: |
| 98 | + """Replace the existing 'requires' assignment with a new one. |
| 99 | +
|
| 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. |
| 104 | +
|
| 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) |
| 112 | + return updated_node.with_changes( |
| 113 | + 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] |
| 115 | + ) |
| 116 | + ) |
| 117 | + |
| 118 | + |
12 | 119 | class Builder:
|
13 | 120 | """Aids in building the information needed for the Conan plugin"""
|
14 | 121 |
|
@@ -57,12 +164,14 @@ def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanD
|
57 | 164 | """Generate a conanfile.py file for the project."""
|
58 | 165 | conan_file = directory / self._filename
|
59 | 166 |
|
60 |
| - # If the file exists then we need to inject our information into it |
61 | 167 | if conan_file.exists():
|
62 |
| - raise NotImplementedError( |
63 |
| - 'Updating existing conanfile.py is not yet supported. Please remove the file and try again.' |
64 |
| - ) |
| 168 | + source_code = conan_file.read_text(encoding='utf-8') |
| 169 | + |
| 170 | + module = cst.parse_module(source_code) |
| 171 | + transformer = RequiresTransformer(dependencies) |
| 172 | + modified = module.visit(transformer) |
65 | 173 |
|
| 174 | + conan_file.write_text(modified.code, encoding='utf-8') |
66 | 175 | else:
|
67 | 176 | directory.mkdir(parents=True, exist_ok=True)
|
68 | 177 | self._create_conanfile(conan_file, dependencies)
|
0 commit comments