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
« 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"""
5import ast
6import builtins
7import importlib
8import textwrap
9import typing
11BUILTINS = set(builtins.__dict__.keys())
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)
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)
30 # could raise SyntaxError
31 tree: ast.Module = ast.parse(code_str)
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()
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)
48 def collect_definitions(node: ast.AST) -> None:
49 if isinstance(node, ast.Assign):
50 node_targets = typing.cast(list[ast.Name], node.targets)
52 defined_variables.update(target.id for target in node_targets)
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)
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)
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)
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)
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 }
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"}
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.
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
137 def __getitem__(self, _):
138 return self
140 def __get__(self):
141 return self
143 def __call__(self, *_):
144 return self
146 def __str__(self):
147 return ''
149 def __repr__(self):
150 return ''
152 # todo: overload more methods
153 empty = Empty()
154 \n
155 """
156 for variable in missing_vars:
157 extra_code += f"{variable} = empty; "
159 return textwrap.dedent(extra_code)