2025-01-17 02:19:32 +01:00

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