tlarsson commited on
Commit
5abb0c5
·
verified ·
1 Parent(s): f00ff00

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -0
app.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import ast
3
+ import base64
4
+ import dash
5
+ from dash import html, dcc, dash_table
6
+ import dash_cytoscape as cyto
7
+ from dash.dependencies import Input, Output, State
8
+ import io
9
+ import json
10
+ import urllib.parse
11
+
12
+ app = dash.Dash(__name__, suppress_callback_exceptions=True)
13
+ server = app.server
14
+
15
+ def parse_functions_from_files(file_dict):
16
+ functions = {}
17
+ defined_funcs = set()
18
+
19
+ # First pass to gather all defined function names with file context
20
+ for fname, code in file_dict.items():
21
+ tree = ast.parse(code)
22
+ for node in ast.walk(tree):
23
+ if isinstance(node, ast.FunctionDef):
24
+ defined_funcs.add(node.name)
25
+
26
+ # Second pass to build detailed structure
27
+ for fname, code in file_dict.items():
28
+ tree = ast.parse(code)
29
+ for node in ast.walk(tree):
30
+ if isinstance(node, ast.FunctionDef):
31
+ func_name = node.name
32
+ args = [arg.arg for arg in node.args.args]
33
+ returns = []
34
+ calls = []
35
+
36
+ for sub in ast.walk(node):
37
+ if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
38
+ if sub.func.id in defined_funcs:
39
+ calls.append(sub.func.id)
40
+ elif isinstance(sub, ast.Return):
41
+ if sub.value is None:
42
+ continue
43
+ elif isinstance(sub.value, ast.Tuple):
44
+ returns.extend([ast.unparse(elt) for elt in sub.value.elts])
45
+ else:
46
+ returns.append(ast.unparse(sub.value))
47
+
48
+ functions[func_name] = {
49
+ "args": args,
50
+ "returns": returns,
51
+ "calls": calls,
52
+ "filename": fname
53
+ }
54
+
55
+ return functions
56
+
57
+ def get_reachable_functions(start, graph):
58
+ visited = set()
59
+ stack = [start]
60
+ while stack:
61
+ node = stack.pop()
62
+ if node not in visited:
63
+ visited.add(node)
64
+ stack.extend(graph.get(node, []))
65
+ return visited
66
+
67
+ app.layout = html.Div([
68
+ html.H2("Multi-File Function Dependency Graph (AST-based)"),
69
+ dcc.Upload(id="upload", children=html.Button("Upload Python Files"), multiple=True),
70
+ html.Div(id="file-name"),
71
+ html.Br(),
72
+ html.Div(id="main-function-ui"),
73
+ html.A("Download Graph JSON", id="download-link", download="graph.json", href="", target="_blank"),
74
+ cyto.Cytoscape(
75
+ id="cytoscape-graph",
76
+ layout={"name": "breadthfirst", "directed": True, "padding": 10},
77
+ style={"width": "100%", "height": "600px"},
78
+ elements=[],
79
+ userZoomingEnabled=True,
80
+ userPanningEnabled=True,
81
+ minZoom=0.2,
82
+ maxZoom=2,
83
+ wheelSensitivity=0.1
84
+ ),
85
+ html.Hr(),
86
+ html.H4("Function Input/Output Table"),
87
+ dash_table.DataTable(
88
+ id="function-table",
89
+ columns=[
90
+ {"name": "Function", "id": "Function"},
91
+ {"name": "Arguments", "id": "Arguments"},
92
+ {"name": "Returns", "id": "Returns"},
93
+ {"name": "File", "id": "File"}
94
+ ],
95
+ style_table={"overflowX": "auto"},
96
+ style_cell={
97
+ "textAlign": "left",
98
+ "whiteSpace": "normal",
99
+ "wordBreak": "break-word",
100
+ "maxWidth": 300
101
+ }
102
+ )
103
+ ])
104
+
105
+ uploaded_files = {}
106
+
107
+ @app.callback(
108
+ Output("main-function-ui", "children"),
109
+ Output("file-name", "children"),
110
+ Input("upload", "contents"),
111
+ State("upload", "filename")
112
+ )
113
+ def store_multi_upload(list_of_contents, list_of_names):
114
+ if not list_of_contents or not list_of_names:
115
+ return "", ""
116
+ uploaded_files.clear()
117
+ for content, name in zip(list_of_contents, list_of_names):
118
+ _, content_string = content.split(",")
119
+ uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")
120
+
121
+ parsed = parse_functions_from_files(uploaded_files)
122
+ main_candidates = [fn for fn, f in parsed.items() if len(f["calls"]) > 0]
123
+ options = [{"label": fn, "value": fn} for fn in main_candidates]
124
+ dropdown = dcc.Dropdown(id="main-function", options=options, value=options[0]["value"] if options else None)
125
+ return dropdown, f"Uploaded files: {', '.join(list_of_names)}"
126
+
127
+ @app.callback(
128
+ Output("cytoscape-graph", "elements"),
129
+ Output("function-table", "data"),
130
+ Output("download-link", "href"),
131
+ Input("main-function", "value")
132
+ )
133
+ def update_multi_graph(main_func):
134
+ if not uploaded_files or not main_func:
135
+ return [], [], ""
136
+
137
+ parsed = parse_functions_from_files(uploaded_files)
138
+ graph = {k: v["calls"] for k, v in parsed.items()}
139
+ reachable = get_reachable_functions(main_func, graph)
140
+
141
+ nodes = [{"data": {"id": name, "label": f"{name}\n({parsed[name]['filename']})"}} for name in reachable]
142
+
143
+ edges = []
144
+ for src in reachable:
145
+ for tgt in parsed[src]["calls"]:
146
+ if tgt in reachable:
147
+ edge = {
148
+ "data": {
149
+ "source": src,
150
+ "target": tgt
151
+ }
152
+ }
153
+ if parsed[src]["filename"] != parsed[tgt]["filename"]:
154
+ edge["classes"] = "crossfile"
155
+ edges.append(edge)
156
+
157
+ table_data = [{
158
+ "Function": fn,
159
+ "Arguments": ", ".join(parsed[fn]["args"]),
160
+ "Returns": ", ".join(parsed[fn]["returns"]),
161
+ "File": parsed[fn]["filename"]
162
+ } for fn in reachable]
163
+
164
+ json_data = json.dumps({"nodes": nodes, "edges": edges}, indent=2)
165
+ href_data = "data:application/json;charset=utf-8," + urllib.parse.quote(json_data)
166
+
167
+ return nodes + edges, table_data, href_data
168
+
169
+ if __name__ == "__main__":
170
+ app.run(debug=True)