Coverage for src/renaissance/recipes/python_refactoring.py: 99%
68 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
1"""AI: Base processor for Python-specific source refactoring recipes."""
3import importlib
4from collections.abc import Sequence
5from pathlib import Path
6from typing import cast
8from termcolor import colored
10from renaissance.integrations.python.ast.factory import PythonFactory, PythonPatternFactory
11from renaissance.integrations.python.ast.rst_node import PythonRstNode
12from renaissance.integrations.python.ast.util import to_str
13from renaissance.syntax_tree import ASTProcessor
14from renaissance.syntax_tree.match_finder import match_pattern
15from renaissance.syntax_tree.semantic_kind import SemanticKind
16from renaissance.utils.text_utils import snake_case
19class PythonRefactoring(ASTProcessor):
20 """AI: Base processor for Python-specific source refactoring recipes."""
22 def __init__(self, file):
23 """AI: Prepare a Python-specific refactoring processor for the given source file."""
24 factory = PythonFactory(PythonRstNode)
25 atu = factory.create(file)
26 super().__init__(atu, factory, False)
27 self.pattern_factory = PythonPatternFactory(self.factory)
28 self.black_list_pattern = ".git"
29 self.white_list_pattern = ""
31 def replace_stmt(self, find, repl):
32 """AI: Replace all statements matching the find pattern with the repl template, expanding captures."""
33 pattern = self.pattern_factory.create_statements(find)
34 for match in match_pattern(self.root.children, pattern):
35 replacement = repl
36 for exp in match.expansions:
37 arg_str = ", ".join([to_str(node) for node in match.expansions[exp]])
38 replacement = replacement.replace(exp, arg_str)
40 replacement = replacement.replace(" ,)", ")").replace(", )", ")")
41 self.replace(replacement, match.nodes, False, False)
43 @staticmethod
44 def process(class_name, file):
45 """Return a subclass by name using importlib, like Java's Class.forName()."""
46 snake = snake_case(class_name)
47 module = importlib.import_module(f"renaissance.recipes.{snake}")
48 cls = getattr(module, class_name)
49 refactor = cls(file)
50 if refactor.black_list_pattern in refactor.filename or refactor.white_list_pattern not in refactor.filename:
51 print(f"skipping: {Path(refactor.filename).resolve()}")
52 return
54 print(colored(f"refactor {Path(refactor.filename).resolve()}", "green", attrs=["bold"]))
55 refactor.run()
57 def extract_call_arguments(self, node: PythonRstNode) -> tuple[list[str], dict[str, str]]:
58 """Extract positional and keyword arguments from a Call node.
60 The input may be a `Call` node itself or any descendant node.
61 When given a descendant, this method walks up parent links and uses the first
62 ancestor whose semantic kind is `CALL`.
64 Returned keyword arguments preserve Python call semantics where keyword
65 arguments appear after positional arguments.
66 """
67 current = node
68 while current is not None and current.semantic_kind != SemanticKind.CALL:
69 current = current.parent
71 call_node = current
72 if call_node is None:
73 return [], {}
75 args_implicit = next((c for c in call_node.children if c.name == "args"), None)
76 keywords_implicit = next((c for c in call_node.children if c.name == "keywords"), None)
78 positional_args = [arg_node.signature for arg_node in (args_implicit.children if args_implicit else [])]
79 keyword_args: dict[str, str] = {}
80 for kw_node in (keywords_implicit.children if keywords_implicit else []):
81 kw_name = kw_node.node.arg
82 if kw_name:
83 value_node = kw_node.children[0] if kw_node.children else kw_node
84 keyword_args[str(kw_name)] = value_node.signature
86 return positional_args, keyword_args
88 def class_declares_base(self, class_node: PythonRstNode, base_name: str) -> bool:
89 """Return whether class_node explicitly declares base_name as a base class.
91 This check uses only the names declared in the class header's base list,
92 so implicit Python inheritance from `object` is not treated as a declared base.
93 """
94 return base_name in self.class_base_arguments(class_node)
96 def class_base_arguments(self, class_node: PythonRstNode) -> list[str]:
97 """Return base class signatures explicitly listed in the class declaration.
99 Only names inside the parentheses of ``class Name(...):`` are returned.
100 The implicit default base object is not returned when no bases are declared.
101 """
102 bases_implicit = next((c for c in class_node.children if c.name == "bases"), None)
103 if bases_implicit is None:
104 return []
105 return [child.signature for child in bases_implicit.children]
107 @property
108 def body(self) -> Sequence[PythonRstNode]:
109 """AI: Return the root node's body statements."""
110 return cast("PythonRstNode", cast("object", self.root)).body
112 def run(self):
113 """AI: Run this refactoring recipe. Subclasses override this to perform the refactoring."""