navred61 commited on
Commit
ef444e4
ยท
1 Parent(s): fc740d5

updated code with working mcp

Browse files
Files changed (3) hide show
  1. app.py +404 -279
  2. utils.py +50 -0
  3. uv.lock +9 -9
app.py CHANGED
@@ -12,6 +12,7 @@ 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"
@@ -19,55 +20,6 @@ if os.name == "nt": # nt == Windows
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]]:
@@ -88,7 +40,7 @@ def generate_call_graph_with_pyan3(
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:
@@ -383,7 +335,7 @@ def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[st
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
@@ -394,7 +346,7 @@ def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[st
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...")
@@ -402,7 +354,7 @@ def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[st
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
 
@@ -451,7 +403,7 @@ def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[st
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
@@ -736,277 +688,450 @@ def get_sample_code(filename: str) -> str:
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=True, # Share the app publicly
1011
- # debug=True, # Enable debug mode for development
1012
- )
 
12
  from plantuml import PlantUML
13
  import pyan
14
  from pathlib import Path
15
+ from utils import setup_testing_space, verify_testing_space, cleanup_testing_space
16
 
17
  if os.name == "nt": # nt == Windows
18
  graphviz_bin = r"C:\\Program Files\\Graphviz\\bin"
 
20
  os.environ["PATH"] += os.pathsep + graphviz_bin
21
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  def generate_call_graph_with_pyan3(
24
  python_code: str, filename: str = "analysis"
25
  ) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:
 
40
  unique_filename = f"{filename}_{timestamp}"
41
 
42
  # Paths
43
+ testing_dir = os.path.join(os.getcwd(), "inputs")
44
  code_file = os.path.join(testing_dir, f"{unique_filename}.py")
45
 
46
  try:
 
335
  unique_filename = f"{filename}_{timestamp}"
336
 
337
  # Paths
338
+ testing_dir = os.path.join(os.getcwd(), "inputs")
339
  code_file = os.path.join(testing_dir, f"{unique_filename}.py")
340
 
341
  # Use PlantUML web service for rendering
 
346
  with open(code_file, "w", encoding="utf-8") as f:
347
  f.write(python_code)
348
 
349
+ print(f"๐Ÿ“ Created temporary file: inputs/{unique_filename}.py")
350
 
351
  # Generate PlantUML content using py2puml (no sys.path manipulation needed)
352
  print(f"๐Ÿ“ Generating PlantUML content...")
 
354
  os.path.join(
355
  testing_dir, unique_filename
356
  ), # path to the .py file (without extension)
357
+ f"inputs.{unique_filename}", # module name
358
  )
359
  puml_content = "".join(puml_content_lines)
360
 
 
403
 
404
 
405
  def analyze_code_structure(python_code: str) -> str:
406
+ """Return a Markdown report with complexity metrics and recommendations.
407
 
408
  Args:
409
  python_code: The Python code to analyze
 
688
  return f.read()
689
 
690
 
691
+ def generate_all_diagrams(
692
+ python_code: str, filename: str = "diagram"
693
+ ) -> Tuple[Optional[str], Optional[str], str]:
694
+ """Generate class diagram, call-graph diagram and analysis in one call.
695
+
696
  Args:
697
  python_code: The Python code to analyze
698
  filename: Base filename for diagrams
699
+
700
  Returns:
701
  Tuple of (uml_diagram_path, call_graph_path, analysis_text)
702
  """
703
  if not python_code.strip():
704
  return None, None, "No code provided for analysis."
705
+
706
  print("๐Ÿš€ Starting comprehensive diagram generation...")
707
+
708
  # Step 1: Generate UML Class Diagram
709
  print("๐Ÿ“Š Step 1/3: Generating UML class diagram...")
710
  uml_diagram_path = generate_diagram(python_code, filename)
711
+
712
  # Step 2: Generate Call Graph
713
  print("๐Ÿ”— Step 2/3: Generating call graph...")
714
  try:
715
  cleanup_testing_space()
716
+ dot_content, call_graph_path, structured_data = generate_call_graph_with_pyan3(
717
+ python_code
718
+ )
719
  except Exception as e:
720
  print(f"โš ๏ธ Call graph generation failed: {e}")
721
  call_graph_path = None
722
+
723
  # Step 3: Generate Analysis
724
  print("๐Ÿ“ˆ Step 3/3: Performing code analysis...")
725
  analysis_text = analyze_code_structure(python_code)
726
+
727
  print("โœ… All diagrams and analysis completed!")
728
+
729
  return uml_diagram_path, call_graph_path, analysis_text
730
 
731
 
732
+ # def generate_class_diagram(python_code: str) -> str:
733
+ # """Create a UML class diagram from Python code and return the PNG path.
734
+
735
+ # Args:
736
+ # python_code (str): Python source to analyse.
737
+
738
+ # Returns:
739
+ # str: Local path to a PNG image with the UML diagram.
740
+ # """
741
+ # result = generate_diagram(python_code)
742
+ # return result if result is not None else ""
743
+
744
+
745
+ # def generate_call_graph_diagram(python_code: str) -> str:
746
+ # """Create a function-call graph diagram (PNG).
747
+
748
+ # Args:
749
+ # python_code (str): Python source to analyse.
750
+
751
+ # Returns:
752
+ # str: Local path to the generated call-graph PNG.
753
+ # """
754
+ # _, png_path, _ = generate_call_graph_with_pyan3(python_code)
755
+ # return png_path or ""
756
+
757
+
758
+
759
+
760
+ # # -----------------------------------------------------------------------------
761
+ # # โถ Four MCPโ€‘exposed Interfaces ------------------------------------------------
762
+ # # -----------------------------------------------------------------------------
763
+ # iface_class = gr.Interface(
764
+ # fn=generate_class_diagram,
765
+ # inputs=gr.Textbox(lines=20, label="Python code"),
766
+ # outputs=gr.Image(label="UML diagram"),
767
+ # api_name="generate_class_diagram",
768
+ # description="Create a UML class diagram (PNG) from Python code.",
769
+ # )
770
+
771
+ # iface_call = gr.Interface(
772
+ # fn=generate_call_graph_diagram,
773
+ # inputs=gr.Textbox(lines=20, label="Python code"),
774
+ # outputs=gr.Image(label="Callโ€‘graph"),
775
+ # api_name="generate_call_graph_diagram",
776
+ # description="Generate a functionโ€‘call graph (PNG) from Python code.",
777
+ # )
778
+
779
+ # iface_analysis = gr.Interface(
780
+ # fn=analyze_code_structure,
781
+ # inputs=gr.Textbox(lines=20, label="Python code"),
782
+ # outputs=gr.Textbox(lines=20, label="Analysis"),
783
+ # api_name="analyze_code_structure",
784
+ # description="Return a Markdown report with complexity metrics.",
785
+ # )
786
+
787
+ # iface_all = gr.Interface(
788
+ # fn=generate_all_diagrams,
789
+ # inputs=[
790
+ # gr.Textbox(lines=20, label="Python code"),
791
+ # gr.Textbox(value="diagram", label="Base filename"),
792
+ # ],
793
+ # outputs=[
794
+ # gr.Image(label="UML diagram"),
795
+ # gr.Image(label="Callโ€‘graph"),
796
+ # gr.Textbox(lines=20, label="Analysis"),
797
+ # ],
798
+ # api_name="generate_all",
799
+ # description="Run class diagram, call graph and analysis in one call.",
800
+ # )
801
+
802
+ # # -----------------------------------------------------------------------------
803
+ # # โท Web UI (Blocks) -----------------------------------------------------------
804
+ # # -----------------------------------------------------------------------------
805
+ # with gr.Blocks(
806
+ # title="Python Diagram Generator MCP Demo",
807
+ # theme=gr.themes.Soft(),
808
+ # css="""
809
+ # .gradio-container { max-width: 1400px !important; }
810
+ # .code-input { font-family: 'Inter', monospace !important; }
811
+ # """,
812
+ # ) as demo:
813
+
814
+ # # -- Header ----------------------------------------------------------------
815
+ # gr.Markdown(
816
+ # """
817
+ # # ๐Ÿ Python UML Diagram Generator & MCP Server
818
+
819
+ # **Dual Functionality**
820
+ # 1. ๐Ÿ–ฅ๏ธ *Web Interface* โ€“ generate UML & callโ€‘graph diagrams + analysis
821
+ # 2. ๐Ÿค– *MCP Server* โ€“ four tools ready for assistants such as Claude Desktop or Cursor
822
+ # """
823
+ # )
824
+
825
+ # # -- Main TAB --------------------------------------------------------------
826
+ # with gr.Tab("๐ŸŽจ Diagram Generator"):
827
+ # with gr.Row():
828
+ # # ---------- Left column โ€“ inputs -----------------------------------
829
+ # with gr.Column(scale=1):
830
+ # gr.Markdown("### Input")
831
+
832
+ # example_files = list_example_files()
833
+ # example_dropdown = gr.Dropdown(
834
+ # label="Choose Example",
835
+ # choices=example_files,
836
+ # value=example_files[0] if example_files else None,
837
+ # )
838
+
839
+ # code_input = gr.Textbox(
840
+ # label="Python Code",
841
+ # placeholder="Paste your Python code hereโ€ฆ",
842
+ # lines=20,
843
+ # max_lines=35,
844
+ # value=get_sample_code(example_files[0]) if example_files else "",
845
+ # elem_classes=["code-input"],
846
+ # )
847
+
848
+ # filename_input = gr.Textbox(
849
+ # label="Diagram Name",
850
+ # value="my_diagram",
851
+ # placeholder="Enter a name for your diagram",
852
+ # )
853
+
854
+ # generate_diagrams_btn = gr.Button(
855
+ # "๐Ÿ”„ Generate Diagrams", variant="primary", size="lg"
856
+ # )
857
+
858
+ # # ---------- Right column โ€“ outputs ---------------------------------
859
+ # with gr.Column(scale=1):
860
+ # gr.Markdown("### Generated UML Class Diagram")
861
+ # uml_diagram_output = gr.Image(
862
+ # label="UML Class Diagram", show_download_button=True, height=300
863
+ # )
864
+
865
+ # gr.Markdown("### Generated Call Graph Diagram")
866
+ # call_graph_output = gr.Image(
867
+ # label="Function Call Graph", show_download_button=True, height=300
868
+ # )
869
+
870
+ # # ---------- Analysis row ----------------------------------------------
871
+ # gr.Markdown("### Code Analysis")
872
+ # analysis_output = gr.Textbox(
873
+ # label="Comprehensive Code Analysis",
874
+ # lines=15,
875
+ # max_lines=25,
876
+ # interactive=False,
877
+ # show_copy_button=True,
878
+ # )
879
+
880
+ # # -- About TAB -------------------------------------------------------------
881
+ # # with gr.Tab("โ„น๏ธ About & Help"):
882
+ # # gr.Markdown(open("ABOUT.md", "r", encoding="utf-8").read()) # keep long text external
883
+
884
+ # # -- Hidden TabbedInterface to register MCP tools -------------------------
885
+ # # (render=False avoids GUI duplication)
886
+ # # gr.TabbedInterface(
887
+ # # [iface_class, iface_call, iface_analysis, iface_all],
888
+ # # ["Class diagram", "Call graph", "Analysis", "Allโ€‘inโ€‘one"],
889
+ # # # render=False,
890
+ # # )
891
+
892
+ # # -------------------------------------------------------------------------
893
+ # # Event handlers -----------------------------------------------------------
894
+ # # -------------------------------------------------------------------------
895
+
896
+ # def _load_example(example_filename: str):
897
+ # """Load bundled example code into the textbox."""
898
+ # return get_sample_code(example_filename)
899
+
900
+ # example_dropdown.change(_load_example, example_dropdown, code_input)
901
+
902
+ # generate_diagrams_btn.click(
903
+ # fn=generate_all_diagrams,
904
+ # inputs=[code_input, filename_input],
905
+ # outputs=[uml_diagram_output, call_graph_output, analysis_output],
906
+ # show_progress=True,
907
+ # )
908
+
909
+ # # code_input.change(
910
+ # # fn=analyze_code_structure,
911
+ # # inputs=code_input,
912
+ # # outputs=analysis_output,
913
+ # # show_progress=False,
914
+ # # )
915
+
916
+ # # -----------------------------------------------------------------------------
917
+ # # โธ Launch -------------------------------------------------------------------
918
+ # # -----------------------------------------------------------------------------
919
+ # if __name__ == "__main__":
920
+ # setup_testing_space() # create a persistent working dir if needed
921
+
922
+ # demo.launch(
923
+ # mcp_server=True, # Enable MCP endpoints (/gradio_api/mcp/*)
924
+ # show_api=False, # Expose ONLY the 4 Interfaces as tools
925
+ # show_error=True, # Display exceptions in the UI
926
+ # debug=True, # Verbose server logs
927
+ # )
928
+
929
+
930
+
931
+ # =============================================================================
932
+ # โถ ย Wrapper functions for diagram and analysis generation
933
+ # These will be connected to the UI buttons and the MCP interfaces.
934
+ # =============================================================================
935
+
936
+ def generate_class_diagram_only(python_code: str) -> Optional[str]:
937
+ """Generates just the UML class diagram."""
938
+ if not python_code.strip():
939
+ gr.Warning("Input code is empty!")
940
+ return None
941
+ return generate_diagram(python_code)
942
+
943
+ def generate_call_graph_only(python_code: str) -> Optional[str]:
944
+ """Generates just the call graph diagram."""
945
+ if not python_code.strip():
946
+ gr.Warning("Input code is empty!")
947
+ return None
948
+ _, png_path, _ = generate_call_graph_with_pyan3(python_code)
949
+ return png_path
950
+
951
+ def analyze_code_only(python_code: str) -> str:
952
+ """Generates just the code analysis report."""
953
+ if not python_code.strip():
954
+ gr.Warning("Input code is empty!")
955
+ return "No code provided to analyze."
956
+ return analyze_code_structure(python_code)
957
+
958
+ def generate_all_outputs(python_code: str) -> Tuple[Optional[str], Optional[str], str]:
959
+ """Generates all three outputs: UML diagram, call graph, and analysis."""
960
+ if not python_code.strip():
961
+ gr.Warning("Input code is empty!")
962
+ return None, None, "No code provided to analyze."
963
+
964
+ print("๐Ÿš€ Starting comprehensive generation...")
965
+ uml_path = generate_diagram(python_code)
966
+ _, call_graph_path, _ = generate_call_graph_with_pyan3(python_code)
967
+ analysis_text = analyze_code_structure(python_code)
968
+ print("โœ… All outputs generated!")
969
+
970
+ return uml_path, call_graph_path, analysis_text
971
+
972
+ # =============================================================================
973
+ # โท ย Four MCP-exposed Interfaces
974
+ # These are NOT rendered in the UI but are exposed as tools for agents.
975
+ # =============================================================================
976
+
977
+ iface_class = gr.Interface(
978
+ fn=generate_class_diagram_only,
979
+ inputs=gr.Textbox(lines=20, label="Python code"),
980
+ outputs=gr.Image(label="UML diagram"),
981
+ api_name="generate_class_diagram",
982
+ description="Create a UML class diagram (PNG) from Python code.",
983
+ )
984
+
985
+ iface_call = gr.Interface(
986
+ fn=generate_call_graph_only,
987
+ inputs=gr.Textbox(lines=20, label="Python code"),
988
+ outputs=gr.Image(label="Callโ€‘graph"),
989
+ api_name="generate_call_graph_diagram",
990
+ description="Generate a functionโ€‘call graph (PNG) from Python code.",
991
+ )
992
+
993
+ iface_analysis = gr.Interface(
994
+ fn=analyze_code_only,
995
+ inputs=gr.Textbox(lines=20, label="Python code"),
996
+ outputs=gr.Markdown(label="Analysis"),
997
+ api_name="analyze_code_structure",
998
+ description="Return a Markdown report with complexity metrics.",
999
+ )
1000
+
1001
+ iface_all = gr.Interface(
1002
+ fn=generate_all_outputs,
1003
+ inputs=gr.Textbox(lines=20, label="Python code"),
1004
+ outputs=[
1005
+ gr.Image(label="UML diagram"),
1006
+ gr.Image(label="Callโ€‘graph"),
1007
+ gr.Markdown(label="Analysis"),
1008
+ ],
1009
+ api_name="generate_all",
1010
+ description="Run class diagram, call graph and analysis in one call.",
1011
+ )
1012
+
1013
+
1014
+ # =============================================================================
1015
+ # โธ ย The Cleaned-up Web UI (using gr.Blocks)
1016
+ # =============================================================================
1017
  with gr.Blocks(
1018
+ title="Python Code Visualizer & Analyzer",
1019
+ theme=gr.themes.Soft(primary_hue="blue"),
1020
+ css=""" .gradio-container { max-width: 1400px !important; } """,
 
 
 
 
 
 
 
1021
  ) as demo:
1022
+ # iface_class = gr.Interface(fn=generate_class_diagram_only, inputs=gr.Textbox(), outputs=gr.Image(), api_name="generate_class_diagram", description="Create a UML class diagram (PNG) from Python code.", visible =False)
1023
+ # iface_call = gr.Interface(fn=generate_call_graph_only, inputs=gr.Textbox(), outputs=gr.Image(), api_name="generate_call_graph_diagram", description="Generate a functionโ€‘call graph (PNG) from Python code.", visible =False)
1024
+ # iface_analysis = gr.Interface(fn=analyze_code_only, inputs=gr.Textbox(), outputs=gr.Markdown(), api_name="analyze_code_structure", description="Return a Markdown report with complexity metrics.", visible =False)
1025
+ # iface_all = gr.Interface(fn=generate_all_outputs, inputs=gr.Textbox(), outputs=[gr.Image(), gr.Image(), gr.Markdown()], api_name="generate_all", description="Run class diagram, call graph and analysis in one call.", visible =False)
1026
  gr.Markdown(
1027
  """
1028
+ # ๐Ÿ Python Code Visualizer & Analyzer
1029
+ **Enter Python code, then choose an action to generate diagrams and analysis.**
1030
+ This app also functions as an MCP Server, exposing four tools for AI assistants.
1031
+ """
 
 
 
 
1032
  )
1033
 
1034
+ with gr.Row():
1035
+ # ---------- Left column โ€“ inputs and actions -----------------------------------
1036
+ with gr.Column(scale=2):
1037
+ gr.Markdown("### 1. Input Code")
1038
+
1039
+ example_files = list_example_files()
1040
+ print(f"๐Ÿ” Found {len(example_files)} example files: {example_files}")
1041
+ if example_files:
1042
  example_dropdown = gr.Dropdown(
1043
+ label="Load an Example",
1044
  choices=example_files,
1045
+ value=example_files[0],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
  )
1047
+ # initial_code = get_sample_code(example_files[0])
1048
+ # initial_code = "# Paste your Python code here\n\nclass MyClass:\n pass"
1049
+ initial_code = "Choose an example file from dropdown or paster your python code here "
1050
+ # initial_code = get_sample_code("simple_class.py")
1051
+ else:
1052
+ initial_code = "# Paste your Python code here\n\nclass MyClass:\n pass"
1053
 
1054
+ code_input = gr.Textbox(
1055
+ label="Python Code",
1056
+ placeholder="Paste your Python code hereโ€ฆ",
 
 
 
1057
  lines=15,
1058
+ max_lines=200,
1059
+ value=initial_code,
1060
+ elem_classes=["code-input"],
1061
  )
1062
 
1063
+ gr.Markdown("### 2. Choose an Action")
1064
+ with gr.Row():
1065
+ class_btn = gr.Button("๐Ÿ–ผ๏ธ Generate Class Diagram")
1066
+ call_graph_btn = gr.Button("๐Ÿ”— Generate Call Graph")
1067
+ analyze_btn = gr.Button("๐Ÿ“ˆ Analyze Code")
1068
+ all_btn = gr.Button("โœจ Generate All", variant="primary")
1069
+
1070
+ # ---------- Right column โ€“ outputs ---------------------------------
1071
+ with gr.Column(scale=3):
1072
+ gr.Markdown("### 3. Results")
1073
+ with gr.Tabs():
1074
+ with gr.TabItem("UML Class Diagram"):
1075
+ uml_output = gr.Image(label="UML Class Diagram", show_download_button=True, interactive=False)
1076
+ with gr.TabItem("Function Call Graph"):
1077
+ call_graph_output = gr.Image(label="Function Call Graph", show_download_button=True, interactive=False)
1078
+ with gr.TabItem("Code Analysis Report"):
1079
+ analysis_output = gr.Markdown(label="Comprehensive Code Analysis", elem_classes=["analysis-output"])
1080
+
1081
+ # -------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1082
  # Event handlers
1083
+ # -------------------------------------------------------------------------
1084
+
1085
+ # Handler to load example code when dropdown changes
1086
+ if example_files:
1087
+ def _load_example(example_filename: str):
1088
+ return get_sample_code(example_filename)
1089
+ example_dropdown.change(fn=_load_example, inputs=example_dropdown, outputs=code_input, api_name = False)
1090
+
1091
+ # Handlers for the four action buttons
1092
+ # class_btn.click(
1093
+ # fn=generate_class_diagram_only,
1094
+ # inputs=[code_input],
1095
+ # outputs=[uml_output],
1096
+ # api_name=False # Prevents this from creating a duplicate API endpoint
1097
+ # )
1098
+ class_btn.click(
1099
+ fn=generate_class_diagram_only,
1100
+ inputs=[code_input],
1101
+ outputs=[uml_output],
1102
+ # api_name=False # Prevents this from creating a duplicate API endpoint
1103
+ )
1104
+
1105
+ call_graph_btn.click(
1106
+ fn=generate_call_graph_only,
1107
+ inputs=[code_input],
1108
+ outputs=[call_graph_output],
1109
+ # api_name=False
1110
  )
1111
 
1112
+ analyze_btn.click(
1113
+ fn=analyze_code_only,
1114
+ inputs=[code_input],
1115
+ outputs=[analysis_output],
1116
+ # api_name=False
1117
  )
1118
 
1119
+ all_btn.click(
1120
+ fn=generate_all_outputs,
1121
+ inputs=[code_input],
1122
+ outputs=[uml_output, call_graph_output, analysis_output],
1123
+ # api_name=False
1124
  )
1125
 
1126
+ # =============================================================================
1127
+ # โน ย Launch the App and MCP Server
1128
+ # =============================================================================
1129
  if __name__ == "__main__":
1130
+ setup_testing_space() # Create a persistent working dir if needed
 
1131
 
1132
  demo.launch(
1133
+ mcp_server=True, # Enable MCP endpoints (/gradio_api/mcp/*)
1134
+ show_api=True, # Expose ONLY the 4 Interfaces as tools
1135
+ show_error=True, # Display exceptions in the UI
1136
+ debug=True, # Verbose server logs
1137
+ )
 
utils.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+
4
+ def setup_testing_space():
5
+ """Create persistent testing_space directory and __init__.py at startup."""
6
+ testing_dir = os.path.join(os.getcwd(), "inputs")
7
+ os.makedirs(testing_dir, exist_ok=True)
8
+
9
+ init_file = os.path.join(testing_dir, "__init__.py")
10
+ if not os.path.exists(init_file):
11
+ with open(init_file, "w", encoding="utf-8") as f:
12
+ f.write("# Testing space for py2puml analysis\n")
13
+ print("๐Ÿ“ Created testing_space directory and __init__.py")
14
+ else:
15
+ print("๐Ÿ”„ testing_space directory already exists")
16
+
17
+
18
+ def cleanup_testing_space():
19
+ """Remove all .py files except __init__.py from testing_space."""
20
+ testing_dir = os.path.join(os.getcwd(), "inputs")
21
+ if not os.path.exists(testing_dir):
22
+ print("โš ๏ธ testing_space directory not found, creating it...")
23
+ setup_testing_space()
24
+ return
25
+
26
+ # Clean up any leftover .py files (keep only __init__.py)
27
+ files_removed = 0
28
+ for file in os.listdir(testing_dir):
29
+ if file.endswith(".py") and file != "__init__.py":
30
+ file_path = os.path.join(testing_dir, file)
31
+ try:
32
+ os.remove(file_path)
33
+ files_removed += 1
34
+ except Exception as e:
35
+ print(f"โš ๏ธ Could not remove {file}: {e}")
36
+
37
+ if files_removed > 0:
38
+ print(f"๐Ÿงน Cleaned up {files_removed} leftover .py files from testing_space")
39
+
40
+
41
+ def verify_testing_space():
42
+ """Verify testing_space contains only __init__.py."""
43
+ testing_dir = os.path.join(os.getcwd(), "inputs")
44
+ if not os.path.exists(testing_dir):
45
+ return False
46
+
47
+ files = os.listdir(testing_dir)
48
+ expected_files = ["__init__.py"]
49
+
50
+ return files == expected_files
uv.lock CHANGED
@@ -466,7 +466,7 @@ http = [
466
 
467
  [[package]]
468
  name = "gradio"
469
- version = "5.33.0"
470
  source = { registry = "https://pypi.org/simple" }
471
  dependencies = [
472
  { name = "aiofiles" },
@@ -499,9 +499,9 @@ dependencies = [
499
  { name = "urllib3", marker = "sys_platform == 'emscripten'" },
500
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
501
  ]
502
- sdist = { url = "https://files.pythonhosted.org/packages/b0/97/908eb543fbce7c69250d6fbe87b6ccf4ce397d31bceb360b40316357c68c/gradio-5.33.0.tar.gz", hash = "sha256:0cba3a1596fda6cb0048dd7ddc2d57e6238a047c0df9dee5a4a0e5c2a74e8e50", size = 64888401, upload-time = "2025-06-04T21:47:57.431Z" }
503
  wheels = [
504
- { url = "https://files.pythonhosted.org/packages/4f/c3/c9b09b8d7efd63d83a9c8d9c53b02e1b77238e14305a7ee561e0a8990465/gradio-5.33.0-py3-none-any.whl", hash = "sha256:165e412e1510a22471901744722f99a52cb56465a7e9609f1e400cac9999e9d8", size = 54208887, upload-time = "2025-06-04T21:47:52.002Z" },
505
  ]
506
 
507
  [package.optional-dependencies]
@@ -512,7 +512,7 @@ mcp = [
512
 
513
  [[package]]
514
  name = "gradio-client"
515
- version = "1.10.2"
516
  source = { registry = "https://pypi.org/simple" }
517
  dependencies = [
518
  { name = "fsspec" },
@@ -522,9 +522,9 @@ dependencies = [
522
  { name = "typing-extensions" },
523
  { name = "websockets" },
524
  ]
525
- sdist = { url = "https://files.pythonhosted.org/packages/d2/86/6684afe8691b024200fdc8983924f04b5f76bb401b9c700e5752a23595a0/gradio_client-1.10.2.tar.gz", hash = "sha256:bf71ba95714784fa77ca0cfb20189ad91c55e563c2dc71722d023a97f1815d7f", size = 321294, upload-time = "2025-05-30T13:59:55.756Z" }
526
  wheels = [
527
- { url = "https://files.pythonhosted.org/packages/9b/1b/b372308c263379ae3ebc440512432979458330113bdee26cef86c89bf48e/gradio_client-1.10.2-py3-none-any.whl", hash = "sha256:6de67b6224123d264c7887caa0586b2a9e2c369ec32ca38927cf8a841694edcd", size = 323311, upload-time = "2025-05-30T13:59:54.555Z" },
528
  ]
529
 
530
  [[package]]
@@ -857,7 +857,7 @@ wheels = [
857
 
858
  [[package]]
859
  name = "mcp"
860
- version = "1.9.0"
861
  source = { registry = "https://pypi.org/simple" }
862
  dependencies = [
863
  { name = "anyio" },
@@ -870,9 +870,9 @@ dependencies = [
870
  { name = "starlette" },
871
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
872
  ]
873
- sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432, upload-time = "2025-05-15T18:51:06.615Z" }
874
  wheels = [
875
- { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082, upload-time = "2025-05-15T18:51:04.916Z" },
876
  ]
877
 
878
  [[package]]
 
466
 
467
  [[package]]
468
  name = "gradio"
469
+ version = "5.33.1"
470
  source = { registry = "https://pypi.org/simple" }
471
  dependencies = [
472
  { name = "aiofiles" },
 
499
  { name = "urllib3", marker = "sys_platform == 'emscripten'" },
500
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
501
  ]
502
+ sdist = { url = "https://files.pythonhosted.org/packages/5d/b9/1b59586d91dc5fa083c067789c06a6fbd288242351b506f5af26109188cb/gradio-5.33.1.tar.gz", hash = "sha256:f74c737aa92fc02b4d7dca7e50ee13ddce548aa16c9fcbe907ceabf93722f94d", size = 64908955, upload-time = "2025-06-10T00:51:35.594Z" }
503
  wheels = [
504
+ { url = "https://files.pythonhosted.org/packages/8a/de/209775586904b55395e046b0e201d74a0bf91aa06fd087fb0fdcbaa39134/gradio-5.33.1-py3-none-any.whl", hash = "sha256:c4329b04280d62041fbf0113e94fb5c4d20e0555ce1ac69174bf98225350159b", size = 54221598, upload-time = "2025-06-10T00:51:29.237Z" },
505
  ]
506
 
507
  [package.optional-dependencies]
 
512
 
513
  [[package]]
514
  name = "gradio-client"
515
+ version = "1.10.3"
516
  source = { registry = "https://pypi.org/simple" }
517
  dependencies = [
518
  { name = "fsspec" },
 
522
  { name = "typing-extensions" },
523
  { name = "websockets" },
524
  ]
525
+ sdist = { url = "https://files.pythonhosted.org/packages/0b/91/a31536da8fd18ed1c1d5b05929b7291a7bfe5963dfcd37a20a42fc2194f0/gradio_client-1.10.3.tar.gz", hash = "sha256:9e99b88e47f05dc3b68e40a3f3f83819f8d0ddcd43466ad385fe42e137825774", size = 321637, upload-time = "2025-06-10T00:51:46.408Z" }
526
  wheels = [
527
+ { url = "https://files.pythonhosted.org/packages/ea/72/1e76abc821f8efaaeb2e3bd727a6c97bf87c6a9a0ffacfed0647e587824a/gradio_client-1.10.3-py3-none-any.whl", hash = "sha256:941e7f8d9a160f88487e9780a3db2736a40ea2b8b69d53ffdb306e47ef658b76", size = 323599, upload-time = "2025-06-10T00:51:45.204Z" },
528
  ]
529
 
530
  [[package]]
 
857
 
858
  [[package]]
859
  name = "mcp"
860
+ version = "1.9.3"
861
  source = { registry = "https://pypi.org/simple" }
862
  dependencies = [
863
  { name = "anyio" },
 
870
  { name = "starlette" },
871
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
872
  ]
873
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/df/8fefc0c6c7a5c66914763e3ff3893f9a03435628f6625d5e3b0dc45d73db/mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388", size = 333045, upload-time = "2025-06-05T15:48:25.681Z" }
874
  wheels = [
875
+ { url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063, upload-time = "2025-06-05T15:48:24.171Z" },
876
  ]
877
 
878
  [[package]]