"""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 *__init__.py* 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 frame.name == "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