src/litterate/cli.py annotated source
Back to index1#!/usr/bin/env python3Command-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.
78import importlib.util9import json10import sys11from glob import glob12from pathlib import Path13from typing import Any1415import click1617from litterate.defaults import DEFAULTS18from litterate.generator import generate_litterate_pages1920Configuration Loading
Load configuration from a Python or JSON file. This allows users to specify their Litterate configuration in whichever format is most convenient.
2526def load_config_file(config_path: str) -> dict[str, Any]:27 path = Path(config_path)2829 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)39Look for CONFIG or config variable, or extract all non-private module attributes
41 if hasattr(module, "CONFIG"):42 return module.CONFIG43 elif hasattr(module, "config"):44 return module.config45 else:46 return {47 k: v48 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")5354CLI Command Definition
Using Click to define the command-line interface with various options for customization.
5859@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.9899 Read the full documentation at https://github.com/thesephist/litterate100101 \b102 Basic usage:103 litterate --config your-litterate-config.py104 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()109110 if config:111 user_config = load_config_file(config)112 merged_config.update(user_config)113Command-line arguments have highest priority and override everything else
115 if name is not None:116 merged_config["name"] = name117 if description is not None:118 merged_config["description"] = description119 if wrap is not None:120 merged_config["wrap"] = wrap121 if base_url is not None:122 merged_config["baseURL"] = base_url123 if verbose:124 merged_config["verbose"] = True125 if output is not None:126 merged_config["output_directory"] = output127 if files:128 merged_config["files"] = list(files)129Expand 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)139Ensure 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 + "/"144145 if merged_config["verbose"]:146 click.echo(f"Using configuration: {merged_config}")147 click.echo(f"Found {len(source_files)} source files")148149 if not source_files:150 click.echo(151 "Warning: No source files found matching the specified patterns", err=True152 )153 sys.exit(1)154Generate the literate documentation pages
156 generate_litterate_pages(source_files, merged_config)157158159if __name__ == "__main__":160 main()161