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