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