| import gradio as gr |
| import sys |
| import os |
| import tempfile |
| import shutil |
| import ast |
| import time |
| import subprocess |
| import re |
| from typing import List, Dict, Optional, Tuple, Any |
| from py2puml.py2puml import py2puml |
| from plantuml import PlantUML |
| import pyan |
| from pathlib import Path |
| from utils import setup_testing_space, verify_testing_space, cleanup_testing_space |
|
|
| if os.name == "nt": |
| graphviz_bin = r"C:\\Program Files\\Graphviz\\bin" |
| if graphviz_bin not in os.environ["PATH"]: |
| os.environ["PATH"] += os.pathsep + graphviz_bin |
|
|
|
|
| def generate_call_graph_with_pyan3( |
| python_code: str, filename: str = "analysis" |
| ) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]: |
| """Generate call graph using pyan3 and return DOT content, PNG path, and structured data. |
| |
| Args: |
| python_code: The Python code to analyze |
| filename: Base filename for temporary files |
| |
| Returns: |
| Tuple of (dot_content, png_path, structured_data) |
| """ |
| if not python_code.strip(): |
| return None, None, {} |
|
|
| |
| timestamp = str(int(time.time() * 1000)) |
| unique_filename = f"{filename}_{timestamp}" |
|
|
| |
| testing_dir = os.path.join(os.getcwd(), "inputs") |
| code_file = os.path.join(testing_dir, f"{unique_filename}.py") |
|
|
| try: |
| |
| with open(code_file, "w", encoding="utf-8") as f: |
| f.write(python_code) |
|
|
| print(f"📊 Generating call graph for: {unique_filename}.py") |
|
|
| try: |
|
|
| dot_content = pyan.create_callgraph( |
| filenames=[str(code_file)], |
| format="dot", |
| colored=True, |
| grouped=True, |
| annotated=True, |
| ) |
|
|
| png_path = None |
| with tempfile.TemporaryDirectory() as temp_dir: |
| dot_file = os.path.join(temp_dir, f"{unique_filename}.dot") |
| temp_png = os.path.join(temp_dir, f"{unique_filename}.png") |
|
|
| |
| with open(dot_file, "w", encoding="utf-8") as f: |
| f.write(dot_content) |
|
|
| |
| dot_cmd = ["dot", "-Tpng", dot_file, "-o", temp_png] |
|
|
| try: |
| subprocess.run(dot_cmd, check=True, timeout=30) |
|
|
| if os.path.exists(temp_png): |
| |
| permanent_dir = os.path.join(os.getcwd(), "temp_diagrams") |
| os.makedirs(permanent_dir, exist_ok=True) |
| png_path = os.path.join( |
| permanent_dir, f"callgraph_{unique_filename}.png" |
| ) |
| shutil.copy2(temp_png, png_path) |
| print(f"🎨 Call graph PNG saved: {os.path.basename(png_path)}") |
|
|
| except subprocess.SubprocessError as e: |
| print(f"⚠️ Graphviz PNG generation failed: {e}") |
| |
|
|
| |
| structured_data = parse_call_graph_data(dot_content) |
|
|
| return dot_content, png_path, structured_data |
|
|
| except subprocess.TimeoutExpired: |
| print("⚠️ pyan3 analysis timed out, trying simplified approach...") |
| return try_fallback_analysis(python_code, unique_filename) |
| except subprocess.SubprocessError as e: |
| print(f"⚠️ pyan3 execution failed: {e}, trying fallback...") |
| return try_fallback_analysis(python_code, unique_filename) |
|
|
| except Exception as e: |
| print(f"❌ Call graph generation error: {e}") |
| return None, None, {"error": str(e)} |
|
|
| finally: |
| |
| if os.path.exists(code_file): |
| try: |
| os.remove(code_file) |
| print(f"🧹 Cleaned up analysis file: {unique_filename}.py") |
| except Exception as e: |
| print(f"⚠️ Could not remove analysis file: {e}") |
|
|
|
|
| def parse_call_graph_data(dot_content: str) -> Dict[str, Any]: |
| """Parse pyan3 DOT output into structured function call data. |
| |
| Args: |
| dot_content: DOT format string from pyan3 |
| |
| Returns: |
| Dictionary with parsed call graph information |
| """ |
| if not dot_content: |
| return {} |
|
|
| try: |
| |
| node_pattern = r'"([^"]+)"\s*\[' |
| nodes = re.findall(node_pattern, dot_content) |
|
|
| |
| edge_pattern = r'"([^"]+)"\s*->\s*"([^"]+)"' |
| edges = re.findall(edge_pattern, dot_content) |
|
|
| |
| call_graph = {} |
| called_by = {} |
|
|
| for caller, callee in edges: |
| if caller not in call_graph: |
| call_graph[caller] = [] |
| call_graph[caller].append(callee) |
|
|
| if callee not in called_by: |
| called_by[callee] = [] |
| called_by[callee].append(caller) |
|
|
| |
| function_metrics = {} |
| for node in nodes: |
| out_degree = len(call_graph.get(node, [])) |
| in_degree = len(called_by.get(node, [])) |
|
|
| function_metrics[node] = { |
| "calls_made": out_degree, |
| "called_by_count": in_degree, |
| "calls_to": call_graph.get(node, []), |
| "called_by": called_by.get(node, []), |
| } |
|
|
| return { |
| "nodes": nodes, |
| "edges": edges, |
| "total_functions": len(nodes), |
| "total_calls": len(edges), |
| "call_graph": call_graph, |
| "function_metrics": function_metrics, |
| } |
|
|
| except Exception as e: |
| return {"parse_error": str(e)} |
|
|
|
|
| def try_fallback_analysis( |
| python_code: str, unique_filename: str |
| ) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]: |
| """Fallback analysis when pyan3 fails - basic function call detection. |
| |
| Args: |
| python_code: The Python code to analyze |
| unique_filename: Unique filename for this analysis |
| |
| Returns: |
| Tuple of (None, None, fallback_analysis_data) |
| """ |
| print("🔄 Using fallback analysis approach...") |
|
|
| try: |
| import ast |
| import re |
|
|
| tree = ast.parse(python_code) |
| functions = [] |
| calls = [] |
|
|
| |
| for node in ast.walk(tree): |
| if isinstance(node, ast.FunctionDef): |
| functions.append(node.name) |
|
|
| |
| for func in functions: |
| |
| pattern = rf"\b{re.escape(func)}\s*\(" |
| if re.search(pattern, python_code): |
| calls.append(("unknown", func)) |
|
|
| return ( |
| None, |
| None, |
| { |
| "fallback": True, |
| "functions_detected": functions, |
| "total_functions": len(functions), |
| "total_calls": len(calls), |
| "info": f"Fallback analysis: detected {len(functions)} functions", |
| "function_metrics": { |
| func: { |
| "calls_made": 0, |
| "called_by_count": 0, |
| "calls_to": [], |
| "called_by": [], |
| } |
| for func in functions |
| }, |
| }, |
| ) |
|
|
| except Exception as e: |
| return None, None, {"error": f"Fallback analysis also failed: {str(e)}"} |
|
|
|
|
| def analyze_function_complexity(python_code: str) -> Dict[str, Any]: |
| """Analyze function complexity using AST. |
| |
| Args: |
| python_code: The Python code to analyze |
| |
| Returns: |
| Dictionary with function complexity metrics |
| """ |
| if not python_code.strip(): |
| return {} |
|
|
| try: |
| tree = ast.parse(python_code) |
| function_analysis = {} |
|
|
| for node in ast.walk(tree): |
| if isinstance(node, ast.FunctionDef): |
| |
| complexity = 1 |
|
|
| for child in ast.walk(node): |
| if isinstance( |
| child, |
| ( |
| ast.If, |
| ast.While, |
| ast.For, |
| ast.Try, |
| ast.ExceptHandler, |
| ast.With, |
| ast.Assert, |
| ), |
| ): |
| complexity += 1 |
| elif isinstance(child, ast.BoolOp): |
| complexity += len(child.values) - 1 |
|
|
| |
| lines = ( |
| node.end_lineno - node.lineno + 1 |
| if hasattr(node, "end_lineno") |
| else 0 |
| ) |
|
|
| |
| params = [arg.arg for arg in node.args.args] |
|
|
| |
| has_docstring = ( |
| len(node.body) > 0 |
| and isinstance(node.body[0], ast.Expr) |
| and isinstance(node.body[0].value, ast.Constant) |
| and isinstance(node.body[0].value.value, str) |
| ) |
|
|
| function_analysis[node.name] = { |
| "complexity": complexity, |
| "lines_of_code": lines, |
| "parameter_count": len(params), |
| "parameters": params, |
| "has_docstring": has_docstring, |
| "line_start": node.lineno, |
| "line_end": getattr(node, "end_lineno", node.lineno), |
| } |
|
|
| return function_analysis |
|
|
| except Exception as e: |
| return {"error": str(e)} |
|
|
|
|
| def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[str]: |
| """Generate a UML class diagram from Python code. |
| |
| Args: |
| python_code: The Python code to analyze and convert to UML |
| filename: Optional name for the generated diagram file |
| |
| Returns: |
| Path to the generated PNG diagram image or None if failed |
| """ |
| if not python_code.strip(): |
| return None |
|
|
| print(f"🔄 Processing code for diagram generation...") |
|
|
| |
| cleanup_testing_space() |
|
|
| |
| if not verify_testing_space(): |
| print("⚠️ testing_space verification failed, recreating...") |
| setup_testing_space() |
| cleanup_testing_space() |
|
|
| |
| timestamp = str(int(time.time() * 1000)) |
| unique_filename = f"{filename}_{timestamp}" |
|
|
| |
| testing_dir = os.path.join(os.getcwd(), "inputs") |
| code_file = os.path.join(testing_dir, f"{unique_filename}.py") |
|
|
| |
| server = PlantUML(url="http://www.plantuml.com/plantuml/img/") |
|
|
| try: |
| |
| with open(code_file, "w", encoding="utf-8") as f: |
| f.write(python_code) |
|
|
| print(f"📝 Created temporary file: inputs/{unique_filename}.py") |
|
|
| |
| print(f"📝 Generating PlantUML content...") |
| puml_content_lines = py2puml( |
| os.path.join( |
| testing_dir, unique_filename |
| ), |
| f"inputs.{unique_filename}", |
| ) |
| puml_content = "".join(puml_content_lines) |
|
|
| if not puml_content.strip(): |
| print("⚠️ No UML content generated - check if your code contains classes") |
| return None |
|
|
| |
| with tempfile.TemporaryDirectory() as temp_dir: |
| |
| puml_file = os.path.join(temp_dir, f"{unique_filename}.puml") |
| with open(puml_file, "w", encoding="utf-8") as f: |
| f.write(puml_content) |
|
|
| print(f"🎨 Rendering diagram...") |
| |
| output_png = os.path.join(temp_dir, f"{unique_filename}.png") |
| server.processes_file(puml_file, outfile=output_png) |
|
|
| if os.path.exists(output_png): |
| print("✅ Diagram generated successfully!") |
| |
| permanent_dir = os.path.join(os.getcwd(), "temp_diagrams") |
| os.makedirs(permanent_dir, exist_ok=True) |
| permanent_path = os.path.join( |
| permanent_dir, f"{filename}_{hash(python_code) % 10000}.png" |
| ) |
| shutil.copy2(output_png, permanent_path) |
| return permanent_path |
| else: |
| print("❌ Failed to generate PNG") |
| return None |
|
|
| except Exception as e: |
| print(f"❌ Error: {e}") |
| return None |
|
|
| finally: |
| |
| if os.path.exists(code_file): |
| try: |
| os.remove(code_file) |
| print(f"🧹 Cleaned up temporary file: {unique_filename}.py") |
| except Exception as e: |
| print(f"⚠️ Could not remove temporary file: {e}") |
|
|
|
|
| def analyze_code_structure(python_code: str) -> str: |
| """Return a Markdown report with complexity metrics and recommendations. |
| |
| Args: |
| python_code: The Python code to analyze |
| |
| Returns: |
| Comprehensive analysis report in markdown format |
| """ |
| if not python_code.strip(): |
| return "No code provided for analysis." |
|
|
| try: |
| |
| tree = ast.parse(python_code) |
| classes = [] |
| functions = [] |
| imports = [] |
|
|
| for node in ast.walk(tree): |
| if isinstance(node, ast.ClassDef): |
| methods = [] |
| attributes = [] |
|
|
| for item in node.body: |
| if isinstance(item, ast.FunctionDef): |
| methods.append(item.name) |
| elif isinstance(item, ast.Assign): |
| for target in item.targets: |
| if isinstance(target, ast.Name): |
| attributes.append(target.id) |
|
|
| |
| parents = [base.id for base in node.bases if isinstance(base, ast.Name)] |
|
|
| classes.append( |
| { |
| "name": node.name, |
| "methods": methods, |
| "attributes": attributes, |
| "parents": parents, |
| } |
| ) |
|
|
| elif isinstance(node, ast.FunctionDef): |
| |
| is_method = any( |
| isinstance(parent, ast.ClassDef) |
| for parent in ast.walk(tree) |
| if hasattr(parent, "body") and node in getattr(parent, "body", []) |
| ) |
| if not is_method: |
| functions.append(node.name) |
|
|
| elif isinstance(node, (ast.Import, ast.ImportFrom)): |
| if isinstance(node, ast.Import): |
| for alias in node.names: |
| imports.append(alias.name) |
| else: |
| module = node.module or "" |
| for alias in node.names: |
| imports.append( |
| f"{module}.{alias.name}" if module else alias.name |
| ) |
|
|
| |
| function_complexity = analyze_function_complexity(python_code) |
|
|
| |
| call_graph_data = {} |
| if functions or any(classes): |
| try: |
| cleanup_testing_space() |
| dot_content, png_path, call_graph_data = generate_call_graph_with_pyan3( |
| python_code |
| ) |
| except Exception as e: |
| print(f"⚠️ Call graph analysis failed: {e}") |
| call_graph_data = {"error": str(e)} |
|
|
| |
| summary = "📊 **Enhanced Code Analysis Results**\n\n" |
|
|
| |
| summary += "## 📋 **Overview**\n" |
| summary += f"• **{len(classes)}** classes found\n" |
| summary += f"• **{len(functions)}** standalone functions found\n" |
| summary += f"• **{len(set(imports))}** unique imports\n" |
|
|
| if call_graph_data and "total_functions" in call_graph_data: |
| summary += f"• **{call_graph_data['total_functions']}** total functions/methods in call graph\n" |
| summary += ( |
| f"• **{call_graph_data['total_calls']}** function calls detected\n" |
| ) |
|
|
| summary += "\n" |
|
|
| |
| if classes: |
| summary += "## 🏗️ **Classes**\n" |
| for cls in classes: |
| summary += f"### **{cls['name']}**\n" |
| if cls["parents"]: |
| summary += f" - **Inherits from**: {', '.join(cls['parents'])}\n" |
| summary += f" - **Methods**: {len(cls['methods'])}" |
| if cls["methods"]: |
| summary += f" ({', '.join(cls['methods'])})" |
| summary += "\n" |
| if cls["attributes"]: |
| summary += f" - **Attributes**: {', '.join(cls['attributes'])}\n" |
| summary += "\n" |
|
|
| |
| if functions: |
| summary += "## ⚙️ **Standalone Functions**\n" |
| for func in functions: |
| summary += f"### **{func}()**\n" |
|
|
| |
| if func in function_complexity: |
| metrics = function_complexity[func] |
| summary += ( |
| f" - **Complexity**: {metrics['complexity']} (cyclomatic)\n" |
| ) |
| summary += f" - **Lines of Code**: {metrics['lines_of_code']}\n" |
| summary += f" - **Parameters**: {metrics['parameter_count']}" |
| if metrics["parameters"]: |
| summary += f" ({', '.join(metrics['parameters'])})" |
| summary += "\n" |
| summary += f" - **Has Docstring**: {'✅' if metrics['has_docstring'] else '❌'}\n" |
| summary += f" - **Lines**: {metrics['line_start']}-{metrics['line_end']}\n" |
|
|
| |
| if call_graph_data and "function_metrics" in call_graph_data: |
| if func in call_graph_data["function_metrics"]: |
| call_metrics = call_graph_data["function_metrics"][func] |
| summary += f" - **Calls Made**: {call_metrics['calls_made']}\n" |
| if call_metrics["calls_to"]: |
| summary += ( |
| f" - Calls: {', '.join(call_metrics['calls_to'])}\n" |
| ) |
| summary += f" - **Called By**: {call_metrics['called_by_count']} functions\n" |
| if call_metrics["called_by"]: |
| summary += f" - Called by: {', '.join(call_metrics['called_by'])}\n" |
|
|
| summary += "\n" |
|
|
| |
| if ( |
| call_graph_data |
| and "function_metrics" in call_graph_data |
| and call_graph_data["total_calls"] > 0 |
| ): |
| summary += "## 🔗 **Function Call Analysis**\n" |
|
|
| |
| sorted_by_calls = sorted( |
| call_graph_data["function_metrics"].items(), |
| key=lambda x: x[1]["called_by_count"], |
| reverse=True, |
| )[:5] |
|
|
| if sorted_by_calls and sorted_by_calls[0][1]["called_by_count"] > 0: |
| summary += "**Most Called Functions:**\n" |
| for func_name, metrics in sorted_by_calls: |
| if metrics["called_by_count"] > 0: |
| summary += f"• **{func_name}**: called {metrics['called_by_count']} times\n" |
| summary += "\n" |
|
|
| |
| sorted_by_complexity = sorted( |
| call_graph_data["function_metrics"].items(), |
| key=lambda x: x[1]["calls_made"], |
| reverse=True, |
| )[:5] |
|
|
| if sorted_by_complexity and sorted_by_complexity[0][1]["calls_made"] > 0: |
| summary += "**Functions Making Most Calls:**\n" |
| for func_name, metrics in sorted_by_complexity: |
| if metrics["calls_made"] > 0: |
| summary += ( |
| f"• **{func_name}**: makes {metrics['calls_made']} calls\n" |
| ) |
| summary += "\n" |
|
|
| |
| if function_complexity: |
| summary += "## 📈 **Complexity Analysis**\n" |
|
|
| |
| sorted_complexity = sorted( |
| function_complexity.items(), |
| key=lambda x: x[1]["complexity"], |
| reverse=True, |
| )[:5] |
|
|
| summary += "**Most Complex Functions:**\n" |
| for func_name, metrics in sorted_complexity: |
| summary += f"• **{func_name}**: complexity {metrics['complexity']}, {metrics['lines_of_code']} lines\n" |
|
|
| |
| total_functions = len(function_complexity) |
| avg_complexity = ( |
| sum(m["complexity"] for m in function_complexity.values()) |
| / total_functions |
| ) |
| avg_lines = ( |
| sum(m["lines_of_code"] for m in function_complexity.values()) |
| / total_functions |
| ) |
| functions_with_docs = sum( |
| 1 for m in function_complexity.values() if m["has_docstring"] |
| ) |
|
|
| summary += "\n**Overall Function Metrics:**\n" |
| summary += f"• **Average Complexity**: {avg_complexity:.1f}\n" |
| summary += f"• **Average Lines per Function**: {avg_lines:.1f}\n" |
| summary += f"• **Functions with Docstrings**: {functions_with_docs}/{total_functions} ({100*functions_with_docs/total_functions:.1f}%)\n" |
| summary += "\n" |
|
|
| |
| if imports: |
| summary += "## 📦 **Imports**\n" |
| unique_imports = list(set(imports)) |
| for imp in unique_imports[:10]: |
| summary += f"• {imp}\n" |
| if len(unique_imports) > 10: |
| summary += f"• ... and {len(unique_imports) - 10} more\n" |
| summary += "\n" |
|
|
| |
| if call_graph_data and "error" in call_graph_data: |
| summary += "## ⚠️ **Call Graph Analysis**\n" |
| summary += f"Call graph generation failed: {call_graph_data['error']}\n\n" |
| elif call_graph_data and "info" in call_graph_data: |
| summary += "## 📊 **Call Graph Analysis**\n" |
| summary += f"{call_graph_data['info']}\n\n" |
|
|
| |
| summary += "## 💡 **Recommendations**\n" |
| if function_complexity: |
| high_complexity = [ |
| f for f, m in function_complexity.items() if m["complexity"] > 10 |
| ] |
| if high_complexity: |
| summary += f"• Consider refactoring high-complexity functions: {', '.join(high_complexity)}\n" |
|
|
| no_docs = [ |
| f for f, m in function_complexity.items() if not m["has_docstring"] |
| ] |
| if no_docs: |
| summary += f"• Add docstrings to: {', '.join(no_docs[:5])}{'...' if len(no_docs) > 5 else ''}\n" |
|
|
| if call_graph_data and "function_metrics" in call_graph_data: |
| isolated_functions = [ |
| f |
| for f, m in call_graph_data["function_metrics"].items() |
| if m["calls_made"] == 0 and m["called_by_count"] == 0 |
| ] |
| if isolated_functions: |
| summary += f"• Review isolated functions: {', '.join(isolated_functions[:3])}{'...' if len(isolated_functions) > 3 else ''}\n" |
|
|
| return summary |
|
|
| except SyntaxError as e: |
| return f"❌ **Syntax Error in Python code:**\n```\n{str(e)}\n```" |
| except Exception as e: |
| return f"❌ **Error analyzing code:**\n```\n{str(e)}\n```" |
|
|
|
|
| def list_example_files() -> list: |
| """List all example .py files in the examples/ directory.""" |
| examples_dir = os.path.join(os.getcwd(), "examples") |
| if not os.path.exists(examples_dir): |
| return [] |
| return [f for f in os.listdir(examples_dir) if f.endswith(".py")] |
|
|
|
|
| def get_sample_code(filename: str) -> str: |
| """Return sample Python code from examples/ directory.""" |
| examples_dir = os.path.join(os.getcwd(), "examples") |
| file_path = os.path.join(examples_dir, filename) |
| with open(file_path, "r", encoding="utf-8") as f: |
| return f.read() |
|
|
|
|
| def generate_all_diagrams( |
| python_code: str, filename: str = "diagram" |
| ) -> Tuple[Optional[str], Optional[str], str]: |
| """Generate class diagram, call-graph diagram and analysis in one call. |
| |
| Args: |
| python_code: The Python code to analyze |
| filename: Base filename for diagrams |
| |
| Returns: |
| Tuple of (uml_diagram_path, call_graph_path, analysis_text) |
| """ |
| if not python_code.strip(): |
| return None, None, "No code provided for analysis." |
|
|
| print("🚀 Starting comprehensive diagram generation...") |
|
|
| |
| print("📊 Step 1/3: Generating UML class diagram...") |
| uml_diagram_path = generate_diagram(python_code, filename) |
|
|
| |
| print("🔗 Step 2/3: Generating call graph...") |
| try: |
| cleanup_testing_space() |
| dot_content, call_graph_path, structured_data = generate_call_graph_with_pyan3( |
| python_code |
| ) |
| except Exception as e: |
| print(f"⚠️ Call graph generation failed: {e}") |
| call_graph_path = None |
|
|
| |
| print("📈 Step 3/3: Performing code analysis...") |
| analysis_text = analyze_code_structure(python_code) |
|
|
| print("✅ All diagrams and analysis completed!") |
|
|
| return uml_diagram_path, call_graph_path, analysis_text |
|
|
|
|
| |
| |
| |
| |
|
|
| def generate_class_diagram_only(python_code: str) -> Optional[str]: |
| """Generates just the UML class diagram.""" |
| if not python_code.strip(): |
| gr.Warning("Input code is empty!") |
| return None |
| return generate_diagram(python_code) |
|
|
| def generate_call_graph_only(python_code: str) -> Optional[str]: |
| """Generates just the call graph diagram.""" |
| if not python_code.strip(): |
| gr.Warning("Input code is empty!") |
| return None |
| _, png_path, _ = generate_call_graph_with_pyan3(python_code) |
| return png_path |
|
|
| def analyze_code_only(python_code: str) -> str: |
| """Generates just the code analysis report.""" |
| if not python_code.strip(): |
| gr.Warning("Input code is empty!") |
| return "No code provided to analyze." |
| return analyze_code_structure(python_code) |
| |
| def generate_all_outputs(python_code: str) -> Tuple[Optional[str], Optional[str], str]: |
| """Generates all three outputs: UML diagram, call graph, and analysis.""" |
| if not python_code.strip(): |
| gr.Warning("Input code is empty!") |
| return None, None, "No code provided to analyze." |
| |
| print("🚀 Starting comprehensive generation...") |
| uml_path = generate_diagram(python_code) |
| _, call_graph_path, _ = generate_call_graph_with_pyan3(python_code) |
| analysis_text = analyze_code_structure(python_code) |
| print("✅ All outputs generated!") |
| |
| return uml_path, call_graph_path, analysis_text |
|
|
| |
| |
| |
| |
|
|
| iface_class = gr.Interface( |
| fn=generate_class_diagram_only, |
| inputs=gr.Textbox(lines=20, label="Python code"), |
| outputs=gr.Image(label="UML diagram"), |
| api_name="generate_class_diagram", |
| description="Create a UML class diagram (PNG) from Python code.", |
| ) |
|
|
| iface_call = gr.Interface( |
| fn=generate_call_graph_only, |
| inputs=gr.Textbox(lines=20, label="Python code"), |
| outputs=gr.Image(label="Call‑graph"), |
| api_name="generate_call_graph_diagram", |
| description="Generate a function‑call graph (PNG) from Python code.", |
| ) |
|
|
| iface_analysis = gr.Interface( |
| fn=analyze_code_only, |
| inputs=gr.Textbox(lines=20, label="Python code"), |
| outputs=gr.Markdown(label="Analysis"), |
| api_name="analyze_code_structure", |
| description="Return a Markdown report with complexity metrics.", |
| ) |
|
|
| iface_all = gr.Interface( |
| fn=generate_all_outputs, |
| inputs=gr.Textbox(lines=20, label="Python code"), |
| outputs=[ |
| gr.Image(label="UML diagram"), |
| gr.Image(label="Call‑graph"), |
| gr.Markdown(label="Analysis"), |
| ], |
| api_name="generate_all", |
| description="Run class diagram, call graph and analysis in one call.", |
| ) |
|
|
|
|
| |
| |
| |
| with gr.Blocks( |
| title="Python Code Visualizer & Analyzer", |
| theme=gr.themes.Soft(primary_hue="blue"), |
| css=""" .gradio-container { max-width: 1400px !important; } """, |
| ) as demo: |
| |
| |
| |
| |
| gr.Markdown( |
| """ |
| # 🐍 Python Code Visualizer & Analyzer |
| **Enter Python code, then choose an action to generate diagrams and analysis.** |
| This app also functions as an MCP Server, exposing four tools for AI assistants. |
| """ |
| ) |
|
|
| with gr.Row(): |
| |
| with gr.Column(scale=2): |
| gr.Markdown("### 1. Input Code") |
| |
| example_files = list_example_files() |
| print(f"🔍 Found {len(example_files)} example files: {example_files}") |
| if example_files: |
| example_dropdown = gr.Dropdown( |
| label="Load an Example", |
| choices=example_files, |
| value=example_files[0], |
| ) |
| |
| |
| initial_code = "Choose an example file from dropdown or paster your python code here " |
| |
| else: |
| initial_code = "# Paste your Python code here\n\nclass MyClass:\n pass" |
|
|
| code_input = gr.Textbox( |
| label="Python Code", |
| placeholder="Paste your Python code here…", |
| lines=15, |
| max_lines=200, |
| value=initial_code, |
| elem_classes=["code-input"], |
| ) |
|
|
| gr.Markdown("### 2. Choose an Action") |
| with gr.Row(): |
| class_btn = gr.Button("🖼️ Generate Class Diagram") |
| call_graph_btn = gr.Button("🔗 Generate Call Graph") |
| analyze_btn = gr.Button("📈 Analyze Code") |
| all_btn = gr.Button("✨ Generate All", variant="primary") |
|
|
| |
| with gr.Column(scale=3): |
| gr.Markdown("### 3. Results") |
| with gr.Tabs(): |
| with gr.TabItem("UML Class Diagram"): |
| uml_output = gr.Image(label="UML Class Diagram", show_download_button=True, interactive=False) |
| with gr.TabItem("Function Call Graph"): |
| call_graph_output = gr.Image(label="Function Call Graph", show_download_button=True, interactive=False) |
| with gr.TabItem("Code Analysis Report"): |
| analysis_output = gr.Markdown(label="Comprehensive Code Analysis", elem_classes=["analysis-output"]) |
|
|
| |
| |
| |
|
|
| |
| if example_files: |
| def _load_example(example_filename: str): |
| return get_sample_code(example_filename) |
| example_dropdown.change(fn=_load_example, inputs=example_dropdown, outputs=code_input, api_name = False) |
|
|
| |
| |
| |
| |
| |
| |
| |
| class_btn.click( |
| fn=generate_class_diagram_only, |
| inputs=[code_input], |
| outputs=[uml_output], |
| |
| ) |
| |
| call_graph_btn.click( |
| fn=generate_call_graph_only, |
| inputs=[code_input], |
| outputs=[call_graph_output], |
| |
| ) |
|
|
| analyze_btn.click( |
| fn=analyze_code_only, |
| inputs=[code_input], |
| outputs=[analysis_output], |
| |
| ) |
|
|
| all_btn.click( |
| fn=generate_all_outputs, |
| inputs=[code_input], |
| outputs=[uml_output, call_graph_output, analysis_output], |
| |
| ) |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| setup_testing_space() |
|
|
| demo.launch( |
| mcp_server=True, |
| show_api=True, |
| show_error=True, |
| debug=True, |
| share = True, |
| ) |