diff --git a/hy/_compat.py b/hy/_compat.py index 4416ea5b6..1680212c6 100644 --- a/hy/_compat.py +++ b/hy/_compat.py @@ -6,7 +6,10 @@ import __builtin__ as builtins except ImportError: import builtins # NOQA -import sys, keyword + +import sys +import keyword +import textwrap PY3 = sys.version_info[0] >= 3 PY35 = sys.version_info >= (3, 5) @@ -23,10 +26,21 @@ string_types = str if PY3 else basestring # NOQA if PY3: - exec('def raise_empty(t, *args): raise t(*args) from None') + reraise_src = textwrap.dedent(''' + def reraise(exc_type, values, traceback=None): + if not isinstance(values, tuple): + values = values.args + raise exc_type(*values) from traceback + ''') else: - def raise_empty(t, *args): - raise t(*args) + reraise_src = textwrap.dedent(''' + def reraise(exc_type, values, traceback=None): + raise exc_type, getattr(values, "args", values), traceback + ''') + +reraise_code = compile(reraise_src, __file__, 'exec') +exec(reraise_code) + def isidentifier(x): if x in ('True', 'False', 'None', 'print'): diff --git a/hy/cmdline.py b/hy/cmdline.py index f38355bd9..d77735937 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -17,8 +17,11 @@ import astor.code_gen import hy -from hy.lex import LexException, PrematureEndOfInput, mangle -from hy.compiler import HyTypeError, hy_compile + +from hy.lex import mangle +from hy.lex.exceptions import HySyntaxError, PrematureEndOfInput +from hy.errors import filtered_hy_exceptions +from hy.compiler import hy_compile from hy.importer import hy_eval, hy_parse, runhy from hy.completer import completion, Completer from hy.macros import macro, require @@ -47,7 +50,7 @@ def __call__(self, code=None): builtins.exit = HyQuitter('exit') -class HyREPL(code.InteractiveConsole): +class HyREPL(code.InteractiveConsole, object): def __init__(self, spy=False, output_fn=None, locals=None, filename=""): @@ -65,33 +68,48 @@ def __init__(self, spy=False, output_fn=None, locals=None, else: self.output_fn = __builtins__[mangle(output_fn)] - code.InteractiveConsole.__init__(self, locals=locals, - filename=filename) + super(HyREPL, self).__init__(locals=locals, filename=filename) # Pre-mangle symbols for repl recent results: *1, *2, *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}) - def runsource(self, source, filename='', symbol='single'): - global SIMPLE_TRACEBACKS + def error_handler(self, e): + self.locals[mangle("*e")] = e + self.showtraceback() - def error_handler(e, use_simple_traceback=False): - self.locals[mangle("*e")] = e - if use_simple_traceback: - print(e, file=sys.stderr) - else: - self.showtraceback() + def showsyntaxerror(self, filename=None): + if filename is None: + filename = self.filename + + sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() + + # Sadly, this method in Python 2.7 ignores an overridden `sys.excepthook`. + if sys.excepthook is sys.__excepthook__: + super(HyREPL, self).showsyntaxerror(filename=filename) + else: + sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) + + self.locals[mangle("*e")] = sys.last_value + def showtraceback(self): + sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() + + # Sadly, this method in Python 2.7 ignores an overridden `sys.excepthook`. + if sys.excepthook is sys.__excepthook__: + super(HyREPL, self).showtraceback() + else: + sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) + + self.locals[mangle("*e")] = sys.last_value + + def runsource(self, source, filename='', symbol='single'): try: - try: - do = hy_parse(source) - except PrematureEndOfInput: - return True - except LexException as e: - if e.source is None: - e.source = source - e.filename = filename - error_handler(e, use_simple_traceback=True) + do = hy_parse(source, filename=filename) + except PrematureEndOfInput: + return True + except HySyntaxError: + self.showsyntaxerror(filename=filename) return False try: @@ -102,16 +120,13 @@ def ast_callback(main_ast, expr_ast): new_ast = ast.Module(main_ast.body + [ast.Expr(expr_ast.body)]) print(astor.to_source(new_ast)) + value = hy_eval(do, self.locals, "__console__", - ast_callback) - except HyTypeError as e: - if e.source is None: - e.source = source - e.filename = filename - error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS) - return False - except Exception as e: - error_handler(e) + ast_callback, filename=filename, source=source) + except SystemExit: + raise + except Exception: + self.showtraceback() return False if value is not None: @@ -123,10 +138,12 @@ def ast_callback(main_ast, expr_ast): # Print the value. try: output = self.output_fn(value) - except Exception as e: - error_handler(e) + except Exception: + self.showtraceback() return False + print(output) + return False @@ -184,22 +201,11 @@ def ideas_macro(ETname): require("hy.cmdline", "__console__", assignments="ALL") require("hy.cmdline", "__main__", assignments="ALL") -SIMPLE_TRACEBACKS = True - -def pretty_error(func, *args, **kw): - try: - return func(*args, **kw) - except (HyTypeError, LexException) as e: - if SIMPLE_TRACEBACKS: - print(e, file=sys.stderr) - sys.exit(1) - raise - - -def run_command(source): - tree = hy_parse(source) - pretty_error(hy_eval, tree, module_name="__main__") +def run_command(source, filename=None): + tree = hy_parse(source, filename=filename) + with filtered_hy_exceptions(): + hy_eval(tree, module_name="__main__", filename=filename, source=source) return 0 @@ -210,7 +216,7 @@ def run_repl(hr=None, **kwargs): namespace = {'__name__': '__console__', '__doc__': ''} - with completion(Completer(namespace)): + with filtered_hy_exceptions(), completion(Completer(namespace)): if not hr: hr = HyREPL(locals=namespace, **kwargs) @@ -243,9 +249,10 @@ def run_icommand(source, **kwargs): else: filename = '' - hr = HyREPL(**kwargs) - hr.runsource(source, filename=filename, symbol='single') - return run_repl(hr) + with filtered_hy_exceptions(): + hr = HyREPL(**kwargs) + hr.runsource(source, filename=filename, symbol='single') + return run_repl(hr) USAGE = "%(prog)s [-h | -i cmd | -c cmd | -m module | file | -] [arg] ..." @@ -281,9 +288,6 @@ def cmdline_handler(scriptname, argv): "(e.g., hy.contrib.hy-repr.hy-repr)") parser.add_argument("-v", "--version", action="version", version=VERSION) - parser.add_argument("--show-tracebacks", action="store_true", - help="show complete tracebacks for Hy exceptions") - # this will contain the script/program name and any arguments for it. parser.add_argument('args', nargs=argparse.REMAINDER, help=argparse.SUPPRESS) @@ -308,10 +312,6 @@ def cmdline_handler(scriptname, argv): options = parser.parse_args(argv[1:]) - if options.show_tracebacks: - global SIMPLE_TRACEBACKS - SIMPLE_TRACEBACKS = False - if options.E: # User did "hy -E ..." _remove_python_envs() @@ -321,7 +321,7 @@ def cmdline_handler(scriptname, argv): if options.command: # User did "hy -c ..." - return run_command(options.command) + return run_command(options.command, filename='') if options.mod: # User did "hy -m ..." @@ -337,7 +337,7 @@ def cmdline_handler(scriptname, argv): if options.args: if options.args[0] == "-": # Read the program from stdin - return run_command(sys.stdin.read()) + return run_command(sys.stdin.read(), filename='') else: # User did "hy " @@ -352,7 +352,8 @@ def cmdline_handler(scriptname, argv): try: sys.argv = options.args - runhy.run_path(filename, run_name='__main__') + with filtered_hy_exceptions(): + runhy.run_path(filename, run_name='__main__') return 0 except FileNotFoundError as e: print("hy: Can't open file '{0}': [Errno {1}] {2}".format( @@ -427,41 +428,44 @@ def hy2py_main(): options = parser.parse_args(sys.argv[1:]) - if options.FILE is None or options.FILE == '-': - source = sys.stdin.read() - else: - with io.open(options.FILE, 'r', encoding='utf-8') as source_file: - source = source_file.read() - - hst = pretty_error(hy_parse, source) - if options.with_source: - # need special printing on Windows in case the - # codepage doesn't support utf-8 characters - if PY3 and platform.system() == "Windows": - for h in hst: - try: - print(h) - except: - print(str(h).encode('utf-8')) - else: - print(hst) - print() - print() - - _ast = pretty_error(hy_compile, hst, module_name) - if options.with_ast: - if PY3 and platform.system() == "Windows": - _print_for_windows(astor.dump_tree(_ast)) + with filtered_hy_exceptions(): + if options.FILE is None or options.FILE == '-': + source = sys.stdin.read() + hst = hy_parse(source, filename='') else: - print(astor.dump_tree(_ast)) - print() - print() + with io.open(options.FILE, 'r', encoding='utf-8') as source_file: + source = source_file.read() + hst = hy_parse(source, filename=options.FILE) + + if options.with_source: + # need special printing on Windows in case the + # codepage doesn't support utf-8 characters + if PY3 and platform.system() == "Windows": + for h in hst: + try: + print(h) + except Exception: + print(str(h).encode('utf-8')) + else: + print(hst) + print() + print() - if not options.without_python: - if PY3 and platform.system() == "Windows": - _print_for_windows(astor.code_gen.to_source(_ast)) - else: - print(astor.code_gen.to_source(_ast)) + _ast = hy_compile(hst, module_name) + + if options.with_ast: + if PY3 and platform.system() == "Windows": + _print_for_windows(astor.dump_tree(_ast)) + else: + print(astor.dump_tree(_ast)) + print() + print() + + if not options.without_python: + if PY3 and platform.system() == "Windows": + _print_for_windows(astor.code_gen.to_source(_ast)) + else: + print(astor.code_gen.to_source(_ast)) parser.exit(0) diff --git a/hy/compiler.py b/hy/compiler.py index bd43cf7f2..356583ec4 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -14,8 +14,7 @@ from hy.lex import mangle, unmangle import hy.macros -from hy._compat import ( - str_type, bytes_type, long_type, PY3, PY35, raise_empty) +from hy._compat import (str_type, bytes_type, long_type, PY3, PY35, reraise) from hy.macros import require, macroexpand, tag_macroexpand import hy.importer @@ -284,10 +283,16 @@ def is_unpack(kind, x): class HyASTCompiler(object): - def __init__(self, module_name): + + + def __init__(self, module_name, filename='', source=None): self.anon_var_count = 0 self.imports = defaultdict(set) self.module_name = module_name + + self.filename = filename + self.source = source + self.temp_if = None self.can_use_stdlib = ( not module_name.startswith("hy.core") @@ -355,10 +360,12 @@ def compile(self, tree): # nested; so let's re-raise this exception, let's not wrap it in # another HyCompileError! raise - except HyTypeError: - raise + except HyTypeError as e: + reraise(type(e), e, None) except Exception as e: - raise_empty(HyCompileError, e, sys.exc_info()[2]) + f_exc = traceback.format_exc() + exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(''.join(f_exc)) + reraise(HyCompileError, (exc_msg,), sys.exc_info()[2]) def _compile_collect(self, exprs, with_kwargs=False, dict_display=False, oldpy_unpack=False): @@ -379,8 +386,9 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False, if not PY35 and oldpy_unpack and is_unpack("iterable", expr): if oldpy_starargs: - raise HyTypeError(expr, "Pythons < 3.5 allow only one " - "`unpack-iterable` per call") + raise HyTypeError("Pythons < 3.5 allow only one " + "`unpack-iterable` per call", + (self.filename, expr, self.source)) oldpy_starargs = self.compile(expr[1]) ret += oldpy_starargs oldpy_starargs = oldpy_starargs.force_expr @@ -396,21 +404,23 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False, expr, arg=None, value=ret.force_expr)) elif oldpy_unpack: if oldpy_kwargs: - raise HyTypeError(expr, "Pythons < 3.5 allow only one " - "`unpack-mapping` per call") + raise HyTypeError("Pythons < 3.5 allow only one " + "`unpack-mapping` per call", + (self.filename, expr, self.source)) oldpy_kwargs = ret.force_expr elif with_kwargs and isinstance(expr, HyKeyword): try: value = next(exprs_iter) except StopIteration: - raise HyTypeError(expr, - "Keyword argument {kw} needs " - "a value.".format(kw=expr)) + raise HyTypeError("Keyword argument {kw} needs " + "a value.".format(kw=expr), + (self.filename, expr, self.source)) if not expr: - raise HyTypeError(expr, "Can't call a function with the " - "empty keyword") + raise HyTypeError("Can't call a function with the " + "empty keyword", + (self.filename, expr, self.source)) compiled_value = self.compile(value) ret += compiled_value @@ -451,8 +461,8 @@ def _storeize(self, expr, name, func=None): if isinstance(name, Result): if not name.is_expr(): - raise HyTypeError(expr, - "Can't assign or delete a non-expression") + raise HyTypeError("Can't assign or delete a non-expression", + (self.filename, expr, self.source)) name = name.expr if isinstance(name, (ast.Tuple, ast.List)): @@ -471,9 +481,8 @@ def _storeize(self, expr, name, func=None): new_name = ast.Starred( value=self._storeize(expr, name.value, func)) else: - raise HyTypeError(expr, - "Can't assign or delete a %s" % - type(expr).__name__) + raise HyTypeError("Can't assign or delete a %s" % type(expr).__name__, + (self.filename, expr, self.source)) new_name.ctx = func() ast.copy_location(new_name, name) @@ -499,9 +508,8 @@ def _render_quoted_form(self, form, level): op = unmangle(ast_str(form[0])) if level == 0 and op in ("unquote", "unquote-splice"): if len(form) != 2: - raise HyTypeError(form, - ("`%s' needs 1 argument, got %s" % - op, len(form) - 1)) + raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1, + (self.filename, expr, self.source)) return set(), form[1], op == "unquote-splice" elif op == "quasiquote": level += 1 @@ -553,7 +561,8 @@ def compile_quote(self, expr, root, arg): @special("unpack-iterable", [FORM]) def compile_unpack_iterable(self, expr, root, arg): if not PY3: - raise HyTypeError(expr, "`unpack-iterable` isn't allowed here") + raise HyTypeError("`unpack-iterable` isn't allowed here", + (self.filename, expr, self.source)) ret = self.compile(arg) ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load()) return ret @@ -583,7 +592,8 @@ def compile_raise_expression(self, expr, root, exc, cause): if cause is not None: if not PY3: - raise HyTypeError(expr, "raise from only supported in python 3") + raise HyTypeError("raise from only supported in python 3", + (self.filename, expr, self.source)) cause = self.compile(cause) ret += cause cause = cause.force_expr @@ -630,14 +640,12 @@ def compile_try_expression(self, expr, root, body, catchers, orelse, finalbody): # Using (else) without (except) is verboten! if orelse and not handlers: - raise HyTypeError( - expr, - "`try' cannot have `else' without `except'") + raise HyTypeError("`try' cannot have `else' without `except'", + (self.filename, expr, self.source)) # Likewise a bare (try) or (try BODY). if not (handlers or finalbody): - raise HyTypeError( - expr, - "`try' must have an `except' or `finally' clause") + raise HyTypeError("`try' must have an `except' or `finally' clause", + (self.filename, expr, self.source)) returnable = Result( expr=asty.Name(expr, id=return_var.id, ctx=ast.Load()), @@ -887,7 +895,8 @@ def c(e): def compile_decorate_expression(self, expr, name, args): decs, fn = args[:-1], self.compile(args[-1]) if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables): - raise HyTypeError(args[-1], "Decorated a non-function") + raise HyTypeError("Decorated a non-function", + (self.filename, args[-1], self.source)) decs, ret, _ = self._compile_collect(decs) fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list return ret + fn @@ -1123,8 +1132,9 @@ def compile_import_or_require(self, expr, root, entries): if (HySymbol('*'), None) in kids: if len(kids) != 1: star = kids[kids.index((HySymbol('*'), None))][0] - raise HyTypeError(star, "* in an import name list " - "must be on its own") + raise HyTypeError("* in an import name list " + "must be on its own", + (self.filename, star, self.source)) else: assignments = [(k, v or k) for k, v in kids] @@ -1305,15 +1315,15 @@ def _compile_assign(self, name, result): if str_name in (["None"] + (["True", "False"] if PY3 else [])): # Python 2 allows assigning to True and False, although # this is rarely wise. - raise HyTypeError(name, - "Can't assign to `%s'" % str_name) + raise HyTypeError("Can't assign to `%s'" % str_name, + (self.filename, name, self.source)) result = self.compile(result) ld_name = self.compile(name) if isinstance(ld_name.expr, ast.Call): - raise HyTypeError(name, - "Can't assign to a callable: `%s'" % str_name) + raise HyTypeError("Can't assign to a callable: `%s'" % str_name, + (self.filename, name, self.source)) if (result.temp_variables and isinstance(name, HySymbol) @@ -1389,7 +1399,8 @@ def compile_function_def(self, expr, root, params, body): mandatory, optional, rest, kwonly, kwargs = params optional, defaults, ret = self._parse_optional_args(optional) if kwonly is not None and not PY3: - raise HyTypeError(params, "&kwonly parameters require Python 3") + raise HyTypeError("&kwonly parameters require Python 3", + (self.filename, params, self.source)) kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True) ret += ret2 main_args = mandatory + optional @@ -1533,7 +1544,8 @@ def compile_eval_and_compile(self, expr, root, body): 'hy': hy, '__name__': self.module_name} hy.importer.hy_eval(new_expr + body, self._namespaces[self.module_name], - self.module_name) + self.module_name, + filename=self.filename) return (self._compile_branch(body) if ast_str(root) == "eval_and_compile" else Result()) @@ -1547,8 +1559,8 @@ def compile_expression(self, expr): return self.compile(expr) if not expr: - raise HyTypeError( - expr, "empty expressions are not allowed at top level") + raise HyTypeError("empty expressions are not allowed at top level", + (self.filename, expr, self.source)) args = list(expr) root = args.pop(0) @@ -1566,20 +1578,19 @@ def compile_expression(self, expr): sroot in (mangle(","), mangle(".")) or not any(is_unpack("iterable", x) for x in args)): if sroot in _bad_roots: - raise HyTypeError( - expr, - "The special form '{}' is not allowed here".format(root)) + raise HyTypeError("The special form '{}' is not allowed here".format(root), + (self.filename, expr, self.source)) # `sroot` is a special operator. Get the build method and # pattern-match the arguments. build_method, pattern = _special_form_compilers[sroot] try: parse_tree = pattern.parse(args) except NoParseError as e: - raise HyTypeError( - expr[min(e.state.pos + 1, len(expr) - 1)], - "parse error for special form '{}': {}".format( - root, - e.msg.replace("", "end of form"))) + raise HyTypeError("parse error for special form '{}': {}".format( + root, e.msg.replace("", "end of form")), + (self.filename, + expr[min(e.state.pos + 1, len(expr) - 1)], + self.source)) return Result() + build_method( self, expr, unmangle(sroot), *parse_tree) @@ -1601,13 +1612,13 @@ def compile_expression(self, expr): FORM + many(FORM)).parse(args) except NoParseError: - raise HyTypeError( - expr, "attribute access requires object") + raise HyTypeError("attribute access requires object", + (self.filename, expr, self.source)) # Reconstruct `args` to exclude `obj`. args = [x for p in kws for x in p] + list(rest) if is_unpack("iterable", obj): - raise HyTypeError( - obj, "can't call a method on an unpacking form") + raise HyTypeError("can't call a method on an unpacking form", + (self.filename, obj, self.source)) func = self.compile(HyExpression( [HySymbol(".").replace(root), obj] + attrs)) @@ -1645,16 +1656,17 @@ def compile_symbol(self, symbol): glob, local = symbol.rsplit(".", 1) if not glob: - raise HyTypeError(symbol, 'cannot access attribute on ' - 'anything other than a name ' - '(in order to get attributes of ' - 'expressions, use ' - '`(. {attr})` or ' - '`(.{attr} )`)'.format( - attr=local)) + raise HyTypeError('cannot access attribute on ' + 'anything other than a name ' + '(in order to get attributes of ' + 'expressions, use ' + '`(. {attr})` or ' + '`(.{attr} )`)'.format(attr=local), + (self.filename, symbol, self.source)) if not local: - raise HyTypeError(symbol, 'cannot access empty attribute') + raise HyTypeError('cannot access empty attribute', + (self.filename, symbol, self.source)) glob = HySymbol(glob).replace(symbol) ret = self.compile_symbol(glob) @@ -1699,7 +1711,8 @@ def compile_dict(self, m): return ret + asty.Dict(m, keys=keyvalues[::2], values=keyvalues[1::2]) -def hy_compile(tree, module_name, root=ast.Module, get_expr=False): +def hy_compile(tree, module_name, root=ast.Module, get_expr=False, + filename=None, source=None): """ Compile a HyObject tree into a Python AST Module. @@ -1709,10 +1722,22 @@ def hy_compile(tree, module_name, root=ast.Module, get_expr=False): tree = wrap_value(tree) if not isinstance(tree, HyObject): - raise HyCompileError("`tree` must be a HyObject or capable of " - "being promoted to one") - - compiler = HyASTCompiler(module_name) + raise TypeError("`tree` must be a HyObject or capable of " + "being promoted to one") + + try: + mod_loader = pkgutil.get_loader(module_name) + if filename is None: + filename = mod_loader.get_filename() + # TODO: Lazy load this. + if source is None: + source = mod_loader.get_source() + except Exception: + pass + + filename = filename or '' + + compiler = HyASTCompiler(module_name, filename=filename, source=source) result = compiler.compile(tree) expr = result.force_expr diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy index cc576bc74..2255844dd 100644 --- a/hy/core/bootstrap.hy +++ b/hy/core/bootstrap.hy @@ -14,15 +14,15 @@ (if* (not (isinstance macro-name hy.models.HySymbol)) (raise (hy.errors.HyTypeError - macro-name (% "received a `%s' instead of a symbol for macro name" (. (type name) - __name__))))) + __name__)) + (, None macro-name None)))) (for [kw '[&kwonly &kwargs]] (if* (in kw lambda-list) - (raise (hy.errors.HyTypeError macro-name - (% "macros cannot use %s" - kw))))) + (raise (hy.errors.HyTypeError (% "macros cannot use %s" + kw) + (, None macro-name None))))) ;; this looks familiar... `(eval-and-compile (import hy) @@ -45,9 +45,9 @@ (if (and (not (isinstance tag-name hy.models.HySymbol)) (not (isinstance tag-name hy.models.HyString))) (raise (hy.errors.HyTypeError - tag-name (% "received a `%s' instead of a symbol for tag macro name" - (. (type tag-name) __name__))))) + (. (type tag-name) __name__)) + (, None tag-name None)))) (if (or (= tag-name ":") (= tag-name "&")) (raise (NameError (% "%s can't be used as a tag macro name" tag-name)))) @@ -60,7 +60,7 @@ (defmacro macro-error [location reason] "Error out properly within a macro at `location` giving `reason`." - `(raise (hy.errors.HyMacroExpansionError ~location ~reason))) + `(raise (hy.errors.HyMacroExpansionError ~reason (, None ~location None)))) (defmacro defn [name lambda-list &rest body] "Define `name` as a function with `lambda-list` signature and body `body`." diff --git a/hy/core/language.hy b/hy/core/language.hy index abb387fc6..5ea13cfa1 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -19,7 +19,8 @@ (import [collections :as cabc]) (import [collections.abc :as cabc])) (import [hy.models [HySymbol HyKeyword]]) -(import [hy.lex [LexException PrematureEndOfInput tokenize mangle unmangle]]) +(import [hy.lex [tokenize mangle unmangle]]) +(import [hy.lex.exceptions [PrematureEndOfInput]]) (import [hy.compiler [HyASTCompiler]]) (import [hy.importer [hy-eval :as eval]]) diff --git a/hy/errors.py b/hy/errors.py index a60b09cb5..a0ae0ca0a 100644 --- a/hy/errors.py +++ b/hy/errors.py @@ -2,52 +2,88 @@ # Copyright 2018 the authors. # This file is part of Hy, which is free software licensed under the Expat # license. See the LICENSE. - +import os +import sys import traceback +import pkgutil + +from contextlib import contextmanager + + +__colored_errors = False -from clint.textui import colored + +def colored_errors(): + global __colored_errors + return os.environ.get('HY_COLORED_ERRORS', __colored_errors) class HyError(Exception): + def __init__(self, message, *args): + self.message = message + super(HyError, self).__init__(message, *args) + + +class HyInternalError(HyError): + """Unexpected errors occurring during compilation or parsing of Hy code. + + Errors sub-classing this are not intended to be user-facing. """ - Generic Hy error. All internal Exceptions will be subclassed from this - Exception. + + def __init__(self, message, *args): + super(HyInternalError, self).__init__(message, *args) + + +class HyLanguageError(HyError): + """Errors occurring during the expected use of Hy. + + Errors sub-classing this will be user-facing. """ - pass + def __init__(self, message, *args): + super(HyLanguageError, self).__init__(message, *args) -class HyCompileError(HyError): - def __init__(self, exception, traceback=None): - self.exception = exception - self.traceback = traceback - def __str__(self): - if isinstance(self.exception, HyTypeError): - return str(self.exception) - if self.traceback: - tb = "".join(traceback.format_tb(self.traceback)).strip() - else: - tb = "No traceback available. 😟" - return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s" - % (self.exception.__class__.__name__, - self.exception, tb)) +class HyCompileError(HyInternalError): + def __init__(self, message, *args): + super(HyCompileError, self).__init__(message, *args) -class HyTypeError(TypeError): - def __init__(self, expression, message): - super(HyTypeError, self).__init__(message) - self.expression = expression +class HyTypeError(HyLanguageError, TypeError): + def __init__(self, message, args=None): + """ + Parameters + ---------- + message: str + The exception's message. + args: tuple, optional + A tuple with the form `(filename, expression, source)`. + """ self.message = message - self.source = None - self.filename = None + + if args: + self.filename, self.expression, self.source = args + else: + self.filename = self.expression = self.source = None + + new_args = (self.filename, self.expression, self.source) + + super(HyTypeError, self).__init__(message, new_args) def __str__(self): result = "" - if all(getattr(self.expression, x, None) is not None - for x in ("start_line", "start_column", "end_column")): + if colored_errors(): + from clint.textui import colored + red, green, yellow = colored.red, colored.green, colored.yellow + else: + red = green = yellow = lambda x: x + has_syntax_info = all(hasattr(self.expression, x) + for x in ("start_line", "start_column", + "end_column")) + if has_syntax_info: line = self.expression.start_line start = self.expression.start_column end = self.expression.end_column @@ -61,44 +97,145 @@ def __str__(self): else: length = len(source[0]) - start - result += ' File "%s", line %d, column %d\n\n' % (self.filename, - line, - start) + result += ' File "%s", line %d, column %d\n\n' % (self.filename, line, start) if len(source) == 1: - result += ' %s\n' % colored.red(source[0]) + result += ' %s\n' % red(source[0]) result += ' %s%s\n' % (' '*(start-1), - colored.green('^' + '-'*(length-1) + '^')) + green('^' + '-'*(length-1) + '^')) if len(source) > 1: - result += ' %s\n' % colored.red(source[0]) + result += ' %s\n' % red(source[0]) result += ' %s%s\n' % (' '*(start-1), - colored.green('^' + '-'*length)) + green('^' + '-'*length)) if len(source) > 2: # write the middle lines for line in source[1:-1]: - result += ' %s\n' % colored.red("".join(line)) - result += ' %s\n' % colored.green("-"*len(line)) + result += ' %s\n' % red("".join(line)) + result += ' %s\n' % green("-"*len(line)) # write the last line - result += ' %s\n' % colored.red("".join(source[-1])) - result += ' %s\n' % colored.green('-'*(end-1) + '^') + result += ' %s\n' % red("".join(source[-1])) + result += ' %s\n' % green('-'*(end-1) + '^') else: result += ' File "%s", unknown location\n' % self.filename - result += colored.yellow("%s: %s\n\n" % - (self.__class__.__name__, - self.message)) + result += yellow("%s: %s\n\n" % (self.__class__.__name__, self.message)) return result class HyMacroExpansionError(HyTypeError): - pass + def __init__(self, message, args=None): + super(HyMacroExpansionError, self).__init__(message, args) -class HyIOError(HyError, IOError): +class HyIOError(HyInternalError, IOError): + """ Subclass used to distinguish between IOErrors raised by Hy itself as + opposed to Hy programs. """ - Trivial subclass of IOError and HyError, to distinguish between - IOErrors raised by Hy itself as opposed to Hy programs. + + def __init__(self, message, *args): + super(HyIOError, self).__init__(message, *args) + + +class HySyntaxError(HyLanguageError, SyntaxError): + """Error during the Lexing of a Hython expression.""" + + def __init__(self, message, args=None): + """ + Parameters + ---------- + message: str + The exception's message. + args: tuple, optional + A tuple with the form `(filename, lineno, colno, source)`. + """ + self.message = message + + if args: + self.filename, self.lineno, self.colno, self.source = args + else: + self.filename = self.lineno = self.colno = self.source = None + + new_args = (self.filename, self.lineno, self.colno, self.source) + + super(HySyntaxError, self).__init__(message, new_args) + + def __str__(self): + + output = traceback.format_exception_only(SyntaxError, + SyntaxError(*self.args)) + + if 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]) + + return ''.join(output) + + +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 = {_get_module_info(m) + for m in ['hy.compiler', 'hy.lex', + 'hy.cmdline', 'hy.lex.parser', + 'hy.importer', 'hy._compat', + 'rply']} + + +def hy_exc_handler(exc_type, exc_value, exc_traceback): + """Produce exceptions print-outs with all frames originating from the + modules in `__tb_hidden_modules` filtered out. + + The frames are actually filtered by each module's filename and only when a + subclass of `HyLanguageError` is emitted. + + This does not remove the frames from the actual tracebacks, so debugging + will show everything. """ - pass + + 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) + + sys.stderr.write(output) + sys.stderr.flush() + except Exception: + sys.__excepthook__(exc_type, exc_value, exc_traceback) + + +@contextmanager +def filtered_hy_exceptions(): + """Remove Hy internal frames from tracebacks.""" + current_hook = sys.excepthook + sys.excepthook = hy_exc_handler + yield + sys.excepthook = current_hook diff --git a/hy/importer.py b/hy/importer.py index 1bc857377..ae01a77cf 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -6,7 +6,6 @@ import sys import os -import ast import inspect import pkgutil import re @@ -18,23 +17,23 @@ from functools import partial -from hy.errors import HyTypeError from hy.compiler import hy_compile -from hy.lex import tokenize, LexException +from hy.lex import tokenize from hy.models import HyExpression, HySymbol -from hy._compat import string_types, PY3 +from hy.lex.exceptions import HySyntaxError +from hy._compat import string_types, PY3, reraise hy_ast_compile_flags = (__future__.CO_FUTURE_DIVISION | __future__.CO_FUTURE_PRINT_FUNCTION) -def ast_compile(ast, filename, mode): +def ast_compile(hy_ast, filename, mode): """Compile AST. Parameters ---------- - ast : instance of `ast.AST` + hy_ast : instance of `ast.AST` filename : str Filename used for run-time error messages @@ -46,10 +45,10 @@ def ast_compile(ast, filename, mode): ------- out : instance of `types.CodeType` """ - return compile(ast, filename, mode, hy_ast_compile_flags) + return compile(hy_ast, filename, mode, hy_ast_compile_flags) -def hy_parse(source): +def hy_parse(source, filename=None): """Parse a Hy source string. Parameters @@ -57,21 +56,38 @@ def hy_parse(source): source: string Source code to parse. + filename: string, optional + File name corresponding to source. + Returns ------- - out : instance of `types.CodeType` + out : HyExpression """ - source = re.sub(r'\A#!.*', '', source) - return HyExpression([HySymbol("do")] + tokenize(source + "\n")) + _source = re.sub(r'\A#!.*', '', source) + # TODO: Could check for shebang removal a better way. + lineno_offset = 1 if _source != source else 0 + source = _source + try: + return HyExpression([HySymbol("do")] + + tokenize(source + "\n", + filename=filename, + lineno_offset=lineno_offset)) + except HySyntaxError as e: + reraise(type(e), e.args, None) -def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None): +def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None, + filename=None, source=None): """Evaluates a quoted expression and returns the value. The optional second and third arguments specify the dictionary of globals to use and the module name. The globals dictionary defaults to ``(local)`` and the module name defaults to the name of the current module. + NOTE: If you're evaluating hand-crafted AST trees, make sure line numbers + are set properly. Try `fix_missing_locations` and related functions in the + Python `ast` library. + Examples -------- @@ -102,6 +118,15 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None): expression object, in that order, after compilation but before evaluation. + filename: str, optional + The filename given to `compile`. Will be obtained from the module + given by `module_name` if `None` (and available). + + source: str, optional + Source string to use for error messages (e.g. macro expansion errors). + Will be obtained from the module given by `module_name` if `None` (and + available). + Returns ------- out : Result of evaluating the Hy compiled tree. @@ -117,16 +142,8 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None): if not isinstance(module_name, string_types): raise TypeError("Module name must be a string") - _ast, expr = hy_compile(hytree, module_name, get_expr=True) - - # Spoof the positions in the generated ast... - for node in ast.walk(_ast): - node.lineno = 1 - node.col_offset = 1 - - for node in ast.walk(expr): - node.lineno = 1 - node.col_offset = 1 + _ast, expr = hy_compile(hytree, module_name, get_expr=True, + filename=filename, source=source) if ast_callback: ast_callback(_ast, expr) @@ -134,11 +151,13 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None): if not isinstance(namespace, dict): raise TypeError("Globals must be a dictionary") + filename = filename or '' + # Two-step eval: eval() the body of the exec call - eval(ast_compile(_ast, "", "exec"), namespace) + eval(ast_compile(_ast, filename, "exec"), namespace) # Then eval the expression context and return that - return eval(ast_compile(expr, "", "eval"), namespace) + return eval(ast_compile(expr, filename, "eval"), namespace) def cache_from_source(source_path): @@ -224,14 +243,8 @@ def _could_be_hy_src(filename): def _hy_source_to_code(self, data, path, _optimize=-1): if _could_be_hy_src(path): source = data.decode("utf-8") - try: - hy_tree = hy_parse(source) - data = hy_compile(hy_tree, self.name) - except (HyTypeError, LexException) as e: - if e.source is None: - e.source = source - e.filename = path - raise + hy_tree = hy_parse(source, filename=path) + data = hy_compile(hy_tree, self.name) return _py_source_to_code(self, data, path, _optimize=_optimize) @@ -339,18 +352,14 @@ def byte_compile_hy(self, fullname=None): fullname = self._fix_name(fullname) if fullname is None: fullname = self.fullname - try: - hy_source = self.get_source(fullname) - hy_tree = hy_parse(hy_source) - hy_ast = hy_compile(hy_tree, fullname) - - code = compile(hy_ast, self.filename, 'exec', - hy_ast_compile_flags) - except (HyTypeError, LexException) as e: - if e.source is None: - e.source = hy_source - e.filename = self.filename - raise + + hy_source = self.get_source(fullname) + + hy_tree = hy_parse(hy_source, filename=self.filename) + hy_ast = hy_compile(hy_tree, fullname) + + code = compile(hy_ast, self.filename, 'exec', + hy_ast_compile_flags) if not sys.dont_write_bytecode: try: @@ -500,15 +509,12 @@ def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False): try: flags = None if _could_be_hy_src(filename): - hy_tree = hy_parse(source_str) + hy_tree = hy_parse(source_str, filename=filename) source = hy_compile(hy_tree, '') flags = hy_ast_compile_flags codeobject = compile(source, dfile or filename, 'exec', flags) except Exception as err: - if isinstance(err, (HyTypeError, LexException)) and err.source is None: - err.source = source_str - err.filename = filename py_exc = py_compile.PyCompileError(err.__class__, err, dfile or filename) diff --git a/hy/lex/__init__.py b/hy/lex/__init__.py index 5c0514380..02a453f5a 100644 --- a/hy/lex/__init__.py +++ b/hy/lex/__init__.py @@ -4,27 +4,45 @@ from __future__ import unicode_literals -import re, unicodedata +import re +import unicodedata + from hy._compat import str_type, isidentifier, UCS4 -from hy.lex.exceptions import LexException, PrematureEndOfInput # NOQA +from hy.lex.exceptions import LexException # NOQA -def tokenize(buf): - """ - Tokenize a Lisp file or string buffer into internal Hy objects. + +class ParserState(object): + def __init__(self, source, filename, lineno_offset=0): + self.source = source + self.filename = filename + self.lineno_offset = lineno_offset + + +def tokenize(source, filename=None, lineno_offset=0): + """ Tokenize a Lisp file or string buffer into internal Hy objects. + + Parameters + ---------- + source: str + The source to tokenize. + filename: str, optional + The filename corresponding to `source`. + lineno_offset: int, optional + An offset for the line numbering. Useful when `source` has been + manipulated relative to `filename` (e.g. removal of shebang headers). """ from hy.lex.lexer import lexer from hy.lex.parser import parser from rply.errors import LexingError + try: - return parser.parse(lexer.lex(buf)) + return parser.parse(lexer.lex(source), + state=ParserState(source, filename, lineno_offset)) except LexingError as e: pos = e.getsourcepos() raise LexException("Could not identify the next token.", - pos.lineno, pos.colno, buf) - except LexException as e: - if e.source is None: - e.source = buf - raise + (filename, pos.lineno + lineno_offset, pos.colno, + source)) mangle_delim = 'X' diff --git a/hy/lex/exceptions.py b/hy/lex/exceptions.py index 14df78bfe..7feb741c2 100644 --- a/hy/lex/exceptions.py +++ b/hy/lex/exceptions.py @@ -1,49 +1,12 @@ # Copyright 2018 the authors. # This file is part of Hy, which is free software licensed under the Expat # license. See the LICENSE. +from hy.errors import HySyntaxError -from hy.errors import HyError +class LexException(HySyntaxError): + pass -class LexException(HyError): - """Error during the Lexing of a Hython expression.""" - def __init__(self, message, lineno, colno, source=None): - super(LexException, self).__init__(message) - self.message = message - self.lineno = lineno - self.colno = colno - self.source = source - self.filename = '' - def __str__(self): - from hy.errors import colored - - line = self.lineno - start = self.colno - - result = "" - - source = self.source.split("\n") - - if line > 0 and start > 0: - result += ' File "%s", line %d, column %d\n\n' % (self.filename, - line, - start) - - if len(self.source) > 0: - source_line = source[line-1] - else: - source_line = "" - - result += ' %s\n' % colored.red(source_line) - result += ' %s%s\n' % (' '*(start-1), colored.green('^')) - - result += colored.yellow("LexException: %s\n\n" % self.message) - - return result - - -class PrematureEndOfInput(LexException): - """We got a premature end of input""" - def __init__(self, message): - super(PrematureEndOfInput, self).__init__(message, -1, -1) +class PrematureEndOfInput(HySyntaxError): + pass diff --git a/hy/lex/parser.py b/hy/lex/parser.py index a13c79380..8a4660868 100755 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -22,84 +22,85 @@ def set_boundaries(fun): @wraps(fun) - def wrapped(p): + def wrapped(state, p): start = p[0].source_pos end = p[-1].source_pos - ret = fun(p) - ret.start_line = start.lineno + ret = fun(state, p) + ret.start_line = start.lineno + state.lineno_offset ret.start_column = start.colno if start is not end: ret.end_line = end.lineno ret.end_column = end.colno else: - ret.end_line = start.lineno + ret.end_line = start.lineno + state.lineno_offset ret.end_column = start.colno + len(p[0].value) + ret.end_line += state.lineno_offset return ret return wrapped def set_quote_boundaries(fun): @wraps(fun) - def wrapped(p): + def wrapped(state, p): start = p[0].source_pos - ret = fun(p) - ret.start_line = start.lineno + ret = fun(state, p) + ret.start_line = start.lineno + state.lineno_offset ret.start_column = start.colno - ret.end_line = p[-1].end_line + ret.end_line = p[-1].end_line + state.lineno_offset ret.end_column = p[-1].end_column return ret return wrapped @pg.production("main : list_contents") -def main(p): +def main(state, p): return p[0] @pg.production("main : $end") -def main_empty(p): +def main_empty(state, p): return [] -def reject_spurious_dots(*items): - "Reject the spurious dots from items" - for list in items: - for tok in list: - if tok == "." and type(tok) == HySymbol: - raise LexException("Malformed dotted list", - tok.start_line, tok.start_column) +# def reject_spurious_dots(*items): +# "Reject the spurious dots from items" +# for list in items: +# for tok in list: +# if tok == "." and type(tok) == HySymbol: +# raise LexException("Malformed dotted list", +# tok.start_line, tok.start_column) @pg.production("paren : LPAREN list_contents RPAREN") @set_boundaries -def paren(p): +def paren(state, p): return HyExpression(p[1]) @pg.production("paren : LPAREN RPAREN") @set_boundaries -def empty_paren(p): +def empty_paren(state, p): return HyExpression([]) @pg.production("list_contents : term list_contents") -def list_contents(p): +def list_contents(state, p): return [p[0]] + p[1] @pg.production("list_contents : term") -def list_contents_single(p): +def list_contents_single(state, p): return [p[0]] @pg.production("list_contents : DISCARD term discarded_list_contents") -def list_contents_empty(p): +def list_contents_empty(state, p): return [] @pg.production("discarded_list_contents : DISCARD term discarded_list_contents") @pg.production("discarded_list_contents :") -def discarded_list_contents(p): +def discarded_list_contents(state, p): pass @@ -109,42 +110,42 @@ def discarded_list_contents(p): @pg.production("term : list") @pg.production("term : set") @pg.production("term : string") -def term(p): +def term(state, p): return p[0] @pg.production("term : DISCARD term term") -def term_discard(p): +def term_discard(state, p): return p[2] @pg.production("term : QUOTE term") @set_quote_boundaries -def term_quote(p): +def term_quote(state, p): return HyExpression([HySymbol("quote"), p[1]]) @pg.production("term : QUASIQUOTE term") @set_quote_boundaries -def term_quasiquote(p): +def term_quasiquote(state, p): return HyExpression([HySymbol("quasiquote"), p[1]]) @pg.production("term : UNQUOTE term") @set_quote_boundaries -def term_unquote(p): +def term_unquote(state, p): return HyExpression([HySymbol("unquote"), p[1]]) @pg.production("term : UNQUOTESPLICE term") @set_quote_boundaries -def term_unquote_splice(p): +def term_unquote_splice(state, p): return HyExpression([HySymbol("unquote-splice"), p[1]]) @pg.production("term : HASHSTARS term") @set_quote_boundaries -def term_hashstars(p): +def term_hashstars(state, p): n_stars = len(p[0].getstr()[1:]) if n_stars == 1: sym = "unpack-iterable" @@ -154,13 +155,13 @@ def term_hashstars(p): raise LexException( "Too many stars in `#*` construct (if you want to unpack a symbol " "beginning with a star, separate it with whitespace)", - p[0].source_pos.lineno, p[0].source_pos.colno) + _get_exc_args(state, p[0])) return HyExpression([HySymbol(sym), p[1]]) @pg.production("term : HASHOTHER term") @set_quote_boundaries -def hash_other(p): +def hash_other(state, p): # p == [(Token('HASHOTHER', '#foo'), bar)] st = p[0].getstr()[1:] str_object = HyString(st) @@ -170,63 +171,64 @@ def hash_other(p): @pg.production("set : HLCURLY list_contents RCURLY") @set_boundaries -def t_set(p): +def t_set(state, p): return HySet(p[1]) @pg.production("set : HLCURLY RCURLY") @set_boundaries -def empty_set(p): +def empty_set(state, p): return HySet([]) @pg.production("dict : LCURLY list_contents RCURLY") @set_boundaries -def t_dict(p): +def t_dict(state, p): return HyDict(p[1]) @pg.production("dict : LCURLY RCURLY") @set_boundaries -def empty_dict(p): +def empty_dict(state, p): return HyDict([]) @pg.production("list : LBRACKET list_contents RBRACKET") @set_boundaries -def t_list(p): +def t_list(state, p): return HyList(p[1]) @pg.production("list : LBRACKET RBRACKET") @set_boundaries -def t_empty_list(p): +def t_empty_list(state, p): return HyList([]) @pg.production("string : STRING") @set_boundaries -def t_string(p): +def t_string(state, p): # Replace the single double quotes with triple double quotes to allow # embedded newlines. try: s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""') except SyntaxError: raise LexException("Can't convert {} to a HyString".format(p[0].value), - p[0].source_pos.lineno, p[0].source_pos.colno) + _get_exc_args(state, p[0])) return (HyString if isinstance(s, str_type) else HyBytes)(s) @pg.production("string : PARTIAL_STRING") -def t_partial_string(p): +def t_partial_string(state, p): # Any unterminated string requires more input - raise PrematureEndOfInput("Premature end of input") + raise PrematureEndOfInput("Partial string literal", + _get_exc_args(state, p[0])) bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING') @pg.production("string : BRACKETSTRING") @set_boundaries -def t_bracket_string(p): +def t_bracket_string(state, p): m = bracket_string_re.match(p[0].value) delim, content = m.groups() return HyString(content, brackets=delim) @@ -234,7 +236,7 @@ def t_bracket_string(p): @pg.production("identifier : IDENTIFIER") @set_boundaries -def t_identifier(p): +def t_identifier(state, p): obj = p[0].value val = symbol_like(obj) @@ -247,7 +249,7 @@ def t_identifier(p): 'Cannot access attribute on anything other than a name (in ' 'order to get attributes of expressions, use ' '`(. )` or `(. )`)', - p[0].source_pos.lineno, p[0].source_pos.colno) + _get_exc_args(state, p[0])) return HySymbol(obj) @@ -283,15 +285,47 @@ def symbol_like(obj): return HyKeyword(obj[1:]) +def _get_exc_args(state, token): + source_pos = token.getsourcepos() + if token.source_pos: + lineno = source_pos.lineno + state.lineno_offset + colno = source_pos.colno + else: + lineno = -1 + colno = -1 + + if state.source: + lines = state.source.splitlines() + if lines[-1] == '': + del lines[-1] + + if lineno < 1: + lineno = len(lines) + colno = len(lines[-1]) + + source = lines[lineno - 1] + return (state.filename, lineno, colno, source) + + @pg.error -def error_handler(token): +def error_handler(state, token): tokentype = token.gettokentype() if tokentype == '$end': - raise PrematureEndOfInput("Premature end of input") + source_pos = token.source_pos or token.getsourcepos() + source = state.source + if source_pos: + lineno = source_pos.lineno + state.lineno_offset + colno = source_pos.colno + else: + lineno = -1 + colno = -1 + + raise PrematureEndOfInput("Premature end of input", + _get_exc_args(state, token)) else: raise LexException( "Ran into a %s where it wasn't expected." % tokentype, - token.source_pos.lineno, token.source_pos.colno) + _get_exc_args(state, token)) parser = pg.build() diff --git a/hy/macros.py b/hy/macros.py index a86f04302..59707b804 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -10,7 +10,7 @@ import hy.inspect from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value from hy.lex import mangle -from hy._compat import str_type +from hy._compat import str_type, reraise from hy.errors import HyTypeError, HyMacroExpansionError @@ -184,7 +184,9 @@ def macroexpand(tree, compiler, once=False): except TypeError as e: msg = "expanding `" + str(tree[0]) + "': " msg += str(e).replace("()", "", 1).strip() - raise HyMacroExpansionError(tree, msg) + reraise(HyMacroExpansionError, + (msg, (compiler.filename, tree, compiler.source)), + None) try: obj = m(compiler.module_name, *tree[1:], **opts) @@ -194,7 +196,10 @@ def macroexpand(tree, compiler, once=False): raise except Exception as e: msg = "expanding `" + str(tree[0]) + "': " + repr(e) - raise HyMacroExpansionError(tree, msg) + reraise(HyMacroExpansionError, + (msg, (compiler.filename, tree, compiler.source)), + None) + tree = replace_hy_obj(obj, tree) if once: @@ -203,6 +208,7 @@ def macroexpand(tree, compiler, once=False): tree = wrap_value(tree) return tree + def macroexpand_1(tree, compiler): """Expand the toplevel macro from `tree` once, in the context of `compiler`.""" @@ -219,9 +225,8 @@ def tag_macroexpand(tag, tree, compiler): tag_macro = _hy_tag[None][tag] except KeyError: raise HyTypeError( - tag, - "`{0}' is not a defined tag macro.".format(tag) - ) + "`{0}' is not a defined tag macro.".format(tag), + (None, tag, None)) expr = tag_macro(tree) return replace_hy_obj(expr, tree) diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index cb3ab7bbb..a69a7a7e8 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -8,8 +8,8 @@ from hy import HyString from hy.models import HyObject from hy.importer import hy_compile, hy_eval, hy_parse -from hy.errors import HyCompileError, HyTypeError -from hy.lex.exceptions import LexException +from hy.errors import HyCompileError, HyTypeError, HyError +from hy.lex.exceptions import LexException, PrematureEndOfInput from hy._compat import PY3 import ast @@ -34,21 +34,22 @@ def can_eval(expr): def cant_compile(expr): - try: + with pytest.raises(HyError) as excinfo: hy_compile(hy_parse(expr), "__main__") - assert False - except HyTypeError as e: + + if issubclass(excinfo.type, HyTypeError): # 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: + assert isinstance(excinfo.value.expression, HyObject) + assert excinfo.value.message + 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.exception, HyTypeError) - assert e.traceback - return e + assert 'Internal Compiler Bug' in excinfo.value.message + return excinfo.value + else: + assert False def s(x): @@ -59,11 +60,9 @@ def test_ast_bad_type(): "Make sure AST breakage can happen" class C: pass - try: + + with pytest.raises(TypeError): hy_compile(C(), "__main__") - assert True is False - except TypeError: - pass def test_empty_expr(): @@ -473,7 +472,7 @@ def test_lambda_list_keywords_kwonly(): else: exception = cant_compile(kwonly_demo) assert isinstance(exception, HyTypeError) - message, = exception.args + message, args = exception.args assert message == "&kwonly parameters require Python 3" @@ -546,7 +545,7 @@ def test_compile_error(): def test_for_compile_error(): """Ensure we get compile error in tricky 'for' cases""" - with pytest.raises(LexException) as excinfo: + with pytest.raises(PrematureEndOfInput) as excinfo: can_compile("(fn [] (for)") assert excinfo.value.message == "Premature end of input" diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py index 900da0314..48515040b 100644 --- a/tests/importer/test_importer.py +++ b/tests/importer/test_importer.py @@ -17,7 +17,7 @@ import hy from hy._compat import bytes_type from hy.errors import HyTypeError -from hy.lex import LexException +from hy.lex.exceptions import PrematureEndOfInput from hy.compiler import hy_compile from hy.importer import hy_parse, HyLoader, cache_from_source @@ -186,14 +186,14 @@ def unlink(filename): assert mod.a == 11 assert mod.b == 20 - # Now cause a LexException + # Now cause a syntax error unlink(source) with open(source, "w") as f: f.write("(setv a 11") f.write("(setv b (// 20 1))") - with pytest.raises(LexException): + with pytest.raises(PrematureEndOfInput): imp.reload(mod) mod = sys.modules.get(TESTFN) diff --git a/tests/test_bin.py b/tests/test_bin.py index 58d144884..aaf75c137 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -149,7 +149,7 @@ def test_bin_hy_stdin_unlocatable_hytypeerror(): # inside run_cmd. _, err = run_cmd("hy", """ (import hy.errors) - (raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""") + (raise (hy.errors.HyTypeError (+ "A" "Z") (, None '[] None)))""") assert "AZ" in err diff --git a/tests/test_lex.py b/tests/test_lex.py index 411c246c1..cc0dbf298 100644 --- a/tests/test_lex.py +++ b/tests/test_lex.py @@ -1,12 +1,18 @@ # Copyright 2018 the authors. # This file is part of Hy, which is free software licensed under the Expat # license. See the LICENSE. +import sys +import traceback + +import pytest from math import isnan from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol, HyString, HyDict, HyList, HySet, HyKeyword) -from hy.lex import LexException, PrematureEndOfInput, tokenize -import pytest +from hy.lex import tokenize +from hy.lex.exceptions import LexException, PrematureEndOfInput +from hy.errors import hy_exc_handler + def peoi(): return pytest.raises(PrematureEndOfInput) def lexe(): return pytest.raises(LexException) @@ -29,8 +35,18 @@ def test_unbalanced_exception(): def test_lex_single_quote_err(): "Ensure tokenizing \"' \" throws a LexException that can be stringified" # https://github.com/hylang/hy/issues/1252 - with lexe() as e: tokenize("' ") - assert "Could not identify the next token" in str(e.value) + with lexe() as execinfo: + tokenize("' ") + + expected = [' File "", line -1\n', + " '\n", + ' ^\n', + 'LexException: Could not identify the next token.\n'] + output = traceback.format_exception_only(execinfo.type, execinfo.value) + + assert output[:-1:1] == expected[:-1:1] + # Python 2.7 doesn't give the full exception name, so we compensate. + assert output[-1].endswith(expected[-1]) def test_lex_expression_symbols(): @@ -73,7 +89,16 @@ def test_lex_strings_exception(): """ Make sure tokenize throws when codec can't decode some bytes""" with lexe() as execinfo: tokenize('\"\\x8\"') - assert "Can't convert \"\\x8\" to a HyString" in str(execinfo.value) + + expected = [' File "", line 1\n', + ' "\\x8"\n', + ' ^\n', + 'LexException: Can\'t convert "\\x8" to a HyString\n'] + output = traceback.format_exception_only(execinfo.type, execinfo.value) + + assert output[:-1:1] == expected[:-1:1] + # Python 2.7 doesn't give the full exception name, so we compensate. + assert output[-1].endswith(expected[-1]) def test_lex_bracket_strings(): @@ -179,13 +204,30 @@ def test_lex_digit_separators(): def test_lex_bad_attrs(): - with lexe(): tokenize("1.foo") + with lexe() as execinfo: + tokenize("1.foo") + + expected = [ + ' File "", line 1\n', + ' 1.foo\n', + ' ^\n', + ('LexException: Cannot access attribute on anything other' + ' than a name (in order to get attributes of expressions,' + ' use `(. )` or `(. )`)\n') + ] + output = traceback.format_exception_only(execinfo.type, execinfo.value) + + assert output[:-1:1] == expected[:-1:1] + # Python 2.7 doesn't give the full exception name, so we compensate. + assert output[-1].endswith(expected[-1]) + with lexe(): tokenize("0.foo") with lexe(): tokenize("1.5.foo") with lexe(): tokenize("1e3.foo") with lexe(): tokenize("5j.foo") with lexe(): tokenize("3+5j.foo") with lexe(): tokenize("3.1+5.1j.foo") + assert tokenize("j.foo") with lexe(): tokenize("3/4.foo") assert tokenize("a/1.foo") @@ -418,3 +460,51 @@ def test_discard(): assert tokenize("a '#_b c") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("c")])] assert tokenize("a '#_b #_c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])] assert tokenize("a '#_ #_b c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])] + + + +def test_lex_exception_filtering(capsys): + """Confirm that the exception filtering works for lexer errors.""" + + def _check_trace_output(execinfo, expected): + sys.__excepthook__(execinfo.type, execinfo.value, execinfo.tb) + captured_wo_filtering = capsys.readouterr()[-1].strip('\n') + + hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb) + captured_w_filtering = capsys.readouterr()[-1].strip('\n') + + output = captured_w_filtering.split('\n') + + # Make sure the filtered frames aren't the same as the unfiltered ones. + assert output[:-1:1] != captured_wo_filtering.split('\n')[:-1:1] + # Remove the origin frame lines. + assert output[3:-1:1] == expected[:-1:1] + # Python 2.7 doesn't give the full exception name, so we compensate. + assert output[-1].endswith(expected[-1]) + + + # First, test for PrematureEndOfInput + with peoi() as execinfo: + tokenize(" \n (foo") + + expected = [' File "", line 2', + ' (foo', + ' ^', + 'PrematureEndOfInput: Premature end of input'] + + _check_trace_output(execinfo, expected) + + # Now, for a generic LexException + with lexe() as execinfo: + tokenize(" \n\n 1.foo ") + + expected = [ + ' File "", line 3', + ' 1.foo', + ' ^', + ('LexException: Cannot access attribute on anything other' + ' than a name (in order to get attributes of expressions,' + ' use `(. )` or `(. )`)') + ] + + _check_trace_output(execinfo, expected)