2013-12-26 17:36:45 +01:00
|
|
|
# -*- encoding: utf-8 -*-
|
2019-02-07 14:57:35 +01:00
|
|
|
# Copyright 2019 the authors.
|
2017-04-27 23:16:57 +02:00
|
|
|
# This file is part of Hy, which is free software licensed under the Expat
|
|
|
|
# license. See the LICENSE.
|
2018-10-29 03:42:18 +01:00
|
|
|
import os
|
2018-10-13 06:25:43 +02:00
|
|
|
import re
|
2018-10-29 03:42:18 +01:00
|
|
|
import sys
|
2013-12-26 04:02:20 +01:00
|
|
|
import traceback
|
2018-10-29 03:42:18 +01:00
|
|
|
import pkgutil
|
2013-12-26 04:02:20 +01:00
|
|
|
|
2018-10-29 02:43:17 +01:00
|
|
|
from functools import reduce
|
2018-10-29 03:42:18 +01:00
|
|
|
from contextlib import contextmanager
|
|
|
|
from hy import _initialize_env_var
|
2018-10-29 02:43:17 +01:00
|
|
|
|
2014-12-06 20:15:59 +01:00
|
|
|
from clint.textui import colored
|
|
|
|
|
2018-10-29 03:42:18 +01:00
|
|
|
_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS',
|
|
|
|
True)
|
2018-10-29 04:02:08 +01:00
|
|
|
_hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2013-03-01 04:27:20 +01:00
|
|
|
|
|
|
|
class HyError(Exception):
|
2018-10-13 06:25:43 +02:00
|
|
|
pass
|
2018-10-29 02:43:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
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!
|
2013-03-03 02:38:18 +01:00
|
|
|
"""
|
2018-10-29 02:43:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HyLanguageError(HyError):
|
|
|
|
"""Errors caused by invalid use of the Hy language.
|
|
|
|
|
|
|
|
This, and any errors inheriting from this, are user-facing.
|
2013-03-03 02:38:18 +01:00
|
|
|
"""
|
Much better version of new error messages.
This version is much simpler.
At the point that the exception is raised, we don't have access to
the actual source, just the current expression. but as the
exception percolates up, we can intercept it, add the source and
the re-raise it.
Then at the final point, in the cmdline handler, we can choose to
let the entire traceback print, or just the simpler, direct error
message.
And even with the full traceback, the last bit is nicely formatted
just like the shorter, simpler message.
The error message is colored if clint is installed, but to avoid
yet another dependency, you get monochrome without clint.
I'm sure there is a better way to do the markup, the current method
is kludgy but works.
I wish there was more shared code between HyTypeError and LexException
but they are kind of different in some fundamental ways.
This doesn't work (yet) with runtime errors generated from Python,
like NameError, but I have a method that can catch NameError and turn it
into a more pleasing output.
Finally, there is no obvious way to raise HyTypeError from pure Hy code,
so methods in core/language.hy throw ugly TypeError/ValueError.
2013-12-22 20:56:03 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
def __init__(self, message, expression=None, filename=None, source=None,
|
|
|
|
lineno=1, colno=1):
|
2018-10-29 02:43:17 +01:00
|
|
|
"""
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
message: str
|
|
|
|
The message to display for this error.
|
|
|
|
expression: HyObject, optional
|
|
|
|
The Hy expression generating this error.
|
2018-10-13 06:25:43 +02:00
|
|
|
filename: str, optional
|
|
|
|
The filename for the source code generating this error.
|
|
|
|
Expression-provided information will take precedence of this value.
|
2018-10-29 02:43:17 +01:00
|
|
|
source: str, optional
|
2018-10-13 06:25:43 +02:00
|
|
|
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.
|
2018-10-29 02:43:17 +01:00
|
|
|
"""
|
2018-10-13 06:25:43 +02:00
|
|
|
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)
|
2018-10-29 02:43:17 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
# 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
|
2013-12-26 04:02:20 +01:00
|
|
|
|
|
|
|
def __str__(self):
|
2018-10-13 06:25:43 +02:00
|
|
|
"""Provide an exception message that includes SyntaxError-like source
|
|
|
|
line information when available.
|
|
|
|
"""
|
2018-10-29 04:02:08 +01:00
|
|
|
global _hy_colored_errors
|
2013-12-26 04:02:20 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
# 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.
|
|
|
|
if not isinstance(self, SyntaxError) and 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))
|
2013-12-26 04:02:20 +01:00
|
|
|
|
2018-10-29 04:02:08 +01:00
|
|
|
if _hy_colored_errors:
|
|
|
|
from clint.textui import colored
|
2018-10-13 06:25:43 +02:00
|
|
|
output[msg_idx:] = [colored.yellow(o) for o in output[msg_idx:]]
|
|
|
|
if arrow_idx:
|
|
|
|
output[arrow_idx] = colored.green(output[arrow_idx])
|
|
|
|
for idx, line in enumerate(output[::msg_idx]):
|
|
|
|
if line.strip().startswith(
|
|
|
|
'File "{}", line'.format(self.filename)):
|
|
|
|
output[idx] = colored.red(line)
|
|
|
|
|
|
|
|
# This resulting string will come after a "<class-name>:" prompt, so
|
|
|
|
# put it down a line.
|
|
|
|
output.insert(0, '\n')
|
2018-10-29 04:02:08 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
# Avoid "...expected str instance, ColoredString found"
|
|
|
|
return reduce(lambda x, y: x + y, output)
|
2017-09-20 19:40:52 +02:00
|
|
|
|
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
class HyCompileError(HyInternalError):
|
|
|
|
"""Unexpected errors occurring within the compiler."""
|
|
|
|
|
2017-09-20 19:40:52 +02:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
class HyTypeError(HyLanguageError, TypeError):
|
|
|
|
"""TypeError occurring during the normal use of Hy."""
|
2017-09-20 19:40:52 +02:00
|
|
|
|
2013-12-26 04:02:20 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
class HyNameError(HyLanguageError, NameError):
|
|
|
|
"""NameError occurring during the normal use of Hy."""
|
2013-12-26 04:02:20 +01:00
|
|
|
|
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
class HyRequireError(HyLanguageError):
|
|
|
|
"""Errors arising during the use of `require`
|
2013-12-26 04:02:20 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
class HyMacroExpansionError(HyLanguageError):
|
2018-10-29 02:43:17 +01:00
|
|
|
"""Errors caused by invalid use of Hy macros.
|
2015-02-16 23:27:18 +01:00
|
|
|
|
2018-10-29 02:43:17 +01:00
|
|
|
This, and any errors inheriting from this, are user-facing.
|
|
|
|
"""
|
2015-02-16 23:27:18 +01:00
|
|
|
|
2018-10-29 02:43:17 +01:00
|
|
|
|
|
|
|
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.
|
2015-02-16 23:27:18 +01:00
|
|
|
"""
|
2018-10-29 02:43:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HyIOError(HyInternalError, IOError):
|
|
|
|
""" Subclass used to distinguish between IOErrors raised by Hy itself as
|
|
|
|
opposed to Hy programs.
|
2015-02-16 23:27:18 +01:00
|
|
|
"""
|
2018-10-29 02:43:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HySyntaxError(HyLanguageError, SyntaxError):
|
|
|
|
"""Error during the Lexing of a Hython expression."""
|
|
|
|
|
|
|
|
|
2018-11-13 19:11:51 +01:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
def _module_filter_name(module_name):
|
|
|
|
try:
|
|
|
|
compiler_loader = pkgutil.get_loader(module_name)
|
|
|
|
if not compiler_loader:
|
|
|
|
return None
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
filename = compiler_loader.get_filename(module_name)
|
|
|
|
if not filename:
|
|
|
|
return None
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
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
|
2018-10-29 03:42:18 +01:00
|
|
|
|
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
_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}
|
2018-10-29 03:42:18 +01:00
|
|
|
|
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
def hy_exc_filter(exc_type, exc_value, exc_traceback):
|
2018-10-29 03:42:18 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-10-13 06:25:43 +02:00
|
|
|
# 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)
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-11-01 22:40:13 +01:00
|
|
|
lines.insert(0, "Traceback (most recent call last):\n")
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
lines.extend(traceback.format_exception_only(exc_type, exc_value))
|
|
|
|
output = ''.join(lines)
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
return output
|
2018-10-29 03:42:18 +01:00
|
|
|
|
2018-10-13 06:25:43 +02:00
|
|
|
|
|
|
|
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)
|
2018-10-29 03:42:18 +01:00
|
|
|
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
|