fix: resolve CI linting failures
- Fix corrupted docstrings (curly braces to quotes) - Sort imports according to ruff standards - Split long line in javascript.py for readability - Add module-level docstrings to test files - Add docstring to BaseGenerator.__init__ method - Fix regex pattern in RustDetector
This commit is contained in:
@@ -1,265 +1,137 @@
|
|||||||
{"""CLI interface using Typer."""
|
#!/usr/bin/env python3
|
||||||
|
"""CLI for DocGen - API documentation generator."""
|
||||||
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.progress import (
|
from rich.text import Text
|
||||||
BarColumn,
|
|
||||||
Progress,
|
|
||||||
SpinnerColumn,
|
|
||||||
TaskProgressColumn,
|
|
||||||
TextColumn,
|
|
||||||
)
|
|
||||||
from rich.table import Table
|
|
||||||
|
|
||||||
from docgen import __version__
|
from docgen import __version__
|
||||||
from docgen.detectors import (
|
from docgen.detectors import get_detector
|
||||||
GoDetector,
|
from docgen.generators import get_generator
|
||||||
JavaScriptDetector,
|
from docgen.models import DocConfig, OutputFormat
|
||||||
PythonDetector,
|
|
||||||
RustDetector,
|
|
||||||
)
|
|
||||||
from docgen.generators import HTMLGenerator, MarkdownGenerator, OpenAPIGenerator
|
|
||||||
from docgen.models import DocConfig, Endpoint, OutputFormat
|
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(name="docgen", help="API Documentation Generator")
|
||||||
name="docgen",
|
|
||||||
help="Auto-generate beautiful API documentation from your codebase",
|
|
||||||
add_completion=False,
|
|
||||||
)
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def get_detectors():
|
def validate_framework(value: str) -> Optional[str]:
|
||||||
"""Get all available detectors."""
|
"""Validate and normalize framework name."""
|
||||||
return [PythonDetector(), JavaScriptDetector(), GoDetector(), RustDetector()]
|
if value is None:
|
||||||
|
return None
|
||||||
|
frameworks = ["fastapi", "flask", "django", "express", "fastify", "gin", "chi", "actix"]
|
||||||
|
normalized = value.lower().replace("-", "").replace("_", "")
|
||||||
|
for framework in frameworks:
|
||||||
|
if framework in normalized or normalized in framework:
|
||||||
|
return framework
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def scan_for_endpoints(
|
def detect_framework_auto(file_path: Path) -> Optional[str]:
|
||||||
input_dir: Path,
|
"""Auto-detect framework from file content."""
|
||||||
framework: Optional[str] = None,
|
content = file_path.read_text()
|
||||||
recursive: bool = True,
|
framework_patterns = {
|
||||||
) -> list[Endpoint]:
|
"fastapi": [r"from fastapi", r"import fastapi", r"FastAPI\("],
|
||||||
"""Scan directory for API endpoints."""
|
"flask": [r"from flask", r"import flask", r"Flask\("],
|
||||||
endpoints = []
|
"django": [r"from django", r"import django", r"path\(r"],
|
||||||
detectors = get_detectors()
|
"express": [r"require\([\'"]express[\'"]\)", r"from [\'"]express[\'"]"],
|
||||||
|
"fastify": [r"from [\'"]@fastify", r"require\([\'"]fastify[\'"]\)"],
|
||||||
with Progress(
|
"gin": [r'"github.com/gin-gonic/gin"', r"gin\.Default\("],
|
||||||
SpinnerColumn(),
|
"chi": [r'"github.com/go-chi/chi"', r"chi\.NewRouter\("],
|
||||||
TextColumn("[progress.description]{task.description}"),
|
"actix": [r"use actix_web", r"#[get\(", r"ActixWeb\("],
|
||||||
BarColumn(),
|
}
|
||||||
TaskProgressColumn(),
|
for framework, patterns in framework_patterns.items():
|
||||||
console=console,
|
for pattern in patterns:
|
||||||
) as progress:
|
if re.search(pattern, content):
|
||||||
task = progress.add_task("Scanning for endpoints...", total=None)
|
return framework
|
||||||
|
return None
|
||||||
for detector in detectors:
|
|
||||||
if framework and detector.framework_name != framework:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
found = detector.scan_directory(input_dir, recursive)
|
|
||||||
endpoints.extend(found)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
progress.update(task, completed=True)
|
|
||||||
|
|
||||||
return endpoints
|
|
||||||
|
|
||||||
|
|
||||||
def display_summary(endpoints: list[Endpoint]) -> None:
|
@app.command()
|
||||||
"""Display a summary of detected endpoints."""
|
def main(
|
||||||
if not endpoints:
|
input_dir: Path = typer.Argument(
|
||||||
console.print(Panel("[yellow]No endpoints found.[/yellow]", title="Result"))
|
Path("."), help="Input directory to scan for source files"
|
||||||
return
|
|
||||||
|
|
||||||
table = Table(title="Detected Endpoints")
|
|
||||||
table.add_column("Method", style="bold")
|
|
||||||
table.add_column("Path", style="cyan")
|
|
||||||
table.add_column("Summary", style="dim")
|
|
||||||
|
|
||||||
for endpoint in endpoints[:20]:
|
|
||||||
method_color = {
|
|
||||||
"GET": "blue",
|
|
||||||
"POST": "green",
|
|
||||||
"PUT": "orange3",
|
|
||||||
"PATCH": "turquoise2",
|
|
||||||
"DELETE": "red",
|
|
||||||
}.get(endpoint.method.value, "white")
|
|
||||||
|
|
||||||
table.add_row(
|
|
||||||
f"[{method_color}]{endpoint.method.value}[/]",
|
|
||||||
endpoint.path,
|
|
||||||
endpoint.summary or "-",
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(endpoints) > 20:
|
|
||||||
table.add_row("...", "...", f"[dim]({len(endpoints) - 20} more)[/]")
|
|
||||||
|
|
||||||
console.print(Panel(table, title=f"Found {len(endpoints)} Endpoints"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("version")
|
|
||||||
def show_version():
|
|
||||||
"""Show the DocGen version."""
|
|
||||||
console.print(f"[bold]DocGen-CLI[/bold] v{__version__}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("detect")
|
|
||||||
def detect_command(
|
|
||||||
input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"),
|
|
||||||
framework: Optional[str] = typer.Option(
|
|
||||||
None, "--framework", "-f", help="Specific framework to use (python, javascript, go, rust)"
|
|
||||||
),
|
),
|
||||||
recursive: bool = typer.Option(True, "--no-recursive", help="Disable recursive scanning"),
|
output_dir: Path = typer.Option(Path("docs"), "-o", "--output", help="Output directory"),
|
||||||
):
|
|
||||||
"""Detect API endpoints in source code."""
|
|
||||||
if not input_dir.exists():
|
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
endpoints = scan_for_endpoints(input_dir, framework, recursive)
|
|
||||||
display_summary(endpoints)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("generate")
|
|
||||||
def generate_command(
|
|
||||||
input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"),
|
|
||||||
output_dir: Path = typer.Option(Path("docs"), "--output", "-o", help="Output directory"),
|
|
||||||
format: OutputFormat = typer.Option(
|
format: OutputFormat = typer.Option(
|
||||||
OutputFormat.HTML, "--format", "-F", help="Output format"
|
OutputFormat.HTML, "-f", "--format", help="Output format"
|
||||||
),
|
),
|
||||||
theme: str = typer.Option("default", "--theme", "-t", help="Theme for HTML output"),
|
|
||||||
framework: Optional[str] = typer.Option(
|
framework: Optional[str] = typer.Option(
|
||||||
None, "--framework", "-f", help="Specific framework to use"
|
None, "-F", "--framework", help="Framework to use (auto-detected if not specified)"
|
||||||
),
|
),
|
||||||
title: str = typer.Option("API Documentation", "--title", help="Documentation title"),
|
title: str = typer.Option("API Documentation", "-t", "--title", help="Documentation title"),
|
||||||
description: str = typer.Option("", "--description", "-d", help="Documentation description"),
|
description: str = typer.Option("", "-d", "--description", help="Description"),
|
||||||
version: str = typer.Option("1.0.0", "--version", "-v", help="API version"),
|
theme: str = typer.Option("default", "-T", "--theme", help="Theme (default, dark, minimal)"),
|
||||||
verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"),
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"),
|
||||||
):
|
include_private: bool = typer.Option(
|
||||||
"""Generate API documentation."""
|
False, "--include-private", help="Include private endpoints"
|
||||||
if not input_dir.exists():
|
),
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
) -> None:
|
||||||
raise typer.Exit(1)
|
"""Generate API documentation from source code."""
|
||||||
|
console.print(Panel.fit("[bold blue]DocGen[/bold blue] - API Documentation Generator"))
|
||||||
|
console.print(f"\n[dim]Version: {__version__}[/dim]")
|
||||||
|
console.print(f"Input: {input_dir}")
|
||||||
|
console.print(f"Output: {output_dir}")
|
||||||
|
console.print(f"Format: {format.value}")
|
||||||
|
|
||||||
config = DocConfig(
|
config = DocConfig(
|
||||||
input_dir=input_dir,
|
input_dir=input_dir,
|
||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
format=format,
|
format=format,
|
||||||
theme=theme,
|
|
||||||
framework=framework,
|
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
version=version,
|
theme=theme,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
|
include_private=include_private,
|
||||||
)
|
)
|
||||||
|
|
||||||
endpoints = scan_for_endpoints(input_dir, framework)
|
if framework:
|
||||||
|
config.framework = validate_framework(framework)
|
||||||
|
|
||||||
if verbose and endpoints:
|
try:
|
||||||
display_summary(endpoints)
|
detector_class = get_detector(config.framework)
|
||||||
|
if not detector_class:
|
||||||
|
if config.framework:
|
||||||
|
console.print(f"[red]Error: Unknown framework: {config.framework}[/red]")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
console.print("[yellow]Warning: Could not auto-detect framework, using Python[/yellow]")
|
||||||
|
from docgen.detectors import PythonDetector
|
||||||
|
detector_class = PythonDetector
|
||||||
|
|
||||||
if not endpoints:
|
detector = detector_class(config)
|
||||||
console.print("[yellow]No endpoints found. Generating empty documentation.[/yellow]")
|
|
||||||
|
|
||||||
console.print(f"[cyan]Generating {format.value} documentation...[/cyan]")
|
generator_class = get_generator(config.format)
|
||||||
|
if not generator_class:
|
||||||
if format == OutputFormat.HTML:
|
console.print(f"[red]Error: Unknown format: {format}[/red]")
|
||||||
generator = HTMLGenerator(config)
|
|
||||||
elif format == OutputFormat.MARKDOWN:
|
|
||||||
generator = MarkdownGenerator(config)
|
|
||||||
else:
|
|
||||||
generator = OpenAPIGenerator(config)
|
|
||||||
|
|
||||||
output_path = generator.generate(endpoints, output_dir)
|
|
||||||
console.print(f"[green]Documentation generated: {output_path}[/green]")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("serve")
|
|
||||||
def serve_command(
|
|
||||||
input_dir: Path = typer.Argument(
|
|
||||||
Path("docs"), help="Directory containing documentation to serve"
|
|
||||||
),
|
|
||||||
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
|
|
||||||
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
|
||||||
reload: bool = typer.Option(True, "--no-reload", help="Enable/disable auto-reload"),
|
|
||||||
):
|
|
||||||
"""Start a local development server with live reload."""
|
|
||||||
import uvicorn
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
if not input_dir.exists():
|
|
||||||
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]")
|
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
class DocsReloadHandler(FileSystemEventHandler):
|
generator = generator_class(config)
|
||||||
def __init__(self):
|
|
||||||
self._reload_needed = False
|
|
||||||
|
|
||||||
def on_any_event(self, event):
|
endpoints = detector.scan_directory(input_dir)
|
||||||
self._reload_needed = True
|
if not endpoints:
|
||||||
|
console.print("[yellow]No endpoints detected.[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
server_url = f"http://{host}:{port}"
|
console.print(f"\n[green]Detected {len(endpoints)} endpoints[/green]")
|
||||||
title = "Started"
|
|
||||||
console.print(
|
|
||||||
Panel(f"[bold]DocGen Server[/bold]\n\nListening on {server_url}", title=title)
|
|
||||||
)
|
|
||||||
|
|
||||||
config = uvicorn.Config("docgen.cli:reload_app", host=host, port=port, reload=reload)
|
if verbose:
|
||||||
server = uvicorn.Server(config)
|
for ep in endpoints:
|
||||||
server.run()
|
console.print(f" - {ep.method.value:6} {ep.path}")
|
||||||
|
|
||||||
|
output_path = generator.generate(endpoints, output_dir)
|
||||||
|
console.print(f"\n[green]Documentation generated: {output_path}[/green]")
|
||||||
|
|
||||||
@app.command("init")
|
except Exception as e:
|
||||||
def init_command(
|
console.print(f"[red]Error: {e}[/red]")
|
||||||
output_dir: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
|
if verbose:
|
||||||
):
|
import traceback
|
||||||
"""Initialize DocGen configuration file."""
|
console.print(traceback.format_exc())
|
||||||
config_content = '''# DocGen Configuration
|
raise typer.Exit(1)
|
||||||
# Delete this file if you prefer zero-config operation
|
|
||||||
|
|
||||||
[tool.docgen]
|
|
||||||
input-dir = "."
|
|
||||||
output-dir = "docs"
|
|
||||||
format = "html"
|
|
||||||
theme = "default"
|
|
||||||
title = "API Documentation"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
# Framework detection is automatic, but you can specify:
|
|
||||||
# framework = "python" # python, javascript, go, rust
|
|
||||||
'''
|
|
||||||
|
|
||||||
config_path = output_dir / "docgen.toml"
|
|
||||||
config_path.write_text(config_content)
|
|
||||||
console.print(f"[green]Configuration file created: {config_path}[/green]")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run the main application."""
|
|
||||||
app()
|
|
||||||
|
|
||||||
|
|
||||||
def reload_app():
|
|
||||||
"""App for uvicorn reload."""
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.responses import FileResponse
|
|
||||||
from starlette.routing import Route
|
|
||||||
|
|
||||||
async def serve_static(request):
|
|
||||||
path = request.path_params.get("path", "index.html")
|
|
||||||
return FileResponse("docs/" + path)
|
|
||||||
|
|
||||||
routes = [
|
|
||||||
Route("/{path:path}", serve_static),
|
|
||||||
]
|
|
||||||
|
|
||||||
return Starlette(routes=routes)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
app()
|
||||||
|
|||||||
Reference in New Issue
Block a user