diff --git a/Makefile b/Makefile index 4658b6f..13aaa6f 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ ifeq (Python 2.6,$(findstring Python 2.6,$(shell python -V 2>&1))) endif $(pip) install -r requirements-travis.txt $(pip) install coveralls - $(pip) install --allow-all-external -e . + $(pip) install -e . coveralls: $(coveralls) diff --git a/NEWS.rst b/NEWS.rst index 235f34b..689dae5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,5 +1,12 @@ .. default-role:: code +Unreleased +============================== + +Bug Fixes +------------------------------ +* Fix `(return)` so it works correctly to exit a Python 2 generator + 0.14.0 ============================== diff --git a/hy/compiler.py b/hy/compiler.py index ccacdce..e6a888e 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -16,6 +16,7 @@ from hy._compat import ( raise_empty) from hy.macros import require, macroexpand, tag_macroexpand import hy.importer +import hy.inspect import traceback import importlib @@ -24,7 +25,6 @@ import ast import sys import keyword import copy -import inspect from collections import defaultdict from cmath import isnan @@ -441,10 +441,11 @@ class HyASTCompiler(object): # _compile_table[atom_type] is a method for compiling this # type of atom, so call it. If it has an extra parameter, # pass in `atom_type`. - arity = len(inspect.getargspec(_compile_table[atom_type])[0]) - ret = (_compile_table[atom_type](self, atom, atom_type) + atom_compiler = _compile_table[atom_type] + arity = hy.inspect.get_arity(atom_compiler) + ret = (atom_compiler(self, atom, atom_type) if arity == 3 - else _compile_table[atom_type](self, atom)) + else atom_compiler(self, atom)) if not isinstance(ret, Result): ret = Result() + ret return ret @@ -2040,8 +2041,10 @@ class HyASTCompiler(object): @checkargs(max=1) def compile_return(self, expr): ret = Result() - if len(expr) > 1: - ret += self.compile(expr[1]) + if len(expr) == 1: + return asty.Return(expr, value=None) + + ret += self.compile(expr[1]) return ret + asty.Return(expr, value=ret.force_expr) @builds("defclass") diff --git a/hy/contrib/sequences.hy b/hy/contrib/sequences.hy index 08630ce..703da2b 100644 --- a/hy/contrib/sequences.hy +++ b/hy/contrib/sequences.hy @@ -29,15 +29,15 @@ (try (while True (yield (get self index)) (setv index (inc index))) - (except [_ IndexError] - (raise StopIteration)))) + (except [IndexError] + (return)))) --len-- (fn [self] "length of the sequence, dangerous for infinite sequences" (setv index (. self high-water)) (try (while True (get self index) (setv index (inc index))) - (except [_ IndexError] + (except [IndexError] (len (. self cache))))) max-items-in-repr 10 --str-- (fn [self] diff --git a/hy/core/language.hy b/hy/core/language.hy index d68a237..cda8d0a 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -422,7 +422,10 @@ Raises ValueError for (not (pos? n))." (for* [val citer] (yield val) (for* [_ (range skip)] - (next citer)))) + (try + (next citer) + (except [StopIteration] + (return)))))) (defn zero? [n] "Check if `n` equals 0." diff --git a/hy/importer.py b/hy/importer.py index 450de27..1b368a8 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -2,6 +2,8 @@ # This file is part of Hy, which is free software licensed under the Expat # license. See the LICENSE. +from __future__ import absolute_import + from hy.compiler import hy_compile, HyTypeError from hy.models import HyObject, HyExpression, HySymbol, replace_hy_obj from hy.lex import tokenize, LexException diff --git a/hy/inspect.py b/hy/inspect.py new file mode 100644 index 0000000..ff972ec --- /dev/null +++ b/hy/inspect.py @@ -0,0 +1,37 @@ +# Copyright 2018 the authors. +# This file is part of Hy, which is free software licensed under the Expat +# license. See the LICENSE. + +from __future__ import absolute_import + +import inspect + +try: + # Check if we have the newer inspect.signature available. + # Otherwise fallback to the legacy getargspec. + inspect.signature # noqa +except AttributeError: + def get_arity(fn): + return len(inspect.getargspec(fn)[0]) + + def has_kwargs(fn): + argspec = inspect.getargspec(fn) + return argspec.keywords is not None + + def format_args(fn): + argspec = inspect.getargspec(fn) + return inspect.formatargspec(*argspec) + +else: + def get_arity(fn): + parameters = inspect.signature(fn).parameters + return sum(1 for param in parameters.values() + if param.kind == param.POSITIONAL_OR_KEYWORD) + + def has_kwargs(fn): + parameters = inspect.signature(fn).parameters + return any(param.kind == param.VAR_KEYWORD + for param in parameters.values()) + + def format_args(fn): + return str(inspect.signature(fn)) diff --git a/hy/macros.py b/hy/macros.py index 9b1a743..0613711 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -2,9 +2,8 @@ # This file is part of Hy, which is free software licensed under the Expat # license. See the LICENSE. -from inspect import getargspec, formatargspec +import hy.inspect from hy.models import replace_hy_obj, HyExpression, HySymbol - from hy.errors import HyTypeError, HyMacroExpansionError from collections import defaultdict @@ -36,8 +35,7 @@ def macro(name): def _(fn): fn.__name__ = '({})'.format(name) try: - argspec = getargspec(fn) - fn._hy_macro_pass_compiler = argspec.keywords is not None + fn._hy_macro_pass_compiler = hy.inspect.has_kwargs(fn) except Exception: # An exception might be raised if fn has arguments with # names that are invalid in Python. @@ -136,9 +134,7 @@ def make_empty_fn_copy(fn): # can continue running. Unfortunately, the error message that might get # raised later on while expanding a macro might not make sense at all. - argspec = getargspec(fn) - formatted_args = formatargspec(*argspec) - + formatted_args = hy.inspect.format_args(fn) fn_str = 'lambda {}: None'.format( formatted_args.lstrip('(').rstrip(')')) empty_fn = eval(fn_str) diff --git a/setup.cfg b/setup.cfg index 3923777..18c8fbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,3 +20,6 @@ ignore_errors = True # Be sure to include Hy test functions whose names end with "?", # which will be mangled to begin with "is_". python_functions=test_* is_test_* +filterwarnings = + once::DeprecationWarning + once::PendingDeprecationWarning diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 4836ad2..0ee88cc 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from hy import HyString from hy.models import HyObject from hy.compiler import hy_compile -from hy.importer import import_buffer_to_hst +from hy.importer import hy_eval, import_buffer_to_hst from hy.errors import HyCompileError, HyTypeError from hy.lex.exceptions import LexException from hy._compat import PY3 @@ -30,6 +30,10 @@ def can_compile(expr): return hy_compile(import_buffer_to_hst(expr), "__main__") +def can_eval(expr): + return hy_eval(import_buffer_to_hst(expr)) + + def cant_compile(expr): try: hy_compile(import_buffer_to_hst(expr), "__main__") @@ -664,3 +668,8 @@ def test_ast_good_yield_from(): def test_ast_bad_yield_from(): "Make sure AST can't compile invalid yield-from" cant_compile("(yield-from)") + + +def test_eval_generator_with_return(): + """Ensure generators with a return statement works.""" + can_eval("(fn [] (yield 1) (yield 2) (return))") diff --git a/tests/test_bin.py b/tests/test_bin.py index e53626e..33701ea 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -37,19 +37,9 @@ def run_cmd(cmd, stdin_data=None, expect=0, dontwritebytecode=False): universal_newlines=True, shell=False, env=env) - if stdin_data is not None: - p.stdin.write(stdin_data) - p.stdin.flush() - p.stdin.close() - # Read stdout and stderr otherwise if the PIPE buffer is full, we might - # wait for ever… - stdout = "" - stderr = "" - while p.poll() is None: - stdout += p.stdout.read() - stderr += p.stderr.read() - assert p.returncode == expect - return stdout, stderr + output = p.communicate(input=stdin_data) + assert p.wait() == expect + return output def rm(fpath): diff --git a/tests/test_lex.py b/tests/test_lex.py index c9009b2..8c236c5 100644 --- a/tests/test_lex.py +++ b/tests/test_lex.py @@ -304,11 +304,11 @@ def test_nospace(): def test_escapes(): """ Ensure we can escape things """ - entry = tokenize("(foo \"foo\\n\")")[0] + entry = tokenize(r"""(foo "foo\n")""")[0] assert entry[1] == "foo\n" - entry = tokenize("(foo \"foo\\s\")")[0] - assert entry[1] == "foo\\s" + entry = tokenize(r"""(foo r"foo\s")""")[0] + assert entry[1] == r"foo\s" def test_unicode_escapes(): diff --git a/tox.ini b/tox.ini index 1f5be88..ad4e2cb 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,8 @@ skipsdist = True [testenv] commands = - pip install --allow-all-external -e . - pytest + pip install -e . + pytest {posargs} passenv = TERM deps =