Files
kevinveenbirkenbachandClaude Opus 5.5 caec064e6b fix(env): reach ip_range through the handler registry
builder.py imported utils.env.handlers.infinito.ip_range directly, while
ip_range imports EnvBuilder from builder for its annotations. CodeQL
reported that as py/unsafe-cyclic-import on PR #999. The import only
runs under TYPE_CHECKING, so nothing breaks at runtime, but builder now
takes ip_range from utils.env.handlers like ORDERED_HANDLERS, the path
every other handler module already uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:46:18 +02:00
..

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.