tlarsson commited on
Commit
97db4d2
·
verified ·
1 Parent(s): 75b9a57

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -23
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import ast
3
  import base64
4
  import dash
@@ -16,14 +15,12 @@ def parse_functions_from_files(file_dict):
16
  functions = {}
17
  defined_funcs = set()
18
 
19
- # First pass to gather all defined function names with file context
20
  for fname, code in file_dict.items():
21
  tree = ast.parse(code)
22
  for node in ast.walk(tree):
23
  if isinstance(node, ast.FunctionDef):
24
  defined_funcs.add(node.name)
25
 
26
- # Second pass to build detailed structure
27
  for fname, code in file_dict.items():
28
  tree = ast.parse(code)
29
  for node in ast.walk(tree):
@@ -32,11 +29,16 @@ def parse_functions_from_files(file_dict):
32
  args = [arg.arg for arg in node.args.args]
33
  returns = []
34
  calls = []
 
 
35
 
36
  for sub in ast.walk(node):
 
37
  if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
38
  if sub.func.id in defined_funcs:
39
  calls.append(sub.func.id)
 
 
40
  elif isinstance(sub, ast.Return):
41
  if sub.value is None:
42
  continue
@@ -45,11 +47,36 @@ def parse_functions_from_files(file_dict):
45
  else:
46
  returns.append(ast.unparse(sub.value))
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  functions[func_name] = {
49
  "args": args,
50
  "returns": returns,
51
  "calls": calls,
52
- "filename": fname
 
 
53
  }
54
 
55
  return functions
@@ -66,16 +93,33 @@ def get_reachable_functions(start, graph):
66
 
67
  app.layout = html.Div([
68
  html.H2("Multi-File Function Dependency Graph (AST-based)"),
69
- dcc.Upload(id="upload", children=html.Button("Upload Python Files"), multiple=True),
70
- html.Div(id="file-name"),
71
- html.Br(),
72
- html.Div(id="main-function-ui"),
73
- html.A("Download Graph JSON", id="download-link", download="graph.json", href="", target="_blank"),
 
74
  cyto.Cytoscape(
75
  id="cytoscape-graph",
76
- layout={"name": "breadthfirst", "directed": True, "padding": 10},
77
  style={"width": "100%", "height": "600px"},
78
  elements=[],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  userZoomingEnabled=True,
80
  userPanningEnabled=True,
81
  minZoom=0.2,
@@ -86,12 +130,7 @@ app.layout = html.Div([
86
  html.H4("Function Input/Output Table"),
87
  dash_table.DataTable(
88
  id="function-table",
89
- columns=[
90
- {"name": "Function", "id": "Function"},
91
- {"name": "Arguments", "id": "Arguments"},
92
- {"name": "Returns", "id": "Returns"},
93
- {"name": "File", "id": "File"}
94
- ],
95
  style_table={"overflowX": "auto"},
96
  style_cell={
97
  "textAlign": "left",
@@ -127,18 +166,19 @@ def store_multi_upload(list_of_contents, list_of_names):
127
  @app.callback(
128
  Output("cytoscape-graph", "elements"),
129
  Output("function-table", "data"),
 
130
  Output("download-link", "href"),
131
  Input("main-function", "value")
132
  )
133
  def update_multi_graph(main_func):
134
  if not uploaded_files or not main_func:
135
- return [], [], ""
136
 
137
  parsed = parse_functions_from_files(uploaded_files)
138
  graph = {k: v["calls"] for k, v in parsed.items()}
139
  reachable = get_reachable_functions(main_func, graph)
140
 
141
- nodes = [{"data": {"id": name, "label": f"{name}\n({parsed[name]['filename']})"}} for name in reachable]
142
 
143
  edges = []
144
  for src in reachable:
@@ -154,19 +194,33 @@ def update_multi_graph(main_func):
154
  edge["classes"] = "crossfile"
155
  edges.append(edge)
156
 
157
- table_data = [{
158
  "Function": fn,
159
  "Arguments": ", ".join(parsed[fn]["args"]),
160
  "Returns": ", ".join(parsed[fn]["returns"]),
 
 
161
  "File": parsed[fn]["filename"]
162
  } for fn in reachable]
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  json_data = json.dumps({"nodes": nodes, "edges": edges}, indent=2)
165
  href_data = "data:application/json;charset=utf-8," + urllib.parse.quote(json_data)
166
 
167
- return nodes + edges, table_data, href_data
168
-
169
 
170
  if __name__ == "__main__":
171
- app.run(host="0.0.0.0", port=7860, debug=False)
172
-
 
 
1
  import ast
2
  import base64
3
  import dash
 
15
  functions = {}
16
  defined_funcs = set()
17
 
 
18
  for fname, code in file_dict.items():
19
  tree = ast.parse(code)
20
  for node in ast.walk(tree):
21
  if isinstance(node, ast.FunctionDef):
22
  defined_funcs.add(node.name)
23
 
 
24
  for fname, code in file_dict.items():
25
  tree = ast.parse(code)
26
  for node in ast.walk(tree):
 
29
  args = [arg.arg for arg in node.args.args]
30
  returns = []
31
  calls = []
32
+ reads_state = set()
33
+ writes_state = set()
34
 
35
  for sub in ast.walk(node):
36
+ # Function calls
37
  if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
38
  if sub.func.id in defined_funcs:
39
  calls.append(sub.func.id)
40
+
41
+ # Return values
42
  elif isinstance(sub, ast.Return):
43
  if sub.value is None:
44
  continue
 
47
  else:
48
  returns.append(ast.unparse(sub.value))
49
 
50
+ # Streamlit session_state interactions
51
+ elif isinstance(sub, ast.Subscript):
52
+ if (
53
+ isinstance(sub.value, ast.Attribute)
54
+ and isinstance(sub.value.value, ast.Name)
55
+ and sub.value.value.id == "st"
56
+ and sub.value.attr == "session_state"
57
+ ):
58
+ key = None
59
+ try:
60
+ if isinstance(sub.slice, ast.Constant): # Python 3.9+
61
+ key = sub.slice.value
62
+ elif isinstance(sub.slice, ast.Index) and isinstance(sub.slice.value, ast.Constant): # <3.9
63
+ key = sub.slice.value.value
64
+ except Exception:
65
+ pass
66
+
67
+ if key:
68
+ if isinstance(sub.ctx, ast.Store):
69
+ writes_state.add(str(key))
70
+ else:
71
+ reads_state.add(str(key))
72
+
73
  functions[func_name] = {
74
  "args": args,
75
  "returns": returns,
76
  "calls": calls,
77
+ "filename": fname,
78
+ "reads_state": sorted(reads_state),
79
+ "writes_state": sorted(writes_state),
80
  }
81
 
82
  return functions
 
93
 
94
  app.layout = html.Div([
95
  html.H2("Multi-File Function Dependency Graph (AST-based)"),
96
+ html.Div([
97
+ dcc.Upload(id="upload", children=html.Button("Upload Python Files"), multiple=True, style={"marginRight": "10px"}),
98
+ html.Div(id="main-function-ui", style={"marginRight": "10px", "width": "300px"}),
99
+ html.A("Download Graph JSON", id="download-link", download="graph.json", href="", target="_blank")
100
+ ], style={"display": "flex", "flexDirection": "row", "alignItems": "center", "gap": "10px"}),
101
+ html.Div(id="file-name", style={"marginTop": "10px"}),
102
  cyto.Cytoscape(
103
  id="cytoscape-graph",
104
+ layout={"name": "breadthfirst", "directed": True, "padding": 30, "spacingFactor": 1.5},
105
  style={"width": "100%", "height": "600px"},
106
  elements=[],
107
+ stylesheet=[
108
+ {"selector": "node", "style": {
109
+ "label": "data(label)",
110
+ "text-wrap": "wrap",
111
+ "text-max-width": 120,
112
+ "text-valign": "center",
113
+ "background-color": "#aed6f1"
114
+ }},
115
+ {"selector": "edge", "style": {
116
+ "curve-style": "bezier",
117
+ "target-arrow-shape": "triangle",
118
+ "arrow-scale": 2,
119
+ "line-color": "#888",
120
+ "target-arrow-color": "#888"
121
+ }}
122
+ ],
123
  userZoomingEnabled=True,
124
  userPanningEnabled=True,
125
  minZoom=0.2,
 
130
  html.H4("Function Input/Output Table"),
131
  dash_table.DataTable(
132
  id="function-table",
133
+ columns=[], # dynamically injected
 
 
 
 
 
134
  style_table={"overflowX": "auto"},
135
  style_cell={
136
  "textAlign": "left",
 
166
  @app.callback(
167
  Output("cytoscape-graph", "elements"),
168
  Output("function-table", "data"),
169
+ Output("function-table", "columns"),
170
  Output("download-link", "href"),
171
  Input("main-function", "value")
172
  )
173
  def update_multi_graph(main_func):
174
  if not uploaded_files or not main_func:
175
+ return [], [], [], ""
176
 
177
  parsed = parse_functions_from_files(uploaded_files)
178
  graph = {k: v["calls"] for k, v in parsed.items()}
179
  reachable = get_reachable_functions(main_func, graph)
180
 
181
+ nodes = [{"data": {"id": name, "label": name}} for name in reachable]
182
 
183
  edges = []
184
  for src in reachable:
 
194
  edge["classes"] = "crossfile"
195
  edges.append(edge)
196
 
197
+ raw_table_data = [{
198
  "Function": fn,
199
  "Arguments": ", ".join(parsed[fn]["args"]),
200
  "Returns": ", ".join(parsed[fn]["returns"]),
201
+ "Reads State": ", ".join(parsed[fn]["reads_state"]),
202
+ "Writes State": ", ".join(parsed[fn]["writes_state"]),
203
  "File": parsed[fn]["filename"]
204
  } for fn in reachable]
205
 
206
+ show_reads = any(row["Reads State"] for row in raw_table_data)
207
+ show_writes = any(row["Writes State"] for row in raw_table_data)
208
+
209
+ columns = [
210
+ {"name": "Function", "id": "Function"},
211
+ {"name": "Arguments", "id": "Arguments"},
212
+ {"name": "Returns", "id": "Returns"},
213
+ ]
214
+ if show_reads:
215
+ columns.append({"name": "Reads State", "id": "Reads State"})
216
+ if show_writes:
217
+ columns.append({"name": "Writes State", "id": "Writes State"})
218
+ columns.append({"name": "File", "id": "File"})
219
+
220
  json_data = json.dumps({"nodes": nodes, "edges": edges}, indent=2)
221
  href_data = "data:application/json;charset=utf-8," + urllib.parse.quote(json_data)
222
 
223
+ return nodes + edges, raw_table_data, columns, href_data
 
224
 
225
  if __name__ == "__main__":
226
+ app.run(host="0.0.0.0", port=7860)