#!/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()