mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2025-01-31 08:25:24 +00:00
371 lines
12 KiB
Python
371 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Enhanced Python Code Documentation Generator
|
|
|
|
This script creates a PyQt application that generates comprehensive markdown documentation
|
|
from Python source files. It extracts complete information about classes, functions,
|
|
methods, variables, decorators and their associated docstrings.
|
|
|
|
Author: Generated by LoLLMs
|
|
Date: 2025-01-16
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import ast
|
|
import inspect
|
|
from typing import Dict, List, Optional, Tuple, Union, Any
|
|
from PyQt5.QtWidgets import (
|
|
QApplication,
|
|
QMainWindow,
|
|
QWidget,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QPushButton,
|
|
QTextEdit,
|
|
QFileDialog,
|
|
QMessageBox,
|
|
QLabel,
|
|
QStyle
|
|
)
|
|
from PyQt5.QtGui import QFont, QIcon
|
|
from PyQt5.QtCore import Qt
|
|
import sys
|
|
|
|
class CodeParser:
|
|
"""
|
|
A class to parse Python source code and extract comprehensive documentation.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the CodeParser."""
|
|
self.markdown: str = ""
|
|
self.current_class: Optional[str] = None
|
|
|
|
def get_decorator_list(self, node: ast.AST) -> str:
|
|
"""
|
|
Extract decorator information from a node.
|
|
|
|
Args:
|
|
node: AST node containing decorators
|
|
|
|
Returns:
|
|
str: Formatted decorator string
|
|
"""
|
|
decorators = []
|
|
for decorator in node.decorator_list:
|
|
if isinstance(decorator, ast.Name):
|
|
decorators.append(f"@{decorator.id}")
|
|
elif isinstance(decorator, ast.Call):
|
|
if isinstance(decorator.func, ast.Name):
|
|
args = []
|
|
for arg in decorator.args:
|
|
if isinstance(arg, ast.Constant):
|
|
args.append(str(arg.value))
|
|
if args:
|
|
decorators.append(f"@{decorator.func.id}({', '.join(args)})")
|
|
else:
|
|
decorators.append(f"@{decorator.func.id}()")
|
|
return "\n".join(decorators)
|
|
|
|
def get_arguments(self, node: ast.AST) -> str:
|
|
"""
|
|
Extract function arguments information.
|
|
|
|
Args:
|
|
node: AST node containing function arguments
|
|
|
|
Returns:
|
|
str: Formatted argument string
|
|
"""
|
|
args = []
|
|
|
|
# Process arguments
|
|
for arg in node.args.args:
|
|
arg_str = arg.arg
|
|
if hasattr(arg, 'annotation') and arg.annotation is not None:
|
|
if isinstance(arg.annotation, ast.Name):
|
|
arg_str += f": {arg.annotation.id}"
|
|
elif isinstance(arg.annotation, ast.Subscript):
|
|
arg_str += f": {ast.unparse(arg.annotation)}"
|
|
args.append(arg_str)
|
|
|
|
# Process default values
|
|
defaults = node.args.defaults
|
|
if defaults:
|
|
default_offset = len(args) - len(defaults)
|
|
for i, default in enumerate(defaults):
|
|
args[default_offset + i] += f" = {ast.unparse(default)}"
|
|
|
|
# Process variable arguments
|
|
if node.args.vararg:
|
|
args.append(f"*{node.args.vararg.arg}")
|
|
|
|
# Process keyword arguments
|
|
if node.args.kwarg:
|
|
args.append(f"**{node.args.kwarg.arg}")
|
|
|
|
return ", ".join(args)
|
|
|
|
def get_return_annotation(self, node: ast.AST) -> str:
|
|
"""
|
|
Extract return type annotation.
|
|
|
|
Args:
|
|
node: AST node containing return annotation
|
|
|
|
Returns:
|
|
str: Formatted return type string
|
|
"""
|
|
if node.returns:
|
|
return f" -> {ast.unparse(node.returns)}"
|
|
return ""
|
|
|
|
def parse_assign(self, node: ast.AST, level: int) -> None:
|
|
"""
|
|
Parse assignment nodes to extract class/module variables.
|
|
|
|
Args:
|
|
node: AST assignment node
|
|
level: Current nesting level
|
|
"""
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name):
|
|
name = target.id
|
|
value = ast.unparse(node.value)
|
|
if self.current_class:
|
|
self.markdown += f"{'#' * (level + 3)} Class Variable: {name} = {value}\n\n"
|
|
else:
|
|
self.markdown += f"{'#' * (level + 2)} Module Variable: {name} = {value}\n\n"
|
|
|
|
def parse_class_bases(self, node: ast.ClassDef) -> str:
|
|
"""
|
|
Extract class inheritance information.
|
|
|
|
Args:
|
|
node: Class definition node
|
|
|
|
Returns:
|
|
str: Formatted base classes string
|
|
"""
|
|
bases = []
|
|
for base in node.bases:
|
|
if isinstance(base, ast.Name):
|
|
bases.append(base.id)
|
|
elif isinstance(base, ast.Attribute):
|
|
bases.append(ast.unparse(base))
|
|
return ", ".join(bases)
|
|
|
|
def parse_node(self, node: ast.AST, level: int = 0) -> None:
|
|
"""
|
|
Parse an AST node and extract relevant documentation.
|
|
|
|
Args:
|
|
node: The AST node to parse
|
|
level: The current nesting level for markdown formatting
|
|
"""
|
|
# Handle class definitions
|
|
if isinstance(node, ast.ClassDef):
|
|
bases = self.parse_class_bases(node)
|
|
class_decor = self.get_decorator_list(node)
|
|
|
|
if class_decor:
|
|
self.markdown += f"```python\n{class_decor}\n```\n"
|
|
|
|
self.markdown += f"{'#' * (level + 2)} Class: {node.name}"
|
|
if bases:
|
|
self.markdown += f" ({bases})"
|
|
self.markdown += "\n\n"
|
|
|
|
if ast.get_docstring(node):
|
|
self.markdown += f"```python\n{ast.get_docstring(node)}\n```\n\n"
|
|
|
|
# Parse class body
|
|
self.current_class = node.name
|
|
for child in node.body:
|
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign)):
|
|
self.parse_node(child, level + 1)
|
|
self.current_class = None
|
|
|
|
# Handle function definitions
|
|
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
prefix = "Method" if self.current_class else "Function"
|
|
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
|
|
# Get decorators
|
|
decorators = self.get_decorator_list(node)
|
|
if decorators:
|
|
self.markdown += f"```python\n{decorators}\n```\n"
|
|
|
|
# Function signature
|
|
args = self.get_arguments(node)
|
|
returns = self.get_return_annotation(node)
|
|
async_prefix = "async " if is_async else ""
|
|
|
|
self.markdown += f"{'#' * (level + 2)} {prefix}: {async_prefix}{node.name}({args}){returns}\n\n"
|
|
|
|
# Function docstring
|
|
if ast.get_docstring(node):
|
|
self.markdown += f"```python\n{ast.get_docstring(node)}\n```\n\n"
|
|
|
|
# Handle assignments
|
|
elif isinstance(node, ast.Assign):
|
|
self.parse_assign(node, level)
|
|
|
|
def parse_imports(self, tree: ast.AST) -> None:
|
|
"""
|
|
Parse and format import statements.
|
|
|
|
Args:
|
|
tree: AST tree
|
|
"""
|
|
imports = []
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Import):
|
|
for name in node.names:
|
|
imports.append(f"import {name.name}")
|
|
elif isinstance(node, ast.ImportFrom):
|
|
names = ", ".join(name.name for name in node.names)
|
|
imports.append(f"from {node.module} import {names}")
|
|
|
|
if imports:
|
|
self.markdown += "## Imports\n\n```python\n"
|
|
self.markdown += "\n".join(imports)
|
|
self.markdown += "\n```\n\n"
|
|
|
|
def parse_file(self, file_path: Path) -> str:
|
|
"""
|
|
Parse a Python file and generate comprehensive markdown documentation.
|
|
|
|
Args:
|
|
file_path: Path to the Python file
|
|
|
|
Returns:
|
|
str: Generated markdown documentation
|
|
"""
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
source = f.read()
|
|
|
|
self.markdown = f"# Documentation for {file_path.name}\n\n"
|
|
|
|
tree = ast.parse(source)
|
|
|
|
# Get module docstring
|
|
module_doc = ast.get_docstring(tree)
|
|
if module_doc:
|
|
self.markdown += f"## Module Documentation\n\n```python\n{module_doc}\n```\n\n"
|
|
|
|
# Parse imports
|
|
self.parse_imports(tree)
|
|
|
|
# Parse all top-level nodes
|
|
for node in tree.body:
|
|
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign)):
|
|
self.parse_node(node)
|
|
|
|
return self.markdown
|
|
|
|
except Exception as e:
|
|
return f"Error parsing file: {str(e)}"
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""
|
|
Main application window.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the main window."""
|
|
super().__init__()
|
|
self.setWindowTitle("Python Code Documentation Generator")
|
|
self.setMinimumSize(800, 600)
|
|
|
|
# Initialize UI
|
|
self.init_ui()
|
|
|
|
def init_ui(self) -> None:
|
|
"""Initialize the user interface."""
|
|
# Create central widget and layout
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
layout = QVBoxLayout(central_widget)
|
|
|
|
# Create top button bar
|
|
button_layout = QHBoxLayout()
|
|
|
|
# Open file button
|
|
self.open_btn = QPushButton("Open Python File")
|
|
self.open_btn.clicked.connect(self.open_file)
|
|
self.open_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton))
|
|
button_layout.addWidget(self.open_btn)
|
|
|
|
# Copy button
|
|
self.copy_btn = QPushButton("Copy to Clipboard")
|
|
self.copy_btn.clicked.connect(self.copy_to_clipboard)
|
|
self.copy_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
|
|
button_layout.addWidget(self.copy_btn)
|
|
|
|
# Export button
|
|
self.export_btn = QPushButton("Export Markdown")
|
|
self.export_btn.clicked.connect(self.export_markdown)
|
|
self.export_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
|
|
button_layout.addWidget(self.export_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
# Create text display area
|
|
self.text_edit = QTextEdit()
|
|
self.text_edit.setFont(QFont("Consolas", 10))
|
|
self.text_edit.setReadOnly(True)
|
|
layout.addWidget(self.text_edit)
|
|
|
|
# Status bar
|
|
self.statusBar().showMessage("Ready")
|
|
|
|
def open_file(self) -> None:
|
|
"""Handle file opening."""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Select Python File",
|
|
str(Path.home()),
|
|
"Python Files (*.py)"
|
|
)
|
|
|
|
if file_path:
|
|
parser = CodeParser()
|
|
markdown = parser.parse_file(Path(file_path))
|
|
self.text_edit.setPlainText(markdown)
|
|
self.statusBar().showMessage(f"Loaded: {file_path}")
|
|
|
|
def copy_to_clipboard(self) -> None:
|
|
"""Copy the generated markdown to clipboard."""
|
|
QApplication.clipboard().setText(self.text_edit.toPlainText())
|
|
self.statusBar().showMessage("Copied to clipboard!")
|
|
|
|
def export_markdown(self) -> None:
|
|
"""Export the markdown to a file."""
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
self,
|
|
"Save Markdown File",
|
|
str(Path.home()),
|
|
"Markdown Files (*.md)"
|
|
)
|
|
|
|
if file_path:
|
|
try:
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
f.write(self.text_edit.toPlainText())
|
|
self.statusBar().showMessage(f"Saved to: {file_path}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")
|
|
|
|
def main():
|
|
"""Main application entry point."""
|
|
app = QApplication(sys.argv)
|
|
app.setStyle('Fusion')
|
|
window = MainWindow()
|
|
window.show()
|
|
sys.exit(app.exec_())
|
|
|
|
if __name__ == "__main__":
|
|
main()
|