parent
e4f3044d95
commit
b2aa5dd6a6
1 changed files with 610 additions and 0 deletions
@ -0,0 +1,610 @@ |
||||
#!/usr/bin/env python3 |
||||
""" |
||||
Python Documentation Generator using Ollama LLM |
||||
Automatically generates comprehensive markdown documentation for Python projects. |
||||
""" |
||||
|
||||
import os |
||||
import ast |
||||
import json |
||||
import argparse |
||||
import subprocess |
||||
import sys |
||||
from pathlib import Path |
||||
from typing import Dict, List, Set, Tuple, Optional |
||||
import requests |
||||
import re |
||||
from urllib.parse import quote |
||||
|
||||
class PythonAnalyzer: |
||||
"""Analyzes Python files to extract structural information.""" |
||||
|
||||
def __init__(self): |
||||
self.imports = set() |
||||
self.classes = [] |
||||
self.functions = [] |
||||
self.constants = [] |
||||
self.module_docstring = None |
||||
|
||||
def analyze_file(self, file_path: str) -> Dict: |
||||
"""Analyze a Python file and extract its structure.""" |
||||
try: |
||||
with open(file_path, 'r', encoding='utf-8') as f: |
||||
content = f.read() |
||||
|
||||
tree = ast.parse(content) |
||||
|
||||
# Reset for each file |
||||
self.imports = set() |
||||
self.classes = [] |
||||
self.functions = [] |
||||
self.constants = [] |
||||
self.module_docstring = ast.get_docstring(tree) |
||||
|
||||
for node in ast.walk(tree): |
||||
if isinstance(node, ast.Import): |
||||
for alias in node.names: |
||||
self.imports.add(alias.name) |
||||
elif isinstance(node, ast.ImportFrom): |
||||
module = node.module or "" |
||||
for alias in node.names: |
||||
self.imports.add(f"{module}.{alias.name}") |
||||
elif isinstance(node, ast.ClassDef): |
||||
self.classes.append({ |
||||
'name': node.name, |
||||
'bases': [ast.unparse(base) for base in node.bases], |
||||
'docstring': ast.get_docstring(node), |
||||
'methods': [n.name for n in node.body if isinstance(n, ast.FunctionDef)], |
||||
'lineno': node.lineno |
||||
}) |
||||
elif isinstance(node, ast.FunctionDef): |
||||
# Only top-level functions (not methods) |
||||
parent_classes = [n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)] |
||||
is_method = False |
||||
for cls in parent_classes: |
||||
if hasattr(cls, 'body') and node in cls.body: |
||||
is_method = True |
||||
break |
||||
|
||||
if not is_method: |
||||
self.functions.append({ |
||||
'name': node.name, |
||||
'args': [arg.arg for arg in node.args.args], |
||||
'docstring': ast.get_docstring(node), |
||||
'lineno': node.lineno, |
||||
'returns': ast.unparse(node.returns) if node.returns else None |
||||
}) |
||||
elif isinstance(node, ast.Assign): |
||||
# Top-level constants (ALL_CAPS variables) |
||||
for target in node.targets: |
||||
if isinstance(target, ast.Name) and target.id.isupper(): |
||||
self.constants.append({ |
||||
'name': target.id, |
||||
'value': ast.unparse(node.value), |
||||
'lineno': node.lineno |
||||
}) |
||||
|
||||
return { |
||||
'file_path': file_path, |
||||
'content': content, |
||||
'module_docstring': self.module_docstring, |
||||
'imports': list(self.imports), |
||||
'classes': self.classes, |
||||
'functions': self.functions, |
||||
'constants': self.constants, |
||||
'lines_of_code': len(content.splitlines()) |
||||
} |
||||
|
||||
except Exception as e: |
||||
print(f"Error analyzing {file_path}: {e}") |
||||
return None |
||||
|
||||
class OllamaDocGenerator: |
||||
"""Generates documentation using Ollama LLM.""" |
||||
|
||||
def __init__(self, model_name: str = "deepseek-r1:latest", ollama_url: str = "http://localhost:11434", thinking: bool = False): |
||||
self.model_name = model_name |
||||
self.ollama_url = ollama_url |
||||
self.session = requests.Session() |
||||
self.thinking = thinking |
||||
|
||||
def check_ollama_connection(self) -> bool: |
||||
"""Check if Ollama is running and accessible.""" |
||||
try: |
||||
response = self.session.get(f"{self.ollama_url}/api/tags") |
||||
return response.status_code == 200 |
||||
except requests.exceptions.RequestException: |
||||
return False |
||||
|
||||
def check_model_availability(self) -> bool: |
||||
"""Check if the specified model is available.""" |
||||
try: |
||||
response = self.session.get(f"{self.ollama_url}/api/tags") |
||||
if response.status_code == 200: |
||||
models = response.json().get('models', []) |
||||
return any(model['name'].startswith(self.model_name) for model in models) |
||||
return False |
||||
except requests.exceptions.RequestException: |
||||
return False |
||||
|
||||
def generate_documentation(self, file_analysis: Dict, project_context: Dict) -> str: |
||||
"""Generate documentation for a single Python file.""" |
||||
|
||||
# Create comprehensive prompt with context |
||||
prompt = self.create_documentation_prompt(file_analysis, project_context) |
||||
|
||||
# Check if this is a thinking model (o1-like models) |
||||
is_thinking_model = self.thinking |
||||
try: |
||||
if is_thinking_model: |
||||
print("Thinking model chosen") |
||||
# For thinking models, use chat format and handle thinking tokens |
||||
response = self.session.post( |
||||
f"{self.ollama_url}/api/chat", |
||||
json={ |
||||
"model": self.model_name, |
||||
"messages": [ |
||||
{ |
||||
"role": "user", |
||||
"content": prompt |
||||
} |
||||
], |
||||
"stream": False, |
||||
"options": { |
||||
"temperature": 0.1, |
||||
"top_p": 0.9, |
||||
} |
||||
}, |
||||
timeout=600 # 10 minute timeout for thinking models |
||||
) |
||||
|
||||
if response.status_code == 200: |
||||
result = response.json() |
||||
message = result.get('message', {}) |
||||
content = message.get('content', '') |
||||
# Parse and display thinking process |
||||
thinking_content, final_answer = self.parse_thinking_response(content) |
||||
|
||||
if thinking_content: |
||||
print(f" 🧠 Model thinking process:") |
||||
print(f" {thinking_content[:200]}..." if len(thinking_content) > 200 else f" {thinking_content}") |
||||
|
||||
return final_answer if final_answer else content |
||||
else: |
||||
print(f"Error generating documentation: {response.status_code}") |
||||
return None |
||||
else: |
||||
print("None thinking model chosen") |
||||
# Standard generation for regular models |
||||
response = self.session.post( |
||||
f"{self.ollama_url}/api/generate", |
||||
json={ |
||||
"model": self.model_name, |
||||
"prompt": prompt, |
||||
"stream": False, |
||||
"think": False, |
||||
"options": { |
||||
"temperature": 0.1, |
||||
"top_p": 0.9, |
||||
} |
||||
}, |
||||
timeout=300 # 5 minute timeout |
||||
) |
||||
|
||||
if response.status_code == 200: |
||||
return response.json()['response'] |
||||
else: |
||||
print(f"Error generating documentation: {response.status_code}") |
||||
return None |
||||
|
||||
except requests.exceptions.RequestException as e: |
||||
print(f"Error communicating with Ollama: {e}") |
||||
return None |
||||
|
||||
def parse_thinking_response(self, content: str) -> Tuple[Optional[str], str]: |
||||
"""Parse thinking model response to extract thinking process and final answer.""" |
||||
import re |
||||
|
||||
# Try different thinking tag patterns |
||||
thinking_patterns = [ |
||||
r'<thinking>(.*?)</thinking>', |
||||
r'<think>(.*?)</think>', |
||||
r'<reasoning>(.*?)</reasoning>', |
||||
r'<analysis>(.*?)</analysis>' |
||||
] |
||||
|
||||
thinking_content = None |
||||
final_answer = content |
||||
|
||||
for pattern in thinking_patterns: |
||||
match = re.search(pattern, content, re.DOTALL) |
||||
if match: |
||||
thinking_content = match.group(1).strip() |
||||
# Remove thinking section from final answer |
||||
final_answer = re.sub(pattern, '', content, flags=re.DOTALL).strip() |
||||
break |
||||
|
||||
# If no thinking tags found, check for other patterns like "I need to think about..." |
||||
if not thinking_content: |
||||
# Look for thinking indicators at the start |
||||
thinking_indicators = [ |
||||
r'^(Let me think about.*?(?=\n\n|\n#|\nI\'ll))', |
||||
r'^(I need to analyze.*?(?=\n\n|\n#|\nI\'ll))', |
||||
r'^(First, let me understand.*?(?=\n\n|\n#|\nI\'ll))', |
||||
r'^(To document this.*?(?=\n\n|\n#|\nI\'ll))' |
||||
] |
||||
|
||||
for pattern in thinking_indicators: |
||||
match = re.search(pattern, content, re.DOTALL | re.MULTILINE) |
||||
if match: |
||||
thinking_content = match.group(1).strip() |
||||
final_answer = content[match.end():].strip() |
||||
break |
||||
|
||||
return thinking_content, final_answer |
||||
|
||||
def create_documentation_prompt(self, file_analysis: Dict, project_context: Dict) -> str: |
||||
"""Create a comprehensive prompt for documentation generation.""" |
||||
|
||||
file_path = file_analysis['file_path'] |
||||
relative_path = os.path.relpath(file_path, project_context['root_path']) |
||||
|
||||
prompt = f"""You are a technical documentation expert. Generate comprehensive markdown documentation for the Python file: `{relative_path}` |
||||
|
||||
## PROJECT CONTEXT: |
||||
- **Project Root**: {project_context['root_path']} |
||||
- **Total Python Files**: {len(project_context['all_files'])} |
||||
- **External Dependencies**: {', '.join(project_context['external_dependencies']) if project_context['external_dependencies'] else 'None detected'} |
||||
- **Project Structure**: |
||||
{self.format_project_structure(project_context['file_structure'])} |
||||
|
||||
## FILE ANALYSIS: |
||||
- **File Path**: `{relative_path}` |
||||
- **Lines of Code**: {file_analysis['lines_of_code']} |
||||
- **Module Docstring**: {file_analysis['module_docstring'] or 'None'} |
||||
|
||||
### Imports ({len(file_analysis['imports'])} total): |
||||
{chr(10).join(f'- `{imp}`' for imp in file_analysis['imports'])} |
||||
|
||||
### Classes ({len(file_analysis['classes'])} total): |
||||
{self.format_classes(file_analysis['classes'])} |
||||
|
||||
### Functions ({len(file_analysis['functions'])} total): |
||||
{self.format_functions(file_analysis['functions'])} |
||||
|
||||
### Constants ({len(file_analysis['constants'])} total): |
||||
{self.format_constants(file_analysis['constants'])} |
||||
|
||||
## RELATED FILES: |
||||
{self.format_related_files(file_analysis, project_context)} |
||||
|
||||
## FULL SOURCE CODE: |
||||
```python |
||||
{file_analysis['content']} |
||||
``` |
||||
|
||||
## DOCUMENTATION REQUIREMENTS: |
||||
|
||||
Generate a complete markdown documentation file that includes: |
||||
|
||||
1. **File Header**: Title ('Documentation ' + file), purpose, and brief description |
||||
2. **Overview**: What this module/file does and its role in the project |
||||
3. **Dependencies**: External and internal dependencies with explanations |
||||
4. **API Reference**: Detailed documentation of all classes, functions, and constants |
||||
5. **Usage Examples**: Practical code examples where applicable |
||||
6. **Cross-References**: Links to related files using relative markdown links |
||||
7. **Implementation Notes**: Architecture decisions, patterns used, etc. |
||||
|
||||
## FORMATTING GUIDELINES: |
||||
- YOUR ARE **NOT ALLOWED** TO USE markdown CODE BLOCKS! |
||||
- Use proper markdown syntax, so no **# title** or other none standard markdown features |
||||
- Be carefull with indentation |
||||
- Limite the use of unecessary newlines |
||||
- Include code blocks with syntax highlighting |
||||
- Add tables for parameter/return value documentation |
||||
- Use relative links to other documentation files: `[filename](./filename.md)` |
||||
- Include line number references where helpful |
||||
- Make it professional and comprehensive |
||||
- Focus on clarity and usefulness for developers |
||||
|
||||
Generate the complete markdown documentation now:""" |
||||
|
||||
return prompt |
||||
|
||||
def format_project_structure(self, file_structure: Dict) -> str: |
||||
"""Format project structure for the prompt.""" |
||||
lines = [] |
||||
for root, dirs, files in file_structure: |
||||
level = root.replace(file_structure[0][0], '').count(os.sep) |
||||
indent = ' ' * level |
||||
lines.append(f"{indent}- {os.path.basename(root)}/") |
||||
subindent = ' ' * (level + 1) |
||||
for file in files: |
||||
if file.endswith('.py'): |
||||
lines.append(f"{subindent}- {file}") |
||||
return '\n'.join(lines[:20]) # Limit to first 20 lines |
||||
|
||||
def format_classes(self, classes: List[Dict]) -> str: |
||||
"""Format class information for the prompt.""" |
||||
if not classes: |
||||
return "None" |
||||
|
||||
lines = [] |
||||
for cls in classes: |
||||
lines.append(f"- **{cls['name']}** (line {cls['lineno']})") |
||||
if cls['bases']: |
||||
lines.append(f" - Inherits from: {', '.join(cls['bases'])}") |
||||
if cls['methods']: |
||||
lines.append(f" - Methods: {', '.join(cls['methods'])}") |
||||
if cls['docstring']: |
||||
lines.append(f" - Description: {cls['docstring'][:100]}...") |
||||
return '\n'.join(lines) |
||||
|
||||
def format_functions(self, functions: List[Dict]) -> str: |
||||
"""Format function information for the prompt.""" |
||||
if not functions: |
||||
return "None" |
||||
|
||||
lines = [] |
||||
for func in functions: |
||||
args_str = ', '.join(func['args']) if func['args'] else '' |
||||
lines.append(f"- **{func['name']}({args_str})** (line {func['lineno']})") |
||||
if func['returns']: |
||||
lines.append(f" - Returns: {func['returns']}") |
||||
if func['docstring']: |
||||
lines.append(f" - Description: {func['docstring'][:100]}...") |
||||
return '\n'.join(lines) |
||||
|
||||
def format_constants(self, constants: List[Dict]) -> str: |
||||
"""Format constant information for the prompt.""" |
||||
if not constants: |
||||
return "None" |
||||
|
||||
lines = [] |
||||
for const in constants: |
||||
lines.append(f"- **{const['name']}** = {const['value']} (line {const['lineno']})") |
||||
return '\n'.join(lines) |
||||
|
||||
def format_related_files(self, file_analysis: Dict, project_context: Dict) -> str: |
||||
"""Format related files information.""" |
||||
current_imports = set(file_analysis['imports']) |
||||
related_files = [] |
||||
|
||||
for other_file in project_context['all_files']: |
||||
if other_file != file_analysis['file_path']: |
||||
rel_path = os.path.relpath(other_file, project_context['root_path']) |
||||
module_name = rel_path.replace('/', '.').replace('\\', '.').replace('.py', '') |
||||
|
||||
# Check if this file imports the other or vice versa |
||||
if any(imp.startswith(module_name) for imp in current_imports): |
||||
related_files.append(f"- `{rel_path}` (imported by this file)") |
||||
|
||||
return '\n'.join(related_files) if related_files else "None detected" |
||||
|
||||
class ProjectAnalyzer: |
||||
"""Analyzes the entire project structure.""" |
||||
|
||||
def __init__(self, root_path: str): |
||||
self.root_path = Path(root_path).resolve() |
||||
self.python_files = [] |
||||
self.external_dependencies = set() |
||||
|
||||
def scan_project(self, exclude_dirs: List[str] = None) -> Dict: |
||||
"""Scan the project and collect all Python files.""" |
||||
if exclude_dirs is None: exclude_dirs = ['.git', '__pycache__', '.pytest_cache', 'venv', 'env', '.venv', 'node_modules'] |
||||
else: exclude_dirs = exclude_dirs + ['.git', '__pycache__', '.pytest_cache', 'venv', 'env', '.venv', 'node_modules'] |
||||
|
||||
self.python_files = [] |
||||
file_structure = [] |
||||
|
||||
for root, dirs, files in os.walk(self.root_path): |
||||
# Remove excluded directories |
||||
dirs[:] = [d for d in dirs if d not in exclude_dirs] |
||||
files[:] = [f for f in files if f not in exclude_dirs] |
||||
file_structure.append((root, dirs, files)) |
||||
|
||||
for file in files: |
||||
if file.endswith('.py'): |
||||
self.python_files.append(os.path.join(root, file)) |
||||
|
||||
# Analyze dependencies |
||||
self.analyze_dependencies() |
||||
|
||||
return { |
||||
'root_path': str(self.root_path), |
||||
'all_files': self.python_files, |
||||
'file_structure': file_structure, |
||||
'external_dependencies': list(self.external_dependencies) |
||||
} |
||||
|
||||
def analyze_dependencies(self): |
||||
"""Analyze external dependencies across all Python files.""" |
||||
analyzer = PythonAnalyzer() |
||||
|
||||
for file_path in self.python_files: |
||||
analysis = analyzer.analyze_file(file_path) |
||||
if analysis: |
||||
for imp in analysis['imports']: |
||||
# Check if it's an external dependency (not local) |
||||
if not self.is_local_import(imp): |
||||
self.external_dependencies.add(imp.split('.')[0]) |
||||
|
||||
def is_local_import(self, import_name: str) -> bool: |
||||
"""Check if an import is local to the project.""" |
||||
# Simple heuristic: if the import starts with a relative path or matches a local file |
||||
if import_name.startswith('.'): |
||||
return True |
||||
|
||||
# Check if it matches any of our Python files |
||||
for py_file in self.python_files: |
||||
rel_path = os.path.relpath(py_file, self.root_path) |
||||
module_path = rel_path.replace('/', '.').replace('\\', '.').replace('.py', '') |
||||
if import_name.startswith(module_path): |
||||
return True |
||||
|
||||
return False |
||||
|
||||
class DocumentationManager: |
||||
"""Manages the documentation generation process.""" |
||||
|
||||
def __init__(self, output_dir: str = "./pydocs"): |
||||
self.output_dir = Path(output_dir) |
||||
self.output_dir.mkdir(exist_ok=True) |
||||
|
||||
def generate_index(self, project_context: Dict, generated_docs: List[str]): |
||||
"""Generate an index.md file linking to all documentation.""" |
||||
|
||||
index_content = f"""# Project Documentation |
||||
|
||||
Auto-generated documentation for Python project: `{os.path.basename(project_context['root_path'])}` |
||||
|
||||
## Project Overview |
||||
|
||||
- **Total Python Files**: {len(project_context['all_files'])} |
||||
- **External Dependencies**: {len(project_context['external_dependencies'])} |
||||
- **Documentation Files**: {len(generated_docs)} |
||||
|
||||
## External Dependencies |
||||
|
||||
{chr(10).join(f'- `{dep}`' for dep in sorted(project_context['external_dependencies']))} |
||||
|
||||
## File Documentation |
||||
|
||||
""" |
||||
|
||||
for doc_file in sorted(generated_docs): |
||||
rel_path = os.path.relpath(doc_file.replace('.md', '.py'), '.') |
||||
doc_name = os.path.basename(doc_file) |
||||
index_content += f"- [`{rel_path}`](./{doc_name})\n" |
||||
|
||||
index_content += f""" |
||||
## Project Structure |
||||
|
||||
``` |
||||
{self.generate_tree_structure(project_context)} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
*Documentation generated automatically using Ollama LLM* |
||||
""" |
||||
|
||||
with open(self.output_dir / "index.md", 'w', encoding='utf-8') as f: |
||||
f.write(index_content) |
||||
|
||||
def generate_tree_structure(self, project_context: Dict, max_depth: int = 3) -> str: |
||||
"""Generate a tree-like structure of the project.""" |
||||
lines = [] |
||||
root_path = project_context['root_path'] |
||||
|
||||
for py_file in sorted(project_context['all_files']): |
||||
rel_path = os.path.relpath(py_file, root_path) |
||||
depth = rel_path.count(os.sep) |
||||
if depth <= max_depth: |
||||
indent = " " * depth |
||||
filename = os.path.basename(rel_path) |
||||
lines.append(f"{indent}{filename}") |
||||
|
||||
return '\n'.join(lines[:50]) # Limit output |
||||
|
||||
def sanitize_filename(self, file_path: str, root_path: str) -> str: |
||||
"""Convert file path to a safe markdown filename.""" |
||||
rel_path = os.path.relpath(file_path, root_path) |
||||
# Replace path separators and special characters |
||||
safe_name = rel_path.replace('\\', '/').replace('.py', '.md') |
||||
return safe_name |
||||
|
||||
def main(): |
||||
parser = argparse.ArgumentParser(description="Generate documentation for Python project using Ollama") |
||||
parser.add_argument("path", help="Path to Python project directory") |
||||
parser.add_argument("--model", default="deepseek-r1:latest", help="Ollama model to use (default: deepseek-r1:latest). For thinking models use 'thinking' in name") |
||||
parser.add_argument("--thinking", action=argparse.BooleanOptionalAction, help="Does the model think", type=bool) |
||||
parser.add_argument("--output", default="./pydocs", help="Output directory for documentation (default: ./pydocs)") |
||||
parser.add_argument("--ollama-url", default="http://localhost:11434", help="Ollama server URL") |
||||
parser.add_argument("--exclude", nargs="*", default=[], help="Directories to exclude from scanning") |
||||
parser.add_argument("--max-files", type=int, default=400, help="Maximum number of files to process") |
||||
|
||||
args = parser.parse_args() |
||||
|
||||
# Validate project path |
||||
if not os.path.exists(args.path): |
||||
print(f"Error: Path '{args.path}' does not exist") |
||||
sys.exit(1) |
||||
|
||||
# Initialize components |
||||
doc_generator = OllamaDocGenerator(args.model, args.ollama_url, args.thinking) |
||||
project_analyzer = ProjectAnalyzer(args.path) |
||||
doc_manager = DocumentationManager(args.output) |
||||
analyzer = PythonAnalyzer() |
||||
|
||||
# Check Ollama connection |
||||
print("Checking Ollama connection...") |
||||
if not doc_generator.check_ollama_connection(): |
||||
print(f"Error: Cannot connect to Ollama at {args.ollama_url}") |
||||
print("Make sure Ollama is running: ollama serve") |
||||
sys.exit(1) |
||||
|
||||
# Check model availability |
||||
print(f"Checking model availability: {args.model}") |
||||
if not doc_generator.check_model_availability(): |
||||
print(f"Error: Model '{args.model}' is not available") |
||||
print(f"Install it with: ollama pull {args.model}") |
||||
sys.exit(1) |
||||
|
||||
print(f"✓ Ollama connection established with model: {args.model}") |
||||
|
||||
# Scan project |
||||
print("Scanning project...") |
||||
project_context = project_analyzer.scan_project(args.exclude) |
||||
|
||||
if not project_context['all_files']: |
||||
print("No Python files found in the project") |
||||
sys.exit(1) |
||||
|
||||
print(f"Found {len(project_context['all_files'])} Python files") |
||||
|
||||
# Limit files if specified |
||||
files_to_process = project_context['all_files'][:args.max_files] |
||||
if len(files_to_process) < len(project_context['all_files']): |
||||
print(f"Processing first {args.max_files} files (use --max-files to change)") |
||||
|
||||
# Generate documentation for each file |
||||
generated_docs = [] |
||||
|
||||
for i, file_path in enumerate(files_to_process, 1): |
||||
rel_path = os.path.relpath(file_path, args.path) |
||||
print(f"[{i}/{len(files_to_process)}] Documenting {rel_path}...") |
||||
|
||||
# Analyze file |
||||
file_analysis = analyzer.analyze_file(file_path) |
||||
if not file_analysis: |
||||
print(f" ⚠ Skipped due to analysis error") |
||||
continue |
||||
|
||||
# Generate documentation |
||||
documentation = doc_generator.generate_documentation(file_analysis, project_context) if len(file_analysis['content'].strip(" \n\t")) else "" |
||||
if not documentation: |
||||
print(f" ⚠ Failed to generate documentation" if len(file_analysis['content'].strip(" \n\t")) else " ⚠ No document generated because no code was found in the file") |
||||
continue |
||||
|
||||
# Save documentation |
||||
doc_filename = doc_manager.sanitize_filename(file_path, args.path) |
||||
doc_path = doc_manager.output_dir / doc_filename |
||||
os.makedirs(os.path.dirname(doc_path), exist_ok=True) |
||||
with open(doc_path, 'w', encoding='utf-8') as f: |
||||
f.write(documentation) |
||||
|
||||
generated_docs.append(doc_filename) |
||||
print(f" ✓ Generated: {doc_filename}") |
||||
|
||||
# Generate index file |
||||
if generated_docs: |
||||
print("Generating index file...") |
||||
doc_manager.generate_index(project_context, generated_docs) |
||||
print(f"✓ Documentation complete! Check {args.output}/index.md") |
||||
print(f"Generated {len(generated_docs)} documentation files") |
||||
else: |
||||
print("No documentation files were generated") |
||||
|
||||
if __name__ == "__main__": |
||||
main() |
Loading…
Reference in New Issue