Files
computer-playbook/utils/env/handlers/README.md
kevinveenbirkenbachandClaude Opus 5 79ba511bb7 fix(env): keep the .env generator stdlib-only
The env generator runs on the bare bootstrap python, before any
dependency is installed. utils/distros.py reached PyYAML through
utils.cache.yaml, so `python -m cli.meta.env` died with
ModuleNotFoundError on every dev-runtime image that ships python3
without PyYAML.

utils/yaml_bootstrap.load_block reads the block-YAML subset the SPOT
files use with stdlib only. It raises BootstrapYamlError on anything it
cannot parse identically to yaml.safe_load, so meta/distros.yml fails
loud instead of drifting.

The per-line regex lint meant to guard this is gone: one hop of
indirection defeated it and it stayed green while the invariant was
broken. It is replaced by a test that runs the real generator with
every non-stdlib import blocked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:47:01 +02:00

3.9 KiB

utils/env/handlers 🧩

Per-variable mapping logic for the .env generator. For the package's overall role see README.md. For general README conventions see documentation.md.

Purpose 🎯

Each module in this directory owns exactly one variable, or one tightly-coupled group resolved by a single helper call. The orchestrator in builder.py iterates the registry in __init__.py and calls each handler's apply(eb, ctx) in order.

File Naming 🏷️

  • A handler module name MUST match the lowercase of the variable it owns:
    • INFINITO_CONTAINER -> infinito/container.py
    • INFINITO_GHCR_MIRROR_PREFIX -> infinito/ghcr_mirror_prefix.py
  • The infinito_ prefix in the filename MUST be present iff the owned key carries the INFINITO_ prefix.
  • Aggregate handlers (one helper call resolves several keys at once) MUST take the singular topic name without a key suffix. Example: infinito/inventory.py resolves INFINITO_INVENTORY_DIR, INFINITO_INVENTORY_FILE, and INFINITO_INVENTORY_HOST_VARS_FILE from one scripts/inventory/resolve.sh invocation.
  • The trivial static passthroughs live in two collector files: passthrough.py (always emitted) and gha_passthrough.py (emitted only when ctx.on_gha is true).

Module Shape 📐

Every handler module MUST expose the same surface so the registry can call it uniformly:

"""<one-line docstring: what variable, what it derives from>."""

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from utils.env.builder import BuildContext, EnvBuilder

KEY = "INFINITO_..."
COMMENT = "<single-line per-key comment, mirrors default.env style>"


def apply(eb: EnvBuilder, ctx: BuildContext) -> None: ...
  • KEY MUST be the owned variable's name.
  • COMMENT MUST be a single line, no em dashes, that ends up above the key in the generated .env.
  • apply MUST take (eb, ctx) and return None.
  • A handler that targets a conditional branch (GHA-only, NIX-only, ...) MUST self-check the relevant ctx flag and return early. The registry calls every handler unconditionally.

Import Rules 🔗

  • A handler MUST NOT import another handler. Cross-handler data flows through eb.get(...) / eb.set(...) only.
  • A handler MAY import stdlib, parser.py, runtime.py, and any repo module whose own import closure is stdlib-only. The whole closure MUST import and run on the bare bootstrap python, before any dependency is installed. utils.cache.files and yaml_bootstrap.py qualify; utils.cache.yaml does not, because it imports PyYAML. Read SPOT files with utils.cache.files.read_text plus utils.yaml_bootstrap.load_block or a stdlib line parse. load_block accepts block mappings, block lists and string scalars only, and raises BootstrapYamlError on everything else, so a SPOT file read this way MUST stay inside that subset. test_bare_bootstrap.py enforces this by running the generator with every non-stdlib import blocked.
  • A handler MUST NOT import from writer.py or builder.py at runtime. The EnvBuilder and BuildContext types MAY be referenced under TYPE_CHECKING for annotations only.

Registry 📋

  • __init__.py MUST list every handler module in ORDERED_HANDLERS.
  • The order in ORDERED_HANDLERS MUST respect data dependencies. Example: infinito_container runs after passthrough so it can read the resolved INFINITO_DISTRO.
  • Adding a handler MUST also bump the relevant baseline test in test_dotenv_generator.py when the new key is a static-default in default.env, so the drift test stays honest.