Coverage for src/renaissance/integrations/python/ast/factory.py: 98%
114 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: Factories for creating Python AST/pattern nodes for parsing and pattern matching."""
3import ast
4import re
5from collections.abc import Sequence
6from pathlib import Path
8import tree_sitter_python
9from libcst import SimpleStatementLine
11from renaissance.integrations import MATCH_ALL, MATCH_ONE
12from renaissance.integrations.python.ast.ast_node import ASTExtension
13from renaissance.integrations.python.ast.cst_node import PythonCstNode
14from renaissance.integrations.python.ast.rst_node import PythonRstNode
15from renaissance.integrations.tree_sitter.adapter import TreeSitterAdapter
16from renaissance.integrations.tree_sitter.lst import LSTNode
17from renaissance.syntax_tree.match_finder import is_match
18from renaissance.syntax_tree.node_protocol import NodeProtocol
19from renaissance.syntax_tree.pattern_kind import PatternKind
20from renaissance.utils.ast_utils import replace_dollar, use_dollar
22_MATCH_ALL_RE = re.compile(r"^" + re.escape(MATCH_ALL) + r"\w+$")
23_MATCH_ONE_RE = re.compile(r"^" + re.escape(MATCH_ONE) + r"\w+$")
25SHOW_NODE = False
28class PythonPattern(NodeProtocol):
29 """AI: Wrap a Python AST/RST node as a matchable pattern, detecting match-all/match-one placeholders."""
31 def __init__(self, node):
32 """AI: Wrap a Python AST/RST node as a matchable pattern, detecting match-all/match-one placeholders."""
33 self.node: PythonRstNode = node
34 if type(node) is str:
35 print(node)
36 return
37 self.parser_kind = getattr(node, "parser_kind", type(node).__name__)
38 self.semantic_kind = getattr(node, "semantic_kind", None)
39 self.pattern_kind = None
40 self._derive_pattern_kind(node)
42 self.properties: dict = node.properties
43 self.children: list[PythonPattern] = [PythonPattern(node) for node in node.children]
44 self.signature: str = node.signature
45 if hasattr(node, "name") and node.name:
46 self.name: str = use_dollar(node.name)
47 else:
48 self.name = ""
50 def __eq__(self, other: NodeProtocol) -> bool:
51 """AI: Return whether `other` matches this pattern node."""
52 return is_match(other, self)
54 def __repr__(self):
55 """AI: Return a dollar-escaped repr of the wrapped node."""
56 return use_dollar(str(self.node))
58 def _derive_pattern_kind(self, node) -> None:
59 if isinstance(node, ast.arg):
60 signature = node.arg
61 elif isinstance(node, ast.Name):
62 signature = node.id
63 elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Name):
64 signature = node.value.id
65 elif isinstance(node, ast.AST):
66 signature = str(node)
67 else:
68 signature = node.name
70 if node.parser_kind in {"Name", "Expr", "arg", "Param"}:
71 if _MATCH_ALL_RE.match(signature):
72 self.pattern_kind = PatternKind.MATCH_ALL
73 elif _MATCH_ONE_RE.match(signature):
74 self.pattern_kind = PatternKind.MATCH_ONE
77class PythonFactory:
78 """AI: Factory for creating Python AST nodes of a configured node-implementation type."""
80 def __init__(self, clazz: type[PythonRstNode | PythonCstNode | LSTNode | ast.AST]) -> None:
81 """AI: Configure a factory that creates Python AST nodes of the given node-implementation type."""
82 self.clazz = clazz
83 if clazz == LSTNode:
84 clazz.load_from_text = self.load_from_lst
85 elif clazz == ast.AST:
86 clazz.load_from_text = ASTExtension.load_from_ast
87 # matcher
88 clazz.node = ASTExtension.ast_node
90 # clazz.name = ASTExtension.ast_name
91 clazz.parser_kind = ASTExtension.parser_kind
92 clazz.semantic_kind = ASTExtension.semantic_kind
93 clazz.properties = ASTExtension.ast_properties
94 clazz.children = ASTExtension.ast_children
95 clazz.signature = ASTExtension.ast_signature
97 # writer
98 clazz.text = ASTExtension.ast_signature
99 clazz.filename = "dummy.py"
101 # shower
102 clazz.is_implicit = True
103 clazz.show_props = False
104 clazz.indent = ""
106 def create(self, file_path: Path) -> PythonRstNode | PythonCstNode:
107 """AI: Parse the Python source file at file_path into an AST node tree."""
108 atu = self.clazz.load(file_path=file_path)
109 assert isinstance(atu, self.clazz)
110 return atu
112 def create_from_text(self, text: str, file_name: str = "snippet.py") -> PythonRstNode | PythonCstNode | LSTNode | ast.AST:
113 """AI: Parse Python source text (attributed to file_name) into an AST node tree."""
114 atu = self.clazz.load_from_text(text, file_name)
115 assert isinstance(atu, self.clazz)
116 return atu
118 @staticmethod
119 def load_from_lst(text: str, _file_name: str) -> LSTNode:
120 """AI: Parse Python source text with tree-sitter into an LST node tree."""
121 adapter = TreeSitterAdapter(tree_sitter_python)
122 tree = adapter.parse_code(text)
123 return adapter.to_lst(text, tree).root
126class PythonPatternFactory:
127 """AI: Factory for building Python AST patterns from text."""
129 def __init__(self, factory: PythonFactory):
130 """AI: Prepare a pattern factory for creating Python AST patterns from text."""
131 self.factory = factory
133 def _create(self, text: str) -> PythonPattern:
134 return PythonPattern(self.factory.create_from_text(text, "pattern.py"))
136 def create(self, text: str) -> PythonPattern:
137 """AI: Parse text as a Python pattern, substituting placeholder dollar syntax first."""
138 text = replace_dollar(text)
139 return self._create(text)
141 def create_statements(self, text: str) -> Sequence[PythonPattern]:
142 """AI: Parse text and return its top-level statement pattern nodes."""
143 atu = self.create(text)
144 return atu.children
146 def create_statement(self, text: str) -> PythonPattern:
147 """AI: Parse text and return its last top-level statement pattern node."""
148 stmt = self.create_statements(text)[-1]
149 if isinstance(stmt.node.node, SimpleStatementLine):
150 return stmt.children[0]
151 return stmt
152 # return stmt
154 def create_expression(self, text: str) -> PythonPattern:
155 """AI: Parse text and return the expression pattern node of its last statement."""
156 my_pattern = self.create_statement(text)
157 if isinstance(my_pattern.node, PythonRstNode):
158 return PythonPattern(my_pattern.node.expression)
159 if isinstance(my_pattern.node, (LSTNode, PythonCstNode)):
160 return PythonPattern(my_pattern.node.children[-1])
161 return PythonPattern(my_pattern.node.children[0])
163 def create_decorators(self, param):
164 """AI: Parse param as a decorator applied to a dummy test function and return the decorator pattern node."""
165 return self.create_statement(param + "\ndef test(): pass").children[2]
167 @staticmethod
168 def create_kwargs(kw_str) -> Sequence[PythonPattern]:
169 """AI: Parse kw_str as call keyword arguments and return their pattern nodes."""
170 call = ast.parse(f"fun({replace_dollar(kw_str)})", "kwarg_pattern.py", type_comments=True).body[0].value
171 return [PythonPattern(PythonRstNode(kwarg)) for kwarg in call.keywords]