| | 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, |
| | ) |