diff --git a/hy/compiler.py b/hy/compiler.py index e20964b..6da7215 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -485,11 +485,13 @@ class HyASTCompiler(object): def _parse_lambda_list(self, exprs): """ Return FunctionDef parameter values from lambda list.""" - ll_keywords = ("&rest", "&optional", "&key", "&kwargs") + ll_keywords = ("&rest", "&optional", "&key", "&kwonly", "&kwargs") ret = Result() args = [] defaults = [] varargs = None + kwonlyargs = [] + kwonlydefaults = [] kwargs = None lambda_keyword = None @@ -506,6 +508,8 @@ class HyASTCompiler(object): lambda_keyword = expr elif expr == "&key": lambda_keyword = expr + elif expr == "&kwonly": + lambda_keyword = expr elif expr == "&kwargs": lambda_keyword = expr else: @@ -554,6 +558,24 @@ class HyASTCompiler(object): args.append(k) ret += self.compile(v) defaults.append(ret.force_expr) + elif lambda_keyword == "&kwonly": + if not PY3: + raise HyTypeError(expr, + "keyword-only arguments are only " + "available under Python 3") + if isinstance(expr, HyList): + if len(expr) != 2: + raise HyTypeError(expr, + "keyword-only args should be bare " + "names or 2-item lists") + k, v = expr + kwonlyargs.append(k) + ret += self.compile(v) + kwonlydefaults.append(ret.force_expr) + else: + k = expr + kwonlyargs.append(k) + kwonlydefaults.append(None) elif lambda_keyword == "&kwargs": if kwargs: raise HyTypeError(expr, @@ -561,7 +583,7 @@ class HyASTCompiler(object): "&kwargs argument") kwargs = str(expr) - return ret, args, defaults, varargs, kwargs + return ret, args, defaults, varargs, kwonlyargs, kwonlydefaults, kwargs def _storeize(self, name, func=None): """Return a new `name` object with an ast.Store() context""" @@ -2026,7 +2048,9 @@ class HyASTCompiler(object): if not isinstance(arglist, HyList): raise HyTypeError(expression, "First argument to (fn) must be a list") - ret, args, defaults, stararg, kwargs = self._parse_lambda_list(arglist) + + (ret, args, defaults, stararg, + kwonlyargs, kwonlydefaults, kwargs) = self._parse_lambda_list(arglist) for i, arg in enumerate(args): if isinstance(arg, HyList): # Destructuring argument @@ -2049,6 +2073,11 @@ class HyASTCompiler(object): lineno=x.start_line, col_offset=x.start_column) for x in args] + kwonlyargs = [ast.arg(arg=ast_str(x), annotation=None, + lineno=x.start_line, + col_offset=x.start_column) + for x in kwonlyargs] + # XXX: Beware. Beware. This wasn't put into the parse lambda # list because it's really just an internal parsing thing. @@ -2065,12 +2094,18 @@ class HyASTCompiler(object): lineno=x.start_line, col_offset=x.start_column) for x in args] + if PY3: + kwonlyargs = [ast.Name(arg=ast_str(x), id=ast_str(x), + ctx=ast.Param(), lineno=x.start_line, + col_offset=x.start_column) + for x in kwonlyargs] + args = ast.arguments( args=args, vararg=stararg, kwarg=kwargs, - kwonlyargs=[], - kw_defaults=[], + kwonlyargs=kwonlyargs, + kw_defaults=kwonlydefaults, defaults=defaults) body = self._compile_branch(expression) diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy index 9be88f1..1ad58a7 100644 --- a/tests/native_tests/py3_only_tests.hy +++ b/tests/native_tests/py3_only_tests.hy @@ -10,3 +10,29 @@ (try (raise ValueError :from NameError) (except [e [ValueError]] (assert (= (type (. e __cause__)) NameError))))) + + +(defn test-kwonly [] + "NATIVE: test keyword-only arguments" + ;; keyword-only with default works + (let [[kwonly-foo-default-false (fn [&kwonly [foo false]] foo)]] + (assert (= (apply kwonly-foo-default-false) false)) + (assert (= (apply kwonly-foo-default-false [] {"foo" true}) true))) + ;; keyword-only without default ... + (let [[kwonly-foo-no-default (fn [&kwonly foo] foo)] + [attempt-to-omit-default (try + (kwonly-foo-no-default) + (catch [e [Exception]] e))]] + ;; works + (assert (= (apply 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'" + (. attempt-to-omit-default args [0])))) + ;; keyword-only with other arg types works + (let [[function-of-various-args + (fn [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}) + (, 1 2 (, 3 4) 5 {"bar" 6 "quux" 7})))))