|
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([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 | + |
12 | 98 | class Builder: |
13 | 99 | """Aids in building the information needed for the Conan plugin""" |
14 | 100 |
|
@@ -57,12 +143,14 @@ def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanD |
57 | 143 | """Generate a conanfile.py file for the project.""" |
58 | 144 | conan_file = directory / self._filename |
59 | 145 |
|
60 | | - # If the file exists then we need to inject our information into it |
61 | 146 | 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) |
65 | 152 |
|
| 153 | + conan_file.write_text(modified.code, encoding='utf-8') |
66 | 154 | else: |
67 | 155 | directory.mkdir(parents=True, exist_ok=True) |
68 | 156 | self._create_conanfile(conan_file, dependencies) |
0 commit comments