Spaces:
Sleeping
Sleeping
| 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" | |
| }) | |
| ]) | |
| 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 | |
| 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"} | |
| ) | |
| def show_intro(_): | |
| return get_intro_markdown() | |
| from collections import deque, defaultdict | |
| 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 | |
| ) | |
| 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) |