Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
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)
|