Coverage for src/renaissance/integrations/python/ast/factory.py: 98%
114 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Factories for creating Python AST/pattern nodes for parsing and pattern matching."""
9import ast
10import re
11from collections.abc import Sequence
12from pathlib import Path
14import tree_sitter_python
15from libcst import SimpleStatementLine
17from renaissance.integrations import MATCH_ALL, MATCH_ONE
18from renaissance.integrations.python.ast.ast_node import ASTExtension
19from renaissance.integrations.python.ast.cst_node import PythonCstNode
20from renaissance.integrations.python.ast.rst_node import PythonRstNode
21from renaissance.integrations.tree_sitter.adapter import TreeSitterAdapter
22from renaissance.integrations.tree_sitter.lst import LSTNode
23from renaissance.syntax_tree.match_finder import is_match
24from renaissance.syntax_tree.node_protocol import NodeProtocol
25from renaissance.syntax_tree.pattern_kind import PatternKind
26from renaissance.utils.ast_utils import replace_dollar, use_dollar
28_MATCH_ALL_RE = re.compile(r"^" + re.escape(MATCH_ALL) + r"\w+$")
29_MATCH_ONE_RE = re.compile(r"^" + re.escape(MATCH_ONE) + r"\w+$")
31SHOW_NODE = False
34class PythonPattern:
35 """AI: Wrap a Python AST/RST node as a matchable pattern, detecting match-all/match-one placeholders."""
37 def __init__(self, node) -> None:
38 """AI: Wrap a Python AST/RST node as a matchable pattern, detecting match-all/match-one placeholders."""
39 self.node: PythonRstNode = node
40 if type(node) is str:
41 print(node)
42 return
43 self.parser_kind = getattr(node, "parser_kind", type(node).__name__)
44 self.semantic_kind = getattr(node, "semantic_kind", None)
45 self.pattern_kind = None
46 self._derive_pattern_kind(node)
48 self.properties: dict = node.properties
49 self.children: list[PythonPattern] = [PythonPattern(node) for node in node.children]
50 self.signature: str = node.signature
51 if hasattr(node, "name") and node.name:
52 self.name: str = use_dollar(node.name)
53 else:
54 self.name = ""
56 def __eq__(self, other: NodeProtocol) -> bool:
57 """AI: Return whether `other` matches this pattern node."""
58 return is_match(other, self)
60 def __repr__(self) -> str:
61 """AI: Return a dollar-escaped repr of the wrapped node."""
62 return use_dollar(str(self.node))
64 def _derive_pattern_kind(self, node) -> None:
65 if isinstance(node, ast.arg):
66 signature = node.arg
67 elif isinstance(node, ast.Name):
68 signature = node.id
69 elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Name):
70 signature = node.value.id
71 elif isinstance(node, ast.AST):
72 signature = str(node)
73 else:
74 signature = node.name
76 if node.parser_kind in {"Name", "Expr", "arg", "Param"}:
77 if _MATCH_ALL_RE.match(signature):
78 self.pattern_kind = PatternKind.MATCH_ALL
79 elif _MATCH_ONE_RE.match(signature):
80 self.pattern_kind = PatternKind.MATCH_ONE
83class PythonFactory:
84 """AI: Factory for creating Python AST nodes of a configured node-implementation type."""
86 def __init__(self, clazz: type[PythonRstNode | PythonCstNode | LSTNode | ast.AST]) -> None:
87 """AI: Configure a factory that creates Python AST nodes of the given node-implementation type."""
88 self.clazz = clazz
89 if clazz == LSTNode:
90 clazz.load_from_text = self.load_from_lst
91 elif clazz == ast.AST:
92 clazz.load_from_text = ASTExtension.load_from_ast
93 # matcher
94 clazz.node = ASTExtension.ast_node
96 # clazz.name = ASTExtension.ast_name
97 clazz.parser_kind = ASTExtension.parser_kind
98 clazz.semantic_kind = ASTExtension.semantic_kind
99 clazz.properties = ASTExtension.ast_properties
100 clazz.children = ASTExtension.ast_children
101 clazz.signature = ASTExtension.ast_signature
103 # writer
104 clazz.text = ASTExtension.ast_signature
105 clazz.filename = "dummy.py"
107 # shower
108 clazz.is_implicit = True
109 clazz.show_props = False
110 clazz.indent = ""
112 def create(self, file_path: Path) -> PythonRstNode | PythonCstNode:
113 """AI: Parse the Python source file at file_path into an AST node tree."""
114 atu = self.clazz.load(file_path=file_path)
115 assert isinstance(atu, self.clazz)
116 return atu
118 def create_from_text(self, text: str, file_name: str = "snippet.py") -> PythonRstNode | PythonCstNode | LSTNode | ast.AST:
119 """AI: Parse Python source text (attributed to file_name) into an AST node tree."""
120 atu = self.clazz.load_from_text(text, file_name)
121 assert isinstance(atu, self.clazz)
122 return atu
124 @staticmethod
125 def load_from_lst(text: str, _file_name: str) -> LSTNode:
126 """AI: Parse Python source text with tree-sitter into an LST node tree."""
127 adapter = TreeSitterAdapter(tree_sitter_python)
128 tree = adapter.parse_code(text)
129 return adapter.to_lst(text, tree).root
132class PythonPatternFactory:
133 """AI: Factory for building Python AST patterns from text."""
135 def __init__(self, factory: PythonFactory) -> None:
136 """AI: Prepare a pattern factory for creating Python AST patterns from text."""
137 self.factory = factory
139 def _create(self, text: str) -> PythonPattern:
140 return PythonPattern(self.factory.create_from_text(text, "pattern.py"))
142 def create(self, text: str) -> PythonPattern:
143 """AI: Parse text as a Python pattern, substituting placeholder dollar syntax first."""
144 text = replace_dollar(text)
145 return self._create(text)
147 def create_statements(self, text: str) -> Sequence[PythonPattern]:
148 """AI: Parse text and return its top-level statement pattern nodes."""
149 atu = self.create(text)
150 return atu.children
152 def create_statement(self, text: str) -> PythonPattern:
153 """AI: Parse text and return its last top-level statement pattern node."""
154 stmt = self.create_statements(text)[-1]
155 if isinstance(stmt.node.node, SimpleStatementLine):
156 return stmt.children[0]
157 return stmt
158 # return stmt
160 def create_expression(self, text: str) -> PythonPattern:
161 """AI: Parse text and return the expression pattern node of its last statement."""
162 my_pattern = self.create_statement(text)
163 if isinstance(my_pattern.node, PythonRstNode):
164 return PythonPattern(my_pattern.node.expression)
165 if isinstance(my_pattern.node, (LSTNode, PythonCstNode)):
166 return PythonPattern(my_pattern.node.children[-1])
167 return PythonPattern(my_pattern.node.children[0])
169 def create_decorators(self, param):
170 """AI: Parse param as a decorator applied to a dummy test function and return the decorator pattern node."""
171 return self.create_statement(param + "\ndef test(): pass").children[2]
173 @staticmethod
174 def create_kwargs(kw_str) -> Sequence[PythonPattern]:
175 """AI: Parse kw_str as call keyword arguments and return their pattern nodes."""
176 call = ast.parse(f"fun({replace_dollar(kw_str)})", "kwarg_pattern.py", type_comments=True).body[0].value
177 return [PythonPattern(PythonRstNode(kwarg)) for kwarg in call.keywords]