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

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

2 

3import ast 

4import re 

5from collections.abc import Sequence 

6from pathlib import Path 

7 

8import tree_sitter_python 

9from libcst import SimpleStatementLine 

10 

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 

21 

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

24 

25SHOW_NODE = False 

26 

27 

28class PythonPattern(NodeProtocol): 

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

30 

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) 

41 

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

49 

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

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

52 return is_match(other, self) 

53 

54 def __repr__(self): 

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

56 return use_dollar(str(self.node)) 

57 

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 

69 

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 

75 

76 

77class PythonFactory: 

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

79 

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 

89 

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 

96 

97 # writer 

98 clazz.text = ASTExtension.ast_signature 

99 clazz.filename = "dummy.py" 

100 

101 # shower 

102 clazz.is_implicit = True 

103 clazz.show_props = False 

104 clazz.indent = "" 

105 

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 

111 

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 

117 

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 

124 

125 

126class PythonPatternFactory: 

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

128 

129 def __init__(self, factory: PythonFactory): 

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

131 self.factory = factory 

132 

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

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

135 

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) 

140 

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 

145 

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 

153 

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

162 

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] 

166 

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]