Skip to content

servomexlib.errors

The typed exception hierarchy and ErrorContext.

servomexlib.errors

Typed exception hierarchy for :mod:servomexlib.

Every library exception inherits from :class:ServomexError and carries a structured :class:ErrorContext. The message is the human-readable summary; the context is the machine-readable detail (port, protocol, address, channel, register/function code, request/response bytes, elapsed time).

The pattern matches the *lib family: ErrorContext is a frozen dataclass(slots=True) whose extra mapping is always frozen into a read-only :class:types.MappingProxyType, and :meth:ServomexError.with_context does a slot-safe copy so an inner layer can raise and an outer layer enrich.

MRO caution. Cross-branch classes are kept single-rooted: every exception resolves :meth:__init__ and :attr:with_context through exactly one path. Where a class genuinely belongs to two branches (a sink dependency that is also a configuration problem) the two bases share the single ServomexError.__init__ and define no competing __slots__, so the MRO is unambiguous — see the construction/with_context round-trip test.

ErrorContext dataclass

ErrorContext(
    port=None,
    protocol=None,
    address=None,
    channel=None,
    register=None,
    function_code=None,
    request=None,
    response=None,
    elapsed_s=None,
    extra=_empty_extra(),
)

Structured context attached to every :class:ServomexError.

Fields are best-effort — missing data is None rather than raising.

extra accepts any Mapping and is always frozen into a read-only :class:types.MappingProxyType at construction so the shared empty sentinel can never be mutated through error.context.extra[k] = v.

merged

merged(**updates)

Return a new context with updates overlaid. Unknown keys go to extra.

Source code in src/servomexlib/errors.py
def merged(self, **updates: Any) -> Self:
    """Return a new context with ``updates`` overlaid. Unknown keys go to ``extra``."""
    known: dict[str, Any] = {}
    extra_updates: dict[str, Any] = {}
    for key, value in updates.items():
        if key in _CONTEXT_KNOWN_FIELDS:
            known[key] = value
        else:
            extra_updates[key] = value

    new_extra: Mapping[str, Any] = (
        MappingProxyType({**self.extra, **extra_updates}) if extra_updates else self.extra
    )
    return replace(self, **known, extra=new_extra)

ServomexCapabilityError

ServomexCapabilityError(message='', *, context=None)

Bases: ServomexError

An operation is not available on this device / mode.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexChecksumError

ServomexChecksumError(message='', *, context=None)

Bases: ServomexProtocolError

A continuous-frame checksum did not match the recomputed value.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexConfigurationError

ServomexConfigurationError(message='', *, context=None)

Bases: ServomexError

Configuration-level error (bad args, wrong confirm flag, etc.).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexConfirmationRequiredError

ServomexConfirmationRequiredError(
    message="", *, context=None
)

Bases: ServomexConfigurationError

A SafetyTier.STATEFUL op was attempted without confirm=True.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexConnectionError

ServomexConnectionError(message='', *, context=None)

Bases: ServomexTransportError

Could not open / lost the connection (or no recognised protocol on AUTO).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexError

ServomexError(message='', *, context=None)

Bases: Exception

Base class for every :mod:servomexlib exception.

Carries a typed :class:ErrorContext. The message is the human-readable summary; the context is the machine-readable detail.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

with_context

with_context(**updates)

Return a copy of this error with its context updated.

Useful when an inner layer raises and an outer layer wants to enrich the context (for instance adding port or elapsed_s).

Source code in src/servomexlib/errors.py
def with_context(self, **updates: Any) -> Self:
    """Return a copy of this error with its context updated.

    Useful when an inner layer raises and an outer layer wants to enrich
    the context (for instance adding ``port`` or ``elapsed_s``).
    """
    cls = type(self)
    new = cls.__new__(cls)
    new.args = self.args
    try:
        new.__dict__.update(self.__dict__)
    except AttributeError:  # pragma: no cover — no slotted subclass today
        for slot in getattr(cls, "__slots__", ()):
            if hasattr(self, slot):
                object.__setattr__(new, slot, getattr(self, slot))
    new.context = self.context.merged(**updates)
    new.__cause__ = self.__cause__
    new.__context__ = self.__context__
    new.__traceback__ = self.__traceback__
    return new

ServomexFrameError

ServomexFrameError(message='', *, context=None)

Bases: ServomexProtocolError

Structural frame error (wrong field count, truncated block).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexIllegalDataAddressError

ServomexIllegalDataAddressError(
    message="", *, context=None
)

Bases: ServomexModbusError

Modbus exception code 02 — data address not valid for the slave.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexIllegalFunctionError

ServomexIllegalFunctionError(message='', *, context=None)

Bases: ServomexModbusError

Modbus exception code 01 — function not supported by the slave.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexModbusError

ServomexModbusError(message='', *, context=None)

Bases: ServomexProtocolError

A Modbus exception response or engine-level Modbus failure.

Rooted under :class:ServomexProtocolError (single MRO path) so the inherited __init__ and :meth:~ServomexError.with_context resolve unambiguously; the per-exception-code subclasses below add no competing __init__ / __slots__.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexParseError

ServomexParseError(message='', *, context=None)

Bases: ServomexProtocolError

A frame could not be parsed (bad header, unparsable field).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexProtocolError

ServomexProtocolError(message='', *, context=None)

Bases: ServomexError

Protocol-level error (framing, parsing, checksum, mode mismatch).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexProtocolUnsupportedError

ServomexProtocolUnsupportedError(
    message="", *, context=None
)

Bases: ServomexProtocolError

The active protocol cannot perform this operation (e.g. autocal in continuous).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexSinkDependencyError

ServomexSinkDependencyError(message='', *, context=None)

Bases: ServomexSinkError, ServomexConfigurationError

A sink's optional backing library is not installed.

Multi-inherits :class:ServomexConfigurationError because a missing extra is a configuration problem from the caller's perspective. Both bases share the single :class:ServomexError __init__ and define no __slots__, so the MRO stays single-pathed (see the MRO caution above).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexSinkError

ServomexSinkError(message='', *, context=None)

Bases: ServomexError

Base class for errors raised by sinks (CSV, JSONL, SQLite, Parquet, Postgres).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexSinkSchemaError

ServomexSinkSchemaError(message='', *, context=None)

Bases: ServomexSinkError

A batch's shape is incompatible with the sink's locked schema.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexSinkWriteError

ServomexSinkWriteError(message='', *, context=None)

Bases: ServomexSinkError

The backing store rejected a write (driver exception wrapped via from).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexTimeoutError

ServomexTimeoutError(message='', *, context=None)

Bases: ServomexTransportError

A transport read or write timed out (or a Modbus request got no reply).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexTransportError

ServomexTransportError(message='', *, context=None)

Bases: ServomexError

I/O-layer error from the serial transport.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexUnsupportedCommandError

ServomexUnsupportedCommandError(
    message="", *, context=None
)

Bases: ServomexCapabilityError

The device does not support the requested command.

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

ServomexValidationError

ServomexValidationError(message='', *, context=None)

Bases: ServomexConfigurationError

Request validation failed before I/O (bad channel, group out of range).

Source code in src/servomexlib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT