# src/litterate/cli.py annotated source

Back to index

    1#!/usr/bin/env python3

## Command-line Interface

This is the entry point for the command-line utility, focused on handling and processing CLI arguments and figuring out the right options to pass to the docs generator.

    7

    8import importlib.util

    9import json

    10import sys

    11from glob import glob

    12from pathlib import Path

    13from typing import Any

    14

    15import click

    16

    17from litterate.defaults import DEFAULTS

    18from litterate.generator import generate_litterate_pages

    19

    20

### Configuration Loading

Load configuration from a Python or JSON file. This allows users to specify their Litterate configuration in whichever format is most convenient.

    25

    26def load_config_file(config_path: str) -> dict[str, Any]:

    27    path = Path(config_path)

    28

    29    if path.suffix == ".json":

    30        with open(path, encoding="utf-8") as f:

    31            return json.load(f)

    32    elif path.suffix == ".py":

For Python config files, we dynamically load the module and extract configuration

    34        spec = importlib.util.spec_from_file_location("config", path)

    35        if spec is None or spec.loader is None:

    36            raise ValueError(f"Cannot load config from {path}")

    37        module = importlib.util.module_from_spec(spec)

    38        spec.loader.exec_module(module)

    39

Look for CONFIG or config variable, or extract all non-private module attributes

    41        if hasattr(module, "CONFIG"):

    42            return module.CONFIG

    43        elif hasattr(module, "config"):

    44            return module.config

    45        else:

    46            return {

    47                k: v

    48                for k, v in module.__dict__.items()

    49                if not k.startswith("_") and not callable(v)

    50            }

    51    else:

    52        raise ValueError(f"Unsupported config file type: {path.suffix}. Use .py or .json")

    53

    54

### CLI Command Definition

Using Click to define the command-line interface with various options for customization.

    58

    59@click.command()

    60@click.option(

    61    "--config",

    62    type=click.Path(exists=True),

    63    help="Specify a Python or JSON file for configuration",

    64)

    65@click.option("-n", "--name", help="Name of your project, shown in the generated site")

    66@click.option(

    67    "-d", "--description", help="Description text for your project, shown in the generated site"

    68)

    69@click.option(

    70    "-w",

    71    "--wrap",

    72    type=int,

    73    help="Wrap long lines to N characters (0 = no wrapping)",

    74)

    75@click.option(

    76    "-b",

    77    "--base-url",

    78    help="Base URL for the generated site (e.g., /project-name for GitHub Pages)",

    79)

    80@click.option("-v", "--verbose", is_flag=True, help="Verbose output while litterate runs")

    81@click.option(

    82    "-o",

    83    "--output",

    84    help="Destination directory for generated docs (default: ./docs/)",

    85)

    86@click.argument("files", nargs=-1, type=click.Path())

    87def main(

    88    config: str | None,

    89    name: str | None,

    90    description: str | None,

    91    wrap: int | None,

    92    base_url: str | None,

    93    verbose: bool,

    94    output: str | None,

    95    files: tuple[str, ...],

    96) -> None:

    97    """Litterate - Generate beautiful literate programming-style code annotations.

    98

    99    Read the full documentation at https://github.com/thesephist/litterate

    100

    101    \b

    102    Basic usage:

    103        litterate --config your-litterate-config.py

    104        litterate [options] [files]

    105        (if no files are specified, litterate runs on src/**/*.py)

    106    """

Configuration merging: Start with defaults, then layer on config file, then CLI args

    108    merged_config = DEFAULTS.copy()

    109

    110    if config:

    111        user_config = load_config_file(config)

    112        merged_config.update(user_config)

    113

Command-line arguments have highest priority and override everything else

    115    if name is not None:

    116        merged_config["name"] = name

    117    if description is not None:

    118        merged_config["description"] = description

    119    if wrap is not None:

    120        merged_config["wrap"] = wrap

    121    if base_url is not None:

    122        merged_config["baseURL"] = base_url

    123    if verbose:

    124        merged_config["verbose"] = True

    125    if output is not None:

    126        merged_config["output_directory"] = output

    127    if files:

    128        merged_config["files"] = list(files)

    129

Expand glob patterns to actual file paths, filtering out directories

    131    source_files = []

    132    for glob_pattern in merged_config["files"]:

    133        try:

    134            matches = glob(glob_pattern, recursive=True)

    135            file_matches = [Path(f) for f in matches if Path(f).is_file()]

    136            source_files.extend(file_matches)

    137        except Exception as e:

    138            click.echo(f"Error while looking for matching source files: {e}", err=True)

    139

Ensure baseURL ends with / for proper path joining

    141    base_url_value = merged_config["baseURL"]

    142    if not base_url_value.endswith("/"):

    143        merged_config["baseURL"] = base_url_value + "/"

    144

    145    if merged_config["verbose"]:

    146        click.echo(f"Using configuration: {merged_config}")

    147        click.echo(f"Found {len(source_files)} source files")

    148

    149    if not source_files:

    150        click.echo(

    151            "Warning: No source files found matching the specified patterns", err=True

    152        )

    153        sys.exit(1)

    154

Generate the literate documentation pages

    156    generate_litterate_pages(source_files, merged_config)

    157

    158

    159if __name__ == "__main__":

    160    main()

    161