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