Skip to content

feat(api): add prepare_end callback - #2063

Open
madduck wants to merge 1 commit into
pyinfra-dev:3.xfrom
madduck:feat/prepare-end-callback
Open

madduck wants to merge 1 commit into
pyinfra-dev:3.xfrom
madduck:feat/prepare-end-callback

Conversation

@madduck

@madduck madduck commented Nov 4, 2026

Copy link
Copy Markdown

feat(api): add prepare_end callback

What this does

Adds a prepare_end hook to BaseStateCallback, fired once per registered handler at
the end of the prepare phase — after the deploy code has run and change detection is
done, but before any operation executes.

This closes a gap in the callback surface: pyinfra can currently tell you when a host
connects, when an operation starts, and when a host succeeds or fails, but there is no
seam between "the inventory is prepared" and "the run begins". Whole-inventory work that
must happen once per run — cache invalidation, controller-side bookkeeping, or gathering a
fact from every host and caching the result — has nowhere natural to live.

prepare_end fires from the CLI right after _handle_commands returns, before the
diff-print, --dry, and debug exit branches. So it also runs for dry runs and debug dumps;
handlers learn the run mode through two flags:

  • dry — True for a --dry run.
  • debug — True for --debug-facts or --debug-operations.

A handler that only makes sense when operations will follow returns early when either flag
is set:

from pyinfra.api import BaseStateCallback

class CollectInventoryWideFacts(BaseStateCallback):
    def prepare_end(self, state, dry=False, debug=False):
        if dry or debug:
            return
        gather_facts_from_every_host(state.inventory.get_active_hosts())

state.add_callback_handler(CollectInventoryWideFacts())

An exception raised in the handler aborts the run.

Design decisions

Why prepare_end and not prepare_complete. The event names the end of the phase,
not the completion of an abstract thing, and prepare_end pairs with the existing
operation_start/operation_end vocabulary.

Fired from the CLI, not run_ops. "Prepare" is a phase the CLI drives; the API has no
prepare boundary of its own (the caller owns stage management). Firing from run_ops would
also double-fire, because the CLI calls run_ops after setting the stage. The CLI-only
home is what makes the dry/debug flags meaningful, since those are CLI concepts. This
is stated in the docstring.

Flags as keyword args with defaults. They are additive and optional, so an API-mode
handler that overrides prepare_end(state) and ignores run mode keeps working. This
matches the existing retry_count/max_retries parameters on the operation-host hooks.

Not fired for commands that end before prepare. A fact query exits inside
_handle_commands, and debug-inventory exits before the prepare stage, so neither fires
the hook. That is deliberate and documented.

Once per registered handler. This follows the existing callback contract — the hook
runs once for each handler registered on the state. Because a deploy file is executed once
per host, registering a handler in a deploy file installs one handler per host; the docs
call this out and recommend registering once before the run.

Tests

tests/test_cli/test_cli_callbacks.py runs the CLI end to end against a single @fake
host and a deploy fixture that records what prepare_end received, covering the normal
run, --dry, --debug-operations, and --debug-facts.

Checklist

  • uv run pytest --disable-warnings -m 'not end_to_end' — 2251 passed
  • scripts/dev-lint.sh — ruff, mypy (168 files), arguments-sync all clean
  • Docs updated (docs/api/index.md, docs/deploy-process.md) and CHANGELOG.md entry
  • Atomic scope — one feature, no drive-by edits

AI disclosure

This change was authored with AI assistance (a coding agent running against the pyinfra
tree). The human submitter reviewed the implementation, understands how it interacts with
the CLI stage machine and the callback dispatch in state.trigger_callbacks, and is
responsible for the final content.

🤖

Add a `prepare_end` callback to `BaseStateCallback`, fired once per
registered handler at the end of the prepare phase. The CLI fires it
after the deploy code has run and change detection is done, but before
any of the exit paths that follow, so it runs for dry runs and debug
dumps too.

The handler receives `dry` and `debug` flags so it can decide what to do
for those runs: work that only makes sense when operations will follow
should return early when either is set. An exception raised in the
handler aborts the run.

Signed-off-by: martin f. krafft <madduck@madduck.net>
@wowi42 wowi42 added new feature API API mode specific issues. CLI CLI mode specific issues. labels Nov 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API API mode specific issues. CLI CLI mode specific issues. new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants