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