Skip to content

StrictVerifier

StrictVerifier

StrictVerifier()

Manages plugin lifecycle, sandbox context, and assertion verification.

Do NOT instantiate directly. Use tripwire's pytest integration:

# In your test:
tripwire.http.mock_response("GET", "/api", json={"ok": True})
with tripwire:
    response = requests.get("/api")
tripwire.http.assert_request("GET", "/api", status=200)

Direct instantiation bypasses forced assertion checking and will silently produce tests that pass without verifying anything.

To access the verifier in a fixture or helper: verifier = tripwire.current_verifier()

Source code in src/tripwire/_verifier.py
def __init__(self) -> None:
    # Detect direct instantiation outside pytest
    if not StrictVerifier._suppress_direct_warning:
        from tripwire._context import _current_test_verifier  # noqa: PLC0415

        if _current_test_verifier.get(None) is None:
            warnings.warn(
                "StrictVerifier instantiated directly. "
                "Use `with tripwire:` for proper assertion enforcement. "
                "Direct instantiation bypasses the pytest fixture that "
                "enforces verify_all() at teardown.",
                stacklevel=2,
            )
    self._plugins: list[BasePlugin] = []
    self._timeline: Timeline = Timeline()
    self._tripwire_config: dict[str, Any] = load_tripwire_config()
    self._auto_instantiate_plugins()

mock

mock(path)

Create an import-site mock. Path format: 'module:attr'.

Lazily creates MockPlugin if not already registered.

Source code in src/tripwire/_verifier.py
def mock(self, path: str) -> "ImportSiteMock":
    """Create an import-site mock. Path format: 'module:attr'.

    Lazily creates MockPlugin if not already registered.
    """

    mock_plugin = self._get_or_create_mock_plugin()
    return mock_plugin.create_import_site_mock(path, spy=False)

spy

spy(path)

Create an import-site spy. Path format: 'module:attr'.

Lazily creates MockPlugin if not already registered.

Source code in src/tripwire/_verifier.py
def spy(self, path: str) -> "ImportSiteMock":
    """Create an import-site spy. Path format: 'module:attr'.

    Lazily creates MockPlugin if not already registered.
    """
    mock_plugin = self._get_or_create_mock_plugin()
    return mock_plugin.create_import_site_mock(path, spy=True)

assert_interaction

assert_interaction(source, **expected)

Assert the next interaction matches source and expected fields.

Completeness enforcement: after matching by source_id, the interaction's plugin.assertable_fields() is called. Any field in the returned set that is absent from **expected raises MissingAssertionFieldsError immediately, before the field-value match check.

Source code in src/tripwire/_verifier.py
def assert_interaction(
    self,
    source: _HasSourceId,
    **expected: object,
) -> None:
    """Assert the next interaction matches source and expected fields.

    Completeness enforcement: after matching by source_id, the interaction's
    plugin.assertable_fields() is called. Any field in the returned set that
    is absent from **expected raises MissingAssertionFieldsError immediately,
    before the field-value match check.
    """
    self._assert_no_active_sandbox()
    source_id: str = source.source_id

    if _any_order_depth.get() > 0:
        # Two-pass in_any_order: Pass 1 — find an interaction matching source_id only,
        # then enforce completeness. Pass 2 — verify full field-value match.
        candidate = self._timeline.find_any_unasserted(lambda i: i.source_id == source_id)
        if candidate is None:
            remaining = self._timeline.all_unasserted()
            hint = self._format_mismatch_error(
                source_id, expected, actual=None, remaining=remaining
            )
            raise InteractionMismatchError(
                expected={"source_id": source_id, **expected},
                actual=None,
                hint=hint,
            )
        # Completeness check against the candidate
        required_fields = candidate.plugin.assertable_fields(candidate)
        missing = required_fields - set(expected.keys())
        if missing:
            raise MissingAssertionFieldsError(
                missing_fields=frozenset(missing),
                provided_fields=frozenset(expected.keys()),
            )
        # Detect all-wildcard assertions
        if expected and self._all_wildcards(expected):
            hint = candidate.plugin.format_assert_hint(candidate)
            raise AllWildcardAssertionError(interaction=candidate, hint=hint)
        # Pass 2 — full field-value match (searching all unasserted, not just candidate)
        interaction = self._timeline.find_any_unasserted(
            lambda i: i.source_id == source_id and i.plugin.matches(i, expected)
        )
        if interaction is None:
            remaining = self._timeline.all_unasserted()
            hint = self._format_mismatch_error(
                source_id, expected, actual=None, remaining=remaining
            )
            raise InteractionMismatchError(
                expected={"source_id": source_id, **expected},
                actual=None,
                hint=hint,
            )
    else:
        interaction = self._timeline.peek_next_unasserted()
        if interaction is None or interaction.source_id != source_id:
            remaining = self._timeline.all_unasserted()
            hint = self._format_mismatch_error(
                source_id, expected, actual=interaction, remaining=remaining
            )
            raise InteractionMismatchError(
                expected={"source_id": source_id, **expected},
                actual=interaction,
                hint=hint,
            )
        # Completeness enforcement: check for missing required fields
        required_fields = interaction.plugin.assertable_fields(interaction)
        missing = required_fields - set(expected.keys())
        if missing:
            raise MissingAssertionFieldsError(
                missing_fields=frozenset(missing),
                provided_fields=frozenset(expected.keys()),
            )
        # Detect all-wildcard assertions
        if expected and self._all_wildcards(expected):
            hint = interaction.plugin.format_assert_hint(interaction)
            raise AllWildcardAssertionError(interaction=interaction, hint=hint)
        if not interaction.plugin.matches(interaction, expected):
            remaining = self._timeline.all_unasserted()
            hint = self._format_mismatch_error(
                source_id, expected, actual=interaction, remaining=remaining
            )
            raise InteractionMismatchError(
                expected={"source_id": source_id, **expected},
                actual=interaction,
                hint=hint,
            )

    self._timeline.mark_asserted(interaction)

in_any_order

in_any_order()

Context manager: assertions within block matched in any order.

IMPORTANT: in_any_order() relaxes ordering globally across ALL plugins. It is not possible to relax ordering for only one plugin type. Any assert_interaction() call within this block will match any unasserted interaction regardless of which plugin (mock or HTTP) recorded it.

Source code in src/tripwire/_verifier.py
def in_any_order(self) -> "InAnyOrderContext":
    """Context manager: assertions within block matched in any order.

    IMPORTANT: in_any_order() relaxes ordering globally across ALL plugins.
    It is not possible to relax ordering for only one plugin type. Any
    assert_interaction() call within this block will match any unasserted
    interaction regardless of which plugin (mock or HTTP) recorded it.
    """
    self._assert_no_active_sandbox()
    return InAnyOrderContext()

verify_all

verify_all()

Run Enforcement 2 and 3. Called at teardown.

Only interactions with enforce=True are checked. Interactions recorded during individual mock activation (enforce=False) are not required to be asserted.

Source code in src/tripwire/_verifier.py
def verify_all(self) -> None:
    """Run Enforcement 2 and 3. Called at teardown.

    Only interactions with enforce=True are checked. Interactions recorded
    during individual mock activation (enforce=False) are not required to
    be asserted.
    """
    self._assert_no_active_sandbox()
    unasserted = [i for i in self._timeline.all_unasserted() if i.enforce]
    unused: list[tuple[BasePlugin, Any]] = []

    for plugin in self._plugins:
        for mock_config in plugin.get_unused_mocks():
            unused.append((plugin, mock_config))

    has_unasserted = bool(unasserted)
    has_unused = bool(unused)

    if not has_unasserted and not has_unused:
        return

    if has_unasserted and has_unused:
        raise VerificationError(
            unasserted=UnassertedInteractionsError(
                interactions=unasserted,
                hint=self._format_unasserted_error(unasserted),
            ),
            unused=UnusedMocksError(
                mocks=[mc for _, mc in unused],
                hint=self._format_unused_mocks_error(unused),
            ),
        )
    elif has_unasserted:
        raise UnassertedInteractionsError(
            interactions=unasserted,
            hint=self._format_unasserted_error(unasserted),
        )
    else:
        raise UnusedMocksError(
            mocks=[mc for _, mc in unused],
            hint=self._format_unused_mocks_error(unused),
        )

sandbox

sandbox()

Return SandboxContext for this verifier (sync + async).

Source code in src/tripwire/_verifier.py
def sandbox(self) -> "SandboxContext":
    """Return SandboxContext for this verifier (sync + async)."""
    return SandboxContext(self)

SandboxContext

SandboxContext(verifier)

Activates all plugins and mocks. Supports both sync (with) and async (async with).

Source code in src/tripwire/_verifier.py
def __init__(self, verifier: StrictVerifier) -> None:
    self._verifier = verifier
    self._token: contextvars.Token[Any] | None = None
    self._activated_mocks: list[Any] = []  # list[_BaseMock]
    # C4: sandbox_id allocated at _enter(); ContextVar token stashed so
    # _exit() (and the _enter() error path) can reset cleanly.
    self.sandbox_id: int | None = None
    self._sandbox_id_token: contextvars.Token[int | None] | None = None

InAnyOrderContext

InAnyOrderContext()

Context manager: assertions within block matched in any order.

Source code in src/tripwire/_verifier.py
def __init__(self) -> None:
    self._token: contextvars.Token[int] | None = None