This commit is contained in:
278
.code_doc_cli/cli/main.py
Normal file
278
.code_doc_cli/cli/main.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""Command-line interface for code-doc-cli."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import click
|
||||
from datetime import datetime
|
||||
|
||||
from ..parsers.registry import ParserRegistry
|
||||
from ..generators.markdown_generator import MarkdownGenerator
|
||||
from ..utils.file_utils import find_files, read_file_safe, ensure_directory_exists
|
||||
from ..utils.config import load_config, Config
|
||||
|
||||
|
||||
class ExitCode:
|
||||
SUCCESS = 0
|
||||
NO_FILES = 1
|
||||
PARSE_ERROR = 2
|
||||
INVALID_CONFIG = 3
|
||||
WARNING = 4
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
"--config",
|
||||
"-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file",
|
||||
)
|
||||
@click.option(
|
||||
"--language",
|
||||
"-l",
|
||||
type=click.Choice(["python", "typescript", "go", "auto"]),
|
||||
default="auto",
|
||||
help="Programming language",
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
"-o",
|
||||
type=click.Path(),
|
||||
help="Output file path",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"-f",
|
||||
type=click.Choice(["markdown", "json"]),
|
||||
default="markdown",
|
||||
help="Output format",
|
||||
)
|
||||
@click.option(
|
||||
"--fail-on-warning",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Exit with error code on warnings",
|
||||
)
|
||||
@click.option(
|
||||
"--exclude",
|
||||
multiple=True,
|
||||
help="Exclude patterns (can be used multiple times)",
|
||||
)
|
||||
@click.option(
|
||||
"--include-private",
|
||||
is_flag=False,
|
||||
help="Include private members",
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(
|
||||
ctx: click.Context,
|
||||
config: Optional[str],
|
||||
language: str,
|
||||
output: Optional[str],
|
||||
format: str,
|
||||
fail_on_warning: bool,
|
||||
exclude: tuple,
|
||||
include_private: bool,
|
||||
) -> None:
|
||||
"""code-doc-cli: Auto-generate documentation from code."""
|
||||
ctx.ensure_object(dict)
|
||||
|
||||
loaded_config = load_config(config)
|
||||
|
||||
if not config and not output:
|
||||
output = loaded_config.output_file
|
||||
|
||||
ctx.obj["config"] = loaded_config
|
||||
ctx.obj["language"] = language if language != "auto" else loaded_config.language
|
||||
ctx.obj["output"] = output
|
||||
ctx.obj["format"] = format
|
||||
ctx.obj["fail_on_warning"] = fail_on_warning or loaded_config.fail_on_warning
|
||||
ctx.obj["exclude"] = list(exclude) or loaded_config.exclude_patterns
|
||||
ctx.obj["include_private"] = include_private or loaded_config.include_private
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument(
|
||||
"paths",
|
||||
type=click.Path(exists=True),
|
||||
nargs=-1,
|
||||
)
|
||||
@click.option(
|
||||
"--pattern",
|
||||
"-p",
|
||||
multiple=True,
|
||||
help="File patterns to match (default: **/*.py, **/*.ts, **/*.go)",
|
||||
)
|
||||
@click.pass_context
|
||||
def generate(
|
||||
ctx: click.Context,
|
||||
paths: tuple[str],
|
||||
pattern: tuple[str],
|
||||
) -> None:
|
||||
"""Generate documentation from source files."""
|
||||
config: Config = ctx.obj["config"]
|
||||
language: Optional[str] = ctx.obj["language"]
|
||||
output: Optional[str] = ctx.obj["output"]
|
||||
output_format: str = ctx.obj["format"]
|
||||
fail_on_warning: bool = ctx.obj["fail_on_warning"]
|
||||
exclude_patterns: list[str] = ctx.obj["exclude"]
|
||||
include_private: bool = ctx.obj["include_private"]
|
||||
|
||||
warnings = []
|
||||
errors = []
|
||||
|
||||
if pattern:
|
||||
patterns = list(pattern)
|
||||
else:
|
||||
patterns = config.input_patterns
|
||||
|
||||
files = set()
|
||||
for path in paths:
|
||||
path_obj = Path(path)
|
||||
if path_obj.is_file():
|
||||
files.add(str(path_obj.resolve()))
|
||||
else:
|
||||
for pattern in patterns:
|
||||
for match in find_files(pattern, base_path=str(path_obj)):
|
||||
if match not in files:
|
||||
matches_exclude = False
|
||||
for exclude_pat in exclude_patterns:
|
||||
if exclude_pat in match:
|
||||
matches_exclude = True
|
||||
break
|
||||
if not matches_exclude:
|
||||
files.add(match)
|
||||
|
||||
if not files:
|
||||
click.echo("Error: No source files found matching the specified patterns.", err=True)
|
||||
sys.exit(ExitCode.NO_FILES)
|
||||
|
||||
click.echo(f"Found {len(files)} source file(s) to process.")
|
||||
|
||||
all_elements = []
|
||||
for file_path in sorted(files):
|
||||
file_ext = os.path.splitext(file_path)[1].lower()
|
||||
detected_lang = ParserRegistry.get_language_from_extension(file_path)
|
||||
|
||||
if detected_lang is None:
|
||||
warnings.append(f"Skipping unsupported file: {file_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
parser = ParserRegistry.get_parser(file_path, language if language != "auto" else None)
|
||||
elements = parser.parse()
|
||||
|
||||
filtered_elements = []
|
||||
for elem in elements:
|
||||
if include_private or elem.visibility == "public":
|
||||
if elem.element_type.value != "module":
|
||||
filtered_elements.append(elem)
|
||||
else:
|
||||
filtered_elements.append(elem)
|
||||
|
||||
all_elements.extend(filtered_elements)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error parsing {file_path}: {e}")
|
||||
|
||||
if warnings and fail_on_warning:
|
||||
for warning in warnings:
|
||||
click.echo(f"Warning: {warning}", err=True)
|
||||
sys.exit(ExitCode.WARNING)
|
||||
|
||||
for warning in warnings:
|
||||
click.echo(f"Warning: {warning}", err=True)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
click.echo(f"Error: {error}", err=True)
|
||||
sys.exit(ExitCode.PARSE_ERROR)
|
||||
|
||||
if not all_elements:
|
||||
click.echo("No documentation elements found.", err=True)
|
||||
sys.exit(ExitCode.NO_FILES)
|
||||
|
||||
generator = MarkdownGenerator(
|
||||
template_style=config.template_style,
|
||||
theme=config.theme,
|
||||
syntax_highlighting=config.syntax_highlighting,
|
||||
)
|
||||
|
||||
if output_format == "json":
|
||||
output_content = generator.generate_json(all_elements)
|
||||
else:
|
||||
output_content = generator.generate(
|
||||
elements=all_elements,
|
||||
title="API Documentation",
|
||||
include_metadata=True,
|
||||
)
|
||||
|
||||
if output:
|
||||
ensure_directory_exists(os.path.dirname(output))
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
f.write(output_content)
|
||||
click.echo(f"Documentation written to: {output}")
|
||||
else:
|
||||
click.echo(output_content)
|
||||
|
||||
sys.exit(ExitCode.SUCCESS)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def languages(ctx: click.Context) -> None:
|
||||
"""List supported languages and file extensions."""
|
||||
languages_list = ParserRegistry.get_supported_languages()
|
||||
extensions = ParserRegistry.get_supported_extensions()
|
||||
|
||||
click.echo("Supported Languages:")
|
||||
for lang in sorted(set(languages_list)):
|
||||
click.echo(f" - {lang}")
|
||||
|
||||
click.echo("")
|
||||
click.echo("Supported Extensions:")
|
||||
for ext in sorted(set(extensions)):
|
||||
click.echo(f" - {ext}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def init(ctx: click.Context) -> None:
|
||||
"""Initialize a configuration file."""
|
||||
config_content = '''[tool.code-doc]
|
||||
input_patterns = ["**/*.py", "**/*.ts", "**/*.go"]
|
||||
output_file = "docs/API.md"
|
||||
language = "auto"
|
||||
template_style = "default"
|
||||
syntax_highlighting = true
|
||||
theme = "default"
|
||||
fail_on_warning = false
|
||||
exclude_patterns = ["**/test_*", "**/*_test.go"]
|
||||
include_private = false
|
||||
output_format = "markdown"
|
||||
'''
|
||||
|
||||
config_path = "code-doc.toml"
|
||||
if os.path.exists(config_path):
|
||||
if not click.confirm(f"{config_path} already exists. Overwrite?"):
|
||||
return
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
click.echo(f"Configuration file created: {config_path}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def version(ctx: click.Context) -> None:
|
||||
"""Display version information."""
|
||||
from .. import __version__
|
||||
click.echo(f"code-doc-cli version: {__version__}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point."""
|
||||
cli(obj={})
|
||||
Reference in New Issue
Block a user