Merge pull request #1687 from brandonwillard/clean-up-exceptions
Generate more concise syntax and compiler errors
This commit is contained in:
commit
e2d6640e8c
1
NEWS.rst
1
NEWS.rst
@ -21,6 +21,7 @@ New Features
|
||||
|
||||
Bug Fixes
|
||||
------------------------------
|
||||
* Cleaned up syntax and compiler errors
|
||||
* Fixed issue with empty arguments in `defmain`.
|
||||
* `require` now compiles to Python AST.
|
||||
* Fixed circular `require`s.
|
||||
|
@ -48,12 +48,6 @@ Command Line Options
|
||||
`--spy` only works on REPL mode.
|
||||
.. versionadded:: 0.9.11
|
||||
|
||||
.. cmdoption:: --show-tracebacks
|
||||
|
||||
Print extended tracebacks for Hy exceptions.
|
||||
|
||||
.. versionadded:: 0.9.12
|
||||
|
||||
.. cmdoption:: --repl-output-fn
|
||||
|
||||
Format REPL output using specific function (e.g., hy.contrib.hy-repr.hy-repr)
|
||||
|
@ -5,6 +5,16 @@ except ImportError:
|
||||
__version__ = 'unknown'
|
||||
|
||||
|
||||
def _initialize_env_var(env_var, default_val):
|
||||
import os, distutils.util
|
||||
try:
|
||||
res = bool(distutils.util.strtobool(
|
||||
os.environ.get(env_var, str(default_val))))
|
||||
except ValueError as e:
|
||||
res = default_val
|
||||
return res
|
||||
|
||||
|
||||
from hy.models import HyExpression, HyInteger, HyKeyword, HyComplex, HyString, HyBytes, HySymbol, HyFloat, HyDict, HyList, HySet # NOQA
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@ try:
|
||||
import __builtin__ as builtins
|
||||
except ImportError:
|
||||
import builtins # NOQA
|
||||
import sys, keyword
|
||||
import sys, keyword, textwrap
|
||||
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
PY35 = sys.version_info >= (3, 5)
|
||||
@ -22,11 +22,60 @@ bytes_type = bytes if PY3 else str # NOQA
|
||||
long_type = int if PY3 else long # NOQA
|
||||
string_types = str if PY3 else basestring # NOQA
|
||||
|
||||
#
|
||||
# Inspired by the same-named `six` functions.
|
||||
#
|
||||
if PY3:
|
||||
exec('def raise_empty(t, *args): raise t(*args) from None')
|
||||
raise_src = textwrap.dedent('''
|
||||
def raise_from(value, from_value):
|
||||
raise value from from_value
|
||||
''')
|
||||
|
||||
def reraise(exc_type, value, traceback=None):
|
||||
try:
|
||||
raise value.with_traceback(traceback)
|
||||
finally:
|
||||
traceback = None
|
||||
|
||||
code_obj_args = ['argcount', 'kwonlyargcount', 'nlocals', 'stacksize',
|
||||
'flags', 'code', 'consts', 'names', 'varnames',
|
||||
'filename', 'name', 'firstlineno', 'lnotab', 'freevars',
|
||||
'cellvars']
|
||||
else:
|
||||
def raise_empty(t, *args):
|
||||
raise t(*args)
|
||||
def raise_from(value, from_value=None):
|
||||
raise value
|
||||
|
||||
raise_src = textwrap.dedent('''
|
||||
def reraise(exc_type, value, traceback=None):
|
||||
try:
|
||||
raise exc_type, value, traceback
|
||||
finally:
|
||||
traceback = None
|
||||
''')
|
||||
|
||||
code_obj_args = ['argcount', 'nlocals', 'stacksize', 'flags', 'code',
|
||||
'consts', 'names', 'varnames', 'filename', 'name',
|
||||
'firstlineno', 'lnotab', 'freevars', 'cellvars']
|
||||
|
||||
raise_code = compile(raise_src, __file__, 'exec')
|
||||
exec(raise_code)
|
||||
|
||||
|
||||
def rename_function(func, new_name):
|
||||
"""Creates a copy of a function and [re]sets the name at the code-object
|
||||
level.
|
||||
"""
|
||||
c = func.__code__
|
||||
new_code = type(c)(*[getattr(c, 'co_{}'.format(a))
|
||||
if a != 'name' else str(new_name)
|
||||
for a in code_obj_args])
|
||||
|
||||
_fn = type(func)(new_code, func.__globals__, str(new_name),
|
||||
func.__defaults__, func.__closure__)
|
||||
_fn.__dict__.update(func.__dict__)
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def isidentifier(x):
|
||||
if x in ('True', 'False', 'None', 'print'):
|
||||
|
385
hy/cmdline.py
385
hy/cmdline.py
@ -12,16 +12,25 @@ import os
|
||||
import io
|
||||
import importlib
|
||||
import py_compile
|
||||
import traceback
|
||||
import runpy
|
||||
import types
|
||||
import time
|
||||
import linecache
|
||||
import hashlib
|
||||
import codeop
|
||||
|
||||
import astor.code_gen
|
||||
|
||||
import hy
|
||||
|
||||
from hy.lex import hy_parse, mangle
|
||||
from hy.lex.exceptions import LexException, PrematureEndOfInput
|
||||
from hy.compiler import HyASTCompiler, hy_compile, hy_eval
|
||||
from hy.errors import HyTypeError
|
||||
from contextlib import contextmanager
|
||||
from hy.lex.exceptions import PrematureEndOfInput
|
||||
from hy.compiler import (HyASTCompiler, hy_eval, hy_compile,
|
||||
hy_ast_compile_flags)
|
||||
from hy.errors import (HyLanguageError, HyRequireError, HyMacroExpansionError,
|
||||
filtered_hy_exceptions, hy_exc_handler)
|
||||
from hy.importer import runhy
|
||||
from hy.completer import completion, Completer
|
||||
from hy.macros import macro, require
|
||||
@ -29,6 +38,11 @@ from hy.models import HyExpression, HyString, HySymbol
|
||||
from hy._compat import builtins, PY3, FileNotFoundError
|
||||
|
||||
|
||||
sys.last_type = None
|
||||
sys.last_value = None
|
||||
sys.last_traceback = None
|
||||
|
||||
|
||||
class HyQuitter(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
@ -49,30 +63,188 @@ class HyQuitter(object):
|
||||
builtins.quit = HyQuitter('quit')
|
||||
builtins.exit = HyQuitter('exit')
|
||||
|
||||
@contextmanager
|
||||
def extend_linecache(add_cmdline_cache):
|
||||
_linecache_checkcache = linecache.checkcache
|
||||
|
||||
def _cmdline_checkcache(*args):
|
||||
_linecache_checkcache(*args)
|
||||
linecache.cache.update(add_cmdline_cache)
|
||||
|
||||
linecache.checkcache = _cmdline_checkcache
|
||||
yield
|
||||
linecache.checkcache = _linecache_checkcache
|
||||
|
||||
|
||||
_codeop_maybe_compile = codeop._maybe_compile
|
||||
|
||||
|
||||
def _hy_maybe_compile(compiler, source, filename, symbol):
|
||||
"""The `codeop` version of this will compile the same source multiple
|
||||
times, and, since we have macros and things like `eval-and-compile`, we
|
||||
can't allow that.
|
||||
"""
|
||||
if not isinstance(compiler, HyCompile):
|
||||
return _codeop_maybe_compile(compiler, source, filename, symbol)
|
||||
|
||||
for line in source.split("\n"):
|
||||
line = line.strip()
|
||||
if line and line[0] != ';':
|
||||
# Leave it alone (could do more with Hy syntax)
|
||||
break
|
||||
else:
|
||||
if symbol != "eval":
|
||||
# Replace it with a 'pass' statement (i.e. tell the compiler to do
|
||||
# nothing)
|
||||
source = "pass"
|
||||
|
||||
return compiler(source, filename, symbol)
|
||||
|
||||
|
||||
codeop._maybe_compile = _hy_maybe_compile
|
||||
|
||||
|
||||
class HyCompile(codeop.Compile, object):
|
||||
"""This compiler uses `linecache` like
|
||||
`IPython.core.compilerop.CachingCompiler`.
|
||||
"""
|
||||
|
||||
def __init__(self, module, locals, ast_callback=None,
|
||||
hy_compiler=None, cmdline_cache={}):
|
||||
self.module = module
|
||||
self.locals = locals
|
||||
self.ast_callback = ast_callback
|
||||
self.hy_compiler = hy_compiler
|
||||
|
||||
super(HyCompile, self).__init__()
|
||||
|
||||
self.flags |= hy_ast_compile_flags
|
||||
|
||||
self.cmdline_cache = cmdline_cache
|
||||
|
||||
def _cache(self, source, name):
|
||||
entry = (len(source),
|
||||
time.time(),
|
||||
[line + '\n' for line in source.splitlines()],
|
||||
name)
|
||||
|
||||
linecache.cache[name] = entry
|
||||
self.cmdline_cache[name] = entry
|
||||
|
||||
def _update_exc_info(self):
|
||||
self.locals['_hy_last_type'] = sys.last_type
|
||||
self.locals['_hy_last_value'] = sys.last_value
|
||||
# Skip our frame.
|
||||
sys.last_traceback = getattr(sys.last_traceback, 'tb_next',
|
||||
sys.last_traceback)
|
||||
self.locals['_hy_last_traceback'] = sys.last_traceback
|
||||
|
||||
def __call__(self, source, filename="<input>", symbol="single"):
|
||||
|
||||
if source == 'pass':
|
||||
# We need to return a no-op to signal that no more input is needed.
|
||||
return (compile(source, filename, symbol),) * 2
|
||||
|
||||
hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest()
|
||||
name = '{}-{}'.format(filename.strip('<>'), hash_digest)
|
||||
|
||||
try:
|
||||
hy_ast = hy_parse(source, filename=name)
|
||||
except Exception:
|
||||
# Capture a traceback without the compiler/REPL frames.
|
||||
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
|
||||
self._update_exc_info()
|
||||
raise
|
||||
|
||||
self._cache(source, name)
|
||||
|
||||
try:
|
||||
hy_ast = hy_parse(source, filename=filename)
|
||||
root_ast = ast.Interactive if symbol == 'single' else ast.Module
|
||||
|
||||
# Our compiler doesn't correspond to a real, fixed source file, so
|
||||
# we need to [re]set these.
|
||||
self.hy_compiler.filename = filename
|
||||
self.hy_compiler.source = source
|
||||
exec_ast, eval_ast = hy_compile(hy_ast, self.module, root=root_ast,
|
||||
get_expr=True,
|
||||
compiler=self.hy_compiler,
|
||||
filename=filename, source=source)
|
||||
|
||||
if self.ast_callback:
|
||||
self.ast_callback(exec_ast, eval_ast)
|
||||
|
||||
exec_code = super(HyCompile, self).__call__(exec_ast, name, symbol)
|
||||
eval_code = super(HyCompile, self).__call__(eval_ast, name, 'eval')
|
||||
|
||||
except HyLanguageError:
|
||||
# Hy will raise exceptions during compile-time that Python would
|
||||
# raise during run-time (e.g. import errors for `require`). In
|
||||
# order to work gracefully with the Python world, we convert such
|
||||
# Hy errors to code that purposefully reraises those exceptions in
|
||||
# the places where Python code expects them.
|
||||
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
|
||||
self._update_exc_info()
|
||||
exec_code = super(HyCompile, self).__call__(
|
||||
'import hy._compat; hy._compat.reraise('
|
||||
'_hy_last_type, _hy_last_value, _hy_last_traceback)',
|
||||
name, symbol)
|
||||
eval_code = super(HyCompile, self).__call__('None', name, 'eval')
|
||||
|
||||
return exec_code, eval_code
|
||||
|
||||
|
||||
class HyCommandCompiler(codeop.CommandCompiler, object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.compiler = HyCompile(*args, **kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return super(HyCommandCompiler, self).__call__(*args, **kwargs)
|
||||
except PrematureEndOfInput:
|
||||
# We have to do this here, because `codeop._maybe_compile` won't
|
||||
# take `None` for a return value (at least not in Python 2.7) and
|
||||
# this exception type is also a `SyntaxError`, so it will be caught
|
||||
# by `code.InteractiveConsole` base methods before it reaches our
|
||||
# `runsource`.
|
||||
return None
|
||||
|
||||
|
||||
class HyREPL(code.InteractiveConsole, object):
|
||||
def __init__(self, spy=False, output_fn=None, locals=None,
|
||||
filename="<input>"):
|
||||
|
||||
super(HyREPL, self).__init__(locals=locals,
|
||||
filename=filename)
|
||||
filename="<stdin>"):
|
||||
|
||||
# Create a proper module for this REPL so that we can obtain it easily
|
||||
# (e.g. using `importlib.import_module`).
|
||||
# Also, make sure it's properly introduced to `sys.modules` and
|
||||
# consistently use its namespace as `locals` from here on.
|
||||
# We let `InteractiveConsole` initialize `self.locals` when it's
|
||||
# `None`.
|
||||
super(HyREPL, self).__init__(locals=locals,
|
||||
filename=filename)
|
||||
|
||||
module_name = self.locals.get('__name__', '__console__')
|
||||
# Make sure our newly created module is properly introduced to
|
||||
# `sys.modules`, and consistently use its namespace as `self.locals`
|
||||
# from here on.
|
||||
self.module = sys.modules.setdefault(module_name,
|
||||
types.ModuleType(module_name))
|
||||
self.module.__dict__.update(self.locals)
|
||||
self.locals = self.module.__dict__
|
||||
|
||||
# Load cmdline-specific macros.
|
||||
require('hy.cmdline', module_name, assignments='ALL')
|
||||
require('hy.cmdline', self.module, assignments='ALL')
|
||||
|
||||
self.hy_compiler = HyASTCompiler(self.module)
|
||||
|
||||
self.cmdline_cache = {}
|
||||
self.compile = HyCommandCompiler(self.module,
|
||||
self.locals,
|
||||
ast_callback=self.ast_callback,
|
||||
hy_compiler=self.hy_compiler,
|
||||
cmdline_cache=self.cmdline_cache)
|
||||
|
||||
self.spy = spy
|
||||
self.last_value = None
|
||||
self.print_last_value = True
|
||||
|
||||
if output_fn is None:
|
||||
self.output_fn = repr
|
||||
@ -90,64 +262,95 @@ class HyREPL(code.InteractiveConsole, object):
|
||||
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
|
||||
self.locals.update({sym: None for sym in self._repl_results_symbols})
|
||||
|
||||
def runsource(self, source, filename='<input>', symbol='single'):
|
||||
global SIMPLE_TRACEBACKS
|
||||
# Allow access to the running REPL instance
|
||||
self.locals['_hy_repl'] = self
|
||||
|
||||
def error_handler(e, use_simple_traceback=False):
|
||||
self.locals[mangle("*e")] = e
|
||||
if use_simple_traceback:
|
||||
print(e, file=sys.stderr)
|
||||
else:
|
||||
self.showtraceback()
|
||||
|
||||
try:
|
||||
def ast_callback(self, exec_ast, eval_ast):
|
||||
if self.spy:
|
||||
try:
|
||||
do = hy_parse(source)
|
||||
except PrematureEndOfInput:
|
||||
return True
|
||||
except LexException as e:
|
||||
if e.source is None:
|
||||
e.source = source
|
||||
e.filename = filename
|
||||
error_handler(e, use_simple_traceback=True)
|
||||
return False
|
||||
# Mush the two AST chunks into a single module for
|
||||
# conversion into Python.
|
||||
new_ast = ast.Module(exec_ast.body +
|
||||
[ast.Expr(eval_ast.body)])
|
||||
print(astor.to_source(new_ast))
|
||||
except Exception:
|
||||
msg = 'Exception in AST callback:\n{}\n'.format(
|
||||
traceback.format_exc())
|
||||
self.write(msg)
|
||||
|
||||
def _error_wrap(self, error_fn, exc_info_override=False, *args, **kwargs):
|
||||
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
|
||||
|
||||
if exc_info_override:
|
||||
# Use a traceback that doesn't have the REPL frames.
|
||||
sys.last_type = self.locals.get('_hy_last_type', sys.last_type)
|
||||
sys.last_value = self.locals.get('_hy_last_value', sys.last_value)
|
||||
sys.last_traceback = self.locals.get('_hy_last_traceback',
|
||||
sys.last_traceback)
|
||||
|
||||
# Sadly, this method in Python 2.7 ignores an overridden `sys.excepthook`.
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
error_fn(*args, **kwargs)
|
||||
else:
|
||||
sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback)
|
||||
|
||||
self.locals[mangle("*e")] = sys.last_value
|
||||
|
||||
def showsyntaxerror(self, filename=None):
|
||||
if filename is None:
|
||||
filename = self.filename
|
||||
|
||||
self._error_wrap(super(HyREPL, self).showsyntaxerror,
|
||||
exc_info_override=True,
|
||||
filename=filename)
|
||||
|
||||
def showtraceback(self):
|
||||
self._error_wrap(super(HyREPL, self).showtraceback)
|
||||
|
||||
def runcode(self, code):
|
||||
try:
|
||||
def ast_callback(main_ast, expr_ast):
|
||||
if self.spy:
|
||||
# Mush the two AST chunks into a single module for
|
||||
# conversion into Python.
|
||||
new_ast = ast.Module(main_ast.body +
|
||||
[ast.Expr(expr_ast.body)])
|
||||
print(astor.to_source(new_ast))
|
||||
|
||||
value = hy_eval(do, self.locals,
|
||||
ast_callback=ast_callback,
|
||||
compiler=self.hy_compiler)
|
||||
except HyTypeError as e:
|
||||
if e.source is None:
|
||||
e.source = source
|
||||
e.filename = filename
|
||||
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
|
||||
return False
|
||||
eval(code[0], self.locals)
|
||||
self.last_value = eval(code[1], self.locals)
|
||||
# Don't print `None` values.
|
||||
self.print_last_value = self.last_value is not None
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_handler(e)
|
||||
# Set this to avoid a print-out of the last value on errors.
|
||||
self.print_last_value = False
|
||||
self.showtraceback()
|
||||
|
||||
def runsource(self, source, filename='<stdin>', symbol='exec'):
|
||||
try:
|
||||
res = super(HyREPL, self).runsource(source, filename, symbol)
|
||||
except (HyMacroExpansionError, HyRequireError):
|
||||
# We need to handle these exceptions ourselves, because the base
|
||||
# method only handles `OverflowError`, `SyntaxError` and
|
||||
# `ValueError`.
|
||||
self.showsyntaxerror(filename)
|
||||
return False
|
||||
except (HyLanguageError):
|
||||
# Our compiler will also raise `TypeError`s
|
||||
self.showtraceback()
|
||||
return False
|
||||
|
||||
if value is not None:
|
||||
# Shift exisitng REPL results
|
||||
next_result = value
|
||||
# Shift exisitng REPL results
|
||||
if not res:
|
||||
next_result = self.last_value
|
||||
for sym in self._repl_results_symbols:
|
||||
self.locals[sym], next_result = next_result, self.locals[sym]
|
||||
|
||||
# Print the value.
|
||||
try:
|
||||
output = self.output_fn(value)
|
||||
except Exception as e:
|
||||
error_handler(e)
|
||||
return False
|
||||
print(output)
|
||||
return False
|
||||
if self.print_last_value:
|
||||
try:
|
||||
output = self.output_fn(self.last_value)
|
||||
except Exception:
|
||||
self.showtraceback()
|
||||
return False
|
||||
|
||||
print(output)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@macro("koan")
|
||||
@ -202,23 +405,17 @@ def ideas_macro(ETname):
|
||||
""")])
|
||||
|
||||
|
||||
SIMPLE_TRACEBACKS = True
|
||||
|
||||
|
||||
def pretty_error(func, *args, **kw):
|
||||
def run_command(source, filename=None):
|
||||
__main__ = importlib.import_module('__main__')
|
||||
require("hy.cmdline", __main__, assignments="ALL")
|
||||
try:
|
||||
return func(*args, **kw)
|
||||
except (HyTypeError, LexException) as e:
|
||||
if SIMPLE_TRACEBACKS:
|
||||
print(e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise
|
||||
tree = hy_parse(source, filename=filename)
|
||||
except HyLanguageError:
|
||||
hy_exc_handler(*sys.exc_info())
|
||||
return 1
|
||||
|
||||
|
||||
def run_command(source):
|
||||
tree = hy_parse(source)
|
||||
require("hy.cmdline", "__main__", assignments="ALL")
|
||||
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
|
||||
with filtered_hy_exceptions():
|
||||
hy_eval(tree, None, __main__, filename=filename, source=source)
|
||||
return 0
|
||||
|
||||
|
||||
@ -231,9 +428,9 @@ def run_repl(hr=None, **kwargs):
|
||||
hr = HyREPL(**kwargs)
|
||||
|
||||
namespace = hr.locals
|
||||
|
||||
with completion(Completer(namespace)):
|
||||
|
||||
with filtered_hy_exceptions(), \
|
||||
extend_linecache(hr.cmdline_cache), \
|
||||
completion(Completer(namespace)):
|
||||
hr.interact("{appname} {version} using "
|
||||
"{py}({build}) {pyversion} on {os}".format(
|
||||
appname=hy.__appname__,
|
||||
@ -260,10 +457,17 @@ def run_icommand(source, **kwargs):
|
||||
source = f.read()
|
||||
filename = source
|
||||
else:
|
||||
filename = '<input>'
|
||||
filename = '<string>'
|
||||
|
||||
hr = HyREPL(**kwargs)
|
||||
hr.runsource(source, filename=filename, symbol='single')
|
||||
with filtered_hy_exceptions():
|
||||
res = hr.runsource(source, filename=filename)
|
||||
|
||||
# If the command was prematurely ended, show an error (just like Python
|
||||
# does).
|
||||
if res:
|
||||
hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback)
|
||||
|
||||
return run_repl(hr)
|
||||
|
||||
|
||||
@ -300,9 +504,6 @@ def cmdline_handler(scriptname, argv):
|
||||
"(e.g., hy.contrib.hy-repr.hy-repr)")
|
||||
parser.add_argument("-v", "--version", action="version", version=VERSION)
|
||||
|
||||
parser.add_argument("--show-tracebacks", action="store_true",
|
||||
help="show complete tracebacks for Hy exceptions")
|
||||
|
||||
# this will contain the script/program name and any arguments for it.
|
||||
parser.add_argument('args', nargs=argparse.REMAINDER,
|
||||
help=argparse.SUPPRESS)
|
||||
@ -327,10 +528,6 @@ def cmdline_handler(scriptname, argv):
|
||||
|
||||
options = parser.parse_args(argv[1:])
|
||||
|
||||
if options.show_tracebacks:
|
||||
global SIMPLE_TRACEBACKS
|
||||
SIMPLE_TRACEBACKS = False
|
||||
|
||||
if options.E:
|
||||
# User did "hy -E ..."
|
||||
_remove_python_envs()
|
||||
@ -340,7 +537,7 @@ def cmdline_handler(scriptname, argv):
|
||||
|
||||
if options.command:
|
||||
# User did "hy -c ..."
|
||||
return run_command(options.command)
|
||||
return run_command(options.command, filename='<string>')
|
||||
|
||||
if options.mod:
|
||||
# User did "hy -m ..."
|
||||
@ -356,7 +553,7 @@ def cmdline_handler(scriptname, argv):
|
||||
if options.args:
|
||||
if options.args[0] == "-":
|
||||
# Read the program from stdin
|
||||
return run_command(sys.stdin.read())
|
||||
return run_command(sys.stdin.read(), filename='<stdin>')
|
||||
|
||||
else:
|
||||
# User did "hy <filename>"
|
||||
@ -371,12 +568,16 @@ def cmdline_handler(scriptname, argv):
|
||||
|
||||
try:
|
||||
sys.argv = options.args
|
||||
runhy.run_path(filename, run_name='__main__')
|
||||
with filtered_hy_exceptions():
|
||||
runhy.run_path(filename, run_name='__main__')
|
||||
return 0
|
||||
except FileNotFoundError as e:
|
||||
print("hy: Can't open file '{0}': [Errno {1}] {2}".format(
|
||||
e.filename, e.errno, e.strerror), file=sys.stderr)
|
||||
sys.exit(e.errno)
|
||||
except HyLanguageError:
|
||||
hy_exc_handler(*sys.exc_info())
|
||||
sys.exit(1)
|
||||
|
||||
# User did NOTHING!
|
||||
return run_repl(spy=options.spy, output_fn=options.repl_output_fn)
|
||||
@ -446,12 +647,16 @@ def hy2py_main():
|
||||
options = parser.parse_args(sys.argv[1:])
|
||||
|
||||
if options.FILE is None or options.FILE == '-':
|
||||
filename = '<stdin>'
|
||||
source = sys.stdin.read()
|
||||
else:
|
||||
filename = options.FILE
|
||||
with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
|
||||
source = source_file.read()
|
||||
|
||||
hst = pretty_error(hy_parse, source)
|
||||
with filtered_hy_exceptions():
|
||||
hst = hy_parse(source, filename=filename)
|
||||
|
||||
if options.with_source:
|
||||
# need special printing on Windows in case the
|
||||
# codepage doesn't support utf-8 characters
|
||||
@ -466,7 +671,9 @@ def hy2py_main():
|
||||
print()
|
||||
print()
|
||||
|
||||
_ast = pretty_error(hy_compile, hst, '__main__')
|
||||
with filtered_hy_exceptions():
|
||||
_ast = hy_compile(hst, '__main__', filename=filename, source=source)
|
||||
|
||||
if options.with_ast:
|
||||
if PY3 and platform.system() == "Windows":
|
||||
_print_for_windows(astor.dump_tree(_ast))
|
||||
|
267
hy/compiler.py
267
hy/compiler.py
@ -9,20 +9,21 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
|
||||
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
|
||||
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
|
||||
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
|
||||
from hy.errors import HyCompileError, HyTypeError
|
||||
from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
|
||||
HySyntaxError, HyEvalError, HyInternalError)
|
||||
|
||||
from hy.lex import mangle, unmangle
|
||||
|
||||
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
|
||||
PY35, raise_empty)
|
||||
from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
|
||||
PY35, reraise)
|
||||
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
|
||||
|
||||
import hy.core
|
||||
|
||||
import pkgutil
|
||||
import traceback
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
import types
|
||||
import ast
|
||||
import sys
|
||||
@ -340,22 +341,33 @@ def is_unpack(kind, x):
|
||||
class HyASTCompiler(object):
|
||||
"""A Hy-to-Python AST compiler"""
|
||||
|
||||
def __init__(self, module):
|
||||
def __init__(self, module, filename=None, source=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
module: str or types.ModuleType
|
||||
Module in which the Hy tree is evaluated.
|
||||
Module name or object in which the Hy tree is evaluated.
|
||||
filename: str, optional
|
||||
The name of the file for the source to be compiled.
|
||||
This is optional information for informative error messages and
|
||||
debugging.
|
||||
source: str, optional
|
||||
The source for the file, if any, being compiled. This is optional
|
||||
information for informative error messages and debugging.
|
||||
"""
|
||||
self.anon_var_count = 0
|
||||
self.imports = defaultdict(set)
|
||||
self.temp_if = None
|
||||
|
||||
if not inspect.ismodule(module):
|
||||
module = importlib.import_module(module)
|
||||
self.module = importlib.import_module(module)
|
||||
else:
|
||||
self.module = module
|
||||
|
||||
self.module = module
|
||||
self.module_name = module.__name__
|
||||
self.module_name = self.module.__name__
|
||||
|
||||
self.filename = filename
|
||||
self.source = source
|
||||
|
||||
# Hy expects these to be present, so we prep the module for Hy
|
||||
# compilation.
|
||||
@ -431,10 +443,18 @@ class HyASTCompiler(object):
|
||||
# nested; so let's re-raise this exception, let's not wrap it in
|
||||
# another HyCompileError!
|
||||
raise
|
||||
except HyTypeError:
|
||||
raise
|
||||
except HyLanguageError as e:
|
||||
# These are expected errors that should be passed to the user.
|
||||
reraise(type(e), e, sys.exc_info()[2])
|
||||
except Exception as e:
|
||||
raise_empty(HyCompileError, e, sys.exc_info()[2])
|
||||
# These are unexpected errors that will--hopefully--never be seen
|
||||
# by the user.
|
||||
f_exc = traceback.format_exc()
|
||||
exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(f_exc)
|
||||
reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
|
||||
|
||||
def _syntax_error(self, expr, message):
|
||||
return HySyntaxError(message, expr, self.filename, self.source)
|
||||
|
||||
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
|
||||
oldpy_unpack=False):
|
||||
@ -455,8 +475,8 @@ class HyASTCompiler(object):
|
||||
|
||||
if not PY35 and oldpy_unpack and is_unpack("iterable", expr):
|
||||
if oldpy_starargs:
|
||||
raise HyTypeError(expr, "Pythons < 3.5 allow only one "
|
||||
"`unpack-iterable` per call")
|
||||
raise self._syntax_error(expr,
|
||||
"Pythons < 3.5 allow only one `unpack-iterable` per call")
|
||||
oldpy_starargs = self.compile(expr[1])
|
||||
ret += oldpy_starargs
|
||||
oldpy_starargs = oldpy_starargs.force_expr
|
||||
@ -472,21 +492,20 @@ class HyASTCompiler(object):
|
||||
expr, arg=None, value=ret.force_expr))
|
||||
elif oldpy_unpack:
|
||||
if oldpy_kwargs:
|
||||
raise HyTypeError(expr, "Pythons < 3.5 allow only one "
|
||||
"`unpack-mapping` per call")
|
||||
raise self._syntax_error(expr,
|
||||
"Pythons < 3.5 allow only one `unpack-mapping` per call")
|
||||
oldpy_kwargs = ret.force_expr
|
||||
|
||||
elif with_kwargs and isinstance(expr, HyKeyword):
|
||||
try:
|
||||
value = next(exprs_iter)
|
||||
except StopIteration:
|
||||
raise HyTypeError(expr,
|
||||
"Keyword argument {kw} needs "
|
||||
"a value.".format(kw=expr))
|
||||
raise self._syntax_error(expr,
|
||||
"Keyword argument {kw} needs a value.".format(kw=expr))
|
||||
|
||||
if not expr:
|
||||
raise HyTypeError(expr, "Can't call a function with the "
|
||||
"empty keyword")
|
||||
raise self._syntax_error(expr,
|
||||
"Can't call a function with the empty keyword")
|
||||
|
||||
compiled_value = self.compile(value)
|
||||
ret += compiled_value
|
||||
@ -527,8 +546,8 @@ class HyASTCompiler(object):
|
||||
|
||||
if isinstance(name, Result):
|
||||
if not name.is_expr():
|
||||
raise HyTypeError(expr,
|
||||
"Can't assign or delete a non-expression")
|
||||
raise self._syntax_error(expr,
|
||||
"Can't assign or delete a non-expression")
|
||||
name = name.expr
|
||||
|
||||
if isinstance(name, (ast.Tuple, ast.List)):
|
||||
@ -547,9 +566,8 @@ class HyASTCompiler(object):
|
||||
new_name = ast.Starred(
|
||||
value=self._storeize(expr, name.value, func))
|
||||
else:
|
||||
raise HyTypeError(expr,
|
||||
"Can't assign or delete a %s" %
|
||||
type(expr).__name__)
|
||||
raise self._syntax_error(expr,
|
||||
"Can't assign or delete a %s" % type(expr).__name__)
|
||||
|
||||
new_name.ctx = func()
|
||||
ast.copy_location(new_name, name)
|
||||
@ -575,9 +593,8 @@ class HyASTCompiler(object):
|
||||
op = unmangle(ast_str(form[0]))
|
||||
if level == 0 and op in ("unquote", "unquote-splice"):
|
||||
if len(form) != 2:
|
||||
raise HyTypeError(form,
|
||||
("`%s' needs 1 argument, got %s" %
|
||||
op, len(form) - 1))
|
||||
raise HyTypeError("`%s' needs 1 argument, got %s" % op, len(form) - 1,
|
||||
self.filename, form, self.source)
|
||||
return set(), form[1], op == "unquote-splice"
|
||||
elif op == "quasiquote":
|
||||
level += 1
|
||||
@ -629,7 +646,8 @@ class HyASTCompiler(object):
|
||||
@special("unpack-iterable", [FORM])
|
||||
def compile_unpack_iterable(self, expr, root, arg):
|
||||
if not PY3:
|
||||
raise HyTypeError(expr, "`unpack-iterable` isn't allowed here")
|
||||
raise self._syntax_error(expr,
|
||||
"`unpack-iterable` isn't allowed here")
|
||||
ret = self.compile(arg)
|
||||
ret += asty.Starred(expr, value=ret.force_expr, ctx=ast.Load())
|
||||
return ret
|
||||
@ -659,7 +677,8 @@ class HyASTCompiler(object):
|
||||
|
||||
if cause is not None:
|
||||
if not PY3:
|
||||
raise HyTypeError(expr, "raise from only supported in python 3")
|
||||
raise self._syntax_error(expr,
|
||||
"raise from only supported in python 3")
|
||||
cause = self.compile(cause)
|
||||
ret += cause
|
||||
cause = cause.force_expr
|
||||
@ -706,13 +725,11 @@ class HyASTCompiler(object):
|
||||
|
||||
# Using (else) without (except) is verboten!
|
||||
if orelse and not handlers:
|
||||
raise HyTypeError(
|
||||
expr,
|
||||
raise self._syntax_error(expr,
|
||||
"`try' cannot have `else' without `except'")
|
||||
# Likewise a bare (try) or (try BODY).
|
||||
if not (handlers or finalbody):
|
||||
raise HyTypeError(
|
||||
expr,
|
||||
raise self._syntax_error(expr,
|
||||
"`try' must have an `except' or `finally' clause")
|
||||
|
||||
returnable = Result(
|
||||
@ -963,7 +980,8 @@ class HyASTCompiler(object):
|
||||
def compile_decorate_expression(self, expr, name, args):
|
||||
decs, fn = args[:-1], self.compile(args[-1])
|
||||
if not fn.stmts or not isinstance(fn.stmts[-1], _decoratables):
|
||||
raise HyTypeError(args[-1], "Decorated a non-function")
|
||||
raise self._syntax_error(args[-1],
|
||||
"Decorated a non-function")
|
||||
decs, ret, _ = self._compile_collect(decs)
|
||||
fn.stmts[-1].decorator_list = decs + fn.stmts[-1].decorator_list
|
||||
return ret + fn
|
||||
@ -1194,8 +1212,8 @@ class HyASTCompiler(object):
|
||||
if (HySymbol('*'), None) in kids:
|
||||
if len(kids) != 1:
|
||||
star = kids[kids.index((HySymbol('*'), None))][0]
|
||||
raise HyTypeError(star, "* in an import name list "
|
||||
"must be on its own")
|
||||
raise self._syntax_error(star,
|
||||
"* in an import name list must be on its own")
|
||||
else:
|
||||
assignments = [(k, v or k) for k, v in kids]
|
||||
|
||||
@ -1390,15 +1408,15 @@ class HyASTCompiler(object):
|
||||
if str_name in (["None"] + (["True", "False"] if PY3 else [])):
|
||||
# Python 2 allows assigning to True and False, although
|
||||
# this is rarely wise.
|
||||
raise HyTypeError(name,
|
||||
"Can't assign to `%s'" % str_name)
|
||||
raise self._syntax_error(name,
|
||||
"Can't assign to `%s'" % str_name)
|
||||
|
||||
result = self.compile(result)
|
||||
ld_name = self.compile(name)
|
||||
|
||||
if isinstance(ld_name.expr, ast.Call):
|
||||
raise HyTypeError(name,
|
||||
"Can't assign to a callable: `%s'" % str_name)
|
||||
raise self._syntax_error(name,
|
||||
"Can't assign to a callable: `%s'" % str_name)
|
||||
|
||||
if (result.temp_variables
|
||||
and isinstance(name, HySymbol)
|
||||
@ -1474,7 +1492,8 @@ class HyASTCompiler(object):
|
||||
mandatory, optional, rest, kwonly, kwargs = params
|
||||
optional, defaults, ret = self._parse_optional_args(optional)
|
||||
if kwonly is not None and not PY3:
|
||||
raise HyTypeError(params, "&kwonly parameters require Python 3")
|
||||
raise self._syntax_error(params,
|
||||
"&kwonly parameters require Python 3")
|
||||
kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True)
|
||||
ret += ret2
|
||||
main_args = mandatory + optional
|
||||
@ -1612,7 +1631,29 @@ class HyASTCompiler(object):
|
||||
def compile_eval_and_compile(self, expr, root, body):
|
||||
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
|
||||
|
||||
hy_eval(new_expr + body, self.module.__dict__, self.module)
|
||||
try:
|
||||
hy_eval(new_expr + body,
|
||||
self.module.__dict__,
|
||||
self.module,
|
||||
filename=self.filename,
|
||||
source=self.source)
|
||||
except HyInternalError:
|
||||
# Unexpected "meta" compilation errors need to be treated
|
||||
# like normal (unexpected) compilation errors at this level
|
||||
# (or the compilation level preceding this one).
|
||||
raise
|
||||
except Exception as e:
|
||||
# These could be expected Hy language errors (e.g. syntax errors)
|
||||
# or regular Python runtime errors that do not signify errors in
|
||||
# the compilation *process* (although compilation did technically
|
||||
# fail).
|
||||
# We wrap these exceptions and pass them through.
|
||||
reraise(HyEvalError,
|
||||
HyEvalError(str(e),
|
||||
self.filename,
|
||||
body,
|
||||
self.source),
|
||||
sys.exc_info()[2])
|
||||
|
||||
return (self._compile_branch(body)
|
||||
if ast_str(root) == "eval_and_compile"
|
||||
@ -1627,8 +1668,8 @@ class HyASTCompiler(object):
|
||||
return self.compile(expr)
|
||||
|
||||
if not expr:
|
||||
raise HyTypeError(
|
||||
expr, "empty expressions are not allowed at top level")
|
||||
raise self._syntax_error(expr,
|
||||
"empty expressions are not allowed at top level")
|
||||
|
||||
args = list(expr)
|
||||
root = args.pop(0)
|
||||
@ -1646,8 +1687,7 @@ class HyASTCompiler(object):
|
||||
sroot in (mangle(","), mangle(".")) or
|
||||
not any(is_unpack("iterable", x) for x in args)):
|
||||
if sroot in _bad_roots:
|
||||
raise HyTypeError(
|
||||
expr,
|
||||
raise self._syntax_error(expr,
|
||||
"The special form '{}' is not allowed here".format(root))
|
||||
# `sroot` is a special operator. Get the build method and
|
||||
# pattern-match the arguments.
|
||||
@ -1655,11 +1695,10 @@ class HyASTCompiler(object):
|
||||
try:
|
||||
parse_tree = pattern.parse(args)
|
||||
except NoParseError as e:
|
||||
raise HyTypeError(
|
||||
raise self._syntax_error(
|
||||
expr[min(e.state.pos + 1, len(expr) - 1)],
|
||||
"parse error for special form '{}': {}".format(
|
||||
root,
|
||||
e.msg.replace("<EOF>", "end of form")))
|
||||
root, e.msg.replace("<EOF>", "end of form")))
|
||||
return Result() + build_method(
|
||||
self, expr, unmangle(sroot), *parse_tree)
|
||||
|
||||
@ -1681,13 +1720,13 @@ class HyASTCompiler(object):
|
||||
FORM +
|
||||
many(FORM)).parse(args)
|
||||
except NoParseError:
|
||||
raise HyTypeError(
|
||||
expr, "attribute access requires object")
|
||||
raise self._syntax_error(expr,
|
||||
"attribute access requires object")
|
||||
# Reconstruct `args` to exclude `obj`.
|
||||
args = [x for p in kws for x in p] + list(rest)
|
||||
if is_unpack("iterable", obj):
|
||||
raise HyTypeError(
|
||||
obj, "can't call a method on an unpacking form")
|
||||
raise self._syntax_error(obj,
|
||||
"can't call a method on an unpacking form")
|
||||
func = self.compile(HyExpression(
|
||||
[HySymbol(".").replace(root), obj] +
|
||||
attrs))
|
||||
@ -1725,16 +1764,12 @@ class HyASTCompiler(object):
|
||||
glob, local = symbol.rsplit(".", 1)
|
||||
|
||||
if not glob:
|
||||
raise HyTypeError(symbol, 'cannot access attribute on '
|
||||
'anything other than a name '
|
||||
'(in order to get attributes of '
|
||||
'expressions, use '
|
||||
'`(. <expression> {attr})` or '
|
||||
'`(.{attr} <expression>)`)'.format(
|
||||
attr=local))
|
||||
raise self._syntax_error(symbol,
|
||||
'cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. <expression> {attr})` or `(.{attr} <expression>)`)'.format(attr=local))
|
||||
|
||||
if not local:
|
||||
raise HyTypeError(symbol, 'cannot access empty attribute')
|
||||
raise self._syntax_error(symbol,
|
||||
'cannot access empty attribute')
|
||||
|
||||
glob = HySymbol(glob).replace(symbol)
|
||||
ret = self.compile_symbol(glob)
|
||||
@ -1802,8 +1837,13 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False):
|
||||
|
||||
|
||||
def hy_eval(hytree, locals=None, module=None, ast_callback=None,
|
||||
compiler=None):
|
||||
compiler=None, filename=None, source=None):
|
||||
"""Evaluates a quoted expression and returns the value.
|
||||
|
||||
If you're evaluating hand-crafted AST trees, make sure the line numbers
|
||||
are set properly. Try `fix_missing_locations` and related functions in the
|
||||
Python `ast` library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
=> (eval '(print "Hello World"))
|
||||
@ -1816,8 +1856,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hytree: a Hy expression tree
|
||||
Source code to parse.
|
||||
hytree: HyObject
|
||||
The Hy AST object to evaluate.
|
||||
|
||||
locals: dict, optional
|
||||
Local environment in which to evaluate the Hy tree. Defaults to the
|
||||
@ -1839,6 +1879,19 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
|
||||
An existing Hy compiler to use for compilation. Also serves as
|
||||
the `module` value when given.
|
||||
|
||||
filename: str, optional
|
||||
The filename corresponding to the source for `tree`. This will be
|
||||
overridden by the `filename` field of `tree`, if any; otherwise, it
|
||||
defaults to "<string>". When `compiler` is given, its `filename` field
|
||||
value is always used.
|
||||
|
||||
source: str, optional
|
||||
A string containing the source code for `tree`. This will be
|
||||
overridden by the `source` field of `tree`, if any; otherwise,
|
||||
if `None`, an attempt will be made to obtain it from the module given by
|
||||
`module`. When `compiler` is given, its `source` field value is always
|
||||
used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : Result of evaluating the Hy compiled tree.
|
||||
@ -1853,36 +1906,53 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
|
||||
if not isinstance(locals, dict):
|
||||
raise TypeError("Locals must be a dictionary")
|
||||
|
||||
_ast, expr = hy_compile(hytree, module=module, get_expr=True,
|
||||
compiler=compiler)
|
||||
# Does the Hy AST object come with its own information?
|
||||
filename = getattr(hytree, 'filename', filename) or '<string>'
|
||||
source = getattr(hytree, 'source', source)
|
||||
|
||||
# Spoof the positions in the generated ast...
|
||||
for node in ast.walk(_ast):
|
||||
node.lineno = 1
|
||||
node.col_offset = 1
|
||||
|
||||
for node in ast.walk(expr):
|
||||
node.lineno = 1
|
||||
node.col_offset = 1
|
||||
_ast, expr = hy_compile(hytree, module, get_expr=True,
|
||||
compiler=compiler, filename=filename,
|
||||
source=source)
|
||||
|
||||
if ast_callback:
|
||||
ast_callback(_ast, expr)
|
||||
|
||||
globals = module.__dict__
|
||||
|
||||
# Two-step eval: eval() the body of the exec call
|
||||
eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals)
|
||||
eval(ast_compile(_ast, filename, "exec"),
|
||||
module.__dict__, locals)
|
||||
|
||||
# Then eval the expression context and return that
|
||||
return eval(ast_compile(expr, "<eval>", "eval"), globals, locals)
|
||||
return eval(ast_compile(expr, filename, "eval"),
|
||||
module.__dict__, locals)
|
||||
|
||||
|
||||
def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
|
||||
compiler=None):
|
||||
"""Compile a Hy tree into a Python AST tree.
|
||||
def _module_file_source(module_name, filename, source):
|
||||
"""Try to obtain missing filename and source information from a module name
|
||||
without actually loading the module.
|
||||
"""
|
||||
if filename is None or source is None:
|
||||
mod_loader = pkgutil.get_loader(module_name)
|
||||
if mod_loader:
|
||||
if filename is None:
|
||||
filename = mod_loader.get_filename(module_name)
|
||||
if source is None:
|
||||
source = mod_loader.get_source(module_name)
|
||||
|
||||
# We need a non-None filename.
|
||||
filename = filename or '<string>'
|
||||
|
||||
return filename, source
|
||||
|
||||
|
||||
def hy_compile(tree, module, root=ast.Module, get_expr=False,
|
||||
compiler=None, filename=None, source=None):
|
||||
"""Compile a HyObject tree into a Python AST Module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tree: HyObject
|
||||
The Hy AST object to compile.
|
||||
|
||||
module: str or types.ModuleType, optional
|
||||
Module, or name of the module, in which the Hy tree is evaluated.
|
||||
The module associated with `compiler` takes priority over this value.
|
||||
@ -1897,18 +1967,43 @@ def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
|
||||
An existing Hy compiler to use for compilation. Also serves as
|
||||
the `module` value when given.
|
||||
|
||||
filename: str, optional
|
||||
The filename corresponding to the source for `tree`. This will be
|
||||
overridden by the `filename` field of `tree`, if any; otherwise, it
|
||||
defaults to "<string>". When `compiler` is given, its `filename` field
|
||||
value is always used.
|
||||
|
||||
source: str, optional
|
||||
A string containing the source code for `tree`. This will be
|
||||
overridden by the `source` field of `tree`, if any; otherwise,
|
||||
if `None`, an attempt will be made to obtain it from the module given by
|
||||
`module`. When `compiler` is given, its `source` field value is always
|
||||
used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : A Python AST tree
|
||||
"""
|
||||
module = get_compiler_module(module, compiler, False)
|
||||
|
||||
if isinstance(module, string_types):
|
||||
if module.startswith('<') and module.endswith('>'):
|
||||
module = types.ModuleType(module)
|
||||
else:
|
||||
module = importlib.import_module(ast_str(module, piecewise=True))
|
||||
|
||||
if not inspect.ismodule(module):
|
||||
raise TypeError('Invalid module type: {}'.format(type(module)))
|
||||
|
||||
filename = getattr(tree, 'filename', filename)
|
||||
source = getattr(tree, 'source', source)
|
||||
|
||||
tree = wrap_value(tree)
|
||||
if not isinstance(tree, HyObject):
|
||||
raise HyCompileError("`tree` must be a HyObject or capable of "
|
||||
"being promoted to one")
|
||||
raise TypeError("`tree` must be a HyObject or capable of "
|
||||
"being promoted to one")
|
||||
|
||||
compiler = compiler or HyASTCompiler(module)
|
||||
compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
|
||||
result = compiler.compile(tree)
|
||||
expr = result.force_expr
|
||||
|
||||
|
@ -14,15 +14,14 @@
|
||||
(if* (not (isinstance macro-name hy.models.HySymbol))
|
||||
(raise
|
||||
(hy.errors.HyTypeError
|
||||
macro-name
|
||||
(% "received a `%s' instead of a symbol for macro name"
|
||||
(. (type name)
|
||||
__name__)))))
|
||||
(. (type name) __name__))
|
||||
None --file-- None)))
|
||||
(for [kw '[&kwonly &kwargs]]
|
||||
(if* (in kw lambda-list)
|
||||
(raise (hy.errors.HyTypeError macro-name
|
||||
(% "macros cannot use %s"
|
||||
kw)))))
|
||||
(raise (hy.errors.HyTypeError (% "macros cannot use %s"
|
||||
kw)
|
||||
macro-name --file-- None))))
|
||||
;; this looks familiar...
|
||||
`(eval-and-compile
|
||||
(import hy)
|
||||
@ -45,12 +44,12 @@
|
||||
(if (and (not (isinstance tag-name hy.models.HySymbol))
|
||||
(not (isinstance tag-name hy.models.HyString)))
|
||||
(raise (hy.errors.HyTypeError
|
||||
tag-name
|
||||
(% "received a `%s' instead of a symbol for tag macro name"
|
||||
(. (type tag-name) __name__)))))
|
||||
(. (type tag-name) --name--))
|
||||
tag-name --file-- None)))
|
||||
(if (or (= tag-name ":")
|
||||
(= tag-name "&"))
|
||||
(raise (NameError (% "%s can't be used as a tag macro name" tag-name))))
|
||||
(raise (hy.errors.HyNameError (% "%s can't be used as a tag macro name" tag-name))))
|
||||
(setv tag-name (.replace (hy.models.HyString tag-name)
|
||||
tag-name))
|
||||
`(eval-and-compile
|
||||
@ -58,9 +57,8 @@
|
||||
((hy.macros.tag ~tag-name)
|
||||
(fn ~lambda-list ~@body))))
|
||||
|
||||
(defmacro macro-error [location reason]
|
||||
"Error out properly within a macro at `location` giving `reason`."
|
||||
`(raise (hy.errors.HyMacroExpansionError ~location ~reason)))
|
||||
(defmacro macro-error [expression reason &optional [filename '--name--]]
|
||||
`(raise (hy.errors.HyMacroExpansionError ~reason ~filename ~expression None)))
|
||||
|
||||
(defmacro defn [name lambda-list &rest body]
|
||||
"Define `name` as a function with `lambda-list` signature and body `body`."
|
||||
|
348
hy/errors.py
348
hy/errors.py
@ -2,103 +2,307 @@
|
||||
# Copyright 2019 the authors.
|
||||
# This file is part of Hy, which is free software licensed under the Expat
|
||||
# license. See the LICENSE.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import pkgutil
|
||||
|
||||
from functools import reduce
|
||||
from contextlib import contextmanager
|
||||
from hy import _initialize_env_var
|
||||
|
||||
from clint.textui import colored
|
||||
|
||||
_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS',
|
||||
True)
|
||||
_hy_colored_errors = _initialize_env_var('HY_COLORED_ERRORS', False)
|
||||
|
||||
|
||||
class HyError(Exception):
|
||||
"""
|
||||
Generic Hy error. All internal Exceptions will be subclassed from this
|
||||
Exception.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HyCompileError(HyError):
|
||||
def __init__(self, exception, traceback=None):
|
||||
self.exception = exception
|
||||
self.traceback = traceback
|
||||
class HyInternalError(HyError):
|
||||
"""Unexpected errors occurring during compilation or parsing of Hy code.
|
||||
|
||||
def __str__(self):
|
||||
if isinstance(self.exception, HyTypeError):
|
||||
return str(self.exception)
|
||||
if self.traceback:
|
||||
tb = "".join(traceback.format_tb(self.traceback)).strip()
|
||||
Errors sub-classing this are not intended to be user-facing, and will,
|
||||
hopefully, never be seen by users!
|
||||
"""
|
||||
|
||||
|
||||
class HyLanguageError(HyError):
|
||||
"""Errors caused by invalid use of the Hy language.
|
||||
|
||||
This, and any errors inheriting from this, are user-facing.
|
||||
"""
|
||||
|
||||
def __init__(self, message, expression=None, filename=None, source=None,
|
||||
lineno=1, colno=1):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
message: str
|
||||
The message to display for this error.
|
||||
expression: HyObject, optional
|
||||
The Hy expression generating this error.
|
||||
filename: str, optional
|
||||
The filename for the source code generating this error.
|
||||
Expression-provided information will take precedence of this value.
|
||||
source: str, optional
|
||||
The actual source code generating this error. Expression-provided
|
||||
information will take precedence of this value.
|
||||
lineno: int, optional
|
||||
The line number of the error. Expression-provided information will
|
||||
take precedence of this value.
|
||||
colno: int, optional
|
||||
The column number of the error. Expression-provided information
|
||||
will take precedence of this value.
|
||||
"""
|
||||
self.msg = message
|
||||
self.compute_lineinfo(expression, filename, source, lineno, colno)
|
||||
|
||||
if isinstance(self, SyntaxError):
|
||||
syntax_error_args = (self.filename, self.lineno, self.offset,
|
||||
self.text)
|
||||
super(HyLanguageError, self).__init__(message, syntax_error_args)
|
||||
else:
|
||||
tb = "No traceback available. 😟"
|
||||
return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s"
|
||||
% (self.exception.__class__.__name__,
|
||||
self.exception, tb))
|
||||
super(HyLanguageError, self).__init__(message)
|
||||
|
||||
def compute_lineinfo(self, expression, filename, source, lineno, colno):
|
||||
|
||||
class HyTypeError(TypeError):
|
||||
def __init__(self, expression, message):
|
||||
super(HyTypeError, self).__init__(message)
|
||||
self.expression = expression
|
||||
self.message = message
|
||||
self.source = None
|
||||
self.filename = None
|
||||
# NOTE: We use `SyntaxError`'s field names (i.e. `text`, `offset`,
|
||||
# `msg`) for compatibility and print-outs.
|
||||
self.text = getattr(expression, 'source', source)
|
||||
self.filename = getattr(expression, 'filename', filename)
|
||||
|
||||
def __str__(self):
|
||||
if self.text:
|
||||
lines = self.text.splitlines()
|
||||
|
||||
result = ""
|
||||
self.lineno = getattr(expression, 'start_line', lineno)
|
||||
self.offset = getattr(expression, 'start_column', colno)
|
||||
end_column = getattr(expression, 'end_column',
|
||||
len(lines[self.lineno-1]))
|
||||
end_line = getattr(expression, 'end_line', self.lineno)
|
||||
|
||||
if all(getattr(self.expression, x, None) is not None
|
||||
for x in ("start_line", "start_column", "end_column")):
|
||||
# Trim the source down to the essentials.
|
||||
self.text = '\n'.join(lines[self.lineno-1:end_line])
|
||||
|
||||
line = self.expression.start_line
|
||||
start = self.expression.start_column
|
||||
end = self.expression.end_column
|
||||
|
||||
source = []
|
||||
if self.source is not None:
|
||||
source = self.source.split("\n")[line-1:self.expression.end_line]
|
||||
|
||||
if line == self.expression.end_line:
|
||||
length = end - start
|
||||
if end_column:
|
||||
if self.lineno == end_line:
|
||||
self.arrow_offset = end_column
|
||||
else:
|
||||
length = len(source[0]) - start
|
||||
|
||||
result += ' File "%s", line %d, column %d\n\n' % (self.filename,
|
||||
line,
|
||||
start)
|
||||
|
||||
if len(source) == 1:
|
||||
result += ' %s\n' % colored.red(source[0])
|
||||
result += ' %s%s\n' % (' '*(start-1),
|
||||
colored.green('^' + '-'*(length-1) + '^'))
|
||||
if len(source) > 1:
|
||||
result += ' %s\n' % colored.red(source[0])
|
||||
result += ' %s%s\n' % (' '*(start-1),
|
||||
colored.green('^' + '-'*length))
|
||||
if len(source) > 2: # write the middle lines
|
||||
for line in source[1:-1]:
|
||||
result += ' %s\n' % colored.red("".join(line))
|
||||
result += ' %s\n' % colored.green("-"*len(line))
|
||||
|
||||
# write the last line
|
||||
result += ' %s\n' % colored.red("".join(source[-1]))
|
||||
result += ' %s\n' % colored.green('-'*(end-1) + '^')
|
||||
self.arrow_offset = len(self.text[0])
|
||||
|
||||
self.arrow_offset -= self.offset
|
||||
else:
|
||||
self.arrow_offset = None
|
||||
else:
|
||||
result += ' File "%s", unknown location\n' % self.filename
|
||||
# We could attempt to extract the source given a filename, but we
|
||||
# don't.
|
||||
self.lineno = lineno
|
||||
self.offset = colno
|
||||
self.arrow_offset = None
|
||||
|
||||
result += colored.yellow("%s: %s\n\n" %
|
||||
(self.__class__.__name__,
|
||||
self.message))
|
||||
def __str__(self):
|
||||
"""Provide an exception message that includes SyntaxError-like source
|
||||
line information when available.
|
||||
"""
|
||||
global _hy_colored_errors
|
||||
|
||||
return result
|
||||
# Syntax errors are special and annotate the traceback (instead of what
|
||||
# we would do in the message that follows the traceback).
|
||||
if isinstance(self, SyntaxError):
|
||||
return super(HyLanguageError, self).__str__()
|
||||
|
||||
# When there isn't extra source information, use the normal message.
|
||||
if not isinstance(self, SyntaxError) and not self.text:
|
||||
return super(HyLanguageError, self).__str__()
|
||||
|
||||
# Re-purpose Python's builtin syntax error formatting.
|
||||
output = traceback.format_exception_only(
|
||||
SyntaxError,
|
||||
SyntaxError(self.msg, (self.filename, self.lineno, self.offset,
|
||||
self.text)))
|
||||
|
||||
arrow_idx, _ = next(((i, x) for i, x in enumerate(output)
|
||||
if x.strip() == '^'),
|
||||
(None, None))
|
||||
if arrow_idx:
|
||||
msg_idx = arrow_idx + 1
|
||||
else:
|
||||
msg_idx, _ = next((i, x) for i, x in enumerate(output)
|
||||
if x.startswith('SyntaxError: '))
|
||||
|
||||
# Get rid of erroneous error-type label.
|
||||
output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx])
|
||||
|
||||
# Extend the text arrow, when given enough source info.
|
||||
if arrow_idx and self.arrow_offset:
|
||||
output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'),
|
||||
'-' * (self.arrow_offset - 1))
|
||||
|
||||
if _hy_colored_errors:
|
||||
from clint.textui import colored
|
||||
output[msg_idx:] = [colored.yellow(o) for o in output[msg_idx:]]
|
||||
if arrow_idx:
|
||||
output[arrow_idx] = colored.green(output[arrow_idx])
|
||||
for idx, line in enumerate(output[::msg_idx]):
|
||||
if line.strip().startswith(
|
||||
'File "{}", line'.format(self.filename)):
|
||||
output[idx] = colored.red(line)
|
||||
|
||||
# This resulting string will come after a "<class-name>:" prompt, so
|
||||
# put it down a line.
|
||||
output.insert(0, '\n')
|
||||
|
||||
# Avoid "...expected str instance, ColoredString found"
|
||||
return reduce(lambda x, y: x + y, output)
|
||||
|
||||
|
||||
class HyMacroExpansionError(HyTypeError):
|
||||
pass
|
||||
class HyCompileError(HyInternalError):
|
||||
"""Unexpected errors occurring within the compiler."""
|
||||
|
||||
|
||||
class HyIOError(HyError, IOError):
|
||||
class HyTypeError(HyLanguageError, TypeError):
|
||||
"""TypeError occurring during the normal use of Hy."""
|
||||
|
||||
|
||||
class HyNameError(HyLanguageError, NameError):
|
||||
"""NameError occurring during the normal use of Hy."""
|
||||
|
||||
|
||||
class HyRequireError(HyLanguageError):
|
||||
"""Errors arising during the use of `require`
|
||||
|
||||
This, and any errors inheriting from this, are user-facing.
|
||||
"""
|
||||
Trivial subclass of IOError and HyError, to distinguish between
|
||||
IOErrors raised by Hy itself as opposed to Hy programs.
|
||||
|
||||
|
||||
class HyMacroExpansionError(HyLanguageError):
|
||||
"""Errors caused by invalid use of Hy macros.
|
||||
|
||||
This, and any errors inheriting from this, are user-facing.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HyEvalError(HyLanguageError):
|
||||
"""Errors occurring during code evaluation at compile-time.
|
||||
|
||||
These errors distinguish unexpected errors within the compilation process
|
||||
(i.e. `HyInternalError`s) from unrelated errors in user code evaluated by
|
||||
the compiler (e.g. in `eval-and-compile`).
|
||||
|
||||
This, and any errors inheriting from this, are user-facing.
|
||||
"""
|
||||
|
||||
|
||||
class HyIOError(HyInternalError, IOError):
|
||||
""" Subclass used to distinguish between IOErrors raised by Hy itself as
|
||||
opposed to Hy programs.
|
||||
"""
|
||||
|
||||
|
||||
class HySyntaxError(HyLanguageError, SyntaxError):
|
||||
"""Error during the Lexing of a Hython expression."""
|
||||
|
||||
|
||||
class HyWrapperError(HyError, TypeError):
|
||||
"""Errors caused by language model object wrapping.
|
||||
|
||||
These can be caused by improper user-level use of a macro, so they're
|
||||
not really "internal". If they arise due to anything else, they're an
|
||||
internal/compiler problem, though.
|
||||
"""
|
||||
|
||||
|
||||
def _module_filter_name(module_name):
|
||||
try:
|
||||
compiler_loader = pkgutil.get_loader(module_name)
|
||||
if not compiler_loader:
|
||||
return None
|
||||
|
||||
filename = compiler_loader.get_filename(module_name)
|
||||
if not filename:
|
||||
return None
|
||||
|
||||
if compiler_loader.is_package(module_name):
|
||||
# Use the package directory (e.g. instead of `.../__init__.py`) so
|
||||
# that we can filter all modules in a package.
|
||||
return os.path.dirname(filename)
|
||||
else:
|
||||
# Normalize filename endings, because tracebacks will use `pyc` when
|
||||
# the loader says `py`.
|
||||
return filename.replace('.pyc', '.py')
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
_tb_hidden_modules = {m for m in map(_module_filter_name,
|
||||
['hy.compiler', 'hy.lex',
|
||||
'hy.cmdline', 'hy.lex.parser',
|
||||
'hy.importer', 'hy._compat',
|
||||
'hy.macros', 'hy.models',
|
||||
'rply'])
|
||||
if m is not None}
|
||||
|
||||
|
||||
def hy_exc_filter(exc_type, exc_value, exc_traceback):
|
||||
"""Produce exceptions print-outs with all frames originating from the
|
||||
modules in `_tb_hidden_modules` filtered out.
|
||||
|
||||
The frames are actually filtered by each module's filename and only when a
|
||||
subclass of `HyLanguageError` is emitted.
|
||||
|
||||
This does not remove the frames from the actual tracebacks, so debugging
|
||||
will show everything.
|
||||
"""
|
||||
# frame = (filename, line number, function name*, text)
|
||||
new_tb = []
|
||||
for frame in traceback.extract_tb(exc_traceback):
|
||||
if not (frame[0].replace('.pyc', '.py') in _tb_hidden_modules or
|
||||
os.path.dirname(frame[0]) in _tb_hidden_modules):
|
||||
new_tb += [frame]
|
||||
|
||||
lines = traceback.format_list(new_tb)
|
||||
|
||||
lines.insert(0, "Traceback (most recent call last):\n")
|
||||
|
||||
lines.extend(traceback.format_exception_only(exc_type, exc_value))
|
||||
output = ''.join(lines)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def hy_exc_handler(exc_type, exc_value, exc_traceback):
|
||||
"""A `sys.excepthook` handler that uses `hy_exc_filter` to
|
||||
remove internal Hy frames from a traceback print-out.
|
||||
"""
|
||||
if os.environ.get('HY_DEBUG', False):
|
||||
return sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
|
||||
try:
|
||||
output = hy_exc_filter(exc_type, exc_value, exc_traceback)
|
||||
sys.stderr.write(output)
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def filtered_hy_exceptions():
|
||||
"""Temporarily apply a `sys.excepthook` that filters Hy internal frames
|
||||
from tracebacks.
|
||||
|
||||
Filtering can be controlled by the variable
|
||||
`hy.errors._hy_filter_internal_errors` and environment variable
|
||||
`HY_FILTER_INTERNAL_ERRORS`.
|
||||
"""
|
||||
global _hy_filter_internal_errors
|
||||
if _hy_filter_internal_errors:
|
||||
current_hook = sys.excepthook
|
||||
sys.excepthook = hy_exc_handler
|
||||
yield
|
||||
sys.excepthook = current_hook
|
||||
else:
|
||||
yield
|
||||
|
@ -17,10 +17,8 @@ import importlib
|
||||
from functools import partial
|
||||
from contextlib import contextmanager
|
||||
|
||||
from hy.errors import HyTypeError
|
||||
from hy.compiler import hy_compile, hy_ast_compile_flags
|
||||
from hy.lex import hy_parse
|
||||
from hy.lex.exceptions import LexException
|
||||
from hy._compat import PY3
|
||||
|
||||
|
||||
@ -153,15 +151,9 @@ if PY3:
|
||||
def _hy_source_to_code(self, data, path, _optimize=-1):
|
||||
if _could_be_hy_src(path):
|
||||
source = data.decode("utf-8")
|
||||
try:
|
||||
hy_tree = hy_parse(source)
|
||||
with loader_module_obj(self) as module:
|
||||
data = hy_compile(hy_tree, module)
|
||||
except (HyTypeError, LexException) as e:
|
||||
if e.source is None:
|
||||
e.source = source
|
||||
e.filename = path
|
||||
raise
|
||||
hy_tree = hy_parse(source, filename=path)
|
||||
with loader_module_obj(self) as module:
|
||||
data = hy_compile(hy_tree, module)
|
||||
|
||||
return _py_source_to_code(self, data, path, _optimize=_optimize)
|
||||
|
||||
@ -287,19 +279,15 @@ else:
|
||||
fullname = self._fix_name(fullname)
|
||||
if fullname is None:
|
||||
fullname = self.fullname
|
||||
try:
|
||||
hy_source = self.get_source(fullname)
|
||||
hy_tree = hy_parse(hy_source)
|
||||
with loader_module_obj(self) as module:
|
||||
hy_ast = hy_compile(hy_tree, module)
|
||||
|
||||
code = compile(hy_ast, self.filename, 'exec',
|
||||
hy_ast_compile_flags)
|
||||
except (HyTypeError, LexException) as e:
|
||||
if e.source is None:
|
||||
e.source = hy_source
|
||||
e.filename = self.filename
|
||||
raise
|
||||
hy_source = self.get_source(fullname)
|
||||
hy_tree = hy_parse(hy_source, filename=self.filename)
|
||||
|
||||
with loader_module_obj(self) as module:
|
||||
hy_ast = hy_compile(hy_tree, module)
|
||||
|
||||
code = compile(hy_ast, self.filename, 'exec',
|
||||
hy_ast_compile_flags)
|
||||
|
||||
if not sys.dont_write_bytecode:
|
||||
try:
|
||||
@ -453,7 +441,7 @@ else:
|
||||
try:
|
||||
flags = None
|
||||
if _could_be_hy_src(filename):
|
||||
hy_tree = hy_parse(source_str)
|
||||
hy_tree = hy_parse(source_str, filename=filename)
|
||||
|
||||
if module is None:
|
||||
module = inspect.getmodule(inspect.stack()[1][0])
|
||||
@ -465,9 +453,6 @@ else:
|
||||
|
||||
codeobject = compile(source, dfile or filename, 'exec', flags)
|
||||
except Exception as err:
|
||||
if isinstance(err, (HyTypeError, LexException)) and err.source is None:
|
||||
err.source = source_str
|
||||
err.filename = filename
|
||||
|
||||
py_exc = py_compile.PyCompileError(err.__class__, err,
|
||||
dfile or filename)
|
||||
|
@ -18,7 +18,7 @@ except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
def hy_parse(source):
|
||||
def hy_parse(source, filename='<string>'):
|
||||
"""Parse a Hy source string.
|
||||
|
||||
Parameters
|
||||
@ -26,31 +26,52 @@ def hy_parse(source):
|
||||
source: string
|
||||
Source code to parse.
|
||||
|
||||
filename: string, optional
|
||||
File name corresponding to source. Defaults to "<string>".
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : instance of `types.CodeType`
|
||||
out : HyExpression
|
||||
"""
|
||||
source = re.sub(r'\A#!.*', '', source)
|
||||
return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
|
||||
_source = re.sub(r'\A#!.*', '', source)
|
||||
res = HyExpression([HySymbol("do")] +
|
||||
tokenize(_source + "\n",
|
||||
filename=filename))
|
||||
res.source = source
|
||||
res.filename = filename
|
||||
return res
|
||||
|
||||
|
||||
def tokenize(buf):
|
||||
"""
|
||||
Tokenize a Lisp file or string buffer into internal Hy objects.
|
||||
class ParserState(object):
|
||||
def __init__(self, source, filename):
|
||||
self.source = source
|
||||
self.filename = filename
|
||||
|
||||
|
||||
def tokenize(source, filename=None):
|
||||
""" Tokenize a Lisp file or string buffer into internal Hy objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source: str
|
||||
The source to tokenize.
|
||||
filename: str, optional
|
||||
The filename corresponding to `source`.
|
||||
"""
|
||||
from hy.lex.lexer import lexer
|
||||
from hy.lex.parser import parser
|
||||
from rply.errors import LexingError
|
||||
try:
|
||||
return parser.parse(lexer.lex(buf))
|
||||
return parser.parse(lexer.lex(source),
|
||||
state=ParserState(source, filename))
|
||||
except LexingError as e:
|
||||
pos = e.getsourcepos()
|
||||
raise LexException("Could not identify the next token.",
|
||||
pos.lineno, pos.colno, buf)
|
||||
None, filename, source,
|
||||
max(pos.lineno, 1),
|
||||
max(pos.colno, 1))
|
||||
except LexException as e:
|
||||
if e.source is None:
|
||||
e.source = buf
|
||||
raise
|
||||
raise e
|
||||
|
||||
|
||||
mangle_delim = 'X'
|
||||
|
@ -1,49 +1,39 @@
|
||||
# Copyright 2019 the authors.
|
||||
# This file is part of Hy, which is free software licensed under the Expat
|
||||
# license. See the LICENSE.
|
||||
|
||||
from hy.errors import HyError
|
||||
from hy.errors import HySyntaxError
|
||||
|
||||
|
||||
class LexException(HyError):
|
||||
"""Error during the Lexing of a Hython expression."""
|
||||
def __init__(self, message, lineno, colno, source=None):
|
||||
super(LexException, self).__init__(message)
|
||||
self.message = message
|
||||
self.lineno = lineno
|
||||
self.colno = colno
|
||||
self.source = source
|
||||
self.filename = '<stdin>'
|
||||
class LexException(HySyntaxError):
|
||||
|
||||
def __str__(self):
|
||||
from hy.errors import colored
|
||||
@classmethod
|
||||
def from_lexer(cls, message, state, token):
|
||||
lineno = None
|
||||
colno = None
|
||||
source = state.source
|
||||
source_pos = token.getsourcepos()
|
||||
|
||||
line = self.lineno
|
||||
start = self.colno
|
||||
if source_pos:
|
||||
lineno = source_pos.lineno
|
||||
colno = source_pos.colno
|
||||
elif source:
|
||||
# Use the end of the last line of source for `PrematureEndOfInput`.
|
||||
# We get rid of empty lines and spaces so that the error matches
|
||||
# with the last piece of visible code.
|
||||
lines = source.rstrip().splitlines()
|
||||
lineno = lineno or len(lines)
|
||||
colno = colno or len(lines[lineno - 1])
|
||||
else:
|
||||
lineno = lineno or 1
|
||||
colno = colno or 1
|
||||
|
||||
result = ""
|
||||
|
||||
source = self.source.split("\n")
|
||||
|
||||
if line > 0 and start > 0:
|
||||
result += ' File "%s", line %d, column %d\n\n' % (self.filename,
|
||||
line,
|
||||
start)
|
||||
|
||||
if len(self.source) > 0:
|
||||
source_line = source[line-1]
|
||||
else:
|
||||
source_line = ""
|
||||
|
||||
result += ' %s\n' % colored.red(source_line)
|
||||
result += ' %s%s\n' % (' '*(start-1), colored.green('^'))
|
||||
|
||||
result += colored.yellow("LexException: %s\n\n" % self.message)
|
||||
|
||||
return result
|
||||
return cls(message,
|
||||
None,
|
||||
state.filename,
|
||||
source,
|
||||
lineno,
|
||||
colno)
|
||||
|
||||
|
||||
class PrematureEndOfInput(LexException):
|
||||
"""We got a premature end of input"""
|
||||
def __init__(self, message):
|
||||
super(PrematureEndOfInput, self).__init__(message, -1, -1)
|
||||
pass
|
||||
|
@ -6,7 +6,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from functools import wraps
|
||||
import re, unicodedata
|
||||
|
||||
from rply import ParserGenerator
|
||||
|
||||
@ -22,10 +21,10 @@ pg = ParserGenerator([rule.name for rule in lexer.rules] + ['$end'])
|
||||
|
||||
def set_boundaries(fun):
|
||||
@wraps(fun)
|
||||
def wrapped(p):
|
||||
def wrapped(state, p):
|
||||
start = p[0].source_pos
|
||||
end = p[-1].source_pos
|
||||
ret = fun(p)
|
||||
ret = fun(state, p)
|
||||
ret.start_line = start.lineno
|
||||
ret.start_column = start.colno
|
||||
if start is not end:
|
||||
@ -40,9 +39,9 @@ def set_boundaries(fun):
|
||||
|
||||
def set_quote_boundaries(fun):
|
||||
@wraps(fun)
|
||||
def wrapped(p):
|
||||
def wrapped(state, p):
|
||||
start = p[0].source_pos
|
||||
ret = fun(p)
|
||||
ret = fun(state, p)
|
||||
ret.start_line = start.lineno
|
||||
ret.start_column = start.colno
|
||||
ret.end_line = p[-1].end_line
|
||||
@ -52,54 +51,45 @@ def set_quote_boundaries(fun):
|
||||
|
||||
|
||||
@pg.production("main : list_contents")
|
||||
def main(p):
|
||||
def main(state, p):
|
||||
return p[0]
|
||||
|
||||
|
||||
@pg.production("main : $end")
|
||||
def main_empty(p):
|
||||
def main_empty(state, p):
|
||||
return []
|
||||
|
||||
|
||||
def reject_spurious_dots(*items):
|
||||
"Reject the spurious dots from items"
|
||||
for list in items:
|
||||
for tok in list:
|
||||
if tok == "." and type(tok) == HySymbol:
|
||||
raise LexException("Malformed dotted list",
|
||||
tok.start_line, tok.start_column)
|
||||
|
||||
|
||||
@pg.production("paren : LPAREN list_contents RPAREN")
|
||||
@set_boundaries
|
||||
def paren(p):
|
||||
def paren(state, p):
|
||||
return HyExpression(p[1])
|
||||
|
||||
|
||||
@pg.production("paren : LPAREN RPAREN")
|
||||
@set_boundaries
|
||||
def empty_paren(p):
|
||||
def empty_paren(state, p):
|
||||
return HyExpression([])
|
||||
|
||||
|
||||
@pg.production("list_contents : term list_contents")
|
||||
def list_contents(p):
|
||||
def list_contents(state, p):
|
||||
return [p[0]] + p[1]
|
||||
|
||||
|
||||
@pg.production("list_contents : term")
|
||||
def list_contents_single(p):
|
||||
def list_contents_single(state, p):
|
||||
return [p[0]]
|
||||
|
||||
|
||||
@pg.production("list_contents : DISCARD term discarded_list_contents")
|
||||
def list_contents_empty(p):
|
||||
def list_contents_empty(state, p):
|
||||
return []
|
||||
|
||||
|
||||
@pg.production("discarded_list_contents : DISCARD term discarded_list_contents")
|
||||
@pg.production("discarded_list_contents :")
|
||||
def discarded_list_contents(p):
|
||||
def discarded_list_contents(state, p):
|
||||
pass
|
||||
|
||||
|
||||
@ -109,58 +99,58 @@ def discarded_list_contents(p):
|
||||
@pg.production("term : list")
|
||||
@pg.production("term : set")
|
||||
@pg.production("term : string")
|
||||
def term(p):
|
||||
def term(state, p):
|
||||
return p[0]
|
||||
|
||||
|
||||
@pg.production("term : DISCARD term term")
|
||||
def term_discard(p):
|
||||
def term_discard(state, p):
|
||||
return p[2]
|
||||
|
||||
|
||||
@pg.production("term : QUOTE term")
|
||||
@set_quote_boundaries
|
||||
def term_quote(p):
|
||||
def term_quote(state, p):
|
||||
return HyExpression([HySymbol("quote"), p[1]])
|
||||
|
||||
|
||||
@pg.production("term : QUASIQUOTE term")
|
||||
@set_quote_boundaries
|
||||
def term_quasiquote(p):
|
||||
def term_quasiquote(state, p):
|
||||
return HyExpression([HySymbol("quasiquote"), p[1]])
|
||||
|
||||
|
||||
@pg.production("term : UNQUOTE term")
|
||||
@set_quote_boundaries
|
||||
def term_unquote(p):
|
||||
def term_unquote(state, p):
|
||||
return HyExpression([HySymbol("unquote"), p[1]])
|
||||
|
||||
|
||||
@pg.production("term : UNQUOTESPLICE term")
|
||||
@set_quote_boundaries
|
||||
def term_unquote_splice(p):
|
||||
def term_unquote_splice(state, p):
|
||||
return HyExpression([HySymbol("unquote-splice"), p[1]])
|
||||
|
||||
|
||||
@pg.production("term : HASHSTARS term")
|
||||
@set_quote_boundaries
|
||||
def term_hashstars(p):
|
||||
def term_hashstars(state, p):
|
||||
n_stars = len(p[0].getstr()[1:])
|
||||
if n_stars == 1:
|
||||
sym = "unpack-iterable"
|
||||
elif n_stars == 2:
|
||||
sym = "unpack-mapping"
|
||||
else:
|
||||
raise LexException(
|
||||
raise LexException.from_lexer(
|
||||
"Too many stars in `#*` construct (if you want to unpack a symbol "
|
||||
"beginning with a star, separate it with whitespace)",
|
||||
p[0].source_pos.lineno, p[0].source_pos.colno)
|
||||
state, p[0])
|
||||
return HyExpression([HySymbol(sym), p[1]])
|
||||
|
||||
|
||||
@pg.production("term : HASHOTHER term")
|
||||
@set_quote_boundaries
|
||||
def hash_other(p):
|
||||
def hash_other(state, p):
|
||||
# p == [(Token('HASHOTHER', '#foo'), bar)]
|
||||
st = p[0].getstr()[1:]
|
||||
str_object = HyString(st)
|
||||
@ -170,63 +160,63 @@ def hash_other(p):
|
||||
|
||||
@pg.production("set : HLCURLY list_contents RCURLY")
|
||||
@set_boundaries
|
||||
def t_set(p):
|
||||
def t_set(state, p):
|
||||
return HySet(p[1])
|
||||
|
||||
|
||||
@pg.production("set : HLCURLY RCURLY")
|
||||
@set_boundaries
|
||||
def empty_set(p):
|
||||
def empty_set(state, p):
|
||||
return HySet([])
|
||||
|
||||
|
||||
@pg.production("dict : LCURLY list_contents RCURLY")
|
||||
@set_boundaries
|
||||
def t_dict(p):
|
||||
def t_dict(state, p):
|
||||
return HyDict(p[1])
|
||||
|
||||
|
||||
@pg.production("dict : LCURLY RCURLY")
|
||||
@set_boundaries
|
||||
def empty_dict(p):
|
||||
def empty_dict(state, p):
|
||||
return HyDict([])
|
||||
|
||||
|
||||
@pg.production("list : LBRACKET list_contents RBRACKET")
|
||||
@set_boundaries
|
||||
def t_list(p):
|
||||
def t_list(state, p):
|
||||
return HyList(p[1])
|
||||
|
||||
|
||||
@pg.production("list : LBRACKET RBRACKET")
|
||||
@set_boundaries
|
||||
def t_empty_list(p):
|
||||
def t_empty_list(state, p):
|
||||
return HyList([])
|
||||
|
||||
|
||||
@pg.production("string : STRING")
|
||||
@set_boundaries
|
||||
def t_string(p):
|
||||
def t_string(state, p):
|
||||
# Replace the single double quotes with triple double quotes to allow
|
||||
# embedded newlines.
|
||||
try:
|
||||
s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""')
|
||||
except SyntaxError:
|
||||
raise LexException("Can't convert {} to a HyString".format(p[0].value),
|
||||
p[0].source_pos.lineno, p[0].source_pos.colno)
|
||||
raise LexException.from_lexer("Can't convert {} to a HyString".format(p[0].value),
|
||||
state, p[0])
|
||||
return (HyString if isinstance(s, str_type) else HyBytes)(s)
|
||||
|
||||
|
||||
@pg.production("string : PARTIAL_STRING")
|
||||
def t_partial_string(p):
|
||||
def t_partial_string(state, p):
|
||||
# Any unterminated string requires more input
|
||||
raise PrematureEndOfInput("Premature end of input")
|
||||
raise PrematureEndOfInput.from_lexer("Partial string literal", state, p[0])
|
||||
|
||||
|
||||
bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING')
|
||||
@pg.production("string : BRACKETSTRING")
|
||||
@set_boundaries
|
||||
def t_bracket_string(p):
|
||||
def t_bracket_string(state, p):
|
||||
m = bracket_string_re.match(p[0].value)
|
||||
delim, content = m.groups()
|
||||
return HyString(content, brackets=delim)
|
||||
@ -234,7 +224,7 @@ def t_bracket_string(p):
|
||||
|
||||
@pg.production("identifier : IDENTIFIER")
|
||||
@set_boundaries
|
||||
def t_identifier(p):
|
||||
def t_identifier(state, p):
|
||||
obj = p[0].value
|
||||
|
||||
val = symbol_like(obj)
|
||||
@ -243,11 +233,11 @@ def t_identifier(p):
|
||||
|
||||
if "." in obj and symbol_like(obj.split(".", 1)[0]) is not None:
|
||||
# E.g., `5.attr` or `:foo.attr`
|
||||
raise LexException(
|
||||
raise LexException.from_lexer(
|
||||
'Cannot access attribute on anything other than a name (in '
|
||||
'order to get attributes of expressions, use '
|
||||
'`(. <expression> <attr>)` or `(.<attr> <expression>)`)',
|
||||
p[0].source_pos.lineno, p[0].source_pos.colno)
|
||||
state, p[0])
|
||||
|
||||
return HySymbol(obj)
|
||||
|
||||
@ -284,14 +274,15 @@ def symbol_like(obj):
|
||||
|
||||
|
||||
@pg.error
|
||||
def error_handler(token):
|
||||
def error_handler(state, token):
|
||||
tokentype = token.gettokentype()
|
||||
if tokentype == '$end':
|
||||
raise PrematureEndOfInput("Premature end of input")
|
||||
raise PrematureEndOfInput.from_lexer("Premature end of input", state,
|
||||
token)
|
||||
else:
|
||||
raise LexException(
|
||||
"Ran into a %s where it wasn't expected." % tokentype,
|
||||
token.source_pos.lineno, token.source_pos.colno)
|
||||
raise LexException.from_lexer(
|
||||
"Ran into a %s where it wasn't expected." % tokentype, state,
|
||||
token)
|
||||
|
||||
|
||||
parser = pg.build()
|
||||
|
87
hy/macros.py
87
hy/macros.py
@ -1,15 +1,19 @@
|
||||
# Copyright 2019 the authors.
|
||||
# This file is part of Hy, which is free software licensed under the Expat
|
||||
# license. See the LICENSE.
|
||||
import sys
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
import traceback
|
||||
|
||||
from hy._compat import PY3, string_types
|
||||
from contextlib import contextmanager
|
||||
|
||||
from hy._compat import PY3, string_types, reraise, rename_function
|
||||
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
|
||||
from hy.lex import mangle
|
||||
|
||||
from hy.errors import HyTypeError, HyMacroExpansionError
|
||||
from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
|
||||
HyRequireError)
|
||||
|
||||
try:
|
||||
# Check if we have the newer inspect.signature available.
|
||||
@ -48,7 +52,7 @@ def macro(name):
|
||||
"""
|
||||
name = mangle(name)
|
||||
def _(fn):
|
||||
fn.__name__ = '({})'.format(name)
|
||||
fn = rename_function(fn, name)
|
||||
try:
|
||||
fn._hy_macro_pass_compiler = has_kwargs(fn)
|
||||
except Exception:
|
||||
@ -73,7 +77,7 @@ def tag(name):
|
||||
if not PY3:
|
||||
_name = _name.encode('UTF-8')
|
||||
|
||||
fn.__name__ = _name
|
||||
fn = rename_function(fn, _name)
|
||||
|
||||
module = inspect.getmodule(fn)
|
||||
|
||||
@ -148,7 +152,6 @@ def require(source_module, target_module, assignments, prefix=""):
|
||||
out: boolean
|
||||
Whether or not macros and tags were actually transferred.
|
||||
"""
|
||||
|
||||
if target_module is None:
|
||||
parent_frame = inspect.stack()[1][0]
|
||||
target_namespace = parent_frame.f_globals
|
||||
@ -159,7 +162,7 @@ def require(source_module, target_module, assignments, prefix=""):
|
||||
elif inspect.ismodule(target_module):
|
||||
target_namespace = target_module.__dict__
|
||||
else:
|
||||
raise TypeError('`target_module` is not a recognized type: {}'.format(
|
||||
raise HyTypeError('`target_module` is not a recognized type: {}'.format(
|
||||
type(target_module)))
|
||||
|
||||
# Let's do a quick check to make sure the source module isn't actually
|
||||
@ -171,14 +174,17 @@ def require(source_module, target_module, assignments, prefix=""):
|
||||
return False
|
||||
|
||||
if not inspect.ismodule(source_module):
|
||||
source_module = importlib.import_module(source_module)
|
||||
try:
|
||||
source_module = importlib.import_module(source_module)
|
||||
except ImportError as e:
|
||||
reraise(HyRequireError, HyRequireError(e.args[0]), None)
|
||||
|
||||
source_macros = source_module.__dict__.setdefault('__macros__', {})
|
||||
source_tags = source_module.__dict__.setdefault('__tags__', {})
|
||||
|
||||
if len(source_module.__macros__) + len(source_module.__tags__) == 0:
|
||||
if assignments != "ALL":
|
||||
raise ImportError('The module {} has no macros or tags'.format(
|
||||
raise HyRequireError('The module {} has no macros or tags'.format(
|
||||
source_module))
|
||||
else:
|
||||
return False
|
||||
@ -203,7 +209,7 @@ def require(source_module, target_module, assignments, prefix=""):
|
||||
elif _name in source_module.__tags__:
|
||||
target_tags[alias] = source_tags[_name]
|
||||
else:
|
||||
raise ImportError('Could not require name {} from {}'.format(
|
||||
raise HyRequireError('Could not require name {} from {}'.format(
|
||||
_name, source_module))
|
||||
|
||||
return True
|
||||
@ -237,24 +243,33 @@ def load_macros(module):
|
||||
if k not in module_tags})
|
||||
|
||||
|
||||
def make_empty_fn_copy(fn):
|
||||
@contextmanager
|
||||
def macro_exceptions(module, macro_tree, compiler=None):
|
||||
try:
|
||||
# This might fail if fn has parameters with funny names, like o!n. In
|
||||
# such a case, we return a generic function that ensures the program
|
||||
# can continue running. Unfortunately, the error message that might get
|
||||
# raised later on while expanding a macro might not make sense at all.
|
||||
yield
|
||||
except HyLanguageError as e:
|
||||
# These are user-level Hy errors occurring in the macro.
|
||||
# We want to pass them up to the user.
|
||||
reraise(type(e), e, sys.exc_info()[2])
|
||||
except Exception as e:
|
||||
|
||||
formatted_args = format_args(fn)
|
||||
fn_str = 'lambda {}: None'.format(
|
||||
formatted_args.lstrip('(').rstrip(')'))
|
||||
empty_fn = eval(fn_str)
|
||||
if compiler:
|
||||
filename = compiler.filename
|
||||
source = compiler.source
|
||||
else:
|
||||
filename = None
|
||||
source = None
|
||||
|
||||
except Exception:
|
||||
exc_msg = ' '.join(traceback.format_exception_only(
|
||||
sys.exc_info()[0], sys.exc_info()[1]))
|
||||
|
||||
def empty_fn(*args, **kwargs):
|
||||
None
|
||||
msg = "expanding macro {}\n ".format(str(macro_tree[0]))
|
||||
msg += exc_msg
|
||||
|
||||
return empty_fn
|
||||
reraise(HyMacroExpansionError,
|
||||
HyMacroExpansionError(
|
||||
msg, macro_tree, filename, source),
|
||||
sys.exc_info()[2])
|
||||
|
||||
|
||||
def macroexpand(tree, module, compiler=None, once=False):
|
||||
@ -324,28 +339,13 @@ def macroexpand(tree, module, compiler=None, once=False):
|
||||
compiler = HyASTCompiler(module)
|
||||
opts['compiler'] = compiler
|
||||
|
||||
try:
|
||||
m_copy = make_empty_fn_copy(m)
|
||||
m_copy(module.__name__, *tree[1:], **opts)
|
||||
except TypeError as e:
|
||||
msg = "expanding `" + str(tree[0]) + "': "
|
||||
msg += str(e).replace("<lambda>()", "", 1).strip()
|
||||
raise HyMacroExpansionError(tree, msg)
|
||||
|
||||
try:
|
||||
with macro_exceptions(module, tree, compiler):
|
||||
obj = m(module.__name__, *tree[1:], **opts)
|
||||
except HyTypeError as e:
|
||||
if e.expression is None:
|
||||
e.expression = tree
|
||||
raise
|
||||
except Exception as e:
|
||||
msg = "expanding `" + str(tree[0]) + "': " + repr(e)
|
||||
raise HyMacroExpansionError(tree, msg)
|
||||
|
||||
if isinstance(obj, HyExpression):
|
||||
obj.module = inspect.getmodule(m)
|
||||
if isinstance(obj, HyExpression):
|
||||
obj.module = inspect.getmodule(m)
|
||||
|
||||
tree = replace_hy_obj(obj, tree)
|
||||
tree = replace_hy_obj(obj, tree)
|
||||
|
||||
if once:
|
||||
break
|
||||
@ -375,7 +375,8 @@ def tag_macroexpand(tag, tree, module):
|
||||
None)
|
||||
|
||||
if tag_macro is None:
|
||||
raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
|
||||
raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
|
||||
None, tag, None)
|
||||
|
||||
expr = tag_macro(tree)
|
||||
|
||||
|
15
hy/models.py
15
hy/models.py
@ -1,16 +1,18 @@
|
||||
# Copyright 2019 the authors.
|
||||
# This file is part of Hy, which is free software licensed under the Expat
|
||||
# license. See the LICENSE.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from contextlib import contextmanager
|
||||
from math import isnan, isinf
|
||||
from hy import _initialize_env_var
|
||||
from hy._compat import PY3, str_type, bytes_type, long_type, string_types
|
||||
from hy.errors import HyWrapperError
|
||||
from fractions import Fraction
|
||||
from clint.textui import colored
|
||||
|
||||
|
||||
PRETTY = True
|
||||
_hy_colored_ast_objects = _initialize_env_var('HY_COLORED_AST_OBJECTS', False)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@ -63,7 +65,7 @@ def wrap_value(x):
|
||||
|
||||
new = _wrappers.get(type(x), lambda y: y)(x)
|
||||
if not isinstance(new, HyObject):
|
||||
raise TypeError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
|
||||
raise HyWrapperError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
|
||||
if isinstance(x, HyObject):
|
||||
new = new.replace(x, recursive=False)
|
||||
if not hasattr(new, "start_column"):
|
||||
@ -271,8 +273,9 @@ class HySequence(HyObject, list):
|
||||
return str(self) if PRETTY else super(HySequence, self).__repr__()
|
||||
|
||||
def __str__(self):
|
||||
global _hy_colored_ast_objects
|
||||
with pretty():
|
||||
c = self.color
|
||||
c = self.color if _hy_colored_ast_objects else str
|
||||
if self:
|
||||
return ("{}{}\n {}{}").format(
|
||||
c(self.__class__.__name__),
|
||||
@ -298,10 +301,12 @@ class HyDict(HySequence):
|
||||
"""
|
||||
HyDict (just a representation of a dict)
|
||||
"""
|
||||
color = staticmethod(colored.green)
|
||||
|
||||
def __str__(self):
|
||||
global _hy_colored_ast_objects
|
||||
with pretty():
|
||||
g = colored.green
|
||||
g = self.color if _hy_colored_ast_objects else str
|
||||
if self:
|
||||
pairs = []
|
||||
for k, v in zip(self[::2],self[1::2]):
|
||||
|
@ -6,11 +6,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from hy import HyString
|
||||
from hy.models import HyObject
|
||||
from hy.compiler import hy_compile, hy_eval
|
||||
from hy.errors import HyCompileError, HyTypeError
|
||||
from hy.errors import HyCompileError, HyLanguageError, HyError
|
||||
from hy.lex import hy_parse
|
||||
from hy.lex.exceptions import LexException
|
||||
from hy.lex.exceptions import LexException, PrematureEndOfInput
|
||||
from hy._compat import PY3
|
||||
|
||||
import ast
|
||||
@ -27,7 +26,7 @@ def _ast_spotcheck(arg, root, secondary):
|
||||
|
||||
|
||||
def can_compile(expr):
|
||||
return hy_compile(hy_parse(expr), "__main__")
|
||||
return hy_compile(hy_parse(expr), __name__)
|
||||
|
||||
|
||||
def can_eval(expr):
|
||||
@ -35,21 +34,16 @@ def can_eval(expr):
|
||||
|
||||
|
||||
def cant_compile(expr):
|
||||
try:
|
||||
hy_compile(hy_parse(expr), "__main__")
|
||||
assert False
|
||||
except HyTypeError as e:
|
||||
with pytest.raises(HyError) as excinfo:
|
||||
hy_compile(hy_parse(expr), __name__)
|
||||
|
||||
if issubclass(excinfo.type, HyLanguageError):
|
||||
assert excinfo.value.msg
|
||||
return excinfo.value
|
||||
elif issubclass(excinfo.type, HyCompileError):
|
||||
# Anything that can't be compiled should raise a user friendly
|
||||
# error, otherwise it's a compiler bug.
|
||||
assert isinstance(e.expression, HyObject)
|
||||
assert e.message
|
||||
return e
|
||||
except HyCompileError as e:
|
||||
# Anything that can't be compiled should raise a user friendly
|
||||
# error, otherwise it's a compiler bug.
|
||||
assert isinstance(e.exception, HyTypeError)
|
||||
assert e.traceback
|
||||
return e
|
||||
return excinfo.value
|
||||
|
||||
|
||||
def s(x):
|
||||
@ -60,11 +54,9 @@ def test_ast_bad_type():
|
||||
"Make sure AST breakage can happen"
|
||||
class C:
|
||||
pass
|
||||
try:
|
||||
hy_compile(C(), "__main__")
|
||||
assert True is False
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
hy_compile(C(), __name__, filename='<string>', source='')
|
||||
|
||||
|
||||
def test_empty_expr():
|
||||
@ -473,8 +465,8 @@ def test_lambda_list_keywords_kwonly():
|
||||
assert code.body[0].args.kw_defaults[1].n == 2
|
||||
else:
|
||||
exception = cant_compile(kwonly_demo)
|
||||
assert isinstance(exception, HyTypeError)
|
||||
message, = exception.args
|
||||
assert isinstance(exception, HyLanguageError)
|
||||
message = exception.args[0]
|
||||
assert message == "&kwonly parameters require Python 3"
|
||||
|
||||
|
||||
@ -489,9 +481,9 @@ def test_lambda_list_keywords_mixed():
|
||||
|
||||
def test_missing_keyword_argument_value():
|
||||
"""Ensure the compiler chokes on missing keyword argument values."""
|
||||
with pytest.raises(HyTypeError) as excinfo:
|
||||
with pytest.raises(HyLanguageError) as excinfo:
|
||||
can_compile("((fn [x] x) :x)")
|
||||
assert excinfo.value.message == "Keyword argument :x needs a value."
|
||||
assert excinfo.value.msg == "Keyword argument :x needs a value."
|
||||
|
||||
|
||||
def test_ast_unicode_strings():
|
||||
@ -500,7 +492,7 @@ def test_ast_unicode_strings():
|
||||
def _compile_string(s):
|
||||
hy_s = HyString(s)
|
||||
|
||||
code = hy_compile([hy_s], "__main__")
|
||||
code = hy_compile([hy_s], __name__, filename='<string>', source=s)
|
||||
# We put hy_s in a list so it isn't interpreted as a docstring.
|
||||
|
||||
# code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))])
|
||||
@ -541,19 +533,19 @@ Only one leading newline should be removed.
|
||||
|
||||
def test_compile_error():
|
||||
"""Ensure we get compile error in tricky cases"""
|
||||
with pytest.raises(HyTypeError) as excinfo:
|
||||
with pytest.raises(HyLanguageError) as excinfo:
|
||||
can_compile("(fn [] (in [1 2 3]))")
|
||||
|
||||
|
||||
def test_for_compile_error():
|
||||
"""Ensure we get compile error in tricky 'for' cases"""
|
||||
with pytest.raises(LexException) as excinfo:
|
||||
with pytest.raises(PrematureEndOfInput) as excinfo:
|
||||
can_compile("(fn [] (for)")
|
||||
assert excinfo.value.message == "Premature end of input"
|
||||
assert excinfo.value.msg == "Premature end of input"
|
||||
|
||||
with pytest.raises(LexException) as excinfo:
|
||||
can_compile("(fn [] (for)))")
|
||||
assert excinfo.value.message == "Ran into a RPAREN where it wasn't expected."
|
||||
assert excinfo.value.msg == "Ran into a RPAREN where it wasn't expected."
|
||||
|
||||
cant_compile("(fn [] (for [x] x))")
|
||||
|
||||
@ -605,13 +597,13 @@ def test_setv_builtins():
|
||||
|
||||
|
||||
def test_top_level_unquote():
|
||||
with pytest.raises(HyTypeError) as excinfo:
|
||||
with pytest.raises(HyLanguageError) as excinfo:
|
||||
can_compile("(unquote)")
|
||||
assert excinfo.value.message == "The special form 'unquote' is not allowed here"
|
||||
assert excinfo.value.msg == "The special form 'unquote' is not allowed here"
|
||||
|
||||
with pytest.raises(HyTypeError) as excinfo:
|
||||
with pytest.raises(HyLanguageError) as excinfo:
|
||||
can_compile("(unquote-splice)")
|
||||
assert excinfo.value.message == "The special form 'unquote-splice' is not allowed here"
|
||||
assert excinfo.value.msg == "The special form 'unquote-splice' is not allowed here"
|
||||
|
||||
|
||||
def test_lots_of_comment_lines():
|
||||
|
@ -14,10 +14,10 @@ from fractions import Fraction
|
||||
import pytest
|
||||
|
||||
import hy
|
||||
from hy.errors import HyTypeError
|
||||
from hy.lex import hy_parse
|
||||
from hy.lex.exceptions import LexException
|
||||
from hy.compiler import hy_compile
|
||||
from hy.errors import HyLanguageError
|
||||
from hy.lex.exceptions import PrematureEndOfInput
|
||||
from hy.compiler import hy_eval, hy_compile
|
||||
from hy.importer import HyLoader, cache_from_source
|
||||
|
||||
try:
|
||||
@ -57,7 +57,7 @@ def test_runpy():
|
||||
|
||||
|
||||
def test_stringer():
|
||||
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
|
||||
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), __name__)
|
||||
|
||||
assert type(_ast.body[0]) == ast.FunctionDef
|
||||
|
||||
@ -79,14 +79,8 @@ def test_imports():
|
||||
def test_import_error_reporting():
|
||||
"Make sure that (import) reports errors correctly."
|
||||
|
||||
def _import_error_test():
|
||||
try:
|
||||
_ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
|
||||
except HyTypeError:
|
||||
return "Error reported"
|
||||
|
||||
assert _import_error_test() == "Error reported"
|
||||
assert _import_error_test() is not None
|
||||
with pytest.raises(HyLanguageError):
|
||||
hy_compile(hy_parse("(import \"sys\")"), __name__)
|
||||
|
||||
|
||||
def test_import_error_cleanup():
|
||||
@ -124,7 +118,7 @@ def test_import_autocompiles():
|
||||
|
||||
def test_eval():
|
||||
def eval_str(s):
|
||||
return hy.eval(hy.read_str(s))
|
||||
return hy_eval(hy.read_str(s), filename='<string>', source=s)
|
||||
|
||||
assert eval_str('[1 2 3]') == [1, 2, 3]
|
||||
assert eval_str('{"dog" "bark" "cat" "meow"}') == {
|
||||
@ -205,8 +199,7 @@ def test_reload():
|
||||
assert mod.a == 11
|
||||
assert mod.b == 20
|
||||
|
||||
# Now cause a `LexException`, and confirm that the good module and its
|
||||
# contents stick around.
|
||||
# Now cause a syntax error
|
||||
unlink(source)
|
||||
|
||||
with open(source, "w") as f:
|
||||
@ -214,7 +207,7 @@ def test_reload():
|
||||
f.write("(setv a 11")
|
||||
f.write("(setv b (// 20 1))")
|
||||
|
||||
with pytest.raises(LexException):
|
||||
with pytest.raises(PrematureEndOfInput):
|
||||
reload(mod)
|
||||
|
||||
mod = sys.modules.get(TESTFN)
|
||||
|
@ -50,8 +50,7 @@ def test_preprocessor_exceptions():
|
||||
""" Test that macro expansion raises appropriate exceptions"""
|
||||
with pytest.raises(HyMacroExpansionError) as excinfo:
|
||||
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
|
||||
assert "_hy_anon_fn_" not in excinfo.value.message
|
||||
assert "TypeError" not in excinfo.value.message
|
||||
assert "_hy_anon_" not in excinfo.value.msg
|
||||
|
||||
|
||||
def test_macroexpand_nan():
|
||||
|
@ -687,5 +687,5 @@ result['y in globals'] = 'y' in globals()")
|
||||
(doc doc)
|
||||
(setv out_err (.readouterr capsys))
|
||||
(assert (.startswith (.strip (first out_err))
|
||||
"Help on function (doc) in module hy.core.macros:"))
|
||||
"Help on function doc in module hy.core.macros:"))
|
||||
(assert (empty? (second out_err))))
|
||||
|
@ -7,7 +7,7 @@
|
||||
[sys :as systest]
|
||||
re
|
||||
[operator [or_]]
|
||||
[hy.errors [HyTypeError]]
|
||||
[hy.errors [HyLanguageError]]
|
||||
pytest)
|
||||
(import sys)
|
||||
|
||||
@ -68,16 +68,16 @@
|
||||
"NATIVE: test that setv doesn't work on names Python can't assign to
|
||||
and that we can't mangle"
|
||||
(try (eval '(setv None 1))
|
||||
(except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
|
||||
(except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
|
||||
(try (eval '(defn None [] (print "hello")))
|
||||
(except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
|
||||
(except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
|
||||
(when PY3
|
||||
(try (eval '(setv False 1))
|
||||
(except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
|
||||
(except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
|
||||
(try (eval '(setv True 0))
|
||||
(except [e [TypeError]] (assert (in "Can't assign to" (str e)))))
|
||||
(except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
|
||||
(try (eval '(defn True [] (print "hello")))
|
||||
(except [e [TypeError]] (assert (in "Can't assign to" (str e)))))))
|
||||
(except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))))
|
||||
|
||||
|
||||
(defn test-setv-pairs []
|
||||
@ -87,7 +87,7 @@
|
||||
(assert (= b 2))
|
||||
(setv y 0 x 1 y x)
|
||||
(assert (= y 1))
|
||||
(with [(pytest.raises HyTypeError)]
|
||||
(with [(pytest.raises HyLanguageError)]
|
||||
(eval '(setv a 1 b))))
|
||||
|
||||
|
||||
@ -144,29 +144,29 @@
|
||||
(do
|
||||
(eval '(setv (do 1 2) 1))
|
||||
(assert False))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "Can't assign or delete a non-expression"))))
|
||||
(except [e HyLanguageError]
|
||||
(assert (= e.msg "Can't assign or delete a non-expression"))))
|
||||
|
||||
(try
|
||||
(do
|
||||
(eval '(setv 1 1))
|
||||
(assert False))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "Can't assign or delete a HyInteger"))))
|
||||
(except [e HyLanguageError]
|
||||
(assert (= e.msg "Can't assign or delete a HyInteger"))))
|
||||
|
||||
(try
|
||||
(do
|
||||
(eval '(setv {1 2} 1))
|
||||
(assert False))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "Can't assign or delete a HyDict"))))
|
||||
(except [e HyLanguageError]
|
||||
(assert (= e.msg "Can't assign or delete a HyDict"))))
|
||||
|
||||
(try
|
||||
(do
|
||||
(eval '(del 1 1))
|
||||
(assert False))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "Can't assign or delete a HyInteger")))))
|
||||
(except [e HyLanguageError]
|
||||
(assert (= e.msg "Can't assign or delete a HyInteger")))))
|
||||
|
||||
|
||||
(defn test-no-str-as-sym []
|
||||
|
@ -3,7 +3,7 @@
|
||||
;; license. See the LICENSE.
|
||||
|
||||
(import pytest
|
||||
[hy.errors [HyTypeError]])
|
||||
[hy.errors [HyTypeError HyMacroExpansionError]])
|
||||
|
||||
(defmacro rev [&rest body]
|
||||
"Execute the `body` statements in reverse"
|
||||
@ -66,13 +66,13 @@
|
||||
(try
|
||||
(eval '(defmacro f [&kwonly a b]))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "macros cannot use &kwonly")))
|
||||
(assert (= e.msg "macros cannot use &kwonly")))
|
||||
(else (assert False)))
|
||||
|
||||
(try
|
||||
(eval '(defmacro f [&kwargs kw]))
|
||||
(except [e HyTypeError]
|
||||
(assert (= e.message "macros cannot use &kwargs")))
|
||||
(assert (= e.msg "macros cannot use &kwargs")))
|
||||
(else (assert False))))
|
||||
|
||||
(defn test-fn-calling-macro []
|
||||
@ -162,8 +162,8 @@
|
||||
")
|
||||
;; expand the macro twice, should use a different
|
||||
;; gensym each time
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv s1 (to_source _ast1))
|
||||
(setv s2 (to_source _ast2))
|
||||
;; and make sure there is something new that starts with _;G|
|
||||
@ -189,8 +189,8 @@
|
||||
")
|
||||
;; expand the macro twice, should use a different
|
||||
;; gensym each time
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv s1 (to_source _ast1))
|
||||
(setv s2 (to_source _ast2))
|
||||
(assert (in (mangle "_;a|") s1))
|
||||
@ -213,8 +213,8 @@
|
||||
")
|
||||
;; expand the macro twice, should use a different
|
||||
;; gensym each time
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv s1 (to_source _ast1))
|
||||
(setv s2 (to_source _ast2))
|
||||
(assert (in (mangle "_;res|") s1))
|
||||
@ -224,7 +224,7 @@
|
||||
;; defmacro/g! didn't like numbers initially because they
|
||||
;; don't have a startswith method and blew up during expansion
|
||||
(setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))")
|
||||
(assert (hy-compile (hy-parse macro2) "foo")))
|
||||
(assert (hy-compile (hy-parse macro2) __name__)))
|
||||
|
||||
(defn test-defmacro! []
|
||||
;; defmacro! must do everything defmacro/g! can
|
||||
@ -243,8 +243,8 @@
|
||||
")
|
||||
;; expand the macro twice, should use a different
|
||||
;; gensym each time
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) "foo"))
|
||||
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
|
||||
(setv s1 (to_source _ast1))
|
||||
(setv s2 (to_source _ast2))
|
||||
(assert (in (mangle "_;res|") s1))
|
||||
@ -254,7 +254,7 @@
|
||||
;; defmacro/g! didn't like numbers initially because they
|
||||
;; don't have a startswith method and blew up during expansion
|
||||
(setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))")
|
||||
(assert (hy-compile (hy-parse macro2) "foo"))
|
||||
(assert (hy-compile (hy-parse macro2) __name__))
|
||||
|
||||
(defmacro! foo! [o!foo] `(do ~g!foo ~g!foo))
|
||||
;; test that o! becomes g!
|
||||
@ -483,3 +483,37 @@ in expansions."
|
||||
|
||||
(test-macro)
|
||||
(assert (= blah 1)))
|
||||
|
||||
|
||||
(defn test-macro-errors []
|
||||
(import traceback
|
||||
[hy.importer [hy-parse]])
|
||||
|
||||
(setv test-expr (hy-parse "(defmacro blah [x] `(print ~@z)) (blah y)"))
|
||||
|
||||
(with [excinfo (pytest.raises HyMacroExpansionError)]
|
||||
(eval test-expr))
|
||||
|
||||
(setv output (traceback.format_exception_only
|
||||
excinfo.type excinfo.value))
|
||||
(setv output (cut (.splitlines (.strip (first output))) 1))
|
||||
|
||||
(setv expected [" File \"<string>\", line 1"
|
||||
" (defmacro blah [x] `(print ~@z)) (blah y)"
|
||||
" ^------^"
|
||||
"expanding macro blah"
|
||||
" NameError: global name 'z' is not defined"])
|
||||
|
||||
(assert (= (cut expected 0 -1) (cut output 0 -1)))
|
||||
(assert (or (= (get expected -1) (get output -1))
|
||||
;; Handle PyPy's peculiarities
|
||||
(= (.replace (get expected -1) "global " "") (get output -1))))
|
||||
|
||||
|
||||
;; This should throw a `HyWrapperError` that gets turned into a
|
||||
;; `HyMacroExpansionError`.
|
||||
(with [excinfo (pytest.raises HyMacroExpansionError)]
|
||||
(eval '(do (defmacro wrap-error-test []
|
||||
(fn []))
|
||||
(wrap-error-test))))
|
||||
(assert (in "HyWrapperError" (str excinfo.value))))
|
||||
|
@ -28,7 +28,7 @@
|
||||
(defmacro forbid [expr]
|
||||
`(assert (try
|
||||
(eval '~expr)
|
||||
(except [TypeError] True)
|
||||
(except [[TypeError SyntaxError]] True)
|
||||
(else (raise AssertionError)))))
|
||||
|
||||
|
||||
|
@ -6,11 +6,11 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from hy.importer import cache_from_source
|
||||
from hy._compat import PY3
|
||||
|
||||
import pytest
|
||||
|
||||
@ -123,7 +123,16 @@ def test_bin_hy_stdin_as_arrow():
|
||||
|
||||
def test_bin_hy_stdin_error_underline_alignment():
|
||||
_, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
|
||||
assert "\n (mabcdefghi)\n ^----------^" in err
|
||||
|
||||
msg_idx = err.rindex(" (mabcdefghi)")
|
||||
assert msg_idx
|
||||
err_parts = err[msg_idx:].splitlines()
|
||||
assert err_parts[1].startswith(" ^----------^")
|
||||
assert err_parts[2].startswith("expanding macro mabcdefghi")
|
||||
assert (err_parts[3].startswith(" TypeError: mabcdefghi") or
|
||||
# PyPy can use a function's `__name__` instead of
|
||||
# `__code__.co_name`.
|
||||
err_parts[3].startswith(" TypeError: (mabcdefghi)"))
|
||||
|
||||
|
||||
def test_bin_hy_stdin_except_do():
|
||||
@ -149,10 +158,66 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
|
||||
# inside run_cmd.
|
||||
_, err = run_cmd("hy", """
|
||||
(import hy.errors)
|
||||
(raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""")
|
||||
(raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
|
||||
assert "AZ" in err
|
||||
|
||||
|
||||
def test_bin_hy_error_parts_length():
|
||||
"""Confirm that exception messages print arrows surrounding the affected
|
||||
expression."""
|
||||
prg_str = """
|
||||
(import hy.errors
|
||||
[hy.importer [hy-parse]])
|
||||
|
||||
(setv test-expr (hy-parse "(+ 1\n\n'a 2 3\n\n 1)"))
|
||||
(setv test-expr.start-line {})
|
||||
(setv test-expr.start-column {})
|
||||
(setv test-expr.end-column {})
|
||||
|
||||
(raise (hy.errors.HyLanguageError
|
||||
"this\nis\na\nmessage"
|
||||
test-expr
|
||||
None
|
||||
None))
|
||||
"""
|
||||
|
||||
# Up-arrows right next to each other.
|
||||
_, err = run_cmd("hy", prg_str.format(3, 1, 2))
|
||||
|
||||
msg_idx = err.rindex("HyLanguageError:")
|
||||
assert msg_idx
|
||||
err_parts = err[msg_idx:].splitlines()[1:]
|
||||
|
||||
expected = [' File "<string>", line 3',
|
||||
' \'a 2 3',
|
||||
' ^^',
|
||||
'this',
|
||||
'is',
|
||||
'a',
|
||||
'message']
|
||||
|
||||
for obs, exp in zip(err_parts, expected):
|
||||
assert obs.startswith(exp)
|
||||
|
||||
# Make sure only one up-arrow is printed
|
||||
_, err = run_cmd("hy", prg_str.format(3, 1, 1))
|
||||
|
||||
msg_idx = err.rindex("HyLanguageError:")
|
||||
assert msg_idx
|
||||
err_parts = err[msg_idx:].splitlines()[1:]
|
||||
assert err_parts[2] == ' ^'
|
||||
|
||||
# Make sure lines are printed in between arrows separated by more than one
|
||||
# character.
|
||||
_, err = run_cmd("hy", prg_str.format(3, 1, 6))
|
||||
print(err)
|
||||
|
||||
msg_idx = err.rindex("HyLanguageError:")
|
||||
assert msg_idx
|
||||
err_parts = err[msg_idx:].splitlines()[1:]
|
||||
assert err_parts[2] == ' ^----^'
|
||||
|
||||
|
||||
def test_bin_hy_stdin_bad_repr():
|
||||
# https://github.com/hylang/hy/issues/1389
|
||||
output, err = run_cmd("hy", """
|
||||
@ -423,3 +488,87 @@ def test_bin_hy_macro_require():
|
||||
assert os.path.exists(cache_from_source(test_file))
|
||||
output, _ = run_cmd("hy {}".format(test_file))
|
||||
assert "abc" == output.strip()
|
||||
|
||||
|
||||
def test_bin_hy_tracebacks():
|
||||
"""Make sure the printed tracebacks are correct."""
|
||||
|
||||
# We want the filtered tracebacks.
|
||||
os.environ['HY_DEBUG'] = ''
|
||||
|
||||
def req_err(x):
|
||||
assert x == '{}HyRequireError: No module named {}'.format(
|
||||
'hy.errors.' if PY3 else '',
|
||||
(repr if PY3 else str)('not_a_real_module'))
|
||||
|
||||
# Modeled after
|
||||
# > python -c 'import not_a_real_module'
|
||||
# Traceback (most recent call last):
|
||||
# File "<string>", line 1, in <module>
|
||||
# ImportError: No module named not_a_real_module
|
||||
_, error = run_cmd('hy', '(require not-a-real-module)')
|
||||
error_lines = error.splitlines()
|
||||
if error_lines[-1] == '':
|
||||
del error_lines[-1]
|
||||
assert len(error_lines) <= 10
|
||||
# Rough check for the internal traceback filtering
|
||||
req_err(error_lines[4 if PY3 else -1])
|
||||
|
||||
_, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1)
|
||||
error_lines = error.splitlines()
|
||||
assert len(error_lines) <= 4
|
||||
req_err(error_lines[-1])
|
||||
|
||||
output, error = run_cmd('hy -i "(require not-a-real-module)"')
|
||||
assert output.startswith('=> ')
|
||||
print(error.splitlines())
|
||||
req_err(error.splitlines()[2 if PY3 else -3])
|
||||
|
||||
# Modeled after
|
||||
# > python -c 'print("hi'
|
||||
# File "<string>", line 1
|
||||
# print("hi
|
||||
# ^
|
||||
# SyntaxError: EOL while scanning string literal
|
||||
_, error = run_cmd(r'hy -c "(print \""', expect=1)
|
||||
peoi_re = (
|
||||
r'Traceback \(most recent call last\):\n'
|
||||
r' File "(?:<string>|string-[0-9a-f]+)", line 1\n'
|
||||
r' \(print "\n'
|
||||
r' \^\n' +
|
||||
r'{}PrematureEndOfInput: Partial string literal\n'.format(
|
||||
r'hy\.lex\.exceptions\.' if PY3 else ''))
|
||||
assert re.search(peoi_re, error)
|
||||
|
||||
# Modeled after
|
||||
# > python -i -c "print('"
|
||||
# File "<string>", line 1
|
||||
# print('
|
||||
# ^
|
||||
# SyntaxError: EOL while scanning string literal
|
||||
# >>>
|
||||
output, error = run_cmd(r'hy -i "(print \""')
|
||||
assert output.startswith('=> ')
|
||||
assert re.match(peoi_re, error)
|
||||
|
||||
# Modeled after
|
||||
# > python -c 'print(a)'
|
||||
# Traceback (most recent call last):
|
||||
# File "<string>", line 1, in <module>
|
||||
# NameError: name 'a' is not defined
|
||||
output, error = run_cmd('hy -c "(print a)"', expect=1)
|
||||
error_lines = error.splitlines()
|
||||
assert error_lines[3] == ' File "<string>", line 1, in <module>'
|
||||
# PyPy will add "global" to this error message, so we work around that.
|
||||
assert error_lines[-1].strip().replace(' global', '') == (
|
||||
"NameError: name 'a' is not defined")
|
||||
|
||||
# Modeled after
|
||||
# > python -c 'compile()'
|
||||
# Traceback (most recent call last):
|
||||
# File "<string>", line 1, in <module>
|
||||
# TypeError: Required argument 'source' (pos 1) not found
|
||||
output, error = run_cmd('hy -c "(compile)"', expect=1)
|
||||
error_lines = error.splitlines()
|
||||
assert error_lines[-2] == ' File "<string>", line 1, in <module>'
|
||||
assert error_lines[-1].startswith('TypeError')
|
||||
|
@ -1,18 +1,46 @@
|
||||
# Copyright 2019 the authors.
|
||||
# This file is part of Hy, which is free software licensed under the Expat
|
||||
# license. See the LICENSE.
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
|
||||
from math import isnan
|
||||
from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
|
||||
HyString, HyDict, HyList, HySet, HyKeyword)
|
||||
from hy.lex import tokenize
|
||||
from hy.lex.exceptions import LexException, PrematureEndOfInput
|
||||
import pytest
|
||||
from hy.errors import hy_exc_handler
|
||||
|
||||
def peoi(): return pytest.raises(PrematureEndOfInput)
|
||||
def lexe(): return pytest.raises(LexException)
|
||||
|
||||
|
||||
def check_ex(execinfo, expected):
|
||||
output = traceback.format_exception_only(execinfo.type, execinfo.value)
|
||||
assert output[:-1] == expected[:-1]
|
||||
# Python 2.7 doesn't give the full exception name, so we compensate.
|
||||
assert output[-1].endswith(expected[-1])
|
||||
|
||||
|
||||
def check_trace_output(capsys, execinfo, expected):
|
||||
sys.__excepthook__(execinfo.type, execinfo.value, execinfo.tb)
|
||||
captured_wo_filtering = capsys.readouterr()[-1].strip('\n')
|
||||
|
||||
hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb)
|
||||
captured_w_filtering = capsys.readouterr()[-1].strip('\n')
|
||||
|
||||
output = captured_w_filtering.split('\n')
|
||||
|
||||
# Make sure the filtered frames aren't the same as the unfiltered ones.
|
||||
assert output[:-1] != captured_wo_filtering.split('\n')[:-1]
|
||||
# Remove the origin frame lines.
|
||||
assert output[3:-1] == expected[:-1]
|
||||
# Python 2.7 doesn't give the full exception name, so we compensate.
|
||||
assert output[-1].endswith(expected[-1])
|
||||
|
||||
|
||||
def test_lex_exception():
|
||||
""" Ensure tokenize throws a fit on a partial input """
|
||||
with peoi(): tokenize("(foo")
|
||||
@ -30,8 +58,13 @@ def test_unbalanced_exception():
|
||||
def test_lex_single_quote_err():
|
||||
"Ensure tokenizing \"' \" throws a LexException that can be stringified"
|
||||
# https://github.com/hylang/hy/issues/1252
|
||||
with lexe() as e: tokenize("' ")
|
||||
assert "Could not identify the next token" in str(e.value)
|
||||
with lexe() as execinfo:
|
||||
tokenize("' ")
|
||||
check_ex(execinfo, [
|
||||
' File "<string>", line 1\n',
|
||||
" '\n",
|
||||
' ^\n',
|
||||
'LexException: Could not identify the next token.\n'])
|
||||
|
||||
|
||||
def test_lex_expression_symbols():
|
||||
@ -74,7 +107,11 @@ def test_lex_strings_exception():
|
||||
""" Make sure tokenize throws when codec can't decode some bytes"""
|
||||
with lexe() as execinfo:
|
||||
tokenize('\"\\x8\"')
|
||||
assert "Can't convert \"\\x8\" to a HyString" in str(execinfo.value)
|
||||
check_ex(execinfo, [
|
||||
' File "<string>", line 1\n',
|
||||
' "\\x8"\n',
|
||||
' ^\n',
|
||||
'LexException: Can\'t convert "\\x8" to a HyString\n'])
|
||||
|
||||
|
||||
def test_lex_bracket_strings():
|
||||
@ -180,7 +217,16 @@ def test_lex_digit_separators():
|
||||
|
||||
|
||||
def test_lex_bad_attrs():
|
||||
with lexe(): tokenize("1.foo")
|
||||
with lexe() as execinfo:
|
||||
tokenize("1.foo")
|
||||
check_ex(execinfo, [
|
||||
' File "<string>", line 1\n',
|
||||
' 1.foo\n',
|
||||
' ^\n',
|
||||
'LexException: Cannot access attribute on anything other'
|
||||
' than a name (in order to get attributes of expressions,'
|
||||
' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)\n'])
|
||||
|
||||
with lexe(): tokenize("0.foo")
|
||||
with lexe(): tokenize("1.5.foo")
|
||||
with lexe(): tokenize("1e3.foo")
|
||||
@ -419,3 +465,27 @@ def test_discard():
|
||||
assert tokenize("a '#_b c") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("c")])]
|
||||
assert tokenize("a '#_b #_c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
|
||||
assert tokenize("a '#_ #_b c d") == [HySymbol("a"), HyExpression([HySymbol("quote"), HySymbol("d")])]
|
||||
|
||||
|
||||
def test_lex_exception_filtering(capsys):
|
||||
"""Confirm that the exception filtering works for lexer errors."""
|
||||
|
||||
# First, test for PrematureEndOfInput
|
||||
with peoi() as execinfo:
|
||||
tokenize(" \n (foo\n \n")
|
||||
check_trace_output(capsys, execinfo, [
|
||||
' File "<string>", line 2',
|
||||
' (foo',
|
||||
' ^',
|
||||
'PrematureEndOfInput: Premature end of input'])
|
||||
|
||||
# Now, for a generic LexException
|
||||
with lexe() as execinfo:
|
||||
tokenize(" \n\n 1.foo ")
|
||||
check_trace_output(capsys, execinfo, [
|
||||
' File "<string>", line 3',
|
||||
' 1.foo',
|
||||
' ^',
|
||||
'LexException: Cannot access attribute on anything other'
|
||||
' than a name (in order to get attributes of expressions,'
|
||||
' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)'])
|
||||
|
Loading…
Reference in New Issue
Block a user