From ef079d5e08e97c5df2e91b098ff691ebe3788b54 Mon Sep 17 00:00:00 2001 From: "Zack M. Davis" Date: Sun, 15 Mar 2015 14:59:54 -0700 Subject: [PATCH] implement keyword-only arguments Python 3 supports keyword-only arguments as described in the immortal PEP 3102. This commit implements keyword-only argument support for Hy using a `&kwonly` lambda-list-keyword with semantics analogous how `&optional` arguments are handled: `&kwonly` arguments are either a symbol, in which case the keyword argument so named is mandatory, or a two-element list, the first of which is the symbolic name of the keyword argument and the second of which is its default value if not supplied. If Hy is running under Python 2, attempting to use `&kwonly` args will raise a HyTypeError. This effort is with the aim of resolving #453. --- hy/compiler.py | 45 ++++++++++++++++++++++++---- tests/native_tests/py3_only_tests.hy | 26 ++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) 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})))))