code_graph / app.py
tlarsson's picture
Upload app.py
2206288 verified
raw
history blame
6.64 kB
import base64
import dash
from dash import html, dcc, dash_table
import dash_cytoscape as cyto
from dash.dependencies import Input, Output, State
import pandas as pd
from utility import parse_functions_from_files, get_reachable_functions, get_backtrace_functions
from introduction import get_intro_markdown
app = dash.Dash(__name__, suppress_callback_exceptions=True)
server = app.server
app.layout = html.Div([
html.H2("Function Dependency Visualizer (AST-Based)"),
dcc.Store(id="parsed-functions-store"),
dcc.Upload(id="upload", children=html.Button("Upload Python Files"), multiple=True),
dcc.Dropdown(id="main-function", style={"width": "300px"}),
dcc.Checklist(id="backtrace-toggle", options=[{"label": "Backtrace Mode", "value": "backtrace"}], value=[]),
dcc.Checklist(id="function-detail-toggle", options=[{"label": "Show Function-Level Detail in summary tab", "value": "show"}], value=[]),
dcc.Tabs(id="tab-selector", value="intro", children=[
dcc.Tab(label="📘 Introduction", value="intro"),
dcc.Tab(label="📊 Graph Explorer", value="graph"),
dcc.Tab(label="📁 File Summary", value="summary")
]),
html.Div(id="intro-tab"),
html.Div(id="graph-tab"),
html.Div(id="summary-tab")
])
@app.callback(
Output("main-function", "options"),
Output("main-function", "value"),
Output("parsed-functions-store", "data"),
Input("upload", "contents"),
State("upload", "filename")
)
def handle_upload(contents, filenames):
if not contents:
return [], None, {}
uploaded_files = {}
for content, name in zip(contents, filenames):
_, content_string = content.split(",")
uploaded_files[name] = base64.b64decode(content_string).decode("utf-8")
parsed = parse_functions_from_files(uploaded_files)
options = [{"label": fn, "value": fn} for fn in parsed if parsed[fn]["calls"]]
return options, options[0]["value"] if options else None, parsed
@app.callback(
Output("intro-tab", "style"),
Output("graph-tab", "style"),
Output("summary-tab", "style"),
Input("tab-selector", "value")
)
def toggle_tabs(tab):
return (
{"display": "block"} if tab == "intro" else {"display": "none"},
{"display": "block"} if tab == "graph" else {"display": "none"},
{"display": "block"} if tab == "summary" else {"display": "none"}
)
@app.callback(Output("intro-tab", "children"), Input("tab-selector", "value"))
def show_intro(_):
return get_intro_markdown()
@app.callback(
Output("graph-tab", "children"),
Input("main-function", "value"),
Input("backtrace-toggle", "value"),
State("parsed-functions-store", "data")
)
def update_graph(main_func, backtrace_mode, parsed):
if not parsed or not main_func:
return html.Div("Upload files and select a main function.")
graph = {k: v["calls"] for k, v in parsed.items()}
reachable = get_backtrace_functions(main_func, graph) if "backtrace" in backtrace_mode else get_reachable_functions(main_func, graph)
nodes = [{"data": {"id": name, "label": name}} for name in reachable]
#edges = [{"data": {"source": src, "target": tgt}, "classes": "edge"} for src in reachable for tgt in parsed[src]["calls"] if tgt in reachable]
edges = []
for src in reachable:
call_sequence = parsed[src]["calls"]
call_index = 1
for tgt in call_sequence:
if tgt in reachable:
edge = {
"data": {
"source": src,
"target": tgt,
"label": str(call_index)
},
"style": {
"line-width": 4 if call_sequence.count(tgt) > 1 else 2
}
}
edges.append(edge)
call_index += 1
return cyto.Cytoscape(
id="cytoscape-graph",
layout={"name": "breadthfirst"},
style={"width": "100%", "height": "600px"},
elements=nodes + edges,
stylesheet=[
{"selector": "node", "style": {"label": "data(label)", "text-wrap": "wrap"}},
#{"selector": "edge", "style": {"curve-style": "bezier", "target-arrow-shape": "triangle", "target-arrow-color": "#888", "line-color": "#888"}}
{"selector": "edge", "style": {
"label": "data(label)",
"curve-style": "bezier",
"target-arrow-shape": "triangle",
"target-arrow-color": "#888",
"line-color": "#888",
"font-size": "14px",
"text-margin-y": -10
}}
],
userZoomingEnabled=True,
userPanningEnabled=True,
minZoom=0.2,
maxZoom=2,
wheelSensitivity=0.1
)
@app.callback(
Output("summary-tab", "children"),
Input("parsed-functions-store", "data"),
Input("function-detail-toggle", "value")
)
def update_summary(parsed, toggle):
if not parsed:
return html.Div("No summary available.")
reverse_calls = {k: [] for k in parsed}
for caller, meta in parsed.items():
for callee in meta["calls"]:
reverse_calls[callee].append(caller)
if "show" in toggle:
df = pd.DataFrame([{
"Function": fn,
"File": parsed[fn]["filename"],
"Arguments": ", ".join(parsed[fn]["args"]),
"Returns": ", ".join(parsed[fn]["returns"]),
"Reads State": ", ".join(parsed[fn]["reads_state"]),
"Writes State": ", ".join(parsed[fn]["writes_state"]),
"Called By": len(reverse_calls[fn])
} for fn in parsed])
else:
file_summary = {}
for func, meta in parsed.items():
fname = meta["filename"]
file_summary.setdefault(fname, {"Total": 0, "Calls Others": 0, "Called By Others": 0, "Unused": 0})
file_summary[fname]["Total"] += 1
if meta["calls"]: file_summary[fname]["Calls Others"] += 1
if reverse_calls[func]: file_summary[fname]["Called By Others"] += 1
if not meta["calls"] and not reverse_calls[func]: file_summary[fname]["Unused"] += 1
df = pd.DataFrame([{"File": f, **stats} for f, stats in file_summary.items()])
return dash_table.DataTable(
data=df.to_dict("records"),
columns=[{"name": c, "id": c} for c in df.columns],
style_table={"overflowX": "auto"},
style_cell={"whiteSpace": "normal", "textAlign": "left", "padding": "5px", "maxWidth": 300}
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860)