-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathastree.py
176 lines (135 loc) · 5.04 KB
/
astree.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""A module to visualize Python AST."""
import ast
import inspect
import importlib
import json
import re
import uuid
import pydot_ng as pydot
from IPython.display import Image, display
from _ast import AST
# ~~~~~~~~~~~~~~~~~~ PARSING AST OBJ TO JSON ~~~~~~~~~~~~~~~~~~
def ast_parse(method):
"""Decorator to parse user input to JSON-AST object."""
def wrapper(*args, **kwargs):
if isinstance(args[0], str):
ast_obj = ast.parse(args[0]) # i.e. a dec or exp
else:
obj = inspect.getsource(args[0]) # i.e. a method
ast_obj = ast.parse(obj)
json_parsed = method(ast_obj, **kwargs)
parsed = json.loads(json_parsed)
return parsed
return wrapper
@ast_parse
def json_ast(node):
"""Parse an AST object into JSON."""
def _format(_node):
if isinstance(_node, AST):
fields = [("_PyType", _format(_node.__class__.__name__))]
fields += [(a, _format(b)) for a, b in iter_fields(_node)]
return "{ %s }" % ", ".join(('"%s": %s' % field for field in fields))
if isinstance(_node, list):
return "[ %s ]" % ", ".join([_format(x) for x in _node])
if isinstance(_node, bytes):
return json.dumps(_node.decode("utf-8"))
return json.dumps(_node)
return _format(node)
def iter_fields(node):
"""Get attributes of a node."""
try:
for field in node._fields:
yield field, getattr(node, field)
except AttributeError:
yield
# ~~~~~~~~~~~~~~~~~~~~~~~~ DRAWING AST ~~~~~~~~~~~~~~~~~~~~~~~~~
def grapher(graph, ast_nodes, parent_node="", node_hash="__init__"):
"""Recursively parse JSON-AST object into a tree."""
if isinstance(ast_nodes, dict):
for key, node in ast_nodes.items():
if not parent_node:
parent_node = node
continue
if key == "_PyType":
node = graph_detail(node, ast_nodes) # get node detail for graph
node_hash = draw(parent_node, node, graph=graph, parent_hash=node_hash)
parent_node = node # once a child now parent
continue
# parse recursively
if isinstance(node, dict):
grapher(graph, node, parent_node=parent_node, node_hash=node_hash)
if isinstance(node, list):
[
grapher(graph, item, parent_node=parent_node, node_hash=node_hash)
for item in node
]
def graph_detail(value, ast_scope):
"""Retrieve node details."""
detail_keys = ("module", "n", "s", "id", "name", "attr", "arg")
for key in detail_keys:
if not isinstance(dict.get(ast_scope, key), type(None)):
value = f"{value}\n{key}: {ast_scope[key]}"
return value
def clean_node(method):
"""Decorator to eliminate illegal characters, check type, and\n
shorten lengthy child and parent nodes."""
def wrapper(*args, **kwargs):
parent_name, child_name = tuple(
"_node" if node == "node" else node for node in args
)
illegal_char = re.compile(r"[,\\/]$")
illegal_char.sub("*", child_name)
if not child_name:
return
if len(child_name) > 2500:
child_name = "~~~DOCS: too long to fit on graph~~~"
args = (parent_name, child_name)
return method(*args, **kwargs)
return wrapper
@clean_node
def draw(parent_name, child_name, graph, parent_hash):
"""Draw parent and child nodes. Create and return new hash\n
key declared to a child node."""
parent_node = pydot.Node(parent_hash, label=parent_name, shape="box")
child_hash = str(uuid.uuid4()) # create hash key
child_node = pydot.Node(child_hash, label=child_name, shape="box")
graph.add_node(parent_node)
graph.add_node(child_node)
graph.add_edge(pydot.Edge(parent_node, child_node))
return child_hash
# For jupyter notebooks
def view_tree(pdot):
"""Display tree onto console."""
tree = Image(pdot.create_png())
display(tree)
def parse_input(_input):
"""Parse user input and return an AST-compatible object."""
try:
if "." in _input:
mod, met = _input.split(".") # handle modules and methods
module = importlib.import_module(mod)
method = getattr(module, met)
else:
module = importlib.import_module(_input) # handle modules
method = module
except ModuleNotFoundError:
method = _input # handle dec, exp
return method
def main():
"""Take user input and draw an AST.\n
Save file as PNG."""
graph = pydot.Dot(
graph_type="digraph",
strict=True,
constraint=True,
concentrate=True,
splines="polyline",
)
user_input = input("Input a method name, expression, etc.:\n")
parsed_input = parse_input(user_input)
grapher(graph, json_ast(parsed_input))
# view_tree(graph)
if graph.write_png("astree.png"):
print("Graph made successfully")
if __name__ == "__main__":
main()