diff --git a/docs/language/api.rst b/docs/language/api.rst
index 40c8000..625ebc0 100644
--- a/docs/language/api.rst
+++ b/docs/language/api.rst
@@ -441,6 +441,41 @@ Parameters may have the following keywords in front of them:
=> (zig-zag-sum 1 2 3 4 5 6)
-3
+&kwonly
+ .. versionadded:: 0.12.0
+
+ Parameters that can only be called as keywords. Mandatory
+ keyword-only arguments are declared with the argument's name;
+ optional keyword-only arguments are declared as a two-element list
+ containing the argument name followed by the default value (as
+ with `&optional` above).
+
+ .. code-block:: clj
+
+ => (defn compare [a b &kwonly keyfn [reverse false]]
+ ... (let [[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])))})
+ -4
+ => (apply compare ["lisp" "python"]
+ ... {"keyfn" (fn [x y]
+ ... (reduce - (map (fn [s] (ord (first s))) [x y])))
+ ... "reverse" true})
+ 4
+
+ .. code-block:: python
+
+ => (compare "lisp" "python")
+ Traceback (most recent call last):
+ File "", line 1, in
+ TypeError: compare() missing 1 required keyword-only argument: 'keyfn'
+
+ Availability: Python 3.
+
.. _defn-alias / defun-alias:
defn-alias / defun-alias
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/compilers/test_ast.py b/tests/compilers/test_ast.py
index 7c9fefa..7ef5451 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -54,11 +54,13 @@ def cant_compile(expr):
# error, otherwise it's a compiler bug.
assert isinstance(e.expression, HyObject)
assert e.message
+ return e
except HyCompileError as e:
# Anything that can't be compiled should raise a user friendly
# error, otherwise it's a compiler bug.
assert isinstance(e.exception, HyTypeError)
assert e.traceback
+ return e
def test_ast_bad_type():
@@ -441,10 +443,31 @@ def test_lambda_list_keywords_kwargs():
cant_compile("(fn (x &kwargs xs &kwargs ys) (list x xs ys))")
+def test_lambda_list_keywords_kwonly():
+ """Ensure we can compile functions with &kwonly if we're on Python
+ 3, or fail with an informative message on Python 2."""
+ kwonly_demo = "(fn [&kwonly a [b 2]] (print a b))"
+ if PY3:
+ code = can_compile(kwonly_demo)
+ for i, kwonlyarg_name in enumerate(('a', 'b')):
+ assert kwonlyarg_name == code.body[0].args.kwonlyargs[i].arg
+ assert code.body[0].args.kw_defaults[0] is None
+ assert code.body[0].args.kw_defaults[1].n == 2
+ else:
+ exception = cant_compile(kwonly_demo)
+ assert isinstance(exception, HyTypeError)
+ message, = exception.args
+ assert message == ("keyword-only arguments are only "
+ "available under Python 3")
+
+
def test_lambda_list_keywords_mixed():
""" Ensure we can mix them up."""
can_compile("(fn (x &rest xs &kwargs kw) (list x xs kw))")
cant_compile("(fn (x &rest xs &fasfkey {bar \"baz\"}))")
+ if PY3:
+ can_compile("(fn [x &rest xs &kwargs kwxs &kwonly kwoxs]"
+ " (list x xs kwxs kwoxs))")
def test_ast_unicode_strings():
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})))))