Coverage for src/pydal2sql/magic.py: 100%

58 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-20 20:28 +0200

1""" 

2This file has methods to guess which variables are unknown and to potentially (monkey-patch) fix this. 

3""" 

4 

5import ast 

6import builtins 

7import importlib 

8import textwrap 

9import typing 

10 

11BUILTINS = set(builtins.__dict__.keys()) 

12 

13 

14def traverse_ast(node: ast.AST, variable_collector: typing.Callable[[ast.AST], None]) -> None: 

15 """ 

16 Calls variable_collector on each node recursively. 

17 """ 

18 variable_collector(node) 

19 for child in ast.iter_child_nodes(node): 

20 traverse_ast(child, variable_collector) 

21 

22 

23def find_missing_variables(code_str: str) -> set[str]: 

24 """ 

25 Look through the source code in code_str and try to detect using ast parsing which variables are undefined. 

26 """ 

27 # Partly made by ChatGPT 

28 code_str = textwrap.dedent(code_str) 

29 

30 # could raise SyntaxError 

31 tree: ast.Module = ast.parse(code_str) 

32 

33 used_variables: set[str] = set() 

34 defined_variables: set[str] = set() 

35 imported_modules: set[str] = set() 

36 imported_names: set[str] = set() 

37 loop_variables: set[str] = set() 

38 

39 def collect_variables(node: ast.AST) -> None: 

40 if isinstance(node, ast.Name): 

41 if isinstance(node.ctx, ast.Load): 

42 used_variables.add(node.id) 

43 elif isinstance(node.ctx, ast.Store): 

44 defined_variables.add(node.id) 

45 elif isinstance(node.ctx, ast.Del): 

46 defined_variables.discard(node.id) 

47 

48 def collect_definitions(node: ast.AST) -> None: 

49 if isinstance(node, ast.Assign): 

50 node_targets = typing.cast(list[ast.Name], node.targets) 

51 

52 defined_variables.update(target.id for target in node_targets) 

53 

54 def collect_imports(node: ast.AST) -> None: 

55 if isinstance(node, ast.Import): 

56 for alias in node.names: 

57 imported_names.add(alias.name) 

58 elif isinstance(node, ast.ImportFrom) and node.module: 

59 module_name = node.module 

60 imported_module = importlib.import_module(module_name) 

61 if node.names[0].name == "*": 

62 imported_names.update(name for name in dir(imported_module) if not name.startswith("_")) 

63 else: 

64 imported_names.update(alias.asname or alias.name for alias in node.names) 

65 

66 def collect_imported_names(node: ast.AST) -> None: 

67 if isinstance(node, ast.ImportFrom) and node.module: 

68 for alias in node.names: 

69 imported_names.add(alias.asname or alias.name) 

70 

71 def collect_loop_variables(node: ast.AST) -> None: 

72 if isinstance(node, ast.For) and isinstance(node.target, ast.Name): 

73 loop_variables.add(node.target.id) 

74 

75 traverse_ast(tree, collect_variables) 

76 traverse_ast(tree, collect_definitions) 

77 traverse_ast(tree, collect_imported_names) 

78 traverse_ast(tree, collect_imports) 

79 traverse_ast(tree, collect_loop_variables) 

80 

81 return { 

82 var 

83 for var in used_variables 

84 if var not in defined_variables 

85 and var not in imported_modules 

86 and var not in loop_variables 

87 and var not in imported_names 

88 and var not in BUILTINS 

89 } 

90 

91 

92# if __name__ == "__main__": 

93# # Example usage: 

94# code_string = """ 

95# from math import floor 

96# import datetime 

97# from pydal import DAL 

98# a = 1 

99# b = 2 

100# print(a, b + c) 

101# d = e + b 

102# xyz 

103# floor(d) 

104# ceil(d) 

105# ceil(e) 

106# 

107# datetime.utcnow() 

108# 

109# db = DAL() 

110# 

111# db.define_table('...') 

112# 

113# for table in []: 

114# print(table) 

115# 

116# if toble := True: 

117# print(toble) 

118# """ 

119# missing_variables = find_missing_variables(code_string) 

120# assert missing_variables == {"c", "xyz", "ceil", "e"} 

121 

122 

123def generate_magic_code(missing_vars: set[str]) -> str: 

124 """ 

125 After finding missing vars, fill them in with an object that does nothing except return itself or an empty string. 

126 

127 This way, it's least likely to crash (when used as default or validator in pydal, don't use this for running code!). 

128 """ 

129 extra_code = """ 

130 class Empty: 

131 # class that does absolutely nothing 

132 # but can be accessed like an object (obj.something.whatever) 

133 # or a dict[with][some][keys] 

134 def __getattribute__(self, _): 

135 return self 

136 

137 def __getitem__(self, _): 

138 return self 

139 

140 def __get__(self): 

141 return self 

142 

143 def __call__(self, *_): 

144 return self 

145 

146 def __str__(self): 

147 return '' 

148 

149 def __repr__(self): 

150 return '' 

151 

152 # todo: overload more methods 

153 empty = Empty() 

154 \n 

155 """ 

156 for variable in missing_vars: 

157 extra_code += f"{variable} = empty; " 

158 

159 return textwrap.dedent(extra_code)