"""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
_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 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)