Spaces:
Sleeping
Sleeping
File size: 5,805 Bytes
5abb0c5 75b9a57 5abb0c5 75b9a57 | 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 |
import ast
import base64
import dash
from dash import html, dcc, dash_table
import dash_cytoscape as cyto
from dash.dependencies import Input, Output, State
import io
import json
import urllib.parse
app = dash.Dash(__name__, suppress_callback_exceptions=True)
server = app.server
def parse_functions_from_files(file_dict):
functions = {}
defined_funcs = set()
# First pass to gather all defined function names with file context
for fname, code in file_dict.items():
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
defined_funcs.add(node.name)
# Second pass to build detailed structure
for fname, code in file_dict.items():
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
func_name = node.name
args = [arg.arg for arg in node.args.args]
returns = []
calls = []
for sub in ast.walk(node):
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
if sub.func.id in defined_funcs:
calls.append(sub.func.id)
elif isinstance(sub, ast.Return):
if sub.value is None:
continue
elif isinstance(sub.value, ast.Tuple):
returns.extend([ast.unparse(elt) for elt in sub.value.elts])
else:
returns.append(ast.unparse(sub.value))
functions[func_name] = {
"args": args,
"returns": returns,
"calls": calls,
"filename": fname
}
return functions
def get_reachable_functions(start, graph):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
stack.extend(graph.get(node, []))
return visited
app.layout = html.Div([
html.H2("Multi-File Function Dependency Graph (AST-based)"),
dcc.Upload(id="upload", children=html.Button("Upload Python Files"), multiple=True),
html.Div(id="file-name"),
html.Br(),
html.Div(id="main-function-ui"),
html.A("Download Graph JSON", id="download-link", download="graph.json", href="", target="_blank"),
cyto.Cytoscape(
id="cytoscape-graph",
layout={"name": "breadthfirst", "directed": True, "padding": 10},
style={"width": "100%", "height": "600px"},
elements=[],
userZoomingEnabled=True,
userPanningEnabled=True,
minZoom=0.2,
maxZoom=2,
wheelSensitivity=0.1
),
html.Hr(),
html.H4("Function Input/Output Table"),
dash_table.DataTable(
id="function-table",
columns=[
{"name": "Function", "id": "Function"},
{"name": "Arguments", "id": "Arguments"},
{"name": "Returns", "id": "Returns"},
{"name": "File", "id": "File"}
],
style_table={"overflowX": "auto"},
style_cell={
"textAlign": "left",
"whiteSpace": "normal",
"wordBreak": "break-word",
"maxWidth": 300
}
)
])
uploaded_files = {}
@app.callback(
Output("main-function-ui", "children"),
Output("file-name", "children"),
Input("upload", "contents"),
State("upload", "filename")
)
def store_multi_upload(list_of_contents, list_of_names):
if not list_of_contents or not list_of_names:
return "", ""
uploaded_files.clear()
for content, name in zip(list_of_contents, list_of_names):
_, content_string = content.split(",")
uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")
parsed = parse_functions_from_files(uploaded_files)
main_candidates = [fn for fn, f in parsed.items() if len(f["calls"]) > 0]
options = [{"label": fn, "value": fn} for fn in main_candidates]
dropdown = dcc.Dropdown(id="main-function", options=options, value=options[0]["value"] if options else None)
return dropdown, f"Uploaded files: {', '.join(list_of_names)}"
@app.callback(
Output("cytoscape-graph", "elements"),
Output("function-table", "data"),
Output("download-link", "href"),
Input("main-function", "value")
)
def update_multi_graph(main_func):
if not uploaded_files or not main_func:
return [], [], ""
parsed = parse_functions_from_files(uploaded_files)
graph = {k: v["calls"] for k, v in parsed.items()}
reachable = get_reachable_functions(main_func, graph)
nodes = [{"data": {"id": name, "label": f"{name}\n({parsed[name]['filename']})"}} for name in reachable]
edges = []
for src in reachable:
for tgt in parsed[src]["calls"]:
if tgt in reachable:
edge = {
"data": {
"source": src,
"target": tgt
}
}
if parsed[src]["filename"] != parsed[tgt]["filename"]:
edge["classes"] = "crossfile"
edges.append(edge)
table_data = [{
"Function": fn,
"Arguments": ", ".join(parsed[fn]["args"]),
"Returns": ", ".join(parsed[fn]["returns"]),
"File": parsed[fn]["filename"]
} for fn in reachable]
json_data = json.dumps({"nodes": nodes, "edges": edges}, indent=2)
href_data = "data:application/json;charset=utf-8," + urllib.parse.quote(json_data)
return nodes + edges, table_data, href_data
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, debug=False)
|