Source code for eris._errors

"""Custom Exceptions live here."""

from __future__ import annotations

import traceback
from typing import (
    Final,
    Iterator,
    List,
    Literal,
    Optional,
    TypedDict,
    TypeVar,
    Union,
)

from ion import efill
from metaman import Inspector, cname


Exc_T = TypeVar("Exc_T", bound=Exception)
T = TypeVar("T")


Null = Literal["null"]
Nullable = Union[Null, T]
ErisErrorChain = List["ErisErrorDict"]

FIRST_EXC_IS_WRONG_TYPE: Final = (
    "Logic error. The first exception returned by iterating over this"
    " exception should be THIS exception."
)
NULL: Final[Null] = "null"


class ExcInfo(TypedDict):
    """Represents a single exception."""

    exc_type: str
    exc_value: str
    exc_msg: Nullable[str]


[docs]class ErisErrorDict(TypedDict): """An Error type represented as a dictionary.""" # exception info exc_info: ExcInfo # metadata lineno: int module_name: str func_name: str file_name: str # optional traceback stack + exception that caused it stack: List[str] caused_by: Nullable[List[ExcInfo]]
[docs]class ErisError(Exception): """Custom general-purpose exception.""" def __init__(self, emsg: str, up: int = 0) -> None: self.inspector = Inspector(up=up + 1) super().__init__(emsg) def __str__(self) -> str: # noqa: D105 return self.__repr__() def __repr__(self) -> str: # noqa: D105 return self._repr() def _repr(self, width: int = 80) -> str: """ Format error to width. If width is None, return string suitable for traceback. """ super_str = super().__str__() emsg = efill(super_str, width, indent=2) return "{}::{}::{}::{}{{\n{}\n}}".format( cname(self), self.inspector.module_name, self.inspector.function_name, self.inspector.line_number, emsg, ) def __iter__(self) -> Iterator["BaseException"]: # noqa: D105 yield self e = self.__cause__ while e: yield e e = e.__cause__
[docs] def chain(self, other: Exception) -> "ErisError": """Chains this exception to another.""" return _chain_errors(self, other)
[docs] def to_json(self) -> ErisErrorChain: """Converts this error into a list of dictionaries. This list is JSON serializable and is composed of data on this exception and any that are chained to it. NOTE: The list is sorted from last-to-first exception to be raised (i.e. this exception, the exception that caused this exception, etc...). """ result = [] last_stack: Optional[List[str]] = None last_caused_by: Optional[List[ExcInfo]] = None for error in self: if isinstance(error, ErisError): caused_by = last_caused_by = [] stack = last_stack = list(error.inspector.lines) exc_info: ExcInfo = dict( exc_type=repr(type(error)), exc_value=repr(error), exc_msg=error.args[0], ) eris_error_dict: ErisErrorDict = dict( exc_info=exc_info, lineno=error.inspector.line_number, module_name=error.inspector.module_name, func_name=error.inspector.function_name, file_name=error.inspector.file_name, stack=stack, caused_by=caused_by, ) result.append(eris_error_dict) else: assert last_stack is not None, FIRST_EXC_IS_WRONG_TYPE assert last_caused_by is not None, FIRST_EXC_IS_WRONG_TYPE # extend the last Error's 'caused_by' list with this # Exception's info... exc_info_tuple: ExcInfo = { "exc_type": repr(type(error)), "exc_value": repr(error), "exc_msg": str(error.args[0]) if error.args else NULL, } last_caused_by.append(exc_info_tuple) # extend the stack by using lines from this Exception's # traceback... if tb := error.__traceback__: last_stack.extend(traceback.extract_tb(tb).format()) return result
def _chain_errors(e1: Exc_T, e2: Optional[Exception]) -> Exc_T: """Chain two exceptions together. This is the functional equivalent to ``raise e1 from e2``. Args: e1: An exception. e2: The exception we want to chain to ``e2``. Returns: ``e1`` after chaining ``e2`` to it. """ e: BaseException = e1 cause = e.__cause__ while cause: e = cause cause = e.__cause__ e.__cause__ = e2 return e1