Improve correspondence with Python errors and console behavior

Compiler and command-line error messages now reflect their Python counterparts.
E.g. where Python emits a `SyntaxError`, so does Hy; same for `TypeError`s.
Multiple tests have been added that check the format and type of raised
exceptions over varying command-line invocations (e.g. interactive and not).

A new exception type for `require` errors was added so that they can be treated
like normal run-time errors and not compiler errors.

The Hy REPL has been further refactored to better match the class-structured
API.  Now, different error types are handled separately and leverage more base
class-provided functionality.

Closes hylang/hy#1486.
This commit is contained in:
Brandon T. Willard 2018-10-12 23:25:43 -05:00 committed by Kodi Arfer
parent cadfa4152b
commit fb6feaf082
17 changed files with 630 additions and 388 deletions

View File

@ -40,6 +40,10 @@ if PY3:
finally: finally:
traceback = None traceback = None
code_obj_args = ['argcount', 'kwonlyargcount', 'nlocals', 'stacksize',
'flags', 'code', 'consts', 'names', 'varnames',
'filename', 'name', 'firstlineno', 'lnotab', 'freevars',
'cellvars']
else: else:
def raise_from(value, from_value=None): def raise_from(value, from_value=None):
raise value raise value
@ -52,10 +56,30 @@ else:
traceback = None 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') raise_code = compile(raise_src, __file__, 'exec')
exec(raise_code) 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): def isidentifier(x):
if x in ('True', 'False', 'None', 'print'): if x in ('True', 'False', 'None', 'print'):
# `print` is special-cased here because Python 2's # `print` is special-cased here because Python 2's

View File

@ -12,6 +12,7 @@ import os
import io import io
import importlib import importlib
import py_compile import py_compile
import traceback
import runpy import runpy
import types import types
@ -20,8 +21,9 @@ import astor.code_gen
import hy import hy
from hy.lex import hy_parse, mangle from hy.lex import hy_parse, mangle
from hy.lex.exceptions import PrematureEndOfInput from hy.lex.exceptions import PrematureEndOfInput
from hy.compiler import HyASTCompiler, hy_compile, hy_eval from hy.compiler import HyASTCompiler, hy_eval, hy_compile, ast_compile
from hy.errors import HySyntaxError, filtered_hy_exceptions from hy.errors import (HyLanguageError, HyRequireError, HyMacroExpansionError,
filtered_hy_exceptions, hy_exc_handler)
from hy.importer import runhy from hy.importer import runhy
from hy.completer import completion, Completer from hy.completer import completion, Completer
from hy.macros import macro, require from hy.macros import macro, require
@ -50,29 +52,70 @@ builtins.quit = HyQuitter('quit')
builtins.exit = HyQuitter('exit') 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="<input>", 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): class HyREPL(code.InteractiveConsole, object):
def __init__(self, spy=False, output_fn=None, locals=None, def __init__(self, spy=False, output_fn=None, locals=None,
filename="<input>"): filename="<stdin>"):
super(HyREPL, self).__init__(locals=locals,
filename=filename)
# Create a proper module for this REPL so that we can obtain it easily # Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`). # (e.g. using `importlib.import_module`).
# Also, make sure it's properly introduced to `sys.modules` and # We let `InteractiveConsole` initialize `self.locals` when it's
# consistently use its namespace as `locals` from here on. # `None`.
super(HyREPL, self).__init__(locals=locals,
filename=filename)
module_name = self.locals.get('__name__', '__console__') 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, self.module = sys.modules.setdefault(module_name,
types.ModuleType(module_name)) types.ModuleType(module_name))
self.module.__dict__.update(self.locals) self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__ self.locals = self.module.__dict__
# Load cmdline-specific macros. # 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.hy_compiler = HyASTCompiler(self.module)
self.compile = HyCommandCompiler(self.module, self.ast_callback,
self.hy_compiler)
self.spy = spy self.spy = spy
self.last_value = None
if output_fn is None: if output_fn is None:
self.output_fn = repr 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._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols}) 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: if self.spy:
# Mush the two AST chunks into a single module for try:
# conversion into Python. # Mush the two AST chunks into a single module for
new_ast = ast.Module(main_ast.body + # conversion into Python.
[ast.Expr(expr_ast.body)]) new_ast = ast.Module(exec_ast.body +
print(astor.to_source(new_ast)) [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): def _error_wrap(self, error_fn, *args, **kwargs):
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
@ -120,46 +168,50 @@ class HyREPL(code.InteractiveConsole, object):
def showtraceback(self): def showtraceback(self):
self._error_wrap(super(HyREPL, self).showtraceback) self._error_wrap(super(HyREPL, self).showtraceback)
def runsource(self, source, filename='<input>', symbol='single'): def runcode(self, code):
try: try:
do = hy_parse(source, filename=filename) eval(code[0], self.locals)
except PrematureEndOfInput: self.last_value = eval(code[1], self.locals)
return True # Don't print `None` values.
except HySyntaxError as e: self.print_last_value = self.last_value is not None
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)
except SystemExit: except SystemExit:
raise raise
except Exception as e: 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='<stdin>', 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() self.showtraceback()
return False return False
if value is not None: # Shift exisitng REPL results
# Shift exisitng REPL results if not res:
next_result = value next_result = self.last_value
for sym in self._repl_results_symbols: for sym in self._repl_results_symbols:
self.locals[sym], next_result = next_result, self.locals[sym] self.locals[sym], next_result = next_result, self.locals[sym]
# Print the value. # Print the value.
try: if self.print_last_value:
output = self.output_fn(value) try:
except Exception: output = self.output_fn(self.last_value)
self.showtraceback() except Exception:
return False self.showtraceback()
return False
print(output) print(output)
return False return res
@macro("koan") @macro("koan")
@ -215,9 +267,14 @@ def ideas_macro(ETname):
def run_command(source, filename=None): def run_command(source, filename=None):
tree = hy_parse(source, filename=filename)
__main__ = importlib.import_module('__main__') __main__ = importlib.import_module('__main__')
require("hy.cmdline", __main__, assignments="ALL") 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(): with filtered_hy_exceptions():
hy_eval(tree, None, __main__, filename=filename, source=source) hy_eval(tree, None, __main__, filename=filename, source=source)
return 0 return 0
@ -259,12 +316,18 @@ def run_icommand(source, **kwargs):
source = f.read() source = f.read()
filename = source filename = source
else: else:
filename = '<input>' filename = '<string>'
hr = HyREPL(**kwargs)
with filtered_hy_exceptions(): with filtered_hy_exceptions():
hr = HyREPL(**kwargs) res = hr.runsource(source, filename=filename)
hr.runsource(source, filename=filename, symbol='single')
return run_repl(hr) # 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] ..." 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( print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
e.filename, e.errno, e.strerror), file=sys.stderr) e.filename, e.errno, e.strerror), file=sys.stderr)
sys.exit(e.errno) sys.exit(e.errno)
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
sys.exit(1)
# User did NOTHING! # User did NOTHING!
return run_repl(spy=options.spy, output_fn=options.repl_output_fn) 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:]) options = parser.parse_args(sys.argv[1:])
if options.FILE is None or options.FILE == '-': if options.FILE is None or options.FILE == '-':
filename = '<stdin>'
source = sys.stdin.read() source = sys.stdin.read()
with filtered_hy_exceptions():
hst = hy_parse(source, filename='<stdin>')
else: else:
with filtered_hy_exceptions(), \ filename = options.FILE
io.open(options.FILE, 'r', encoding='utf-8') as source_file: with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read() 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: if options.with_source:
# need special printing on Windows in case the # need special printing on Windows in case the
@ -464,7 +531,7 @@ def hy2py_main():
print() print()
with filtered_hy_exceptions(): with filtered_hy_exceptions():
_ast = hy_compile(hst, '__main__') _ast = hy_compile(hst, '__main__', filename=filename, source=source)
if options.with_ast: if options.with_ast:
if PY3 and platform.system() == "Windows": if PY3 and platform.system() == "Windows":

View File

@ -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, from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack) notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
from hy.errors import (HyCompileError, HyTypeError, HyEvalError, from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
HyInternalError) HySyntaxError, HyEvalError, HyInternalError)
from hy.lex import mangle, unmangle 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 # nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError! # another HyCompileError!
raise raise
except HyTypeError as e: except HyLanguageError as e:
reraise(type(e), e, None) # These are expected errors that should be passed to the user.
reraise(type(e), e, sys.exc_info()[2])
except Exception as e: except Exception as e:
# These are unexpected errors that will--hopefully--never be seen
# by the user.
f_exc = traceback.format_exc() f_exc = traceback.format_exc()
exc_msg = "Internal Compiler Bug 😱\n{}".format(f_exc) exc_msg = "Internal Compiler Bug 😱\n{}".format(f_exc)
reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2]) reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
def _syntax_error(self, expr, message): 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, def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False): oldpy_unpack=False):

View File

@ -15,13 +15,13 @@
(raise (raise
(hy.errors.HyTypeError (hy.errors.HyTypeError
(% "received a `%s' instead of a symbol for macro name" (% "received a `%s' instead of a symbol for macro name"
(. (type name) --name--)) (. (type name) __name__))
--file-- macro-name None))) None --file-- None)))
(for [kw '[&kwonly &kwargs]] (for [kw '[&kwonly &kwargs]]
(if* (in kw lambda-list) (if* (in kw lambda-list)
(raise (hy.errors.HyTypeError (% "macros cannot use %s" (raise (hy.errors.HyTypeError (% "macros cannot use %s"
kw) kw)
--file-- macro-name None)))) macro-name --file-- None))))
;; this looks familiar... ;; this looks familiar...
`(eval-and-compile `(eval-and-compile
(import hy) (import hy)
@ -46,10 +46,10 @@
(raise (hy.errors.HyTypeError (raise (hy.errors.HyTypeError
(% "received a `%s' instead of a symbol for tag macro name" (% "received a `%s' instead of a symbol for tag macro name"
(. (type tag-name) --name--)) (. (type tag-name) --name--))
--file-- tag-name None))) tag-name --file-- None)))
(if (or (= tag-name ":") (if (or (= tag-name ":")
(= 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) (setv tag-name (.replace (hy.models.HyString tag-name)
tag-name)) tag-name))
`(eval-and-compile `(eval-and-compile

View File

@ -3,6 +3,7 @@
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import os import os
import re
import sys import sys
import traceback import traceback
import pkgutil import pkgutil
@ -19,9 +20,7 @@ _hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
class HyError(Exception): class HyError(Exception):
def __init__(self, message, *args): pass
self.message = message
super(HyError, self).__init__(message, *args)
class HyInternalError(HyError): class HyInternalError(HyError):
@ -31,9 +30,6 @@ class HyInternalError(HyError):
hopefully, never be seen by users! hopefully, never be seen by users!
""" """
def __init__(self, message, *args):
super(HyInternalError, self).__init__(message, *args)
class HyLanguageError(HyError): class HyLanguageError(HyError):
"""Errors caused by invalid use of the Hy language. """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. This, and any errors inheriting from this, are user-facing.
""" """
def __init__(self, message, *args): def __init__(self, message, expression=None, filename=None, source=None,
super(HyLanguageError, self).__init__(message, *args) 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 "<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): class HyCompileError(HyInternalError):
@ -50,88 +165,21 @@ class HyCompileError(HyInternalError):
class HyTypeError(HyLanguageError, TypeError): class HyTypeError(HyLanguageError, TypeError):
"""TypeErrors occurring during the normal use of Hy.""" """TypeError 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
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. """Errors caused by invalid use of Hy macros.
This, and any errors inheriting from this, are user-facing. This, and any errors inheriting from this, are user-facing.
@ -158,97 +206,39 @@ class HyIOError(HyInternalError, IOError):
class HySyntaxError(HyLanguageError, SyntaxError): class HySyntaxError(HyLanguageError, SyntaxError):
"""Error during the Lexing of a Hython expression.""" """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 _module_filter_name(module_name):
def from_expression(message, expression, filename=None, source=None): try:
if not source: compiler_loader = pkgutil.get_loader(module_name)
# Maybe the expression object has its own source. if not compiler_loader:
source = getattr(expression, 'source', None) return None
filename = compiler_loader.get_filename(module_name)
if not filename: if not filename:
filename = getattr(expression, 'filename', None) return None
if source: if compiler_loader.is_package(module_name):
lineno = expression.start_line # Use the package directory (e.g. instead of `.../__init__.py`) so
colno = expression.start_column # that we can filter all modules in a package.
end_line = getattr(expression, 'end_line', len(source)) return os.path.dirname(filename)
lines = source.splitlines()
source = '\n'.join(lines[lineno-1:end_line])
else: else:
# We could attempt to extract the source given a filename, but we # Normalize filename endings, because tracebacks will use `pyc` when
# don't. # the loader says `py`.
lineno = colno = -1 return filename.replace('.pyc', '.py')
except Exception:
return HySyntaxError(message, filename, lineno, colno, source) return None
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)
def _get_module_info(module): _tb_hidden_modules = {m for m in map(_module_filter_name,
compiler_loader = pkgutil.get_loader(module) ['hy.compiler', 'hy.lex',
is_pkg = compiler_loader.is_package(module) 'hy.cmdline', 'hy.lex.parser',
filename = compiler_loader.get_filename() 'hy.importer', 'hy._compat',
if is_pkg: 'hy.macros', 'hy.models',
# Use package directory 'rply'])
return os.path.dirname(filename) if m is not None}
else:
# Normalize filename endings, because tracebacks will use `pyc` when
# the loader says `py`.
return filename.replace('.pyc', '.py')
_tb_hidden_modules = {_get_module_info(m) def hy_exc_filter(exc_type, exc_value, exc_traceback):
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):
"""Produce exceptions print-outs with all frames originating from the """Produce exceptions print-outs with all frames originating from the
modules in `_tb_hidden_modules` filtered out. 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 This does not remove the frames from the actual tracebacks, so debugging
will show everything. 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: try:
# frame = (filename, line number, function name*, text) output = hy_exc_filter(exc_type, exc_value, exc_traceback)
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)
sys.stderr.write(output) sys.stderr.write(output)
sys.stderr.flush() sys.stderr.flush()
except Exception: except Exception:

View File

@ -8,10 +8,9 @@ import re
import sys import sys
import unicodedata 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.lex.exceptions import PrematureEndOfInput, LexException # NOQA
from hy.models import HyExpression, HySymbol from hy.models import HyExpression, HySymbol
from hy.errors import HySyntaxError
try: try:
from io import StringIO from io import StringIO
@ -35,15 +34,12 @@ def hy_parse(source, filename='<string>'):
out : HyExpression out : HyExpression
""" """
_source = re.sub(r'\A#!.*', '', source) _source = re.sub(r'\A#!.*', '', source)
try: res = HyExpression([HySymbol("do")] +
res = HyExpression([HySymbol("do")] + tokenize(_source + "\n",
tokenize(_source + "\n", filename=filename))
filename=filename)) res.source = source
res.source = source res.filename = filename
res.filename = filename return res
return res
except HySyntaxError as e:
reraise(type(e), e, None)
class ParserState(object): class ParserState(object):
@ -70,8 +66,12 @@ def tokenize(source, filename=None):
state=ParserState(source, filename)) state=ParserState(source, filename))
except LexingError as e: except LexingError as e:
pos = e.getsourcepos() pos = e.getsourcepos()
raise LexException("Could not identify the next token.", filename, raise LexException("Could not identify the next token.",
pos.lineno, pos.colno, source) None, filename, source,
max(pos.lineno, 1),
max(pos.colno, 1))
except LexException as e:
raise e
mangle_delim = 'X' mangle_delim = 'X'

View File

@ -8,26 +8,31 @@ class LexException(HySyntaxError):
@classmethod @classmethod
def from_lexer(cls, message, state, token): def from_lexer(cls, message, state, token):
lineno = None
colno = None
source = state.source
source_pos = token.getsourcepos() source_pos = token.getsourcepos()
if token.source_pos:
if source_pos:
lineno = source_pos.lineno lineno = source_pos.lineno
colno = source_pos.colno 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: else:
lineno = -1 lineno = lineno or 1
colno = -1 colno = colno or 1
if state.source: return cls(message,
lines = state.source.splitlines() None,
if lines[-1] == '': state.filename,
del lines[-1] source,
lineno,
if lineno < 1: colno)
lineno = len(lines)
if colno < 1:
colno = len(lines[-1])
source = lines[lineno - 1]
return cls(message, state.filename, lineno, colno, source)
class PrematureEndOfInput(LexException): class PrematureEndOfInput(LexException):

View File

@ -6,7 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from functools import wraps from functools import wraps
import re, unicodedata
from rply import ParserGenerator from rply import ParserGenerator
@ -278,15 +277,6 @@ def symbol_like(obj):
def error_handler(state, token): def error_handler(state, token):
tokentype = token.gettokentype() tokentype = token.gettokentype()
if tokentype == '$end': 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, raise PrematureEndOfInput.from_lexer("Premature end of input", state,
token) token)
else: else:

View File

@ -5,13 +5,15 @@ import sys
import importlib import importlib
import inspect import inspect
import pkgutil import pkgutil
import traceback
from contextlib import contextmanager 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.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle from hy.lex import mangle
from hy.errors import HyTypeError, HyMacroExpansionError from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
HyRequireError)
try: try:
# Check if we have the newer inspect.signature available. # Check if we have the newer inspect.signature available.
@ -50,7 +52,7 @@ def macro(name):
""" """
name = mangle(name) name = mangle(name)
def _(fn): def _(fn):
fn.__name__ = '({})'.format(name) fn = rename_function(fn, name)
try: try:
fn._hy_macro_pass_compiler = has_kwargs(fn) fn._hy_macro_pass_compiler = has_kwargs(fn)
except Exception: except Exception:
@ -75,7 +77,7 @@ def tag(name):
if not PY3: if not PY3:
_name = _name.encode('UTF-8') _name = _name.encode('UTF-8')
fn.__name__ = _name fn = rename_function(fn, _name)
module = inspect.getmodule(fn) module = inspect.getmodule(fn)
@ -150,7 +152,6 @@ def require(source_module, target_module, assignments, prefix=""):
out: boolean out: boolean
Whether or not macros and tags were actually transferred. Whether or not macros and tags were actually transferred.
""" """
if target_module is None: if target_module is None:
parent_frame = inspect.stack()[1][0] parent_frame = inspect.stack()[1][0]
target_namespace = parent_frame.f_globals target_namespace = parent_frame.f_globals
@ -161,7 +162,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif inspect.ismodule(target_module): elif inspect.ismodule(target_module):
target_namespace = target_module.__dict__ target_namespace = target_module.__dict__
else: else:
raise TypeError('`target_module` is not a recognized type: {}'.format( raise HyTypeError('`target_module` is not a recognized type: {}'.format(
type(target_module))) type(target_module)))
# Let's do a quick check to make sure the source module isn't actually # 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 return False
if not inspect.ismodule(source_module): 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_macros = source_module.__dict__.setdefault('__macros__', {})
source_tags = source_module.__dict__.setdefault('__tags__', {}) source_tags = source_module.__dict__.setdefault('__tags__', {})
if len(source_module.__macros__) + len(source_module.__tags__) == 0: if len(source_module.__macros__) + len(source_module.__tags__) == 0:
if assignments != "ALL": 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)) source_module))
else: else:
return False return False
@ -205,7 +209,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif _name in source_module.__tags__: elif _name in source_module.__tags__:
target_tags[alias] = source_tags[_name] target_tags[alias] = source_tags[_name]
else: else:
raise ImportError('Could not require name {} from {}'.format( raise HyRequireError('Could not require name {} from {}'.format(
_name, source_module)) _name, source_module))
return True return True
@ -239,50 +243,33 @@ def load_macros(module):
if k not in module_tags}) 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 @contextmanager
def macro_exceptions(module, macro_tree, compiler=None): def macro_exceptions(module, macro_tree, compiler=None):
try: try:
yield 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: 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): if compiler:
exc_type = HyMacroExpansionError filename = compiler.filename
msg = "expanding `{}': ".format(macro_tree[0]) source = compiler.source
msg += str(e).replace("<lambda>()", "", 1).strip()
else: else:
exc_type = HyTypeError filename = None
msg = e.message source = None
reraise(exc_type, exc_msg = ' '.join(traceback.format_exception_only(
exc_type(msg, filename, macro_tree, source), sys.exc_info()[0], sys.exc_info()[1]))
sys.exc_info()[2].tb_next)
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): def macroexpand(tree, module, compiler=None, once=False):
@ -353,8 +340,6 @@ def macroexpand(tree, module, compiler=None, once=False):
opts['compiler'] = compiler opts['compiler'] = compiler
with macro_exceptions(module, tree, 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) obj = m(module.__name__, *tree[1:], **opts)
if isinstance(obj, HyExpression): if isinstance(obj, HyExpression):

View File

@ -6,9 +6,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from hy import HyString from hy import HyString
from hy.models import HyObject
from hy.compiler import hy_compile, hy_eval 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 import hy_parse
from hy.lex.exceptions import LexException, PrematureEndOfInput from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3 from hy._compat import PY3
@ -27,7 +26,7 @@ def _ast_spotcheck(arg, root, secondary):
def can_compile(expr): def can_compile(expr):
return hy_compile(hy_parse(expr), "__main__") return hy_compile(hy_parse(expr), __name__)
def can_eval(expr): def can_eval(expr):
@ -35,21 +34,16 @@ def can_eval(expr):
def cant_compile(expr): def cant_compile(expr):
try: with pytest.raises(HyError) as excinfo:
hy_compile(hy_parse(expr), "__main__") hy_compile(hy_parse(expr), __name__)
assert False
except HyTypeError as e: 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 # Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug. # error, otherwise it's a compiler bug.
assert isinstance(e.expression, HyObject) return excinfo.value
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
def s(x): def s(x):
@ -60,11 +54,9 @@ def test_ast_bad_type():
"Make sure AST breakage can happen" "Make sure AST breakage can happen"
class C: class C:
pass pass
try:
hy_compile(C(), "__main__") with pytest.raises(TypeError):
assert True is False hy_compile(C(), __name__, filename='<string>', source='')
except TypeError:
pass
def test_empty_expr(): def test_empty_expr():
@ -473,7 +465,7 @@ def test_lambda_list_keywords_kwonly():
assert code.body[0].args.kw_defaults[1].n == 2 assert code.body[0].args.kw_defaults[1].n == 2
else: else:
exception = cant_compile(kwonly_demo) exception = cant_compile(kwonly_demo)
assert isinstance(exception, HyTypeError) assert isinstance(exception, HyLanguageError)
message = exception.args[0] message = exception.args[0]
assert message == "&kwonly parameters require Python 3" assert message == "&kwonly parameters require Python 3"
@ -489,9 +481,9 @@ def test_lambda_list_keywords_mixed():
def test_missing_keyword_argument_value(): def test_missing_keyword_argument_value():
"""Ensure the compiler chokes on missing keyword argument values.""" """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)") 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(): def test_ast_unicode_strings():
@ -500,7 +492,7 @@ def test_ast_unicode_strings():
def _compile_string(s): def _compile_string(s):
hy_s = HyString(s) hy_s = HyString(s)
code = hy_compile([hy_s], "__main__") code = hy_compile([hy_s], __name__, filename='<string>', source=s)
# We put hy_s in a list so it isn't interpreted as a docstring. # 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)]))]) # 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(): def test_compile_error():
"""Ensure we get compile error in tricky cases""" """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]))") 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""" """Ensure we get compile error in tricky 'for' cases"""
with pytest.raises(PrematureEndOfInput) as excinfo: with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)") 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: with pytest.raises(LexException) as excinfo:
can_compile("(fn [] (for)))") 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))") cant_compile("(fn [] (for [x] x))")
@ -605,13 +597,13 @@ def test_setv_builtins():
def test_top_level_unquote(): def test_top_level_unquote():
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyLanguageError) as excinfo:
can_compile("(unquote)") 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)") 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(): def test_lots_of_comment_lines():

View File

@ -50,8 +50,7 @@ def test_preprocessor_exceptions():
""" Test that macro expansion raises appropriate exceptions""" """ Test that macro expansion raises appropriate exceptions"""
with pytest.raises(HyMacroExpansionError) as excinfo: with pytest.raises(HyMacroExpansionError) as excinfo:
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__)) macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
assert "_hy_anon_fn_" not in excinfo.value.message assert "_hy_anon_" not in excinfo.value.msg
assert "TypeError" not in excinfo.value.message
def test_macroexpand_nan(): def test_macroexpand_nan():

View File

@ -687,5 +687,5 @@ result['y in globals'] = 'y' in globals()")
(doc doc) (doc doc)
(setv out_err (.readouterr capsys)) (setv out_err (.readouterr capsys))
(assert (.startswith (.strip (first out_err)) (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)))) (assert (empty? (second out_err))))

View File

@ -7,7 +7,7 @@
[sys :as systest] [sys :as systest]
re re
[operator [or_]] [operator [or_]]
[hy.errors [HyTypeError]] [hy.errors [HyLanguageError]]
pytest) pytest)
(import sys) (import sys)
@ -68,16 +68,16 @@
"NATIVE: test that setv doesn't work on names Python can't assign to "NATIVE: test that setv doesn't work on names Python can't assign to
and that we can't mangle" and that we can't mangle"
(try (eval '(setv None 1)) (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"))) (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 (when PY3
(try (eval '(setv False 1)) (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)) (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"))) (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 [] (defn test-setv-pairs []
@ -87,7 +87,7 @@
(assert (= b 2)) (assert (= b 2))
(setv y 0 x 1 y x) (setv y 0 x 1 y x)
(assert (= y 1)) (assert (= y 1))
(with [(pytest.raises HyTypeError)] (with [(pytest.raises HyLanguageError)]
(eval '(setv a 1 b)))) (eval '(setv a 1 b))))
@ -144,29 +144,29 @@
(do (do
(eval '(setv (do 1 2) 1)) (eval '(setv (do 1 2) 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a non-expression")))) (assert (= e.msg "Can't assign or delete a non-expression"))))
(try (try
(do (do
(eval '(setv 1 1)) (eval '(setv 1 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyInteger")))) (assert (= e.msg "Can't assign or delete a HyInteger"))))
(try (try
(do (do
(eval '(setv {1 2} 1)) (eval '(setv {1 2} 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyDict")))) (assert (= e.msg "Can't assign or delete a HyDict"))))
(try (try
(do (do
(eval '(del 1 1)) (eval '(del 1 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyInteger"))))) (assert (= e.msg "Can't assign or delete a HyInteger")))))
(defn test-no-str-as-sym [] (defn test-no-str-as-sym []

View File

@ -3,7 +3,7 @@
;; license. See the LICENSE. ;; license. See the LICENSE.
(import pytest (import pytest
[hy.errors [HyTypeError]]) [hy.errors [HyTypeError HyMacroExpansionError]])
(defmacro rev [&rest body] (defmacro rev [&rest body]
"Execute the `body` statements in reverse" "Execute the `body` statements in reverse"
@ -66,13 +66,13 @@
(try (try
(eval '(defmacro f [&kwonly a b])) (eval '(defmacro f [&kwonly a b]))
(except [e HyTypeError] (except [e HyTypeError]
(assert (= e.message "macros cannot use &kwonly"))) (assert (= e.msg "macros cannot use &kwonly")))
(else (assert False))) (else (assert False)))
(try (try
(eval '(defmacro f [&kwargs kw])) (eval '(defmacro f [&kwargs kw]))
(except [e HyTypeError] (except [e HyTypeError]
(assert (= e.message "macros cannot use &kwargs"))) (assert (= e.msg "macros cannot use &kwargs")))
(else (assert False)))) (else (assert False))))
(defn test-fn-calling-macro [] (defn test-fn-calling-macro []
@ -483,3 +483,28 @@ in expansions."
(test-macro) (test-macro)
(assert (= blah 1))) (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 \"<string>\", 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)))))

View File

@ -28,7 +28,7 @@
(defmacro forbid [expr] (defmacro forbid [expr]
`(assert (try `(assert (try
(eval '~expr) (eval '~expr)
(except [TypeError] True) (except [[TypeError SyntaxError]] True)
(else (raise AssertionError))))) (else (raise AssertionError)))))

View File

@ -11,6 +11,7 @@ import shlex
import subprocess import subprocess
from hy.importer import cache_from_source from hy.importer import cache_from_source
from hy._compat import PY3
import pytest import pytest
@ -123,7 +124,16 @@ def test_bin_hy_stdin_as_arrow():
def test_bin_hy_stdin_error_underline_alignment(): def test_bin_hy_stdin_error_underline_alignment():
_, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)") _, 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(): def test_bin_hy_stdin_except_do():
@ -153,6 +163,62 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
assert "AZ" in err 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 "<string>", 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(): def test_bin_hy_stdin_bad_repr():
# https://github.com/hylang/hy/issues/1389 # https://github.com/hylang/hy/issues/1389
output, err = run_cmd("hy", """ output, err = run_cmd("hy", """
@ -423,3 +489,86 @@ def test_bin_hy_macro_require():
assert os.path.exists(cache_from_source(test_file)) assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file)) output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip() 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 "<string>", line 1, in <module>
# 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 "<string>", line 1
# print("hi
# ^
# SyntaxError: EOL while scanning string literal
_, error = run_cmd(r'hy -c "(print \""', expect=1)
peoi = (
' File "<string>", 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 "<string>", 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 "<string>", line 1, in <module>
# 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 "<string>", line 1, in <module>'
# 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 "<string>", line 1, in <module>
# 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 "<string>", line 1, in <module>'
assert error_lines[-1].startswith('TypeError')

View File

@ -61,7 +61,7 @@ def test_lex_single_quote_err():
with lexe() as execinfo: with lexe() as execinfo:
tokenize("' ") tokenize("' ")
check_ex(execinfo, [ check_ex(execinfo, [
' File "<string>", line -1\n', ' File "<string>", line 1\n',
" '\n", " '\n",
' ^\n', ' ^\n',
'LexException: Could not identify the next token.\n']) 'LexException: Could not identify the next token.\n'])
@ -472,7 +472,7 @@ def test_lex_exception_filtering(capsys):
# First, test for PrematureEndOfInput # First, test for PrematureEndOfInput
with peoi() as execinfo: with peoi() as execinfo:
tokenize(" \n (foo") tokenize(" \n (foo\n \n")
check_trace_output(capsys, execinfo, [ check_trace_output(capsys, execinfo, [
' File "<string>", line 2', ' File "<string>", line 2',
' (foo', ' (foo',