304 lines
10 KiB
Python
304 lines
10 KiB
Python
# -*- encoding: utf-8 -*-
|
|
# Copyright 2020 the authors.
|
|
# This file is part of Hy, which is free software licensed under the Expat
|
|
# license. See the LICENSE.
|
|
import os
|
|
import re
|
|
import sys
|
|
import traceback
|
|
import pkgutil
|
|
|
|
from functools import reduce
|
|
from colorama import Fore
|
|
from contextlib import contextmanager
|
|
from hy import _initialize_env_var
|
|
|
|
_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS',
|
|
True)
|
|
COLORED = _initialize_env_var('HY_COLORED_ERRORS', False)
|
|
|
|
|
|
class HyError(Exception):
|
|
pass
|
|
|
|
|
|
class HyInternalError(HyError):
|
|
"""Unexpected errors occurring during compilation or parsing of Hy code.
|
|
|
|
Errors sub-classing this are not intended to be user-facing, and will,
|
|
hopefully, never be seen by users!
|
|
"""
|
|
|
|
|
|
class HyLanguageError(HyError):
|
|
"""Errors caused by invalid use of the Hy language.
|
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
"""
|
|
|
|
def __init__(self, message, expression=None, filename=None, source=None,
|
|
lineno=1, colno=1):
|
|
"""
|
|
Parameters
|
|
----------
|
|
message: str
|
|
The message to display for this error.
|
|
expression: HyObject, optional
|
|
The Hy expression generating this error.
|
|
filename: str, optional
|
|
The filename for the source code generating this error.
|
|
Expression-provided information will take precedence of this value.
|
|
source: str, optional
|
|
The actual source code generating this error. Expression-provided
|
|
information will take precedence of this value.
|
|
lineno: int, optional
|
|
The line number of the error. Expression-provided information will
|
|
take precedence of this value.
|
|
colno: int, optional
|
|
The column number of the error. Expression-provided information
|
|
will take precedence of this value.
|
|
"""
|
|
self.msg = message
|
|
self.compute_lineinfo(expression, filename, source, lineno, colno)
|
|
|
|
if isinstance(self, SyntaxError):
|
|
syntax_error_args = (self.filename, self.lineno, self.offset,
|
|
self.text)
|
|
super(HyLanguageError, self).__init__(message, syntax_error_args)
|
|
else:
|
|
super(HyLanguageError, self).__init__(message)
|
|
|
|
def compute_lineinfo(self, expression, filename, source, lineno, colno):
|
|
|
|
# NOTE: We use `SyntaxError`'s field names (i.e. `text`, `offset`,
|
|
# `msg`) for compatibility and print-outs.
|
|
self.text = getattr(expression, 'source', source)
|
|
self.filename = getattr(expression, 'filename', filename)
|
|
|
|
if self.text:
|
|
lines = self.text.splitlines()
|
|
|
|
self.lineno = getattr(expression, 'start_line', lineno)
|
|
self.offset = getattr(expression, 'start_column', colno)
|
|
end_column = getattr(expression, 'end_column',
|
|
len(lines[self.lineno-1]))
|
|
end_line = getattr(expression, 'end_line', self.lineno)
|
|
|
|
# Trim the source down to the essentials.
|
|
self.text = '\n'.join(lines[self.lineno-1:end_line])
|
|
|
|
if end_column:
|
|
if self.lineno == end_line:
|
|
self.arrow_offset = end_column
|
|
else:
|
|
self.arrow_offset = len(self.text[0])
|
|
|
|
self.arrow_offset -= self.offset
|
|
else:
|
|
self.arrow_offset = None
|
|
else:
|
|
# We could attempt to extract the source given a filename, but we
|
|
# don't.
|
|
self.lineno = lineno
|
|
self.offset = colno
|
|
self.arrow_offset = None
|
|
|
|
def __str__(self):
|
|
"""Provide an exception message that includes SyntaxError-like source
|
|
line information when available.
|
|
"""
|
|
# Syntax errors are special and annotate the traceback (instead of what
|
|
# we would do in the message that follows the traceback).
|
|
if isinstance(self, SyntaxError):
|
|
return super(HyLanguageError, self).__str__()
|
|
# When there isn't extra source information, use the normal message.
|
|
elif not self.text:
|
|
return super(HyLanguageError, self).__str__()
|
|
|
|
# Re-purpose Python's builtin syntax error formatting.
|
|
output = traceback.format_exception_only(
|
|
SyntaxError,
|
|
SyntaxError(self.msg, (self.filename, self.lineno, self.offset,
|
|
self.text)))
|
|
|
|
arrow_idx, _ = next(((i, x) for i, x in enumerate(output)
|
|
if x.strip() == '^'),
|
|
(None, None))
|
|
if arrow_idx:
|
|
msg_idx = arrow_idx + 1
|
|
else:
|
|
msg_idx, _ = next((i, x) for i, x in enumerate(output)
|
|
if x.startswith('SyntaxError: '))
|
|
|
|
# Get rid of erroneous error-type label.
|
|
output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx])
|
|
|
|
# Extend the text arrow, when given enough source info.
|
|
if arrow_idx and self.arrow_offset:
|
|
output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'),
|
|
'-' * (self.arrow_offset - 1))
|
|
|
|
if COLORED:
|
|
output[msg_idx:] = [Fore.YELLOW + o + Fore.RESET for o in output[msg_idx:]]
|
|
if arrow_idx:
|
|
output[arrow_idx] = Fore.GREEN + output[arrow_idx] + Fore.RESET
|
|
for idx, line in enumerate(output[::msg_idx]):
|
|
if line.strip().startswith(
|
|
'File "{}", line'.format(self.filename)):
|
|
output[idx] = Fore.RED + line + Fore.RESET
|
|
|
|
# This resulting string will come after a "<class-name>:" prompt, so
|
|
# put it down a line.
|
|
output.insert(0, '\n')
|
|
|
|
# Avoid "...expected str instance, ColoredString found"
|
|
return reduce(lambda x, y: x + y, output)
|
|
|
|
|
|
class HyCompileError(HyInternalError):
|
|
"""Unexpected errors occurring within the compiler."""
|
|
|
|
|
|
class HyTypeError(HyLanguageError, TypeError):
|
|
"""TypeError occurring during the normal use of Hy."""
|
|
|
|
|
|
class HyNameError(HyLanguageError, NameError):
|
|
"""NameError occurring during the normal use of Hy."""
|
|
|
|
|
|
class HyRequireError(HyLanguageError):
|
|
"""Errors arising during the use of `require`
|
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
"""
|
|
|
|
|
|
class HyMacroExpansionError(HyLanguageError):
|
|
"""Errors caused by invalid use of Hy macros.
|
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
"""
|
|
|
|
|
|
class HyEvalError(HyLanguageError):
|
|
"""Errors occurring during code evaluation at compile-time.
|
|
|
|
These errors distinguish unexpected errors within the compilation process
|
|
(i.e. `HyInternalError`s) from unrelated errors in user code evaluated by
|
|
the compiler (e.g. in `eval-and-compile`).
|
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
"""
|
|
|
|
|
|
class HyIOError(HyInternalError, IOError):
|
|
""" Subclass used to distinguish between IOErrors raised by Hy itself as
|
|
opposed to Hy programs.
|
|
"""
|
|
|
|
|
|
class HySyntaxError(HyLanguageError, SyntaxError):
|
|
"""Error during the Lexing of a Hython expression."""
|
|
|
|
|
|
class HyWrapperError(HyError, TypeError):
|
|
"""Errors caused by language model object wrapping.
|
|
|
|
These can be caused by improper user-level use of a macro, so they're
|
|
not really "internal". If they arise due to anything else, they're an
|
|
internal/compiler problem, though.
|
|
"""
|
|
|
|
|
|
def _module_filter_name(module_name):
|
|
try:
|
|
compiler_loader = pkgutil.get_loader(module_name)
|
|
if not compiler_loader:
|
|
return None
|
|
|
|
filename = compiler_loader.get_filename(module_name)
|
|
if not filename:
|
|
return None
|
|
|
|
if compiler_loader.is_package(module_name):
|
|
# Use the package directory (e.g. instead of `.../__init__.py`) so
|
|
# that we can filter all modules in a package.
|
|
return os.path.dirname(filename)
|
|
else:
|
|
# Normalize filename endings, because tracebacks will use `pyc` when
|
|
# the loader says `py`.
|
|
return filename.replace('.pyc', '.py')
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
_tb_hidden_modules = {m for m in map(_module_filter_name,
|
|
['hy.compiler', 'hy.lex',
|
|
'hy.cmdline', 'hy.lex.parser',
|
|
'hy.importer', 'hy._compat',
|
|
'hy.macros', 'hy.models',
|
|
'rply'])
|
|
if m is not None}
|
|
|
|
|
|
def hy_exc_filter(exc_type, exc_value, exc_traceback):
|
|
"""Produce exceptions print-outs with all frames originating from the
|
|
modules in `_tb_hidden_modules` filtered out.
|
|
|
|
The frames are actually filtered by each module's filename and only when a
|
|
subclass of `HyLanguageError` is emitted.
|
|
|
|
This does not remove the frames from the actual tracebacks, so debugging
|
|
will show everything.
|
|
"""
|
|
# frame = (filename, line number, function name*, text)
|
|
new_tb = []
|
|
for frame in traceback.extract_tb(exc_traceback):
|
|
if not (frame[0].replace('.pyc', '.py') in _tb_hidden_modules or
|
|
os.path.dirname(frame[0]) in _tb_hidden_modules):
|
|
new_tb += [frame]
|
|
|
|
lines = traceback.format_list(new_tb)
|
|
|
|
lines.insert(0, "Traceback (most recent call last):\n")
|
|
|
|
lines.extend(traceback.format_exception_only(exc_type, exc_value))
|
|
output = ''.join(lines)
|
|
|
|
return output
|
|
|
|
|
|
def hy_exc_handler(exc_type, exc_value, exc_traceback):
|
|
"""A `sys.excepthook` handler that uses `hy_exc_filter` to
|
|
remove internal Hy frames from a traceback print-out.
|
|
"""
|
|
if os.environ.get('HY_DEBUG', False):
|
|
return sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
|
|
try:
|
|
output = hy_exc_filter(exc_type, exc_value, exc_traceback)
|
|
sys.stderr.write(output)
|
|
sys.stderr.flush()
|
|
except Exception:
|
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
|
|
|
|
@contextmanager
|
|
def filtered_hy_exceptions():
|
|
"""Temporarily apply a `sys.excepthook` that filters Hy internal frames
|
|
from tracebacks.
|
|
|
|
Filtering can be controlled by the variable
|
|
`hy.errors._hy_filter_internal_errors` and environment variable
|
|
`HY_FILTER_INTERNAL_ERRORS`.
|
|
"""
|
|
global _hy_filter_internal_errors
|
|
if _hy_filter_internal_errors:
|
|
current_hook = sys.excepthook
|
|
sys.excepthook = hy_exc_handler
|
|
yield
|
|
sys.excepthook = current_hook
|
|
else:
|
|
yield
|