code_graph / app.py
tlarsson's picture
Upload 2 files
3145636 verified
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, build_nodes_and_edges
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)", style={
"textAlign": "center",
"marginTop": "20px",
"fontSize": "28px",
"color": "#333"
}),
dcc.Store(id="parsed-functions-store"),
html.Div([
html.Div([
html.Label("Upload Files"),
dcc.Upload(
id="upload",
children=html.Div("📤 Drag and drop or click to upload Python files", style={
"border": "2px dashed #ccc",
"padding": "10px",
"textAlign": "center",
"cursor": "pointer",
"color": "#555",
"fontSize": "14px"
}),
multiple=True
)
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
html.Div([
html.Label("Main Function"),
dcc.Dropdown(id="main-function", style={"width": "100%"})
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
html.Div([
html.Label("Max Depth"),
dcc.Dropdown(
id="max-depth",
options=[{"label": str(i), "value": i} for i in range(1, 11)],
value=10,
clearable=False,
style={"width": "100%"}
)
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top", "marginRight": "1%"}),
html.Div([
html.Label("Options"),
dcc.Checklist(
id="backtrace-toggle",
options=[{"label": "Backtrace Mode", "value": "backtrace"}],
value=[],
labelStyle={"display": "block", "fontWeight": "normal"}
),
dcc.Checklist(
id="function-detail-toggle",
options=[{"label": "Show Function-Level Detail in Summaray tab", "value": "show"}],
value=[],
labelStyle={"display": "block", "fontWeight": "normal"},
style={"marginTop": "5px"}
)
], style={"width": "24%", "display": "inline-block", "verticalAlign": "top"})
], style={
"backgroundColor": "#f9f9f9",
"padding": "15px",
"borderRadius": "10px",
"margin": "20px auto",
"width": "95%",
"boxShadow": "0 2px 6px rgba(0,0,0,0.1)"
}),
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")
], style={"margin": "0 20px"}),
html.Div(html.Div(id="intro-tab", style={"margin": "0 20px"}), id="intro-tab-container"),
html.Div(id="graph-tab-container", style={"margin": "0 20px"}),
html.Div(id="summary-tab-container", style={"margin": "0 20px"}),
html.Footer("Built by Tomas Larsson • MIT Licensed", style={
"textAlign": "center",
"fontSize": "14px",
"marginTop": "40px",
"marginBottom": "20px",
"color": "#888"
})
])
@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):
print("=== handle_upload triggered ===")
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"]]
options = [{"label": fn, "value": fn} for fn in parsed]
return options, options[0]["value"] if options else None, parsed
@app.callback(
Output("intro-tab-container", "style"),
Output("graph-tab-container", "style"),
Output("summary-tab-container", "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()
from collections import deque, defaultdict
@app.callback(
Output("graph-tab-container", "children"),
Input("main-function", "value"),
Input("backtrace-toggle", "value"),
Input("max-depth", "value"),
State("parsed-functions-store", "data")
)
def update_graph(main_func, backtrace_mode, max_depth, 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, edges = build_nodes_and_edges(parsed, main_func, reachable, reverse="backtrace" in backtrace_mode, max_depth=max_depth)
return cyto.Cytoscape(
id="cytoscape-graph",
layout={"name": "preset"},
style={"width": "100%", "height": "600px"},
elements=nodes + edges,
stylesheet=[
{"selector": "node", "style": {
"label": "data(label)",
"text-wrap": "wrap",
"text-valign": "bottom",
"text-halign": "center"
}},
{"selector": ".main", "style": {
"background-color": "blue",
"line-color": "blue",
"color": "black",
"label": "data(label)",
"text-wrap": "wrap",
"text-valign": "bottom",
"text-halign": "center"
}},
{"selector": "edge", "style": {
"label": "data(label)",
"curve-style": "bezier",
"target-arrow-shape": "triangle",
"target-arrow-color": "#888",
"line-color": "#888",
"arrow-scale": 2,
"font-size": "14px",
"text-margin-y": -10,
"text-margin-x": 10,
}}
],
userZoomingEnabled=True,
userPanningEnabled=True,
minZoom=0.2,
maxZoom=2,
wheelSensitivity=0.1
)
@app.callback(
Output("summary-tab-container", "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)