Much better version of new error messages.

This version is much simpler.

At the point that the exception is raised, we don't have access to
the actual source, just the current expression. but as the
exception percolates up, we can intercept it, add the source and
the re-raise it.

Then at the final point, in the cmdline handler, we can choose to
let the entire traceback print, or just the simpler, direct error
message.

And even with the full traceback, the last bit is nicely formatted
just like the shorter, simpler message.

The error message is colored if clint is installed, but to avoid
yet another dependency, you get monochrome without clint.

I'm sure there is a better way to do the markup, the current method
is kludgy but works.

I wish there was more shared code between HyTypeError and LexException
but they are kind of different in some fundamental ways.

This doesn't work (yet) with runtime errors generated from Python,
like NameError, but I have a method that can catch NameError and turn it
into a more pleasing output.

Finally, there is no obvious way to raise HyTypeError from pure Hy code,
so methods in core/language.hy throw ugly TypeError/ValueError.
This commit is contained in:
Bob Tolbert 2013-12-22 12:56:03 -07:00
parent defbe22349
commit f6160c755a
9 changed files with 200 additions and 36 deletions

View File

@ -5,6 +5,7 @@
# Copyright (c) 2013 Konrad Hinsen <konrad.hinsen@fastmail.net> # Copyright (c) 2013 Konrad Hinsen <konrad.hinsen@fastmail.net>
# Copyright (c) 2013 Thom Neale <twneale@gmail.com> # Copyright (c) 2013 Thom Neale <twneale@gmail.com>
# Copyright (c) 2013 Will Kahn-Greene <willg@bluesock.org> # Copyright (c) 2013 Will Kahn-Greene <willg@bluesock.org>
# Copyright (c) 2013 Bob Tolbert <bob@tolbert.org>
# #
# Permission is hereby granted, free of charge, to any person obtaining a # Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"), # copy of this software and associated documentation files (the "Software"),
@ -32,7 +33,7 @@ import sys
import hy import hy
from hy.lex import LexException, PrematureEndOfInput, tokenize from hy.lex import LexException, PrematureEndOfInput, tokenize
from hy.compiler import hy_compile from hy.compiler import hy_compile, HyTypeError
from hy.importer import ast_compile, import_buffer_to_module from hy.importer import ast_compile, import_buffer_to_module
from hy.completer import completion from hy.completer import completion
@ -83,8 +84,11 @@ class HyREPL(code.InteractiveConsole):
tokens = tokenize(source) tokens = tokenize(source)
except PrematureEndOfInput: except PrematureEndOfInput:
return True return True
except LexException: except LexException as e:
self.showsyntaxerror(filename) if e.source is None:
e.source = source
e.filename = filename
print(e)
return False return False
try: try:
@ -92,6 +96,12 @@ class HyREPL(code.InteractiveConsole):
if self.spy: if self.spy:
print_python_code(_ast) print_python_code(_ast)
code = ast_compile(_ast, filename, symbol) code = ast_compile(_ast, filename, symbol)
except HyTypeError as e:
if e.source is None:
e.source = source
e.filename = filename
print(e)
return False
except Exception: except Exception:
self.showtraceback() self.showtraceback()
return False return False
@ -154,22 +164,34 @@ def ideas_macro():
require("hy.cmdline", "__console__") require("hy.cmdline", "__console__")
require("hy.cmdline", "__main__") require("hy.cmdline", "__main__")
SIMPLE_TRACEBACKS = True
def run_command(source): def run_command(source):
try: try:
import_buffer_to_module("__main__", source) import_buffer_to_module("__main__", source)
except LexException as exc: except (HyTypeError, LexException) as e:
# TODO: This would be better if we had line, col info. if SIMPLE_TRACEBACKS:
print(source) print(e)
print(repr(exc))
return 1 return 1
raise
except Exception:
raise
return 0 return 0
def run_file(filename): def run_file(filename):
from hy.importer import import_file_to_module from hy.importer import import_file_to_module
try:
import_file_to_module("__main__", filename) import_file_to_module("__main__", filename)
return 0 # right? except (HyTypeError, LexException) as e:
if SIMPLE_TRACEBACKS:
print(e)
return 1
raise
except Exception:
raise
return 0
def run_repl(hr=None, spy=False): def run_repl(hr=None, spy=False):
@ -218,6 +240,9 @@ def cmdline_handler(scriptname, argv):
parser.add_argument("-v", action="version", version=VERSION) parser.add_argument("-v", 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. # this will contain the script/program name and any arguments for it.
parser.add_argument('args', nargs=argparse.REMAINDER, parser.add_argument('args', nargs=argparse.REMAINDER,
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
@ -228,6 +253,10 @@ def cmdline_handler(scriptname, argv):
options = parser.parse_args(argv[1:]) options = parser.parse_args(argv[1:])
if options.show_tracebacks:
global SIMPLE_TRACEBACKS
SIMPLE_TRACEBACKS = False
# reset sys.argv like Python # reset sys.argv like Python
sys.argv = options.args or [""] sys.argv = options.args or [""]

View File

@ -4,6 +4,7 @@
# Copyright (c) 2013 Julien Danjou <julien@danjou.info> # Copyright (c) 2013 Julien Danjou <julien@danjou.info>
# Copyright (c) 2013 Nicolas Dandrimont <nicolas.dandrimont@crans.org> # Copyright (c) 2013 Nicolas Dandrimont <nicolas.dandrimont@crans.org>
# Copyright (c) 2013 James King <james@agentultra.com> # Copyright (c) 2013 James King <james@agentultra.com>
# Copyright (c) 2013 Bob Tolbert <bob@tolbert.org>
# #
# Permission is hereby granted, free of charge, to any person obtaining a # Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"), # copy of this software and associated documentation files (the "Software"),
@ -93,11 +94,52 @@ class HyTypeError(TypeError):
def __init__(self, expression, message): def __init__(self, expression, message):
super(HyTypeError, self).__init__(message) super(HyTypeError, self).__init__(message)
self.expression = expression self.expression = expression
self.message = message
self.source = None
self.filename = None
def __str__(self): def __str__(self):
return (super(HyTypeError, self).__str__() + " (line %s, column %d)" from hy.errors import colored
% (self.expression.start_line,
self.expression.start_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 = ""
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%s\n' % (' '*(start-1),
colored.green('^' + '-'*(length-1) + '^'))
if len(source) > 1:
result += ' %s\n' % colored.red(source[0])
result += ' %s%s\n' % (' '*(start-1),
colored.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))
# write the last line
result += ' %s\n' % colored.red("".join(source[-1]))
result += ' %s\n' % colored.green('-'*(end-1) + '^')
result += colored.yellow("HyTypeError: %s\n\n" % self.message)
return result
_compile_table = {} _compile_table = {}
@ -341,7 +383,7 @@ def checkargs(exact=None, min=None, max=None, even=None):
if min is not None and (len(expression) - 1) < min: if min is not None and (len(expression) - 1) < min:
_raise_wrong_args_number( _raise_wrong_args_number(
expression, expression,
"`%%s' needs at least %d arguments, got %%d" % (min)) "`%%s' needs at least %d arguments, got %%d." % (min))
if max is not None and (len(expression) - 1) > max: if max is not None and (len(expression) - 1) > max:
_raise_wrong_args_number( _raise_wrong_args_number(
@ -430,6 +472,8 @@ 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:
raise
except Exception as e: except Exception as e:
raise HyCompileError(e, sys.exc_info()[2]) raise HyCompileError(e, sys.exc_info()[2])
@ -1584,6 +1628,9 @@ class HyASTCompiler(object):
fn.replace(ofn) fn.replace(ofn)
# Get the object we want to take an attribute from # Get the object we want to take an attribute from
if len(expression) < 2:
raise HyTypeError(expression,
"attribute access requires object")
func = self.compile(expression.pop(1)) func = self.compile(expression.pop(1))
# And get the attribute # And get the attribute

View File

@ -1,4 +1,5 @@
# Copyright (c) 2013 Paul Tagliamonte <paultag@debian.org> # Copyright (c) 2013 Paul Tagliamonte <paultag@debian.org>
# Copyright (c) 2013 Bob Tolbert <bob@tolbert.org>
# #
# Permission is hereby granted, free of charge, to any person obtaining a # Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"), # copy of this software and associated documentation files (the "Software"),
@ -25,3 +26,41 @@ class HyError(Exception):
Exception. Exception.
""" """
pass pass
try:
from clint.textui import colored
except:
class colored:
@staticmethod
def black(foo):
return foo
@staticmethod
def red(foo):
return foo
@staticmethod
def green(foo):
return foo
@staticmethod
def yellow(foo):
return foo
@staticmethod
def blue(foo):
return foo
@staticmethod
def magenta(foo):
return foo
@staticmethod
def cyan(foo):
return foo
@staticmethod
def white(foo):
return foo

View File

@ -1,4 +1,5 @@
# Copyright (c) 2013 Paul Tagliamonte <paultag@debian.org> # Copyright (c) 2013 Paul Tagliamonte <paultag@debian.org>
# Copyright (c) 2013 Bob Tolbert <bob@tolbert.org>
# #
# Permission is hereby granted, free of charge, to any person obtaining a # Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"), # copy of this software and associated documentation files (the "Software"),
@ -19,10 +20,9 @@
# DEALINGS IN THE SOFTWARE. # DEALINGS IN THE SOFTWARE.
from py_compile import wr_long, MAGIC from py_compile import wr_long, MAGIC
from hy.compiler import hy_compile from hy.compiler import hy_compile, HyTypeError
from hy.models import HyObject from hy.models import HyObject
from hy.lex import tokenize from hy.lex import tokenize, LexException
from io import open from io import open
import marshal import marshal
@ -73,17 +73,29 @@ def import_file_to_module(module_name, fpath):
mod = imp.new_module(module_name) mod = imp.new_module(module_name)
mod.__file__ = fpath mod.__file__ = fpath
eval(ast_compile(_ast, fpath, "exec"), mod.__dict__) eval(ast_compile(_ast, fpath, "exec"), mod.__dict__)
except (HyTypeError, LexException) as e:
if e.source is None:
fp = open(fpath, 'rt')
e.source = fp.read()
fp.close()
e.filename = fpath
raise
except Exception: except Exception:
sys.modules.pop(module_name, None) sys.modules.pop(module_name, None)
raise raise
return mod return mod
def import_buffer_to_module(module_name, buf): def import_buffer_to_module(module_name, buf):
try:
_ast = import_buffer_to_ast(buf, module_name) _ast = import_buffer_to_ast(buf, module_name)
mod = imp.new_module(module_name) mod = imp.new_module(module_name)
eval(ast_compile(_ast, "", "exec"), mod.__dict__) eval(ast_compile(_ast, "", "exec"), mod.__dict__)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = buf
e.filename = '<stdin>'
raise
return mod return mod

View File

@ -33,6 +33,5 @@ def tokenize(buf):
return parser.parse(lexer.lex(buf)) return parser.parse(lexer.lex(buf))
except LexingError as e: except LexingError as e:
pos = e.getsourcepos() pos = e.getsourcepos()
raise LexException( raise LexException("Could not identify the next token.",
"Could not identify the next token at line %s, column %s" % ( pos.lineno, pos.colno)
pos.lineno, pos.colno))

View File

@ -1,4 +1,5 @@
# Copyright (c) 2013 Nicolas Dandrimont <nicolas.dandrimont@crans.org> # Copyright (c) 2013 Nicolas Dandrimont <nicolas.dandrimont@crans.org>
# Copyright (c) 2013 Bob Tolbert <bob@tolbert.org>
# #
# Permission is hereby granted, free of charge, to any person obtaining a # Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"), # copy of this software and associated documentation files (the "Software"),
@ -23,9 +24,43 @@ from hy.errors import HyError
class LexException(HyError): class LexException(HyError):
"""Error during the Lexing of a Hython expression.""" """Error during the Lexing of a Hython expression."""
pass def __init__(self, message, lineno, colno):
super(LexException, self).__init__(message)
self.message = message
self.lineno = lineno
self.colno = colno
self.source = None
self.filename = '<stdin>'
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): class PrematureEndOfInput(LexException):
"""We got a premature end of input""" """We got a premature end of input"""
pass def __init__(self, message):
super(PrematureEndOfInput, self).__init__(message, -1, -1)

View File

@ -258,12 +258,11 @@ def t_identifier(p):
def error_handler(token): def error_handler(token):
tokentype = token.gettokentype() tokentype = token.gettokentype()
if tokentype == '$end': if tokentype == '$end':
raise PrematureEndOfInput raise PrematureEndOfInput("Premature end of input")
else: else:
raise LexException( raise LexException(
"Ran into a %s where it wasn't expected at line %s, column %s" % "Ran into a %s where it wasn't expected." % tokentype,
(tokentype, token.source_pos.lineno, token.source_pos.colno) token.source_pos.lineno, token.source_pos.colno)
)
parser = pg.build() parser = pg.build()

View File

@ -22,6 +22,7 @@
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, HyCompileError, HyTypeError from hy.compiler import hy_compile, HyCompileError, HyTypeError
from hy.lex import tokenize from hy.lex import tokenize
@ -42,10 +43,14 @@ def can_compile(expr):
def cant_compile(expr): def cant_compile(expr):
expr = tokenize(expr)
try: try:
hy_compile(expr, "__main__") hy_compile(tokenize(expr), "__main__")
assert False assert False
except HyTypeError as e:
# 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
except HyCompileError as e: except HyCompileError as e:
# 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.
@ -422,8 +427,7 @@ def test_compile_error():
"""Ensure we get compile error in tricky cases""" """Ensure we get compile error in tricky cases"""
try: try:
can_compile("(fn [] (= 1))") can_compile("(fn [] (= 1))")
except HyCompileError as e: except HyTypeError as e:
assert(str(e) assert(e.message == "`=' needs at least 2 arguments, got 1.")
== "`=' needs at least 2 arguments, got 1 (line 1, column 8)")
else: else:
assert(False) assert(False)

View File

@ -64,7 +64,7 @@ def test_bin_hy_cmd():
ret = run_cmd("hy -c \"(koan\"") ret = run_cmd("hy -c \"(koan\"")
assert ret[0] == 1 assert ret[0] == 1
assert "PrematureEndOfInput" in ret[1] assert "Premature end of input" in ret[1]
def test_bin_hy_icmd(): def test_bin_hy_icmd():