Source code for scenic.core.errors

"""Common exceptions and error handling."""

import importlib
import itertools
import pathlib
import pdb
import sys
import traceback
import types
import warnings

import scenic
import scenic.core
import scenic.syntax

## Configuration

[docs]def setDebuggingOptions( *, verbosity=0, fullBacktrace=False, debugExceptions=False, debugRejections=False ): """Configure Scenic's debugging options. Args: verbosity (int): Verbosity level. Zero by default, although the command-line interface uses 1 by default. See the :option:`--verbosity` option for the allowed values. fullBacktrace (bool): Whether to include Scenic's innards in backtraces (like the :option:`-b` command-line option). debugExceptions (bool): Whether to use `pdb` for post-mortem debugging of uncaught exceptions (like the :option:`--pdb` option). debugRejections (bool): Whether to enter `pdb` when a scene or simulation is rejected (like the :option:`--pdb-on-reject` option). """ global verbosityLevel, showInternalBacktrace, postMortemDebugging global postMortemRejections verbosityLevel = verbosity postMortemDebugging = debugExceptions postMortemRejections = debugRejections if debugExceptions or debugRejections: fullBacktrace = True # partial backtraces would be confusing in the debugger showInternalBacktrace = fullBacktrace
#: Verbosity level. See :option:`--verbosity` for the allowed values. verbosityLevel = 0 #: Whether or not to include Scenic's innards in backtraces. #: #: Set to True by default so that any errors during import of the scenic module #: will get full backtraces; the :mod:`scenic` module's ** sets it to False. showInternalBacktrace = True #: Whether or not to do post-mortem debugging of uncaught exceptions. postMortemDebugging = False #: Whether or not to do "post-mortem" debugging of rejected scenes/simulations. postMortemRejections = False # fmt: off #: Folders elided from backtraces when :obj:`showInternalBacktrace` is false. #: #: :meta hide-value: hiddenFolders = [ pathlib.Path(scenic.core.__file__).parent, # scenic.core submodules pathlib.Path(scenic.syntax.__file__).parent, # scenic.syntax submodules '<frozen importlib._bootstrap>', # parts of importlib used internally pathlib.Path(importlib.__file__).parent, ] # fmt: on ## Exceptions
[docs]class ScenicError(Exception): """An error produced during Scenic compilation, scene generation, or simulation.""" pass
[docs]class ScenicSyntaxError(ScenicError): """An error produced by attempting to parse an invalid Scenic program. This is intentionally not a subclass of SyntaxError so that pdb can be used for post-mortem debugging of the parser. Our custom excepthook below will arrange to still have it formatted as a SyntaxError, though. """ pass
[docs]class ParseCompileError(ScenicSyntaxError): """Error occurring during Scenic/Python parsing or compilation.""" def __init__(self, exc): self.msg = exc.args[0] self.filename, self.lineno = exc.filename, exc.lineno self.end_lineno = getattr(exc, "end_lineno", None) # added in Python 3.10 end_offset = getattr(exc, "end_offset", None) # ditto self.text, self.offset, self.end_offset = getText( self.filename, self.lineno, exc.text, exc.offset, end_offset ) super().__init__(self.msg) self.with_traceback(exc.__traceback__)
[docs]class ScenicParseError(ParseCompileError): """Error occuring during Scenic parsing or compilation.""" pass
[docs]class PythonCompileError(ParseCompileError): """Error occuring during Python compilation of translated Scenic code.""" pass
[docs]class ASTParseError(ScenicSyntaxError): """Parse error occuring during modification of the Python AST.""" def __init__(self, node, message, filename): self.msg = message self.lineno = node.lineno self.end_lineno = getattr(node, "end_lineno", None) # not always present end_offset = getattr(node, "end_col_offset", None) # ditto self.filename = filename self.text, self.offset, self.end_offset = getText( filename, node.lineno, offset=node.col_offset, end_offset=end_offset ) super().__init__(message)
[docs]class InvalidScenarioError(ScenicError): """Error raised for syntactically-valid but otherwise problematic Scenic programs.""" pass
[docs]class InconsistentScenarioError(InvalidScenarioError): """Error for scenarios with inconsistent requirements.""" def __init__(self, line, message): self.lineno = line super().__init__("Inconsistent requirement on line " + str(line) + ": " + message)
[docs]class SpecifierError(ScenicError): """Error for illegal uses of specifiers.""" pass
## Scenic backtraces def excepthook(ty, value, tb): if sys.version_info >= (3, 11) and issubclass(ty, BaseExceptionGroup): # Use the default mechanism since we don't handle ExceptionGroups sys.__excepthook__(ty, value, tb) else: displayScenicException(value) if postMortemDebugging: print("Uncaught exception. Entering post-mortem debugging") import pdb pdb.post_mortem(tb)
[docs]def displayScenicException(exc, seen=None): """Print a Scenic exception, cleaning up the traceback if desired. If ``showInternalBacktrace`` is False, this hides frames inside Scenic itself. """ ty = type(exc) tb = exc.__traceback__ # Recursively print the cause/context for this exception, if any. # (This code is adapted from IDLE.) if seen is None: seen = set() seen.add(id(exc)) cause = exc.__cause__ context = exc.__context__ if cause is not None and id(cause) not in seen: displayScenicException(cause, seen) print( "\nThe above exception was the direct cause of the following exception:\n", file=sys.stderr, ) elif context is not None and not exc.__suppress_context__ and id(context) not in seen: displayScenicException(context, seen) print( "\nDuring handling of the above exception, another exception occurred:\n", file=sys.stderr, ) if showInternalBacktrace: strings = ["Traceback (most recent call last):\n"] else: strings = [ "Traceback (most recent call last; use -b to show Scenic internals):\n" ] # Work out how to present the exception type pseudoSyntaxError = issubclass(ty, ScenicSyntaxError) if issubclass(ty, ScenicError): if issubclass(ty, ScenicSyntaxError) and not showInternalBacktrace: name = "ScenicSyntaxError" else: name = ty.__name__ if pseudoSyntaxError: # hack to get format_exception_only to format this like a bona fide SyntaxError bases = (SyntaxError,) else: bases = ty.__bases__ formatTy = type(name, bases, {}) if not showInternalBacktrace: formatTy.__module__ = "__main__" # hide qualified name of the exception else: formatTy = ty if issubclass(ty, SyntaxError) or (pseudoSyntaxError and not showInternalBacktrace): pass # no backtrace for these types of errors elif loc := getattr(exc, "_scenic_location", None): strings.extend(traceback.format_list([loc])) else: summary = traceback.extract_tb(tb) if showInternalBacktrace: filtered = summary else: filtered = [] skip = False for frame in summary: if skip: skip = False continue if == "callBeginningScenicTrace": filtered = [] skip = True elif includeFrame(frame): filtered.append(frame) if filtered: strings.extend(traceback.format_list(filtered)) else: strings.append(" <Scenic internals>\n") # Note: we can't directly call traceback.format_exception_only any more, # since as of Python 3.10 it ignores the exception class passed in and # uses type(exc) instead, foiling our formatTy hack. tbe = traceback.TracebackException(formatTy, exc, None) strings.extend(tbe.format_exception_only()) message = "".join(strings) print(message, end="", file=sys.stderr)
def includeFrame(frame): if frame.filename in hiddenFolders: return False parents = pathlib.Path(frame.filename).parents return not any(folder in parents for folder in hiddenFolders) # Install our excepthook if nobody else has already installed one; # we specially allow overwriting excepthooks from the following modules, # which are known to not cause problems: # - exceptiongroup (PEP 654 backport for pre-3.11) if sys.excepthook is not sys.__excepthook__ and not sys.excepthook.__module__.startswith( "exceptiongroup" ): warnings.warn("unable to install sys.excepthook to format Scenic backtraces") else: sys.excepthook = excepthook
[docs]def callBeginningScenicTrace(func): """Call the given function, starting the Scenic backtrace at that point. This function is just a convenience to make Scenic backtraces cleaner when running Scenic programs from the command line. """ return func()
def saveErrorLocation(): stack = traceback.extract_stack() for frame in reversed(stack): if includeFrame(frame): return frame return None ## Utilities def optionallyDebugRejection(exc=None): if not postMortemRejections: return print("Scene/simulation rejected. Entering debugger...") if exc: pdb.post_mortem(exc.__traceback__) else: debugger = pdb.Pdb() debugger.set_trace(sys._getframe().f_back) # TODO more portable way to do this?
[docs]def getText(filename, lineno, line="", offset=0, end_offset=None): """Attempt to recover the text of an error from the original Scenic file.""" if not filename or filename in {"<stream>", "<string>", "<unknown>"}: return line, offset, end_offset # don't bother with the filesystem try: with open(filename, "r") as f: line = list(itertools.islice(f, lineno - 1, lineno)) line = line[0] if line else "" # TODO improve reconstruction of error position? # (see TracebackException._format_syntax_error for spaces calculation below) rline = line.rstrip("\n") lline = rline.lstrip(" \n\f") spaces = len(rline) - len(lline) if end_offset is not None: adj_end_offset = min(end_offset, len(line) + 1) offset = max(spaces + 1, adj_end_offset - (end_offset - offset)) end_offset = adj_end_offset else: offset = min(offset, len(line)) except OSError: pass return line, offset, end_offset