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 Bug Fixes
------------------------------ ------------------------------
* Cleaned up syntax and compiler errors
* Fixed issue with empty arguments in `defmain`. * Fixed issue with empty arguments in `defmain`.
* `require` now compiles to Python AST. * `require` now compiles to Python AST.
* Fixed circular `require`s. * Fixed circular `require`s.

View File

@ -48,12 +48,6 @@ Command Line Options
`--spy` only works on REPL mode. `--spy` only works on REPL mode.
.. versionadded:: 0.9.11 .. versionadded:: 0.9.11
.. cmdoption:: --show-tracebacks
Print extended tracebacks for Hy exceptions.
.. versionadded:: 0.9.12
.. cmdoption:: --repl-output-fn .. cmdoption:: --repl-output-fn
Format REPL output using specific function (e.g., hy.contrib.hy-repr.hy-repr) Format REPL output using specific function (e.g., hy.contrib.hy-repr.hy-repr)

View File

@ -5,6 +5,16 @@ except ImportError:
__version__ = 'unknown' __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 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 import __builtin__ as builtins
except ImportError: except ImportError:
import builtins # NOQA import builtins # NOQA
import sys, keyword import sys, keyword, textwrap
PY3 = sys.version_info[0] >= 3 PY3 = sys.version_info[0] >= 3
PY35 = sys.version_info >= (3, 5) 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 long_type = int if PY3 else long # NOQA
string_types = str if PY3 else basestring # NOQA string_types = str if PY3 else basestring # NOQA
#
# Inspired by the same-named `six` functions.
#
if PY3: 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: else:
def raise_empty(t, *args): def raise_from(value, from_value=None):
raise t(*args) 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): def isidentifier(x):
if x in ('True', 'False', 'None', 'print'): if x in ('True', 'False', 'None', 'print'):

View File

@ -12,16 +12,25 @@ import os
import io import io
import importlib import importlib
import py_compile import py_compile
import traceback
import runpy import runpy
import types import types
import time
import linecache
import hashlib
import codeop
import astor.code_gen import astor.code_gen
import hy import hy
from hy.lex import hy_parse, mangle from hy.lex import hy_parse, mangle
from hy.lex.exceptions import LexException, PrematureEndOfInput from contextlib import contextmanager
from hy.compiler import HyASTCompiler, hy_compile, hy_eval from hy.lex.exceptions import PrematureEndOfInput
from hy.errors import HyTypeError 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.importer import runhy
from hy.completer import completion, Completer from hy.completer import completion, Completer
from hy.macros import macro, require from hy.macros import macro, require
@ -29,6 +38,11 @@ from hy.models import HyExpression, HyString, HySymbol
from hy._compat import builtins, PY3, FileNotFoundError from hy._compat import builtins, PY3, FileNotFoundError
sys.last_type = None
sys.last_value = None
sys.last_traceback = None
class HyQuitter(object): class HyQuitter(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -49,30 +63,188 @@ class HyQuitter(object):
builtins.quit = HyQuitter('quit') builtins.quit = HyQuitter('quit')
builtins.exit = HyQuitter('exit') 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): class HyREPL(code.InteractiveConsole, object):
def __init__(self, spy=False, output_fn=None, locals=None, def __init__(self, spy=False, output_fn=None, locals=None,
filename="<input>"): filename="<stdin>"):
super(HyREPL, self).__init__(locals=locals,
filename=filename)
# Create a proper module for this REPL so that we can obtain it easily # Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`). # (e.g. using `importlib.import_module`).
# Also, make sure it's properly introduced to `sys.modules` and # We let `InteractiveConsole` initialize `self.locals` when it's
# consistently use its namespace as `locals` from here on. # `None`.
super(HyREPL, self).__init__(locals=locals,
filename=filename)
module_name = self.locals.get('__name__', '__console__') module_name = self.locals.get('__name__', '__console__')
# Make sure our newly created module is properly introduced to
# `sys.modules`, and consistently use its namespace as `self.locals`
# from here on.
self.module = sys.modules.setdefault(module_name, self.module = sys.modules.setdefault(module_name,
types.ModuleType(module_name)) types.ModuleType(module_name))
self.module.__dict__.update(self.locals) self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__ self.locals = self.module.__dict__
# Load cmdline-specific macros. # Load cmdline-specific macros.
require('hy.cmdline', module_name, assignments='ALL') require('hy.cmdline', self.module, assignments='ALL')
self.hy_compiler = HyASTCompiler(self.module) self.hy_compiler = HyASTCompiler(self.module)
self.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.spy = spy
self.last_value = None
self.print_last_value = True
if output_fn is None: if output_fn is None:
self.output_fn = repr 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._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols}) self.locals.update({sym: None for sym in self._repl_results_symbols})
def runsource(self, source, filename='<input>', symbol='single'): # Allow access to the running REPL instance
global SIMPLE_TRACEBACKS self.locals['_hy_repl'] = self
def error_handler(e, use_simple_traceback=False): def ast_callback(self, exec_ast, eval_ast):
self.locals[mangle("*e")] = e
if use_simple_traceback:
print(e, file=sys.stderr)
else:
self.showtraceback()
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
try:
def ast_callback(main_ast, expr_ast):
if self.spy: if self.spy:
try:
# Mush the two AST chunks into a single module for # Mush the two AST chunks into a single module for
# conversion into Python. # conversion into Python.
new_ast = ast.Module(main_ast.body + new_ast = ast.Module(exec_ast.body +
[ast.Expr(expr_ast.body)]) [ast.Expr(eval_ast.body)])
print(astor.to_source(new_ast)) print(astor.to_source(new_ast))
except Exception:
msg = 'Exception in AST callback:\n{}\n'.format(
traceback.format_exc())
self.write(msg)
value = hy_eval(do, self.locals, def _error_wrap(self, error_fn, exc_info_override=False, *args, **kwargs):
ast_callback=ast_callback, sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
compiler=self.hy_compiler)
except HyTypeError as e: if exc_info_override:
if e.source is None: # Use a traceback that doesn't have the REPL frames.
e.source = source sys.last_type = self.locals.get('_hy_last_type', sys.last_type)
e.filename = filename sys.last_value = self.locals.get('_hy_last_value', sys.last_value)
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS) sys.last_traceback = self.locals.get('_hy_last_traceback',
return False 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:
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: 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 return False
if value is not None:
# Shift exisitng REPL results # Shift exisitng REPL results
next_result = value if not res:
next_result = self.last_value
for sym in self._repl_results_symbols: for sym in self._repl_results_symbols:
self.locals[sym], next_result = next_result, self.locals[sym] self.locals[sym], next_result = next_result, self.locals[sym]
# Print the value. # Print the value.
if self.print_last_value:
try: try:
output = self.output_fn(value) output = self.output_fn(self.last_value)
except Exception as e: except Exception:
error_handler(e) self.showtraceback()
return False return False
print(output) print(output)
return False
return res
@macro("koan") @macro("koan")
@ -202,23 +405,17 @@ def ideas_macro(ETname):
""")]) """)])
SIMPLE_TRACEBACKS = True def run_command(source, filename=None):
__main__ = importlib.import_module('__main__')
require("hy.cmdline", __main__, assignments="ALL")
def pretty_error(func, *args, **kw):
try: try:
return func(*args, **kw) tree = hy_parse(source, filename=filename)
except (HyTypeError, LexException) as e: except HyLanguageError:
if SIMPLE_TRACEBACKS: hy_exc_handler(*sys.exc_info())
print(e, file=sys.stderr) return 1
sys.exit(1)
raise
with filtered_hy_exceptions():
def run_command(source): hy_eval(tree, None, __main__, filename=filename, source=source)
tree = hy_parse(source)
require("hy.cmdline", "__main__", assignments="ALL")
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
return 0 return 0
@ -231,9 +428,9 @@ def run_repl(hr=None, **kwargs):
hr = HyREPL(**kwargs) hr = HyREPL(**kwargs)
namespace = hr.locals namespace = hr.locals
with filtered_hy_exceptions(), \
with completion(Completer(namespace)): extend_linecache(hr.cmdline_cache), \
completion(Completer(namespace)):
hr.interact("{appname} {version} using " hr.interact("{appname} {version} using "
"{py}({build}) {pyversion} on {os}".format( "{py}({build}) {pyversion} on {os}".format(
appname=hy.__appname__, appname=hy.__appname__,
@ -260,10 +457,17 @@ def run_icommand(source, **kwargs):
source = f.read() source = f.read()
filename = source filename = source
else: else:
filename = '<input>' filename = '<string>'
hr = HyREPL(**kwargs) 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) return run_repl(hr)
@ -300,9 +504,6 @@ def cmdline_handler(scriptname, argv):
"(e.g., hy.contrib.hy-repr.hy-repr)") "(e.g., hy.contrib.hy-repr.hy-repr)")
parser.add_argument("-v", "--version", action="version", version=VERSION) 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. # this will contain the script/program name and any arguments for it.
parser.add_argument('args', nargs=argparse.REMAINDER, parser.add_argument('args', nargs=argparse.REMAINDER,
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
@ -327,10 +528,6 @@ def cmdline_handler(scriptname, argv):
options = parser.parse_args(argv[1:]) options = parser.parse_args(argv[1:])
if options.show_tracebacks:
global SIMPLE_TRACEBACKS
SIMPLE_TRACEBACKS = False
if options.E: if options.E:
# User did "hy -E ..." # User did "hy -E ..."
_remove_python_envs() _remove_python_envs()
@ -340,7 +537,7 @@ def cmdline_handler(scriptname, argv):
if options.command: if options.command:
# User did "hy -c ..." # User did "hy -c ..."
return run_command(options.command) return run_command(options.command, filename='<string>')
if options.mod: if options.mod:
# User did "hy -m ..." # User did "hy -m ..."
@ -356,7 +553,7 @@ def cmdline_handler(scriptname, argv):
if options.args: if options.args:
if options.args[0] == "-": if options.args[0] == "-":
# Read the program from stdin # Read the program from stdin
return run_command(sys.stdin.read()) return run_command(sys.stdin.read(), filename='<stdin>')
else: else:
# User did "hy <filename>" # User did "hy <filename>"
@ -371,12 +568,16 @@ def cmdline_handler(scriptname, argv):
try: try:
sys.argv = options.args sys.argv = options.args
with filtered_hy_exceptions():
runhy.run_path(filename, run_name='__main__') runhy.run_path(filename, run_name='__main__')
return 0 return 0
except FileNotFoundError as e: except FileNotFoundError as e:
print("hy: Can't open file '{0}': [Errno {1}] {2}".format( print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
e.filename, e.errno, e.strerror), file=sys.stderr) e.filename, e.errno, e.strerror), file=sys.stderr)
sys.exit(e.errno) sys.exit(e.errno)
except HyLanguageError:
hy_exc_handler(*sys.exc_info())
sys.exit(1)
# User did NOTHING! # User did NOTHING!
return run_repl(spy=options.spy, output_fn=options.repl_output_fn) return run_repl(spy=options.spy, output_fn=options.repl_output_fn)
@ -446,12 +647,16 @@ def hy2py_main():
options = parser.parse_args(sys.argv[1:]) options = parser.parse_args(sys.argv[1:])
if options.FILE is None or options.FILE == '-': if options.FILE is None or options.FILE == '-':
filename = '<stdin>'
source = sys.stdin.read() source = sys.stdin.read()
else: else:
filename = options.FILE
with io.open(options.FILE, 'r', encoding='utf-8') as source_file: with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read() source = source_file.read()
hst = pretty_error(hy_parse, source) with filtered_hy_exceptions():
hst = hy_parse(source, filename=filename)
if options.with_source: if options.with_source:
# need special printing on Windows in case the # need special printing on Windows in case the
# codepage doesn't support utf-8 characters # codepage doesn't support utf-8 characters
@ -466,7 +671,9 @@ def hy2py_main():
print() print()
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 options.with_ast:
if PY3 and platform.system() == "Windows": if PY3 and platform.system() == "Windows":
_print_for_windows(astor.dump_tree(_ast)) _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, from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack) notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
from hy.errors import HyCompileError, HyTypeError from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
HySyntaxError, HyEvalError, HyInternalError)
from hy.lex import mangle, unmangle from hy.lex import mangle, unmangle
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3, from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
PY35, raise_empty) PY35, reraise)
from hy.macros import require, load_macros, macroexpand, tag_macroexpand from hy.macros import require, load_macros, macroexpand, tag_macroexpand
import hy.core import hy.core
import pkgutil
import traceback import traceback
import importlib import importlib
import inspect import inspect
import pkgutil
import types import types
import ast import ast
import sys import sys
@ -340,22 +341,33 @@ def is_unpack(kind, x):
class HyASTCompiler(object): class HyASTCompiler(object):
"""A Hy-to-Python AST compiler""" """A Hy-to-Python AST compiler"""
def __init__(self, module): def __init__(self, module, filename=None, source=None):
""" """
Parameters Parameters
---------- ----------
module: str or types.ModuleType 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.anon_var_count = 0
self.imports = defaultdict(set) self.imports = defaultdict(set)
self.temp_if = None self.temp_if = None
if not inspect.ismodule(module): 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 # Hy expects these to be present, so we prep the module for Hy
# compilation. # compilation.
@ -431,10 +443,18 @@ class HyASTCompiler(object):
# nested; so let's re-raise this exception, let's not wrap it in # nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError! # another HyCompileError!
raise raise
except HyTypeError: except HyLanguageError as e:
raise # These are expected errors that should be passed to the user.
reraise(type(e), e, sys.exc_info()[2])
except Exception as e: except Exception as e:
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, def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False): oldpy_unpack=False):
@ -455,8 +475,8 @@ class HyASTCompiler(object):
if not PY35 and oldpy_unpack and is_unpack("iterable", expr): if not PY35 and oldpy_unpack and is_unpack("iterable", expr):
if oldpy_starargs: if oldpy_starargs:
raise HyTypeError(expr, "Pythons < 3.5 allow only one " raise self._syntax_error(expr,
"`unpack-iterable` per call") "Pythons < 3.5 allow only one `unpack-iterable` per call")
oldpy_starargs = self.compile(expr[1]) oldpy_starargs = self.compile(expr[1])
ret += oldpy_starargs ret += oldpy_starargs
oldpy_starargs = oldpy_starargs.force_expr oldpy_starargs = oldpy_starargs.force_expr
@ -472,21 +492,20 @@ class HyASTCompiler(object):
expr, arg=None, value=ret.force_expr)) expr, arg=None, value=ret.force_expr))
elif oldpy_unpack: elif oldpy_unpack:
if oldpy_kwargs: if oldpy_kwargs:
raise HyTypeError(expr, "Pythons < 3.5 allow only one " raise self._syntax_error(expr,
"`unpack-mapping` per call") "Pythons < 3.5 allow only one `unpack-mapping` per call")
oldpy_kwargs = ret.force_expr oldpy_kwargs = ret.force_expr
elif with_kwargs and isinstance(expr, HyKeyword): elif with_kwargs and isinstance(expr, HyKeyword):
try: try:
value = next(exprs_iter) value = next(exprs_iter)
except StopIteration: except StopIteration:
raise HyTypeError(expr, raise self._syntax_error(expr,
"Keyword argument {kw} needs " "Keyword argument {kw} needs a value.".format(kw=expr))
"a value.".format(kw=expr))
if not expr: if not expr:
raise HyTypeError(expr, "Can't call a function with the " raise self._syntax_error(expr,
"empty keyword") "Can't call a function with the empty keyword")
compiled_value = self.compile(value) compiled_value = self.compile(value)
ret += compiled_value ret += compiled_value
@ -527,7 +546,7 @@ class HyASTCompiler(object):
if isinstance(name, Result): if isinstance(name, Result):
if not name.is_expr(): if not name.is_expr():
raise HyTypeError(expr, raise self._syntax_error(expr,
"Can't assign or delete a non-expression") "Can't assign or delete a non-expression")
name = name.expr name = name.expr
@ -547,9 +566,8 @@ class HyASTCompiler(object):
new_name = ast.Starred( new_name = ast.Starred(
value=self._storeize(expr, name.value, func)) value=self._storeize(expr, name.value, func))
else: else:
raise HyTypeError(expr, raise self._syntax_error(expr,
"Can't assign or delete a %s" % "Can't assign or delete a %s" % type(expr).__name__)
type(expr).__name__)
new_name.ctx = func() new_name.ctx = func()
ast.copy_location(new_name, name) ast.copy_location(new_name, name)
@ -575,9 +593,8 @@ class HyASTCompiler(object):
op = unmangle(ast_str(form[0])) op = unmangle(ast_str(form[0]))
if level == 0 and op in ("unquote", "unquote-splice"): if level == 0 and op in ("unquote", "unquote-splice"):
if len(form) != 2: if len(form) != 2:
raise HyTypeError(form, raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1,
("`%s' needs 1 argument, got %s" % self.filename, form, self.source)
op, len(form) - 1))
return set(), form[1], op == "unquote-splice" return set(), form[1], op == "unquote-splice"
elif op == "quasiquote": elif op == "quasiquote":
level += 1 level += 1
@ -629,7 +646,8 @@ class HyASTCompiler(object):
@special("unpack-iterable", [FORM]) @special("unpack-iterable", [FORM])
def compile_unpack_iterable(self, expr, root, arg): def compile_unpack_iterable(self, expr, root, arg):
if not PY3: 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 = self.compile(arg)
ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load()) ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
return ret return ret
@ -659,7 +677,8 @@ class HyASTCompiler(object):
if cause is not None: if cause is not None:
if not PY3: 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) cause = self.compile(cause)
ret += cause ret += cause
cause = cause.force_expr cause = cause.force_expr
@ -706,13 +725,11 @@ class HyASTCompiler(object):
# Using (else) without (except) is verboten! # Using (else) without (except) is verboten!
if orelse and not handlers: if orelse and not handlers:
raise HyTypeError( raise self._syntax_error(expr,
expr,
"`try' cannot have `else' without `except'") "`try' cannot have `else' without `except'")
# Likewise a bare (try) or (try BODY). # Likewise a bare (try) or (try BODY).
if not (handlers or finalbody): if not (handlers or finalbody):
raise HyTypeError( raise self._syntax_error(expr,
expr,
"`try' must have an `except' or `finally' clause") "`try' must have an `except' or `finally' clause")
returnable = Result( returnable = Result(
@ -963,7 +980,8 @@ class HyASTCompiler(object):
def compile_decorate_expression(self, expr, name, args): def compile_decorate_expression(self, expr, name, args):
decs, fn = args[:-1], self.compile(args[-1]) decs, fn = args[:-1], self.compile(args[-1])
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables): 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) decs, ret, _ = self._compile_collect(decs)
fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
return ret + fn return ret + fn
@ -1194,8 +1212,8 @@ class HyASTCompiler(object):
if (HySymbol('*'), None) in kids: if (HySymbol('*'), None) in kids:
if len(kids) != 1: if len(kids) != 1:
star = kids[kids.index((HySymbol('*'), None))][0] star = kids[kids.index((HySymbol('*'), None))][0]
raise HyTypeError(star, "* in an import name list " raise self._syntax_error(star,
"must be on its own") "* in an import name list must be on its own")
else: else:
assignments = [(k, v or k) for k, v in kids] assignments = [(k, v or k) for k, v in kids]
@ -1390,14 +1408,14 @@ class HyASTCompiler(object):
if str_name in (["None"] + (["True", "False"] if PY3 else [])): if str_name in (["None"] + (["True", "False"] if PY3 else [])):
# Python 2 allows assigning to True and False, although # Python 2 allows assigning to True and False, although
# this is rarely wise. # this is rarely wise.
raise HyTypeError(name, raise self._syntax_error(name,
"Can't assign to `%s'" % str_name) "Can't assign to `%s'" % str_name)
result = self.compile(result) result = self.compile(result)
ld_name = self.compile(name) ld_name = self.compile(name)
if isinstance(ld_name.expr, ast.Call): if isinstance(ld_name.expr, ast.Call):
raise HyTypeError(name, raise self._syntax_error(name,
"Can't assign to a callable: `%s'" % str_name) "Can't assign to a callable: `%s'" % str_name)
if (result.temp_variables if (result.temp_variables
@ -1474,7 +1492,8 @@ class HyASTCompiler(object):
mandatory, optional, rest, kwonly, kwargs = params mandatory, optional, rest, kwonly, kwargs = params
optional, defaults, ret = self._parse_optional_args(optional) optional, defaults, ret = self._parse_optional_args(optional)
if kwonly is not None and not PY3: 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) kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True)
ret += ret2 ret += ret2
main_args = mandatory + optional main_args = mandatory + optional
@ -1612,7 +1631,29 @@ class HyASTCompiler(object):
def compile_eval_and_compile(self, expr, root, body): def compile_eval_and_compile(self, expr, root, body):
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr) 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) return (self._compile_branch(body)
if ast_str(root) == "eval_and_compile" if ast_str(root) == "eval_and_compile"
@ -1627,8 +1668,8 @@ class HyASTCompiler(object):
return self.compile(expr) return self.compile(expr)
if not expr: if not expr:
raise HyTypeError( raise self._syntax_error(expr,
expr, "empty expressions are not allowed at top level") "empty expressions are not allowed at top level")
args = list(expr) args = list(expr)
root = args.pop(0) root = args.pop(0)
@ -1646,8 +1687,7 @@ class HyASTCompiler(object):
sroot in (mangle(","), mangle(".")) or sroot in (mangle(","), mangle(".")) or
not any(is_unpack("iterable", x) for x in args)): not any(is_unpack("iterable", x) for x in args)):
if sroot in _bad_roots: if sroot in _bad_roots:
raise HyTypeError( raise self._syntax_error(expr,
expr,
"The special form '{}' is not allowed here".format(root)) "The special form '{}' is not allowed here".format(root))
# `sroot` is a special operator. Get the build method and # `sroot` is a special operator. Get the build method and
# pattern-match the arguments. # pattern-match the arguments.
@ -1655,11 +1695,10 @@ class HyASTCompiler(object):
try: try:
parse_tree = pattern.parse(args) parse_tree = pattern.parse(args)
except NoParseError as e: except NoParseError as e:
raise HyTypeError( raise self._syntax_error(
expr[min(e.state.pos + 1, len(expr) - 1)], expr[min(e.state.pos + 1, len(expr) - 1)],
"parse error for special form '{}': {}".format( "parse error for special form '{}': {}".format(
root, root, e.msg.replace("<EOF>", "end of form")))
e.msg.replace("<EOF>", "end of form")))
return Result() + build_method( return Result() + build_method(
self, expr, unmangle(sroot), *parse_tree) self, expr, unmangle(sroot), *parse_tree)
@ -1681,13 +1720,13 @@ class HyASTCompiler(object):
FORM + FORM +
many(FORM)).parse(args) many(FORM)).parse(args)
except NoParseError: except NoParseError:
raise HyTypeError( raise self._syntax_error(expr,
expr, "attribute access requires object") "attribute access requires object")
# Reconstruct `args` to exclude `obj`. # Reconstruct `args` to exclude `obj`.
args = [x for p in kws for x in p] + list(rest) args = [x for p in kws for x in p] + list(rest)
if is_unpack("iterable", obj): if is_unpack("iterable", obj):
raise HyTypeError( raise self._syntax_error(obj,
obj, "can't call a method on an unpacking form") "can't call a method on an unpacking form")
func = self.compile(HyExpression( func = self.compile(HyExpression(
[HySymbol(".").replace(root), obj] + [HySymbol(".").replace(root), obj] +
attrs)) attrs))
@ -1725,16 +1764,12 @@ class HyASTCompiler(object):
glob, local = symbol.rsplit(".", 1) glob, local = symbol.rsplit(".", 1)
if not glob: if not glob:
raise HyTypeError(symbol, 'cannot access attribute on ' raise self._syntax_error(symbol,
'anything other than a name ' '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))
'(in order to get attributes of '
'expressions, use '
'`(. <expression> {attr})` or '
'`(.{attr} <expression>)`)'.format(
attr=local))
if not 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) glob = HySymbol(glob).replace(symbol)
ret = self.compile_symbol(glob) 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, 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. """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 Examples
-------- --------
=> (eval '(print "Hello World")) => (eval '(print "Hello World"))
@ -1816,8 +1856,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
Parameters Parameters
---------- ----------
hytree: a Hy expression tree hytree: HyObject
Source code to parse. The Hy AST object to evaluate.
locals: dict, optional locals: dict, optional
Local environment in which to evaluate the Hy tree. Defaults to the 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 An existing Hy compiler to use for compilation. Also serves as
the `module` value when given. 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 Returns
------- -------
out : Result of evaluating the Hy compiled tree. 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): if not isinstance(locals, dict):
raise TypeError("Locals must be a dictionary") raise TypeError("Locals must be a dictionary")
_ast, expr = hy_compile(hytree, module=module, get_expr=True, # Does the Hy AST object come with its own information?
compiler=compiler) filename = getattr(hytree, 'filename', filename) or '<string>'
source = getattr(hytree, 'source', source)
# Spoof the positions in the generated ast... _ast, expr = hy_compile(hytree, module, get_expr=True,
for node in ast.walk(_ast): compiler=compiler, filename=filename,
node.lineno = 1 source=source)
node.col_offset = 1
for node in ast.walk(expr):
node.lineno = 1
node.col_offset = 1
if ast_callback: if ast_callback:
ast_callback(_ast, expr) ast_callback(_ast, expr)
globals = module.__dict__
# Two-step eval: eval() the body of the exec call # 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 # 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, def _module_file_source(module_name, filename, source):
compiler=None): """Try to obtain missing filename and source information from a module name
"""Compile a Hy tree into a Python AST tree. 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 Parameters
---------- ----------
tree: HyObject
The Hy AST object to compile.
module: str or types.ModuleType, optional module: str or types.ModuleType, optional
Module, or name of the module, in which the Hy tree is evaluated. Module, or name of the module, in which the Hy tree is evaluated.
The module associated with `compiler` takes priority over this value. 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 An existing Hy compiler to use for compilation. Also serves as
the `module` value when given. 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 Returns
------- -------
out : A Python AST tree out : A Python AST tree
""" """
module = get_compiler_module(module, compiler, False) 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) tree = wrap_value(tree)
if not isinstance(tree, HyObject): if not isinstance(tree, HyObject):
raise HyCompileError("`tree` must be a HyObject or capable of " raise TypeError("`tree` must be a HyObject or capable of "
"being promoted to one") "being promoted to one")
compiler = compiler or HyASTCompiler(module) compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
result = compiler.compile(tree) result = compiler.compile(tree)
expr = result.force_expr expr = result.force_expr

View File

@ -14,15 +14,14 @@
(if* (not (isinstance macro-name hy.models.HySymbol)) (if* (not (isinstance macro-name hy.models.HySymbol))
(raise (raise
(hy.errors.HyTypeError (hy.errors.HyTypeError
macro-name
(% "received a `%s' instead of a symbol for macro name" (% "received a `%s' instead of a symbol for macro name"
(. (type name) (. (type name) __name__))
__name__))))) None --file-- None)))
(for [kw '[&kwonly &kwargs]] (for [kw '[&kwonly &kwargs]]
(if* (in kw lambda-list) (if* (in kw lambda-list)
(raise (hy.errors.HyTypeError macro-name (raise (hy.errors.HyTypeError (% "macros cannot use %s"
(% "macros cannot use %s" kw)
kw))))) macro-name --file-- None))))
;; this looks familiar... ;; this looks familiar...
`(eval-and-compile `(eval-and-compile
(import hy) (import hy)
@ -45,12 +44,12 @@
(if (and (not (isinstance tag-name hy.models.HySymbol)) (if (and (not (isinstance tag-name hy.models.HySymbol))
(not (isinstance tag-name hy.models.HyString))) (not (isinstance tag-name hy.models.HyString)))
(raise (hy.errors.HyTypeError (raise (hy.errors.HyTypeError
tag-name
(% "received a `%s' instead of a symbol for tag macro name" (% "received a `%s' instead of a symbol for tag macro name"
(. (type tag-name) __name__))))) (. (type tag-name) --name--))
tag-name --file-- None)))
(if (or (= tag-name ":") (if (or (= tag-name ":")
(= tag-name "&")) (= tag-name "&"))
(raise (NameError (% "%s can't be used as a tag macro name" tag-name)))) (raise (hy.errors.HyNameError (% "%s can't be used as a tag macro name" tag-name))))
(setv tag-name (.replace (hy.models.HyString tag-name) (setv tag-name (.replace (hy.models.HyString tag-name)
tag-name)) tag-name))
`(eval-and-compile `(eval-and-compile
@ -58,9 +57,8 @@
((hy.macros.tag ~tag-name) ((hy.macros.tag ~tag-name)
(fn ~lambda-list ~@body)))) (fn ~lambda-list ~@body))))
(defmacro macro-error [location reason] (defmacro macro-error [expression reason &optional [filename '--name--]]
"Error out properly within a macro at `location` giving `reason`." `(raise (hy.errors.HyMacroExpansionError ~reason ~filename ~expression None)))
`(raise (hy.errors.HyMacroExpansionError ~location ~reason)))
(defmacro defn [name lambda-list &rest body] (defmacro defn [name lambda-list &rest body]
"Define `name` as a function with `lambda-list` signature and body `body`." "Define `name` as a function with `lambda-list` signature and body `body`."

View File

@ -2,103 +2,307 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import os
import re
import sys
import traceback import traceback
import pkgutil
from functools import reduce
from contextlib import contextmanager
from hy import _initialize_env_var
from clint.textui import colored 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): class HyError(Exception):
"""
Generic Hy error. All internal Exceptions will be subclassed from this
Exception.
"""
pass pass
class HyCompileError(HyError): class HyInternalError(HyError):
def __init__(self, exception, traceback=None): """Unexpected errors occurring during compilation or parsing of Hy code.
self.exception = exception
self.traceback = traceback 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:
super(HyLanguageError, self).__init__(message)
def compute_lineinfo(self, expression, filename, source, lineno, colno):
# NOTE: We use `SyntaxError`'s field names (i.e. `text`, `offset`,
# `msg`) for compatibility and print-outs.
self.text = getattr(expression, 'source', source)
self.filename = getattr(expression, 'filename', filename)
if self.text:
lines = self.text.splitlines()
self.lineno = getattr(expression, 'start_line', lineno)
self.offset = getattr(expression, 'start_column', colno)
end_column = getattr(expression, 'end_column',
len(lines[self.lineno-1]))
end_line = getattr(expression, 'end_line', self.lineno)
# Trim the source down to the essentials.
self.text = '\n'.join(lines[self.lineno-1:end_line])
if end_column:
if self.lineno == end_line:
self.arrow_offset = end_column
else:
self.arrow_offset = len(self.text[0])
self.arrow_offset -= self.offset
else:
self.arrow_offset = None
else:
# We could attempt to extract the source given a filename, but we
# don't.
self.lineno = lineno
self.offset = colno
self.arrow_offset = None
def __str__(self): def __str__(self):
if isinstance(self.exception, HyTypeError): """Provide an exception message that includes SyntaxError-like source
return str(self.exception) line information when available.
if self.traceback:
tb = "".join(traceback.format_tb(self.traceback)).strip()
else:
tb = "No traceback available. 😟"
return("Internal Compiler Bug 😱\n%s: %s\nCompilation traceback:\n%s"
% (self.exception.__class__.__name__,
self.exception, tb))
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
def __str__(self):
result = ""
if all(getattr(self.expression, x, None) is not None
for x in ("start_line", "start_column", "end_column")):
line = self.expression.start_line
start = self.expression.start_column
end = self.expression.end_column
source = []
if self.source is not None:
source = self.source.split("\n")[line-1:self.expression.end_line]
if line == self.expression.end_line:
length = end - start
else:
length = len(source[0]) - start
result += ' File "%s", line %d, column %d\n\n' % (self.filename,
line,
start)
if len(source) == 1:
result += ' %s\n' % 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) + '^')
else:
result += ' File "%s", unknown location\n' % self.filename
result += colored.yellow("%s: %s\n\n" %
(self.__class__.__name__,
self.message))
return result
class HyMacroExpansionError(HyTypeError):
pass
class HyIOError(HyError, IOError):
""" """
Trivial subclass of IOError and HyError, to distinguish between global _hy_colored_errors
IOErrors raised by Hy itself as opposed to Hy programs.
# Syntax errors are special and annotate the traceback (instead of what
# we would do in the message that follows the traceback).
if isinstance(self, SyntaxError):
return super(HyLanguageError, self).__str__()
# When there isn't extra source information, use the normal message.
if not isinstance(self, SyntaxError) and not self.text:
return super(HyLanguageError, self).__str__()
# Re-purpose Python's builtin syntax error formatting.
output = traceback.format_exception_only(
SyntaxError,
SyntaxError(self.msg, (self.filename, self.lineno, self.offset,
self.text)))
arrow_idx, _ = next(((i, x) for i, x in enumerate(output)
if x.strip() == '^'),
(None, None))
if arrow_idx:
msg_idx = arrow_idx + 1
else:
msg_idx, _ = next((i, x) for i, x in enumerate(output)
if x.startswith('SyntaxError: '))
# Get rid of erroneous error-type label.
output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx])
# Extend the text arrow, when given enough source info.
if arrow_idx and self.arrow_offset:
output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'),
'-' * (self.arrow_offset - 1))
if _hy_colored_errors:
from clint.textui import colored
output[msg_idx:] = [colored.yellow(o) for o in output[msg_idx:]]
if arrow_idx:
output[arrow_idx] = colored.green(output[arrow_idx])
for idx, line in enumerate(output[::msg_idx]):
if line.strip().startswith(
'File "{}", line'.format(self.filename)):
output[idx] = colored.red(line)
# This resulting string will come after a "<class-name>:" prompt, so
# put it down a line.
output.insert(0, '\n')
# Avoid "...expected str instance, ColoredString found"
return reduce(lambda x, y: x + y, output)
class HyCompileError(HyInternalError):
"""Unexpected errors occurring within the compiler."""
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.
""" """
pass
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:
# 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 functools import partial
from contextlib import contextmanager from contextlib import contextmanager
from hy.errors import HyTypeError
from hy.compiler import hy_compile, hy_ast_compile_flags from hy.compiler import hy_compile, hy_ast_compile_flags
from hy.lex import hy_parse from hy.lex import hy_parse
from hy.lex.exceptions import LexException
from hy._compat import PY3 from hy._compat import PY3
@ -153,15 +151,9 @@ if PY3:
def _hy_source_to_code(self, data, path, _optimize=-1): def _hy_source_to_code(self, data, path, _optimize=-1):
if _could_be_hy_src(path): if _could_be_hy_src(path):
source = data.decode("utf-8") source = data.decode("utf-8")
try: hy_tree = hy_parse(source, filename=path)
hy_tree = hy_parse(source)
with loader_module_obj(self) as module: with loader_module_obj(self) as module:
data = hy_compile(hy_tree, module) data = hy_compile(hy_tree, module)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = source
e.filename = path
raise
return _py_source_to_code(self, data, path, _optimize=_optimize) return _py_source_to_code(self, data, path, _optimize=_optimize)
@ -287,19 +279,15 @@ else:
fullname = self._fix_name(fullname) fullname = self._fix_name(fullname)
if fullname is None: if fullname is None:
fullname = self.fullname fullname = self.fullname
try:
hy_source = self.get_source(fullname) hy_source = self.get_source(fullname)
hy_tree = hy_parse(hy_source) hy_tree = hy_parse(hy_source, filename=self.filename)
with loader_module_obj(self) as module: with loader_module_obj(self) as module:
hy_ast = hy_compile(hy_tree, module) hy_ast = hy_compile(hy_tree, module)
code = compile(hy_ast, self.filename, 'exec', code = compile(hy_ast, self.filename, 'exec',
hy_ast_compile_flags) hy_ast_compile_flags)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = hy_source
e.filename = self.filename
raise
if not sys.dont_write_bytecode: if not sys.dont_write_bytecode:
try: try:
@ -453,7 +441,7 @@ else:
try: try:
flags = None flags = None
if _could_be_hy_src(filename): if _could_be_hy_src(filename):
hy_tree = hy_parse(source_str) hy_tree = hy_parse(source_str, filename=filename)
if module is None: if module is None:
module = inspect.getmodule(inspect.stack()[1][0]) module = inspect.getmodule(inspect.stack()[1][0])
@ -465,9 +453,6 @@ else:
codeobject = compile(source, dfile or filename, 'exec', flags) codeobject = compile(source, dfile or filename, 'exec', flags)
except Exception as err: 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, py_exc = py_compile.PyCompileError(err.__class__, err,
dfile or filename) dfile or filename)

View File

@ -18,7 +18,7 @@ except ImportError:
from StringIO import StringIO from StringIO import StringIO
def hy_parse(source): def hy_parse(source, filename='<string>'):
"""Parse a Hy source string. """Parse a Hy source string.
Parameters Parameters
@ -26,31 +26,52 @@ def hy_parse(source):
source: string source: string
Source code to parse. Source code to parse.
filename: string, optional
File name corresponding to source. Defaults to "<string>".
Returns Returns
------- -------
out : instance of `types.CodeType` out : HyExpression
""" """
source = re.sub(r'\A#!.*', '', source) _source = re.sub(r'\A#!.*', '', source)
return HyExpression([HySymbol("do")] + tokenize(source + "\n")) res = HyExpression([HySymbol("do")] +
tokenize(_source + "\n",
filename=filename))
res.source = source
res.filename = filename
return res
def tokenize(buf): class ParserState(object):
""" def __init__(self, source, filename):
Tokenize a Lisp file or string buffer into internal Hy objects. 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.lexer import lexer
from hy.lex.parser import parser from hy.lex.parser import parser
from rply.errors import LexingError from rply.errors import LexingError
try: try:
return parser.parse(lexer.lex(buf)) return parser.parse(lexer.lex(source),
state=ParserState(source, filename))
except LexingError as e: except LexingError as e:
pos = e.getsourcepos() pos = e.getsourcepos()
raise LexException("Could not identify the next token.", 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: except LexException as e:
if e.source is None: raise e
e.source = buf
raise
mangle_delim = 'X' mangle_delim = 'X'

View File

@ -1,49 +1,39 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
from hy.errors import HySyntaxError
from hy.errors import HyError
class LexException(HyError): class LexException(HySyntaxError):
"""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>'
def __str__(self): @classmethod
from hy.errors import colored def from_lexer(cls, message, state, token):
lineno = None
colno = None
source = state.source
source_pos = token.getsourcepos()
line = self.lineno if source_pos:
start = self.colno lineno = source_pos.lineno
colno = source_pos.colno
result = "" elif source:
# Use the end of the last line of source for `PrematureEndOfInput`.
source = self.source.split("\n") # We get rid of empty lines and spaces so that the error matches
# with the last piece of visible code.
if line > 0 and start > 0: lines = source.rstrip().splitlines()
result += ' File "%s", line %d, column %d\n\n' % (self.filename, lineno = lineno or len(lines)
line, colno = colno or len(lines[lineno - 1])
start)
if len(self.source) > 0:
source_line = source[line-1]
else: else:
source_line = "" lineno = lineno or 1
colno = colno or 1
result += ' %s\n' % colored.red(source_line) return cls(message,
result += ' %s%s\n' % (' '*(start-1), colored.green('^')) None,
state.filename,
result += colored.yellow("LexException: %s\n\n" % self.message) source,
lineno,
return result colno)
class PrematureEndOfInput(LexException): class PrematureEndOfInput(LexException):
"""We got a premature end of input""" pass
def __init__(self, message):
super(PrematureEndOfInput, self).__init__(message, -1, -1)

View File

@ -6,7 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from functools import wraps from functools import wraps
import re, unicodedata
from rply import ParserGenerator from rply import ParserGenerator
@ -22,10 +21,10 @@ pg = ParserGenerator([rule.name for rule in lexer.rules] + ['$end'])
def set_boundaries(fun): def set_boundaries(fun):
@wraps(fun) @wraps(fun)
def wrapped(p): def wrapped(state, p):
start = p[0].source_pos start = p[0].source_pos
end = p[-1].source_pos end = p[-1].source_pos
ret = fun(p) ret = fun(state, p)
ret.start_line = start.lineno ret.start_line = start.lineno
ret.start_column = start.colno ret.start_column = start.colno
if start is not end: if start is not end:
@ -40,9 +39,9 @@ def set_boundaries(fun):
def set_quote_boundaries(fun): def set_quote_boundaries(fun):
@wraps(fun) @wraps(fun)
def wrapped(p): def wrapped(state, p):
start = p[0].source_pos start = p[0].source_pos
ret = fun(p) ret = fun(state, p)
ret.start_line = start.lineno ret.start_line = start.lineno
ret.start_column = start.colno ret.start_column = start.colno
ret.end_line = p[-1].end_line ret.end_line = p[-1].end_line
@ -52,54 +51,45 @@ def set_quote_boundaries(fun):
@pg.production("main : list_contents") @pg.production("main : list_contents")
def main(p): def main(state, p):
return p[0] return p[0]
@pg.production("main : $end") @pg.production("main : $end")
def main_empty(p): def main_empty(state, p):
return [] 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") @pg.production("paren : LPAREN list_contents RPAREN")
@set_boundaries @set_boundaries
def paren(p): def paren(state, p):
return HyExpression(p[1]) return HyExpression(p[1])
@pg.production("paren : LPAREN RPAREN") @pg.production("paren : LPAREN RPAREN")
@set_boundaries @set_boundaries
def empty_paren(p): def empty_paren(state, p):
return HyExpression([]) return HyExpression([])
@pg.production("list_contents : term list_contents") @pg.production("list_contents : term list_contents")
def list_contents(p): def list_contents(state, p):
return [p[0]] + p[1] return [p[0]] + p[1]
@pg.production("list_contents : term") @pg.production("list_contents : term")
def list_contents_single(p): def list_contents_single(state, p):
return [p[0]] return [p[0]]
@pg.production("list_contents : DISCARD term discarded_list_contents") @pg.production("list_contents : DISCARD term discarded_list_contents")
def list_contents_empty(p): def list_contents_empty(state, p):
return [] return []
@pg.production("discarded_list_contents : DISCARD term discarded_list_contents") @pg.production("discarded_list_contents : DISCARD term discarded_list_contents")
@pg.production("discarded_list_contents :") @pg.production("discarded_list_contents :")
def discarded_list_contents(p): def discarded_list_contents(state, p):
pass pass
@ -109,58 +99,58 @@ def discarded_list_contents(p):
@pg.production("term : list") @pg.production("term : list")
@pg.production("term : set") @pg.production("term : set")
@pg.production("term : string") @pg.production("term : string")
def term(p): def term(state, p):
return p[0] return p[0]
@pg.production("term : DISCARD term term") @pg.production("term : DISCARD term term")
def term_discard(p): def term_discard(state, p):
return p[2] return p[2]
@pg.production("term : QUOTE term") @pg.production("term : QUOTE term")
@set_quote_boundaries @set_quote_boundaries
def term_quote(p): def term_quote(state, p):
return HyExpression([HySymbol("quote"), p[1]]) return HyExpression([HySymbol("quote"), p[1]])
@pg.production("term : QUASIQUOTE term") @pg.production("term : QUASIQUOTE term")
@set_quote_boundaries @set_quote_boundaries
def term_quasiquote(p): def term_quasiquote(state, p):
return HyExpression([HySymbol("quasiquote"), p[1]]) return HyExpression([HySymbol("quasiquote"), p[1]])
@pg.production("term : UNQUOTE term") @pg.production("term : UNQUOTE term")
@set_quote_boundaries @set_quote_boundaries
def term_unquote(p): def term_unquote(state, p):
return HyExpression([HySymbol("unquote"), p[1]]) return HyExpression([HySymbol("unquote"), p[1]])
@pg.production("term : UNQUOTESPLICE term") @pg.production("term : UNQUOTESPLICE term")
@set_quote_boundaries @set_quote_boundaries
def term_unquote_splice(p): def term_unquote_splice(state, p):
return HyExpression([HySymbol("unquote-splice"), p[1]]) return HyExpression([HySymbol("unquote-splice"), p[1]])
@pg.production("term : HASHSTARS term") @pg.production("term : HASHSTARS term")
@set_quote_boundaries @set_quote_boundaries
def term_hashstars(p): def term_hashstars(state, p):
n_stars = len(p[0].getstr()[1:]) n_stars = len(p[0].getstr()[1:])
if n_stars == 1: if n_stars == 1:
sym = "unpack-iterable" sym = "unpack-iterable"
elif n_stars == 2: elif n_stars == 2:
sym = "unpack-mapping" sym = "unpack-mapping"
else: else:
raise LexException( raise LexException.from_lexer(
"Too many stars in `#*` construct (if you want to unpack a symbol " "Too many stars in `#*` construct (if you want to unpack a symbol "
"beginning with a star, separate it with whitespace)", "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]]) return HyExpression([HySymbol(sym), p[1]])
@pg.production("term : HASHOTHER term") @pg.production("term : HASHOTHER term")
@set_quote_boundaries @set_quote_boundaries
def hash_other(p): def hash_other(state, p):
# p == [(Token('HASHOTHER', '#foo'), bar)] # p == [(Token('HASHOTHER', '#foo'), bar)]
st = p[0].getstr()[1:] st = p[0].getstr()[1:]
str_object = HyString(st) str_object = HyString(st)
@ -170,63 +160,63 @@ def hash_other(p):
@pg.production("set : HLCURLY list_contents RCURLY") @pg.production("set : HLCURLY list_contents RCURLY")
@set_boundaries @set_boundaries
def t_set(p): def t_set(state, p):
return HySet(p[1]) return HySet(p[1])
@pg.production("set : HLCURLY RCURLY") @pg.production("set : HLCURLY RCURLY")
@set_boundaries @set_boundaries
def empty_set(p): def empty_set(state, p):
return HySet([]) return HySet([])
@pg.production("dict : LCURLY list_contents RCURLY") @pg.production("dict : LCURLY list_contents RCURLY")
@set_boundaries @set_boundaries
def t_dict(p): def t_dict(state, p):
return HyDict(p[1]) return HyDict(p[1])
@pg.production("dict : LCURLY RCURLY") @pg.production("dict : LCURLY RCURLY")
@set_boundaries @set_boundaries
def empty_dict(p): def empty_dict(state, p):
return HyDict([]) return HyDict([])
@pg.production("list : LBRACKET list_contents RBRACKET") @pg.production("list : LBRACKET list_contents RBRACKET")
@set_boundaries @set_boundaries
def t_list(p): def t_list(state, p):
return HyList(p[1]) return HyList(p[1])
@pg.production("list : LBRACKET RBRACKET") @pg.production("list : LBRACKET RBRACKET")
@set_boundaries @set_boundaries
def t_empty_list(p): def t_empty_list(state, p):
return HyList([]) return HyList([])
@pg.production("string : STRING") @pg.production("string : STRING")
@set_boundaries @set_boundaries
def t_string(p): def t_string(state, p):
# Replace the single double quotes with triple double quotes to allow # Replace the single double quotes with triple double quotes to allow
# embedded newlines. # embedded newlines.
try: try:
s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""') s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""')
except SyntaxError: except SyntaxError:
raise LexException("Can't convert {} to a HyString".format(p[0].value), raise LexException.from_lexer("Can't convert {} to a HyString".format(p[0].value),
p[0].source_pos.lineno, p[0].source_pos.colno) state, p[0])
return (HyString if isinstance(s, str_type) else HyBytes)(s) return (HyString if isinstance(s, str_type) else HyBytes)(s)
@pg.production("string : PARTIAL_STRING") @pg.production("string : PARTIAL_STRING")
def t_partial_string(p): def t_partial_string(state, p):
# Any unterminated string requires more input # 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') bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING')
@pg.production("string : BRACKETSTRING") @pg.production("string : BRACKETSTRING")
@set_boundaries @set_boundaries
def t_bracket_string(p): def t_bracket_string(state, p):
m = bracket_string_re.match(p[0].value) m = bracket_string_re.match(p[0].value)
delim, content = m.groups() delim, content = m.groups()
return HyString(content, brackets=delim) return HyString(content, brackets=delim)
@ -234,7 +224,7 @@ def t_bracket_string(p):
@pg.production("identifier : IDENTIFIER") @pg.production("identifier : IDENTIFIER")
@set_boundaries @set_boundaries
def t_identifier(p): def t_identifier(state, p):
obj = p[0].value obj = p[0].value
val = symbol_like(obj) 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: if "." in obj and symbol_like(obj.split(".", 1)[0]) is not None:
# E.g., `5.attr` or `:foo.attr` # E.g., `5.attr` or `:foo.attr`
raise LexException( raise LexException.from_lexer(
'Cannot access attribute on anything other than a name (in ' 'Cannot access attribute on anything other than a name (in '
'order to get attributes of expressions, use ' 'order to get attributes of expressions, use '
'`(. <expression> <attr>)` or `(.<attr> <expression>)`)', '`(. <expression> <attr>)` or `(.<attr> <expression>)`)',
p[0].source_pos.lineno, p[0].source_pos.colno) state, p[0])
return HySymbol(obj) return HySymbol(obj)
@ -284,14 +274,15 @@ def symbol_like(obj):
@pg.error @pg.error
def error_handler(token): def error_handler(state, token):
tokentype = token.gettokentype() tokentype = token.gettokentype()
if tokentype == '$end': if tokentype == '$end':
raise PrematureEndOfInput("Premature end of input") raise PrematureEndOfInput.from_lexer("Premature end of input", state,
token)
else: else:
raise LexException( raise LexException.from_lexer(
"Ran into a %s where it wasn't expected." % tokentype, "Ran into a %s where it wasn't expected." % tokentype, state,
token.source_pos.lineno, token.source_pos.colno) token)
parser = pg.build() parser = pg.build()

View File

@ -1,15 +1,19 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import sys
import importlib import importlib
import inspect import inspect
import pkgutil 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.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle from hy.lex import mangle
from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
from hy.errors import HyTypeError, HyMacroExpansionError HyRequireError)
try: try:
# Check if we have the newer inspect.signature available. # Check if we have the newer inspect.signature available.
@ -48,7 +52,7 @@ def macro(name):
""" """
name = mangle(name) name = mangle(name)
def _(fn): def _(fn):
fn.__name__ = '({})'.format(name) fn = rename_function(fn, name)
try: try:
fn._hy_macro_pass_compiler = has_kwargs(fn) fn._hy_macro_pass_compiler = has_kwargs(fn)
except Exception: except Exception:
@ -73,7 +77,7 @@ def tag(name):
if not PY3: if not PY3:
_name = _name.encode('UTF-8') _name = _name.encode('UTF-8')
fn.__name__ = _name fn = rename_function(fn, _name)
module = inspect.getmodule(fn) module = inspect.getmodule(fn)
@ -148,7 +152,6 @@ def require(source_module, target_module, assignments, prefix=""):
out: boolean out: boolean
Whether or not macros and tags were actually transferred. Whether or not macros and tags were actually transferred.
""" """
if target_module is None: if target_module is None:
parent_frame = inspect.stack()[1][0] parent_frame = inspect.stack()[1][0]
target_namespace = parent_frame.f_globals target_namespace = parent_frame.f_globals
@ -159,7 +162,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif inspect.ismodule(target_module): elif inspect.ismodule(target_module):
target_namespace = target_module.__dict__ target_namespace = target_module.__dict__
else: else:
raise TypeError('`target_module` is not a recognized type: {}'.format( raise HyTypeError('`target_module` is not a recognized type: {}'.format(
type(target_module))) type(target_module)))
# Let's do a quick check to make sure the source module isn't actually # Let's do a quick check to make sure the source module isn't actually
@ -171,14 +174,17 @@ def require(source_module, target_module, assignments, prefix=""):
return False return False
if not inspect.ismodule(source_module): if not inspect.ismodule(source_module):
try:
source_module = importlib.import_module(source_module) source_module = importlib.import_module(source_module)
except ImportError as e:
reraise(HyRequireError, HyRequireError(e.args[0]), None)
source_macros = source_module.__dict__.setdefault('__macros__', {}) source_macros = source_module.__dict__.setdefault('__macros__', {})
source_tags = source_module.__dict__.setdefault('__tags__', {}) source_tags = source_module.__dict__.setdefault('__tags__', {})
if len(source_module.__macros__) + len(source_module.__tags__) == 0: if len(source_module.__macros__) + len(source_module.__tags__) == 0:
if assignments != "ALL": if assignments != "ALL":
raise ImportError('The module {} has no macros or tags'.format( raise HyRequireError('The module {} has no macros or tags'.format(
source_module)) source_module))
else: else:
return False return False
@ -203,7 +209,7 @@ def require(source_module, target_module, assignments, prefix=""):
elif _name in source_module.__tags__: elif _name in source_module.__tags__:
target_tags[alias] = source_tags[_name] target_tags[alias] = source_tags[_name]
else: else:
raise ImportError('Could not require name {} from {}'.format( raise HyRequireError('Could not require name {} from {}'.format(
_name, source_module)) _name, source_module))
return True return True
@ -237,24 +243,33 @@ def load_macros(module):
if k not in module_tags}) if k not in module_tags})
def make_empty_fn_copy(fn): @contextmanager
def macro_exceptions(module, macro_tree, compiler=None):
try: try:
# This might fail if fn has parameters with funny names, like o!n. In yield
# such a case, we return a generic function that ensures the program except HyLanguageError as e:
# can continue running. Unfortunately, the error message that might get # These are user-level Hy errors occurring in the macro.
# raised later on while expanding a macro might not make sense at all. # 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) if compiler:
fn_str = 'lambda {}: None'.format( filename = compiler.filename
formatted_args.lstrip('(').rstrip(')')) source = compiler.source
empty_fn = eval(fn_str) 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): msg = "expanding macro {}\n ".format(str(macro_tree[0]))
None 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): def macroexpand(tree, module, compiler=None, once=False):
@ -324,23 +339,8 @@ def macroexpand(tree, module, compiler=None, once=False):
compiler = HyASTCompiler(module) compiler = HyASTCompiler(module)
opts['compiler'] = compiler opts['compiler'] = compiler
try: with macro_exceptions(module, tree, compiler):
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:
obj = m(module.__name__, *tree[1:], **opts) 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): if isinstance(obj, HyExpression):
obj.module = inspect.getmodule(m) obj.module = inspect.getmodule(m)
@ -375,7 +375,8 @@ def tag_macroexpand(tag, tree, module):
None) None)
if tag_macro is 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) expr = tag_macro(tree)

View File

@ -1,16 +1,18 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
from __future__ import unicode_literals from __future__ import unicode_literals
from contextlib import contextmanager from contextlib import contextmanager
from math import isnan, isinf 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._compat import PY3, str_type, bytes_type, long_type, string_types
from hy.errors import HyWrapperError
from fractions import Fraction from fractions import Fraction
from clint.textui import colored from clint.textui import colored
PRETTY = True PRETTY = True
_hy_colored_ast_objects = _initialize_env_var('HY_COLORED_AST_OBJECTS', False)
@contextmanager @contextmanager
@ -63,7 +65,7 @@ def wrap_value(x):
new = _wrappers.get(type(x), lambda y: y)(x) new = _wrappers.get(type(x), lambda y: y)(x)
if not isinstance(new, HyObject): 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): if isinstance(x, HyObject):
new = new.replace(x, recursive=False) new = new.replace(x, recursive=False)
if not hasattr(new, "start_column"): if not hasattr(new, "start_column"):
@ -271,8 +273,9 @@ class HySequence(HyObject, list):
return str(self) if PRETTY else super(HySequence, self).__repr__() return str(self) if PRETTY else super(HySequence, self).__repr__()
def __str__(self): def __str__(self):
global _hy_colored_ast_objects
with pretty(): with pretty():
c = self.color c = self.color if _hy_colored_ast_objects else str
if self: if self:
return ("{}{}\n {}{}").format( return ("{}{}\n {}{}").format(
c(self.__class__.__name__), c(self.__class__.__name__),
@ -298,10 +301,12 @@ class HyDict(HySequence):
""" """
HyDict (just a representation of a dict) HyDict (just a representation of a dict)
""" """
color = staticmethod(colored.green)
def __str__(self): def __str__(self):
global _hy_colored_ast_objects
with pretty(): with pretty():
g = colored.green g = self.color if _hy_colored_ast_objects else str
if self: if self:
pairs = [] pairs = []
for k, v in zip(self[::2],self[1::2]): for k, v in zip(self[::2],self[1::2]):

View File

@ -6,11 +6,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from hy import HyString from hy import HyString
from hy.models import HyObject
from hy.compiler import hy_compile, hy_eval from hy.compiler import hy_compile, hy_eval
from hy.errors import HyCompileError, HyTypeError from hy.errors import HyCompileError, HyLanguageError, HyError
from hy.lex import hy_parse from hy.lex import hy_parse
from hy.lex.exceptions import LexException from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3 from hy._compat import PY3
import ast import ast
@ -27,7 +26,7 @@ def _ast_spotcheck(arg, root, secondary):
def can_compile(expr): def can_compile(expr):
return hy_compile(hy_parse(expr), "__main__") return hy_compile(hy_parse(expr), __name__)
def can_eval(expr): def can_eval(expr):
@ -35,21 +34,16 @@ def can_eval(expr):
def cant_compile(expr): def cant_compile(expr):
try: with pytest.raises(HyError) as excinfo:
hy_compile(hy_parse(expr), "__main__") hy_compile(hy_parse(expr), __name__)
assert False
except HyTypeError as e: if issubclass(excinfo.type, HyLanguageError):
assert excinfo.value.msg
return excinfo.value
elif issubclass(excinfo.type, HyCompileError):
# Anything that can't be compiled should raise a user friendly # Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug. # error, otherwise it's a compiler bug.
assert isinstance(e.expression, HyObject) return excinfo.value
assert e.message
return e
except HyCompileError as e:
# Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug.
assert isinstance(e.exception, HyTypeError)
assert e.traceback
return e
def s(x): def s(x):
@ -60,11 +54,9 @@ def test_ast_bad_type():
"Make sure AST breakage can happen" "Make sure AST breakage can happen"
class C: class C:
pass pass
try:
hy_compile(C(), "__main__") with pytest.raises(TypeError):
assert True is False hy_compile(C(), __name__, filename='<string>', source='')
except TypeError:
pass
def test_empty_expr(): def test_empty_expr():
@ -473,8 +465,8 @@ def test_lambda_list_keywords_kwonly():
assert code.body[0].args.kw_defaults[1].n == 2 assert code.body[0].args.kw_defaults[1].n == 2
else: else:
exception = cant_compile(kwonly_demo) exception = cant_compile(kwonly_demo)
assert isinstance(exception, HyTypeError) assert isinstance(exception, HyLanguageError)
message, = exception.args message = exception.args[0]
assert message == "&kwonly parameters require Python 3" assert message == "&kwonly parameters require Python 3"
@ -489,9 +481,9 @@ def test_lambda_list_keywords_mixed():
def test_missing_keyword_argument_value(): def test_missing_keyword_argument_value():
"""Ensure the compiler chokes on missing keyword argument values.""" """Ensure the compiler chokes on missing keyword argument values."""
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyLanguageError) as excinfo:
can_compile("((fn [x] x) :x)") can_compile("((fn [x] x) :x)")
assert excinfo.value.message == "Keyword argument :x needs a value." assert excinfo.value.msg == "Keyword argument :x needs a value."
def test_ast_unicode_strings(): def test_ast_unicode_strings():
@ -500,7 +492,7 @@ def test_ast_unicode_strings():
def _compile_string(s): def _compile_string(s):
hy_s = HyString(s) hy_s = HyString(s)
code = hy_compile([hy_s], "__main__") code = hy_compile([hy_s], __name__, filename='<string>', source=s)
# We put hy_s in a list so it isn't interpreted as a docstring. # We put hy_s in a list so it isn't interpreted as a docstring.
# code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))]) # code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))])
@ -541,19 +533,19 @@ Only one leading newline should be removed.
def test_compile_error(): def test_compile_error():
"""Ensure we get compile error in tricky cases""" """Ensure we get compile error in tricky cases"""
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyLanguageError) as excinfo:
can_compile("(fn [] (in [1 2 3]))") can_compile("(fn [] (in [1 2 3]))")
def test_for_compile_error(): def test_for_compile_error():
"""Ensure we get compile error in tricky 'for' cases""" """Ensure we get compile error in tricky 'for' cases"""
with pytest.raises(LexException) as excinfo: with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)") can_compile("(fn [] (for)")
assert excinfo.value.message == "Premature end of input" assert excinfo.value.msg == "Premature end of input"
with pytest.raises(LexException) as excinfo: with pytest.raises(LexException) as excinfo:
can_compile("(fn [] (for)))") can_compile("(fn [] (for)))")
assert excinfo.value.message == "Ran into a RPAREN where it wasn't expected." assert excinfo.value.msg == "Ran into a RPAREN where it wasn't expected."
cant_compile("(fn [] (for [x] x))") cant_compile("(fn [] (for [x] x))")
@ -605,13 +597,13 @@ def test_setv_builtins():
def test_top_level_unquote(): def test_top_level_unquote():
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyLanguageError) as excinfo:
can_compile("(unquote)") can_compile("(unquote)")
assert excinfo.value.message == "The special form 'unquote' is not allowed here" assert excinfo.value.msg == "The special form 'unquote' is not allowed here"
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyLanguageError) as excinfo:
can_compile("(unquote-splice)") can_compile("(unquote-splice)")
assert excinfo.value.message == "The special form 'unquote-splice' is not allowed here" assert excinfo.value.msg == "The special form 'unquote-splice' is not allowed here"
def test_lots_of_comment_lines(): def test_lots_of_comment_lines():

View File

@ -14,10 +14,10 @@ from fractions import Fraction
import pytest import pytest
import hy import hy
from hy.errors import HyTypeError
from hy.lex import hy_parse from hy.lex import hy_parse
from hy.lex.exceptions import LexException from hy.errors import HyLanguageError
from hy.compiler import hy_compile from hy.lex.exceptions import PrematureEndOfInput
from hy.compiler import hy_eval, hy_compile
from hy.importer import HyLoader, cache_from_source from hy.importer import HyLoader, cache_from_source
try: try:
@ -57,7 +57,7 @@ def test_runpy():
def test_stringer(): 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 assert type(_ast.body[0]) == ast.FunctionDef
@ -79,14 +79,8 @@ def test_imports():
def test_import_error_reporting(): def test_import_error_reporting():
"Make sure that (import) reports errors correctly." "Make sure that (import) reports errors correctly."
def _import_error_test(): with pytest.raises(HyLanguageError):
try: hy_compile(hy_parse("(import \"sys\")"), __name__)
_ = 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
def test_import_error_cleanup(): def test_import_error_cleanup():
@ -124,7 +118,7 @@ def test_import_autocompiles():
def test_eval(): def test_eval():
def eval_str(s): 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('[1 2 3]') == [1, 2, 3]
assert eval_str('{"dog" "bark" "cat" "meow"}') == { assert eval_str('{"dog" "bark" "cat" "meow"}') == {
@ -205,8 +199,7 @@ def test_reload():
assert mod.a == 11 assert mod.a == 11
assert mod.b == 20 assert mod.b == 20
# Now cause a `LexException`, and confirm that the good module and its # Now cause a syntax error
# contents stick around.
unlink(source) unlink(source)
with open(source, "w") as f: with open(source, "w") as f:
@ -214,7 +207,7 @@ def test_reload():
f.write("(setv a 11") f.write("(setv a 11")
f.write("(setv b (// 20 1))") f.write("(setv b (// 20 1))")
with pytest.raises(LexException): with pytest.raises(PrematureEndOfInput):
reload(mod) reload(mod)
mod = sys.modules.get(TESTFN) mod = sys.modules.get(TESTFN)

View File

@ -50,8 +50,7 @@ def test_preprocessor_exceptions():
""" Test that macro expansion raises appropriate exceptions""" """ Test that macro expansion raises appropriate exceptions"""
with pytest.raises(HyMacroExpansionError) as excinfo: with pytest.raises(HyMacroExpansionError) as excinfo:
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__)) macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
assert "_hy_anon_fn_" not in excinfo.value.message assert "_hy_anon_" not in excinfo.value.msg
assert "TypeError" not in excinfo.value.message
def test_macroexpand_nan(): def test_macroexpand_nan():

View File

@ -687,5 +687,5 @@ result['y in globals'] = 'y' in globals()")
(doc doc) (doc doc)
(setv out_err (.readouterr capsys)) (setv out_err (.readouterr capsys))
(assert (.startswith (.strip (first out_err)) (assert (.startswith (.strip (first out_err))
"Help on function (doc) in module hy.core.macros:")) "Help on function doc in module hy.core.macros:"))
(assert (empty? (second out_err)))) (assert (empty? (second out_err))))

View File

@ -7,7 +7,7 @@
[sys :as systest] [sys :as systest]
re re
[operator [or_]] [operator [or_]]
[hy.errors [HyTypeError]] [hy.errors [HyLanguageError]]
pytest) pytest)
(import sys) (import sys)
@ -68,16 +68,16 @@
"NATIVE: test that setv doesn't work on names Python can't assign to "NATIVE: test that setv doesn't work on names Python can't assign to
and that we can't mangle" and that we can't mangle"
(try (eval '(setv None 1)) (try (eval '(setv None 1))
(except [e [TypeError]] (assert (in "Can't assign to" (str e))))) (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(defn None [] (print "hello"))) (try (eval '(defn None [] (print "hello")))
(except [e [TypeError]] (assert (in "Can't assign to" (str e))))) (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(when PY3 (when PY3
(try (eval '(setv False 1)) (try (eval '(setv False 1))
(except [e [TypeError]] (assert (in "Can't assign to" (str e))))) (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(setv True 0)) (try (eval '(setv True 0))
(except [e [TypeError]] (assert (in "Can't assign to" (str e))))) (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
(try (eval '(defn True [] (print "hello"))) (try (eval '(defn True [] (print "hello")))
(except [e [TypeError]] (assert (in "Can't assign to" (str e))))))) (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))))
(defn test-setv-pairs [] (defn test-setv-pairs []
@ -87,7 +87,7 @@
(assert (= b 2)) (assert (= b 2))
(setv y 0 x 1 y x) (setv y 0 x 1 y x)
(assert (= y 1)) (assert (= y 1))
(with [(pytest.raises HyTypeError)] (with [(pytest.raises HyLanguageError)]
(eval '(setv a 1 b)))) (eval '(setv a 1 b))))
@ -144,29 +144,29 @@
(do (do
(eval '(setv (do 1 2) 1)) (eval '(setv (do 1 2) 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a non-expression")))) (assert (= e.msg "Can't assign or delete a non-expression"))))
(try (try
(do (do
(eval '(setv 1 1)) (eval '(setv 1 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyInteger")))) (assert (= e.msg "Can't assign or delete a HyInteger"))))
(try (try
(do (do
(eval '(setv {1 2} 1)) (eval '(setv {1 2} 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyDict")))) (assert (= e.msg "Can't assign or delete a HyDict"))))
(try (try
(do (do
(eval '(del 1 1)) (eval '(del 1 1))
(assert False)) (assert False))
(except [e HyTypeError] (except [e HyLanguageError]
(assert (= e.message "Can't assign or delete a HyInteger"))))) (assert (= e.msg "Can't assign or delete a HyInteger")))))
(defn test-no-str-as-sym [] (defn test-no-str-as-sym []

View File

@ -3,7 +3,7 @@
;; license. See the LICENSE. ;; license. See the LICENSE.
(import pytest (import pytest
[hy.errors [HyTypeError]]) [hy.errors [HyTypeError HyMacroExpansionError]])
(defmacro rev [&rest body] (defmacro rev [&rest body]
"Execute the `body` statements in reverse" "Execute the `body` statements in reverse"
@ -66,13 +66,13 @@
(try (try
(eval '(defmacro f [&kwonly a b])) (eval '(defmacro f [&kwonly a b]))
(except [e HyTypeError] (except [e HyTypeError]
(assert (= e.message "macros cannot use &kwonly"))) (assert (= e.msg "macros cannot use &kwonly")))
(else (assert False))) (else (assert False)))
(try (try
(eval '(defmacro f [&kwargs kw])) (eval '(defmacro f [&kwargs kw]))
(except [e HyTypeError] (except [e HyTypeError]
(assert (= e.message "macros cannot use &kwargs"))) (assert (= e.msg "macros cannot use &kwargs")))
(else (assert False)))) (else (assert False))))
(defn test-fn-calling-macro [] (defn test-fn-calling-macro []
@ -162,8 +162,8 @@
") ")
;; expand the macro twice, should use a different ;; expand the macro twice, should use a different
;; gensym each time ;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) "foo")) (setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) "foo")) (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1)) (setv s1 (to_source _ast1))
(setv s2 (to_source _ast2)) (setv s2 (to_source _ast2))
;; and make sure there is something new that starts with _;G| ;; and make sure there is something new that starts with _;G|
@ -189,8 +189,8 @@
") ")
;; expand the macro twice, should use a different ;; expand the macro twice, should use a different
;; gensym each time ;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) "foo")) (setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) "foo")) (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1)) (setv s1 (to_source _ast1))
(setv s2 (to_source _ast2)) (setv s2 (to_source _ast2))
(assert (in (mangle "_;a|") s1)) (assert (in (mangle "_;a|") s1))
@ -213,8 +213,8 @@
") ")
;; expand the macro twice, should use a different ;; expand the macro twice, should use a different
;; gensym each time ;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) "foo")) (setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) "foo")) (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1)) (setv s1 (to_source _ast1))
(setv s2 (to_source _ast2)) (setv s2 (to_source _ast2))
(assert (in (mangle "_;res|") s1)) (assert (in (mangle "_;res|") s1))
@ -224,7 +224,7 @@
;; defmacro/g! didn't like numbers initially because they ;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion ;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))") (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! [] (defn test-defmacro! []
;; defmacro! must do everything defmacro/g! can ;; defmacro! must do everything defmacro/g! can
@ -243,8 +243,8 @@
") ")
;; expand the macro twice, should use a different ;; expand the macro twice, should use a different
;; gensym each time ;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) "foo")) (setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) "foo")) (setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1)) (setv s1 (to_source _ast1))
(setv s2 (to_source _ast2)) (setv s2 (to_source _ast2))
(assert (in (mangle "_;res|") s1)) (assert (in (mangle "_;res|") s1))
@ -254,7 +254,7 @@
;; defmacro/g! didn't like numbers initially because they ;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion ;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))") (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)) (defmacro! foo! [o!foo] `(do ~g!foo ~g!foo))
;; test that o! becomes g! ;; test that o! becomes g!
@ -483,3 +483,37 @@ in expansions."
(test-macro) (test-macro)
(assert (= blah 1))) (assert (= blah 1)))
(defn test-macro-errors []
(import traceback
[hy.importer [hy-parse]])
(setv test-expr (hy-parse "(defmacro blah [x] `(print ~@z)) (blah y)"))
(with [excinfo (pytest.raises HyMacroExpansionError)]
(eval test-expr))
(setv output (traceback.format_exception_only
excinfo.type excinfo.value))
(setv output (cut (.splitlines (.strip (first output))) 1))
(setv expected [" File \"<string>\", line 1"
" (defmacro blah [x] `(print ~@z)) (blah y)"
" ^------^"
"expanding macro blah"
" NameError: global name 'z' is not defined"])
(assert (= (cut expected 0 -1) (cut output 0 -1)))
(assert (or (= (get expected -1) (get output -1))
;; Handle PyPy's peculiarities
(= (.replace (get expected -1) "global " "") (get output -1))))
;; 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] (defmacro forbid [expr]
`(assert (try `(assert (try
(eval '~expr) (eval '~expr)
(except [TypeError] True) (except [[TypeError SyntaxError]] True)
(else (raise AssertionError))))) (else (raise AssertionError)))))

View File

@ -6,11 +6,11 @@
import os import os
import re import re
import sys
import shlex import shlex
import subprocess import subprocess
from hy.importer import cache_from_source from hy.importer import cache_from_source
from hy._compat import PY3
import pytest import pytest
@ -123,7 +123,16 @@ def test_bin_hy_stdin_as_arrow():
def test_bin_hy_stdin_error_underline_alignment(): def test_bin_hy_stdin_error_underline_alignment():
_, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)") _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
assert "\n (mabcdefghi)\n ^----------^" in err
msg_idx = err.rindex(" (mabcdefghi)")
assert msg_idx
err_parts = err[msg_idx:].splitlines()
assert err_parts[1].startswith(" ^----------^")
assert err_parts[2].startswith("expanding macro mabcdefghi")
assert (err_parts[3].startswith(" TypeError: mabcdefghi") or
# PyPy can use a function's `__name__` instead of
# `__code__.co_name`.
err_parts[3].startswith(" TypeError: (mabcdefghi)"))
def test_bin_hy_stdin_except_do(): def test_bin_hy_stdin_except_do():
@ -149,10 +158,66 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
# inside run_cmd. # inside run_cmd.
_, err = run_cmd("hy", """ _, err = run_cmd("hy", """
(import hy.errors) (import hy.errors)
(raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""") (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
assert "AZ" in err assert "AZ" in err
def test_bin_hy_error_parts_length():
"""Confirm that exception messages print arrows surrounding the affected
expression."""
prg_str = """
(import hy.errors
[hy.importer [hy-parse]])
(setv test-expr (hy-parse "(+ 1\n\n'a 2 3\n\n 1)"))
(setv test-expr.start-line {})
(setv test-expr.start-column {})
(setv test-expr.end-column {})
(raise (hy.errors.HyLanguageError
"this\nis\na\nmessage"
test-expr
None
None))
"""
# Up-arrows right next to each other.
_, err = run_cmd("hy", prg_str.format(3, 1, 2))
msg_idx = err.rindex("HyLanguageError:")
assert msg_idx
err_parts = err[msg_idx:].splitlines()[1:]
expected = [' File "<string>", line 3',
' \'a 2 3',
' ^^',
'this',
'is',
'a',
'message']
for obs, exp in zip(err_parts, expected):
assert obs.startswith(exp)
# Make sure only one up-arrow is printed
_, err = run_cmd("hy", prg_str.format(3, 1, 1))
msg_idx = err.rindex("HyLanguageError:")
assert msg_idx
err_parts = err[msg_idx:].splitlines()[1:]
assert err_parts[2] == ' ^'
# Make sure lines are printed in between arrows separated by more than one
# character.
_, err = run_cmd("hy", prg_str.format(3, 1, 6))
print(err)
msg_idx = err.rindex("HyLanguageError:")
assert msg_idx
err_parts = err[msg_idx:].splitlines()[1:]
assert err_parts[2] == ' ^----^'
def test_bin_hy_stdin_bad_repr(): def test_bin_hy_stdin_bad_repr():
# https://github.com/hylang/hy/issues/1389 # https://github.com/hylang/hy/issues/1389
output, err = run_cmd("hy", """ output, err = run_cmd("hy", """
@ -423,3 +488,87 @@ def test_bin_hy_macro_require():
assert os.path.exists(cache_from_source(test_file)) assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file)) output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip() assert "abc" == output.strip()
def test_bin_hy_tracebacks():
"""Make sure the printed tracebacks are correct."""
# We want the filtered tracebacks.
os.environ['HY_DEBUG'] = ''
def req_err(x):
assert x == '{}HyRequireError: No module named {}'.format(
'hy.errors.' if PY3 else '',
(repr if PY3 else str)('not_a_real_module'))
# Modeled after
# > python -c 'import not_a_real_module'
# Traceback (most recent call last):
# File "<string>", line 1, in <module>
# ImportError: No module named not_a_real_module
_, error = run_cmd('hy', '(require not-a-real-module)')
error_lines = error.splitlines()
if error_lines[-1] == '':
del error_lines[-1]
assert len(error_lines) <= 10
# Rough check for the internal traceback filtering
req_err(error_lines[4 if PY3 else -1])
_, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
error_lines = error.splitlines()
assert len(error_lines) <= 4
req_err(error_lines[-1])
output, error = run_cmd('hy -i "(require not-a-real-module)"')
assert output.startswith('=> ')
print(error.splitlines())
req_err(error.splitlines()[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. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import sys
import traceback
import pytest
from math import isnan from math import isnan
from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol, from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
HyString, HyDict, HyList, HySet, HyKeyword) HyString, HyDict, HyList, HySet, HyKeyword)
from hy.lex import tokenize from hy.lex import tokenize
from hy.lex.exceptions import LexException, PrematureEndOfInput from hy.lex.exceptions import LexException, PrematureEndOfInput
import pytest from hy.errors import hy_exc_handler
def peoi(): return pytest.raises(PrematureEndOfInput) def peoi(): return pytest.raises(PrematureEndOfInput)
def lexe(): return pytest.raises(LexException) 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(): def test_lex_exception():
""" Ensure tokenize throws a fit on a partial input """ """ Ensure tokenize throws a fit on a partial input """
with peoi(): tokenize("(foo") with peoi(): tokenize("(foo")
@ -30,8 +58,13 @@ def test_unbalanced_exception():
def test_lex_single_quote_err(): def test_lex_single_quote_err():
"Ensure tokenizing \"' \" throws a LexException that can be stringified" "Ensure tokenizing \"' \" throws a LexException that can be stringified"
# https://github.com/hylang/hy/issues/1252 # https://github.com/hylang/hy/issues/1252
with lexe() as e: tokenize("' ") with lexe() as execinfo:
assert "Could not identify the next token" in str(e.value) tokenize("' ")
check_ex(execinfo, [
' File "<string>", line 1\n',
" '\n",
' ^\n',
'LexException: Could not identify the next token.\n'])
def test_lex_expression_symbols(): 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""" """ Make sure tokenize throws when codec can't decode some bytes"""
with lexe() as execinfo: with lexe() as execinfo:
tokenize('\"\\x8\"') 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(): def test_lex_bracket_strings():
@ -180,7 +217,16 @@ def test_lex_digit_separators():
def test_lex_bad_attrs(): 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("0.foo")
with lexe(): tokenize("1.5.foo") with lexe(): tokenize("1.5.foo")
with lexe(): tokenize("1e3.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") == [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")])]
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>)`)'])