antithesis.catalog

This module gives read and write access to the assertion catalog: the set of assertion declarations in your code, whether or not they ever run.

It exists for local tooling rather than for Antithesis tests: enumerating the test properties a source tree declares, comparing them against the ones a local test run actually evaluated, failing CI when a change silently drops an assertion, or checking an inventory of test properties into the repository. Workloads use antithesis.assertions and antithesis.random instead and never need this module.

On the Antithesis platform the catalog is produced at build time by the platform's instrumentor; this module is just that machinery, in case you need it:

  • scan_tree / scan_source / scan_ast find assertion declarations in source, with the same scanner the platform runs. Nothing is imported or executed.
  • write serializes entries as registration events. load reads such a file back; it is what the ANTITHESIS_ASSERTION_CATALOG environment variable resolution uses. load(write(entries)) round-trips.
  • register declares entries to the active output handler at runtime, the way an instrumented process declares its catalog at startup.
  • assertions() lists the catalog registered for this process, resolved through ANTITHESIS_ASSERTION_CATALOG. Outside an instrumented container that variable is normally unset and the result is empty -- use scan_tree on your source instead.

The scanner only catalogs what the platform would: calls to the assertion functions of antithesis.assertions (under any import alias) with a literal message. Assertions issued through your own wrappers, or with dynamically built messages, are invisible to any static scan; declare those at runtime with register (or antithesis.assertions.assert_raw), which works both locally and on the platform.

A typical local workflow, with ANTITHESIS_SDK_LOCAL_OUTPUT set so assertions are logged to a file:

python -m antithesis.catalog src/ -o catalog.json
ANTITHESIS_ASSERTION_CATALOG=catalog.json pytest

The output file then opens with one "hit": false line per declared assertion, followed by the assertions the run actually evaluated ("hit": true) -- so the test properties your suite never exercised are the messages with no "hit": true line. Equivalently, a harness can skip the files entirely: call scan_tree at session start, register the result, and join in memory.

@dataclass(frozen=True)
class Assertion:

One assertion declaration.

There is one per assertion call site the catalog knows about, whether or not that code ever runs: the catalog is a static property of the source, not of any particular execution.

Assertion( id: str, message: str, kind: AssertionKind, location: SourceLocation)
id: str

The key under which Antithesis aggregates this assertion's evaluations into one test property. Currently equal to message; treat it as opaque.

message: str

The name of the test property, as shown in the triage report.

location: SourceLocation
class AssertionKind(builtins.str, enum.Enum):

Which assertion function a declaration came from.

This is the typed form of the display_type field of serialized assertion events; the enum value is that string, which is also how the triage report labels the property. It is distinct from AssertType, the three-way wire-level type, which cannot tell always from always_or_unreachable, or reachable from unreachable, without also consulting must_hit.

ALWAYS = <AssertionKind.ALWAYS: 'Always'>
ALWAYS_OR_UNREACHABLE = <AssertionKind.ALWAYS_OR_UNREACHABLE: 'AlwaysOrUnreachable'>
SOMETIMES = <AssertionKind.SOMETIMES: 'Sometimes'>
REACHABLE = <AssertionKind.REACHABLE: 'Reachable'>
UNREACHABLE = <AssertionKind.UNREACHABLE: 'Unreachable'>
display_type: str

str: The display_type string serialized events carry for this kind.

AssertType: The wire-level assertion type for this kind.

must_hit: bool

bool: Whether the property fails when the assertion is never reached.

@dataclass(frozen=True)
class SourceLocation:

Where an assertion was declared.

For scanned entries, file is relative to the scanned root; for loaded entries it is whatever the catalog file recorded. class_name is the enclosing class (serialized as class, which Python reserves) and function the enclosing function, or "" where there is none.

SourceLocation( file: str, begin_line: int, begin_column: int, class_name: str, function: str)
file: str
begin_line: int
begin_column: int
class_name: str
function: str
def assertions() -> List[Assertion]:

The assertion catalog registered for this process, resolved through the ANTITHESIS_ASSERTION_CATALOG environment variable.

Inside an instrumented container this is the catalog the platform produced for this program. Outside one the variable is normally unset and the result is empty: the local catalog is whatever you scanned or loaded yourself. Reading it registers nothing and emits nothing.

def load( path: Union[str, os.PathLike]) -> List[Assertion]:

Reads catalog entries back from a catalog file, or from a directory the platform's instrumentor populated (such as the one ANTITHESIS_ASSERTION_CATALOG points to inside a container).

Lines that do not parse as catalog entries are reported and skipped. Raises FileNotFoundError if path does not exist or is a directory in which no catalog can be located.

def register(entries: Iterable[Assertion]) -> None:

Declares catalog entries at runtime, emitting each one (with "hit": false) through the active output handler.

This is how an instrumented process declares its catalog at startup, and it works the same on the platform and locally -- so it is the right tool for declaring assertions no static scan can see (wrapped call sites, messages drawn from a data table), as well as for harnesses that scan at session start instead of shipping a catalog file.

Entries are emitted unconditionally: registering the same entry twice writes two declaration lines. Consumers should treat declarations as a set keyed by message.

def scan_ast( tree: ast.AST, relpath: str, *, verbose: bool = False) -> List[Assertion]:

The assertion declarations in an already-parsed module.

relpath names the module in each entry's location.file and should be relative to the scanned root.

def scan_source( source: str, relpath: str, *, verbose: bool = False) -> List[Assertion]:

The assertion declarations in one module's source text.

Raises SyntaxError if the source does not parse.

def scan_tree( root: str, *, verbose: bool = False) -> List[Assertion]:

The assertion declarations under a source directory.

Scans every file iter_sources yields. Files that do not parse are reported to stderr and skipped. Entries are in path order, so the result is stable for a given tree.

def write( entries: Iterable[Assertion], out: Union[str, os.PathLike, IO[str]]) -> None:

Writes catalog entries to out (a path or a text file object) as JSON lines. load reads it back.