copied from private space
Browse files- .gitignore +125 -0
- QUICKSTART.md +119 -0
- README.md +189 -5
- app.py +1012 -0
- examples/complex_class.py +73 -0
- examples/fibonacci_functions.py +88 -0
- examples/inheritance_example.py +48 -0
- examples/main_with_helpers.py +223 -0
- examples/multiple_classes.py +62 -0
- examples/sample_classes.py +62 -0
- examples/sample_functions.py +139 -0
- examples/simple_class.py +21 -0
- examples/string_processing.py +163 -0
- packages.txt +1 -0
- pyproject.toml +23 -0
- requirements.txt +302 -0
- uv.lock +0 -0
.gitignore
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
build/
|
8 |
+
develop-eggs/
|
9 |
+
dist/
|
10 |
+
downloads/
|
11 |
+
eggs/
|
12 |
+
.eggs/
|
13 |
+
lib/
|
14 |
+
lib64/
|
15 |
+
parts/
|
16 |
+
sdist/
|
17 |
+
var/
|
18 |
+
wheels/
|
19 |
+
pip-wheel-metadata/
|
20 |
+
share/python-wheels/
|
21 |
+
*.egg-info/
|
22 |
+
.installed.cfg
|
23 |
+
*.egg
|
24 |
+
MANIFEST
|
25 |
+
|
26 |
+
# Virtual Environment
|
27 |
+
venv/
|
28 |
+
env/
|
29 |
+
ENV/
|
30 |
+
env.bak/
|
31 |
+
venv.bak/
|
32 |
+
|
33 |
+
# IDE
|
34 |
+
.vscode/
|
35 |
+
.idea/
|
36 |
+
*.swp
|
37 |
+
*.swo
|
38 |
+
*~
|
39 |
+
|
40 |
+
# OS
|
41 |
+
.DS_Store
|
42 |
+
.DS_Store?
|
43 |
+
._*
|
44 |
+
.Spotlight-V100
|
45 |
+
.Trashes
|
46 |
+
ehthumbs.db
|
47 |
+
Thumbs.db
|
48 |
+
|
49 |
+
# Gradio
|
50 |
+
.gradio/
|
51 |
+
gradio_cached_examples/
|
52 |
+
|
53 |
+
# Temporary files
|
54 |
+
testing_space/
|
55 |
+
temp_diagrams/
|
56 |
+
*.tmp
|
57 |
+
*.temp
|
58 |
+
temp_*/
|
59 |
+
output/
|
60 |
+
|
61 |
+
# Logs
|
62 |
+
*.log
|
63 |
+
logs/
|
64 |
+
|
65 |
+
# PlantUML output
|
66 |
+
*.puml
|
67 |
+
*.png
|
68 |
+
*.svg
|
69 |
+
diagrams/
|
70 |
+
|
71 |
+
# Environment variables
|
72 |
+
.env
|
73 |
+
.env.local
|
74 |
+
.env.*.local
|
75 |
+
|
76 |
+
# Node modules (for MCP tools)
|
77 |
+
node_modules/
|
78 |
+
package-lock.json
|
79 |
+
|
80 |
+
# Test files
|
81 |
+
test_*.py
|
82 |
+
*_test.py
|
83 |
+
tests/
|
84 |
+
|
85 |
+
# Documentation build
|
86 |
+
docs/_build/
|
87 |
+
|
88 |
+
# Coverage reports
|
89 |
+
htmlcov/
|
90 |
+
.coverage
|
91 |
+
.coverage.*
|
92 |
+
coverage.xml
|
93 |
+
*.cover
|
94 |
+
.hypothesis/
|
95 |
+
.pytest_cache/
|
96 |
+
|
97 |
+
# Jupyter Notebook
|
98 |
+
.ipynb_checkpoints
|
99 |
+
|
100 |
+
# pyenv
|
101 |
+
.python-version
|
102 |
+
|
103 |
+
# Celery
|
104 |
+
celerybeat-schedule
|
105 |
+
celerybeat.pid
|
106 |
+
|
107 |
+
# SageMath parsed files
|
108 |
+
*.sage.py
|
109 |
+
|
110 |
+
# Spyder project settings
|
111 |
+
.spyderproject
|
112 |
+
.spyproject
|
113 |
+
|
114 |
+
# Rope project settings
|
115 |
+
.ropeproject
|
116 |
+
|
117 |
+
# mkdocs documentation
|
118 |
+
/site
|
119 |
+
|
120 |
+
# mypy
|
121 |
+
.mypy_cache/
|
122 |
+
.dmypy.json
|
123 |
+
dmypy.json
|
124 |
+
|
125 |
+
repomix-output.xml
|
QUICKSTART.md
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🚀 Quick Start Guide
|
2 |
+
|
3 |
+
## Getting Started in 3 Steps
|
4 |
+
|
5 |
+
### Step 1: Test Your Setup
|
6 |
+
```bash
|
7 |
+
python test_setup.py
|
8 |
+
```
|
9 |
+
This will verify all dependencies are working correctly.
|
10 |
+
|
11 |
+
### Step 2: Start the Application
|
12 |
+
|
13 |
+
**Option A: Use the batch file (Windows)**
|
14 |
+
```bash
|
15 |
+
run.bat
|
16 |
+
```
|
17 |
+
|
18 |
+
**Option B: Manual start**
|
19 |
+
```bash
|
20 |
+
# Activate virtual environment
|
21 |
+
venv\Scripts\activate # Windows
|
22 |
+
# or
|
23 |
+
source venv/bin/activate # Linux/Mac
|
24 |
+
|
25 |
+
# Run the app
|
26 |
+
python app.py
|
27 |
+
```
|
28 |
+
|
29 |
+
### Step 3: Access the Application
|
30 |
+
- **Web Interface**: http://127.0.0.1:7860
|
31 |
+
- **MCP Endpoint**: http://127.0.0.1:7860/gradio_api/mcp/sse
|
32 |
+
|
33 |
+
## Claude Desktop Integration
|
34 |
+
|
35 |
+
### Prerequisites
|
36 |
+
1. Install Node.js from https://nodejs.org
|
37 |
+
2. Install mcp-remote:
|
38 |
+
```bash
|
39 |
+
npm install -g mcp-remote
|
40 |
+
```
|
41 |
+
|
42 |
+
### Configuration
|
43 |
+
1. **Find your Claude Desktop config file:**
|
44 |
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
45 |
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
46 |
+
|
47 |
+
2. **Copy the contents of `claude_config.json` to your Claude config file**
|
48 |
+
|
49 |
+
3. **Restart Claude Desktop**
|
50 |
+
|
51 |
+
### Testing Claude Integration
|
52 |
+
In Claude Desktop, try these prompts:
|
53 |
+
|
54 |
+
```
|
55 |
+
Generate a UML diagram for this Python code:
|
56 |
+
|
57 |
+
class Vehicle:
|
58 |
+
def __init__(self, brand):
|
59 |
+
self.brand = brand
|
60 |
+
|
61 |
+
def start(self):
|
62 |
+
pass
|
63 |
+
|
64 |
+
class Car(Vehicle):
|
65 |
+
def start(self):
|
66 |
+
return "Engine started"
|
67 |
+
```
|
68 |
+
|
69 |
+
or
|
70 |
+
|
71 |
+
```
|
72 |
+
Analyze the structure of this Python code:
|
73 |
+
|
74 |
+
class Calculator:
|
75 |
+
def add(self, a, b):
|
76 |
+
return a + b
|
77 |
+
|
78 |
+
def multiply(self, a, b):
|
79 |
+
return a * b
|
80 |
+
```
|
81 |
+
|
82 |
+
## Troubleshooting
|
83 |
+
|
84 |
+
### Common Issues
|
85 |
+
|
86 |
+
1. **Import errors when running test_setup.py:**
|
87 |
+
```bash
|
88 |
+
pip install -r requirements.txt
|
89 |
+
```
|
90 |
+
|
91 |
+
2. **Claude Desktop doesn't recognize the MCP server:**
|
92 |
+
- Check Node.js installation: `node --version`
|
93 |
+
- Check mcp-remote installation: `npx mcp-remote --version`
|
94 |
+
- Verify the app is running: visit http://127.0.0.1:7860
|
95 |
+
- Restart Claude Desktop after config changes
|
96 |
+
|
97 |
+
3. **No diagram generated:**
|
98 |
+
- Ensure your Python code contains class definitions
|
99 |
+
- Check for syntax errors in your code
|
100 |
+
- Verify internet connection (PlantUML service)
|
101 |
+
|
102 |
+
### Debug Commands
|
103 |
+
|
104 |
+
```bash
|
105 |
+
# Check if the app is running
|
106 |
+
curl http://127.0.0.1:7860/gradio_api/mcp/schema
|
107 |
+
|
108 |
+
# Test MCP endpoint
|
109 |
+
npx mcp-remote http://127.0.0.1:7860/gradio_api/mcp/sse --transport sse-only
|
110 |
+
```
|
111 |
+
|
112 |
+
## Next Steps
|
113 |
+
|
114 |
+
1. Try the sample code in the web interface
|
115 |
+
2. Experiment with your own Python classes
|
116 |
+
3. Test the MCP integration with Claude Desktop
|
117 |
+
4. Explore the code analysis features
|
118 |
+
|
119 |
+
Happy diagramming! 🎨
|
README.md
CHANGED
@@ -1,14 +1,198 @@
|
|
1 |
---
|
2 |
-
title: Python
|
3 |
-
emoji:
|
4 |
colorFrom: blue
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Python UML Diagram Generator & MCP Server
|
3 |
+
emoji: 🐍
|
4 |
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
11 |
+
models:
|
12 |
+
- PlantUML
|
13 |
+
tags:
|
14 |
+
- uml
|
15 |
+
- python
|
16 |
+
- diagrams
|
17 |
+
- mcp
|
18 |
+
- code-analysis
|
19 |
+
- visualization
|
20 |
+
- mcp-server-track
|
21 |
+
short_description: Python UML diagram generator with MCP server support
|
22 |
---
|
23 |
|
24 |
+
# 🐍 Python UML Diagram Generator & MCP Server
|
25 |
+
|
26 |
+
A powerful tool that generates UML class diagrams from Python code with integrated Model Context Protocol (MCP) server functionality for AI assistants.
|
27 |
+
|
28 |
+
## 🚀 Features
|
29 |
+
|
30 |
+
- **🎨 Interactive Web Interface**: Generate UML class diagrams interactively from Python code
|
31 |
+
- **🤖 MCP Server Integration**: Works with Claude Desktop, Cursor, and other MCP clients
|
32 |
+
- **📊 Advanced Code Analysis**: Enhanced analysis with call graphs, complexity metrics, and function dependencies
|
33 |
+
- **🔗 Function Call Graphs**: Visualize function relationships and dependencies using pyan3 + graphviz
|
34 |
+
- **📈 Complexity Metrics**: Cyclomatic complexity, lines of code, parameter analysis
|
35 |
+
- **⚙️ Standalone Function Analysis**: Perfect for utility scripts with functions (not just classes)
|
36 |
+
- **🔄 Real-time Processing**: Fast diagram generation using PlantUML
|
37 |
+
- **💾 Multiple Formats**: PNG images, PlantUML source code, and call graph visualizations
|
38 |
+
- **🔍 Inheritance Visualization**: Clear parent-child class relationships
|
39 |
+
|
40 |
+
## 🎯 Quick Start
|
41 |
+
|
42 |
+
1. **Paste Python Code**: Enter your Python code in the text area
|
43 |
+
2. **Generate Diagram**: Click "Generate Diagram" to create UML visualization
|
44 |
+
3. **Analyze Structure**: Use "Analyze Code" for detailed code analysis
|
45 |
+
4. **Download Results**: Save generated diagrams and analysis
|
46 |
+
|
47 |
+
## 🤖 MCP Server Integration
|
48 |
+
|
49 |
+
This Space automatically serves as an MCP server! To integrate with AI assistants:
|
50 |
+
|
51 |
+
### For Claude Desktop (with mcp-remote):
|
52 |
+
```json
|
53 |
+
{
|
54 |
+
"mcpServers": {
|
55 |
+
"python-diagram-generator": {
|
56 |
+
"command": "npx",
|
57 |
+
"args": [
|
58 |
+
"mcp-remote",
|
59 |
+
"https://your-username-space-name.hf.space/gradio_api/mcp/sse",
|
60 |
+
"--transport",
|
61 |
+
"sse-only"
|
62 |
+
]
|
63 |
+
}
|
64 |
+
}
|
65 |
+
}
|
66 |
+
```
|
67 |
+
|
68 |
+
### For Cursor, Cline, and other SSE-compatible clients:
|
69 |
+
```json
|
70 |
+
{
|
71 |
+
"mcpServers": {
|
72 |
+
"python-diagram-generator": {
|
73 |
+
"url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse"
|
74 |
+
}
|
75 |
+
}
|
76 |
+
}
|
77 |
+
```
|
78 |
+
|
79 |
+
### For Private Spaces:
|
80 |
+
```json
|
81 |
+
{
|
82 |
+
"mcpServers": {
|
83 |
+
"python-diagram-generator": {
|
84 |
+
"url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse",
|
85 |
+
"headers": {
|
86 |
+
"Authorization": "Bearer hf_your_token_here"
|
87 |
+
}
|
88 |
+
}
|
89 |
+
}
|
90 |
+
}
|
91 |
+
```
|
92 |
+
|
93 |
+
## 🛠️ Available MCP Tools
|
94 |
+
|
95 |
+
1. **generate_diagram**: Creates UML class diagrams from Python code
|
96 |
+
2. **analyze_code_structure**: Provides detailed code structure analysis with enhanced metrics
|
97 |
+
|
98 |
+
## 📊 Advanced Analysis Features
|
99 |
+
|
100 |
+
### **🔬 Advanced Analysis Tab**
|
101 |
+
- **Function Call Graphs**: Visual network diagrams showing function dependencies
|
102 |
+
- **Complexity Analysis**: Cyclomatic complexity scoring for each function
|
103 |
+
- **Function Metrics**: Lines of code, parameter counts, docstring detection
|
104 |
+
- **Call Relationship Mapping**: Which functions call which, and call frequency analysis
|
105 |
+
- **Isolated Function Detection**: Find functions that aren't connected to others
|
106 |
+
- **Code Quality Recommendations**: Suggestions for refactoring and improvements
|
107 |
+
|
108 |
+
### **Enhanced for Standalone Functions**
|
109 |
+
Perfect for analyzing utility scripts, mathematical functions, data processing pipelines, and other function-heavy code that traditional class-based UML tools miss.
|
110 |
+
|
111 |
+
## 💡 Usage Examples
|
112 |
+
|
113 |
+
### Example 1: Basic Classes
|
114 |
+
```python
|
115 |
+
class Animal:
|
116 |
+
def __init__(self, name: str):
|
117 |
+
self.name = name
|
118 |
+
|
119 |
+
def speak(self) -> str:
|
120 |
+
pass
|
121 |
+
|
122 |
+
class Dog(Animal):
|
123 |
+
def speak(self) -> str:
|
124 |
+
return f"{self.name} says Woof!"
|
125 |
+
```
|
126 |
+
|
127 |
+
### Example 2: Complex Inheritance
|
128 |
+
```python
|
129 |
+
class Vehicle:
|
130 |
+
def __init__(self, brand: str, model: str):
|
131 |
+
self.brand = brand
|
132 |
+
self.model = model
|
133 |
+
|
134 |
+
class Car(Vehicle):
|
135 |
+
def __init__(self, brand: str, model: str, doors: int):
|
136 |
+
super().__init__(brand, model)
|
137 |
+
self.doors = doors
|
138 |
+
|
139 |
+
def start_engine(self):
|
140 |
+
return "Engine started"
|
141 |
+
|
142 |
+
class ElectricCar(Car):
|
143 |
+
def __init__(self, brand: str, model: str, doors: int, battery_capacity: float):
|
144 |
+
super().__init__(brand, model, doors)
|
145 |
+
self.battery_capacity = battery_capacity
|
146 |
+
|
147 |
+
def charge(self):
|
148 |
+
return "Charging battery"
|
149 |
+
```
|
150 |
+
|
151 |
+
## 🔧 Technical Details
|
152 |
+
|
153 |
+
- **Built with**: Gradio 5.33+ with MCP support
|
154 |
+
- **Analysis Engine**: Python AST + py2puml + pyan3
|
155 |
+
- **Call Graph Generation**: pyan3 + graphviz for function dependency visualization
|
156 |
+
- **Diagram Rendering**: PlantUML web service + graphviz DOT rendering
|
157 |
+
- **Output Formats**: PNG images, PlantUML source code, DOT call graphs
|
158 |
+
- **MCP Protocol**: Server-Sent Events (SSE) transport
|
159 |
+
- **Function Analysis**: Cyclomatic complexity, parameter analysis, docstring detection
|
160 |
+
|
161 |
+
## 🎨 Features Showcase
|
162 |
+
|
163 |
+
- **Real-time Analysis**: Code structure updates as you type
|
164 |
+
- **Inheritance Detection**: Visualizes parent-child relationships
|
165 |
+
- **Method & Attribute Mapping**: Complete class member analysis
|
166 |
+
- **Function Call Graphs**: Interactive network diagrams of function dependencies
|
167 |
+
- **Complexity Metrics**: Cyclomatic complexity analysis with recommendations
|
168 |
+
- **Standalone Function Support**: Perfect for utility scripts and mathematical functions
|
169 |
+
- **Error Handling**: Syntax error detection and reporting
|
170 |
+
- **Sample Code**: Ready-to-use examples for both classes and functions
|
171 |
+
|
172 |
+
## 📱 Compatible MCP Clients
|
173 |
+
|
174 |
+
- **Claude Desktop** (via mcp-remote)
|
175 |
+
- **Cursor IDE**
|
176 |
+
- **Cline VS Code Extension**
|
177 |
+
- **Continue VS Code Extension**
|
178 |
+
- **Custom MCP implementations**
|
179 |
+
|
180 |
+
## 🌟 Perfect For
|
181 |
+
|
182 |
+
- **Developers**: Understanding code structure and function dependencies quickly
|
183 |
+
- **Code Reviews**: Visualizing class relationships and call patterns
|
184 |
+
- **Documentation**: Generating architectural diagrams and function flow charts
|
185 |
+
- **Education**: Teaching OOP concepts and functional programming patterns
|
186 |
+
- **Utility Script Analysis**: Understanding standalone function files and mathematical algorithms
|
187 |
+
- **Refactoring**: Identifying complex functions and isolated code segments
|
188 |
+
- **AI Assistants**: Enhanced code analysis capabilities with complexity metrics
|
189 |
+
|
190 |
+
## 📞 Support & Feedback
|
191 |
+
|
192 |
+
- Test with the provided sample code first
|
193 |
+
- Check the MCP configuration tab for setup instructions
|
194 |
+
- View API documentation via the "View API" link in the footer
|
195 |
+
|
196 |
+
---
|
197 |
+
|
198 |
+
**Transform your Python code into beautiful UML diagrams! 🎨✨**
|
app.py
ADDED
@@ -0,0 +1,1012 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import sys
|
3 |
+
import os
|
4 |
+
import tempfile
|
5 |
+
import shutil
|
6 |
+
import ast
|
7 |
+
import time
|
8 |
+
import subprocess
|
9 |
+
import re
|
10 |
+
from typing import List, Dict, Optional, Tuple, Any
|
11 |
+
from py2puml.py2puml import py2puml
|
12 |
+
from plantuml import PlantUML
|
13 |
+
import pyan
|
14 |
+
from pathlib import Path
|
15 |
+
|
16 |
+
if os.name == "nt": # nt == Windows
|
17 |
+
graphviz_bin = r"C:\\Program Files\\Graphviz\\bin"
|
18 |
+
if graphviz_bin not in os.environ["PATH"]:
|
19 |
+
os.environ["PATH"] += os.pathsep + graphviz_bin
|
20 |
+
|
21 |
+
|
22 |
+
def setup_testing_space():
|
23 |
+
"""Create persistent testing_space directory and __init__.py at startup."""
|
24 |
+
testing_dir = os.path.join(os.getcwd(), "testing_space")
|
25 |
+
os.makedirs(testing_dir, exist_ok=True)
|
26 |
+
|
27 |
+
init_file = os.path.join(testing_dir, "__init__.py")
|
28 |
+
if not os.path.exists(init_file):
|
29 |
+
with open(init_file, "w", encoding="utf-8") as f:
|
30 |
+
f.write("# Testing space for py2puml analysis\n")
|
31 |
+
print("📁 Created testing_space directory and __init__.py")
|
32 |
+
else:
|
33 |
+
print("🔄 testing_space directory already exists")
|
34 |
+
|
35 |
+
|
36 |
+
def cleanup_testing_space():
|
37 |
+
"""Remove all .py files except __init__.py from testing_space."""
|
38 |
+
testing_dir = os.path.join(os.getcwd(), "testing_space")
|
39 |
+
if not os.path.exists(testing_dir):
|
40 |
+
print("⚠️ testing_space directory not found, creating it...")
|
41 |
+
setup_testing_space()
|
42 |
+
return
|
43 |
+
|
44 |
+
# Clean up any leftover .py files (keep only __init__.py)
|
45 |
+
files_removed = 0
|
46 |
+
for file in os.listdir(testing_dir):
|
47 |
+
if file.endswith(".py") and file != "__init__.py":
|
48 |
+
file_path = os.path.join(testing_dir, file)
|
49 |
+
try:
|
50 |
+
os.remove(file_path)
|
51 |
+
files_removed += 1
|
52 |
+
except Exception as e:
|
53 |
+
print(f"⚠️ Could not remove {file}: {e}")
|
54 |
+
|
55 |
+
if files_removed > 0:
|
56 |
+
print(f"🧹 Cleaned up {files_removed} leftover .py files from testing_space")
|
57 |
+
|
58 |
+
|
59 |
+
def verify_testing_space():
|
60 |
+
"""Verify testing_space contains only __init__.py."""
|
61 |
+
testing_dir = os.path.join(os.getcwd(), "testing_space")
|
62 |
+
if not os.path.exists(testing_dir):
|
63 |
+
return False
|
64 |
+
|
65 |
+
files = os.listdir(testing_dir)
|
66 |
+
expected_files = ["__init__.py"]
|
67 |
+
|
68 |
+
return files == expected_files
|
69 |
+
|
70 |
+
|
71 |
+
def generate_call_graph_with_pyan3(
|
72 |
+
python_code: str, filename: str = "analysis"
|
73 |
+
) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:
|
74 |
+
"""Generate call graph using pyan3 and return DOT content, PNG path, and structured data.
|
75 |
+
|
76 |
+
Args:
|
77 |
+
python_code: The Python code to analyze
|
78 |
+
filename: Base filename for temporary files
|
79 |
+
|
80 |
+
Returns:
|
81 |
+
Tuple of (dot_content, png_path, structured_data)
|
82 |
+
"""
|
83 |
+
if not python_code.strip():
|
84 |
+
return None, None, {}
|
85 |
+
|
86 |
+
# Create unique filename using timestamp
|
87 |
+
timestamp = str(int(time.time() * 1000))
|
88 |
+
unique_filename = f"{filename}_{timestamp}"
|
89 |
+
|
90 |
+
# Paths
|
91 |
+
testing_dir = os.path.join(os.getcwd(), "testing_space")
|
92 |
+
code_file = os.path.join(testing_dir, f"{unique_filename}.py")
|
93 |
+
|
94 |
+
try:
|
95 |
+
# Write Python code to file
|
96 |
+
with open(code_file, "w", encoding="utf-8") as f:
|
97 |
+
f.write(python_code)
|
98 |
+
|
99 |
+
print(f"📊 Generating call graph for: {unique_filename}.py")
|
100 |
+
|
101 |
+
try:
|
102 |
+
|
103 |
+
dot_content = pyan.create_callgraph(
|
104 |
+
filenames=[str(code_file)],
|
105 |
+
format="dot",
|
106 |
+
colored=True,
|
107 |
+
grouped=True,
|
108 |
+
annotated=True,
|
109 |
+
)
|
110 |
+
|
111 |
+
png_path = None
|
112 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
113 |
+
dot_file = os.path.join(temp_dir, f"{unique_filename}.dot")
|
114 |
+
temp_png = os.path.join(temp_dir, f"{unique_filename}.png")
|
115 |
+
|
116 |
+
# Write DOT content to file
|
117 |
+
with open(dot_file, "w", encoding="utf-8") as f:
|
118 |
+
f.write(dot_content)
|
119 |
+
|
120 |
+
# Generate PNG using dot command
|
121 |
+
dot_cmd = ["dot", "-Tpng", dot_file, "-o", temp_png]
|
122 |
+
|
123 |
+
try:
|
124 |
+
subprocess.run(dot_cmd, check=True, timeout=30)
|
125 |
+
|
126 |
+
if os.path.exists(temp_png):
|
127 |
+
# Copy to permanent location
|
128 |
+
permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")
|
129 |
+
os.makedirs(permanent_dir, exist_ok=True)
|
130 |
+
png_path = os.path.join(
|
131 |
+
permanent_dir, f"callgraph_{unique_filename}.png"
|
132 |
+
)
|
133 |
+
shutil.copy2(temp_png, png_path)
|
134 |
+
print(f"🎨 Call graph PNG saved: {os.path.basename(png_path)}")
|
135 |
+
|
136 |
+
except subprocess.SubprocessError as e:
|
137 |
+
print(f"⚠️ Graphviz PNG generation failed: {e}")
|
138 |
+
# Continue without PNG, DOT content is still useful
|
139 |
+
|
140 |
+
# Parse DOT content for structured data
|
141 |
+
structured_data = parse_call_graph_data(dot_content)
|
142 |
+
|
143 |
+
return dot_content, png_path, structured_data
|
144 |
+
|
145 |
+
except subprocess.TimeoutExpired:
|
146 |
+
print("⚠️ pyan3 analysis timed out, trying simplified approach...")
|
147 |
+
return try_fallback_analysis(python_code, unique_filename)
|
148 |
+
except subprocess.SubprocessError as e:
|
149 |
+
print(f"⚠️ pyan3 execution failed: {e}, trying fallback...")
|
150 |
+
return try_fallback_analysis(python_code, unique_filename)
|
151 |
+
|
152 |
+
except Exception as e:
|
153 |
+
print(f"❌ Call graph generation error: {e}")
|
154 |
+
return None, None, {"error": str(e)}
|
155 |
+
|
156 |
+
finally:
|
157 |
+
# Clean up temporary file
|
158 |
+
if os.path.exists(code_file):
|
159 |
+
try:
|
160 |
+
os.remove(code_file)
|
161 |
+
print(f"🧹 Cleaned up analysis file: {unique_filename}.py")
|
162 |
+
except Exception as e:
|
163 |
+
print(f"⚠️ Could not remove analysis file: {e}")
|
164 |
+
|
165 |
+
|
166 |
+
def parse_call_graph_data(dot_content: str) -> Dict[str, Any]:
|
167 |
+
"""Parse pyan3 DOT output into structured function call data.
|
168 |
+
|
169 |
+
Args:
|
170 |
+
dot_content: DOT format string from pyan3
|
171 |
+
|
172 |
+
Returns:
|
173 |
+
Dictionary with parsed call graph information
|
174 |
+
"""
|
175 |
+
if not dot_content:
|
176 |
+
return {}
|
177 |
+
|
178 |
+
try:
|
179 |
+
# Extract nodes (functions/classes)
|
180 |
+
node_pattern = r'"([^"]+)"\s*\['
|
181 |
+
nodes = re.findall(node_pattern, dot_content)
|
182 |
+
|
183 |
+
# Extract edges (function calls)
|
184 |
+
edge_pattern = r'"([^"]+)"\s*->\s*"([^"]+)"'
|
185 |
+
edges = re.findall(edge_pattern, dot_content)
|
186 |
+
|
187 |
+
# Build function call mapping
|
188 |
+
call_graph = {}
|
189 |
+
called_by = {}
|
190 |
+
|
191 |
+
for caller, callee in edges:
|
192 |
+
if caller not in call_graph:
|
193 |
+
call_graph[caller] = []
|
194 |
+
call_graph[caller].append(callee)
|
195 |
+
|
196 |
+
if callee not in called_by:
|
197 |
+
called_by[callee] = []
|
198 |
+
called_by[callee].append(caller)
|
199 |
+
|
200 |
+
# Calculate metrics
|
201 |
+
function_metrics = {}
|
202 |
+
for node in nodes:
|
203 |
+
out_degree = len(call_graph.get(node, []))
|
204 |
+
in_degree = len(called_by.get(node, []))
|
205 |
+
|
206 |
+
function_metrics[node] = {
|
207 |
+
"calls_made": out_degree,
|
208 |
+
"called_by_count": in_degree,
|
209 |
+
"calls_to": call_graph.get(node, []),
|
210 |
+
"called_by": called_by.get(node, []),
|
211 |
+
}
|
212 |
+
|
213 |
+
return {
|
214 |
+
"nodes": nodes,
|
215 |
+
"edges": edges,
|
216 |
+
"total_functions": len(nodes),
|
217 |
+
"total_calls": len(edges),
|
218 |
+
"call_graph": call_graph,
|
219 |
+
"function_metrics": function_metrics,
|
220 |
+
}
|
221 |
+
|
222 |
+
except Exception as e:
|
223 |
+
return {"parse_error": str(e)}
|
224 |
+
|
225 |
+
|
226 |
+
def try_fallback_analysis(
|
227 |
+
python_code: str, unique_filename: str
|
228 |
+
) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:
|
229 |
+
"""Fallback analysis when pyan3 fails - basic function call detection.
|
230 |
+
|
231 |
+
Args:
|
232 |
+
python_code: The Python code to analyze
|
233 |
+
unique_filename: Unique filename for this analysis
|
234 |
+
|
235 |
+
Returns:
|
236 |
+
Tuple of (None, None, fallback_analysis_data)
|
237 |
+
"""
|
238 |
+
print("🔄 Using fallback analysis approach...")
|
239 |
+
|
240 |
+
try:
|
241 |
+
import ast
|
242 |
+
import re
|
243 |
+
|
244 |
+
tree = ast.parse(python_code)
|
245 |
+
functions = []
|
246 |
+
calls = []
|
247 |
+
|
248 |
+
# Extract function definitions
|
249 |
+
for node in ast.walk(tree):
|
250 |
+
if isinstance(node, ast.FunctionDef):
|
251 |
+
functions.append(node.name)
|
252 |
+
|
253 |
+
# Simple regex-based call detection (fallback approach)
|
254 |
+
for func in functions:
|
255 |
+
# Look for calls to this function
|
256 |
+
pattern = rf"\b{re.escape(func)}\s*\("
|
257 |
+
if re.search(pattern, python_code):
|
258 |
+
calls.append(("unknown", func))
|
259 |
+
|
260 |
+
return (
|
261 |
+
None,
|
262 |
+
None,
|
263 |
+
{
|
264 |
+
"fallback": True,
|
265 |
+
"functions_detected": functions,
|
266 |
+
"total_functions": len(functions),
|
267 |
+
"total_calls": len(calls),
|
268 |
+
"info": f"Fallback analysis: detected {len(functions)} functions",
|
269 |
+
"function_metrics": {
|
270 |
+
func: {
|
271 |
+
"calls_made": 0,
|
272 |
+
"called_by_count": 0,
|
273 |
+
"calls_to": [],
|
274 |
+
"called_by": [],
|
275 |
+
}
|
276 |
+
for func in functions
|
277 |
+
},
|
278 |
+
},
|
279 |
+
)
|
280 |
+
|
281 |
+
except Exception as e:
|
282 |
+
return None, None, {"error": f"Fallback analysis also failed: {str(e)}"}
|
283 |
+
|
284 |
+
|
285 |
+
def analyze_function_complexity(python_code: str) -> Dict[str, Any]:
|
286 |
+
"""Analyze function complexity using AST.
|
287 |
+
|
288 |
+
Args:
|
289 |
+
python_code: The Python code to analyze
|
290 |
+
|
291 |
+
Returns:
|
292 |
+
Dictionary with function complexity metrics
|
293 |
+
"""
|
294 |
+
if not python_code.strip():
|
295 |
+
return {}
|
296 |
+
|
297 |
+
try:
|
298 |
+
tree = ast.parse(python_code)
|
299 |
+
function_analysis = {}
|
300 |
+
|
301 |
+
for node in ast.walk(tree):
|
302 |
+
if isinstance(node, ast.FunctionDef):
|
303 |
+
# Calculate cyclomatic complexity (simplified)
|
304 |
+
complexity = 1 # Base complexity
|
305 |
+
|
306 |
+
for child in ast.walk(node):
|
307 |
+
if isinstance(
|
308 |
+
child,
|
309 |
+
(
|
310 |
+
ast.If,
|
311 |
+
ast.While,
|
312 |
+
ast.For,
|
313 |
+
ast.Try,
|
314 |
+
ast.ExceptHandler,
|
315 |
+
ast.With,
|
316 |
+
ast.Assert,
|
317 |
+
),
|
318 |
+
):
|
319 |
+
complexity += 1
|
320 |
+
elif isinstance(child, ast.BoolOp):
|
321 |
+
complexity += len(child.values) - 1
|
322 |
+
|
323 |
+
# Count lines of code
|
324 |
+
lines = (
|
325 |
+
node.end_lineno - node.lineno + 1
|
326 |
+
if hasattr(node, "end_lineno")
|
327 |
+
else 0
|
328 |
+
)
|
329 |
+
|
330 |
+
# Extract parameters
|
331 |
+
params = [arg.arg for arg in node.args.args]
|
332 |
+
|
333 |
+
# Check for docstring
|
334 |
+
has_docstring = (
|
335 |
+
len(node.body) > 0
|
336 |
+
and isinstance(node.body[0], ast.Expr)
|
337 |
+
and isinstance(node.body[0].value, ast.Constant)
|
338 |
+
and isinstance(node.body[0].value.value, str)
|
339 |
+
)
|
340 |
+
|
341 |
+
function_analysis[node.name] = {
|
342 |
+
"complexity": complexity,
|
343 |
+
"lines_of_code": lines,
|
344 |
+
"parameter_count": len(params),
|
345 |
+
"parameters": params,
|
346 |
+
"has_docstring": has_docstring,
|
347 |
+
"line_start": node.lineno,
|
348 |
+
"line_end": getattr(node, "end_lineno", node.lineno),
|
349 |
+
}
|
350 |
+
|
351 |
+
return function_analysis
|
352 |
+
|
353 |
+
except Exception as e:
|
354 |
+
return {"error": str(e)}
|
355 |
+
|
356 |
+
|
357 |
+
def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[str]:
|
358 |
+
"""Generate a UML class diagram from Python code.
|
359 |
+
|
360 |
+
Args:
|
361 |
+
python_code: The Python code to analyze and convert to UML
|
362 |
+
filename: Optional name for the generated diagram file
|
363 |
+
|
364 |
+
Returns:
|
365 |
+
Path to the generated PNG diagram image or None if failed
|
366 |
+
"""
|
367 |
+
if not python_code.strip():
|
368 |
+
return None
|
369 |
+
|
370 |
+
print(f"🔄 Processing code for diagram generation...")
|
371 |
+
|
372 |
+
# Clean testing space (ensure only __init__.py exists)
|
373 |
+
cleanup_testing_space()
|
374 |
+
|
375 |
+
# Verify clean state
|
376 |
+
if not verify_testing_space():
|
377 |
+
print("⚠️ testing_space verification failed, recreating...")
|
378 |
+
setup_testing_space()
|
379 |
+
cleanup_testing_space()
|
380 |
+
|
381 |
+
# Create unique filename using timestamp
|
382 |
+
timestamp = str(int(time.time() * 1000)) # millisecond timestamp
|
383 |
+
unique_filename = f"{filename}_{timestamp}"
|
384 |
+
|
385 |
+
# Paths
|
386 |
+
testing_dir = os.path.join(os.getcwd(), "testing_space")
|
387 |
+
code_file = os.path.join(testing_dir, f"{unique_filename}.py")
|
388 |
+
|
389 |
+
# Use PlantUML web service for rendering
|
390 |
+
server = PlantUML(url="http://www.plantuml.com/plantuml/img/")
|
391 |
+
|
392 |
+
try:
|
393 |
+
# Write Python code to file in testing_space
|
394 |
+
with open(code_file, "w", encoding="utf-8") as f:
|
395 |
+
f.write(python_code)
|
396 |
+
|
397 |
+
print(f"📝 Created temporary file: testing_space/{unique_filename}.py")
|
398 |
+
|
399 |
+
# Generate PlantUML content using py2puml (no sys.path manipulation needed)
|
400 |
+
print(f"📝 Generating PlantUML content...")
|
401 |
+
puml_content_lines = py2puml(
|
402 |
+
os.path.join(
|
403 |
+
testing_dir, unique_filename
|
404 |
+
), # path to the .py file (without extension)
|
405 |
+
f"testing_space.{unique_filename}", # module name
|
406 |
+
)
|
407 |
+
puml_content = "".join(puml_content_lines)
|
408 |
+
|
409 |
+
if not puml_content.strip():
|
410 |
+
print("⚠️ No UML content generated - check if your code contains classes")
|
411 |
+
return None
|
412 |
+
|
413 |
+
# Create temporary directory for PlantUML processing
|
414 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
415 |
+
# Save PUML file
|
416 |
+
puml_file = os.path.join(temp_dir, f"{unique_filename}.puml")
|
417 |
+
with open(puml_file, "w", encoding="utf-8") as f:
|
418 |
+
f.write(puml_content)
|
419 |
+
|
420 |
+
print(f"🎨 Rendering diagram...")
|
421 |
+
# Generate PNG
|
422 |
+
output_png = os.path.join(temp_dir, f"{unique_filename}.png")
|
423 |
+
server.processes_file(puml_file, outfile=output_png)
|
424 |
+
|
425 |
+
if os.path.exists(output_png):
|
426 |
+
print("✅ Diagram generated successfully!")
|
427 |
+
# Copy to a permanent location for Gradio to serve
|
428 |
+
permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")
|
429 |
+
os.makedirs(permanent_dir, exist_ok=True)
|
430 |
+
permanent_path = os.path.join(
|
431 |
+
permanent_dir, f"{filename}_{hash(python_code) % 10000}.png"
|
432 |
+
)
|
433 |
+
shutil.copy2(output_png, permanent_path)
|
434 |
+
return permanent_path
|
435 |
+
else:
|
436 |
+
print("❌ Failed to generate PNG")
|
437 |
+
return None
|
438 |
+
|
439 |
+
except Exception as e:
|
440 |
+
print(f"❌ Error: {e}")
|
441 |
+
return None
|
442 |
+
|
443 |
+
finally:
|
444 |
+
# Always clean up the temporary .py file
|
445 |
+
if os.path.exists(code_file):
|
446 |
+
try:
|
447 |
+
os.remove(code_file)
|
448 |
+
print(f"🧹 Cleaned up temporary file: {unique_filename}.py")
|
449 |
+
except Exception as e:
|
450 |
+
print(f"⚠️ Could not remove temporary file: {e}")
|
451 |
+
|
452 |
+
|
453 |
+
def analyze_code_structure(python_code: str) -> str:
|
454 |
+
"""Enhanced code analysis combining AST + pyan3 call graphs.
|
455 |
+
|
456 |
+
Args:
|
457 |
+
python_code: The Python code to analyze
|
458 |
+
|
459 |
+
Returns:
|
460 |
+
Comprehensive analysis report in markdown format
|
461 |
+
"""
|
462 |
+
if not python_code.strip():
|
463 |
+
return "No code provided for analysis."
|
464 |
+
|
465 |
+
try:
|
466 |
+
# Basic AST analysis
|
467 |
+
tree = ast.parse(python_code)
|
468 |
+
classes = []
|
469 |
+
functions = []
|
470 |
+
imports = []
|
471 |
+
|
472 |
+
for node in ast.walk(tree):
|
473 |
+
if isinstance(node, ast.ClassDef):
|
474 |
+
methods = []
|
475 |
+
attributes = []
|
476 |
+
|
477 |
+
for item in node.body:
|
478 |
+
if isinstance(item, ast.FunctionDef):
|
479 |
+
methods.append(item.name)
|
480 |
+
elif isinstance(item, ast.Assign):
|
481 |
+
for target in item.targets:
|
482 |
+
if isinstance(target, ast.Name):
|
483 |
+
attributes.append(target.id)
|
484 |
+
|
485 |
+
# Check for inheritance
|
486 |
+
parents = [base.id for base in node.bases if isinstance(base, ast.Name)]
|
487 |
+
|
488 |
+
classes.append(
|
489 |
+
{
|
490 |
+
"name": node.name,
|
491 |
+
"methods": methods,
|
492 |
+
"attributes": attributes,
|
493 |
+
"parents": parents,
|
494 |
+
}
|
495 |
+
)
|
496 |
+
|
497 |
+
elif isinstance(node, ast.FunctionDef):
|
498 |
+
# Check if it's a top-level function (not inside a class)
|
499 |
+
is_method = any(
|
500 |
+
isinstance(parent, ast.ClassDef)
|
501 |
+
for parent in ast.walk(tree)
|
502 |
+
if hasattr(parent, "body") and node in getattr(parent, "body", [])
|
503 |
+
)
|
504 |
+
if not is_method:
|
505 |
+
functions.append(node.name)
|
506 |
+
|
507 |
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
508 |
+
if isinstance(node, ast.Import):
|
509 |
+
for alias in node.names:
|
510 |
+
imports.append(alias.name)
|
511 |
+
else:
|
512 |
+
module = node.module or ""
|
513 |
+
for alias in node.names:
|
514 |
+
imports.append(
|
515 |
+
f"{module}.{alias.name}" if module else alias.name
|
516 |
+
)
|
517 |
+
|
518 |
+
# Enhanced function complexity analysis
|
519 |
+
function_complexity = analyze_function_complexity(python_code)
|
520 |
+
|
521 |
+
# Call graph analysis (for files with functions)
|
522 |
+
call_graph_data = {}
|
523 |
+
if functions or any(classes): # Only run if there are functions to analyze
|
524 |
+
try:
|
525 |
+
cleanup_testing_space() # Ensure clean state
|
526 |
+
dot_content, png_path, call_graph_data = generate_call_graph_with_pyan3(
|
527 |
+
python_code
|
528 |
+
)
|
529 |
+
except Exception as e:
|
530 |
+
print(f"⚠️ Call graph analysis failed: {e}")
|
531 |
+
call_graph_data = {"error": str(e)}
|
532 |
+
|
533 |
+
# Build comprehensive summary
|
534 |
+
summary = "📊 **Enhanced Code Analysis Results**\n\n"
|
535 |
+
|
536 |
+
# === OVERVIEW SECTION ===
|
537 |
+
summary += "## 📋 **Overview**\n"
|
538 |
+
summary += f"• **{len(classes)}** classes found\n"
|
539 |
+
summary += f"• **{len(functions)}** standalone functions found\n"
|
540 |
+
summary += f"• **{len(set(imports))}** unique imports\n"
|
541 |
+
|
542 |
+
if call_graph_data and "total_functions" in call_graph_data:
|
543 |
+
summary += f"• **{call_graph_data['total_functions']}** total functions/methods in call graph\n"
|
544 |
+
summary += (
|
545 |
+
f"• **{call_graph_data['total_calls']}** function calls detected\n"
|
546 |
+
)
|
547 |
+
|
548 |
+
summary += "\n"
|
549 |
+
|
550 |
+
# === CLASSES SECTION ===
|
551 |
+
if classes:
|
552 |
+
summary += "## 🏗️ **Classes**\n"
|
553 |
+
for cls in classes:
|
554 |
+
summary += f"### **{cls['name']}**\n"
|
555 |
+
if cls["parents"]:
|
556 |
+
summary += f" - **Inherits from**: {', '.join(cls['parents'])}\n"
|
557 |
+
summary += f" - **Methods**: {len(cls['methods'])}"
|
558 |
+
if cls["methods"]:
|
559 |
+
summary += f" ({', '.join(cls['methods'])})"
|
560 |
+
summary += "\n"
|
561 |
+
if cls["attributes"]:
|
562 |
+
summary += f" - **Attributes**: {', '.join(cls['attributes'])}\n"
|
563 |
+
summary += "\n"
|
564 |
+
|
565 |
+
# === STANDALONE FUNCTIONS SECTION ===
|
566 |
+
if functions:
|
567 |
+
summary += "## ⚙️ **Standalone Functions**\n"
|
568 |
+
for func in functions:
|
569 |
+
summary += f"### **{func}()**\n"
|
570 |
+
|
571 |
+
# Add complexity metrics if available
|
572 |
+
if func in function_complexity:
|
573 |
+
metrics = function_complexity[func]
|
574 |
+
summary += (
|
575 |
+
f" - **Complexity**: {metrics['complexity']} (cyclomatic)\n"
|
576 |
+
)
|
577 |
+
summary += f" - **Lines of Code**: {metrics['lines_of_code']}\n"
|
578 |
+
summary += f" - **Parameters**: {metrics['parameter_count']}"
|
579 |
+
if metrics["parameters"]:
|
580 |
+
summary += f" ({', '.join(metrics['parameters'])})"
|
581 |
+
summary += "\n"
|
582 |
+
summary += f" - **Has Docstring**: {'✅' if metrics['has_docstring'] else '❌'}\n"
|
583 |
+
summary += f" - **Lines**: {metrics['line_start']}-{metrics['line_end']}\n"
|
584 |
+
|
585 |
+
# Add call graph info if available
|
586 |
+
if call_graph_data and "function_metrics" in call_graph_data:
|
587 |
+
if func in call_graph_data["function_metrics"]:
|
588 |
+
call_metrics = call_graph_data["function_metrics"][func]
|
589 |
+
summary += f" - **Calls Made**: {call_metrics['calls_made']}\n"
|
590 |
+
if call_metrics["calls_to"]:
|
591 |
+
summary += (
|
592 |
+
f" - Calls: {', '.join(call_metrics['calls_to'])}\n"
|
593 |
+
)
|
594 |
+
summary += f" - **Called By**: {call_metrics['called_by_count']} functions\n"
|
595 |
+
if call_metrics["called_by"]:
|
596 |
+
summary += f" - Called by: {', '.join(call_metrics['called_by'])}\n"
|
597 |
+
|
598 |
+
summary += "\n"
|
599 |
+
|
600 |
+
# === CALL GRAPH ANALYSIS ===
|
601 |
+
if (
|
602 |
+
call_graph_data
|
603 |
+
and "function_metrics" in call_graph_data
|
604 |
+
and call_graph_data["total_calls"] > 0
|
605 |
+
):
|
606 |
+
summary += "## 🔗 **Function Call Analysis**\n"
|
607 |
+
|
608 |
+
# Most called functions
|
609 |
+
sorted_by_calls = sorted(
|
610 |
+
call_graph_data["function_metrics"].items(),
|
611 |
+
key=lambda x: x[1]["called_by_count"],
|
612 |
+
reverse=True,
|
613 |
+
)[:5]
|
614 |
+
|
615 |
+
if sorted_by_calls and sorted_by_calls[0][1]["called_by_count"] > 0:
|
616 |
+
summary += "**Most Called Functions:**\n"
|
617 |
+
for func_name, metrics in sorted_by_calls:
|
618 |
+
if metrics["called_by_count"] > 0:
|
619 |
+
summary += f"• **{func_name}**: called {metrics['called_by_count']} times\n"
|
620 |
+
summary += "\n"
|
621 |
+
|
622 |
+
# Most complex functions (by calls made)
|
623 |
+
sorted_by_complexity = sorted(
|
624 |
+
call_graph_data["function_metrics"].items(),
|
625 |
+
key=lambda x: x[1]["calls_made"],
|
626 |
+
reverse=True,
|
627 |
+
)[:5]
|
628 |
+
|
629 |
+
if sorted_by_complexity and sorted_by_complexity[0][1]["calls_made"] > 0:
|
630 |
+
summary += "**Functions Making Most Calls:**\n"
|
631 |
+
for func_name, metrics in sorted_by_complexity:
|
632 |
+
if metrics["calls_made"] > 0:
|
633 |
+
summary += (
|
634 |
+
f"• **{func_name}**: makes {metrics['calls_made']} calls\n"
|
635 |
+
)
|
636 |
+
summary += "\n"
|
637 |
+
|
638 |
+
# === COMPLEXITY ANALYSIS ===
|
639 |
+
if function_complexity:
|
640 |
+
summary += "## 📈 **Complexity Analysis**\n"
|
641 |
+
|
642 |
+
# Sort by complexity
|
643 |
+
sorted_complexity = sorted(
|
644 |
+
function_complexity.items(),
|
645 |
+
key=lambda x: x[1]["complexity"],
|
646 |
+
reverse=True,
|
647 |
+
)[:5]
|
648 |
+
|
649 |
+
summary += "**Most Complex Functions:**\n"
|
650 |
+
for func_name, metrics in sorted_complexity:
|
651 |
+
summary += f"• **{func_name}**: complexity {metrics['complexity']}, {metrics['lines_of_code']} lines\n"
|
652 |
+
|
653 |
+
# Overall stats
|
654 |
+
total_functions = len(function_complexity)
|
655 |
+
avg_complexity = (
|
656 |
+
sum(m["complexity"] for m in function_complexity.values())
|
657 |
+
/ total_functions
|
658 |
+
)
|
659 |
+
avg_lines = (
|
660 |
+
sum(m["lines_of_code"] for m in function_complexity.values())
|
661 |
+
/ total_functions
|
662 |
+
)
|
663 |
+
functions_with_docs = sum(
|
664 |
+
1 for m in function_complexity.values() if m["has_docstring"]
|
665 |
+
)
|
666 |
+
|
667 |
+
summary += "\n**Overall Function Metrics:**\n"
|
668 |
+
summary += f"• **Average Complexity**: {avg_complexity:.1f}\n"
|
669 |
+
summary += f"• **Average Lines per Function**: {avg_lines:.1f}\n"
|
670 |
+
summary += f"• **Functions with Docstrings**: {functions_with_docs}/{total_functions} ({100*functions_with_docs/total_functions:.1f}%)\n"
|
671 |
+
summary += "\n"
|
672 |
+
|
673 |
+
# === IMPORTS SECTION ===
|
674 |
+
if imports:
|
675 |
+
summary += "## 📦 **Imports**\n"
|
676 |
+
unique_imports = list(set(imports))
|
677 |
+
for imp in unique_imports[:10]: # Show first 10 imports
|
678 |
+
summary += f"• {imp}\n"
|
679 |
+
if len(unique_imports) > 10:
|
680 |
+
summary += f"• ... and {len(unique_imports) - 10} more\n"
|
681 |
+
summary += "\n"
|
682 |
+
|
683 |
+
# === CALL GRAPH ERROR/INFO ===
|
684 |
+
if call_graph_data and "error" in call_graph_data:
|
685 |
+
summary += "## ⚠️ **Call Graph Analysis**\n"
|
686 |
+
summary += f"Call graph generation failed: {call_graph_data['error']}\n\n"
|
687 |
+
elif call_graph_data and "info" in call_graph_data:
|
688 |
+
summary += "## 📊 **Call Graph Analysis**\n"
|
689 |
+
summary += f"{call_graph_data['info']}\n\n"
|
690 |
+
|
691 |
+
# === RECOMMENDATIONS ===
|
692 |
+
summary += "## 💡 **Recommendations**\n"
|
693 |
+
if function_complexity:
|
694 |
+
high_complexity = [
|
695 |
+
f for f, m in function_complexity.items() if m["complexity"] > 10
|
696 |
+
]
|
697 |
+
if high_complexity:
|
698 |
+
summary += f"• Consider refactoring high-complexity functions: {', '.join(high_complexity)}\n"
|
699 |
+
|
700 |
+
no_docs = [
|
701 |
+
f for f, m in function_complexity.items() if not m["has_docstring"]
|
702 |
+
]
|
703 |
+
if no_docs:
|
704 |
+
summary += f"• Add docstrings to: {', '.join(no_docs[:5])}{'...' if len(no_docs) > 5 else ''}\n"
|
705 |
+
|
706 |
+
if call_graph_data and "function_metrics" in call_graph_data:
|
707 |
+
isolated_functions = [
|
708 |
+
f
|
709 |
+
for f, m in call_graph_data["function_metrics"].items()
|
710 |
+
if m["calls_made"] == 0 and m["called_by_count"] == 0
|
711 |
+
]
|
712 |
+
if isolated_functions:
|
713 |
+
summary += f"• Review isolated functions: {', '.join(isolated_functions[:3])}{'...' if len(isolated_functions) > 3 else ''}\n"
|
714 |
+
|
715 |
+
return summary
|
716 |
+
|
717 |
+
except SyntaxError as e:
|
718 |
+
return f"❌ **Syntax Error in Python code:**\n```\n{str(e)}\n```"
|
719 |
+
except Exception as e:
|
720 |
+
return f"❌ **Error analyzing code:**\n```\n{str(e)}\n```"
|
721 |
+
|
722 |
+
|
723 |
+
def list_example_files() -> list:
|
724 |
+
"""List all example .py files in the examples/ directory."""
|
725 |
+
examples_dir = os.path.join(os.getcwd(), "examples")
|
726 |
+
if not os.path.exists(examples_dir):
|
727 |
+
return []
|
728 |
+
return [f for f in os.listdir(examples_dir) if f.endswith(".py")]
|
729 |
+
|
730 |
+
|
731 |
+
def get_sample_code(filename: str) -> str:
|
732 |
+
"""Return sample Python code from examples/ directory."""
|
733 |
+
examples_dir = os.path.join(os.getcwd(), "examples")
|
734 |
+
file_path = os.path.join(examples_dir, filename)
|
735 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
736 |
+
return f.read()
|
737 |
+
|
738 |
+
|
739 |
+
def generate_all_diagrams(python_code: str, filename: str = "diagram") -> Tuple[Optional[str], Optional[str], str]:
|
740 |
+
"""Generate all diagrams and analysis at once.
|
741 |
+
|
742 |
+
Args:
|
743 |
+
python_code: The Python code to analyze
|
744 |
+
filename: Base filename for diagrams
|
745 |
+
|
746 |
+
Returns:
|
747 |
+
Tuple of (uml_diagram_path, call_graph_path, analysis_text)
|
748 |
+
"""
|
749 |
+
if not python_code.strip():
|
750 |
+
return None, None, "No code provided for analysis."
|
751 |
+
|
752 |
+
print("🚀 Starting comprehensive diagram generation...")
|
753 |
+
|
754 |
+
# Step 1: Generate UML Class Diagram
|
755 |
+
print("📊 Step 1/3: Generating UML class diagram...")
|
756 |
+
uml_diagram_path = generate_diagram(python_code, filename)
|
757 |
+
|
758 |
+
# Step 2: Generate Call Graph
|
759 |
+
print("🔗 Step 2/3: Generating call graph...")
|
760 |
+
try:
|
761 |
+
cleanup_testing_space()
|
762 |
+
dot_content, call_graph_path, structured_data = generate_call_graph_with_pyan3(python_code)
|
763 |
+
except Exception as e:
|
764 |
+
print(f"⚠️ Call graph generation failed: {e}")
|
765 |
+
call_graph_path = None
|
766 |
+
|
767 |
+
# Step 3: Generate Analysis
|
768 |
+
print("📈 Step 3/3: Performing code analysis...")
|
769 |
+
analysis_text = analyze_code_structure(python_code)
|
770 |
+
|
771 |
+
print("✅ All diagrams and analysis completed!")
|
772 |
+
|
773 |
+
return uml_diagram_path, call_graph_path, analysis_text
|
774 |
+
|
775 |
+
|
776 |
+
# Create Gradio interface
|
777 |
+
with gr.Blocks(
|
778 |
+
title="Python UML Diagram Generator & MCP Server",
|
779 |
+
theme=gr.themes.Soft(),
|
780 |
+
css="""
|
781 |
+
.gradio-container {
|
782 |
+
max-width: 1400px !important;
|
783 |
+
}
|
784 |
+
.code-input {
|
785 |
+
font-family: 'Courier New', monospace !important;
|
786 |
+
}
|
787 |
+
""",
|
788 |
+
) as demo:
|
789 |
+
# Header
|
790 |
+
gr.Markdown(
|
791 |
+
"""
|
792 |
+
# 🐍 Python UML Diagram Generator & MCP Server
|
793 |
+
|
794 |
+
**Dual Functionality:**
|
795 |
+
- 🖥️ **Web Interface**: Generate UML class diagrams and call graphs from Python code
|
796 |
+
- 🤖 **MCP Server**: Provides tools for AI assistants (Claude Desktop, Cursor, etc.)
|
797 |
+
|
798 |
+
Transform your Python code into comprehensive visual diagrams and analysis!
|
799 |
+
"""
|
800 |
+
)
|
801 |
+
|
802 |
+
with gr.Tab("🎨 Diagram Generator"):
|
803 |
+
with gr.Row():
|
804 |
+
with gr.Column(scale=1):
|
805 |
+
gr.Markdown("### Input")
|
806 |
+
|
807 |
+
example_files = list_example_files()
|
808 |
+
example_dropdown = gr.Dropdown(
|
809 |
+
label="Choose Example",
|
810 |
+
choices=example_files,
|
811 |
+
value=example_files[0] if example_files else None,
|
812 |
+
)
|
813 |
+
|
814 |
+
code_input = gr.Textbox(
|
815 |
+
label="Python Code",
|
816 |
+
placeholder="Paste your Python code here...",
|
817 |
+
lines=20,
|
818 |
+
max_lines=35,
|
819 |
+
value=get_sample_code(example_files[0]) if example_files else "",
|
820 |
+
elem_classes=["code-input"],
|
821 |
+
)
|
822 |
+
|
823 |
+
with gr.Row():
|
824 |
+
filename_input = gr.Textbox(
|
825 |
+
label="Diagram Name",
|
826 |
+
value="my_diagram",
|
827 |
+
placeholder="Enter a name for your diagram",
|
828 |
+
scale=2,
|
829 |
+
)
|
830 |
+
|
831 |
+
with gr.Row():
|
832 |
+
generate_diagrams_btn = gr.Button(
|
833 |
+
"🔄 Generate Diagrams", variant="primary", size="lg"
|
834 |
+
)
|
835 |
+
|
836 |
+
with gr.Column(scale=1):
|
837 |
+
gr.Markdown("### Generated UML Class Diagram")
|
838 |
+
|
839 |
+
uml_diagram_output = gr.Image(
|
840 |
+
label="UML Class Diagram",
|
841 |
+
show_download_button=True,
|
842 |
+
height=300,
|
843 |
+
)
|
844 |
+
|
845 |
+
gr.Markdown("### Generated Call Graph Diagram")
|
846 |
+
|
847 |
+
call_graph_output = gr.Image(
|
848 |
+
label="Function Call Graph",
|
849 |
+
show_download_button=True,
|
850 |
+
height=300,
|
851 |
+
)
|
852 |
+
|
853 |
+
with gr.Row():
|
854 |
+
gr.Markdown("### Code Analysis")
|
855 |
+
|
856 |
+
with gr.Row():
|
857 |
+
analysis_output = gr.Textbox(
|
858 |
+
label="Comprehensive Code Analysis",
|
859 |
+
lines=15,
|
860 |
+
max_lines=25,
|
861 |
+
interactive=False,
|
862 |
+
show_copy_button=True,
|
863 |
+
)
|
864 |
+
|
865 |
+
with gr.Tab("ℹ️ About & Help"):
|
866 |
+
gr.Markdown(
|
867 |
+
"""
|
868 |
+
## About This Tool
|
869 |
+
|
870 |
+
This Python UML Diagram Generator helps you visualize the structure of your Python code by creating comprehensive diagrams and analysis.
|
871 |
+
### Inspiration:
|
872 |
+
The idea for this mcp server was inspired by a tweet made by karpathy [tweet](https://x.com/karpathy/status/1930305209747812559).
|
873 |
+
He makes the point that generated images are easy to discriminate by humans while going through a 300 line LLM generated code is time consuming.
|
874 |
+
This tool aims to provide a visual quick smell test for generated code so that user can quickly identify issues instead of going through the code line by line.
|
875 |
+
This is only a very rough and basic implementation of the idea.
|
876 |
+
Making compound AI systems instead of text-to-text chatbots is the necessary direction.
|
877 |
+
|
878 |
+
### ✨ Features:
|
879 |
+
- **UML Class Diagrams**: Automatically identifies classes, methods, attributes, and inheritance
|
880 |
+
- **Call Graph Diagrams**: Visualizes function dependencies and call relationships
|
881 |
+
- **Code Analysis**: Provides detailed structure analysis with complexity metrics
|
882 |
+
- **MCP Integration**: Works with AI assistants via Model Context Protocol
|
883 |
+
|
884 |
+
### 📚 How to Use:
|
885 |
+
1. **Paste Code**: Enter your Python code in the text area
|
886 |
+
2. **Set Name**: Choose a name for your diagrams (optional)
|
887 |
+
3. **Generate**: Click "Generate Diagrams" to create all visualizations and analysis
|
888 |
+
4. **Download**: Save the generated diagram images
|
889 |
+
5. **Review**: Read the comprehensive code analysis
|
890 |
+
|
891 |
+
### 🔧 Technical Details:
|
892 |
+
- Built with **Gradio** for the web interface
|
893 |
+
- Uses **py2puml** for Python-to-PlantUML conversion
|
894 |
+
- **PlantUML** for UML diagram rendering
|
895 |
+
- **pyan3** and **Graphviz** for call graph generation
|
896 |
+
- **AST** (Abstract Syntax Tree) for code analysis
|
897 |
+
|
898 |
+
### 💡 Tips:
|
899 |
+
- Include type hints for better diagram quality
|
900 |
+
- Use meaningful class and method names
|
901 |
+
- Keep inheritance hierarchies clear
|
902 |
+
- Add docstrings for better understanding
|
903 |
+
- Works great with both class-based and function-based code
|
904 |
+
|
905 |
+
### 🐛 Troubleshooting:
|
906 |
+
- **No UML diagram generated**: Check if your code contains class definitions
|
907 |
+
- **No call graph generated**: Ensure your code has function definitions and calls
|
908 |
+
- **Syntax errors**: Ensure your Python code is valid
|
909 |
+
- **Import errors**: Stick to standard library imports for best results
|
910 |
+
|
911 |
+
## Model Context Protocol (MCP) Server
|
912 |
+
|
913 |
+
This application automatically serves as an MCP server for AI assistants!
|
914 |
+
|
915 |
+
### 🌐 For Hugging Face Spaces (Public):
|
916 |
+
```json
|
917 |
+
{
|
918 |
+
"mcpServers": {
|
919 |
+
"python-diagram-generator": {
|
920 |
+
"url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse"
|
921 |
+
}
|
922 |
+
}
|
923 |
+
}
|
924 |
+
```
|
925 |
+
|
926 |
+
### 🏠 For Local Development:
|
927 |
+
```json
|
928 |
+
{
|
929 |
+
"mcpServers": {
|
930 |
+
"python-diagram-generator": {
|
931 |
+
"command": "npx",
|
932 |
+
"args": [
|
933 |
+
"mcp-remote",
|
934 |
+
"http://127.0.0.1:7860/gradio_api/mcp/sse",
|
935 |
+
"--transport",
|
936 |
+
"sse-only"
|
937 |
+
]
|
938 |
+
}
|
939 |
+
}
|
940 |
+
}
|
941 |
+
```
|
942 |
+
|
943 |
+
### 🔒 For Private Spaces:
|
944 |
+
```json
|
945 |
+
{
|
946 |
+
"mcpServers": {
|
947 |
+
"python-diagram-generator": {
|
948 |
+
"url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse",
|
949 |
+
"headers": {
|
950 |
+
"Authorization": "Bearer hf_your_token_here"
|
951 |
+
}
|
952 |
+
}
|
953 |
+
}
|
954 |
+
}
|
955 |
+
```
|
956 |
+
|
957 |
+
### 📋 Setup Instructions:
|
958 |
+
1. Install Node.js and mcp-remote: `npm install -g mcp-remote`
|
959 |
+
2. Add the configuration above to your MCP client
|
960 |
+
3. Restart your MCP client (e.g., Claude Desktop)
|
961 |
+
4. Test with prompts like: "Generate a UML diagram for this Python code: [your code]"
|
962 |
+
|
963 |
+
---
|
964 |
+
|
965 |
+
**Local MCP Endpoint**: `http://127.0.0.1:7860/gradio_api/mcp/sse`
|
966 |
+
**MCP Schema**: View at `/gradio_api/mcp/schema`
|
967 |
+
|
968 |
+
### 🚀 Future Features:
|
969 |
+
- Logic flowcharts
|
970 |
+
- Data flow diagrams
|
971 |
+
- State machine diagrams
|
972 |
+
- Multi-file analysis
|
973 |
+
- Enhanced UML features
|
974 |
+
"""
|
975 |
+
)
|
976 |
+
|
977 |
+
# Event handlers
|
978 |
+
def load_example(example_filename):
|
979 |
+
return get_sample_code(example_filename)
|
980 |
+
|
981 |
+
example_dropdown.change(
|
982 |
+
fn=load_example,
|
983 |
+
inputs=example_dropdown,
|
984 |
+
outputs=code_input,
|
985 |
+
)
|
986 |
+
|
987 |
+
generate_diagrams_btn.click(
|
988 |
+
fn=generate_all_diagrams,
|
989 |
+
inputs=[code_input, filename_input],
|
990 |
+
outputs=[uml_diagram_output, call_graph_output, analysis_output],
|
991 |
+
show_progress=True,
|
992 |
+
)
|
993 |
+
|
994 |
+
code_input.change(
|
995 |
+
fn=analyze_code_structure,
|
996 |
+
inputs=code_input,
|
997 |
+
outputs=analysis_output,
|
998 |
+
show_progress=False,
|
999 |
+
)
|
1000 |
+
|
1001 |
+
# Launch configuration
|
1002 |
+
if __name__ == "__main__":
|
1003 |
+
# Setup persistent testing space at startup
|
1004 |
+
setup_testing_space()
|
1005 |
+
|
1006 |
+
demo.launch(
|
1007 |
+
mcp_server=True, # Enable MCP functionality
|
1008 |
+
show_api=True, # Show API documentation
|
1009 |
+
show_error=True, # Show errors in interface
|
1010 |
+
# share = False # Share the app publicly
|
1011 |
+
debug=True, # Enable debug mode for development
|
1012 |
+
)
|
examples/complex_class.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Complex class example with properties, decorators, and advanced features."""
|
2 |
+
|
3 |
+
from datetime import datetime
|
4 |
+
from typing import Optional, List
|
5 |
+
|
6 |
+
|
7 |
+
class Product:
|
8 |
+
"""Product class with advanced Python features."""
|
9 |
+
|
10 |
+
# Class variable
|
11 |
+
total_products = 0
|
12 |
+
|
13 |
+
def __init__(self, name: str, price: float, category: str):
|
14 |
+
self._name = name
|
15 |
+
self._price = price
|
16 |
+
self._category = category
|
17 |
+
self._created_at = datetime.now()
|
18 |
+
self._discount = 0.0
|
19 |
+
Product.total_products += 1
|
20 |
+
|
21 |
+
@property
|
22 |
+
def name(self) -> str:
|
23 |
+
"""Product name property."""
|
24 |
+
return self._name
|
25 |
+
|
26 |
+
@name.setter
|
27 |
+
def name(self, value: str):
|
28 |
+
if not value.strip():
|
29 |
+
raise ValueError("Product name cannot be empty")
|
30 |
+
self._name = value
|
31 |
+
|
32 |
+
@property
|
33 |
+
def price(self) -> float:
|
34 |
+
"""Product price with discount applied."""
|
35 |
+
return self._price * (1 - self._discount)
|
36 |
+
|
37 |
+
@property
|
38 |
+
def original_price(self) -> float:
|
39 |
+
"""Original price before discount."""
|
40 |
+
return self._price
|
41 |
+
|
42 |
+
@original_price.setter
|
43 |
+
def original_price(self, value: float):
|
44 |
+
if value < 0:
|
45 |
+
raise ValueError("Price cannot be negative")
|
46 |
+
self._price = value
|
47 |
+
|
48 |
+
def apply_discount(self, percentage: float):
|
49 |
+
"""Apply discount percentage."""
|
50 |
+
if 0 <= percentage <= 100:
|
51 |
+
self._discount = percentage / 100
|
52 |
+
|
53 |
+
@staticmethod
|
54 |
+
def validate_category(category: str) -> bool:
|
55 |
+
"""Validate if category is allowed."""
|
56 |
+
allowed_categories = ["electronics", "clothing", "books", "food"]
|
57 |
+
return category.lower() in allowed_categories
|
58 |
+
|
59 |
+
@classmethod
|
60 |
+
def create_book(cls, title: str, price: float):
|
61 |
+
"""Factory method to create a book product."""
|
62 |
+
return cls(title, price, "books")
|
63 |
+
|
64 |
+
@classmethod
|
65 |
+
def get_total_products(cls) -> int:
|
66 |
+
"""Get total number of products created."""
|
67 |
+
return cls.total_products
|
68 |
+
|
69 |
+
def __str__(self) -> str:
|
70 |
+
return f"{self._name} - ${self.price:.2f}"
|
71 |
+
|
72 |
+
def __repr__(self) -> str:
|
73 |
+
return f"Product(name='{self._name}', price={self._price}, category='{self._category}')"
|
examples/fibonacci_functions.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Fibonacci computation functions for testing function analysis.
|
3 |
+
"""
|
4 |
+
|
5 |
+
def fibonacci_recursive(n):
|
6 |
+
"""Calculate fibonacci number using recursion."""
|
7 |
+
if n <= 1:
|
8 |
+
return n
|
9 |
+
return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
|
10 |
+
|
11 |
+
|
12 |
+
def fibonacci_iterative(n):
|
13 |
+
"""Calculate fibonacci number using iteration."""
|
14 |
+
if n <= 1:
|
15 |
+
return n
|
16 |
+
|
17 |
+
a, b = 0, 1
|
18 |
+
for _ in range(2, n + 1):
|
19 |
+
a, b = b, a + b
|
20 |
+
return b
|
21 |
+
|
22 |
+
|
23 |
+
def fibonacci_memoized(n, memo=None):
|
24 |
+
"""Calculate fibonacci number using memoization."""
|
25 |
+
if memo is None:
|
26 |
+
memo = {}
|
27 |
+
|
28 |
+
if n in memo:
|
29 |
+
return memo[n]
|
30 |
+
|
31 |
+
if n <= 1:
|
32 |
+
memo[n] = n
|
33 |
+
return n
|
34 |
+
|
35 |
+
memo[n] = fibonacci_memoized(n - 1, memo) + fibonacci_memoized(n - 2, memo)
|
36 |
+
return memo[n]
|
37 |
+
|
38 |
+
|
39 |
+
def fibonacci_sequence(count):
|
40 |
+
"""Generate a sequence of fibonacci numbers."""
|
41 |
+
sequence = []
|
42 |
+
for i in range(count):
|
43 |
+
sequence.append(fibonacci_iterative(i))
|
44 |
+
return sequence
|
45 |
+
|
46 |
+
|
47 |
+
def compare_fibonacci_methods(n):
|
48 |
+
"""Compare different fibonacci calculation methods."""
|
49 |
+
import time
|
50 |
+
|
51 |
+
methods = [
|
52 |
+
("Recursive", fibonacci_recursive),
|
53 |
+
("Iterative", fibonacci_iterative),
|
54 |
+
("Memoized", fibonacci_memoized)
|
55 |
+
]
|
56 |
+
|
57 |
+
results = {}
|
58 |
+
for name, func in methods:
|
59 |
+
start_time = time.time()
|
60 |
+
result = func(n)
|
61 |
+
end_time = time.time()
|
62 |
+
results[name] = {
|
63 |
+
'result': result,
|
64 |
+
'time': end_time - start_time
|
65 |
+
}
|
66 |
+
|
67 |
+
return results
|
68 |
+
|
69 |
+
|
70 |
+
def validate_fibonacci_result(n, result):
|
71 |
+
"""Validate if a fibonacci result is correct."""
|
72 |
+
if n <= 1:
|
73 |
+
return result == n
|
74 |
+
|
75 |
+
# Use iterative method as baseline for validation
|
76 |
+
expected = fibonacci_iterative(n)
|
77 |
+
return result == expected
|
78 |
+
|
79 |
+
|
80 |
+
if __name__ == "__main__":
|
81 |
+
n = 10
|
82 |
+
print(f"Fibonacci({n}) using different methods:")
|
83 |
+
|
84 |
+
results = compare_fibonacci_methods(n)
|
85 |
+
for method, data in results.items():
|
86 |
+
print(f"{method}: {data['result']} (took {data['time']:.6f} seconds)")
|
87 |
+
|
88 |
+
print(f"\nFirst 15 fibonacci numbers: {fibonacci_sequence(15)}")
|
examples/inheritance_example.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Class inheritance example for testing AST parsing."""
|
2 |
+
|
3 |
+
class Animal:
|
4 |
+
"""Base animal class."""
|
5 |
+
|
6 |
+
def __init__(self, name, species):
|
7 |
+
self.name = name
|
8 |
+
self.species = species
|
9 |
+
self.is_alive = True
|
10 |
+
|
11 |
+
def speak(self):
|
12 |
+
return "Some generic animal sound"
|
13 |
+
|
14 |
+
def eat(self):
|
15 |
+
return f"{self.name} is eating"
|
16 |
+
|
17 |
+
|
18 |
+
class Dog(Animal):
|
19 |
+
"""Dog class inheriting from Animal."""
|
20 |
+
|
21 |
+
def __init__(self, name, breed):
|
22 |
+
super().__init__(name, "Canine")
|
23 |
+
self.breed = breed
|
24 |
+
self.is_trained = False
|
25 |
+
|
26 |
+
def speak(self):
|
27 |
+
return "Woof!"
|
28 |
+
|
29 |
+
def fetch(self):
|
30 |
+
return f"{self.name} is fetching the ball"
|
31 |
+
|
32 |
+
def train(self):
|
33 |
+
self.is_trained = True
|
34 |
+
|
35 |
+
|
36 |
+
class Cat(Animal):
|
37 |
+
"""Cat class inheriting from Animal."""
|
38 |
+
|
39 |
+
def __init__(self, name, color):
|
40 |
+
super().__init__(name, "Feline")
|
41 |
+
self.color = color
|
42 |
+
self.lives_left = 9
|
43 |
+
|
44 |
+
def speak(self):
|
45 |
+
return "Meow!"
|
46 |
+
|
47 |
+
def climb(self):
|
48 |
+
return f"{self.name} is climbing"
|
examples/main_with_helpers.py
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Main function with multiple helper functions for testing function analysis.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
from datetime import datetime
|
8 |
+
from typing import Dict, List, Any
|
9 |
+
|
10 |
+
|
11 |
+
def read_config_file(config_path):
|
12 |
+
"""Read configuration from JSON file."""
|
13 |
+
try:
|
14 |
+
with open(config_path, 'r') as file:
|
15 |
+
return json.load(file)
|
16 |
+
except FileNotFoundError:
|
17 |
+
print(f"Config file not found: {config_path}")
|
18 |
+
return {}
|
19 |
+
except json.JSONDecodeError:
|
20 |
+
print(f"Invalid JSON in config file: {config_path}")
|
21 |
+
return {}
|
22 |
+
|
23 |
+
|
24 |
+
def validate_config(config):
|
25 |
+
"""Validate configuration parameters."""
|
26 |
+
required_keys = ['input_directory', 'output_directory', 'file_extensions']
|
27 |
+
|
28 |
+
for key in required_keys:
|
29 |
+
if key not in config:
|
30 |
+
print(f"Missing required config key: {key}")
|
31 |
+
return False
|
32 |
+
|
33 |
+
# Validate directories exist
|
34 |
+
if not os.path.exists(config['input_directory']):
|
35 |
+
print(f"Input directory does not exist: {config['input_directory']}")
|
36 |
+
return False
|
37 |
+
|
38 |
+
return True
|
39 |
+
|
40 |
+
|
41 |
+
def create_output_directory(output_path):
|
42 |
+
"""Create output directory if it doesn't exist."""
|
43 |
+
try:
|
44 |
+
os.makedirs(output_path, exist_ok=True)
|
45 |
+
print(f"Output directory ready: {output_path}")
|
46 |
+
return True
|
47 |
+
except PermissionError:
|
48 |
+
print(f"Permission denied creating directory: {output_path}")
|
49 |
+
return False
|
50 |
+
|
51 |
+
|
52 |
+
def scan_files(directory, extensions):
|
53 |
+
"""Scan directory for files with specified extensions."""
|
54 |
+
found_files = []
|
55 |
+
|
56 |
+
for root, dirs, files in os.walk(directory):
|
57 |
+
for file in files:
|
58 |
+
file_extension = os.path.splitext(file)[1].lower()
|
59 |
+
if file_extension in extensions:
|
60 |
+
full_path = os.path.join(root, file)
|
61 |
+
found_files.append(full_path)
|
62 |
+
|
63 |
+
return found_files
|
64 |
+
|
65 |
+
|
66 |
+
def analyze_file(file_path):
|
67 |
+
"""Analyze a single file and return statistics."""
|
68 |
+
try:
|
69 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
70 |
+
content = file.read()
|
71 |
+
|
72 |
+
stats = {
|
73 |
+
'file_path': file_path,
|
74 |
+
'file_size': os.path.getsize(file_path),
|
75 |
+
'line_count': len(content.splitlines()),
|
76 |
+
'character_count': len(content),
|
77 |
+
'word_count': len(content.split()),
|
78 |
+
'last_modified': datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
|
79 |
+
}
|
80 |
+
|
81 |
+
return stats
|
82 |
+
|
83 |
+
except Exception as e:
|
84 |
+
print(f"Error analyzing file {file_path}: {e}")
|
85 |
+
return None
|
86 |
+
|
87 |
+
|
88 |
+
def process_files(file_list):
|
89 |
+
"""Process multiple files and return aggregated results."""
|
90 |
+
results = []
|
91 |
+
total_size = 0
|
92 |
+
total_lines = 0
|
93 |
+
|
94 |
+
print(f"Processing {len(file_list)} files...")
|
95 |
+
|
96 |
+
for file_path in file_list:
|
97 |
+
print(f" Analyzing: {os.path.basename(file_path)}")
|
98 |
+
|
99 |
+
file_stats = analyze_file(file_path)
|
100 |
+
if file_stats:
|
101 |
+
results.append(file_stats)
|
102 |
+
total_size += file_stats['file_size']
|
103 |
+
total_lines += file_stats['line_count']
|
104 |
+
|
105 |
+
summary = {
|
106 |
+
'total_files': len(results),
|
107 |
+
'total_size_bytes': total_size,
|
108 |
+
'total_lines': total_lines,
|
109 |
+
'average_file_size': total_size / len(results) if results else 0,
|
110 |
+
'average_lines_per_file': total_lines / len(results) if results else 0
|
111 |
+
}
|
112 |
+
|
113 |
+
return results, summary
|
114 |
+
|
115 |
+
|
116 |
+
def generate_report(results, summary, output_file):
|
117 |
+
"""Generate a detailed report of the analysis."""
|
118 |
+
report = {
|
119 |
+
'generated_at': datetime.now().isoformat(),
|
120 |
+
'summary': summary,
|
121 |
+
'file_details': results
|
122 |
+
}
|
123 |
+
|
124 |
+
try:
|
125 |
+
with open(output_file, 'w') as file:
|
126 |
+
json.dump(report, file, indent=2)
|
127 |
+
print(f"Report generated: {output_file}")
|
128 |
+
return True
|
129 |
+
except Exception as e:
|
130 |
+
print(f"Error generating report: {e}")
|
131 |
+
return False
|
132 |
+
|
133 |
+
|
134 |
+
def print_summary(summary):
|
135 |
+
"""Print a human-readable summary."""
|
136 |
+
print("\n" + "="*50)
|
137 |
+
print("FILE ANALYSIS SUMMARY")
|
138 |
+
print("="*50)
|
139 |
+
print(f"Total files processed: {summary['total_files']}")
|
140 |
+
print(f"Total size: {summary['total_size_bytes']:,} bytes")
|
141 |
+
print(f"Total lines: {summary['total_lines']:,}")
|
142 |
+
print(f"Average file size: {summary['average_file_size']:.1f} bytes")
|
143 |
+
print(f"Average lines per file: {summary['average_lines_per_file']:.1f}")
|
144 |
+
print("="*50)
|
145 |
+
|
146 |
+
|
147 |
+
def cleanup_temp_files(temp_directory):
|
148 |
+
"""Clean up temporary files."""
|
149 |
+
if not os.path.exists(temp_directory):
|
150 |
+
return
|
151 |
+
|
152 |
+
try:
|
153 |
+
temp_files = os.listdir(temp_directory)
|
154 |
+
for temp_file in temp_files:
|
155 |
+
if temp_file.startswith('temp_'):
|
156 |
+
file_path = os.path.join(temp_directory, temp_file)
|
157 |
+
os.remove(file_path)
|
158 |
+
print(f"Removed temp file: {temp_file}")
|
159 |
+
except Exception as e:
|
160 |
+
print(f"Error during cleanup: {e}")
|
161 |
+
|
162 |
+
|
163 |
+
def main():
|
164 |
+
"""Main function that orchestrates the file analysis process."""
|
165 |
+
print("Starting File Analysis Tool")
|
166 |
+
print("-" * 30)
|
167 |
+
|
168 |
+
# Step 1: Read configuration
|
169 |
+
config_file = "config.json"
|
170 |
+
config = read_config_file(config_file)
|
171 |
+
|
172 |
+
if not config:
|
173 |
+
print("Using default configuration...")
|
174 |
+
config = {
|
175 |
+
'input_directory': '.',
|
176 |
+
'output_directory': './output',
|
177 |
+
'file_extensions': ['.py', '.txt', '.md'],
|
178 |
+
'generate_report': True
|
179 |
+
}
|
180 |
+
|
181 |
+
# Step 2: Validate configuration
|
182 |
+
if not validate_config(config):
|
183 |
+
print("Configuration validation failed. Exiting.")
|
184 |
+
return False
|
185 |
+
|
186 |
+
# Step 3: Create output directory
|
187 |
+
if not create_output_directory(config['output_directory']):
|
188 |
+
print("Failed to create output directory. Exiting.")
|
189 |
+
return False
|
190 |
+
|
191 |
+
# Step 4: Scan for files
|
192 |
+
print(f"Scanning for files in: {config['input_directory']}")
|
193 |
+
file_list = scan_files(config['input_directory'], config['file_extensions'])
|
194 |
+
|
195 |
+
if not file_list:
|
196 |
+
print("No files found matching criteria.")
|
197 |
+
return False
|
198 |
+
|
199 |
+
print(f"Found {len(file_list)} files to process")
|
200 |
+
|
201 |
+
# Step 5: Process files
|
202 |
+
results, summary = process_files(file_list)
|
203 |
+
|
204 |
+
# Step 6: Generate report
|
205 |
+
if config.get('generate_report', True):
|
206 |
+
report_file = os.path.join(config['output_directory'], 'analysis_report.json')
|
207 |
+
generate_report(results, summary, report_file)
|
208 |
+
|
209 |
+
# Step 7: Display summary
|
210 |
+
print_summary(summary)
|
211 |
+
|
212 |
+
# Step 8: Cleanup
|
213 |
+
temp_dir = config.get('temp_directory')
|
214 |
+
if temp_dir:
|
215 |
+
cleanup_temp_files(temp_dir)
|
216 |
+
|
217 |
+
print("\nFile analysis completed successfully!")
|
218 |
+
return True
|
219 |
+
|
220 |
+
|
221 |
+
if __name__ == "__main__":
|
222 |
+
success = main()
|
223 |
+
exit(0 if success else 1)
|
examples/multiple_classes.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Multiple classes example for testing AST parsing."""
|
2 |
+
|
3 |
+
class BankAccount:
|
4 |
+
"""Simple bank account class."""
|
5 |
+
|
6 |
+
def __init__(self, account_number, initial_balance=0):
|
7 |
+
self.account_number = account_number
|
8 |
+
self.balance = initial_balance
|
9 |
+
self.transaction_history = []
|
10 |
+
|
11 |
+
def deposit(self, amount):
|
12 |
+
self.balance += amount
|
13 |
+
self.transaction_history.append(f"Deposit: +{amount}")
|
14 |
+
|
15 |
+
def withdraw(self, amount):
|
16 |
+
if amount <= self.balance:
|
17 |
+
self.balance -= amount
|
18 |
+
self.transaction_history.append(f"Withdrawal: -{amount}")
|
19 |
+
return True
|
20 |
+
return False
|
21 |
+
|
22 |
+
def get_balance(self):
|
23 |
+
return self.balance
|
24 |
+
|
25 |
+
|
26 |
+
class Customer:
|
27 |
+
"""Customer class to hold account information."""
|
28 |
+
|
29 |
+
def __init__(self, customer_id, name, email):
|
30 |
+
self.customer_id = customer_id
|
31 |
+
self.name = name
|
32 |
+
self.email = email
|
33 |
+
self.accounts = []
|
34 |
+
|
35 |
+
def add_account(self, account):
|
36 |
+
self.accounts.append(account)
|
37 |
+
|
38 |
+
def get_total_balance(self):
|
39 |
+
return sum(account.get_balance() for account in self.accounts)
|
40 |
+
|
41 |
+
|
42 |
+
class Bank:
|
43 |
+
"""Bank class to manage customers and accounts."""
|
44 |
+
|
45 |
+
def __init__(self, name):
|
46 |
+
self.name = name
|
47 |
+
self.customers = {}
|
48 |
+
self.next_account_number = 1000
|
49 |
+
|
50 |
+
def create_customer(self, name, email):
|
51 |
+
customer_id = len(self.customers) + 1
|
52 |
+
customer = Customer(customer_id, name, email)
|
53 |
+
self.customers[customer_id] = customer
|
54 |
+
return customer
|
55 |
+
|
56 |
+
def create_account(self, customer_id, initial_balance=0):
|
57 |
+
if customer_id in self.customers:
|
58 |
+
account = BankAccount(self.next_account_number, initial_balance)
|
59 |
+
self.customers[customer_id].add_account(account)
|
60 |
+
self.next_account_number += 1
|
61 |
+
return account
|
62 |
+
return None
|
examples/sample_classes.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Animal:
|
2 |
+
def __init__(self, name: str, age: int):
|
3 |
+
self.name = name
|
4 |
+
self.age = age
|
5 |
+
|
6 |
+
def speak(self) -> str:
|
7 |
+
pass
|
8 |
+
|
9 |
+
def get_info(self) -> str:
|
10 |
+
return f"{self.name} is {self.age} years old"
|
11 |
+
|
12 |
+
|
13 |
+
class Dog(Animal):
|
14 |
+
def __init__(self, name: str, age: int, breed: str):
|
15 |
+
super().__init__(name, age)
|
16 |
+
self.breed = breed
|
17 |
+
|
18 |
+
def speak(self) -> str:
|
19 |
+
return f"{self.name} says Woof!"
|
20 |
+
|
21 |
+
def fetch(self) -> str:
|
22 |
+
return f"{self.name} is fetching the ball"
|
23 |
+
|
24 |
+
|
25 |
+
class Cat(Animal):
|
26 |
+
def __init__(self, name: str, age: int, indoor: bool = True):
|
27 |
+
super().__init__(name, age)
|
28 |
+
self.indoor = indoor
|
29 |
+
|
30 |
+
def speak(self) -> str:
|
31 |
+
return f"{self.name} says Meow!"
|
32 |
+
|
33 |
+
def climb(self) -> str:
|
34 |
+
return f"{self.name} is climbing"
|
35 |
+
|
36 |
+
|
37 |
+
class PetOwner:
|
38 |
+
def __init__(self, name: str):
|
39 |
+
self.name = name
|
40 |
+
self.pets = []
|
41 |
+
|
42 |
+
def add_pet(self, pet: Animal):
|
43 |
+
self.pets.append(pet)
|
44 |
+
|
45 |
+
def call_all_pets(self) -> list:
|
46 |
+
# return [pet.speak() for pet in self.pets]
|
47 |
+
result = []
|
48 |
+
|
49 |
+
for pet in self.pets:
|
50 |
+
result.append(pet.speak())
|
51 |
+
return result
|
52 |
+
|
53 |
+
|
54 |
+
def create_pet_family():
|
55 |
+
owner = PetOwner("Alice")
|
56 |
+
dog = Dog("Buddy", 3, "Golden Retriever")
|
57 |
+
cat = Cat("Whiskers", 2, True)
|
58 |
+
|
59 |
+
owner.add_pet(dog)
|
60 |
+
owner.add_pet(cat)
|
61 |
+
|
62 |
+
return owner
|
examples/sample_functions.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import math
|
2 |
+
import random
|
3 |
+
from typing import List, Tuple
|
4 |
+
|
5 |
+
def calculate_factorial(n: int) -> int:
|
6 |
+
"""Calculate factorial of a number recursively."""
|
7 |
+
if n <= 1:
|
8 |
+
return 1
|
9 |
+
return n * calculate_factorial(n - 1)
|
10 |
+
|
11 |
+
def fibonacci_sequence(n: int) -> List[int]:
|
12 |
+
"""Generate fibonacci sequence up to n terms."""
|
13 |
+
if n <= 0:
|
14 |
+
return []
|
15 |
+
elif n == 1:
|
16 |
+
return [0]
|
17 |
+
elif n == 2:
|
18 |
+
return [0, 1]
|
19 |
+
|
20 |
+
sequence = [0, 1]
|
21 |
+
for i in range(2, n):
|
22 |
+
sequence.append(sequence[i-1] + sequence[i-2])
|
23 |
+
return sequence
|
24 |
+
|
25 |
+
def is_prime(num: int) -> bool:
|
26 |
+
"""Check if a number is prime."""
|
27 |
+
if num < 2:
|
28 |
+
return False
|
29 |
+
if num == 2:
|
30 |
+
return True
|
31 |
+
if num % 2 == 0:
|
32 |
+
return False
|
33 |
+
|
34 |
+
for i in range(3, int(math.sqrt(num)) + 1, 2):
|
35 |
+
if num % i == 0:
|
36 |
+
return False
|
37 |
+
return True
|
38 |
+
|
39 |
+
def find_primes_in_range(start: int, end: int) -> List[int]:
|
40 |
+
"""Find all prime numbers in a given range."""
|
41 |
+
primes = []
|
42 |
+
for num in range(start, end + 1):
|
43 |
+
if is_prime(num):
|
44 |
+
primes.append(num)
|
45 |
+
return primes
|
46 |
+
|
47 |
+
def calculate_statistics(numbers: List[float]) -> dict:
|
48 |
+
"""Calculate basic statistics for a list of numbers."""
|
49 |
+
if not numbers:
|
50 |
+
return {"error": "Empty list provided"}
|
51 |
+
|
52 |
+
n = len(numbers)
|
53 |
+
mean = sum(numbers) / n
|
54 |
+
sorted_nums = sorted(numbers)
|
55 |
+
|
56 |
+
# Calculate median
|
57 |
+
if n % 2 == 0:
|
58 |
+
median = (sorted_nums[n//2 - 1] + sorted_nums[n//2]) / 2
|
59 |
+
else:
|
60 |
+
median = sorted_nums[n//2]
|
61 |
+
|
62 |
+
# Calculate variance and standard deviation
|
63 |
+
variance = sum((x - mean) ** 2 for x in numbers) / n
|
64 |
+
std_dev = math.sqrt(variance)
|
65 |
+
|
66 |
+
return {
|
67 |
+
"count": n,
|
68 |
+
"mean": mean,
|
69 |
+
"median": median,
|
70 |
+
"min": min(numbers),
|
71 |
+
"max": max(numbers),
|
72 |
+
"variance": variance,
|
73 |
+
"std_deviation": std_dev
|
74 |
+
}
|
75 |
+
|
76 |
+
def monte_carlo_pi(iterations: int) -> float:
|
77 |
+
"""Estimate Pi using Monte Carlo method."""
|
78 |
+
inside_circle = 0
|
79 |
+
|
80 |
+
for _ in range(iterations):
|
81 |
+
x, y = random.random(), random.random()
|
82 |
+
if x*x + y*y <= 1:
|
83 |
+
inside_circle += 1
|
84 |
+
|
85 |
+
return 4 * inside_circle / iterations
|
86 |
+
|
87 |
+
def process_data_pipeline(data: List[int]) -> dict:
|
88 |
+
"""Complex data processing pipeline demonstrating function calls."""
|
89 |
+
# Step 1: Filter for positive numbers
|
90 |
+
positive_data = []
|
91 |
+
for x in data:
|
92 |
+
if x > 0:
|
93 |
+
positive_data.append(x)
|
94 |
+
|
95 |
+
# Step 2: Find primes in the data
|
96 |
+
primes_in_data = []
|
97 |
+
for x in positive_data:
|
98 |
+
if is_prime(x):
|
99 |
+
primes_in_data.append(x)
|
100 |
+
|
101 |
+
# Step 3: Calculate statistics
|
102 |
+
positive_data_floats = []
|
103 |
+
for x in positive_data:
|
104 |
+
positive_data_floats.append(float(x))
|
105 |
+
stats = calculate_statistics(positive_data_floats)
|
106 |
+
|
107 |
+
# Step 4: Generate fibonacci sequence up to max value
|
108 |
+
max_val = max(positive_data) if positive_data else 0
|
109 |
+
fib_count = min(max_val, 20) # Limit to reasonable size
|
110 |
+
fib_sequence = fibonacci_sequence(fib_count)
|
111 |
+
|
112 |
+
# Step 5: Calculate factorials for small numbers
|
113 |
+
small_numbers = []
|
114 |
+
for x in positive_data:
|
115 |
+
if x <= 10:
|
116 |
+
small_numbers.append(x)
|
117 |
+
factorials = {}
|
118 |
+
for x in small_numbers:
|
119 |
+
factorials[x] = calculate_factorial(x)
|
120 |
+
|
121 |
+
return {
|
122 |
+
"original_count": len(data),
|
123 |
+
"positive_count": len(positive_data),
|
124 |
+
"primes_found": primes_in_data,
|
125 |
+
"statistics": stats,
|
126 |
+
"fibonacci_sequence": fib_sequence,
|
127 |
+
"factorials": factorials,
|
128 |
+
"pi_estimate": monte_carlo_pi(1000)
|
129 |
+
}
|
130 |
+
|
131 |
+
def main():
|
132 |
+
"""Main function demonstrating the pipeline."""
|
133 |
+
sample_data = [1, 2, 3, 5, 8, 13, 21, -1, 0, 17, 19, 23]
|
134 |
+
result = process_data_pipeline(sample_data)
|
135 |
+
print(f"Processing complete: {len(result)} metrics calculated")
|
136 |
+
return result
|
137 |
+
|
138 |
+
if __name__ == "__main__":
|
139 |
+
main()
|
examples/simple_class.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Simple class example for testing AST parsing."""
|
2 |
+
|
3 |
+
class Person:
|
4 |
+
"""A simple Person class."""
|
5 |
+
|
6 |
+
def __init__(self, name, age):
|
7 |
+
self.name = name
|
8 |
+
self.age = age
|
9 |
+
self.email = None
|
10 |
+
|
11 |
+
def get_name(self):
|
12 |
+
return self.name
|
13 |
+
|
14 |
+
def set_email(self, email):
|
15 |
+
self.email = email
|
16 |
+
|
17 |
+
def greet(self):
|
18 |
+
return f"Hello, I'm {self.name}"
|
19 |
+
|
20 |
+
def is_adult(self):
|
21 |
+
return self.age >= 18
|
examples/string_processing.py
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
String processing pipeline functions for testing function analysis.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import re
|
6 |
+
from typing import List
|
7 |
+
|
8 |
+
|
9 |
+
def normalize_whitespace(text):
|
10 |
+
"""Normalize whitespace by removing extra spaces and newlines."""
|
11 |
+
# Replace multiple whitespace with single space
|
12 |
+
text = re.sub(r'\s+', ' ', text)
|
13 |
+
# Strip leading and trailing whitespace
|
14 |
+
return text.strip()
|
15 |
+
|
16 |
+
|
17 |
+
def remove_special_characters(text, keep_chars=""):
|
18 |
+
"""Remove special characters, optionally keeping specified characters."""
|
19 |
+
# Keep alphanumeric, spaces, and specified characters
|
20 |
+
pattern = fr"[^a-zA-Z0-9\s{re.escape(keep_chars)}]"
|
21 |
+
return re.sub(pattern, '', text)
|
22 |
+
|
23 |
+
|
24 |
+
def convert_to_lowercase(text):
|
25 |
+
"""Convert text to lowercase."""
|
26 |
+
return text.lower()
|
27 |
+
|
28 |
+
|
29 |
+
def remove_stopwords(text, stopwords=None):
|
30 |
+
"""Remove common stopwords from text."""
|
31 |
+
if stopwords is None:
|
32 |
+
stopwords = {
|
33 |
+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to',
|
34 |
+
'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be',
|
35 |
+
'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
|
36 |
+
'will', 'would', 'could', 'should', 'may', 'might', 'must'
|
37 |
+
}
|
38 |
+
|
39 |
+
words = text.split()
|
40 |
+
filtered_words = [word for word in words if word.lower() not in stopwords]
|
41 |
+
return ' '.join(filtered_words)
|
42 |
+
|
43 |
+
|
44 |
+
def extract_keywords(text, min_length=3):
|
45 |
+
"""Extract keywords (words longer than min_length)."""
|
46 |
+
words = text.split()
|
47 |
+
keywords = [word for word in words if len(word) >= min_length]
|
48 |
+
return keywords
|
49 |
+
|
50 |
+
|
51 |
+
def count_word_frequency(text):
|
52 |
+
"""Count frequency of each word in text."""
|
53 |
+
words = text.split()
|
54 |
+
frequency = {}
|
55 |
+
for word in words:
|
56 |
+
frequency[word] = frequency.get(word, 0) + 1
|
57 |
+
return frequency
|
58 |
+
|
59 |
+
|
60 |
+
def capitalize_words(text, exceptions=None):
|
61 |
+
"""Capitalize first letter of each word, with exceptions."""
|
62 |
+
if exceptions is None:
|
63 |
+
exceptions = {'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}
|
64 |
+
|
65 |
+
words = text.split()
|
66 |
+
capitalized = []
|
67 |
+
|
68 |
+
for i, word in enumerate(words):
|
69 |
+
if i == 0 or word.lower() not in exceptions:
|
70 |
+
capitalized.append(word.capitalize())
|
71 |
+
else:
|
72 |
+
capitalized.append(word.lower())
|
73 |
+
|
74 |
+
return ' '.join(capitalized)
|
75 |
+
|
76 |
+
|
77 |
+
def truncate_text(text, max_length=100, suffix="..."):
|
78 |
+
"""Truncate text to specified length with suffix."""
|
79 |
+
if len(text) <= max_length:
|
80 |
+
return text
|
81 |
+
|
82 |
+
truncated = text[:max_length - len(suffix)]
|
83 |
+
# Try to break at last complete word
|
84 |
+
last_space = truncated.rfind(' ')
|
85 |
+
if last_space > max_length * 0.8: # If we can break at a word boundary
|
86 |
+
truncated = truncated[:last_space]
|
87 |
+
|
88 |
+
return truncated + suffix
|
89 |
+
|
90 |
+
|
91 |
+
def text_processing_pipeline(text, operations=None):
|
92 |
+
"""Process text through a pipeline of operations."""
|
93 |
+
if operations is None:
|
94 |
+
operations = [
|
95 |
+
'normalize_whitespace',
|
96 |
+
'remove_special_characters',
|
97 |
+
'convert_to_lowercase',
|
98 |
+
'remove_stopwords'
|
99 |
+
]
|
100 |
+
|
101 |
+
# Map operation names to functions
|
102 |
+
operation_map = {
|
103 |
+
'normalize_whitespace': normalize_whitespace,
|
104 |
+
'remove_special_characters': remove_special_characters,
|
105 |
+
'convert_to_lowercase': convert_to_lowercase,
|
106 |
+
'remove_stopwords': remove_stopwords,
|
107 |
+
'capitalize_words': capitalize_words,
|
108 |
+
'truncate_text': truncate_text
|
109 |
+
}
|
110 |
+
|
111 |
+
result = text
|
112 |
+
processing_steps = []
|
113 |
+
|
114 |
+
for operation in operations:
|
115 |
+
if operation in operation_map:
|
116 |
+
before = result
|
117 |
+
result = operation_map[operation](result)
|
118 |
+
processing_steps.append({
|
119 |
+
'operation': operation,
|
120 |
+
'before': before[:50] + "..." if len(before) > 50 else before,
|
121 |
+
'after': result[:50] + "..." if len(result) > 50 else result
|
122 |
+
})
|
123 |
+
|
124 |
+
return result, processing_steps
|
125 |
+
|
126 |
+
|
127 |
+
def analyze_text_statistics(text):
|
128 |
+
"""Analyze various statistics about the text."""
|
129 |
+
words = text.split()
|
130 |
+
|
131 |
+
stats = {
|
132 |
+
'character_count': len(text),
|
133 |
+
'word_count': len(words),
|
134 |
+
'sentence_count': len(re.findall(r'[.!?]+', text)),
|
135 |
+
'average_word_length': sum(len(word) for word in words) / len(words) if words else 0,
|
136 |
+
'longest_word': max(words, key=len) if words else "",
|
137 |
+
'shortest_word': min(words, key=len) if words else ""
|
138 |
+
}
|
139 |
+
|
140 |
+
return stats
|
141 |
+
|
142 |
+
|
143 |
+
if __name__ == "__main__":
|
144 |
+
sample_text = """
|
145 |
+
This is a SAMPLE text with various formatting issues!!!
|
146 |
+
It has multiple spaces, special @#$% characters, and
|
147 |
+
needs some serious cleaning & processing...
|
148 |
+
"""
|
149 |
+
|
150 |
+
print("Original text:")
|
151 |
+
print(repr(sample_text))
|
152 |
+
|
153 |
+
processed_text, steps = text_processing_pipeline(sample_text)
|
154 |
+
|
155 |
+
print("\nProcessing steps:")
|
156 |
+
for step in steps:
|
157 |
+
print(f"After {step['operation']}:")
|
158 |
+
print(f" {step['after']}")
|
159 |
+
|
160 |
+
print(f"\nFinal result: {processed_text}")
|
161 |
+
|
162 |
+
stats = analyze_text_statistics(processed_text)
|
163 |
+
print(f"\nText statistics: {stats}")
|
packages.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
graphviz
|
pyproject.toml
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "gradio-plant-uml"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"datasets>=3.6.0",
|
9 |
+
"fastapi>=0.115.12",
|
10 |
+
"flask>=3.1.1",
|
11 |
+
"gradio[mcp]>=5.33.0",
|
12 |
+
"graphviz>=0.20.3",
|
13 |
+
"matplotlib>=3.10.3",
|
14 |
+
"numpy>=2.2.6",
|
15 |
+
"pandas>=2.3.0",
|
16 |
+
"plantuml>=0.3.0",
|
17 |
+
"polars>=1.30.0",
|
18 |
+
"py2puml>=0.10.0",
|
19 |
+
"pyan3>=1.2.0",
|
20 |
+
"requests>=2.32.3",
|
21 |
+
"scikit-learn>=1.7.0",
|
22 |
+
"transformers>=4.52.4",
|
23 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file was autogenerated by uv via the following command:
|
2 |
+
# uv pip compile pyproject.toml -o requirements.txt
|
3 |
+
aiofiles==24.1.0
|
4 |
+
# via gradio
|
5 |
+
aiohappyeyeballs==2.6.1
|
6 |
+
# via aiohttp
|
7 |
+
aiohttp==3.12.9
|
8 |
+
# via fsspec
|
9 |
+
aiosignal==1.3.2
|
10 |
+
# via aiohttp
|
11 |
+
annotated-types==0.7.0
|
12 |
+
# via pydantic
|
13 |
+
anyio==4.9.0
|
14 |
+
# via
|
15 |
+
# gradio
|
16 |
+
# httpx
|
17 |
+
# mcp
|
18 |
+
# sse-starlette
|
19 |
+
# starlette
|
20 |
+
attrs==25.3.0
|
21 |
+
# via aiohttp
|
22 |
+
blinker==1.9.0
|
23 |
+
# via flask
|
24 |
+
certifi==2025.4.26
|
25 |
+
# via
|
26 |
+
# httpcore
|
27 |
+
# httpx
|
28 |
+
# requests
|
29 |
+
charset-normalizer==3.4.2
|
30 |
+
# via requests
|
31 |
+
click==8.2.1
|
32 |
+
# via
|
33 |
+
# flask
|
34 |
+
# typer
|
35 |
+
# uvicorn
|
36 |
+
colorama==0.4.6
|
37 |
+
# via
|
38 |
+
# click
|
39 |
+
# tqdm
|
40 |
+
contourpy==1.3.2
|
41 |
+
# via matplotlib
|
42 |
+
cycler==0.12.1
|
43 |
+
# via matplotlib
|
44 |
+
datasets==3.6.0
|
45 |
+
# via gradio-plant-uml (pyproject.toml)
|
46 |
+
dill==0.3.8
|
47 |
+
# via
|
48 |
+
# datasets
|
49 |
+
# multiprocess
|
50 |
+
fastapi==0.115.12
|
51 |
+
# via
|
52 |
+
# gradio-plant-uml (pyproject.toml)
|
53 |
+
# gradio
|
54 |
+
ffmpy==0.6.0
|
55 |
+
# via gradio
|
56 |
+
filelock==3.18.0
|
57 |
+
# via
|
58 |
+
# datasets
|
59 |
+
# huggingface-hub
|
60 |
+
# transformers
|
61 |
+
flask==3.1.1
|
62 |
+
# via gradio-plant-uml (pyproject.toml)
|
63 |
+
fonttools==4.58.1
|
64 |
+
# via matplotlib
|
65 |
+
frozenlist==1.6.2
|
66 |
+
# via
|
67 |
+
# aiohttp
|
68 |
+
# aiosignal
|
69 |
+
fsspec==2025.3.0
|
70 |
+
# via
|
71 |
+
# datasets
|
72 |
+
# gradio-client
|
73 |
+
# huggingface-hub
|
74 |
+
gradio==5.33.0
|
75 |
+
# via gradio-plant-uml (pyproject.toml)
|
76 |
+
gradio-client==1.10.2
|
77 |
+
# via gradio
|
78 |
+
graphviz==0.20.3
|
79 |
+
# via gradio-plant-uml (pyproject.toml)
|
80 |
+
groovy==0.1.2
|
81 |
+
# via gradio
|
82 |
+
h11==0.16.0
|
83 |
+
# via
|
84 |
+
# httpcore
|
85 |
+
# uvicorn
|
86 |
+
httpcore==1.0.9
|
87 |
+
# via httpx
|
88 |
+
httplib2==0.22.0
|
89 |
+
# via plantuml
|
90 |
+
httpx==0.28.1
|
91 |
+
# via
|
92 |
+
# gradio
|
93 |
+
# gradio-client
|
94 |
+
# mcp
|
95 |
+
# safehttpx
|
96 |
+
httpx-sse==0.4.0
|
97 |
+
# via mcp
|
98 |
+
huggingface-hub==0.32.4
|
99 |
+
# via
|
100 |
+
# datasets
|
101 |
+
# gradio
|
102 |
+
# gradio-client
|
103 |
+
# tokenizers
|
104 |
+
# transformers
|
105 |
+
idna==3.10
|
106 |
+
# via
|
107 |
+
# anyio
|
108 |
+
# httpx
|
109 |
+
# requests
|
110 |
+
# yarl
|
111 |
+
itsdangerous==2.2.0
|
112 |
+
# via flask
|
113 |
+
jinja2==3.1.6
|
114 |
+
# via
|
115 |
+
# flask
|
116 |
+
# gradio
|
117 |
+
# pyan3
|
118 |
+
joblib==1.5.1
|
119 |
+
# via scikit-learn
|
120 |
+
kiwisolver==1.4.8
|
121 |
+
# via matplotlib
|
122 |
+
markdown-it-py==3.0.0
|
123 |
+
# via rich
|
124 |
+
markupsafe==3.0.2
|
125 |
+
# via
|
126 |
+
# flask
|
127 |
+
# gradio
|
128 |
+
# jinja2
|
129 |
+
# werkzeug
|
130 |
+
matplotlib==3.10.3
|
131 |
+
# via gradio-plant-uml (pyproject.toml)
|
132 |
+
mcp==1.9.0
|
133 |
+
# via gradio
|
134 |
+
mdurl==0.1.2
|
135 |
+
# via markdown-it-py
|
136 |
+
multidict==6.4.4
|
137 |
+
# via
|
138 |
+
# aiohttp
|
139 |
+
# yarl
|
140 |
+
multiprocess==0.70.16
|
141 |
+
# via datasets
|
142 |
+
numpy==2.2.6
|
143 |
+
# via
|
144 |
+
# gradio-plant-uml (pyproject.toml)
|
145 |
+
# contourpy
|
146 |
+
# datasets
|
147 |
+
# gradio
|
148 |
+
# matplotlib
|
149 |
+
# pandas
|
150 |
+
# scikit-learn
|
151 |
+
# scipy
|
152 |
+
# transformers
|
153 |
+
orjson==3.10.18
|
154 |
+
# via gradio
|
155 |
+
packaging==25.0
|
156 |
+
# via
|
157 |
+
# datasets
|
158 |
+
# gradio
|
159 |
+
# gradio-client
|
160 |
+
# huggingface-hub
|
161 |
+
# matplotlib
|
162 |
+
# transformers
|
163 |
+
pandas==2.3.0
|
164 |
+
# via
|
165 |
+
# gradio-plant-uml (pyproject.toml)
|
166 |
+
# datasets
|
167 |
+
# gradio
|
168 |
+
pillow==11.2.1
|
169 |
+
# via
|
170 |
+
# gradio
|
171 |
+
# matplotlib
|
172 |
+
plantuml==0.3.0
|
173 |
+
# via gradio-plant-uml (pyproject.toml)
|
174 |
+
polars==1.30.0
|
175 |
+
# via gradio-plant-uml (pyproject.toml)
|
176 |
+
propcache==0.3.1
|
177 |
+
# via
|
178 |
+
# aiohttp
|
179 |
+
# yarl
|
180 |
+
py2puml==0.10.0
|
181 |
+
# via gradio-plant-uml (pyproject.toml)
|
182 |
+
pyan3==1.2.0
|
183 |
+
# via gradio-plant-uml (pyproject.toml)
|
184 |
+
pyarrow==20.0.0
|
185 |
+
# via datasets
|
186 |
+
pydantic==2.11.5
|
187 |
+
# via
|
188 |
+
# fastapi
|
189 |
+
# gradio
|
190 |
+
# mcp
|
191 |
+
# pydantic-settings
|
192 |
+
pydantic-core==2.33.2
|
193 |
+
# via pydantic
|
194 |
+
pydantic-settings==2.9.1
|
195 |
+
# via mcp
|
196 |
+
pydub==0.25.1
|
197 |
+
# via gradio
|
198 |
+
pygments==2.19.1
|
199 |
+
# via rich
|
200 |
+
pyparsing==3.2.3
|
201 |
+
# via
|
202 |
+
# httplib2
|
203 |
+
# matplotlib
|
204 |
+
python-dateutil==2.9.0.post0
|
205 |
+
# via
|
206 |
+
# matplotlib
|
207 |
+
# pandas
|
208 |
+
python-dotenv==1.1.0
|
209 |
+
# via pydantic-settings
|
210 |
+
python-multipart==0.0.20
|
211 |
+
# via
|
212 |
+
# gradio
|
213 |
+
# mcp
|
214 |
+
pytz==2025.2
|
215 |
+
# via pandas
|
216 |
+
pyyaml==6.0.2
|
217 |
+
# via
|
218 |
+
# datasets
|
219 |
+
# gradio
|
220 |
+
# huggingface-hub
|
221 |
+
# transformers
|
222 |
+
regex==2024.11.6
|
223 |
+
# via transformers
|
224 |
+
requests==2.32.3
|
225 |
+
# via
|
226 |
+
# gradio-plant-uml (pyproject.toml)
|
227 |
+
# datasets
|
228 |
+
# huggingface-hub
|
229 |
+
# transformers
|
230 |
+
rich==14.0.0
|
231 |
+
# via typer
|
232 |
+
ruff==0.11.13
|
233 |
+
# via gradio
|
234 |
+
safehttpx==0.1.6
|
235 |
+
# via gradio
|
236 |
+
safetensors==0.5.3
|
237 |
+
# via transformers
|
238 |
+
scikit-learn==1.7.0
|
239 |
+
# via gradio-plant-uml (pyproject.toml)
|
240 |
+
scipy==1.15.3
|
241 |
+
# via scikit-learn
|
242 |
+
semantic-version==2.10.0
|
243 |
+
# via gradio
|
244 |
+
shellingham==1.5.4
|
245 |
+
# via typer
|
246 |
+
six==1.17.0
|
247 |
+
# via python-dateutil
|
248 |
+
sniffio==1.3.1
|
249 |
+
# via anyio
|
250 |
+
sse-starlette==2.3.6
|
251 |
+
# via mcp
|
252 |
+
starlette==0.46.2
|
253 |
+
# via
|
254 |
+
# fastapi
|
255 |
+
# gradio
|
256 |
+
# mcp
|
257 |
+
threadpoolctl==3.6.0
|
258 |
+
# via scikit-learn
|
259 |
+
tokenizers==0.21.1
|
260 |
+
# via transformers
|
261 |
+
tomlkit==0.13.3
|
262 |
+
# via gradio
|
263 |
+
tqdm==4.67.1
|
264 |
+
# via
|
265 |
+
# datasets
|
266 |
+
# huggingface-hub
|
267 |
+
# transformers
|
268 |
+
transformers==4.52.4
|
269 |
+
# via gradio-plant-uml (pyproject.toml)
|
270 |
+
typer==0.16.0
|
271 |
+
# via gradio
|
272 |
+
typing-extensions==4.14.0
|
273 |
+
# via
|
274 |
+
# anyio
|
275 |
+
# fastapi
|
276 |
+
# gradio
|
277 |
+
# gradio-client
|
278 |
+
# huggingface-hub
|
279 |
+
# pydantic
|
280 |
+
# pydantic-core
|
281 |
+
# typer
|
282 |
+
# typing-inspection
|
283 |
+
typing-inspection==0.4.1
|
284 |
+
# via
|
285 |
+
# pydantic
|
286 |
+
# pydantic-settings
|
287 |
+
tzdata==2025.2
|
288 |
+
# via pandas
|
289 |
+
urllib3==2.4.0
|
290 |
+
# via requests
|
291 |
+
uvicorn==0.34.3
|
292 |
+
# via
|
293 |
+
# gradio
|
294 |
+
# mcp
|
295 |
+
websockets==15.0.1
|
296 |
+
# via gradio-client
|
297 |
+
werkzeug==3.1.3
|
298 |
+
# via flask
|
299 |
+
xxhash==3.5.0
|
300 |
+
# via datasets
|
301 |
+
yarl==1.20.0
|
302 |
+
# via aiohttp
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|