diff --git a/NEWS.rst b/NEWS.rst
index 6f3d53cf0..20d45ef85 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -21,6 +21,7 @@ New Features
Bug Fixes
------------------------------
+* Cleaned up syntax and compiler errors
* Fixed issue with empty arguments in `defmain`.
* `require` now compiles to Python AST.
* Fixed circular `require`s.
diff --git a/docs/language/cli.rst b/docs/language/cli.rst
index 2c8a1f7fb..e59640d23 100644
--- a/docs/language/cli.rst
+++ b/docs/language/cli.rst
@@ -48,12 +48,6 @@ Command Line Options
`--spy` only works on REPL mode.
.. versionadded:: 0.9.11
-.. cmdoption:: --show-tracebacks
-
- Print extended tracebacks for Hy exceptions.
-
- .. versionadded:: 0.9.12
-
.. cmdoption:: --repl-output-fn
Format REPL output using specific function (e.g., hy.contrib.hy-repr.hy-repr)
diff --git a/hy/__init__.py b/hy/__init__.py
index f188b6419..eb1d91c1d 100644
--- a/hy/__init__.py
+++ b/hy/__init__.py
@@ -5,6 +5,16 @@
__version__ = 'unknown'
+def _initialize_env_var(env_var, default_val):
+ import os, distutils.util
+ try:
+ res = bool(distutils.util.strtobool(
+ os.environ.get(env_var, str(default_val))))
+ except ValueError as e:
+ res = default_val
+ return res
+
+
from hy.models import HyExpression, HyInteger, HyKeyword, HyComplex, HyString, HyBytes, HySymbol, HyFloat, HyDict, HyList, HySet # NOQA
diff --git a/hy/_compat.py b/hy/_compat.py
index bd9390f85..92aa39230 100644
--- a/hy/_compat.py
+++ b/hy/_compat.py
@@ -6,7 +6,7 @@
import __builtin__ as builtins
except ImportError:
import builtins # NOQA
-import sys, keyword
+import sys, keyword, textwrap
PY3 = sys.version_info[0] >= 3
PY35 = sys.version_info >= (3, 5)
@@ -22,11 +22,60 @@
long_type = int if PY3 else long # NOQA
string_types = str if PY3 else basestring # NOQA
+#
+# Inspired by the same-named `six` functions.
+#
if PY3:
- exec('def raise_empty(t, *args): raise t(*args) from None')
+ raise_src = textwrap.dedent('''
+ def raise_from(value, from_value):
+ raise value from from_value
+ ''')
+
+ def reraise(exc_type, value, traceback=None):
+ try:
+ raise value.with_traceback(traceback)
+ finally:
+ traceback = None
+
+ code_obj_args = ['argcount', 'kwonlyargcount', 'nlocals', 'stacksize',
+ 'flags', 'code', 'consts', 'names', 'varnames',
+ 'filename', 'name', 'firstlineno', 'lnotab', 'freevars',
+ 'cellvars']
else:
- def raise_empty(t, *args):
- raise t(*args)
+ def raise_from(value, from_value=None):
+ raise value
+
+ raise_src = textwrap.dedent('''
+ def reraise(exc_type, value, traceback=None):
+ try:
+ raise exc_type, value, traceback
+ finally:
+ 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'):
diff --git a/hy/cmdline.py b/hy/cmdline.py
index 4c2e1c335..f65379d50 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -12,16 +12,25 @@
import io
import importlib
import py_compile
+import traceback
import runpy
import types
+import time
+import linecache
+import hashlib
+import codeop
import astor.code_gen
import hy
+
from hy.lex import hy_parse, mangle
-from hy.lex.exceptions import LexException, PrematureEndOfInput
-from hy.compiler import HyASTCompiler, hy_compile, hy_eval
-from hy.errors import HyTypeError
+from contextlib import contextmanager
+from hy.lex.exceptions import PrematureEndOfInput
+from hy.compiler import (HyASTCompiler, hy_eval, hy_compile,
+ hy_ast_compile_flags)
+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
@@ -29,6 +38,11 @@
from hy._compat import builtins, PY3, FileNotFoundError
+sys.last_type = None
+sys.last_value = None
+sys.last_traceback = None
+
+
class HyQuitter(object):
def __init__(self, name):
self.name = name
@@ -49,30 +63,188 @@ def __call__(self, code=None):
builtins.quit = HyQuitter('quit')
builtins.exit = HyQuitter('exit')
+@contextmanager
+def extend_linecache(add_cmdline_cache):
+ _linecache_checkcache = linecache.checkcache
+
+ def _cmdline_checkcache(*args):
+ _linecache_checkcache(*args)
+ linecache.cache.update(add_cmdline_cache)
+
+ linecache.checkcache = _cmdline_checkcache
+ yield
+ linecache.checkcache = _linecache_checkcache
+
+
+_codeop_maybe_compile = codeop._maybe_compile
+
+
+def _hy_maybe_compile(compiler, source, filename, symbol):
+ """The `codeop` version of this will compile the same source multiple
+ times, and, since we have macros and things like `eval-and-compile`, we
+ can't allow that.
+ """
+ if not isinstance(compiler, HyCompile):
+ return _codeop_maybe_compile(compiler, source, filename, symbol)
+
+ for line in source.split("\n"):
+ line = line.strip()
+ if line and line[0] != ';':
+ # Leave it alone (could do more with Hy syntax)
+ break
+ else:
+ if symbol != "eval":
+ # Replace it with a 'pass' statement (i.e. tell the compiler to do
+ # nothing)
+ source = "pass"
+
+ return compiler(source, filename, symbol)
+
+
+codeop._maybe_compile = _hy_maybe_compile
+
+
+class HyCompile(codeop.Compile, object):
+ """This compiler uses `linecache` like
+ `IPython.core.compilerop.CachingCompiler`.
+ """
+
+ def __init__(self, module, locals, ast_callback=None,
+ hy_compiler=None, cmdline_cache={}):
+ self.module = module
+ self.locals = locals
+ self.ast_callback = ast_callback
+ self.hy_compiler = hy_compiler
+
+ super(HyCompile, self).__init__()
+
+ self.flags |= hy_ast_compile_flags
+
+ self.cmdline_cache = cmdline_cache
+
+ def _cache(self, source, name):
+ entry = (len(source),
+ time.time(),
+ [line + '\n' for line in source.splitlines()],
+ name)
+
+ linecache.cache[name] = entry
+ self.cmdline_cache[name] = entry
+
+ def _update_exc_info(self):
+ self.locals['_hy_last_type'] = sys.last_type
+ self.locals['_hy_last_value'] = sys.last_value
+ # Skip our frame.
+ sys.last_traceback = getattr(sys.last_traceback, 'tb_next',
+ sys.last_traceback)
+ self.locals['_hy_last_traceback'] = sys.last_traceback
+
+ def __call__(self, source, filename="", symbol="single"):
+
+ if source == 'pass':
+ # We need to return a no-op to signal that no more input is needed.
+ return (compile(source, filename, symbol),) * 2
+
+ hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest()
+ name = '{}-{}'.format(filename.strip('<>'), hash_digest)
+
+ try:
+ hy_ast = hy_parse(source, filename=name)
+ except Exception:
+ # Capture a traceback without the compiler/REPL frames.
+ sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+ self._update_exc_info()
+ raise
+
+ self._cache(source, name)
+
+ 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 = super(HyCompile, self).__call__(exec_ast, name, symbol)
+ eval_code = super(HyCompile, self).__call__(eval_ast, name, 'eval')
+
+ except HyLanguageError:
+ # Hy will raise exceptions during compile-time that Python would
+ # raise during run-time (e.g. import errors for `require`). In
+ # order to work gracefully with the Python world, we convert such
+ # Hy errors to code that purposefully reraises those exceptions in
+ # the places where Python code expects them.
+ sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+ self._update_exc_info()
+ exec_code = super(HyCompile, self).__call__(
+ 'import hy._compat; hy._compat.reraise('
+ '_hy_last_type, _hy_last_value, _hy_last_traceback)',
+ name, symbol)
+ eval_code = super(HyCompile, self).__call__('None', name, 'eval')
+
+ return exec_code, eval_code
+
+
+class HyCommandCompiler(codeop.CommandCompiler, object):
+ def __init__(self, *args, **kwargs):
+ self.compiler = HyCompile(*args, **kwargs)
+
+ def __call__(self, *args, **kwargs):
+ try:
+ return super(HyCommandCompiler, self).__call__(*args, **kwargs)
+ except PrematureEndOfInput:
+ # We have to do this here, because `codeop._maybe_compile` won't
+ # take `None` for a return value (at least not in Python 2.7) and
+ # this exception type is also a `SyntaxError`, so it will be caught
+ # by `code.InteractiveConsole` base methods before it reaches our
+ # `runsource`.
+ return None
+
class HyREPL(code.InteractiveConsole, object):
def __init__(self, spy=False, output_fn=None, locals=None,
- filename=""):
+ filename=""):
+ # Create a proper module for this REPL so that we can obtain it easily
+ # (e.g. using `importlib.import_module`).
+ # We let `InteractiveConsole` initialize `self.locals` when it's
+ # `None`.
super(HyREPL, self).__init__(locals=locals,
filename=filename)
- # 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.
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.cmdline_cache = {}
+ self.compile = HyCommandCompiler(self.module,
+ self.locals,
+ ast_callback=self.ast_callback,
+ hy_compiler=self.hy_compiler,
+ cmdline_cache=self.cmdline_cache)
+
self.spy = spy
+ self.last_value = None
+ self.print_last_value = True
if output_fn is None:
self.output_fn = repr
@@ -90,64 +262,95 @@ def __init__(self, spy=False, output_fn=None, locals=None,
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols})
- def runsource(self, source, filename='', symbol='single'):
- global SIMPLE_TRACEBACKS
+ # Allow access to the running REPL instance
+ self.locals['_hy_repl'] = self
- def error_handler(e, use_simple_traceback=False):
- self.locals[mangle("*e")] = e
- if use_simple_traceback:
- print(e, file=sys.stderr)
- else:
- self.showtraceback()
+ def 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(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, exc_info_override=False, *args, **kwargs):
+ sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+
+ if exc_info_override:
+ # Use a traceback that doesn't have the REPL frames.
+ sys.last_type = self.locals.get('_hy_last_type', sys.last_type)
+ sys.last_value = self.locals.get('_hy_last_value', sys.last_value)
+ sys.last_traceback = self.locals.get('_hy_last_traceback',
+ sys.last_traceback)
+
+ # Sadly, this method in Python 2.7 ignores an overridden `sys.excepthook`.
+ if sys.excepthook is sys.__excepthook__:
+ error_fn(*args, **kwargs)
+ else:
+ sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback)
+
+ self.locals[mangle("*e")] = sys.last_value
+
+ def showsyntaxerror(self, filename=None):
+ if filename is None:
+ filename = self.filename
+
+ self._error_wrap(super(HyREPL, self).showsyntaxerror,
+ exc_info_override=True,
+ filename=filename)
+ def showtraceback(self):
+ self._error_wrap(super(HyREPL, self).showtraceback)
+
+ def runcode(self, code):
try:
- try:
- do = hy_parse(source)
- except PrematureEndOfInput:
- return True
- except LexException as e:
- if e.source is None:
- e.source = source
- e.filename = filename
- error_handler(e, use_simple_traceback=True)
- return False
+ 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='', symbol='exec'):
try:
- def ast_callback(main_ast, expr_ast):
- if self.spy:
- # 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)])
- print(astor.to_source(new_ast))
-
- value = hy_eval(do, self.locals,
- ast_callback=ast_callback,
- compiler=self.hy_compiler)
- except HyTypeError as e:
- if e.source is None:
- e.source = source
- e.filename = filename
- error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
+ 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 Exception as e:
- error_handler(e)
+ 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
+ # Shift exisitng REPL results
+ 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.
- try:
- output = self.output_fn(value)
- except Exception as e:
- error_handler(e)
- return False
- print(output)
- return False
+ if self.print_last_value:
+ try:
+ output = self.output_fn(self.last_value)
+ except Exception:
+ self.showtraceback()
+ return False
+
+ print(output)
+
+ return res
@macro("koan")
@@ -202,23 +405,17 @@ def ideas_macro(ETname):
""")])
-SIMPLE_TRACEBACKS = True
-
-
-def pretty_error(func, *args, **kw):
+def run_command(source, filename=None):
+ __main__ = importlib.import_module('__main__')
+ require("hy.cmdline", __main__, assignments="ALL")
try:
- return func(*args, **kw)
- except (HyTypeError, LexException) as e:
- if SIMPLE_TRACEBACKS:
- print(e, file=sys.stderr)
- sys.exit(1)
- raise
-
-
-def run_command(source):
- tree = hy_parse(source)
- require("hy.cmdline", "__main__", assignments="ALL")
- pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
+ 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
@@ -231,9 +428,9 @@ def run_repl(hr=None, **kwargs):
hr = HyREPL(**kwargs)
namespace = hr.locals
-
- with completion(Completer(namespace)):
-
+ with filtered_hy_exceptions(), \
+ extend_linecache(hr.cmdline_cache), \
+ completion(Completer(namespace)):
hr.interact("{appname} {version} using "
"{py}({build}) {pyversion} on {os}".format(
appname=hy.__appname__,
@@ -260,10 +457,17 @@ def run_icommand(source, **kwargs):
source = f.read()
filename = source
else:
- filename = ''
+ filename = ''
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)
@@ -300,9 +504,6 @@ def cmdline_handler(scriptname, argv):
"(e.g., hy.contrib.hy-repr.hy-repr)")
parser.add_argument("-v", "--version", action="version", version=VERSION)
- parser.add_argument("--show-tracebacks", action="store_true",
- help="show complete tracebacks for Hy exceptions")
-
# this will contain the script/program name and any arguments for it.
parser.add_argument('args', nargs=argparse.REMAINDER,
help=argparse.SUPPRESS)
@@ -327,10 +528,6 @@ def cmdline_handler(scriptname, argv):
options = parser.parse_args(argv[1:])
- if options.show_tracebacks:
- global SIMPLE_TRACEBACKS
- SIMPLE_TRACEBACKS = False
-
if options.E:
# User did "hy -E ..."
_remove_python_envs()
@@ -340,7 +537,7 @@ def cmdline_handler(scriptname, argv):
if options.command:
# User did "hy -c ..."
- return run_command(options.command)
+ return run_command(options.command, filename='')
if options.mod:
# User did "hy -m ..."
@@ -356,7 +553,7 @@ def cmdline_handler(scriptname, argv):
if options.args:
if options.args[0] == "-":
# Read the program from stdin
- return run_command(sys.stdin.read())
+ return run_command(sys.stdin.read(), filename='')
else:
# User did "hy "
@@ -371,12 +568,16 @@ def cmdline_handler(scriptname, argv):
try:
sys.argv = options.args
- runhy.run_path(filename, run_name='__main__')
+ with filtered_hy_exceptions():
+ runhy.run_path(filename, run_name='__main__')
return 0
except FileNotFoundError as e:
print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
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)
@@ -446,12 +647,16 @@ def hy2py_main():
options = parser.parse_args(sys.argv[1:])
if options.FILE is None or options.FILE == '-':
+ filename = ''
source = sys.stdin.read()
else:
+ filename = options.FILE
with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read()
- hst = pretty_error(hy_parse, source)
+ with filtered_hy_exceptions():
+ hst = hy_parse(source, filename=filename)
+
if options.with_source:
# need special printing on Windows in case the
# codepage doesn't support utf-8 characters
@@ -466,7 +671,9 @@ def hy2py_main():
print()
print()
- _ast = pretty_error(hy_compile, hst, '__main__')
+ with filtered_hy_exceptions():
+ _ast = hy_compile(hst, '__main__', filename=filename, source=source)
+
if options.with_ast:
if PY3 and platform.system() == "Windows":
_print_for_windows(astor.dump_tree(_ast))
diff --git a/hy/compiler.py b/hy/compiler.py
index f3b55a3fa..08e0c98a1 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -9,20 +9,21 @@
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
+from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
+ HySyntaxError, HyEvalError, HyInternalError)
from hy.lex import mangle, unmangle
-from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
- PY35, raise_empty)
+from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
+ PY35, reraise)
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
import hy.core
+import pkgutil
import traceback
import importlib
import inspect
-import pkgutil
import types
import ast
import sys
@@ -340,22 +341,33 @@ def is_unpack(kind, x):
class HyASTCompiler(object):
"""A Hy-to-Python AST compiler"""
- def __init__(self, module):
+ def __init__(self, module, filename=None, source=None):
"""
Parameters
----------
module: str or types.ModuleType
- Module in which the Hy tree is evaluated.
+ Module name or object in which the Hy tree is evaluated.
+ filename: str, optional
+ The name of the file for the source to be compiled.
+ This is optional information for informative error messages and
+ debugging.
+ source: str, optional
+ The source for the file, if any, being compiled. This is optional
+ information for informative error messages and debugging.
"""
self.anon_var_count = 0
self.imports = defaultdict(set)
self.temp_if = None
if not inspect.ismodule(module):
- module = importlib.import_module(module)
+ self.module = importlib.import_module(module)
+ else:
+ self.module = module
- self.module = module
- self.module_name = module.__name__
+ self.module_name = self.module.__name__
+
+ self.filename = filename
+ self.source = source
# Hy expects these to be present, so we prep the module for Hy
# compilation.
@@ -431,10 +443,18 @@ def compile(self, tree):
# nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError!
raise
- except HyTypeError:
- raise
+ except 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:
- raise_empty(HyCompileError, e, sys.exc_info()[2])
+ # 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 HySyntaxError(message, expr, self.filename, self.source)
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False):
@@ -455,8 +475,8 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
if not PY35 and oldpy_unpack and is_unpack("iterable", expr):
if oldpy_starargs:
- raise HyTypeError(expr, "Pythons < 3.5 allow only one "
- "`unpack-iterable` per call")
+ raise self._syntax_error(expr,
+ "Pythons < 3.5 allow only one `unpack-iterable` per call")
oldpy_starargs = self.compile(expr[1])
ret += oldpy_starargs
oldpy_starargs = oldpy_starargs.force_expr
@@ -472,21 +492,20 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
expr, arg=None, value=ret.force_expr))
elif oldpy_unpack:
if oldpy_kwargs:
- raise HyTypeError(expr, "Pythons < 3.5 allow only one "
- "`unpack-mapping` per call")
+ raise self._syntax_error(expr,
+ "Pythons < 3.5 allow only one `unpack-mapping` per call")
oldpy_kwargs = ret.force_expr
elif with_kwargs and isinstance(expr, HyKeyword):
try:
value = next(exprs_iter)
except StopIteration:
- raise HyTypeError(expr,
- "Keyword argument {kw} needs "
- "a value.".format(kw=expr))
+ raise self._syntax_error(expr,
+ "Keyword argument {kw} needs a value.".format(kw=expr))
if not expr:
- raise HyTypeError(expr, "Can't call a function with the "
- "empty keyword")
+ raise self._syntax_error(expr,
+ "Can't call a function with the empty keyword")
compiled_value = self.compile(value)
ret += compiled_value
@@ -527,8 +546,8 @@ def _storeize(self, expr, name, func=None):
if isinstance(name, Result):
if not name.is_expr():
- raise HyTypeError(expr,
- "Can't assign or delete a non-expression")
+ raise self._syntax_error(expr,
+ "Can't assign or delete a non-expression")
name = name.expr
if isinstance(name, (ast.Tuple, ast.List)):
@@ -547,9 +566,8 @@ def _storeize(self, expr, name, func=None):
new_name = ast.Starred(
value=self._storeize(expr, name.value, func))
else:
- raise HyTypeError(expr,
- "Can't assign or delete a %s" %
- type(expr).__name__)
+ raise self._syntax_error(expr,
+ "Can't assign or delete a %s" % type(expr).__name__)
new_name.ctx = func()
ast.copy_location(new_name, name)
@@ -575,9 +593,8 @@ def _render_quoted_form(self, form, level):
op = unmangle(ast_str(form[0]))
if level == 0 and op in ("unquote", "unquote-splice"):
if len(form) != 2:
- raise HyTypeError(form,
- ("`%s' needs 1 argument, got %s" %
- op, len(form) - 1))
+ raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1,
+ self.filename, form, self.source)
return set(), form[1], op == "unquote-splice"
elif op == "quasiquote":
level += 1
@@ -629,7 +646,8 @@ def compile_quote(self, expr, root, arg):
@special("unpack-iterable", [FORM])
def compile_unpack_iterable(self, expr, root, arg):
if not PY3:
- raise HyTypeError(expr, "`unpack-iterable` isn't allowed here")
+ raise self._syntax_error(expr,
+ "`unpack-iterable` isn't allowed here")
ret = self.compile(arg)
ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
return ret
@@ -659,7 +677,8 @@ def compile_raise_expression(self, expr, root, exc, cause):
if cause is not None:
if not PY3:
- raise HyTypeError(expr, "raise from only supported in python 3")
+ raise self._syntax_error(expr,
+ "raise from only supported in python 3")
cause = self.compile(cause)
ret += cause
cause = cause.force_expr
@@ -706,13 +725,11 @@ def compile_try_expression(self, expr, root, body, catchers, orelse, finalbody):
# Using (else) without (except) is verboten!
if orelse and not handlers:
- raise HyTypeError(
- expr,
+ raise self._syntax_error(expr,
"`try' cannot have `else' without `except'")
# Likewise a bare (try) or (try BODY).
if not (handlers or finalbody):
- raise HyTypeError(
- expr,
+ raise self._syntax_error(expr,
"`try' must have an `except' or `finally' clause")
returnable = Result(
@@ -963,7 +980,8 @@ def c(e):
def compile_decorate_expression(self, expr, name, args):
decs, fn = args[:-1], self.compile(args[-1])
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
- raise HyTypeError(args[-1], "Decorated a non-function")
+ raise self._syntax_error(args[-1],
+ "Decorated a non-function")
decs, ret, _ = self._compile_collect(decs)
fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
return ret + fn
@@ -1194,8 +1212,8 @@ def compile_import_or_require(self, expr, root, entries):
if (HySymbol('*'), None) in kids:
if len(kids) != 1:
star = kids[kids.index((HySymbol('*'), None))][0]
- raise HyTypeError(star, "* in an import name list "
- "must be on its own")
+ raise self._syntax_error(star,
+ "* in an import name list must be on its own")
else:
assignments = [(k, v or k) for k, v in kids]
@@ -1390,15 +1408,15 @@ def _compile_assign(self, name, result):
if str_name in (["None"] + (["True", "False"] if PY3 else [])):
# Python 2 allows assigning to True and False, although
# this is rarely wise.
- raise HyTypeError(name,
- "Can't assign to `%s'" % str_name)
+ raise self._syntax_error(name,
+ "Can't assign to `%s'" % str_name)
result = self.compile(result)
ld_name = self.compile(name)
if isinstance(ld_name.expr, ast.Call):
- raise HyTypeError(name,
- "Can't assign to a callable: `%s'" % str_name)
+ raise self._syntax_error(name,
+ "Can't assign to a callable: `%s'" % str_name)
if (result.temp_variables
and isinstance(name, HySymbol)
@@ -1474,7 +1492,8 @@ def compile_function_def(self, expr, root, params, body):
mandatory, optional, rest, kwonly, kwargs = params
optional, defaults, ret = self._parse_optional_args(optional)
if kwonly is not None and not PY3:
- raise HyTypeError(params, "&kwonly parameters require Python 3")
+ raise self._syntax_error(params,
+ "&kwonly parameters require Python 3")
kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True)
ret += ret2
main_args = mandatory + optional
@@ -1612,7 +1631,29 @@ def compile_dispatch_tag_macro(self, expr, root, tag, arg):
def compile_eval_and_compile(self, expr, root, body):
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
- hy_eval(new_expr + body, self.module.__dict__, self.module)
+ try:
+ hy_eval(new_expr + body,
+ self.module.__dict__,
+ self.module,
+ filename=self.filename,
+ source=self.source)
+ except HyInternalError:
+ # Unexpected "meta" compilation errors need to be treated
+ # like normal (unexpected) compilation errors at this level
+ # (or the compilation level preceding this one).
+ raise
+ except Exception as e:
+ # These could be expected Hy language errors (e.g. syntax errors)
+ # or regular Python runtime errors that do not signify errors in
+ # the compilation *process* (although compilation did technically
+ # fail).
+ # We wrap these exceptions and pass them through.
+ reraise(HyEvalError,
+ HyEvalError(str(e),
+ self.filename,
+ body,
+ self.source),
+ sys.exc_info()[2])
return (self._compile_branch(body)
if ast_str(root) == "eval_and_compile"
@@ -1627,8 +1668,8 @@ def compile_expression(self, expr):
return self.compile(expr)
if not expr:
- raise HyTypeError(
- expr, "empty expressions are not allowed at top level")
+ raise self._syntax_error(expr,
+ "empty expressions are not allowed at top level")
args = list(expr)
root = args.pop(0)
@@ -1646,8 +1687,7 @@ def compile_expression(self, expr):
sroot in (mangle(","), mangle(".")) or
not any(is_unpack("iterable", x) for x in args)):
if sroot in _bad_roots:
- raise HyTypeError(
- expr,
+ raise self._syntax_error(expr,
"The special form '{}' is not allowed here".format(root))
# `sroot` is a special operator. Get the build method and
# pattern-match the arguments.
@@ -1655,11 +1695,10 @@ def compile_expression(self, expr):
try:
parse_tree = pattern.parse(args)
except NoParseError as e:
- raise HyTypeError(
+ raise self._syntax_error(
expr[min(e.state.pos + 1, len(expr) - 1)],
"parse error for special form '{}': {}".format(
- root,
- e.msg.replace("", "end of form")))
+ root, e.msg.replace("", "end of form")))
return Result() + build_method(
self, expr, unmangle(sroot), *parse_tree)
@@ -1681,13 +1720,13 @@ def compile_expression(self, expr):
FORM +
many(FORM)).parse(args)
except NoParseError:
- raise HyTypeError(
- expr, "attribute access requires object")
+ raise self._syntax_error(expr,
+ "attribute access requires object")
# Reconstruct `args` to exclude `obj`.
args = [x for p in kws for x in p] + list(rest)
if is_unpack("iterable", obj):
- raise HyTypeError(
- obj, "can't call a method on an unpacking form")
+ raise self._syntax_error(obj,
+ "can't call a method on an unpacking form")
func = self.compile(HyExpression(
[HySymbol(".").replace(root), obj] +
attrs))
@@ -1725,16 +1764,12 @@ def compile_symbol(self, symbol):
glob, local = symbol.rsplit(".", 1)
if not glob:
- raise HyTypeError(symbol, 'cannot access attribute on '
- 'anything other than a name '
- '(in order to get attributes of '
- 'expressions, use '
- '`(. {attr})` or '
- '`(.{attr} )`)'.format(
- attr=local))
+ raise self._syntax_error(symbol,
+ 'cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. {attr})` or `(.{attr} )`)'.format(attr=local))
if not local:
- raise HyTypeError(symbol, 'cannot access empty attribute')
+ raise self._syntax_error(symbol,
+ 'cannot access empty attribute')
glob = HySymbol(glob).replace(symbol)
ret = self.compile_symbol(glob)
@@ -1802,8 +1837,13 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False):
def hy_eval(hytree, locals=None, module=None, ast_callback=None,
- compiler=None):
+ compiler=None, filename=None, source=None):
"""Evaluates a quoted expression and returns the value.
+
+ If you're evaluating hand-crafted AST trees, make sure the line numbers
+ are set properly. Try `fix_missing_locations` and related functions in the
+ Python `ast` library.
+
Examples
--------
=> (eval '(print "Hello World"))
@@ -1816,8 +1856,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
Parameters
----------
- hytree: a Hy expression tree
- Source code to parse.
+ hytree: HyObject
+ The Hy AST object to evaluate.
locals: dict, optional
Local environment in which to evaluate the Hy tree. Defaults to the
@@ -1839,6 +1879,19 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
An existing Hy compiler to use for compilation. Also serves as
the `module` value when given.
+ filename: str, optional
+ The filename corresponding to the source for `tree`. This will be
+ overridden by the `filename` field of `tree`, if any; otherwise, it
+ defaults to "". When `compiler` is given, its `filename` field
+ value is always used.
+
+ source: str, optional
+ A string containing the source code for `tree`. This will be
+ overridden by the `source` field of `tree`, if any; otherwise,
+ if `None`, an attempt will be made to obtain it from the module given by
+ `module`. When `compiler` is given, its `source` field value is always
+ used.
+
Returns
-------
out : Result of evaluating the Hy compiled tree.
@@ -1853,36 +1906,53 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
if not isinstance(locals, dict):
raise TypeError("Locals must be a dictionary")
- _ast, expr = hy_compile(hytree, module=module, get_expr=True,
- compiler=compiler)
-
- # Spoof the positions in the generated ast...
- for node in ast.walk(_ast):
- node.lineno = 1
- node.col_offset = 1
+ # Does the Hy AST object come with its own information?
+ filename = getattr(hytree, 'filename', filename) or ''
+ source = getattr(hytree, 'source', source)
- for node in ast.walk(expr):
- node.lineno = 1
- node.col_offset = 1
+ _ast, expr = hy_compile(hytree, module, get_expr=True,
+ compiler=compiler, filename=filename,
+ source=source)
if ast_callback:
ast_callback(_ast, expr)
- globals = module.__dict__
-
# Two-step eval: eval() the body of the exec call
- eval(ast_compile(_ast, "", "exec"), globals, locals)
+ eval(ast_compile(_ast, filename, "exec"),
+ module.__dict__, locals)
# Then eval the expression context and return that
- return eval(ast_compile(expr, "", "eval"), globals, locals)
+ return eval(ast_compile(expr, filename, "eval"),
+ module.__dict__, locals)
-def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
- compiler=None):
- """Compile a Hy tree into a Python AST tree.
+def _module_file_source(module_name, filename, source):
+ """Try to obtain missing filename and source information from a module name
+ without actually loading the module.
+ """
+ if filename is None or source is None:
+ mod_loader = pkgutil.get_loader(module_name)
+ if mod_loader:
+ if filename is None:
+ filename = mod_loader.get_filename(module_name)
+ if source is None:
+ source = mod_loader.get_source(module_name)
+
+ # We need a non-None filename.
+ filename = filename or ''
+
+ return filename, source
+
+
+def hy_compile(tree, module, root=ast.Module, get_expr=False,
+ compiler=None, filename=None, source=None):
+ """Compile a HyObject tree into a Python AST Module.
Parameters
----------
+ tree: HyObject
+ The Hy AST object to compile.
+
module: str or types.ModuleType, optional
Module, or name of the module, in which the Hy tree is evaluated.
The module associated with `compiler` takes priority over this value.
@@ -1897,18 +1967,43 @@ def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
An existing Hy compiler to use for compilation. Also serves as
the `module` value when given.
+ filename: str, optional
+ The filename corresponding to the source for `tree`. This will be
+ overridden by the `filename` field of `tree`, if any; otherwise, it
+ defaults to "". When `compiler` is given, its `filename` field
+ value is always used.
+
+ source: str, optional
+ A string containing the source code for `tree`. This will be
+ overridden by the `source` field of `tree`, if any; otherwise,
+ if `None`, an attempt will be made to obtain it from the module given by
+ `module`. When `compiler` is given, its `source` field value is always
+ used.
+
Returns
-------
out : A Python AST tree
"""
module = get_compiler_module(module, compiler, False)
+ if isinstance(module, string_types):
+ if module.startswith('<') and module.endswith('>'):
+ module = types.ModuleType(module)
+ else:
+ module = importlib.import_module(ast_str(module, piecewise=True))
+
+ if not inspect.ismodule(module):
+ raise TypeError('Invalid module type: {}'.format(type(module)))
+
+ filename = getattr(tree, 'filename', filename)
+ source = getattr(tree, 'source', source)
+
tree = wrap_value(tree)
if not isinstance(tree, HyObject):
- raise HyCompileError("`tree` must be a HyObject or capable of "
- "being promoted to one")
+ raise TypeError("`tree` must be a HyObject or capable of "
+ "being promoted to one")
- compiler = compiler or HyASTCompiler(module)
+ compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
result = compiler.compile(tree)
expr = result.force_expr
diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy
index fa613a601..0d3d737c5 100644
--- a/hy/core/bootstrap.hy
+++ b/hy/core/bootstrap.hy
@@ -14,15 +14,14 @@
(if* (not (isinstance macro-name hy.models.HySymbol))
(raise
(hy.errors.HyTypeError
- macro-name
(% "received a `%s' instead of a symbol for macro name"
- (. (type name)
- __name__)))))
+ (. (type name) __name__))
+ None --file-- None)))
(for [kw '[&kwonly &kwargs]]
(if* (in kw lambda-list)
- (raise (hy.errors.HyTypeError macro-name
- (% "macros cannot use %s"
- kw)))))
+ (raise (hy.errors.HyTypeError (% "macros cannot use %s"
+ kw)
+ macro-name --file-- None))))
;; this looks familiar...
`(eval-and-compile
(import hy)
@@ -45,12 +44,12 @@
(if (and (not (isinstance tag-name hy.models.HySymbol))
(not (isinstance tag-name hy.models.HyString)))
(raise (hy.errors.HyTypeError
- tag-name
(% "received a `%s' instead of a symbol for tag macro name"
- (. (type tag-name) __name__)))))
+ (. (type tag-name) --name--))
+ 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
@@ -58,9 +57,8 @@
((hy.macros.tag ~tag-name)
(fn ~lambda-list ~@body))))
-(defmacro macro-error [location reason]
- "Error out properly within a macro at `location` giving `reason`."
- `(raise (hy.errors.HyMacroExpansionError ~location ~reason)))
+(defmacro macro-error [expression reason &optional [filename '--name--]]
+ `(raise (hy.errors.HyMacroExpansionError ~reason ~filename ~expression None)))
(defmacro defn [name lambda-list &rest body]
"Define `name` as a function with `lambda-list` signature and body `body`."
diff --git a/hy/errors.py b/hy/errors.py
index 7d36ab2e1..0b7619e3d 100644
--- a/hy/errors.py
+++ b/hy/errors.py
@@ -2,103 +2,307 @@
# Copyright 2019 the authors.
# 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
+
+from functools import reduce
+from contextlib import contextmanager
+from hy import _initialize_env_var
from clint.textui import colored
+_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS',
+ True)
+_hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
+
class HyError(Exception):
+ pass
+
+
+class HyInternalError(HyError):
+ """Unexpected errors occurring during compilation or parsing of Hy code.
+
+ Errors sub-classing this are not intended to be user-facing, and will,
+ hopefully, never be seen by users!
"""
- Generic Hy error. All internal Exceptions will be subclassed from this
- Exception.
+
+
+class HyLanguageError(HyError):
+ """Errors caused by invalid use of the Hy language.
+
+ This, and any errors inheriting from this, are user-facing.
"""
- pass
+ 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)
-class HyCompileError(HyError):
- def __init__(self, exception, traceback=None):
- self.exception = exception
- self.traceback = traceback
+ # 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):
- if isinstance(self.exception, HyTypeError):
- return str(self.exception)
- if self.traceback:
- tb = "".join(traceback.format_tb(self.traceback)).strip()
+ """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:
- tb = "No traceback available. 😟"
- return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s"
- % (self.exception.__class__.__name__,
- self.exception, tb))
+ 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])
-class HyTypeError(TypeError):
- def __init__(self, expression, message):
- super(HyTypeError, self).__init__(message)
- self.expression = expression
- self.message = message
- self.source = None
- self.filename = None
+ # 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))
- def __str__(self):
+ 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)
- result = ""
+ # This resulting string will come after a ":" prompt, so
+ # put it down a line.
+ output.insert(0, '\n')
- if all(getattr(self.expression, x, None) is not None
- for x in ("start_line", "start_column", "end_column")):
+ # Avoid "...expected str instance, ColoredString found"
+ return reduce(lambda x, y: x + y, output)
- 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]
+class HyCompileError(HyInternalError):
+ """Unexpected errors occurring within the compiler."""
- 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' % colored.red(source[0])
- result += ' %s%s\n' % (' '*(start-1),
- colored.green('^' + '-'*(length-1) + '^'))
- if len(source) > 1:
- result += ' %s\n' % colored.red(source[0])
- result += ' %s%s\n' % (' '*(start-1),
- colored.green('^' + '-'*length))
- if len(source) > 2: # write the middle lines
- for line in source[1:-1]:
- result += ' %s\n' % colored.red("".join(line))
- result += ' %s\n' % colored.green("-"*len(line))
-
- # write the last line
- result += ' %s\n' % colored.red("".join(source[-1]))
- result += ' %s\n' % colored.green('-'*(end-1) + '^')
+class HyTypeError(HyLanguageError, TypeError):
+ """TypeError occurring during the normal use of Hy."""
+
+
+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.
+
+ This, and any errors inheriting from this, are user-facing.
+ """
+
+
+class HyEvalError(HyLanguageError):
+ """Errors occurring during code evaluation at compile-time.
+
+ These errors distinguish unexpected errors within the compilation process
+ (i.e. `HyInternalError`s) from unrelated errors in user code evaluated by
+ the compiler (e.g. in `eval-and-compile`).
+
+ This, and any errors inheriting from this, are user-facing.
+ """
+
+
+class HyIOError(HyInternalError, IOError):
+ """ Subclass used to distinguish between IOErrors raised by Hy itself as
+ opposed to Hy programs.
+ """
+
+
+class HySyntaxError(HyLanguageError, SyntaxError):
+ """Error during the Lexing of a Hython expression."""
+
+
+class HyWrapperError(HyError, TypeError):
+ """Errors caused by language model object wrapping.
+
+ These can be caused by improper user-level use of a macro, so they're
+ not really "internal". If they arise due to anything else, they're an
+ internal/compiler problem, though.
+ """
+
+
+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:
+ return None
+
+ 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:
- result += ' File "%s", unknown location\n' % self.filename
+ # Normalize filename endings, because tracebacks will use `pyc` when
+ # the loader says `py`.
+ return filename.replace('.pyc', '.py')
+ except Exception:
+ return None
- result += colored.yellow("%s: %s\n\n" %
- (self.__class__.__name__,
- self.message))
- return result
+_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'])
+ if m is not None}
-class HyMacroExpansionError(HyTypeError):
- pass
+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.
+ The frames are actually filtered by each module's filename and only when a
+ subclass of `HyLanguageError` is emitted.
-class HyIOError(HyError, IOError):
+ This does not remove the frames from the actual tracebacks, so debugging
+ will show everything.
"""
- Trivial subclass of IOError and HyError, to distinguish between
- IOErrors raised by Hy itself as opposed to Hy programs.
+ # 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)
+
+ 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.
"""
- pass
+ 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:
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
+
+
+@contextmanager
+def filtered_hy_exceptions():
+ """Temporarily apply a `sys.excepthook` that filters Hy internal frames
+ from tracebacks.
+
+ Filtering can be controlled by the variable
+ `hy.errors._hy_filter_internal_errors` and environment variable
+ `HY_FILTER_INTERNAL_ERRORS`.
+ """
+ global _hy_filter_internal_errors
+ if _hy_filter_internal_errors:
+ current_hook = sys.excepthook
+ sys.excepthook = hy_exc_handler
+ yield
+ sys.excepthook = current_hook
+ else:
+ yield
diff --git a/hy/importer.py b/hy/importer.py
index e3cc50d24..0e5949833 100644
--- a/hy/importer.py
+++ b/hy/importer.py
@@ -17,10 +17,8 @@
from functools import partial
from contextlib import contextmanager
-from hy.errors import HyTypeError
from hy.compiler import hy_compile, hy_ast_compile_flags
from hy.lex import hy_parse
-from hy.lex.exceptions import LexException
from hy._compat import PY3
@@ -153,15 +151,9 @@ def _could_be_hy_src(filename):
def _hy_source_to_code(self, data, path, _optimize=-1):
if _could_be_hy_src(path):
source = data.decode("utf-8")
- try:
- hy_tree = hy_parse(source)
- with loader_module_obj(self) as module:
- data = hy_compile(hy_tree, module)
- except (HyTypeError, LexException) as e:
- if e.source is None:
- e.source = source
- e.filename = path
- raise
+ hy_tree = hy_parse(source, filename=path)
+ with loader_module_obj(self) as module:
+ data = hy_compile(hy_tree, module)
return _py_source_to_code(self, data, path, _optimize=_optimize)
@@ -287,19 +279,15 @@ def byte_compile_hy(self, fullname=None):
fullname = self._fix_name(fullname)
if fullname is None:
fullname = self.fullname
- try:
- hy_source = self.get_source(fullname)
- hy_tree = hy_parse(hy_source)
- with loader_module_obj(self) as module:
- hy_ast = hy_compile(hy_tree, module)
-
- code = compile(hy_ast, self.filename, 'exec',
- hy_ast_compile_flags)
- except (HyTypeError, LexException) as e:
- if e.source is None:
- e.source = hy_source
- e.filename = self.filename
- raise
+
+ hy_source = self.get_source(fullname)
+ hy_tree = hy_parse(hy_source, filename=self.filename)
+
+ with loader_module_obj(self) as module:
+ hy_ast = hy_compile(hy_tree, module)
+
+ code = compile(hy_ast, self.filename, 'exec',
+ hy_ast_compile_flags)
if not sys.dont_write_bytecode:
try:
@@ -453,7 +441,7 @@ def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
try:
flags = None
if _could_be_hy_src(filename):
- hy_tree = hy_parse(source_str)
+ hy_tree = hy_parse(source_str, filename=filename)
if module is None:
module = inspect.getmodule(inspect.stack()[1][0])
@@ -465,9 +453,6 @@ def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
codeobject = compile(source, dfile or filename, 'exec', flags)
except Exception as err:
- if isinstance(err, (HyTypeError, LexException)) and err.source is None:
- err.source = source_str
- err.filename = filename
py_exc = py_compile.PyCompileError(err.__class__, err,
dfile or filename)
diff --git a/hy/lex/__init__.py b/hy/lex/__init__.py
index bf82cf945..eb3ac41f4 100644
--- a/hy/lex/__init__.py
+++ b/hy/lex/__init__.py
@@ -18,7 +18,7 @@
from StringIO import StringIO
-def hy_parse(source):
+def hy_parse(source, filename=''):
"""Parse a Hy source string.
Parameters
@@ -26,31 +26,52 @@ def hy_parse(source):
source: string
Source code to parse.
+ filename: string, optional
+ File name corresponding to source. Defaults to "".
+
Returns
-------
- out : instance of `types.CodeType`
+ out : HyExpression
"""
- source = re.sub(r'\A#!.*', '', source)
- return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
+ _source = re.sub(r'\A#!.*', '', source)
+ res = HyExpression([HySymbol("do")] +
+ tokenize(_source + "\n",
+ filename=filename))
+ res.source = source
+ res.filename = filename
+ return res
-def tokenize(buf):
- """
- Tokenize a Lisp file or string buffer into internal Hy objects.
+class ParserState(object):
+ def __init__(self, source, filename):
+ self.source = source
+ self.filename = filename
+
+
+def tokenize(source, filename=None):
+ """ Tokenize a Lisp file or string buffer into internal Hy objects.
+
+ Parameters
+ ----------
+ source: str
+ The source to tokenize.
+ filename: str, optional
+ The filename corresponding to `source`.
"""
from hy.lex.lexer import lexer
from hy.lex.parser import parser
from rply.errors import LexingError
try:
- return parser.parse(lexer.lex(buf))
+ return parser.parse(lexer.lex(source),
+ state=ParserState(source, filename))
except LexingError as e:
pos = e.getsourcepos()
raise LexException("Could not identify the next token.",
- pos.lineno, pos.colno, buf)
+ None, filename, source,
+ max(pos.lineno, 1),
+ max(pos.colno, 1))
except LexException as e:
- if e.source is None:
- e.source = buf
- raise
+ raise e
mangle_delim = 'X'
diff --git a/hy/lex/exceptions.py b/hy/lex/exceptions.py
index 573a8e857..449119a21 100644
--- a/hy/lex/exceptions.py
+++ b/hy/lex/exceptions.py
@@ -1,49 +1,39 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
-
-from hy.errors import HyError
-
-
-class LexException(HyError):
- """Error during the Lexing of a Hython expression."""
- def __init__(self, message, lineno, colno, source=None):
- super(LexException, self).__init__(message)
- self.message = message
- self.lineno = lineno
- self.colno = colno
- self.source = source
- self.filename = ''
-
- def __str__(self):
- from hy.errors import colored
-
- line = self.lineno
- start = self.colno
-
- result = ""
-
- source = self.source.split("\n")
-
- if line > 0 and start > 0:
- result += ' File "%s", line %d, column %d\n\n' % (self.filename,
- line,
- start)
-
- if len(self.source) > 0:
- source_line = source[line-1]
- else:
- source_line = ""
-
- result += ' %s\n' % colored.red(source_line)
- result += ' %s%s\n' % (' '*(start-1), colored.green('^'))
-
- result += colored.yellow("LexException: %s\n\n" % self.message)
-
- return result
+from hy.errors import HySyntaxError
+
+
+class LexException(HySyntaxError):
+
+ @classmethod
+ def from_lexer(cls, message, state, token):
+ lineno = None
+ colno = None
+ source = state.source
+ source_pos = token.getsourcepos()
+
+ 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 = lineno or 1
+ colno = colno or 1
+
+ return cls(message,
+ None,
+ state.filename,
+ source,
+ lineno,
+ colno)
class PrematureEndOfInput(LexException):
- """We got a premature end of input"""
- def __init__(self, message):
- super(PrematureEndOfInput, self).__init__(message, -1, -1)
+ pass
diff --git a/hy/lex/parser.py b/hy/lex/parser.py
index c602734a5..c4df2a53d 100755
--- a/hy/lex/parser.py
+++ b/hy/lex/parser.py
@@ -6,7 +6,6 @@
from __future__ import unicode_literals
from functools import wraps
-import re, unicodedata
from rply import ParserGenerator
@@ -22,10 +21,10 @@
def set_boundaries(fun):
@wraps(fun)
- def wrapped(p):
+ def wrapped(state, p):
start = p[0].source_pos
end = p[-1].source_pos
- ret = fun(p)
+ ret = fun(state, p)
ret.start_line = start.lineno
ret.start_column = start.colno
if start is not end:
@@ -40,9 +39,9 @@ def wrapped(p):
def set_quote_boundaries(fun):
@wraps(fun)
- def wrapped(p):
+ def wrapped(state, p):
start = p[0].source_pos
- ret = fun(p)
+ ret = fun(state, p)
ret.start_line = start.lineno
ret.start_column = start.colno
ret.end_line = p[-1].end_line
@@ -52,54 +51,45 @@ def wrapped(p):
@pg.production("main : list_contents")
-def main(p):
+def main(state, p):
return p[0]
@pg.production("main : $end")
-def main_empty(p):
+def main_empty(state, p):
return []
-def reject_spurious_dots(*items):
- "Reject the spurious dots from items"
- for list in items:
- for tok in list:
- if tok == "." and type(tok) == HySymbol:
- raise LexException("Malformed dotted list",
- tok.start_line, tok.start_column)
-
-
@pg.production("paren : LPAREN list_contents RPAREN")
@set_boundaries
-def paren(p):
+def paren(state, p):
return HyExpression(p[1])
@pg.production("paren : LPAREN RPAREN")
@set_boundaries
-def empty_paren(p):
+def empty_paren(state, p):
return HyExpression([])
@pg.production("list_contents : term list_contents")
-def list_contents(p):
+def list_contents(state, p):
return [p[0]] + p[1]
@pg.production("list_contents : term")
-def list_contents_single(p):
+def list_contents_single(state, p):
return [p[0]]
@pg.production("list_contents : DISCARD term discarded_list_contents")
-def list_contents_empty(p):
+def list_contents_empty(state, p):
return []
@pg.production("discarded_list_contents : DISCARD term discarded_list_contents")
@pg.production("discarded_list_contents :")
-def discarded_list_contents(p):
+def discarded_list_contents(state, p):
pass
@@ -109,58 +99,58 @@ def discarded_list_contents(p):
@pg.production("term : list")
@pg.production("term : set")
@pg.production("term : string")
-def term(p):
+def term(state, p):
return p[0]
@pg.production("term : DISCARD term term")
-def term_discard(p):
+def term_discard(state, p):
return p[2]
@pg.production("term : QUOTE term")
@set_quote_boundaries
-def term_quote(p):
+def term_quote(state, p):
return HyExpression([HySymbol("quote"), p[1]])
@pg.production("term : QUASIQUOTE term")
@set_quote_boundaries
-def term_quasiquote(p):
+def term_quasiquote(state, p):
return HyExpression([HySymbol("quasiquote"), p[1]])
@pg.production("term : UNQUOTE term")
@set_quote_boundaries
-def term_unquote(p):
+def term_unquote(state, p):
return HyExpression([HySymbol("unquote"), p[1]])
@pg.production("term : UNQUOTESPLICE term")
@set_quote_boundaries
-def term_unquote_splice(p):
+def term_unquote_splice(state, p):
return HyExpression([HySymbol("unquote-splice"), p[1]])
@pg.production("term : HASHSTARS term")
@set_quote_boundaries
-def term_hashstars(p):
+def term_hashstars(state, p):
n_stars = len(p[0].getstr()[1:])
if n_stars == 1:
sym = "unpack-iterable"
elif n_stars == 2:
sym = "unpack-mapping"
else:
- raise LexException(
+ raise LexException.from_lexer(
"Too many stars in `#*` construct (if you want to unpack a symbol "
"beginning with a star, separate it with whitespace)",
- p[0].source_pos.lineno, p[0].source_pos.colno)
+ state, p[0])
return HyExpression([HySymbol(sym), p[1]])
@pg.production("term : HASHOTHER term")
@set_quote_boundaries
-def hash_other(p):
+def hash_other(state, p):
# p == [(Token('HASHOTHER', '#foo'), bar)]
st = p[0].getstr()[1:]
str_object = HyString(st)
@@ -170,63 +160,63 @@ def hash_other(p):
@pg.production("set : HLCURLY list_contents RCURLY")
@set_boundaries
-def t_set(p):
+def t_set(state, p):
return HySet(p[1])
@pg.production("set : HLCURLY RCURLY")
@set_boundaries
-def empty_set(p):
+def empty_set(state, p):
return HySet([])
@pg.production("dict : LCURLY list_contents RCURLY")
@set_boundaries
-def t_dict(p):
+def t_dict(state, p):
return HyDict(p[1])
@pg.production("dict : LCURLY RCURLY")
@set_boundaries
-def empty_dict(p):
+def empty_dict(state, p):
return HyDict([])
@pg.production("list : LBRACKET list_contents RBRACKET")
@set_boundaries
-def t_list(p):
+def t_list(state, p):
return HyList(p[1])
@pg.production("list : LBRACKET RBRACKET")
@set_boundaries
-def t_empty_list(p):
+def t_empty_list(state, p):
return HyList([])
@pg.production("string : STRING")
@set_boundaries
-def t_string(p):
+def t_string(state, p):
# Replace the single double quotes with triple double quotes to allow
# embedded newlines.
try:
s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""')
except SyntaxError:
- raise LexException("Can't convert {} to a HyString".format(p[0].value),
- p[0].source_pos.lineno, p[0].source_pos.colno)
+ raise LexException.from_lexer("Can't convert {} to a HyString".format(p[0].value),
+ state, p[0])
return (HyString if isinstance(s, str_type) else HyBytes)(s)
@pg.production("string : PARTIAL_STRING")
-def t_partial_string(p):
+def t_partial_string(state, p):
# Any unterminated string requires more input
- raise PrematureEndOfInput("Premature end of input")
+ raise PrematureEndOfInput.from_lexer("Partial string literal", state, p[0])
bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING')
@pg.production("string : BRACKETSTRING")
@set_boundaries
-def t_bracket_string(p):
+def t_bracket_string(state, p):
m = bracket_string_re.match(p[0].value)
delim, content = m.groups()
return HyString(content, brackets=delim)
@@ -234,7 +224,7 @@ def t_bracket_string(p):
@pg.production("identifier : IDENTIFIER")
@set_boundaries
-def t_identifier(p):
+def t_identifier(state, p):
obj = p[0].value
val = symbol_like(obj)
@@ -243,11 +233,11 @@ def t_identifier(p):
if "." in obj and symbol_like(obj.split(".", 1)[0]) is not None:
# E.g., `5.attr` or `:foo.attr`
- raise LexException(
+ raise LexException.from_lexer(
'Cannot access attribute on anything other than a name (in '
'order to get attributes of expressions, use '
'`(. )` or `(. )`)',
- p[0].source_pos.lineno, p[0].source_pos.colno)
+ state, p[0])
return HySymbol(obj)
@@ -284,14 +274,15 @@ def symbol_like(obj):
@pg.error
-def error_handler(token):
+def error_handler(state, token):
tokentype = token.gettokentype()
if tokentype == '$end':
- raise PrematureEndOfInput("Premature end of input")
+ raise PrematureEndOfInput.from_lexer("Premature end of input", state,
+ token)
else:
- raise LexException(
- "Ran into a %s where it wasn't expected." % tokentype,
- token.source_pos.lineno, token.source_pos.colno)
+ raise LexException.from_lexer(
+ "Ran into a %s where it wasn't expected." % tokentype, state,
+ token)
parser = pg.build()
diff --git a/hy/macros.py b/hy/macros.py
index be59200ab..e2cec31ac 100644
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -1,15 +1,19 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
+import sys
import importlib
import inspect
import pkgutil
+import traceback
-from hy._compat import PY3, string_types
+from contextlib import contextmanager
+
+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.
@@ -48,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:
@@ -73,7 +77,7 @@ def _(fn):
if not PY3:
_name = _name.encode('UTF-8')
- fn.__name__ = _name
+ fn = rename_function(fn, _name)
module = inspect.getmodule(fn)
@@ -148,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
@@ -159,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
@@ -171,14 +174,17 @@ def require(source_module, target_module, assignments, prefix=""):
return False
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_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
@@ -203,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
@@ -237,24 +243,33 @@ def load_macros(module):
if k not in module_tags})
-def make_empty_fn_copy(fn):
+@contextmanager
+def macro_exceptions(module, macro_tree, compiler=None):
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)
+ 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:
+
+ if compiler:
+ filename = compiler.filename
+ source = compiler.source
+ else:
+ filename = None
+ source = None
- except Exception:
+ exc_msg = ' '.join(traceback.format_exception_only(
+ sys.exc_info()[0], sys.exc_info()[1]))
- def empty_fn(*args, **kwargs):
- None
+ msg = "expanding macro {}\n ".format(str(macro_tree[0]))
+ msg += exc_msg
- return empty_fn
+ reraise(HyMacroExpansionError,
+ HyMacroExpansionError(
+ msg, macro_tree, filename, source),
+ sys.exc_info()[2])
def macroexpand(tree, module, compiler=None, once=False):
@@ -324,28 +339,13 @@ def macroexpand(tree, module, compiler=None, once=False):
compiler = HyASTCompiler(module)
opts['compiler'] = compiler
- try:
- m_copy = make_empty_fn_copy(m)
- m_copy(module.__name__, *tree[1:], **opts)
- except TypeError as e:
- msg = "expanding `" + str(tree[0]) + "': "
- msg += str(e).replace("()", "", 1).strip()
- raise HyMacroExpansionError(tree, msg)
-
- try:
+ with macro_exceptions(module, tree, compiler):
obj = m(module.__name__, *tree[1:], **opts)
- except HyTypeError as e:
- if e.expression is None:
- e.expression = tree
- raise
- except Exception as e:
- msg = "expanding `" + str(tree[0]) + "': " + repr(e)
- raise HyMacroExpansionError(tree, msg)
- if isinstance(obj, HyExpression):
- obj.module = inspect.getmodule(m)
+ if isinstance(obj, HyExpression):
+ obj.module = inspect.getmodule(m)
- tree = replace_hy_obj(obj, tree)
+ tree = replace_hy_obj(obj, tree)
if once:
break
@@ -375,7 +375,8 @@ def tag_macroexpand(tag, tree, module):
None)
if tag_macro is None:
- raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
+ raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
+ None, tag, None)
expr = tag_macro(tree)
diff --git a/hy/models.py b/hy/models.py
index cf02dab0b..478c69102 100644
--- a/hy/models.py
+++ b/hy/models.py
@@ -1,16 +1,18 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
-
from __future__ import unicode_literals
+
from contextlib import contextmanager
from math import isnan, isinf
+from hy import _initialize_env_var
from hy._compat import PY3, str_type, bytes_type, long_type, string_types
+from hy.errors import HyWrapperError
from fractions import Fraction
from clint.textui import colored
-
PRETTY = True
+_hy_colored_ast_objects = _initialize_env_var('HY_COLORED_AST_OBJECTS', False)
@contextmanager
@@ -63,7 +65,7 @@ def wrap_value(x):
new = _wrappers.get(type(x), lambda y: y)(x)
if not isinstance(new, HyObject):
- raise TypeError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
+ raise HyWrapperError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
if isinstance(x, HyObject):
new = new.replace(x, recursive=False)
if not hasattr(new, "start_column"):
@@ -271,8 +273,9 @@ def __repr__(self):
return str(self) if PRETTY else super(HySequence, self).__repr__()
def __str__(self):
+ global _hy_colored_ast_objects
with pretty():
- c = self.color
+ c = self.color if _hy_colored_ast_objects else str
if self:
return ("{}{}\n {}{}").format(
c(self.__class__.__name__),
@@ -298,10 +301,12 @@ class HyDict(HySequence):
"""
HyDict (just a representation of a dict)
"""
+ color = staticmethod(colored.green)
def __str__(self):
+ global _hy_colored_ast_objects
with pretty():
- g = colored.green
+ g = self.color if _hy_colored_ast_objects else str
if self:
pairs = []
for k, v in zip(self[::2],self[1::2]):
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index 75e9c499a..9311eef82 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -6,11 +6,10 @@
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
+from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3
import ast
@@ -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:
- # 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:
+ 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.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='', source='')
def test_empty_expr():
@@ -473,8 +465,8 @@ 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)
- message, = exception.args
+ 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='', 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,19 +533,19 @@ def test_ast_bracket_string():
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]))")
def test_for_compile_error():
"""Ensure we get compile error in tricky 'for' cases"""
- with pytest.raises(LexException) as excinfo:
+ with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)")
- assert excinfo.value.message == "Premature end of input"
+ 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():
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
index 3017d1640..dea1baf56 100644
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -14,10 +14,10 @@
import pytest
import hy
-from hy.errors import HyTypeError
from hy.lex import hy_parse
-from hy.lex.exceptions import LexException
-from hy.compiler import hy_compile
+from hy.errors import HyLanguageError
+from hy.lex.exceptions import PrematureEndOfInput
+from hy.compiler import hy_eval, hy_compile
from hy.importer import HyLoader, cache_from_source
try:
@@ -57,7 +57,7 @@ def test_runpy():
def test_stringer():
- _ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
+ _ast = hy_compile(hy_parse("(defn square [x] (* x x))"), __name__)
assert type(_ast.body[0]) == ast.FunctionDef
@@ -79,14 +79,8 @@ def _import_test():
def test_import_error_reporting():
"Make sure that (import) reports errors correctly."
- def _import_error_test():
- try:
- _ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
- except HyTypeError:
- return "Error reported"
-
- assert _import_error_test() == "Error reported"
- assert _import_error_test() is not None
+ with pytest.raises(HyLanguageError):
+ hy_compile(hy_parse("(import \"sys\")"), __name__)
def test_import_error_cleanup():
@@ -124,7 +118,7 @@ def test_import_autocompiles():
def test_eval():
def eval_str(s):
- return hy.eval(hy.read_str(s))
+ return hy_eval(hy.read_str(s), filename='', source=s)
assert eval_str('[1 2 3]') == [1, 2, 3]
assert eval_str('{"dog" "bark" "cat" "meow"}') == {
@@ -205,8 +199,7 @@ def unlink(filename):
assert mod.a == 11
assert mod.b == 20
- # Now cause a `LexException`, and confirm that the good module and its
- # contents stick around.
+ # Now cause a syntax error
unlink(source)
with open(source, "w") as f:
@@ -214,7 +207,7 @@ def unlink(filename):
f.write("(setv a 11")
f.write("(setv b (// 20 1))")
- with pytest.raises(LexException):
+ with pytest.raises(PrematureEndOfInput):
reload(mod)
mod = sys.modules.get(TESTFN)
diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py
index 309ca49c8..134bd5b8d 100644
--- a/tests/macros/test_macro_processor.py
+++ b/tests/macros/test_macro_processor.py
@@ -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():
diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy
index a89eece9e..ab8eaf417 100644
--- a/tests/native_tests/core.hy
+++ b/tests/native_tests/core.hy
@@ -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))))
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
index c237a5a29..65c629ab6 100644
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -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 []
diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy
index 1700b5d01..d5c48c78c 100644
--- a/tests/native_tests/native_macros.hy
+++ b/tests/native_tests/native_macros.hy
@@ -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 []
@@ -162,8 +162,8 @@
")
;; expand the macro twice, should use a different
;; gensym each time
- (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
- (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+ (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+ (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
;; and make sure there is something new that starts with _;G|
@@ -189,8 +189,8 @@
")
;; expand the macro twice, should use a different
;; gensym each time
- (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
- (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+ (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+ (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_;a|") s1))
@@ -213,8 +213,8 @@
")
;; expand the macro twice, should use a different
;; gensym each time
- (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
- (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+ (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+ (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_;res|") s1))
@@ -224,7 +224,7 @@
;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))")
- (assert (hy-compile (hy-parse macro2) "foo")))
+ (assert (hy-compile (hy-parse macro2) __name__)))
(defn test-defmacro! []
;; defmacro! must do everything defmacro/g! can
@@ -243,8 +243,8 @@
")
;; expand the macro twice, should use a different
;; gensym each time
- (setv _ast1 (hy-compile (hy-parse macro1) "foo"))
- (setv _ast2 (hy-compile (hy-parse macro1) "foo"))
+ (setv _ast1 (hy-compile (hy-parse macro1) __name__))
+ (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_;res|") s1))
@@ -254,7 +254,7 @@
;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))")
- (assert (hy-compile (hy-parse macro2) "foo"))
+ (assert (hy-compile (hy-parse macro2) __name__))
(defmacro! foo! [o!foo] `(do ~g!foo ~g!foo))
;; test that o! becomes g!
@@ -483,3 +483,37 @@ 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 \"\", 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))))
+
+
+ ;; This should throw a `HyWrapperError` that gets turned into a
+ ;; `HyMacroExpansionError`.
+ (with [excinfo (pytest.raises HyMacroExpansionError)]
+ (eval '(do (defmacro wrap-error-test []
+ (fn []))
+ (wrap-error-test))))
+ (assert (in "HyWrapperError" (str excinfo.value))))
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
index 716eb7779..e08edbbc3 100644
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -28,7 +28,7 @@
(defmacro forbid [expr]
`(assert (try
(eval '~expr)
- (except [TypeError] True)
+ (except [[TypeError SyntaxError]] True)
(else (raise AssertionError)))))
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 6336122fd..06b3af9fa 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -6,11 +6,11 @@
import os
import re
-import sys
import shlex
import subprocess
from hy.importer import cache_from_source
+from hy._compat import PY3
import pytest
@@ -123,7 +123,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():
@@ -149,10 +158,66 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
# inside run_cmd.
_, err = run_cmd("hy", """
(import hy.errors)
- (raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""")
+ (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
assert "AZ" in err
+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 "", 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 +488,87 @@ 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 "", line 1, in
+ # 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()[2 if PY3 else -3])
+
+ # Modeled after
+ # > python -c 'print("hi'
+ # File "", line 1
+ # print("hi
+ # ^
+ # SyntaxError: EOL while scanning string literal
+ _, error = run_cmd(r'hy -c "(print \""', expect=1)
+ peoi_re = (
+ r'Traceback \(most recent call last\):\n'
+ r' File "(?:|string-[0-9a-f]+)", line 1\n'
+ r' \(print "\n'
+ r' \^\n' +
+ r'{}PrematureEndOfInput: Partial string literal\n'.format(
+ r'hy\.lex\.exceptions\.' if PY3 else ''))
+ assert re.search(peoi_re, error)
+
+ # Modeled after
+ # > python -i -c "print('"
+ # File "", line 1
+ # print('
+ # ^
+ # SyntaxError: EOL while scanning string literal
+ # >>>
+ output, error = run_cmd(r'hy -i "(print \""')
+ assert output.startswith('=> ')
+ assert re.match(peoi_re, error)
+
+ # Modeled after
+ # > python -c 'print(a)'
+ # Traceback (most recent call last):
+ # File "", line 1, in
+ # 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 "", line 1, in '
+ # 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 "", line 1, in
+ # 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 "", line 1, in '
+ assert error_lines[-1].startswith('TypeError')
diff --git a/tests/test_lex.py b/tests/test_lex.py
index 19da88bab..f70971933 100644
--- a/tests/test_lex.py
+++ b/tests/test_lex.py
@@ -1,18 +1,46 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
+import sys
+import traceback
+
+import pytest
from math import isnan
from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
HyString, HyDict, HyList, HySet, HyKeyword)
from hy.lex import tokenize
from hy.lex.exceptions import LexException, PrematureEndOfInput
-import pytest
+from hy.errors import hy_exc_handler
def peoi(): return pytest.raises(PrematureEndOfInput)
def lexe(): return pytest.raises(LexException)
+def check_ex(execinfo, expected):
+ output = traceback.format_exception_only(execinfo.type, execinfo.value)
+ assert output[:-1] == expected[:-1]
+ # Python 2.7 doesn't give the full exception name, so we compensate.
+ assert output[-1].endswith(expected[-1])
+
+
+def check_trace_output(capsys, execinfo, expected):
+ sys.__excepthook__(execinfo.type, execinfo.value, execinfo.tb)
+ captured_wo_filtering = capsys.readouterr()[-1].strip('\n')
+
+ hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb)
+ captured_w_filtering = capsys.readouterr()[-1].strip('\n')
+
+ output = captured_w_filtering.split('\n')
+
+ # Make sure the filtered frames aren't the same as the unfiltered ones.
+ assert output[:-1] != captured_wo_filtering.split('\n')[:-1]
+ # Remove the origin frame lines.
+ assert output[3:-1] == expected[:-1]
+ # Python 2.7 doesn't give the full exception name, so we compensate.
+ assert output[-1].endswith(expected[-1])
+
+
def test_lex_exception():
""" Ensure tokenize throws a fit on a partial input """
with peoi(): tokenize("(foo")
@@ -30,8 +58,13 @@ def test_unbalanced_exception():
def test_lex_single_quote_err():
"Ensure tokenizing \"' \" throws a LexException that can be stringified"
# https://github.com/hylang/hy/issues/1252
- with lexe() as e: tokenize("' ")
- assert "Could not identify the next token" in str(e.value)
+ with lexe() as execinfo:
+ tokenize("' ")
+ check_ex(execinfo, [
+ ' File "", line 1\n',
+ " '\n",
+ ' ^\n',
+ 'LexException: Could not identify the next token.\n'])
def test_lex_expression_symbols():
@@ -74,7 +107,11 @@ def test_lex_strings_exception():
""" Make sure tokenize throws when codec can't decode some bytes"""
with lexe() as execinfo:
tokenize('\"\\x8\"')
- assert "Can't convert \"\\x8\" to a HyString" in str(execinfo.value)
+ check_ex(execinfo, [
+ ' File "", line 1\n',
+ ' "\\x8"\n',
+ ' ^\n',
+ 'LexException: Can\'t convert "\\x8" to a HyString\n'])
def test_lex_bracket_strings():
@@ -180,7 +217,16 @@ def test_lex_digit_separators():
def test_lex_bad_attrs():
- with lexe(): tokenize("1.foo")
+ with lexe() as execinfo:
+ tokenize("1.foo")
+ check_ex(execinfo, [
+ ' File "", line 1\n',
+ ' 1.foo\n',
+ ' ^\n',
+ 'LexException: Cannot access attribute on anything other'
+ ' than a name (in order to get attributes of expressions,'
+ ' use `(. )` or `(. )`)\n'])
+
with lexe(): tokenize("0.foo")
with lexe(): tokenize("1.5.foo")
with lexe(): tokenize("1e3.foo")
@@ -419,3 +465,27 @@ def test_discard():
assert tokenize("a '#_b c") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("c")])]
assert tokenize("a '#_b #_c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
assert tokenize("a '#_ #_b c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
+
+
+def test_lex_exception_filtering(capsys):
+ """Confirm that the exception filtering works for lexer errors."""
+
+ # First, test for PrematureEndOfInput
+ with peoi() as execinfo:
+ tokenize(" \n (foo\n \n")
+ check_trace_output(capsys, execinfo, [
+ ' File "", line 2',
+ ' (foo',
+ ' ^',
+ 'PrematureEndOfInput: Premature end of input'])
+
+ # Now, for a generic LexException
+ with lexe() as execinfo:
+ tokenize(" \n\n 1.foo ")
+ check_trace_output(capsys, execinfo, [
+ ' File "", line 3',
+ ' 1.foo',
+ ' ^',
+ 'LexException: Cannot access attribute on anything other'
+ ' than a name (in order to get attributes of expressions,'
+ ' use `(. )` or `(. )`)'])