diff --git a/hy/_compat.py b/hy/_compat.py
index 55180ca..2711445 100644
--- a/hy/_compat.py
+++ b/hy/_compat.py
@@ -40,6 +40,10 @@ if PY3:
finally:
traceback = None
+ code_obj_args = ['argcount', 'kwonlyargcount', 'nlocals', 'stacksize',
+ 'flags', 'code', 'consts', 'names', 'varnames',
+ 'filename', 'name', 'firstlineno', 'lnotab', 'freevars',
+ 'cellvars']
else:
def raise_from(value, from_value=None):
raise value
@@ -52,10 +56,30 @@ else:
traceback = None
''')
+ code_obj_args = ['argcount', 'nlocals', 'stacksize', 'flags', 'code',
+ 'consts', 'names', 'varnames', 'filename', 'name',
+ 'firstlineno', 'lnotab', 'freevars', 'cellvars']
+
raise_code = compile(raise_src, __file__, 'exec')
exec(raise_code)
+def rename_function(func, new_name):
+ """Creates a copy of a function and [re]sets the name at the code-object
+ level.
+ """
+ c = func.__code__
+ new_code = type(c)(*[getattr(c, 'co_{}'.format(a))
+ if a != 'name' else str(new_name)
+ for a in code_obj_args])
+
+ _fn = type(func)(new_code, func.__globals__, str(new_name),
+ func.__defaults__, func.__closure__)
+ _fn.__dict__.update(func.__dict__)
+
+ return _fn
+
+
def isidentifier(x):
if x in ('True', 'False', 'None', 'print'):
# `print` is special-cased here because Python 2's
diff --git a/hy/cmdline.py b/hy/cmdline.py
index f7cca52..a9f3af3 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -12,6 +12,7 @@ import os
import io
import importlib
import py_compile
+import traceback
import runpy
import types
@@ -20,8 +21,9 @@ import astor.code_gen
import hy
from hy.lex import hy_parse, mangle
from hy.lex.exceptions import PrematureEndOfInput
-from hy.compiler import HyASTCompiler, hy_compile, hy_eval
-from hy.errors import HySyntaxError, filtered_hy_exceptions
+from hy.compiler import HyASTCompiler, hy_eval, hy_compile, ast_compile
+from hy.errors import (HyLanguageError, HyRequireError, HyMacroExpansionError,
+ filtered_hy_exceptions, hy_exc_handler)
from hy.importer import runhy
from hy.completer import completion, Completer
from hy.macros import macro, require
@@ -50,29 +52,70 @@ builtins.quit = HyQuitter('quit')
builtins.exit = HyQuitter('exit')
+class HyCommandCompiler(object):
+ def __init__(self, module, ast_callback=None, hy_compiler=None):
+ self.module = module
+ self.ast_callback = ast_callback
+ self.hy_compiler = hy_compiler
+
+ def __call__(self, source, filename="", symbol="single"):
+ try:
+ hy_ast = hy_parse(source, filename=filename)
+ root_ast = ast.Interactive if symbol == 'single' else ast.Module
+
+ # Our compiler doesn't correspond to a real, fixed source file, so
+ # we need to [re]set these.
+ self.hy_compiler.filename = filename
+ self.hy_compiler.source = source
+ exec_ast, eval_ast = hy_compile(hy_ast, self.module, root=root_ast,
+ get_expr=True,
+ compiler=self.hy_compiler,
+ filename=filename, source=source)
+
+ if self.ast_callback:
+ self.ast_callback(exec_ast, eval_ast)
+
+ exec_code = ast_compile(exec_ast, filename, symbol)
+ eval_code = ast_compile(eval_ast, filename, 'eval')
+
+ return exec_code, eval_code
+ except PrematureEndOfInput:
+ # Save these so that we can reraise/display when an incomplete
+ # interactive command is given at the prompt.
+ sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+ return None
+
+
class HyREPL(code.InteractiveConsole, object):
def __init__(self, spy=False, output_fn=None, locals=None,
- filename=""):
-
- super(HyREPL, self).__init__(locals=locals,
- filename=filename)
+ filename=""):
# Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`).
- # Also, make sure it's properly introduced to `sys.modules` and
- # consistently use its namespace as `locals` from here on.
+ # We let `InteractiveConsole` initialize `self.locals` when it's
+ # `None`.
+ super(HyREPL, self).__init__(locals=locals,
+ filename=filename)
+
module_name = self.locals.get('__name__', '__console__')
+ # Make sure our newly created module is properly introduced to
+ # `sys.modules`, and consistently use its namespace as `self.locals`
+ # from here on.
self.module = sys.modules.setdefault(module_name,
types.ModuleType(module_name))
self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__
# Load cmdline-specific macros.
- require('hy.cmdline', module_name, assignments='ALL')
+ require('hy.cmdline', self.module, assignments='ALL')
self.hy_compiler = HyASTCompiler(self.module)
+ self.compile = HyCommandCompiler(self.module, self.ast_callback,
+ self.hy_compiler)
+
self.spy = spy
+ self.last_value = None
if output_fn is None:
self.output_fn = repr
@@ -90,13 +133,18 @@ class HyREPL(code.InteractiveConsole, object):
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols})
- def ast_callback(self, main_ast, expr_ast):
+ def ast_callback(self, exec_ast, eval_ast):
if self.spy:
- # Mush the two AST chunks into a single module for
- # conversion into Python.
- new_ast = ast.Module(main_ast.body +
- [ast.Expr(expr_ast.body)])
- print(astor.to_source(new_ast))
+ try:
+ # Mush the two AST chunks into a single module for
+ # conversion into Python.
+ new_ast = ast.Module(exec_ast.body +
+ [ast.Expr(eval_ast.body)])
+ print(astor.to_source(new_ast))
+ except Exception:
+ msg = 'Exception in AST callback:\n{}\n'.format(
+ traceback.format_exc())
+ self.write(msg)
def _error_wrap(self, error_fn, *args, **kwargs):
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
@@ -120,46 +168,50 @@ class HyREPL(code.InteractiveConsole, object):
def showtraceback(self):
self._error_wrap(super(HyREPL, self).showtraceback)
- def runsource(self, source, filename='', symbol='single'):
-
+ def runcode(self, code):
try:
- do = hy_parse(source, filename=filename)
- except PrematureEndOfInput:
- return True
- except HySyntaxError as e:
- self.showsyntaxerror(filename=filename)
- return False
-
- try:
- # Our compiler doesn't correspond to a real, fixed source file, so
- # we need to [re]set these.
- self.hy_compiler.filename = filename
- self.hy_compiler.source = source
- value = hy_eval(do, self.locals, self.module, self.ast_callback,
- compiler=self.hy_compiler, filename=filename,
- source=source)
+ eval(code[0], self.locals)
+ self.last_value = eval(code[1], self.locals)
+ # Don't print `None` values.
+ self.print_last_value = self.last_value is not None
except SystemExit:
raise
except Exception as e:
+ # Set this to avoid a print-out of the last value on errors.
+ self.print_last_value = False
+ self.showtraceback()
+
+ def runsource(self, source, filename='', symbol='exec'):
+ try:
+ res = super(HyREPL, self).runsource(source, filename, symbol)
+ except (HyMacroExpansionError, HyRequireError):
+ # We need to handle these exceptions ourselves, because the base
+ # method only handles `OverflowError`, `SyntaxError` and
+ # `ValueError`.
+ self.showsyntaxerror(filename)
+ return False
+ except (HyLanguageError):
+ # Our compiler will also raise `TypeError`s
self.showtraceback()
return False
- if value is not None:
- # Shift exisitng REPL results
- next_result = value
+ # Shift exisitng REPL results
+ if not res:
+ next_result = self.last_value
for sym in self._repl_results_symbols:
self.locals[sym], next_result = next_result, self.locals[sym]
# Print the value.
- try:
- output = self.output_fn(value)
- except Exception:
- self.showtraceback()
- return False
+ if self.print_last_value:
+ try:
+ output = self.output_fn(self.last_value)
+ except Exception:
+ self.showtraceback()
+ return False
- print(output)
+ print(output)
- return False
+ return res
@macro("koan")
@@ -215,9 +267,14 @@ def ideas_macro(ETname):
def run_command(source, filename=None):
- tree = hy_parse(source, filename=filename)
__main__ = importlib.import_module('__main__')
require("hy.cmdline", __main__, assignments="ALL")
+ try:
+ tree = hy_parse(source, filename=filename)
+ except HyLanguageError:
+ hy_exc_handler(*sys.exc_info())
+ return 1
+
with filtered_hy_exceptions():
hy_eval(tree, None, __main__, filename=filename, source=source)
return 0
@@ -259,12 +316,18 @@ def run_icommand(source, **kwargs):
source = f.read()
filename = source
else:
- filename = ''
+ filename = ''
+ hr = HyREPL(**kwargs)
with filtered_hy_exceptions():
- hr = HyREPL(**kwargs)
- hr.runsource(source, filename=filename, symbol='single')
- return run_repl(hr)
+ res = hr.runsource(source, filename=filename)
+
+ # If the command was prematurely ended, show an error (just like Python
+ # does).
+ if res:
+ hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
+
+ return run_repl(hr)
USAGE = "%(prog)s [-h | -i cmd | -c cmd | -m module | file | -] [arg] ..."
@@ -371,6 +434,9 @@ def cmdline_handler(scriptname, argv):
print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
e.filename, e.errno, e.strerror), file=sys.stderr)
sys.exit(e.errno)
+ except HyLanguageError:
+ hy_exc_handler(*sys.exc_info())
+ sys.exit(1)
# User did NOTHING!
return run_repl(spy=options.spy, output_fn=options.repl_output_fn)
@@ -440,14 +506,15 @@ def hy2py_main():
options = parser.parse_args(sys.argv[1:])
if options.FILE is None or options.FILE == '-':
+ filename = ''
source = sys.stdin.read()
- with filtered_hy_exceptions():
- hst = hy_parse(source, filename='')
else:
- with filtered_hy_exceptions(), \
- io.open(options.FILE, 'r', encoding='utf-8') as source_file:
+ filename = options.FILE
+ with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read()
- hst = hy_parse(source, filename=options.FILE)
+
+ with filtered_hy_exceptions():
+ hst = hy_parse(source, filename=filename)
if options.with_source:
# need special printing on Windows in case the
@@ -464,7 +531,7 @@ def hy2py_main():
print()
with filtered_hy_exceptions():
- _ast = hy_compile(hst, '__main__')
+ _ast = hy_compile(hst, '__main__', filename=filename, source=source)
if options.with_ast:
if PY3 and platform.system() == "Windows":
diff --git a/hy/compiler.py b/hy/compiler.py
index a02e71a..08e0c98 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -9,8 +9,8 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
-from hy.errors import (HyCompileError, HyTypeError, HyEvalError,
- HyInternalError)
+from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
+ HySyntaxError, HyEvalError, HyInternalError)
from hy.lex import mangle, unmangle
@@ -443,15 +443,18 @@ class HyASTCompiler(object):
# nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError!
raise
- except HyTypeError as e:
- reraise(type(e), e, None)
+ except HyLanguageError as e:
+ # These are expected errors that should be passed to the user.
+ reraise(type(e), e, sys.exc_info()[2])
except Exception as e:
+ # These are unexpected errors that will--hopefully--never be seen
+ # by the user.
f_exc = traceback.format_exc()
exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(f_exc)
reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
def _syntax_error(self, expr, message):
- return HyTypeError(message, self.filename, expr, self.source)
+ return HySyntaxError(message, expr, self.filename, self.source)
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False):
diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy
index 0c98f60..0d3d737 100644
--- a/hy/core/bootstrap.hy
+++ b/hy/core/bootstrap.hy
@@ -15,13 +15,13 @@
(raise
(hy.errors.HyTypeError
(% "received a `%s' instead of a symbol for macro name"
- (. (type name) --name--))
- --file-- macro-name None)))
+ (. (type name) __name__))
+ None --file-- None)))
(for [kw '[&kwonly &kwargs]]
(if* (in kw lambda-list)
(raise (hy.errors.HyTypeError (% "macros cannot use %s"
kw)
- --file-- macro-name None))))
+ macro-name --file-- None))))
;; this looks familiar...
`(eval-and-compile
(import hy)
@@ -46,10 +46,10 @@
(raise (hy.errors.HyTypeError
(% "received a `%s' instead of a symbol for tag macro name"
(. (type tag-name) --name--))
- --file-- tag-name None)))
+ tag-name --file-- None)))
(if (or (= tag-name ":")
(= tag-name "&"))
- (raise (NameError (% "%s can't be used as a tag macro name" tag-name))))
+ (raise (hy.errors.HyNameError (% "%s can't be used as a tag macro name" tag-name))))
(setv tag-name (.replace (hy.models.HyString tag-name)
tag-name))
`(eval-and-compile
diff --git a/hy/errors.py b/hy/errors.py
index 9ca823e..0579e96 100644
--- a/hy/errors.py
+++ b/hy/errors.py
@@ -3,6 +3,7 @@
# 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
@@ -19,9 +20,7 @@ _hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
class HyError(Exception):
- def __init__(self, message, *args):
- self.message = message
- super(HyError, self).__init__(message, *args)
+ pass
class HyInternalError(HyError):
@@ -31,9 +30,6 @@ class HyInternalError(HyError):
hopefully, never be seen by users!
"""
- def __init__(self, message, *args):
- super(HyInternalError, self).__init__(message, *args)
-
class HyLanguageError(HyError):
"""Errors caused by invalid use of the Hy language.
@@ -41,8 +37,127 @@ class HyLanguageError(HyError):
This, and any errors inheriting from this, are user-facing.
"""
- def __init__(self, message, *args):
- super(HyLanguageError, self).__init__(message, *args)
+ 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.
+ """
+ global _hy_colored_errors
+
+ # 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))
+
+ if _hy_colored_errors:
+ from clint.textui import colored
+ 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 ":" 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):
@@ -50,88 +165,21 @@ class HyCompileError(HyInternalError):
class HyTypeError(HyLanguageError, TypeError):
- """TypeErrors occurring during the normal use of Hy."""
-
- def __init__(self, message, filename=None, expression=None, source=None):
- """
- Parameters
- ----------
- message: str
- The message to display for this error.
- filename: str, optional
- The filename for the source code generating this error.
- expression: HyObject, optional
- The Hy expression generating this error.
- source: str, optional
- The actual source code generating this error.
- """
- self.message = message
- self.filename = filename
- self.expression = expression
- self.source = source
-
- super(HyTypeError, self).__init__(message, filename, expression,
- source)
-
- def __str__(self):
- global _hy_colored_errors
-
- result = ""
-
- if _hy_colored_errors:
- from clint.textui import colored
- red, green, yellow = colored.red, colored.green, colored.yellow
- else:
- red = green = yellow = lambda x: x
-
- if all(getattr(self.expression, x, None) is not None
- for x in ("start_line", "start_column", "end_column")):
-
- line = self.expression.start_line
- start = self.expression.start_column
- end = self.expression.end_column
-
- source = []
- if self.source is not None:
- source = self.source.split("\n")[line-1:self.expression.end_line]
-
- if line == self.expression.end_line:
- length = end - start
- else:
- length = len(source[0]) - start
-
- result += ' File "%s", line %d, column %d\n\n' % (self.filename,
- line,
- start)
-
- if len(source) == 1:
- result += ' %s\n' % red(source[0])
- result += ' %s%s\n' % (' '*(start-1),
- green('^' + '-'*(length-1) + '^'))
- if len(source) > 1:
- result += ' %s\n' % red(source[0])
- result += ' %s%s\n' % (' '*(start-1),
- green('^' + '-'*length))
- if len(source) > 2: # write the middle lines
- for line in source[1:-1]:
- result += ' %s\n' % red("".join(line))
- result += ' %s\n' % green("-"*len(line))
-
- # write the last line
- result += ' %s\n' % red("".join(source[-1]))
- result += ' %s\n' % green('-'*(end-1) + '^')
-
- else:
- result += ' File "%s", unknown location\n' % self.filename
-
- result += yellow("%s: %s\n\n" %
- (self.__class__.__name__,
- self.message))
-
- return result
+ """TypeError occurring during the normal use of Hy."""
-class HyMacroExpansionError(HyTypeError):
+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.
@@ -158,97 +206,39 @@ class HyIOError(HyInternalError, IOError):
class HySyntaxError(HyLanguageError, SyntaxError):
"""Error during the Lexing of a Hython expression."""
- def __init__(self, message, filename=None, lineno=-1, colno=-1,
- source=None):
- """
- Parameters
- ----------
- message: str
- The exception's message.
- filename: str, optional
- The filename for the source code generating this error.
- lineno: int, optional
- The line number of the error.
- colno: int, optional
- The column number of the error.
- source: str, optional
- The actual source code generating this error.
- """
- self.message = message
- self.filename = filename
- self.lineno = lineno
- self.colno = colno
- self.source = source
- super(HySyntaxError, self).__init__(message,
- # The builtin `SyntaxError` needs a
- # tuple.
- (filename, lineno, colno, source))
- @staticmethod
- def from_expression(message, expression, filename=None, source=None):
- if not source:
- # Maybe the expression object has its own source.
- source = getattr(expression, 'source', None)
+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:
- filename = getattr(expression, 'filename', None)
+ return None
- if source:
- lineno = expression.start_line
- colno = expression.start_column
- end_line = getattr(expression, 'end_line', len(source))
- lines = source.splitlines()
- source = '\n'.join(lines[lineno-1:end_line])
+ 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:
- # We could attempt to extract the source given a filename, but we
- # don't.
- lineno = colno = -1
-
- return HySyntaxError(message, filename, lineno, colno, source)
-
- def __str__(self):
- global _hy_colored_errors
-
- output = traceback.format_exception_only(SyntaxError,
- SyntaxError(*self.args))
-
- if _hy_colored_errors:
- from hy.errors import colored
- output[-1] = colored.yellow(output[-1])
- if len(self.source) > 0:
- output[-2] = colored.green(output[-2])
- for line in output[::-2]:
- if line.strip().startswith(
- 'File "{}", line'.format(self.filename)):
- break
- output[-3] = colored.red(output[-3])
-
- # Avoid "...expected str instance, ColoredString found"
- return reduce(lambda x, y: x + y, output)
+ # Normalize filename endings, because tracebacks will use `pyc` when
+ # the loader says `py`.
+ return filename.replace('.pyc', '.py')
+ except Exception:
+ return None
-def _get_module_info(module):
- compiler_loader = pkgutil.get_loader(module)
- is_pkg = compiler_loader.is_package(module)
- filename = compiler_loader.get_filename()
- if is_pkg:
- # Use package directory
- return os.path.dirname(filename)
- else:
- # Normalize filename endings, because tracebacks will use `pyc` when
- # the loader says `py`.
- return filename.replace('.pyc', '.py')
+_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}
-_tb_hidden_modules = {_get_module_info(m)
- for m in ['hy.compiler', 'hy.lex',
- 'hy.cmdline', 'hy.lex.parser',
- 'hy.importer', 'hy._compat',
- 'hy.macros', 'hy.models',
- 'rply']}
-
-
-def hy_exc_handler(exc_type, exc_value, exc_traceback):
+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.
@@ -258,20 +248,33 @@ def hy_exc_handler(exc_type, exc_value, exc_traceback):
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)
+
+ if lines:
+ 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:
- # frame = (filename, line number, function name*, text)
- new_tb = [frame 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)]
-
- lines = traceback.format_list(new_tb)
-
- if lines:
- lines.insert(0, "Traceback (most recent call last):\n")
-
- lines.extend(traceback.format_exception_only(exc_type, exc_value))
- output = ''.join(lines)
-
+ output = hy_exc_filter(exc_type, exc_value, exc_traceback)
sys.stderr.write(output)
sys.stderr.flush()
except Exception:
diff --git a/hy/lex/__init__.py b/hy/lex/__init__.py
index f1465cd..eb3ac41 100644
--- a/hy/lex/__init__.py
+++ b/hy/lex/__init__.py
@@ -8,10 +8,9 @@ import re
import sys
import unicodedata
-from hy._compat import str_type, isidentifier, UCS4, reraise
+from hy._compat import str_type, isidentifier, UCS4
from hy.lex.exceptions import PrematureEndOfInput, LexException # NOQA
from hy.models import HyExpression, HySymbol
-from hy.errors import HySyntaxError
try:
from io import StringIO
@@ -35,15 +34,12 @@ def hy_parse(source, filename=''):
out : HyExpression
"""
_source = re.sub(r'\A#!.*', '', source)
- try:
- res = HyExpression([HySymbol("do")] +
- tokenize(_source + "\n",
- filename=filename))
- res.source = source
- res.filename = filename
- return res
- except HySyntaxError as e:
- reraise(type(e), e, None)
+ res = HyExpression([HySymbol("do")] +
+ tokenize(_source + "\n",
+ filename=filename))
+ res.source = source
+ res.filename = filename
+ return res
class ParserState(object):
@@ -70,8 +66,12 @@ def tokenize(source, filename=None):
state=ParserState(source, filename))
except LexingError as e:
pos = e.getsourcepos()
- raise LexException("Could not identify the next token.", filename,
- pos.lineno, pos.colno, source)
+ raise LexException("Could not identify the next token.",
+ None, filename, source,
+ max(pos.lineno, 1),
+ max(pos.colno, 1))
+ except LexException as e:
+ raise e
mangle_delim = 'X'
diff --git a/hy/lex/exceptions.py b/hy/lex/exceptions.py
index fb9aa7a..449119a 100644
--- a/hy/lex/exceptions.py
+++ b/hy/lex/exceptions.py
@@ -8,26 +8,31 @@ class LexException(HySyntaxError):
@classmethod
def from_lexer(cls, message, state, token):
+ lineno = None
+ colno = None
+ source = state.source
source_pos = token.getsourcepos()
- if token.source_pos:
+
+ if source_pos:
lineno = source_pos.lineno
colno = source_pos.colno
+ elif source:
+ # Use the end of the last line of source for `PrematureEndOfInput`.
+ # We get rid of empty lines and spaces so that the error matches
+ # with the last piece of visible code.
+ lines = source.rstrip().splitlines()
+ lineno = lineno or len(lines)
+ colno = colno or len(lines[lineno - 1])
else:
- lineno = -1
- colno = -1
+ lineno = lineno or 1
+ colno = colno or 1
- if state.source:
- lines = state.source.splitlines()
- if lines[-1] == '':
- del lines[-1]
-
- if lineno < 1:
- lineno = len(lines)
- if colno < 1:
- colno = len(lines[-1])
-
- source = lines[lineno - 1]
- return cls(message, state.filename, lineno, colno, source)
+ return cls(message,
+ None,
+ state.filename,
+ source,
+ lineno,
+ colno)
class PrematureEndOfInput(LexException):
diff --git a/hy/lex/parser.py b/hy/lex/parser.py
index f5cd5e5..c4df2a5 100755
--- a/hy/lex/parser.py
+++ b/hy/lex/parser.py
@@ -6,7 +6,6 @@
from __future__ import unicode_literals
from functools import wraps
-import re, unicodedata
from rply import ParserGenerator
@@ -278,15 +277,6 @@ def symbol_like(obj):
def error_handler(state, token):
tokentype = token.gettokentype()
if tokentype == '$end':
- source_pos = token.source_pos or token.getsourcepos()
- source = state.source
- if source_pos:
- lineno = source_pos.lineno
- colno = source_pos.colno
- else:
- lineno = -1
- colno = -1
-
raise PrematureEndOfInput.from_lexer("Premature end of input", state,
token)
else:
diff --git a/hy/macros.py b/hy/macros.py
index 274702f..d668077 100644
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -5,13 +5,15 @@ import sys
import importlib
import inspect
import pkgutil
+import traceback
from contextlib import contextmanager
-from hy._compat import PY3, string_types, reraise
+from hy._compat import PY3, string_types, reraise, rename_function
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle
-from hy.errors import HyTypeError, HyMacroExpansionError
+from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
+ HyRequireError)
try:
# Check if we have the newer inspect.signature available.
@@ -50,7 +52,7 @@ def macro(name):
"""
name = mangle(name)
def _(fn):
- fn.__name__ = '({})'.format(name)
+ fn = rename_function(fn, name)
try:
fn._hy_macro_pass_compiler = has_kwargs(fn)
except Exception:
@@ -75,7 +77,7 @@ def tag(name):
if not PY3:
_name = _name.encode('UTF-8')
- fn.__name__ = _name
+ fn = rename_function(fn, _name)
module = inspect.getmodule(fn)
@@ -150,7 +152,6 @@ def require(source_module, target_module, assignments, prefix=""):
out: boolean
Whether or not macros and tags were actually transferred.
"""
-
if target_module is None:
parent_frame = inspect.stack()[1][0]
target_namespace = parent_frame.f_globals
@@ -161,7 +162,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif inspect.ismodule(target_module):
target_namespace = target_module.__dict__
else:
- raise TypeError('`target_module` is not a recognized type: {}'.format(
+ raise HyTypeError('`target_module` is not a recognized type: {}'.format(
type(target_module)))
# Let's do a quick check to make sure the source module isn't actually
@@ -173,14 +174,17 @@ def require(source_module, target_module, assignments, prefix=""):
return False
if not inspect.ismodule(source_module):
- source_module = importlib.import_module(source_module)
+ try:
+ source_module = importlib.import_module(source_module)
+ except ImportError as e:
+ reraise(HyRequireError, HyRequireError(e.args[0]), None)
source_macros = source_module.__dict__.setdefault('__macros__', {})
source_tags = source_module.__dict__.setdefault('__tags__', {})
if len(source_module.__macros__) + len(source_module.__tags__) == 0:
if assignments != "ALL":
- raise ImportError('The module {} has no macros or tags'.format(
+ raise HyRequireError('The module {} has no macros or tags'.format(
source_module))
else:
return False
@@ -205,7 +209,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif _name in source_module.__tags__:
target_tags[alias] = source_tags[_name]
else:
- raise ImportError('Could not require name {} from {}'.format(
+ raise HyRequireError('Could not require name {} from {}'.format(
_name, source_module))
return True
@@ -239,50 +243,33 @@ def load_macros(module):
if k not in module_tags})
-def make_empty_fn_copy(fn):
- try:
- # This might fail if fn has parameters with funny names, like o!n. In
- # such a case, we return a generic function that ensures the program
- # can continue running. Unfortunately, the error message that might get
- # raised later on while expanding a macro might not make sense at all.
-
- formatted_args = format_args(fn)
- fn_str = 'lambda {}: None'.format(
- formatted_args.lstrip('(').rstrip(')'))
- empty_fn = eval(fn_str)
-
- except Exception:
-
- def empty_fn(*args, **kwargs):
- None
-
- return empty_fn
-
-
@contextmanager
def macro_exceptions(module, macro_tree, compiler=None):
try:
yield
+ except HyLanguageError as e:
+ # These are user-level Hy errors occurring in the macro.
+ # We want to pass them up to the user.
+ reraise(type(e), e, sys.exc_info()[2])
except Exception as e:
- try:
- filename = inspect.getsourcefile(module)
- source = inspect.getsource(module)
- except TypeError:
- if compiler:
- filename = compiler.filename
- source = compiler.source
- if not isinstance(e, HyTypeError):
- exc_type = HyMacroExpansionError
- msg = "expanding `{}': ".format(macro_tree[0])
- msg += str(e).replace("()", "", 1).strip()
+ if compiler:
+ filename = compiler.filename
+ source = compiler.source
else:
- exc_type = HyTypeError
- msg = e.message
+ filename = None
+ source = None
- reraise(exc_type,
- exc_type(msg, filename, macro_tree, source),
- sys.exc_info()[2].tb_next)
+ exc_msg = ' '.join(traceback.format_exception_only(
+ sys.exc_info()[0], sys.exc_info()[1]))
+
+ msg = "expanding macro {}\n ".format(str(macro_tree[0]))
+ msg += exc_msg
+
+ reraise(HyMacroExpansionError,
+ HyMacroExpansionError(
+ msg, macro_tree, filename, source),
+ sys.exc_info()[2])
def macroexpand(tree, module, compiler=None, once=False):
@@ -353,8 +340,6 @@ def macroexpand(tree, module, compiler=None, once=False):
opts['compiler'] = compiler
with macro_exceptions(module, tree, compiler):
- m_copy = make_empty_fn_copy(m)
- m_copy(module.__name__, *tree[1:], **opts)
obj = m(module.__name__, *tree[1:], **opts)
if isinstance(obj, HyExpression):
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index 0c7bcd5..9311eef 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -6,9 +6,8 @@
from __future__ import unicode_literals
from hy import HyString
-from hy.models import HyObject
from hy.compiler import hy_compile, hy_eval
-from hy.errors import HyCompileError, HyTypeError
+from hy.errors import HyCompileError, HyLanguageError, HyError
from hy.lex import hy_parse
from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3
@@ -27,7 +26,7 @@ def _ast_spotcheck(arg, root, secondary):
def can_compile(expr):
- return hy_compile(hy_parse(expr), "__main__")
+ return hy_compile(hy_parse(expr), __name__)
def can_eval(expr):
@@ -35,21 +34,16 @@ def can_eval(expr):
def cant_compile(expr):
- try:
- hy_compile(hy_parse(expr), "__main__")
- assert False
- except HyTypeError as e:
+ with pytest.raises(HyError) as excinfo:
+ hy_compile(hy_parse(expr), __name__)
+
+ if issubclass(excinfo.type, HyLanguageError):
+ assert excinfo.value.msg
+ return excinfo.value
+ elif issubclass(excinfo.type, HyCompileError):
# Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug.
- assert isinstance(e.expression, HyObject)
- assert e.message
- return e
- except HyCompileError as e:
- # Anything that can't be compiled should raise a user friendly
- # error, otherwise it's a compiler bug.
- assert isinstance(e.exception, HyTypeError)
- assert e.traceback
- return e
+ return excinfo.value
def s(x):
@@ -60,11 +54,9 @@ def test_ast_bad_type():
"Make sure AST breakage can happen"
class C:
pass
- try:
- hy_compile(C(), "__main__")
- assert True is False
- except TypeError:
- pass
+
+ with pytest.raises(TypeError):
+ hy_compile(C(), __name__, filename='', source='')
def test_empty_expr():
@@ -473,7 +465,7 @@ def test_lambda_list_keywords_kwonly():
assert code.body[0].args.kw_defaults[1].n == 2
else:
exception = cant_compile(kwonly_demo)
- assert isinstance(exception, HyTypeError)
+ assert isinstance(exception, HyLanguageError)
message = exception.args[0]
assert message == "&kwonly parameters require Python 3"
@@ -489,9 +481,9 @@ def test_lambda_list_keywords_mixed():
def test_missing_keyword_argument_value():
"""Ensure the compiler chokes on missing keyword argument values."""
- with pytest.raises(HyTypeError) as excinfo:
+ with pytest.raises(HyLanguageError) as excinfo:
can_compile("((fn [x] x) :x)")
- assert excinfo.value.message == "Keyword argument :x needs a value."
+ assert excinfo.value.msg == "Keyword argument :x needs a value."
def test_ast_unicode_strings():
@@ -500,7 +492,7 @@ def test_ast_unicode_strings():
def _compile_string(s):
hy_s = HyString(s)
- code = hy_compile([hy_s], "__main__")
+ code = hy_compile([hy_s], __name__, filename='', source=s)
# We put hy_s in a list so it isn't interpreted as a docstring.
# code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))])
@@ -541,7 +533,7 @@ Only one leading newline should be removed.
def test_compile_error():
"""Ensure we get compile error in tricky cases"""
- with pytest.raises(HyTypeError) as excinfo:
+ with pytest.raises(HyLanguageError) as excinfo:
can_compile("(fn [] (in [1 2 3]))")
@@ -549,11 +541,11 @@ def test_for_compile_error():
"""Ensure we get compile error in tricky 'for' cases"""
with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)")
- assert excinfo.value.message == "Premature end of input"
+ assert excinfo.value.msg == "Premature end of input"
with pytest.raises(LexException) as excinfo:
can_compile("(fn [] (for)))")
- assert excinfo.value.message == "Ran into a RPAREN where it wasn't expected."
+ assert excinfo.value.msg == "Ran into a RPAREN where it wasn't expected."
cant_compile("(fn [] (for [x] x))")
@@ -605,13 +597,13 @@ def test_setv_builtins():
def test_top_level_unquote():
- with pytest.raises(HyTypeError) as excinfo:
+ with pytest.raises(HyLanguageError) as excinfo:
can_compile("(unquote)")
- assert excinfo.value.message == "The special form 'unquote' is not allowed here"
+ assert excinfo.value.msg == "The special form 'unquote' is not allowed here"
- with pytest.raises(HyTypeError) as excinfo:
+ with pytest.raises(HyLanguageError) as excinfo:
can_compile("(unquote-splice)")
- assert excinfo.value.message == "The special form 'unquote-splice' is not allowed here"
+ assert excinfo.value.msg == "The special form 'unquote-splice' is not allowed here"
def test_lots_of_comment_lines():
diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py
index 309ca49..134bd5b 100644
--- a/tests/macros/test_macro_processor.py
+++ b/tests/macros/test_macro_processor.py
@@ -50,8 +50,7 @@ def test_preprocessor_exceptions():
""" Test that macro expansion raises appropriate exceptions"""
with pytest.raises(HyMacroExpansionError) as excinfo:
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
- assert "_hy_anon_fn_" not in excinfo.value.message
- assert "TypeError" not in excinfo.value.message
+ assert "_hy_anon_" not in excinfo.value.msg
def test_macroexpand_nan():
diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy
index a89eece..ab8eaf4 100644
--- a/tests/native_tests/core.hy
+++ b/tests/native_tests/core.hy
@@ -687,5 +687,5 @@ result['y in globals'] = 'y' in globals()")
(doc doc)
(setv out_err (.readouterr capsys))
(assert (.startswith (.strip (first out_err))
- "Help on function (doc) in module hy.core.macros:"))
+ "Help on function doc in module hy.core.macros:"))
(assert (empty? (second out_err))))
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
index c237a5a..65c629a 100644
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -7,7 +7,7 @@
[sys :as systest]
re
[operator [or_]]
- [hy.errors [HyTypeError]]
+ [hy.errors [HyLanguageError]]
pytest)
(import sys)
@@ -68,16 +68,16 @@
"NATIVE: test that setv doesn't work on names Python can't assign to
and that we can't mangle"
(try (eval '(setv None 1))
- (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+ (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(defn None [] (print "hello")))
- (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+ (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(when PY3
(try (eval '(setv False 1))
- (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+ (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(setv True 0))
- (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
+ (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(defn True [] (print "hello")))
- (except [e [TypeError]] (assert (in "Can't assign to" (str e)))))))
+ (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))))
(defn test-setv-pairs []
@@ -87,7 +87,7 @@
(assert (= b 2))
(setv y 0 x 1 y x)
(assert (= y 1))
- (with [(pytest.raises HyTypeError)]
+ (with [(pytest.raises HyLanguageError)]
(eval '(setv a 1 b))))
@@ -144,29 +144,29 @@
(do
(eval '(setv (do 1 2) 1))
(assert False))
- (except [e HyTypeError]
- (assert (= e.message "Can't assign or delete a non-expression"))))
+ (except [e HyLanguageError]
+ (assert (= e.msg "Can't assign or delete a non-expression"))))
(try
(do
(eval '(setv 1 1))
(assert False))
- (except [e HyTypeError]
- (assert (= e.message "Can't assign or delete a HyInteger"))))
+ (except [e HyLanguageError]
+ (assert (= e.msg "Can't assign or delete a HyInteger"))))
(try
(do
(eval '(setv {1 2} 1))
(assert False))
- (except [e HyTypeError]
- (assert (= e.message "Can't assign or delete a HyDict"))))
+ (except [e HyLanguageError]
+ (assert (= e.msg "Can't assign or delete a HyDict"))))
(try
(do
(eval '(del 1 1))
(assert False))
- (except [e HyTypeError]
- (assert (= e.message "Can't assign or delete a HyInteger")))))
+ (except [e HyLanguageError]
+ (assert (= e.msg "Can't assign or delete a HyInteger")))))
(defn test-no-str-as-sym []
diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy
index 1700b5d..6759f42 100644
--- a/tests/native_tests/native_macros.hy
+++ b/tests/native_tests/native_macros.hy
@@ -3,7 +3,7 @@
;; license. See the LICENSE.
(import pytest
- [hy.errors [HyTypeError]])
+ [hy.errors [HyTypeError HyMacroExpansionError]])
(defmacro rev [&rest body]
"Execute the `body` statements in reverse"
@@ -66,13 +66,13 @@
(try
(eval '(defmacro f [&kwonly a b]))
(except [e HyTypeError]
- (assert (= e.message "macros cannot use &kwonly")))
+ (assert (= e.msg "macros cannot use &kwonly")))
(else (assert False)))
(try
(eval '(defmacro f [&kwargs kw]))
(except [e HyTypeError]
- (assert (= e.message "macros cannot use &kwargs")))
+ (assert (= e.msg "macros cannot use &kwargs")))
(else (assert False))))
(defn test-fn-calling-macro []
@@ -483,3 +483,28 @@ in expansions."
(test-macro)
(assert (= blah 1)))
+
+
+(defn test-macro-errors []
+ (import traceback
+ [hy.importer [hy-parse]])
+
+ (setv test-expr (hy-parse "(defmacro blah [x] `(print ~@z)) (blah y)"))
+
+ (with [excinfo (pytest.raises HyMacroExpansionError)]
+ (eval test-expr))
+
+ (setv output (traceback.format_exception_only
+ excinfo.type excinfo.value))
+ (setv output (cut (.splitlines (.strip (first output))) 1))
+
+ (setv expected [" File \"\", line 1"
+ " (defmacro blah [x] `(print ~@z)) (blah y)"
+ " ^------^"
+ "expanding macro blah"
+ " NameError: global name 'z' is not defined"])
+
+ (assert (= (cut expected 0 -1) (cut output 0 -1)))
+ (assert (or (= (get expected -1) (get output -1))
+ ;; Handle PyPy's peculiarities
+ (= (.replace (get expected -1) "global " "") (get output -1)))))
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
index 716eb77..e08edbb 100644
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -28,7 +28,7 @@
(defmacro forbid [expr]
`(assert (try
(eval '~expr)
- (except [TypeError] True)
+ (except [[TypeError SyntaxError]] True)
(else (raise AssertionError)))))
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 8aef923..aad45e5 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -11,6 +11,7 @@ import shlex
import subprocess
from hy.importer import cache_from_source
+from hy._compat import PY3
import pytest
@@ -123,7 +124,16 @@ def test_bin_hy_stdin_as_arrow():
def test_bin_hy_stdin_error_underline_alignment():
_, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
- assert "\n (mabcdefghi)\n ^----------^" in err
+
+ msg_idx = err.rindex(" (mabcdefghi)")
+ assert msg_idx
+ err_parts = err[msg_idx:].splitlines()
+ assert err_parts[1].startswith(" ^----------^")
+ assert err_parts[2].startswith("expanding macro mabcdefghi")
+ assert (err_parts[3].startswith(" TypeError: mabcdefghi") or
+ # PyPy can use a function's `__name__` instead of
+ # `__code__.co_name`.
+ err_parts[3].startswith(" TypeError: (mabcdefghi)"))
def test_bin_hy_stdin_except_do():
@@ -153,6 +163,62 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
assert "AZ" in err
+def test_bin_hy_error_parts_length():
+ """Confirm that exception messages print arrows surrounding the affected
+ expression."""
+ prg_str = """
+ (import hy.errors
+ [hy.importer [hy-parse]])
+
+ (setv test-expr (hy-parse "(+ 1\n\n'a 2 3\n\n 1)"))
+ (setv test-expr.start-line {})
+ (setv test-expr.start-column {})
+ (setv test-expr.end-column {})
+
+ (raise (hy.errors.HyLanguageError
+ "this\nis\na\nmessage"
+ test-expr
+ None
+ None))
+ """
+
+ # Up-arrows right next to each other.
+ _, err = run_cmd("hy", prg_str.format(3, 1, 2))
+
+ msg_idx = err.rindex("HyLanguageError:")
+ assert msg_idx
+ err_parts = err[msg_idx:].splitlines()[1:]
+
+ expected = [' File "", line 3',
+ ' \'a 2 3',
+ ' ^^',
+ 'this',
+ 'is',
+ 'a',
+ 'message']
+
+ for obs, exp in zip(err_parts, expected):
+ assert obs.startswith(exp)
+
+ # Make sure only one up-arrow is printed
+ _, err = run_cmd("hy", prg_str.format(3, 1, 1))
+
+ msg_idx = err.rindex("HyLanguageError:")
+ assert msg_idx
+ err_parts = err[msg_idx:].splitlines()[1:]
+ assert err_parts[2] == ' ^'
+
+ # Make sure lines are printed in between arrows separated by more than one
+ # character.
+ _, err = run_cmd("hy", prg_str.format(3, 1, 6))
+ print(err)
+
+ msg_idx = err.rindex("HyLanguageError:")
+ assert msg_idx
+ err_parts = err[msg_idx:].splitlines()[1:]
+ assert err_parts[2] == ' ^----^'
+
+
def test_bin_hy_stdin_bad_repr():
# https://github.com/hylang/hy/issues/1389
output, err = run_cmd("hy", """
@@ -423,3 +489,86 @@ def test_bin_hy_macro_require():
assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip()
+
+
+def test_bin_hy_tracebacks():
+ """Make sure the printed tracebacks are correct."""
+
+ # We want the filtered tracebacks.
+ os.environ['HY_DEBUG'] = ''
+
+ def req_err(x):
+ assert x == '{}HyRequireError: No module named {}'.format(
+ 'hy.errors.' if PY3 else '',
+ (repr if PY3 else str)('not_a_real_module'))
+
+ # Modeled after
+ # > python -c 'import not_a_real_module'
+ # Traceback (most recent call last):
+ # File "", line 1, in
+ # ImportError: No module named not_a_real_module
+ _, error = run_cmd('hy', '(require not-a-real-module)')
+ error_lines = error.splitlines()
+ if error_lines[-1] == '':
+ del error_lines[-1]
+ assert len(error_lines) <= 10
+ # Rough check for the internal traceback filtering
+ req_err(error_lines[4 if PY3 else -1])
+
+ _, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
+ error_lines = error.splitlines()
+ assert len(error_lines) <= 4
+ req_err(error_lines[-1])
+
+ output, error = run_cmd('hy -i "(require not-a-real-module)"')
+ assert output.startswith('=> ')
+ print(error.splitlines())
+ req_err(error.splitlines()[3 if PY3 else -3])
+
+ # Modeled after
+ # > python -c 'print("hi'
+ # File "", line 1
+ # print("hi
+ # ^
+ # SyntaxError: EOL while scanning string literal
+ _, error = run_cmd(r'hy -c "(print \""', expect=1)
+ peoi = (
+ ' File "", line 1\n'
+ ' (print "\n'
+ ' ^\n' +
+ '{}PrematureEndOfInput: Partial string literal\n'.format(
+ 'hy.lex.exceptions.' if PY3 else ''))
+ assert error == peoi
+
+ # Modeled after
+ # > python -i -c "print('"
+ # File "", line 1
+ # print('
+ # ^
+ # SyntaxError: EOL while scanning string literal
+ # >>>
+ output, error = run_cmd(r'hy -i "(print \""')
+ assert output.startswith('=> ')
+ assert error.startswith(peoi)
+
+ # Modeled after
+ # > python -c 'print(a)'
+ # Traceback (most recent call last):
+ # File "", line 1, in
+ # NameError: name 'a' is not defined
+ output, error = run_cmd('hy -c "(print a)"', expect=1)
+ error_lines = error.splitlines()
+ assert error_lines[3] == ' File "", line 1, in '
+ # PyPy will add "global" to this error message, so we work around that.
+ assert error_lines[-1].strip().replace(' global', '') == (
+ "NameError: name 'a' is not defined")
+
+ # Modeled after
+ # > python -c 'compile()'
+ # Traceback (most recent call last):
+ # File "", line 1, in
+ # TypeError: Required argument 'source' (pos 1) not found
+ output, error = run_cmd('hy -c "(compile)"', expect=1)
+ error_lines = error.splitlines()
+ assert error_lines[-2] == ' File "", line 1, in '
+ assert error_lines[-1].startswith('TypeError')
diff --git a/tests/test_lex.py b/tests/test_lex.py
index b0a03dc..f709719 100644
--- a/tests/test_lex.py
+++ b/tests/test_lex.py
@@ -61,7 +61,7 @@ def test_lex_single_quote_err():
with lexe() as execinfo:
tokenize("' ")
check_ex(execinfo, [
- ' File "", line -1\n',
+ ' File "", line 1\n',
" '\n",
' ^\n',
'LexException: Could not identify the next token.\n'])
@@ -472,7 +472,7 @@ def test_lex_exception_filtering(capsys):
# First, test for PrematureEndOfInput
with peoi() as execinfo:
- tokenize(" \n (foo")
+ tokenize(" \n (foo\n \n")
check_trace_output(capsys, execinfo, [
' File "", line 2',
' (foo',