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