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

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 

6 

7"""AI: Factories for creating Python AST/pattern nodes for parsing and pattern matching.""" 

8 

9import ast 

10import re 

11from collections.abc import Sequence 

12from pathlib import Path 

13 

14import tree_sitter_python 

15from libcst import SimpleStatementLine 

16 

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 

27 

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+$") 

30 

31SHOW_NODE = False 

32 

33 

34class PythonPattern: 

35 """AI: Wrap a Python AST/RST node as a matchable pattern, detecting match-all/match-one placeholders.""" 

36 

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) 

47 

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 = "" 

55 

56 def __eq__(self, other: NodeProtocol) -> bool: 

57 """AI: Return whether `other` matches this pattern node.""" 

58 return is_match(other, self) 

59 

60 def __repr__(self) -> str: 

61 """AI: Return a dollar-escaped repr of the wrapped node.""" 

62 return use_dollar(str(self.node)) 

63 

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 

75 

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 

81 

82 

83class PythonFactory: 

84 """AI: Factory for creating Python AST nodes of a configured node-implementation type.""" 

85 

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 

95 

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 

102 

103 # writer 

104 clazz.text = ASTExtension.ast_signature 

105 clazz.filename = "dummy.py" 

106 

107 # shower 

108 clazz.is_implicit = True 

109 clazz.show_props = False 

110 clazz.indent = "" 

111 

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 

117 

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 

123 

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 

130 

131 

132class PythonPatternFactory: 

133 """AI: Factory for building Python AST patterns from text.""" 

134 

135 def __init__(self, factory: PythonFactory) -> None: 

136 """AI: Prepare a pattern factory for creating Python AST patterns from text.""" 

137 self.factory = factory 

138 

139 def _create(self, text: str) -> PythonPattern: 

140 return PythonPattern(self.factory.create_from_text(text, "pattern.py")) 

141 

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) 

146 

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 

151 

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 

159 

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]) 

168 

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] 

172 

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]