Retain compiled source and file information for exceptions

This commit refactors the exception/error classes and their handling.
It also retains Hy source strings and their originating file information, when
available, all throughout the core parser and compiler functions.

As well, with these changes, calling code is no longer responsible for providing
source and file details to exceptions,

Closes hylang/hy#657.
This commit is contained in:
Brandon T. Willard 2018-10-28 20:43:17 -05:00 committed by Kodi Arfer
parent 902926c543
commit 51c7efe6e8
14 changed files with 525 additions and 278 deletions

View File

@ -6,7 +6,7 @@ try:
import __builtin__ as builtins import __builtin__ as builtins
except ImportError: except ImportError:
import builtins # NOQA import builtins # NOQA
import sys, keyword import sys, keyword, textwrap
PY3 = sys.version_info[0] >= 3 PY3 = sys.version_info[0] >= 3
PY35 = sys.version_info >= (3, 5) PY35 = sys.version_info >= (3, 5)
@ -22,11 +22,39 @@ bytes_type = bytes if PY3 else str # NOQA
long_type = int if PY3 else long # NOQA long_type = int if PY3 else long # NOQA
string_types = str if PY3 else basestring # NOQA string_types = str if PY3 else basestring # NOQA
#
# Inspired by the same-named `six` functions.
#
if PY3: if PY3:
exec('def raise_empty(t, *args): raise t(*args) from None') raise_src = textwrap.dedent('''
def raise_from(value, from_value):
try:
raise value from from_value
finally:
traceback = None
''')
def reraise(exc_type, value, traceback=None):
try:
raise value.with_traceback(traceback)
finally:
traceback = None
else: else:
def raise_empty(t, *args): def raise_from(value, from_value=None):
raise t(*args) raise value
raise_src = textwrap.dedent('''
def reraise(exc_type, value, traceback=None):
try:
raise exc_type, value, traceback
finally:
traceback = None
''')
raise_code = compile(raise_src, __file__, 'exec')
exec(raise_code)
def isidentifier(x): def isidentifier(x):
if x in ('True', 'False', 'None', 'print'): if x in ('True', 'False', 'None', 'print'):

View File

@ -19,9 +19,9 @@ import astor.code_gen
import hy import hy
from hy.lex import hy_parse, mangle from hy.lex import hy_parse, mangle
from hy.lex.exceptions import LexException, PrematureEndOfInput from hy.lex.exceptions import PrematureEndOfInput
from hy.compiler import HyASTCompiler, hy_compile, hy_eval from hy.compiler import HyASTCompiler, hy_compile, hy_eval
from hy.errors import HyTypeError from hy.errors import HyTypeError, HyLanguageError, HySyntaxError
from hy.importer import runhy from hy.importer import runhy
from hy.completer import completion, Completer from hy.completer import completion, Completer
from hy.macros import macro, require from hy.macros import macro, require
@ -101,15 +101,11 @@ class HyREPL(code.InteractiveConsole, object):
self.showtraceback() self.showtraceback()
try: try:
try: do = hy_parse(source, filename=filename)
do = hy_parse(source) except PrematureEndOfInput:
except PrematureEndOfInput: return True
return True except HySyntaxError as e:
except LexException as e: error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
if e.source is None:
e.source = source
e.filename = filename
error_handler(e, use_simple_traceback=True)
return False return False
try: try:
@ -121,9 +117,12 @@ class HyREPL(code.InteractiveConsole, object):
[ast.Expr(expr_ast.body)]) [ast.Expr(expr_ast.body)])
print(astor.to_source(new_ast)) print(astor.to_source(new_ast))
value = hy_eval(do, self.locals, value = hy_eval(do, self.locals, self.module,
ast_callback=ast_callback, ast_callback=ast_callback,
compiler=self.hy_compiler) compiler=self.hy_compiler,
filename=filename,
source=source)
except HyTypeError as e: except HyTypeError as e:
if e.source is None: if e.source is None:
e.source = source e.source = source
@ -131,7 +130,7 @@ class HyREPL(code.InteractiveConsole, object):
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS) error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
return False return False
except Exception as e: except Exception as e:
error_handler(e) error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
return False return False
if value is not None: if value is not None:
@ -208,17 +207,19 @@ SIMPLE_TRACEBACKS = True
def pretty_error(func, *args, **kw): def pretty_error(func, *args, **kw):
try: try:
return func(*args, **kw) return func(*args, **kw)
except (HyTypeError, LexException) as e: except HyLanguageError as e:
if SIMPLE_TRACEBACKS: if SIMPLE_TRACEBACKS:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(1) sys.exit(1)
raise raise
def run_command(source): def run_command(source, filename=None):
tree = hy_parse(source) tree = hy_parse(source, filename=filename)
require("hy.cmdline", "__main__", assignments="ALL") __main__ = importlib.import_module('__main__')
pretty_error(hy_eval, tree, None, importlib.import_module('__main__')) require("hy.cmdline", __main__, assignments="ALL")
pretty_error(hy_eval, tree, None, __main__, filename=filename,
source=source)
return 0 return 0
@ -340,7 +341,7 @@ def cmdline_handler(scriptname, argv):
if options.command: if options.command:
# User did "hy -c ..." # User did "hy -c ..."
return run_command(options.command) return run_command(options.command, filename='<string>')
if options.mod: if options.mod:
# User did "hy -m ..." # User did "hy -m ..."
@ -356,7 +357,7 @@ def cmdline_handler(scriptname, argv):
if options.args: if options.args:
if options.args[0] == "-": if options.args[0] == "-":
# Read the program from stdin # Read the program from stdin
return run_command(sys.stdin.read()) return run_command(sys.stdin.read(), filename='<stdin>')
else: else:
# User did "hy <filename>" # User did "hy <filename>"
@ -447,11 +448,12 @@ def hy2py_main():
if options.FILE is None or options.FILE == '-': if options.FILE is None or options.FILE == '-':
source = sys.stdin.read() source = sys.stdin.read()
hst = pretty_error(hy_parse, source, filename='<stdin>')
else: else:
with io.open(options.FILE, 'r', encoding='utf-8') as source_file: with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read() source = source_file.read()
hst = hy_parse(source, filename=options.FILE)
hst = pretty_error(hy_parse, source)
if options.with_source: if options.with_source:
# need special printing on Windows in case the # need special printing on Windows in case the
# codepage doesn't support utf-8 characters # codepage doesn't support utf-8 characters

View File

@ -9,20 +9,21 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole, from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack) notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
from hy.errors import HyCompileError, HyTypeError from hy.errors import (HyCompileError, HyTypeError, HyEvalError,
HyInternalError)
from hy.lex import mangle, unmangle from hy.lex import mangle, unmangle
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3, from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
PY35, raise_empty) PY35, reraise)
from hy.macros import require, load_macros, macroexpand, tag_macroexpand from hy.macros import require, load_macros, macroexpand, tag_macroexpand
import hy.core import hy.core
import pkgutil
import traceback import traceback
import importlib import importlib
import inspect import inspect
import pkgutil
import types import types
import ast import ast
import sys import sys
@ -340,22 +341,33 @@ def is_unpack(kind, x):
class HyASTCompiler(object): class HyASTCompiler(object):
"""A Hy-to-Python AST compiler""" """A Hy-to-Python AST compiler"""
def __init__(self, module): def __init__(self, module, filename=None, source=None):
""" """
Parameters Parameters
---------- ----------
module: str or types.ModuleType module: str or types.ModuleType
Module in which the Hy tree is evaluated. Module name or object in which the Hy tree is evaluated.
filename: str, optional
The name of the file for the source to be compiled.
This is optional information for informative error messages and
debugging.
source: str, optional
The source for the file, if any, being compiled. This is optional
information for informative error messages and debugging.
""" """
self.anon_var_count = 0 self.anon_var_count = 0
self.imports = defaultdict(set) self.imports = defaultdict(set)
self.temp_if = None self.temp_if = None
if not inspect.ismodule(module): if not inspect.ismodule(module):
module = importlib.import_module(module) self.module = importlib.import_module(module)
else:
self.module = module
self.module = module self.module_name = self.module.__name__
self.module_name = module.__name__
self.filename = filename
self.source = source
# Hy expects these to be present, so we prep the module for Hy # Hy expects these to be present, so we prep the module for Hy
# compilation. # compilation.
@ -431,13 +443,15 @@ class HyASTCompiler(object):
# nested; so let's re-raise this exception, let's not wrap it in # nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError! # another HyCompileError!
raise raise
except HyTypeError: except HyTypeError as e:
raise reraise(type(e), e, None)
except Exception as e: except Exception as e:
raise_empty(HyCompileError, e, sys.exc_info()[2]) 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): def _syntax_error(self, expr, message):
return HyTypeError(expr, message) return HyTypeError(message, self.filename, expr, self.source)
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False, def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False): oldpy_unpack=False):
@ -1614,7 +1628,29 @@ class HyASTCompiler(object):
def compile_eval_and_compile(self, expr, root, body): def compile_eval_and_compile(self, expr, root, body):
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr) new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
hy_eval(new_expr + body, self.module.__dict__, self.module) try:
hy_eval(new_expr + body,
self.module.__dict__,
self.module,
filename=self.filename,
source=self.source)
except HyInternalError:
# Unexpected "meta" compilation errors need to be treated
# like normal (unexpected) compilation errors at this level
# (or the compilation level preceding this one).
raise
except Exception as e:
# These could be expected Hy language errors (e.g. syntax errors)
# or regular Python runtime errors that do not signify errors in
# the compilation *process* (although compilation did technically
# fail).
# We wrap these exceptions and pass them through.
reraise(HyEvalError,
HyEvalError(str(e),
self.filename,
body,
self.source),
sys.exc_info()[2])
return (self._compile_branch(body) return (self._compile_branch(body)
if ast_str(root) == "eval_and_compile" if ast_str(root) == "eval_and_compile"
@ -1798,8 +1834,13 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False):
def hy_eval(hytree, locals=None, module=None, ast_callback=None, def hy_eval(hytree, locals=None, module=None, ast_callback=None,
compiler=None): compiler=None, filename='<string>', source=None):
"""Evaluates a quoted expression and returns the value. """Evaluates a quoted expression and returns the value.
If you're evaluating hand-crafted AST trees, make sure the line numbers
are set properly. Try `fix_missing_locations` and related functions in the
Python `ast` library.
Examples Examples
-------- --------
=> (eval '(print "Hello World")) => (eval '(print "Hello World"))
@ -1812,8 +1853,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
Parameters Parameters
---------- ----------
hytree: a Hy expression tree hytree: HyObject
Source code to parse. The Hy AST object to evaluate.
locals: dict, optional locals: dict, optional
Local environment in which to evaluate the Hy tree. Defaults to the Local environment in which to evaluate the Hy tree. Defaults to the
@ -1835,6 +1876,19 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
An existing Hy compiler to use for compilation. Also serves as An existing Hy compiler to use for compilation. Also serves as
the `module` value when given. the `module` value when given.
filename: str, optional
The filename corresponding to the source for `tree`. This will be
overridden by the `filename` field of `tree`, if any; otherwise, it
defaults to "<string>". When `compiler` is given, its `filename` field
value is always used.
source: str, optional
A string containing the source code for `tree`. This will be
overridden by the `source` field of `tree`, if any; otherwise,
if `None`, an attempt will be made to obtain it from the module given by
`module`. When `compiler` is given, its `source` field value is always
used.
Returns Returns
------- -------
out : Result of evaluating the Hy compiled tree. out : Result of evaluating the Hy compiled tree.
@ -1849,36 +1903,53 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
if not isinstance(locals, dict): if not isinstance(locals, dict):
raise TypeError("Locals must be a dictionary") raise TypeError("Locals must be a dictionary")
_ast, expr = hy_compile(hytree, module=module, get_expr=True, # Does the Hy AST object come with its own information?
compiler=compiler) filename = getattr(hytree, 'filename', filename) or '<string>'
source = getattr(hytree, 'source', source)
# Spoof the positions in the generated ast... _ast, expr = hy_compile(hytree, module, get_expr=True,
for node in ast.walk(_ast): compiler=compiler, filename=filename,
node.lineno = 1 source=source)
node.col_offset = 1
for node in ast.walk(expr):
node.lineno = 1
node.col_offset = 1
if ast_callback: if ast_callback:
ast_callback(_ast, expr) ast_callback(_ast, expr)
globals = module.__dict__
# Two-step eval: eval() the body of the exec call # Two-step eval: eval() the body of the exec call
eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals) eval(ast_compile(_ast, filename, "exec"),
module.__dict__, locals)
# Then eval the expression context and return that # Then eval the expression context and return that
return eval(ast_compile(expr, "<eval>", "eval"), globals, locals) return eval(ast_compile(expr, filename, "eval"),
module.__dict__, locals)
def hy_compile(tree, module=None, root=ast.Module, get_expr=False, def _module_file_source(module_name, filename, source):
compiler=None): """Try to obtain missing filename and source information from a module name
"""Compile a Hy tree into a Python AST tree. without actually loading the module.
"""
if filename is None or source is None:
mod_loader = pkgutil.get_loader(module_name)
if mod_loader:
if filename is None:
filename = mod_loader.get_filename(module_name)
if source is None:
source = mod_loader.get_source(module_name)
# We need a non-None filename.
filename = filename or '<string>'
return filename, source
def hy_compile(tree, module, root=ast.Module, get_expr=False,
compiler=None, filename=None, source=None):
"""Compile a HyObject tree into a Python AST Module.
Parameters Parameters
---------- ----------
tree: HyObject
The Hy AST object to compile.
module: str or types.ModuleType, optional module: str or types.ModuleType, optional
Module, or name of the module, in which the Hy tree is evaluated. Module, or name of the module, in which the Hy tree is evaluated.
The module associated with `compiler` takes priority over this value. The module associated with `compiler` takes priority over this value.
@ -1893,18 +1964,43 @@ def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
An existing Hy compiler to use for compilation. Also serves as An existing Hy compiler to use for compilation. Also serves as
the `module` value when given. the `module` value when given.
filename: str, optional
The filename corresponding to the source for `tree`. This will be
overridden by the `filename` field of `tree`, if any; otherwise, it
defaults to "<string>". When `compiler` is given, its `filename` field
value is always used.
source: str, optional
A string containing the source code for `tree`. This will be
overridden by the `source` field of `tree`, if any; otherwise,
if `None`, an attempt will be made to obtain it from the module given by
`module`. When `compiler` is given, its `source` field value is always
used.
Returns Returns
------- -------
out : A Python AST tree out : A Python AST tree
""" """
module = get_compiler_module(module, compiler, False) module = get_compiler_module(module, compiler, False)
if isinstance(module, string_types):
if module.startswith('<') and module.endswith('>'):
module = types.ModuleType(module)
else:
module = importlib.import_module(ast_str(module, piecewise=True))
if not inspect.ismodule(module):
raise TypeError('Invalid module type: {}'.format(type(module)))
filename = getattr(tree, 'filename', filename)
source = getattr(tree, 'source', source)
tree = wrap_value(tree) tree = wrap_value(tree)
if not isinstance(tree, HyObject): if not isinstance(tree, HyObject):
raise HyCompileError("`tree` must be a HyObject or capable of " raise TypeError("`tree` must be a HyObject or capable of "
"being promoted to one") "being promoted to one")
compiler = compiler or HyASTCompiler(module) compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
result = compiler.compile(tree) result = compiler.compile(tree)
expr = result.force_expr expr = result.force_expr

View File

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

View File

@ -5,41 +5,65 @@
import traceback import traceback
from functools import reduce
from clint.textui import colored from clint.textui import colored
class HyError(Exception): class HyError(Exception):
""" def __init__(self, message, *args):
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
def __str__(self):
if isinstance(self.exception, HyTypeError):
return str(self.exception)
if self.traceback:
tb = "".join(traceback.format_tb(self.traceback)).strip()
else:
tb = "No traceback available. 😟"
return("Internal Compiler Bug 😱\n%s: %s\nCompilation traceback:\n%s"
% (self.exception.__class__.__name__,
self.exception, tb))
class HyTypeError(TypeError):
def __init__(self, expression, message):
super(HyTypeError, self).__init__(message)
self.expression = expression
self.message = message self.message = message
self.source = None super(HyError, self).__init__(message, *args)
self.filename = None
class HyInternalError(HyError):
"""Unexpected errors occurring during compilation or parsing of Hy code.
Errors sub-classing this are not intended to be user-facing, and will,
hopefully, never be seen by users!
"""
def __init__(self, message, *args):
super(HyInternalError, self).__init__(message, *args)
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, *args):
super(HyLanguageError, self).__init__(message, *args)
class HyCompileError(HyInternalError):
"""Unexpected errors occurring within the compiler."""
class HyTypeError(HyLanguageError, TypeError):
"""TypeErrors occurring during the normal use of Hy."""
def __init__(self, message, filename=None, expression=None, source=None):
"""
Parameters
----------
message: str
The message to display for this error.
filename: str, optional
The filename for the source code generating this error.
expression: HyObject, optional
The Hy expression generating this error.
source: str, optional
The actual source code generating this error.
"""
self.message = message
self.filename = filename
self.expression = expression
self.source = source
super(HyTypeError, self).__init__(message, filename, expression,
source)
def __str__(self): def __str__(self):
@ -93,12 +117,92 @@ class HyTypeError(TypeError):
class HyMacroExpansionError(HyTypeError): class HyMacroExpansionError(HyTypeError):
pass """Errors caused by invalid use of Hy macros.
This, and any errors inheriting from this, are user-facing.
"""
class HyIOError(HyError, IOError): 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.
""" """
Trivial subclass of IOError and HyError, to distinguish between
IOErrors raised by Hy itself as opposed to Hy programs.
class HyIOError(HyInternalError, IOError):
""" Subclass used to distinguish between IOErrors raised by Hy itself as
opposed to Hy programs.
""" """
pass
class HySyntaxError(HyLanguageError, SyntaxError):
"""Error during the Lexing of a Hython expression."""
def __init__(self, message, filename=None, lineno=-1, colno=-1,
source=None):
"""
Parameters
----------
message: str
The exception's message.
filename: str, optional
The filename for the source code generating this error.
lineno: int, optional
The line number of the error.
colno: int, optional
The column number of the error.
source: str, optional
The actual source code generating this error.
"""
self.message = message
self.filename = filename
self.lineno = lineno
self.colno = colno
self.source = source
super(HySyntaxError, self).__init__(message,
# The builtin `SyntaxError` needs a
# tuple.
(filename, lineno, colno, source))
@staticmethod
def from_expression(message, expression, filename=None, source=None):
if not source:
# Maybe the expression object has its own source.
source = getattr(expression, 'source', None)
if not filename:
filename = getattr(expression, 'filename', None)
if source:
lineno = expression.start_line
colno = expression.start_column
end_line = getattr(expression, 'end_line', len(source))
lines = source.splitlines()
source = '\n'.join(lines[lineno-1:end_line])
else:
# We could attempt to extract the source given a filename, but we
# don't.
lineno = colno = -1
return HySyntaxError(message, filename, lineno, colno, source)
def __str__(self):
output = traceback.format_exception_only(SyntaxError, self)
output[-1] = colored.yellow(output[-1])
if len(self.source) > 0:
output[-2] = colored.green(output[-2])
for line in output[::-2]:
if line.strip().startswith(
'File "{}", line'.format(self.filename)):
break
output[-3] = colored.red(output[-3])
# Avoid "...expected str instance, ColoredString found"
return reduce(lambda x, y: x + y, output)

View File

@ -17,10 +17,8 @@ import importlib
from functools import partial from functools import partial
from contextlib import contextmanager from contextlib import contextmanager
from hy.errors import HyTypeError
from hy.compiler import hy_compile, hy_ast_compile_flags from hy.compiler import hy_compile, hy_ast_compile_flags
from hy.lex import hy_parse from hy.lex import hy_parse
from hy.lex.exceptions import LexException
from hy._compat import PY3 from hy._compat import PY3
@ -153,15 +151,9 @@ if PY3:
def _hy_source_to_code(self, data, path, _optimize=-1): def _hy_source_to_code(self, data, path, _optimize=-1):
if _could_be_hy_src(path): if _could_be_hy_src(path):
source = data.decode("utf-8") source = data.decode("utf-8")
try: hy_tree = hy_parse(source, filename=path)
hy_tree = hy_parse(source) with loader_module_obj(self) as module:
with loader_module_obj(self) as module: data = hy_compile(hy_tree, module)
data = hy_compile(hy_tree, module)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = source
e.filename = path
raise
return _py_source_to_code(self, data, path, _optimize=_optimize) return _py_source_to_code(self, data, path, _optimize=_optimize)
@ -287,19 +279,15 @@ else:
fullname = self._fix_name(fullname) fullname = self._fix_name(fullname)
if fullname is None: if fullname is None:
fullname = self.fullname fullname = self.fullname
try:
hy_source = self.get_source(fullname)
hy_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_source = self.get_source(fullname)
hy_ast_compile_flags) hy_tree = hy_parse(hy_source, filename=self.filename)
except (HyTypeError, LexException) as e:
if e.source is None: with loader_module_obj(self) as module:
e.source = hy_source hy_ast = hy_compile(hy_tree, module)
e.filename = self.filename
raise code = compile(hy_ast, self.filename, 'exec',
hy_ast_compile_flags)
if not sys.dont_write_bytecode: if not sys.dont_write_bytecode:
try: try:
@ -453,7 +441,7 @@ else:
try: try:
flags = None flags = None
if _could_be_hy_src(filename): if _could_be_hy_src(filename):
hy_tree = hy_parse(source_str) hy_tree = hy_parse(source_str, filename=filename)
if module is None: if module is None:
module = inspect.getmodule(inspect.stack()[1][0]) module = inspect.getmodule(inspect.stack()[1][0])
@ -465,9 +453,6 @@ else:
codeobject = compile(source, dfile or filename, 'exec', flags) codeobject = compile(source, dfile or filename, 'exec', flags)
except Exception as err: except Exception as err:
if isinstance(err, (HyTypeError, LexException)) and err.source is None:
err.source = source_str
err.filename = filename
py_exc = py_compile.PyCompileError(err.__class__, err, py_exc = py_compile.PyCompileError(err.__class__, err,
dfile or filename) dfile or filename)

View File

@ -8,9 +8,10 @@ import re
import sys import sys
import unicodedata import unicodedata
from hy._compat import str_type, isidentifier, UCS4 from hy._compat import str_type, isidentifier, UCS4, reraise
from hy.lex.exceptions import PrematureEndOfInput, LexException # NOQA from hy.lex.exceptions import PrematureEndOfInput, LexException # NOQA
from hy.models import HyExpression, HySymbol from hy.models import HyExpression, HySymbol
from hy.errors import HySyntaxError
try: try:
from io import StringIO from io import StringIO
@ -18,7 +19,7 @@ except ImportError:
from StringIO import StringIO from StringIO import StringIO
def hy_parse(source): def hy_parse(source, filename='<string>'):
"""Parse a Hy source string. """Parse a Hy source string.
Parameters Parameters
@ -26,31 +27,51 @@ def hy_parse(source):
source: string source: string
Source code to parse. Source code to parse.
filename: string, optional
File name corresponding to source. Defaults to "<string>".
Returns Returns
------- -------
out : instance of `types.CodeType` out : HyExpression
""" """
source = re.sub(r'\A#!.*', '', source) _source = re.sub(r'\A#!.*', '', source)
return HyExpression([HySymbol("do")] + tokenize(source + "\n")) try:
res = HyExpression([HySymbol("do")] +
tokenize(_source + "\n",
filename=filename))
res.source = source
res.filename = filename
return res
except HySyntaxError as e:
reraise(type(e), e, None)
def tokenize(buf): class ParserState(object):
""" def __init__(self, source, filename):
Tokenize a Lisp file or string buffer into internal Hy objects. self.source = source
self.filename = filename
def tokenize(source, filename=None):
""" Tokenize a Lisp file or string buffer into internal Hy objects.
Parameters
----------
source: str
The source to tokenize.
filename: str, optional
The filename corresponding to `source`.
""" """
from hy.lex.lexer import lexer from hy.lex.lexer import lexer
from hy.lex.parser import parser from hy.lex.parser import parser
from rply.errors import LexingError from rply.errors import LexingError
try: try:
return parser.parse(lexer.lex(buf)) return parser.parse(lexer.lex(source),
state=ParserState(source, filename))
except LexingError as e: except LexingError as e:
pos = e.getsourcepos() pos = e.getsourcepos()
raise LexException("Could not identify the next token.", raise LexException("Could not identify the next token.", filename,
pos.lineno, pos.colno, buf) pos.lineno, pos.colno, source)
except LexException as e:
if e.source is None:
e.source = buf
raise
mangle_delim = 'X' mangle_delim = 'X'

View File

@ -1,49 +1,34 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
from hy.errors import HySyntaxError
from hy.errors import HyError
class LexException(HyError): class LexException(HySyntaxError):
"""Error during the Lexing of a Hython expression."""
def __init__(self, message, lineno, colno, source=None):
super(LexException, self).__init__(message)
self.message = message
self.lineno = lineno
self.colno = colno
self.source = source
self.filename = '<stdin>'
def __str__(self): @classmethod
from hy.errors import colored def from_lexer(cls, message, state, token):
source_pos = token.getsourcepos()
if token.source_pos:
lineno = source_pos.lineno
colno = source_pos.colno
else:
lineno = -1
colno = -1
line = self.lineno if state.source:
start = self.colno lines = state.source.splitlines()
if lines[-1] == '':
del lines[-1]
result = "" if lineno < 1:
lineno = len(lines)
if colno < 1:
colno = len(lines[-1])
source = self.source.split("\n") source = lines[lineno - 1]
return cls(message, state.filename, lineno, colno, source)
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
class PrematureEndOfInput(LexException): class PrematureEndOfInput(LexException):
"""We got a premature end of input""" pass
def __init__(self, message):
super(PrematureEndOfInput, self).__init__(message, -1, -1)

View File

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

View File

@ -1,14 +1,16 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import sys
import importlib import importlib
import inspect import inspect
import pkgutil import pkgutil
from hy._compat import PY3, string_types from contextlib import contextmanager
from hy._compat import PY3, string_types, reraise
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle from hy.lex import mangle
from hy.errors import HyTypeError, HyMacroExpansionError from hy.errors import HyTypeError, HyMacroExpansionError
try: try:
@ -257,6 +259,32 @@ def make_empty_fn_copy(fn):
return empty_fn return empty_fn
@contextmanager
def macro_exceptions(module, macro_tree, compiler=None):
try:
yield
except Exception as e:
try:
filename = inspect.getsourcefile(module)
source = inspect.getsource(module)
except TypeError:
if compiler:
filename = compiler.filename
source = compiler.source
if not isinstance(e, HyTypeError):
exc_type = HyMacroExpansionError
msg = "expanding `{}': ".format(macro_tree[0])
msg += str(e).replace("<lambda>()", "", 1).strip()
else:
exc_type = HyTypeError
msg = e.message
reraise(exc_type,
exc_type(msg, filename, macro_tree, source),
sys.exc_info()[2].tb_next)
def macroexpand(tree, module, compiler=None, once=False): def macroexpand(tree, module, compiler=None, once=False):
"""Expand the toplevel macros for the given Hy AST tree. """Expand the toplevel macros for the given Hy AST tree.
@ -324,23 +352,10 @@ def macroexpand(tree, module, compiler=None, once=False):
compiler = HyASTCompiler(module) compiler = HyASTCompiler(module)
opts['compiler'] = compiler opts['compiler'] = compiler
try: with macro_exceptions(module, tree, compiler):
m_copy = make_empty_fn_copy(m) m_copy = make_empty_fn_copy(m)
m_copy(module.__name__, *tree[1:], **opts) m_copy(module.__name__, *tree[1:], **opts)
except TypeError as e:
msg = "expanding `" + str(tree[0]) + "': "
msg += str(e).replace("<lambda>()", "", 1).strip()
raise HyMacroExpansionError(tree, msg)
try:
obj = m(module.__name__, *tree[1:], **opts) obj = m(module.__name__, *tree[1:], **opts)
except HyTypeError as e:
if e.expression is None:
e.expression = tree
raise
except Exception as e:
msg = "expanding `" + str(tree[0]) + "': " + repr(e)
raise HyMacroExpansionError(tree, msg)
if isinstance(obj, HyExpression): if isinstance(obj, HyExpression):
obj.module = inspect.getmodule(m) obj.module = inspect.getmodule(m)
@ -375,7 +390,8 @@ def tag_macroexpand(tag, tree, module):
None) None)
if tag_macro is None: if tag_macro is None:
raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag)) raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
None, tag, None)
expr = tag_macro(tree) expr = tag_macro(tree)

View File

@ -10,7 +10,7 @@ from hy.models import HyObject
from hy.compiler import hy_compile, hy_eval from hy.compiler import hy_compile, hy_eval
from hy.errors import HyCompileError, HyTypeError from hy.errors import HyCompileError, HyTypeError
from hy.lex import hy_parse from hy.lex import hy_parse
from hy.lex.exceptions import LexException from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3 from hy._compat import PY3
import ast import ast
@ -474,7 +474,7 @@ def test_lambda_list_keywords_kwonly():
else: else:
exception = cant_compile(kwonly_demo) exception = cant_compile(kwonly_demo)
assert isinstance(exception, HyTypeError) assert isinstance(exception, HyTypeError)
message, = exception.args message = exception.args[0]
assert message == "&kwonly parameters require Python 3" assert message == "&kwonly parameters require Python 3"
@ -547,7 +547,7 @@ def test_compile_error():
def test_for_compile_error(): def test_for_compile_error():
"""Ensure we get compile error in tricky 'for' cases""" """Ensure we get compile error in tricky 'for' cases"""
with pytest.raises(LexException) as excinfo: with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)") can_compile("(fn [] (for)")
assert excinfo.value.message == "Premature end of input" assert excinfo.value.message == "Premature end of input"

View File

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

View File

@ -149,7 +149,7 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
# inside run_cmd. # inside run_cmd.
_, err = run_cmd("hy", """ _, err = run_cmd("hy", """
(import hy.errors) (import hy.errors)
(raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""") (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
assert "AZ" in err assert "AZ" in err

View File

@ -1,13 +1,15 @@
# Copyright 2019 the authors. # Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat # This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE. # license. See the LICENSE.
import traceback
import pytest
from math import isnan from math import isnan
from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol, from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
HyString, HyDict, HyList, HySet, HyKeyword) HyString, HyDict, HyList, HySet, HyKeyword)
from hy.lex import tokenize from hy.lex import tokenize
from hy.lex.exceptions import LexException, PrematureEndOfInput from hy.lex.exceptions import LexException, PrematureEndOfInput
import pytest
def peoi(): return pytest.raises(PrematureEndOfInput) def peoi(): return pytest.raises(PrematureEndOfInput)
def lexe(): return pytest.raises(LexException) def lexe(): return pytest.raises(LexException)
@ -180,7 +182,23 @@ def test_lex_digit_separators():
def test_lex_bad_attrs(): def test_lex_bad_attrs():
with lexe(): tokenize("1.foo") with lexe() as execinfo:
tokenize("1.foo")
expected = [
' 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')
]
output = traceback.format_exception_only(execinfo.type, execinfo.value)
assert output[:-1:1] == expected[:-1:1]
# Python 2.7 doesn't give the full exception name, so we compensate.
assert output[-1].endswith(expected[-1])
with lexe(): tokenize("0.foo") with lexe(): tokenize("0.foo")
with lexe(): tokenize("1.5.foo") with lexe(): tokenize("1.5.foo")
with lexe(): tokenize("1e3.foo") with lexe(): tokenize("1e3.foo")