Source code for genno.core.exceptions

import logging
from itertools import chain
from traceback import TracebackException, format_exception_only, format_list

log = logging.getLogger(__name__)


class ComputationError(Exception):
    """Wrapper to print intelligible exception information for :meth:`.Computer.get`.

    In order to aid in debugging, this helper:

    - Omits the parts of the stack trace that are internal to :mod:`dask`, and
    - Gives the key in the :attr:`.Computer.graph` and the computation/task that caused
      the exception.
    """

    def __init__(self, exc):
        self._exc = exc

    def __str__(self):
        """String representation.

        Most exception handling (Python, IPython, Jupyter) will print the traceback that
        led to `self` (i.e. the call to :meth:`.Computer.get`), followed by the string
        returned by this method.
        """
        try:
            return self._format()
        except Exception as format_exc:
            # Something went wrong during _format()
            log.error(
                f"Exception raised while formatting {self._exc}:\n" + repr(format_exc)
            )
            # Fall back to printing the underlying exception
            return str(self._exc)

    def _format(self):
        key, task, frames = process_dask_tb(self._exc)

        # Assemble the exception printout
        return "".join(
            chain(
                # Computer information for debugging
                [
                    f"computing {key} using:\n\n" if key else "",
                    f"{task}\n\n" if task else "",
                    "Use Computer.describe(...) to trace the computation.\n\n",
                    "Computation traceback:\n",
                ],
                # Traceback; omitting a few dask internal calls below execute_task
                format_list(frames),
                # Type and message of the original exception
                format_exception_only(self._exc.__class__, self._exc),
            )
        )


class KeyExistsError(KeyError):
    """Raised by :meth:`.Computer.add` when the target key exists."""

    def __str__(self):
        return f"key {repr(self.args[0])} already exists"


[docs]class MissingKeyError(KeyError): """Raised by :meth:`.Computer.add` when a required input key is missing.""" def __str__(self): return f"required keys {repr(self.args)} not defined"
def process_dask_tb(exc): """Process *exc* arising from :meth:`.Computer.get`. Returns a tuple with 3 elements: - The key of the computation. - The info key of the computation. - A list of traceback.FrameSummary objects, without locals, for *only* frames that are not internal to dask. """ key = task = None # Info about the computation that triggered *exc* frames = [] # Frames for an abbreviated stacktrace try: # Get a traceback with captured locals tbe = TracebackException.from_exception(exc, capture_locals=True) except Exception: # Some exception occurred when capturing locals; proceed without tbe = TracebackException.from_exception(exc) # Iterate over frames from the base of the stack # Initial frames are internal to dask dask_internal = True for frame in tbe.stack: if frame.name == "execute_task": # Current frame is the dask internal call to execute a task try: # Retrieve information about the key/task that triggered the # exception. These are not the raw values of variables, but # their string repr(). key = frame.locals["key"] task = frame.locals["task"] except (TypeError, KeyError): # pragma: no cover # No locals, or 'key' or 'task' not present pass # Subsequent frames are related to the exception dask_internal = False if not dask_internal: # Don't display the locals when printing the traceback frame.locals = None # Store the frame for printing the traceback frames.append(frame) # Omit a few dask internal calls below execute_task return key, task, frames[3:]