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 | |
| 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") | |
| ]) | |
| 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 | |
| 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() | |
| 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 | |
| ) | |
| 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) |