Source code for datalad_core.constraints.exceptions

from __future__ import annotations

from textwrap import indent
from types import MappingProxyType
from typing import (
    Any,
)


[docs] class ConstraintError(ValueError): # we derive from ValueError, because it provides the seemingly best fit # of any built-in exception. It is defined as: # # Raised when an operation or function receives an argument that has # the right type but an inappropriate value, and the situation is not # described by a more precise exception such as IndexError. # # In general a validation error can also occur because of a TypeError, but # ultimately what also matters here is an ability to coerce a given value # to a target type/value, but such an exception is not among the built-ins. # Moreover, many pieces of existing code do raise ValueError in practice, # and we aim to be widely applicable with this specialized class """Exception type raised by constraints when their conditions are violated A primary purpose of this class is to provide uniform means for communicating structured information on violated constraints. """ def __init__( self, constraint, value: Any, msg: str, ctx: dict[str, Any] | None = None, ): """ Parameters ---------- constraint: Constraint Instance of the ``Constraint`` class that determined a violation. value: The value that is in violation of a constraint. msg: str A message describing the violation. If ``ctx`` is given too, the message can contain keyword placeholders in Python's ``format()`` syntax that will be applied on-access. ctx: dict, optional Mapping with context information on the violation. This information is used to interpolate a message, but may also contain additional key-value mappings. A recognized key is ``'__caused_by__'``, with a value of one exception (or a tuple of exceptions) that led to a ``ConstraintError`` being raised. """ # the msg/ctx setup is inspired by pydantic # we put `msg` in the `.args` container first to match where # `ValueError` would have it. Everything else goes after it. super().__init__(msg, constraint, value, ctx) @property def msg(self): """Obtain an (interpolated) message on the constraint violation The error message template can be interpolated with any information available in the error context dict (``ctx``). In addition to the information provided by the ``Constraint`` that raised the error, the following additional placeholders are provided: - ``__value__``: the value reported to have caused the error - ``__itemized_causes__``: an indented bullet list str with on item for each error in the ``caused_by`` report of the error. Message template can use any feature of the Python format mini language. For example ``{__value__!r}`` to get a ``repr()``-style representation of the offending value. """ msg_tmpl = self.args[0] # get interpolation values for message formatting # we need a copy, because we need to mutate the dict ctx = dict(self.context) # support a few standard placeholders # the verbatim value that caused the error: with !r and !s both # types of stringifications are accessible ctx['__value__'] = self.value if self.caused_by: ctx['__itemized_causes__'] = indent( '\n'.join(f'- {c!s}' for c in self.caused_by), ' ', ) return msg_tmpl.format(**ctx) @property def constraint(self): """Get the instance of the constraint that was violated""" return self.args[1] @property def caused_by(self) -> tuple[Exception] | None: """Returns a tuple of any underlying exceptions""" cb = self.context.get('__caused_by__', None) if cb is None: return None if isinstance(cb, Exception): return (cb,) return tuple(cb) @property def value(self): """Get the value that violated the constraint""" return self.args[2] @property def context(self) -> MappingProxyType: """Get a constraint violation's context This is a mapping of key/value-pairs matching the ``ctx`` constructor argument. """ return MappingProxyType(self.args[3] or {}) def __str__(self) -> str: return self.msg def __repr__(self) -> str: # rematch constructor arg-order, because we put `msg` first into # `.args` return '{0}({2!r}, {3!r}, {1!r}, {4!r})'.format( self.__class__.__name__, *self.args, )