From 2d863abc854f515253d72f12b630c837fb8e56eb Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 17 Jul 2017 13:34:39 -0700 Subject: [PATCH 1/6] Implement #* and #** unpacking --- conftest.py | 5 +- hy/compiler.py | 91 +++++++++++++++++++++++---- hy/lex/lexer.py | 1 + hy/lex/parser.py | 16 +++++ tests/native_tests/language.hy | 12 ++++ tests/native_tests/py35_only_tests.hy | 26 ++++++++ tests/native_tests/py3_only_tests.hy | 10 +++ tests/test_bin.py | 15 ++--- 8 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 tests/native_tests/py35_only_tests.hy diff --git a/conftest.py b/conftest.py index 72fa553..0735cf7 100644 --- a/conftest.py +++ b/conftest.py @@ -1,12 +1,13 @@ import _pytest import hy -from hy._compat import PY3 +from hy._compat import PY3, PY35 def pytest_collect_file(parent, path): if (path.ext == ".hy" and "/tests/native_tests/" in path.dirname + "/" and path.basename != "__init__.hy" - and not ("py3_only" in path.basename and not PY3)): + and not ("py3_only" in path.basename and not PY3) + and not ("py35_only" in path.basename and not PY35)): m = _pytest.python.pytest_pycollect_makemodule(path, parent) # Spoof the module name to avoid hitting an assertion in pytest. m.name = m.name[:-len(".hy")] + ".py" diff --git a/hy/compiler.py b/hy/compiler.py index b990332..97a4c73 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -359,6 +359,13 @@ def checkargs(exact=None, min=None, max=None, even=None, multiple=None): return _dec +def is_unpack(kind, x): + return (isinstance(x, HyExpression) + and len(x) > 0 + and isinstance(x[0], HySymbol) + and x[0] == "unpack_" + kind) + + class HyASTCompiler(object): def __init__(self, module_name): @@ -441,7 +448,8 @@ class HyASTCompiler(object): raise HyCompileError(Exception("Unknown type: `%s'" % _type)) - def _compile_collect(self, exprs, with_kwargs=False): + def _compile_collect(self, exprs, with_kwargs=False, dict_display=False, + oldpy_unpack=False): """Collect the expression contexts from a list of compiled expression. This returns a list of the expression contexts, and the sum of the @@ -451,10 +459,39 @@ class HyASTCompiler(object): compiled_exprs = [] ret = Result() keywords = [] + oldpy_starargs = None + oldpy_kwargs = None exprs_iter = iter(exprs) for expr in exprs_iter: - if with_kwargs and isinstance(expr, HyKeyword): + + 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") + oldpy_starargs = self.compile(expr[1]) + ret += oldpy_starargs + oldpy_starargs = oldpy_starargs.force_expr + + elif is_unpack("mapping", expr): + ret += self.compile(expr[1]) + if PY35: + if dict_display: + compiled_exprs.append(None) + compiled_exprs.append(ret.force_expr) + elif with_kwargs: + keywords.append(ast.keyword( + arg=None, + value=ret.force_expr, + lineno=expr.start_line, + col_offset=expr.start_column)) + elif oldpy_unpack: + if oldpy_kwargs: + raise HyTypeError(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: @@ -474,11 +511,15 @@ class HyASTCompiler(object): value=compiled_value.force_expr, lineno=expr.start_line, col_offset=expr.start_column)) + else: ret += self.compile(expr) compiled_exprs.append(ret.force_expr) - return compiled_exprs, ret, keywords + if oldpy_unpack: + return compiled_exprs, ret, keywords, oldpy_starargs, oldpy_kwargs + else: + return compiled_exprs, ret, keywords def _compile_branch(self, exprs): return _branch(self.compile(expr) for expr in exprs) @@ -610,6 +651,9 @@ class HyASTCompiler(object): new_name = ast.Subscript(value=name.value, slice=name.slice) elif isinstance(name, ast.Attribute): new_name = ast.Attribute(value=name.value, attr=name.attr) + elif PY3 and isinstance(name, ast.Starred): + new_name = ast.Starred( + value=self._storeize(expr, name.value, func)) else: raise HyTypeError(expr, "Can't assign or delete a %s" % @@ -717,6 +761,23 @@ class HyASTCompiler(object): raise HyTypeError(expr, "`%s' can't be used at the top-level" % expr[0]) + @builds("unpack_iterable") + @checkargs(exact=1) + def compile_unpack_iterable(self, expr): + if not PY3: + raise HyTypeError(expr, "`unpack-iterable` isn't allowed here") + ret = self.compile(expr[1]) + ret += ast.Starred(value=ret.force_expr, + lineno=expr.start_line, + col_offset=expr.start_column, + ctx=ast.Load()) + return ret + + @builds("unpack_mapping") + @checkargs(exact=1) + def compile_unpack_mapping(self, expr): + raise HyTypeError(expr, "`unpack-mapping` isn't allowed here") + @builds("do") def compile_do(self, expression): expression.pop(0) @@ -2001,9 +2062,15 @@ class HyASTCompiler(object): return self._compile_keyword_call(expression) if isinstance(fn, HySymbol): - ret = self.compile_atom(fn, expression) - if ret: - return ret + # First check if `fn` is a special form, unless it has an + # `unpack_iterable` in it, since Python's operators (`+`, + # etc.) can't unpack. An exception to this exception is that + # tuple literals (`,`) can unpack. + if fn == "," or not ( + any(is_unpack("iterable", x) for x in expression[1:])): + ret = self.compile_atom(fn, expression) + if ret: + return ret if fn.startswith("."): # (.split "test test") -> "test test".split() @@ -2054,14 +2121,14 @@ class HyASTCompiler(object): else: with_kwargs = True - args, ret, kwargs = self._compile_collect(expression[1:], - with_kwargs) + args, ret, keywords, oldpy_starargs, oldpy_kwargs = self._compile_collect( + expression[1:], with_kwargs, oldpy_unpack=True) ret += ast.Call(func=func.expr, args=args, - keywords=kwargs, - starargs=None, - kwargs=None, + keywords=keywords, + starargs=oldpy_starargs, + kwargs=oldpy_kwargs, lineno=expression.start_line, col_offset=expression.start_column) @@ -2583,7 +2650,7 @@ class HyASTCompiler(object): @builds(HyDict) def compile_dict(self, m): - keyvalues, ret, _ = self._compile_collect(m) + keyvalues, ret, _ = self._compile_collect(m, dict_display=True) ret += ast.Dict(lineno=m.start_line, col_offset=m.start_column, diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py index c6b5636..9779eda 100755 --- a/hy/lex/lexer.py +++ b/hy/lex/lexer.py @@ -26,6 +26,7 @@ lg.add('QUASIQUOTE', r'`%s' % end_quote) lg.add('UNQUOTESPLICE', r'~@%s' % end_quote) lg.add('UNQUOTE', r'~%s' % end_quote) lg.add('HASHBANG', r'#!.*[^\r\n]') +lg.add('HASHSTARS', r'#\*+') lg.add('HASHOTHER', r'#%s' % identifier) # A regexp which matches incomplete strings, used to support diff --git a/hy/lex/parser.py b/hy/lex/parser.py index 1be896b..d60cec5 100755 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -197,6 +197,22 @@ def term_unquote_splice(p): return HyExpression([HySymbol("unquote_splice"), p[1]]) +@pg.production("term : HASHSTARS term") +@set_quote_boundaries +def term_hashstars(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( + "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) + return HyExpression([HySymbol(sym), p[1]]) + + @pg.production("term : HASHOTHER term") @set_quote_boundaries def hash_other(p): diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index cbef01e..a284b39 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -369,6 +369,18 @@ (assert (is (isfile ".") False))) +(defn test-star-unpacking [] + ; Python 3-only forms of unpacking are in py3_only_tests.hy + (setv l [1 2 3]) + (setv d {"a" "x" "b" "y"}) + (defn fun [&optional x1 x2 x3 x4 a b c] [x1 x2 x3 x4 a b c]) + (assert (= (fun 5 #* l) [5 1 2 3 None None None])) + (assert (= (+ #* l) 6)) + (assert (= (fun 5 #** d) [5 None None None "x" "y" None])) + (assert (= (fun 5 #* l #** d) [5 1 2 3 "x" "y" None]))) + + + (defn test-kwargs [] "NATIVE: test kwargs things." (assert (= (apply kwtest [] {"one" "two"}) {"one" "two"})) diff --git a/tests/native_tests/py35_only_tests.hy b/tests/native_tests/py35_only_tests.hy new file mode 100644 index 0000000..716a9f8 --- /dev/null +++ b/tests/native_tests/py35_only_tests.hy @@ -0,0 +1,26 @@ +;; Copyright 2017 the authors. +;; This file is part of Hy, which is free software licensed under the Expat +;; license. See the LICENSE. + +;; Tests where the emitted code relies on Python ≥3.5. +;; conftest.py skips this file when running on Python <3.5. + + +(defn test-unpacking-pep448-1star [] + (setv l [1 2 3]) + (setv p [4 5]) + (assert (= ["a" #*l "b" #*p #*l] ["a" 1 2 3 "b" 4 5 1 2 3])) + (assert (= (, "a" #*l "b" #*p #*l) (, "a" 1 2 3 "b" 4 5 1 2 3))) + (assert (= #{"a" #*l "b" #*p #*l} #{"a" "b" 1 2 3 4 5})) + (defn f [&rest args] args) + (assert (= (f "a" #*l "b" #*p #*l) (, "a" 1 2 3 "b" 4 5 1 2 3))) + (assert (= (+ #*l #*p) 15)) + (assert (= (and #*l) 3))) + + +(defn test-unpacking-pep448-2star [] + (setv d1 {"a" 1 "b" 2}) + (setv d2 {"c" 3 "d" 4}) + (assert (= {1 "x" #**d1 #**d2 2 "y"} {"a" 1 "b" 2 "c" 3 "d" 4 1 "x" 2 "y"})) + (defn fun [&optional a b c d e f] [a b c d e f]) + (assert (= (fun #**d1 :e "eee" #**d2) [1 2 3 4 "eee" None]))) diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy index b376f6c..b338ffa 100644 --- a/tests/native_tests/py3_only_tests.hy +++ b/tests/native_tests/py3_only_tests.hy @@ -37,6 +37,16 @@ (, 1 2 (, 3 4) 5 {"bar" 6 "quux" 7})))) +(defn test-extended-unpacking-1star-lvalues [] + (setv [x #*y] [1 2 3 4]) + (assert (= x 1)) + (assert (= y [2 3 4])) + (setv [a #*b c] "ghijklmno") + (assert (= a "g")) + (assert (= b (list "hijklmn"))) + (assert (= c "o"))) + + (defn test-yield-from [] "NATIVE: testing yield from" (defn yield-from-test [] diff --git a/tests/test_bin.py b/tests/test_bin.py index c2e31e9..6df4d43 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -7,7 +7,7 @@ import os import subprocess import re -from hy._compat import PY3 +from hy._compat import PY3, PY35 from hy.importer import get_bytecode_path import pytest @@ -210,12 +210,13 @@ def test_hy2py(): if f.endswith(".hy"): if f == "py3_only_tests.hy" and not PY3: continue - else: - i += 1 - output, err = run_cmd("hy2py -s -a " + - os.path.join(dirpath, f)) - assert len(output) > 1, f - assert len(err) == 0, f + if f == "py35_only_tests.hy" and not PY35: + continue + i += 1 + output, err = run_cmd("hy2py -s -a " + + os.path.join(dirpath, f)) + assert len(output) > 1, f + assert len(err) == 0, f assert i From 75e4ad8304ac8849408974b5df3f9ed310f0befa Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 15 Jul 2017 16:28:31 -0700 Subject: [PATCH 2/6] Remove uses of `apply` from /hy --- hy/contrib/loop.hy | 2 +- hy/contrib/multi.hy | 8 ++++---- hy/contrib/profile.hy | 4 ++-- hy/core/language.hy | 18 +++++++++--------- hy/core/macros.hy | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/hy/contrib/loop.hy b/hy/contrib/loop.hy index 9ab9eb8..3417256 100644 --- a/hy/contrib/loop.hy +++ b/hy/contrib/loop.hy @@ -27,7 +27,7 @@ (when (not (first active)) (assoc active 0 True) (while (> (len accumulated) 0) - (setv result (apply f (.pop accumulated)))) + (setv result (f #* (.pop accumulated)))) (assoc active 0 False) result))) diff --git a/hy/contrib/multi.hy b/hy/contrib/multi.hy index 639c96a..8c89486 100644 --- a/hy/contrib/multi.hy +++ b/hy/contrib/multi.hy @@ -29,7 +29,7 @@ (setv output None) (for [[i f] (.items (get self._fns self.f.__module__ self.f.__name__))] (when (.fn? self i args kwargs) - (setv output (apply f args kwargs)) + (setv output (f #* args #** kwargs)) (break))) (if output output @@ -37,10 +37,10 @@ (defn multi-decorator [dispatch-fn] (setv inner (fn [&rest args &kwargs kwargs] - (setv dispatch-key (apply dispatch-fn args kwargs)) + (setv dispatch-key (dispatch-fn #* args #** kwargs)) (if (in dispatch-key inner.--multi--) - (apply (get inner.--multi-- dispatch-key) args kwargs) - (apply inner.--multi-default-- args kwargs)))) + ((get inner.--multi-- dispatch-key) #* args #** kwargs) + (inner.--multi-default-- #* args #** kwargs)))) (setv inner.--multi-- {}) (setv inner.--doc-- dispatch-fn.--doc--) (setv inner.--multi-default-- (fn [&rest args &kwargs kwargs] None)) diff --git a/hy/contrib/profile.hy b/hy/contrib/profile.hy index afee3b0..42ef1a4 100644 --- a/hy/contrib/profile.hy +++ b/hy/contrib/profile.hy @@ -10,7 +10,7 @@ `(do (import [pycallgraph [PyCallGraph]] [pycallgraph.output [GraphvizOutput]]) - (with* [(apply PyCallGraph [] {"output" (GraphvizOutput)})] + (with* [(PyCallGraph :output (GraphvizOutput)))] ~@body))) @@ -29,6 +29,6 @@ (.disable ~g!hy-pr) (setv ~g!hy-s (StringIO)) (setv ~g!hy-ps - (.sort-stats (apply pstats.Stats [~g!hy-pr] {"stream" ~g!hy-s}))) + (.sort-stats (pstats.Stats ~g!hy-pr :stream ~g!hy-s))) (.print-stats ~g!hy-ps) (print (.getvalue ~g!hy-s)))) diff --git a/hy/core/language.hy b/hy/core/language.hy index 6bcdb79..9c29d27 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -37,7 +37,7 @@ first-f (next rfs) fs (tuple rfs)) (fn [&rest args &kwargs kwargs] - (setv res (apply first-f args kwargs)) + (setv res (first-f #* args #** kwargs)) (for* [f fs] (setv res (f res))) res)))) @@ -45,7 +45,7 @@ (defn complement [f] "Create a function that reverses truth value of another function" (fn [&rest args &kwargs kwargs] - (not (apply f args kwargs)))) + (not (f #* args #** kwargs)))) (defn cons [a b] "Return a fresh cons cell with car = a and cdr = b" @@ -160,8 +160,8 @@ (defn drop-last [n coll] "Return a sequence of all but the last n elements in coll." (setv iters (tee coll)) - (map first (apply zip [(get iters 0) - (drop n (get iters 1))]))) + (map first (zip #* [(get iters 0) + (drop n (get iters 1))]))) (defn empty? [coll] "Return True if `coll` is empty" @@ -250,7 +250,7 @@ (defn interleave [&rest seqs] "Return an iterable of the first item in each of seqs, then the second etc." - (chain.from-iterable (apply zip seqs))) + (chain.from-iterable (zip #* seqs))) (defn interpose [item seq] "Return an iterable of the elements of seq separated by item" @@ -275,7 +275,7 @@ set of arguments and collects the results into a list." (setv fs (cons f fs)) (fn [&rest args &kwargs kwargs] - (list-comp (apply f args kwargs) [f fs]))) + (list-comp (f #* args #** kwargs) [f fs]))) (defn last [coll] "Return last item from `coll`" @@ -285,7 +285,7 @@ "Return a dotted list construed from the elements of the argument" (if (not tl) hd - (cons hd (apply list* tl)))) + (cons hd (list* #* tl)))) (defn macroexpand [form] "Return the full macro expansion of form" @@ -350,8 +350,8 @@ slices (genexpr (islice (get coll-clones start) start None step) [start (range n)])) (if (is fillvalue -sentinel) - (apply zip slices) - (apply zip-longest slices {"fillvalue" fillvalue}))) + (zip #* slices) + (zip-longest #* slices :fillvalue fillvalue))) (defn pos? [n] "Return true if n is > 0" diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 27eadf6..82cf3ae 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -207,7 +207,7 @@ (setv retval (gensym)) `(when (= --name-- "__main__") (import sys) - (setv ~retval (apply (fn [~@args] ~@body) sys.argv)) + (setv ~retval ((fn [~@args] ~@body) #* sys.argv)) (if (integer? ~retval) (sys.exit ~retval)))) From 97ecb0b5534c31c233b3ed17e74fb24148669520 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 15 Jul 2017 16:54:43 -0700 Subject: [PATCH 3/6] Remove `apply` from tests --- tests/native_tests/contrib/multi.hy | 6 ++--- tests/native_tests/language.hy | 40 ++++++---------------------- tests/native_tests/native_macros.hy | 4 +-- tests/native_tests/operators.hy | 5 ---- tests/native_tests/py3_only_tests.hy | 9 +++---- tests/native_tests/tag_macros.hy | 5 ++-- 6 files changed, 19 insertions(+), 50 deletions(-) diff --git a/tests/native_tests/contrib/multi.hy b/tests/native_tests/contrib/multi.hy index 90c85dc..9b0294c 100644 --- a/tests/native_tests/contrib/multi.hy +++ b/tests/native_tests/contrib/multi.hy @@ -95,9 +95,9 @@ ([&optional [a "nop"] [b "p"]] (+ a b))) (assert (= (fun 1) 1)) - (assert (= (apply fun [] {"a" "t"}) "t")) - (assert (= (apply fun ["hello "] {"b" "world"}) "hello world")) - (assert (= (apply fun [] {"a" "hello " "b" "world"}) "hello world"))) + (assert (= (fun :a "t") "t")) + (assert (= (fun "hello " :b "world") "hello world")) + (assert (= (fun :a "hello " :b "world") "hello world"))) (defn test-docs [] diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index a284b39..51e221f 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -383,36 +383,12 @@ (defn test-kwargs [] "NATIVE: test kwargs things." - (assert (= (apply kwtest [] {"one" "two"}) {"one" "two"})) + (assert (= (kwtest :one "two") {"one" "two"})) (setv mydict {"one" "three"}) - (assert (= (apply kwtest [] mydict) mydict)) - (assert (= (apply kwtest [] ((fn [] {"one" "two"}))) {"one" "two"}))) + (assert (= (kwtest #** mydict) mydict)) + (assert (= (kwtest #** ((fn [] {"one" "two"}))) {"one" "two"}))) -(defn test-apply [] - "NATIVE: test working with args and functions" - (defn sumit [a b c] (+ a b c)) - (assert (= (apply sumit [1] {"b" 2 "c" 3}) 6)) - (assert (= (apply sumit [1 2 2]) 5)) - (assert (= (apply sumit [] {"a" 1 "b" 1 "c" 2}) 4)) - (assert (= (apply sumit ((fn [] [1 1])) {"c" 1}) 3)) - (defn noargs [] [1 2 3]) - (assert (= (apply noargs) [1 2 3])) - (defn sumit-mangle [an-a a-b a-c a-d] (+ an-a a-b a-c a-d)) - (def Z "a_d") - (assert (= (apply sumit-mangle [] {"an-a" 1 :a-b 2 'a-c 3 Z 4}) 10))) - - -(defn test-apply-with-methods [] - "NATIVE: test apply to call a method" - (setv str "foo {bar}") - (assert (= (apply .format [str] {"bar" "baz"}) - (apply .format ["foo {0}" "baz"]) - "foo baz")) - (setv lst ["a {0} {1} {foo} {bar}" "b" "c"]) - (assert (= (apply .format lst {"foo" "d" "bar" "e"}) - "a b c d e"))) - (defn test-dotted [] "NATIVE: test dotted invocation" @@ -430,20 +406,20 @@ (assert (= (.meth m) "meth")) (assert (= (.meth m "foo" "bar") "meth foo bar")) (assert (= (.meth :b "1" :a "2" m "foo" "bar") "meth foo bar 2 1")) - (assert (= (apply .meth [m "foo" "bar"]) "meth foo bar")) + (assert (= (.meth m #* ["foo" "bar"]) "meth foo bar")) (setv x.p m) (assert (= (.p.meth x) "meth")) (assert (= (.p.meth x "foo" "bar") "meth foo bar")) (assert (= (.p.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) - (assert (= (apply .p.meth [x "foo" "bar"]) "meth foo bar")) + (assert (= (.p.meth x #* ["foo" "bar"]) "meth foo bar")) (setv x.a (X)) (setv x.a.b m) (assert (= (.a.b.meth x) "meth")) (assert (= (.a.b.meth x "foo" "bar") "meth foo bar")) (assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) - (assert (= (apply .a.b.meth [x "foo" "bar"]) "meth foo bar")) + (assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar")) (assert (is (.isdigit :foo) False))) @@ -1185,8 +1161,8 @@ "NATIVE: test &key function arguments" (defn foo [&key {"a" None "b" 1}] [a b]) (assert (= (foo) [None 1])) - (assert (= (apply foo [] {"a" 2}) [2 1])) - (assert (= (apply foo [] {"b" 42}) [None 42]))) + (assert (= (foo :a 2) [2 1])) + (assert (= (foo :b 42) [None 42]))) (defn test-optional-arguments [] diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy index 3a0d428..d3efcb6 100644 --- a/tests/native_tests/native_macros.hy +++ b/tests/native_tests/native_macros.hy @@ -84,12 +84,12 @@ "NATIVE: test macro calling a plain function" (assert (= 3 (bar 1 2)))) -(defn test-optional-and-apply-in-macro [] +(defn test-optional-and-unpacking-in-macro [] ; https://github.com/hylang/hy/issues/1154 (defn f [&rest args] (+ "f:" (repr args))) (defmacro mac [&optional x] - `(apply f [~x])) + `(f #* [~x])) (assert (= (mac) "f:(None,)"))) (defn test-midtree-yield [] diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy index b49c754..be40cc5 100644 --- a/tests/native_tests/operators.hy +++ b/tests/native_tests/operators.hy @@ -288,8 +288,3 @@ (assert (is (f 3 [1 2]) (!= f-name "in"))) (assert (is (f 2 [1 2]) (= f-name "in"))) (forbid (f 2 [1 2] [3 4]))) - -#@(pytest.mark.xfail -(defn test-apply-op [] - ; https://github.com/hylang/hy/issues/647 - (assert (= (eval '(apply + ["a" "b" "c"])) "abc")))) diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy index b338ffa..84b8053 100644 --- a/tests/native_tests/py3_only_tests.hy +++ b/tests/native_tests/py3_only_tests.hy @@ -16,15 +16,15 @@ "NATIVE: test keyword-only arguments" ;; keyword-only with default works (defn kwonly-foo-default-false [&kwonly [foo False]] foo) - (assert (= (apply kwonly-foo-default-false) False)) - (assert (= (apply kwonly-foo-default-false [] {"foo" True}) True)) + (assert (= (kwonly-foo-default-false) False)) + (assert (= (kwonly-foo-default-false :foo True) True)) ;; keyword-only without default ... (defn kwonly-foo-no-default [&kwonly foo] foo) (setv attempt-to-omit-default (try (kwonly-foo-no-default) (except [e [Exception]] e))) ;; works - (assert (= (apply kwonly-foo-no-default [] {"foo" "quux"}) "quux")) + (assert (= (kwonly-foo-no-default :foo "quux") "quux")) ;; raises TypeError with appropriate message if not supplied (assert (isinstance attempt-to-omit-default TypeError)) (assert (in "missing 1 required keyword-only argument: 'foo'" @@ -32,8 +32,7 @@ ;; keyword-only with other arg types works (defn function-of-various-args [a b &rest args &kwonly foo &kwargs kwargs] (, a b args foo kwargs)) - (assert (= (apply function-of-various-args - [1 2 3 4] {"foo" 5 "bar" 6 "quux" 7}) + (assert (= (function-of-various-args 1 2 3 4 :foo 5 :bar 6 :quux 7) (, 1 2 (, 3 4) 5 {"bar" 6 "quux" 7})))) diff --git a/tests/native_tests/tag_macros.hy b/tests/native_tests/tag_macros.hy index ba586f6..18beaa7 100644 --- a/tests/native_tests/tag_macros.hy +++ b/tests/native_tests/tag_macros.hy @@ -100,9 +100,8 @@ "Increments each argument passed to the decorated function." ((wraps func) (fn [&rest args &kwargs kwargs] - (apply func - (map inc args) - (dict-comp k (inc v) [[k v] (.items kwargs)]))))) + (func #* (map inc args) + #** (dict-comp k (inc v) [[k v] (.items kwargs)]))))) #@(increment-arguments (defn foo [&rest args &kwargs kwargs] From 784a44601b63357d28ff150d072bf90a524f9432 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 19 Jul 2017 10:18:55 -0700 Subject: [PATCH 4/6] Remove the implementation of `apply` --- NEWS | 2 + hy/compiler.py | 109 ------------------------------------------------- 2 files changed, 2 insertions(+), 109 deletions(-) diff --git a/NEWS b/NEWS index fe7ffb9..3bae0ca 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ Changes from 0.13.0 [ Language Changes ] * `yield-from` is no longer supported under Python 2 + * `apply` has been replaced with Python-style unpacking operators `#*` and + `#**` (e.g., `(f #* args #** kwargs)`) * Single-character "sharp macros" changed to "tag macros", which can have longer names * Periods are no longer allowed in keywords diff --git a/hy/compiler.py b/hy/compiler.py index 97a4c73..5fd073f 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -1587,115 +1587,6 @@ class HyASTCompiler(object): generators=expr.generators) return ret - @builds("apply") - @checkargs(min=1, max=3) - def compile_apply_expression(self, expr): - expr.pop(0) # apply - - ret = Result() - - fun = expr.pop(0) - - # We actually defer the compilation of the function call to - # @builds(HyExpression), allowing us to work on method calls - call = HyExpression([fun]).replace(fun) - - if isinstance(fun, HySymbol) and fun.startswith("."): - # (apply .foo lst) needs to work as lst[0].foo(*lst[1:]) - if not expr: - raise HyTypeError( - expr, "apply of a method needs to have an argument" - ) - - # We need to grab the arguments, and split them. - - # Assign them to a variable if they're not one already - if type(expr[0]) == HyList: - if len(expr[0]) == 0: - raise HyTypeError( - expr, "apply of a method needs to have an argument" - ) - call.append(expr[0].pop(0)) - else: - if isinstance(expr[0], HySymbol): - tempvar = expr[0] - else: - tempvar = HySymbol(self.get_anon_var()).replace(expr[0]) - assignment = HyExpression( - [HySymbol("setv"), tempvar, expr[0]] - ).replace(expr[0]) - - # and add the assignment to our result - ret += self.compile(assignment) - - # The first argument is the object on which to call the method - # So we translate (apply .foo args) to (.foo (get args 0)) - call.append(HyExpression( - [HySymbol("get"), tempvar, HyInteger(0)] - ).replace(tempvar)) - - # We then pass the other arguments to the function - expr[0] = HyExpression( - [HySymbol("cut"), tempvar, HyInteger(1)] - ).replace(expr[0]) - - ret += self.compile(call) - - if not isinstance(ret.expr, ast.Call): - raise HyTypeError( - fun, "compiling the application of `{}' didn't return a " - "function call, but `{}'".format(fun, type(ret.expr).__name__) - ) - if ret.expr.starargs or ret.expr.kwargs: - raise HyTypeError( - expr, "compiling the function application returned a function " - "call with arguments" - ) - - if expr: - stargs = expr.pop(0) - if stargs is not None: - stargs = self.compile(stargs) - if PY35: - stargs_expr = stargs.force_expr - ret.expr.args.append( - ast.Starred(stargs_expr, ast.Load(), - lineno=stargs_expr.lineno, - col_offset=stargs_expr.col_offset) - ) - else: - ret.expr.starargs = stargs.force_expr - ret = stargs + ret - - if expr: - kwargs = expr.pop(0) - if isinstance(kwargs, HyDict): - new_kwargs = [] - for k, v in kwargs.items(): - if isinstance(k, HySymbol): - pass - elif isinstance(k, HyString): - k = HyString(hy_symbol_mangle(str_type(k))).replace(k) - elif isinstance(k, HyKeyword): - sym = hy_symbol_mangle(str_type(k)[2:]) - k = HyString(sym).replace(k) - new_kwargs += [k, v] - kwargs = HyDict(new_kwargs).replace(kwargs) - - kwargs = self.compile(kwargs) - if PY35: - kwargs_expr = kwargs.force_expr - ret.expr.keywords.append( - ast.keyword(None, kwargs_expr, - lineno=kwargs_expr.lineno, - col_offset=kwargs_expr.col_offset) - ) - else: - ret.expr.kwargs = kwargs.force_expr - ret = kwargs + ret - - return ret - @builds("not") @builds("~") @checkargs(1) From 0bbb5f8e34abd643bcd2fbf970ab66c42ec21eef Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 19 Jul 2017 10:25:24 -0700 Subject: [PATCH 5/6] hy-repr: Support #* and #** --- hy/contrib/hy_repr.hy | 4 ++++ tests/native_tests/contrib/hy_repr.hy | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hy/contrib/hy_repr.hy b/hy/contrib/hy_repr.hy index 2053504..ea67efc 100644 --- a/hy/contrib/hy_repr.hy +++ b/hy/contrib/hy_repr.hy @@ -37,6 +37,10 @@ (+ "~" (f (second x) q)) (= (first x) 'unquote_splice) (+ "~@" (f (second x) q)) + (= (first x) 'unpack_iterable) + (+ "#* " (f (second x) q)) + (= (first x) 'unpack_mapping) + (+ "#** " (f (second x) q)) ; else (+ "(" (catted) ")")) (+ "(" (catted) ")")) diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy index 9bbdd09..a9c0475 100644 --- a/tests/native_tests/contrib/hy_repr.hy +++ b/tests/native_tests/contrib/hy_repr.hy @@ -28,8 +28,8 @@ [1 2 3] (, 1 2 3) #{1 2 3} (frozenset #{1 2 3}) '[1 2 3] '(, 1 2 3) '#{1 2 3} '(frozenset #{1 2 3}) {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3} - [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"}] - '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"}]]) + [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"} '(f #* a #** b)] + '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"} (f #* a #** b)]]) (for [original-val values] (setv evaled (eval (read-str (hy-repr original-val)))) (assert (= evaled original-val)) @@ -59,7 +59,8 @@ "{1 20}" "'{1 10 1 20}" "'asymbol" - ":akeyword"]) + ":akeyword" + "'(f #* args #** kwargs)"]) (for [original-str strs] (setv rep (hy-repr (eval (read-str original-str)))) (assert (= rep original-str)))) From 3f3cce8785c8d11c711d6429a472bbe646512a35 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 19 Jul 2017 11:00:43 -0700 Subject: [PATCH 6/6] Update docs: `apply` is gone; #* and #** are in --- docs/language/api.rst | 96 +++++++++++++++++++++++------------------- docs/language/core.rst | 4 +- docs/tutorial.rst | 19 +++------ 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/docs/language/api.rst b/docs/language/api.rst index 5241b3a..3904e86 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -154,41 +154,6 @@ it appends it as the last argument. The following code demonstrates this: 5 10 -apply ------ - -``apply`` is used to apply an optional list of arguments and an -optional dictionary of kwargs to a function. The symbol mangling -transformations will be applied to all keys in the dictionary of -kwargs, provided the dictionary and its keys are defined in-place. - -Usage: ``(apply fn-name [args] [kwargs])`` - -Examples: - -.. code-block:: clj - - (defn thunk [] - "hy there") - - (apply thunk) - ;=> "hy there" - - (defn total-purchase [price amount &optional [fees 1.05] [vat 1.1]] - (* price amount fees vat)) - - (apply total-purchase [10 15]) - ;=> 173.25 - - (apply total-purchase [10 15] {"vat" 1.05}) - ;=> 165.375 - - (apply total-purchase [] {"price" 10 "amount" 15 "vat" 1.05}) - ;=> 165.375 - - (apply total-purchase [] {:price 10 :amount 15 :vat 1.05}) - ;=> 165.375 - and --- @@ -596,8 +561,8 @@ Parameters may have the following keywords in front of them: parameter_1 1 parameter_2 2 - ; to avoid the mangling of '-' to '_', use apply: - => (apply print-parameters [] {"parameter-1" 1 "parameter-2" 2}) + ; to avoid the mangling of '-' to '_', use unpacking: + => (print-parameters #** {"parameter-1" 1 "parameter-2" 2}) parameter-1 1 parameter-2 2 @@ -634,19 +599,19 @@ Parameters may have the following keywords in front of them: .. code-block:: clj - => (defn compare [a b &kwonly keyfn [reverse false]] + => (defn compare [a b &kwonly keyfn [reverse False]] ... (setv result (keyfn a b)) ... (if (not reverse) ... result ... (- result))) - => (apply compare ["lisp" "python"] - ... {"keyfn" (fn [x y] - ... (reduce - (map (fn [s] (ord (first s))) [x y])))}) + => (compare "lisp" "python" + ... :keyfn (fn [x y] + ... (reduce - (map (fn [s] (ord (first s))) [x y])))) -4 - => (apply compare ["lisp" "python"] - ... {"keyfn" (fn [x y] + => (compare "lisp" "python" + ... :keyfn (fn [x y] ... (reduce - (map (fn [s] (ord (first s))) [x y]))) - ... "reverse" True}) + ... :reverse True) 4 .. code-block:: python @@ -1576,6 +1541,49 @@ the given conditional is ``False``. The following shows the expansion of this ma (do statement)) +unpack-iterable, unpack-mapping +------------------------------- + +``unpack-iterable`` and ``unpack-mapping`` allow an iterable or mapping +object (respectively) to provide positional or keywords arguments +(respectively) to a function. + +.. code-block:: clj + + => (defn f [a b c d] [a b c d]) + => (f (unpack-iterable [1 2]) (unpack-mapping {"c" 3 "d" 4})) + [1, 2, 3, 4] + +``unpack-iterable`` is usually written with the shorthand ``#*``, and +``unpack-mapping`` with ``#**``. + +.. code-block:: clj + + => (f #* [1 2] #** {"c" 3 "d" 4}) + [1, 2, 3, 4] + +With Python 3, you can unpack in an assignment list (:pep:`3132`). + +.. code-block:: clj + + => (setv [a #* b c] [1 2 3 4 5]) + => [a b c] + [1, [2, 3, 4], 5] + +With Python 3.5 or greater, unpacking is allowed in more contexts than just +function calls, and you can unpack more than once in the same expression +(:pep:`448`). + +.. code-block:: clj + + => [#* [1 2] #* [3 4]] + [1, 2, 3, 4] + => {#** {1 2} #** {3 4}} + {1: 2, 3: 4} + => (f #* [1] #* [2] #** {"c" 3} #** {"d" 4}) + [1, 2, 3, 4] + + unquote ------- diff --git a/docs/language/core.rst b/docs/language/core.rst index 21c111b..4983af9 100644 --- a/docs/language/core.rst +++ b/docs/language/core.rst @@ -1216,9 +1216,9 @@ if *from-file* ends before a complete expression can be parsed. => (import io) => (def buffer (io.StringIO "(+ 2 2)\n(- 2 1)")) - => (eval (apply read [] {"from_file" buffer})) + => (eval (read :from_file buffer)) 4 - => (eval (apply read [] {"from_file" buffer})) + => (eval (read :from_file buffer)) 1 => ; assuming "example.hy" contains: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ceeab77..3883abe 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -423,8 +423,7 @@ The same thing in Hy:: => (optional-arg 1 2 3 4) [1 2 3 4] -If you're running a version of Hy past 0.10.1 (eg, git master), -there's also a nice new keyword argument syntax:: +You can call keyword arguments like this:: => (optional-arg :keyword1 1 ... :pos2 2 @@ -432,21 +431,13 @@ there's also a nice new keyword argument syntax:: ... :keyword2 4) [3, 2, 1, 4] -Otherwise, you can always use `apply`. But what's `apply`? - -Are you familiar with passing in `*args` and `**kwargs` in Python?:: - - >>> args = [1 2] - >>> kwargs = {"keyword2": 3 - ... "keyword1": 4} - >>> optional_arg(*args, **kwargs) - -We can reproduce this with `apply`:: +You can unpack arguments with the syntax ``#* args`` and ``#** kwargs``, +similar to `*args` and `**kwargs` in Python:: => (setv args [1 2]) => (setv kwargs {"keyword2" 3 ... "keyword1" 4}) - => (apply optional-arg args kwargs) + => (optional-arg #* args #** kwargs) [1, 2, 4, 3] There's also a dictionary-style keyword arguments construction that @@ -460,7 +451,7 @@ looks like: The difference here is that since it's a dictionary, you can't rely on any specific ordering to the arguments. -Hy also supports ``*args`` and ``**kwargs``. In Python:: +Hy also supports ``*args`` and ``**kwargs`` in parameter lists. In Python:: def some_func(foo, bar, *args, **kwargs): import pprint