Source code for codegen.banner

"""Centralized autogenerated-file banner.

Every generated file gets a header banner the engine prepends at
render time -- one consistent wording across all targets and
languages, with the overwrite-vs-scaffold note *derived from the
file's actual* :attr:`~codegen.spec.GeneratedFile.if_exists`
policy rather than retyped by hand in each template.

Comment syntax is resolved per file extension (a single target
emits several file types -- a Python target also writes
``justfile``/``.toml``/``.ini``), so the lookup keys on the file
name/extension rather than the target's language identifier.
Codegen ships sensible defaults for the languages it knows;
third parties extend or override them by registering a provider
under the ``codegen.comment_styles`` entry-point group -- a
``Callable[[], dict[str, CommentStyle]]`` named for the language.
"""

import functools
import importlib.metadata
from dataclasses import dataclass
from typing import Literal

#: Entry-point group under which packages register comment-style
#: providers.  Each entry point's *name* is a language identifier
#: (e.g. ``"python"``); its value resolves to a
#: ``Callable[[], dict[str, CommentStyle]]`` mapping bare
#: extensions (``"py"``) or full file names (``"justfile"``) to
#: the comment syntax for that file type.  Codegen's built-in
#: defaults (below) are always present; entry-point providers are
#: merged on top, so a third party can add a language or override
#: a single extension.
_ENTRY_POINT_GROUP = "codegen.comment_styles"


#: Width of the banner's top/bottom rule.  Kept <= 80 so the rule
#: never trips a line-length linter in the generated file.
_BANNER_WIDTH = 76


[docs] @dataclass(frozen=True) class CommentStyle: """How to wrap banner text in a given file type's comments. The banner is boxed: a top and bottom rule of the *fill* character frame the text so it reads as a distinct "autogenerated" block. Exactly one of *line* / *block* is set: * *line* -- a line-comment prefix (``"#"``, ``"//"``). Text lines are ``f"{line} {text}"``; the rule is *fill* repeated (e.g. a solid row of ``#`` or ``/``). * *block* -- ``(open, cont, close)`` for block comments. The banner is *open* on its own line, a *fill* rule, the text lines as ``f"{cont}{text}"``, another rule, then *close* (e.g. ``("/*", " * ", " */")`` with ``fill="*"`` for CSS). A file type with no registered style gets no banner at all (e.g. JSON, which has no comment form). """ line: str | None = None block: tuple[str, str, str] | None = None fill: str = "#"
[docs] def render(self, lines: list[str]) -> str: """Render *lines* of banner text, boxed in this comment syntax.""" if self.line is not None: rule = self.fill * _BANNER_WIDTH body = [f"{self.line} {text}".rstrip() for text in lines] return "\n".join([rule, *body, rule]) if self.block is not None: open_delim, cont, close_delim = self.block rule = f"{cont}{self.fill * (_BANNER_WIDTH - len(cont))}" body = [f"{cont}{text}".rstrip() for text in lines] return "\n".join([open_delim, rule, *body, rule, close_delim]) msg = "CommentStyle has neither a line nor a block form" raise ValueError(msg)
_HASH = CommentStyle(line="#", fill="#") _SLASH = CommentStyle(line="//", fill="/") _CSS = CommentStyle(block=("/*", " * ", " */"), fill="*") _HTML = CommentStyle(block=("<!--", " ", "-->"), fill="=") def _python_comment_styles() -> dict[str, CommentStyle]: """Built-in comment styles for Python-target file types.""" return { "py": _HASH, "pyi": _HASH, "toml": _HASH, "ini": _HASH, "cfg": _HASH, "env": _HASH, ".env.example": _HASH, "gitignore": _HASH, "dockerignore": _HASH, "Dockerfile": _HASH, "justfile": _HASH, "just": _HASH, } def _typescript_comment_styles() -> dict[str, CommentStyle]: """Built-in comment styles for TypeScript-target file types.""" return { "ts": _SLASH, "tsx": _SLASH, "js": _SLASH, "jsx": _SLASH, "mjs": _SLASH, "cjs": _SLASH, "jsonnet": _SLASH, "libsonnet": _SLASH, "css": _CSS, "scss": _CSS, "html": _HTML, "htm": _HTML, "gitignore": _HASH, "prettierignore": _HASH, "justfile": _HASH, } def _rego_comment_styles() -> dict[str, CommentStyle]: """Built-in comment styles for Rego-target file types.""" return {"rego": _HASH, "yaml": _HASH, "yml": _HASH} #: Codegen's built-in language tables, merged before any #: entry-point provider. Keyed by language only for organization; #: lookup is by file extension across the merged result. _BUILTIN: dict[str, dict[str, CommentStyle]] = { "python": _python_comment_styles(), "typescript": _typescript_comment_styles(), "rego": _rego_comment_styles(), } @functools.cache def _merged_table() -> dict[str, CommentStyle]: """Build the extension/name -> :class:`CommentStyle` map. Codegen's built-ins first, then entry-point providers on top (so third parties can add languages or override an extension). A conflicting redefinition of the *same* key with a *different* style raises, surfacing accidental disagreement. """ table: dict[str, CommentStyle] = {} for styles in _BUILTIN.values(): _merge_into(table, styles) for entry_point in importlib.metadata.entry_points( group=_ENTRY_POINT_GROUP ): _merge_into(table, entry_point.load()()) return table def _merge_into( table: dict[str, CommentStyle], styles: dict[str, CommentStyle] ) -> None: """Merge *styles* into *table*, raising on conflicting keys.""" for key, style in styles.items(): existing = table.get(key) if existing is not None and existing != style: msg = ( f"Conflicting comment style for {key!r}: " f"{existing!r} vs {style!r}" ) raise ValueError(msg) table[key] = style
[docs] def comment_style_for(path: str) -> CommentStyle | None: """Resolve the comment style for *path*, or ``None`` if unknown. Tries the full file name first (so ``justfile`` / ``.env.example`` match), then the bare extension. """ name = path.rsplit("/", 1)[-1] table = _merged_table() if name in table: return table[name] if "." in name: ext = name.rsplit(".", 1)[-1] return table.get(ext) return None
[docs] def render_banner( *, target_name: str, path: str, if_exists: Literal["overwrite", "skip"], ) -> str: """Render the autogenerated banner for *path*, or ``""``. Returns the empty string when *path*'s file type has no registered comment style (so the caller prepends nothing). The wording is derived from *if_exists*: ``"overwrite"`` files are clobbered every run; ``"skip"`` files are one-shot scaffolds preserved on re-run. """ style = comment_style_for(path) if style is None: return "" cmd = f"`codegen generate --target {target_name}`" if if_exists == "skip": lines = [ f"AUTOGENERATED by {cmd}.", 'One-shot scaffold (if_exists="skip"): your edits are preserved', "on re-run. Pass --force-paths to regenerate it deliberately.", ] else: lines = [ f"AUTOGENERATED by {cmd} -- DO NOT EDIT.", "Overwritten on every run; edit the config or source, not here.", ] return style.render(lines)