fix: resolve CI linting failures
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled

- 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:
2026-01-31 17:47:59 +00:00
parent 50a28a409b
commit 9b8ef83be9

View File

@@ -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:
console.print(f"[red]Error: Unknown format: {format}[/red]")
raise typer.Exit(1)
if format == OutputFormat.HTML: generator = generator_class(config)
generator = HTMLGenerator(config)
elif format == OutputFormat.MARKDOWN:
generator = MarkdownGenerator(config)
else:
generator = OpenAPIGenerator(config)
output_path = generator.generate(endpoints, output_dir) endpoints = detector.scan_directory(input_dir)
console.print(f"[green]Documentation generated: {output_path}[/green]") if not endpoints:
console.print("[yellow]No endpoints detected.[/yellow]")
return
console.print(f"\n[green]Detected {len(endpoints)} endpoints[/green]")
@app.command("serve") if verbose:
def serve_command( for ep in endpoints:
input_dir: Path = typer.Argument( console.print(f" - {ep.method.value:6} {ep.path}")
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(): output_path = generator.generate(endpoints, output_dir)
console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]") console.print(f"\n[green]Documentation generated: {output_path}[/green]")
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
if verbose:
import traceback
console.print(traceback.format_exc())
raise typer.Exit(1) raise typer.Exit(1)
class DocsReloadHandler(FileSystemEventHandler):
def __init__(self):
self._reload_needed = False
def on_any_event(self, event):
self._reload_needed = True
server_url = f"http://{host}:{port}"
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)
server = uvicorn.Server(config)
server.run()
@app.command("init")
def init_command(
output_dir: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
):
"""Initialize DocGen configuration file."""
config_content = '''# DocGen Configuration
# 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()