Merge pull request #1687 from brandonwillard/clean-up-exceptions

Generate more concise syntax and compiler errors
This commit is contained in:
Kodi Arfer 2019-02-07 14:03:24 -05:00 committed by GitHub
commit e2d6640e8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1325 additions and 537 deletions

View File

@ -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.

View File

@ -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)

View File

@ -5,6 +5,16 @@ except ImportError:
__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

View File

@ -6,7 +6,7 @@ try:
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 @@ bytes_type = bytes if PY3 else str # NOQA
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'):

View File

@ -12,16 +12,25 @@ import os
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.models import HyExpression, HyString, HySymbol
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 @@ class HyQuitter(object):
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="<input>", 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="<input>"):
super(HyREPL, self).__init__(locals=locals,
filename=filename)
filename="<stdin>"):
# Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`).
# Also, make sure it's properly introduced to `sys.modules` and
# consistently use its namespace as `locals` from here on.
# We let `InteractiveConsole` initialize `self.locals` when it's
# `None`.
super(HyREPL, self).__init__(locals=locals,
filename=filename)
module_name = self.locals.get('__name__', '__console__')
# Make sure our newly created module is properly introduced to
# `sys.modules`, and consistently use its namespace as `self.locals`
# from here on.
self.module = sys.modules.setdefault(module_name,
types.ModuleType(module_name))
self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__
# Load cmdline-specific macros.
require('hy.cmdline', module_name, assignments='ALL')
require('hy.cmdline', self.module, assignments='ALL')
self.hy_compiler = HyASTCompiler(self.module)
self.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 @@ class HyREPL(code.InteractiveConsole, object):
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols})
def runsource(self, source, filename='<input>', 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()
try:
def ast_callback(self, exec_ast, eval_ast):
if self.spy:
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
# 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:
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)
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:
error_handler(e)
# Set this to avoid a print-out of the last value on errors.
self.print_last_value = False
self.showtraceback()
def runsource(self, source, filename='<stdin>', symbol='exec'):
try:
res = super(HyREPL, self).runsource(source, filename, symbol)
except (HyMacroExpansionError, HyRequireError):
# We need to handle these exceptions ourselves, because the base
# method only handles `OverflowError`, `SyntaxError` and
# `ValueError`.
self.showsyntaxerror(filename)
return False
except (HyLanguageError):
# Our compiler will also raise `TypeError`s
self.showtraceback()
return False
if value is not None:
# Shift exisitng REPL results
next_result = value
# 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
tree = hy_parse(source, filename=filename)
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
return 1
def run_command(source):
tree = hy_parse(source)
require("hy.cmdline", "__main__", assignments="ALL")
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
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 = '<input>'
filename = '<string>'
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='<string>')
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='<stdin>')
else:
# User did "hy <filename>"
@ -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 = '<stdin>'
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))

View File

@ -9,20 +9,21 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
from hy.errors import HyCompileError, HyTypeError
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 @@ class HyASTCompiler(object):
# 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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
@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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
# 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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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("<EOF>", "end of form")))
root, e.msg.replace("<EOF>", "end of form")))
return Result() + build_method(
self, expr, unmangle(sroot), *parse_tree)
@ -1681,13 +1720,13 @@ class HyASTCompiler(object):
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 @@ class HyASTCompiler(object):
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 '
'`(. <expression> {attr})` or '
'`(.{attr} <expression>)`)'.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 `(. <expression> {attr})` or `(.{attr} <expression>)`)'.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 "<string>". 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)
# Does the Hy AST object come with its own information?
filename = getattr(hytree, 'filename', filename) or '<string>'
source = getattr(hytree, 'source', source)
# Spoof the positions in the generated ast...
for node in ast.walk(_ast):
node.lineno = 1
node.col_offset = 1
for node in ast.walk(expr):
node.lineno = 1
node.col_offset = 1
_ast, expr = hy_compile(hytree, module, 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, "<eval_body>", "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>", "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 '<string>'
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 "<string>". 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

View File

@ -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`."

View File

@ -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):
"""
Generic Hy error. All internal Exceptions will be subclassed from this
Exception.
"""
pass
class HyCompileError(HyError):
def __init__(self, exception, traceback=None):
self.exception = exception
self.traceback = traceback
class HyInternalError(HyError):
"""Unexpected errors occurring during compilation or parsing of Hy code.
def __str__(self):
if isinstance(self.exception, HyTypeError):
return str(self.exception)
if self.traceback:
tb = "".join(traceback.format_tb(self.traceback)).strip()
Errors sub-classing this are not intended to be user-facing, and will,
hopefully, never be seen by users!
"""
class HyLanguageError(HyError):
"""Errors caused by invalid use of the Hy language.
This, and any errors inheriting from this, are user-facing.
"""
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:
tb = "No traceback available. 😟"
return("Internal Compiler Bug 😱\n%s: %s\nCompilation traceback:\n%s"
% (self.exception.__class__.__name__,
self.exception, tb))
super(HyLanguageError, self).__init__(message)
def compute_lineinfo(self, expression, filename, source, lineno, colno):
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
# 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)
def __str__(self):
if self.text:
lines = self.text.splitlines()
result = ""
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)
if all(getattr(self.expression, x, None) is not None
for x in ("start_line", "start_column", "end_column")):
# Trim the source down to the essentials.
self.text = '\n'.join(lines[self.lineno-1:end_line])
line = self.expression.start_line
start = self.expression.start_column
end = self.expression.end_column
source = []
if self.source is not None:
source = self.source.split("\n")[line-1:self.expression.end_line]
if line == self.expression.end_line:
length = end - start
if end_column:
if self.lineno == end_line:
self.arrow_offset = end_column
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) + '^')
self.arrow_offset = len(self.text[0])
self.arrow_offset -= self.offset
else:
self.arrow_offset = None
else:
result += ' File "%s", unknown location\n' % self.filename
# We could attempt to extract the source given a filename, but we
# don't.
self.lineno = lineno
self.offset = colno
self.arrow_offset = None
result += colored.yellow("%s: %s\n\n" %
(self.__class__.__name__,
self.message))
def __str__(self):
"""Provide an exception message that includes SyntaxError-like source
line information when available.
"""
global _hy_colored_errors
return result
# Syntax errors are special and annotate the traceback (instead of what
# we would do in the message that follows the traceback).
if isinstance(self, SyntaxError):
return super(HyLanguageError, self).__str__()
# When there isn't extra source information, use the normal message.
if not isinstance(self, SyntaxError) and not self.text:
return super(HyLanguageError, self).__str__()
# Re-purpose Python's builtin syntax error formatting.
output = traceback.format_exception_only(
SyntaxError,
SyntaxError(self.msg, (self.filename, self.lineno, self.offset,
self.text)))
arrow_idx, _ = next(((i, x) for i, x in enumerate(output)
if x.strip() == '^'),
(None, None))
if arrow_idx:
msg_idx = arrow_idx + 1
else:
msg_idx, _ = next((i, x) for i, x in enumerate(output)
if x.startswith('SyntaxError: '))
# Get rid of erroneous error-type label.
output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx])
# Extend the text arrow, when given enough source info.
if arrow_idx and self.arrow_offset:
output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'),
'-' * (self.arrow_offset - 1))
if _hy_colored_errors:
from clint.textui import colored
output[msg_idx:] = [colored.yellow(o) for o in output[msg_idx:]]
if arrow_idx:
output[arrow_idx] = colored.green(output[arrow_idx])
for idx, line in enumerate(output[::msg_idx]):
if line.strip().startswith(
'File "{}", line'.format(self.filename)):
output[idx] = colored.red(line)
# This resulting string will come after a "<class-name>:" prompt, so
# put it down a line.
output.insert(0, '\n')
# Avoid "...expected str instance, ColoredString found"
return reduce(lambda x, y: x + y, output)
class HyMacroExpansionError(HyTypeError):
pass
class HyCompileError(HyInternalError):
"""Unexpected errors occurring within the compiler."""
class HyIOError(HyError, IOError):
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.
"""
Trivial subclass of IOError and HyError, to distinguish between
IOErrors raised by Hy itself as opposed to Hy programs.
class HyMacroExpansionError(HyLanguageError):
"""Errors caused by invalid use of Hy macros.
This, and any errors inheriting from this, are user-facing.
"""
pass
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:
# Normalize filename endings, because tracebacks will use `pyc` when
# the loader says `py`.
return filename.replace('.pyc', '.py')
except Exception:
return None
_tb_hidden_modules = {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}
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.
This does not remove the frames from the actual tracebacks, so debugging
will show everything.
"""
# frame = (filename, line number, function name*, text)
new_tb = []
for frame in traceback.extract_tb(exc_traceback):
if not (frame[0].replace('.pyc', '.py') in _tb_hidden_modules or
os.path.dirname(frame[0]) in _tb_hidden_modules):
new_tb += [frame]
lines = traceback.format_list(new_tb)
lines.insert(0, "Traceback (most recent call last):\n")
lines.extend(traceback.format_exception_only(exc_type, exc_value))
output = ''.join(lines)
return output
def hy_exc_handler(exc_type, exc_value, exc_traceback):
"""A `sys.excepthook` handler that uses `hy_exc_filter` to
remove internal Hy frames from a traceback print-out.
"""
if os.environ.get('HY_DEBUG', False):
return sys.__excepthook__(exc_type, exc_value, exc_traceback)
try:
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

View File

@ -17,10 +17,8 @@ import importlib
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 @@ if PY3:
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 @@ else:
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 @@ else:
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 @@ else:
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)

View File

@ -18,7 +18,7 @@ except ImportError:
from StringIO import StringIO
def hy_parse(source):
def hy_parse(source, filename='<string>'):
"""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 "<string>".
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'

View File

@ -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
from hy.errors import HySyntaxError
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 = '<stdin>'
class LexException(HySyntaxError):
def __str__(self):
from hy.errors import colored
@classmethod
def from_lexer(cls, message, state, token):
lineno = None
colno = None
source = state.source
source_pos = token.getsourcepos()
line = self.lineno
start = self.colno
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
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
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

View File

@ -6,7 +6,6 @@
from __future__ import unicode_literals
from functools import wraps
import re, unicodedata
from rply import ParserGenerator
@ -22,10 +21,10 @@ pg = ParserGenerator([rule.name for rule in lexer.rules] + ['$end'])
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 set_boundaries(fun):
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 set_quote_boundaries(fun):
@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 '
'`(. <expression> <attr>)` or `(.<attr> <expression>)`)',
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()

View File

@ -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 tag(name):
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.
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:
formatted_args = format_args(fn)
fn_str = 'lambda {}: None'.format(
formatted_args.lstrip('(').rstrip(')'))
empty_fn = eval(fn_str)
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("<lambda>()", "", 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)

View File

@ -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 @@ class HySequence(HyObject, list):
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]):

View File

@ -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:
with pytest.raises(HyError) as excinfo:
hy_compile(hy_parse(expr), __name__)
if issubclass(excinfo.type, HyLanguageError):
assert excinfo.value.msg
return excinfo.value
elif issubclass(excinfo.type, HyCompileError):
# Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug.
assert isinstance(e.expression, HyObject)
assert e.message
return e
except HyCompileError as e:
# Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug.
assert isinstance(e.exception, HyTypeError)
assert e.traceback
return e
return excinfo.value
def s(x):
@ -60,11 +54,9 @@ def test_ast_bad_type():
"Make sure AST breakage can happen"
class C:
pass
try:
hy_compile(C(), "__main__")
assert True is False
except TypeError:
pass
with pytest.raises(TypeError):
hy_compile(C(), __name__, filename='<string>', source='')
def test_empty_expr():
@ -473,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='<string>', source=s)
# We put hy_s in a list so it isn't interpreted as a docstring.
# code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))])
@ -541,19 +533,19 @@ Only one leading newline should be removed.
def test_compile_error():
"""Ensure we get compile error in tricky cases"""
with pytest.raises(HyTypeError) as excinfo:
with pytest.raises(HyLanguageError) as excinfo:
can_compile("(fn [] (in [1 2 3]))")
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():

View File

@ -14,10 +14,10 @@ from fractions import Fraction
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 test_imports():
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='<string>', source=s)
assert eval_str('[1 2 3]') == [1, 2, 3]
assert eval_str('{"dog" "bark" "cat" "meow"}') == {
@ -205,8 +199,7 @@ def test_reload():
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 test_reload():
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)

View File

@ -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():

View File

@ -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))))

View File

@ -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 []

View File

@ -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 \"<string>\", line 1"
" (defmacro blah [x] `(print ~@z)) (blah y)"
" ^------^"
"expanding macro blah"
" NameError: global name 'z' is not defined"])
(assert (= (cut expected 0 -1) (cut output 0 -1)))
(assert (or (= (get expected -1) (get output -1))
;; Handle PyPy's peculiarities
(= (.replace (get expected -1) "global " "") (get output -1))))
;; 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))))

View File

@ -28,7 +28,7 @@
(defmacro forbid [expr]
`(assert (try
(eval '~expr)
(except [TypeError] True)
(except [[TypeError SyntaxError]] True)
(else (raise AssertionError)))))

View File

@ -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 "<string>", line 3',
' \'a 2 3',
' ^^',
'this',
'is',
'a',
'message']
for obs, exp in zip(err_parts, expected):
assert obs.startswith(exp)
# Make sure only one up-arrow is printed
_, err = run_cmd("hy", prg_str.format(3, 1, 1))
msg_idx = err.rindex("HyLanguageError:")
assert msg_idx
err_parts = err[msg_idx:].splitlines()[1:]
assert err_parts[2] == ' ^'
# Make sure lines are printed in between arrows separated by more than one
# character.
_, err = run_cmd("hy", prg_str.format(3, 1, 6))
print(err)
msg_idx = err.rindex("HyLanguageError:")
assert msg_idx
err_parts = err[msg_idx:].splitlines()[1:]
assert err_parts[2] == ' ^----^'
def test_bin_hy_stdin_bad_repr():
# https://github.com/hylang/hy/issues/1389
output, err = run_cmd("hy", """
@ -423,3 +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 "<string>", line 1, in <module>
# ImportError: No module named not_a_real_module
_, error = run_cmd('hy', '(require not-a-real-module)')
error_lines = error.splitlines()
if error_lines[-1] == '':
del error_lines[-1]
assert len(error_lines) <= 10
# Rough check for the internal traceback filtering
req_err(error_lines[4 if PY3 else -1])
_, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
error_lines = error.splitlines()
assert len(error_lines) <= 4
req_err(error_lines[-1])
output, error = run_cmd('hy -i "(require not-a-real-module)"')
assert output.startswith('=> ')
print(error.splitlines())
req_err(error.splitlines()[2 if PY3 else -3])
# Modeled after
# > python -c 'print("hi'
# File "<string>", line 1
# print("hi
# ^
# SyntaxError: EOL while scanning string literal
_, error = run_cmd(r'hy -c "(print \""', expect=1)
peoi_re = (
r'Traceback \(most recent call last\):\n'
r' File "(?:<string>|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 "<string>", 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 "<string>", line 1, in <module>
# NameError: name 'a' is not defined
output, error = run_cmd('hy -c "(print a)"', expect=1)
error_lines = error.splitlines()
assert error_lines[3] == ' File "<string>", line 1, in <module>'
# PyPy will add "global" to this error message, so we work around that.
assert error_lines[-1].strip().replace(' global', '') == (
"NameError: name 'a' is not defined")
# Modeled after
# > python -c 'compile()'
# Traceback (most recent call last):
# File "<string>", line 1, in <module>
# TypeError: Required argument 'source' (pos 1) not found
output, error = run_cmd('hy -c "(compile)"', expect=1)
error_lines = error.splitlines()
assert error_lines[-2] == ' File "<string>", line 1, in <module>'
assert error_lines[-1].startswith('TypeError')

View File

@ -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 "<string>", 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 "<string>", 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 "<string>", 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 `(. <expression> <attr>)` or `(.<attr> <expression>)`)\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 "<string>", 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 "<string>", line 3',
' 1.foo',
' ^',
'LexException: Cannot access attribute on anything other'
' than a name (in order to get attributes of expressions,'
' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)'])