Skip to content

ditto.exceptions

pytest-ditto's own exceptions subclass DittoException. Some misuse raises a standard TypeError or ValueError instead, and errors from recorders and backends propagate unchanged. Advisory warnings use the DittoWarning category.

DittoWarning

Bases: UserWarning

Category for all pytest-ditto advisory warnings.

Subclasses UserWarning, so a blanket ignore::UserWarning still applies; use ignore::ditto.exceptions.DittoWarning to silence only ditto's advisories, or error::ditto.exceptions.DittoWarning to escalate them.

DittoException

Bases: Exception

Base class for pytest-ditto's own exceptions.

Not every error ditto raises is one: misuse such as a snapshot key that isn't a str raises TypeError, and a target ditto can't resolve raises ValueError. Errors from a recorder or a backend, such as a value the recorder can't serialise or a storage failure, propagate unchanged.

AdditionalMarkError()

Bases: DittoException

Raised when a test has more than one record mark.

Marks on the test's function, class and module all count, so a function mark doesn't override a class or module mark: use one or the other.

Source code in ditto/exceptions.py
def __init__(self) -> None:
    super().__init__("Only one record mark is allowed per test.")

DittoMarkHasNoIOType()

Bases: DittoException

Raised when a record mark doesn't name a recorder.

The recorder name is the mark's first argument, and is required even when the mark only sets a target: write @ditto.json(target=...) rather than @ditto.record(target=...).

Source code in ditto/exceptions.py
def __init__(self) -> None:
    super().__init__(
        "A record mark must name its recorder as its first argument, such as "
        '@ditto.record("yaml"). To set only a target, name the default '
        "recorder: @ditto.json(target=...)."
    )

DittoUnknownRecorderError(name, available)

Bases: DittoException

Raised when a requested recorder is not installed.

Source code in ditto/exceptions.py
def __init__(self, name: str, available: list[str]) -> None:
    available_str = ", ".join(sorted(available)) if available else "(none)"
    super().__init__(
        f"Unknown ditto recorder {name!r}. Available recorders: {available_str}."
    )

DittoRecorderLoadError(name, distribution, cause, hint='')

Bases: DittoException

Raised when an installed recorder's entry point fails to load.

Source code in ditto/exceptions.py
def __init__(
    self, name: str, distribution: str, cause: BaseException, hint: str = ""
) -> None:
    message = (
        f"ditto recorder {name!r} from {distribution} failed to load: "
        f"{type(cause).__name__}: {cause}"
    )
    super().__init__(f"{message}. {hint}" if hint else message)

DittoRecorderConflictError

Bases: DittoException

Raised when a recorder registration breaks the plugin contract.

Raised when a conflicted recorder is looked up, and when register is given a name that would conflict. The message describes the conflict and names the distributions involved.

DittoBackendLoadError(scheme, distribution, cause)

Bases: DittoException

Raised when an installed backend's entry point fails to load.

Source code in ditto/exceptions.py
def __init__(self, scheme: str, distribution: str, cause: BaseException) -> None:
    super().__init__(
        f"ditto backend {scheme!r} from {distribution} failed to load: "
        f"{type(cause).__name__}: {cause}"
    )

DittoBackendConflictError

Bases: DittoException

Raised when a backend registration breaks the plugin contract.

Raised when a conflicted scheme is looked up. The message describes the conflict and names the distributions involved.

DittoBackendChangedError(scheme, uri)

Bases: DittoException

Raised when a target's backend would change partway through a session.

ditto treats a target URI as one store in the lock, verify and prune, so every backend for a target in a session must come from the same factory.

Source code in ditto/exceptions.py
def __init__(self, scheme: str, uri: str) -> None:
    super().__init__(
        f"The backend for {scheme!r} changed after {uri} was first used in "
        "this session. ditto builds every backend for a target with the same "
        "factory for the whole session, so set BACKEND_REGISTRY.overrides "
        "before any test uses the scheme, for example in a session-scoped "
        "fixture, or give the test a target of its own."
    )

DittoJSONSerializationError(path, detail)

Bases: DittoException

Raised for values outside ditto's strict JSON data model.

Source code in ditto/exceptions.py
def __init__(self, path: str, detail: str) -> None:
    super().__init__(
        f"Strict JSON rejected the value at {path}: {detail}. "
        "Use only exact built-in None, bool, int, finite float, str, list, "
        "and dict values with exact string keys, or select another recorder."
    )

DuplicateSnapshotKeyError(key)

Bases: DittoException

Raised when a test calls snapshot() twice with the same key.

Source code in ditto/exceptions.py
def __init__(self, key: str) -> None:
    super().__init__(
        f"Snapshot key '{key}' has already been used in this test. "
        "Each snapshot call within a test must use a unique key."
    )

DittoSnapshotNameCollisionError(storage_key, first, second)

Bases: DittoException

Raised when two different snapshots would be stored under one name.

A stored name ends in a short hash of the snapshot's identity, so this needs two snapshots whose readable labels and hashes both match.

Source code in ditto/exceptions.py
def __init__(self, storage_key: str, first: str, second: str) -> None:
    super().__init__(
        f"Snapshots {first!r} and {second!r} would both be stored as "
        f"{storage_key!r}. Change one of their keys or parametrize IDs."
    )

DittoSnapshotNameTooLongError(module, limit)

Bases: DittoException

Raised when a test module's path leaves no room for a snapshot's name.

A file:// snapshot's name starts with the module path, dotted, and must fit in the file system's limit for a file name.

Source code in ditto/exceptions.py
def __init__(self, module: str, limit: int) -> None:
    super().__init__(
        f"The test module path {module!r} is too long to name its snapshots "
        f"within {limit} bytes. Use a shorter path for the test file."
    )

DittoAmbiguousTargetError(context)

Bases: DittoException

Raised when a test's target is set in two conflicting ways.

A mark can't set both target= and target_profile=, and the ditto_target and ditto_target_profile ini options can't both be set.

Source code in ditto/exceptions.py
def __init__(self, context: str) -> None:
    super().__init__(context)

DittoUnknownProfileError(name, available)

Bases: DittoException

Raised when a test selects a target profile that isn't defined.

The message lists the profiles that are defined.

Source code in ditto/exceptions.py
def __init__(self, name: str, available: list[str]) -> None:
    available_str = ", ".join(sorted(available)) if available else "(none)"
    super().__init__(
        f"Unknown ditto target profile {name!r}. "
        f"Available profiles: {available_str}."
    )

DittoInvalidProfileError(name, detail=None)

Bases: DittoException

Raised when a target profile isn't a URI string or a valid table.

A table needs a string uri and may have a storage_options mapping; any other key is an error.

Source code in ditto/exceptions.py
def __init__(self, name: str, detail: str | None = None) -> None:
    message = detail or "expected a URI string or mapping with a 'uri' field."
    super().__init__(f"Invalid ditto target profile {name!r}: {message}")

DittoDuplicateProfileError(names)

Bases: DittoException

Raised when a profile name is defined in both places profiles come from.

Profiles come from the ditto_target_profiles fixture and the [tool.pytest-ditto.target_profiles] table in pyproject.toml; neither takes precedence, so a name defined in both is an error.

Source code in ditto/exceptions.py
def __init__(self, names: list[str]) -> None:
    duplicates = ", ".join(sorted(names))
    super().__init__(
        f"Duplicate ditto target profile name(s) defined in both "
        f"ditto_target_profiles fixture and pyproject.toml: {duplicates}."
    )

DittoLockFileError(detail)

Bases: DittoException

Raised when ditto.lock can't be read.

--ditto-verify and --ditto-prune report it and fail the run, and an ordinary run only warns. A --ditto-lock rebuild replaces the unreadable lock instead.

Source code in ditto/exceptions.py
def __init__(self, detail: str) -> None:
    super().__init__(f"Could not read ditto.lock: {detail}")

DittoLockFileVersionError(expected, got)

Bases: DittoLockFileError

Raised when ditto.lock has a format version this pytest-ditto can't read.

Usually a newer pytest-ditto wrote the lock; upgrade to read it.

Source code in ditto/exceptions.py
def __init__(self, expected: int, got: object) -> None:
    super().__init__(
        f"unsupported lock-file version {got!r}; expected {expected}. "
        "Upgrade pytest-ditto."
    )

DittoUnhashableStorageOptionsError(value)

Bases: DittoException

Raised when ditto_storage_options contains an unhashable value.

Source code in ditto/exceptions.py
def __init__(self, value: object) -> None:
    super().__init__(
        f"ditto_storage_options contains an unhashable value ({value!r}, "
        f"type {type(value).__name__}). Backend cache identity requires every "
        "storage option to be hashable: two resolutions of the same logical "
        "target would otherwise silently get separate backend instances, "
        "breaking session-lifecycle tracking (prune, lock) for that target. "
        "Pass a hashable equivalent, or wrap the value so identity can be "
        "established some other way."
    )