├── LICENSE ├── README.md ├── astree.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ira Horecka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astree 2 | ### Visualize abstract syntax trees of methods, declarations, expressions, and more 3 | 4 | ```astree.py``` combines the Python ```ast``` module with 5 | ```pydot_ng``` to draw abstract syntax trees specified in DOT language scripts. 6 | An abstract syntax tree is a tree representation of the abstract 7 | syntactic structure of source code written in a programming language (e.g. Python).
8 |
9 | 10 | Jumpstart -- running the program: 11 | 1) Clone repository 12 | 2) ```$ pip install -r requirements.txt```
13 | 3) ```$ python astree.py``` 14 | 15 | Input modules, methods, declarations, statements, expressions, etc.
16 | View video example here.
17 | 18 | For example, let's look at the ```requests.get``` method:
19 | ```>>> Input a method name, expression, etc.:```
20 | ```requests.get```
21 | 22 |

23 | AST visualize requests.get
25 |

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