Source code for codegen.output
"""File output helpers for writing generated files to disk."""
import fnmatch
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from pathlib import Path
from codegen.spec import GeneratedFile
[docs]
def write_files(
files: Sequence[GeneratedFile],
out_dir: Path,
*,
force: bool = False,
force_paths: Iterable[str] | None = None,
) -> int:
"""Write generated files to disk, honoring per-file write policy.
Each file's :attr:`~codegen.spec.GeneratedFile.path` is joined
with *out_dir* to determine the target path. Parent
directories are created as needed.
The :attr:`~codegen.spec.GeneratedFile.if_exists` policy
decides what happens when the target already exists:
* ``"overwrite"`` (default for be output) -- replace
unconditionally.
* ``"skip"`` (be_root's bootstrap files) -- leave the
existing file untouched.
*force* and *force_paths* let the caller override ``"skip"``
back to ``"overwrite"`` from the CLI without changing the
file declarations themselves:
* ``force=True`` clobbers every ``"skip"`` file.
* ``force_paths={"main.py", "pyproject.toml"}`` clobbers only
those paths -- handy for resetting a single bootstrapped
file without touching the rest. Each entry is a glob
(:mod:`fnmatch`), so ``force_paths={"*.just"}`` resets every
justfile module and ``force_paths={"config/*"}`` the config
tree; a plain filename with no wildcard is an exact match.
Args:
files: Sequence of :class:`~codegen.spec.GeneratedFile`
objects.
out_dir: Root directory for output paths.
force: When ``True``, treat every file as
``"overwrite"`` regardless of its declared policy.
force_paths: Optional collection of glob patterns (matched
against :attr:`~codegen.spec.GeneratedFile.path`, which
is relative to *out_dir*) whose ``"skip"`` declaration
should be overridden to ``"overwrite"``. Ignored when
*force* is ``True``.
Returns:
Number of files written (skipped files do not count).
"""
forced = tuple(force_paths or ())
written = 0
def _is_forced(path: str) -> bool:
# ``fnmatch`` treats a wildcard-free pattern as an exact
# match, so explicit filenames keep working alongside globs
# like ``*.just``. ``*`` spans ``/`` -- ``config/*`` matches
# nested paths too.
return any(fnmatch.fnmatch(path, pattern) for pattern in forced)
for f in files:
target = out_dir / f.path
should_overwrite = (
f.if_exists == "overwrite" or force or _is_forced(f.path)
)
if not should_overwrite and target.exists():
continue
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(f.content)
# Flip ``+x`` on user/group/other for files declared
# executable -- shebang-bearing scripts emitted alongside a
# justfile or package.json that calls them as ``./scripts/
# foo.sh``. Done after ``write_text`` because the latter
# creates the file with the umask-derived default mode.
if f.executable:
mode = target.stat().st_mode | 0o111
target.chmod(mode)
written += 1
return written