From 1d2c73165dcf6d54308328c3f6e81c349de1bf92 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Tue, 24 Jul 2018 09:19:37 -0700 Subject: [PATCH] Make HyKeyword callable Co-authored-by: Simon Gomizelj --- NEWS.rst | 6 ++++++ docs/language/syntax.rst | 5 +++++ hy/compiler.py | 7 ------- hy/models.py | 10 ++++++++++ tests/native_tests/language.hy | 21 ++++++++++++++++++--- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 2b92b99..d4255ef 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,12 @@ Removals ------------------------------ * Empty expressions (`()`) are no longer legal at the top level. +New Features +------------------------------ +* Keyword objects (not just literal keywords) can be called, as + shorthand for `(get obj :key)`, and they accept a default value + as a second argument. + 0.15.0 ============================== diff --git a/docs/language/syntax.rst b/docs/language/syntax.rst index 3e28ac6..32ebec9 100644 --- a/docs/language/syntax.rst +++ b/docs/language/syntax.rst @@ -83,6 +83,11 @@ the error ``Keyword argument :foo needs a value``. To avoid this, you can quote the keyword, as in ``(f ':foo)``, or use it as the value of another keyword argument, as in ``(f :arg :foo)``. +Keywords can be called like functions as shorthand for ``get``. ``(:foo obj)`` +is equivalent to ``(get obj :foo)``. An optional ``default`` argument is also +allowed: ``(:foo obj 2)`` or ``(:foo obj :default 2)`` returns ``2`` if ``(get +obj :foo)`` raises a ``KeyError``. + .. _mangling: symbols diff --git a/hy/compiler.py b/hy/compiler.py index f92fe4d..4f4f694 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -1552,13 +1552,6 @@ class HyASTCompiler(object): fn = expression[0] func = None - if isinstance(fn, HyKeyword): - if len(expression) > 2: - raise HyTypeError( - expression, "keyword calls take only 1 argument") - expression.append(expression.pop(0)) - expression.insert(0, HySymbol("get")) - return self.compile(expression) if isinstance(fn, HySymbol): diff --git a/hy/models.py b/hy/models.py index a0002b6..943458e 100644 --- a/hy/models.py +++ b/hy/models.py @@ -146,6 +146,16 @@ class HyKeyword(HyObject): def __bool__(self): return bool(self.name) + _sentinel = object() + + def __call__(self, data, default=_sentinel): + try: + return data[self] + except KeyError: + if default is HyKeyword._sentinel: + raise + return default + def strip_digit_separators(number): # Don't strip a _ or , if it's the first character, as _42 and diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 59141b5..3d1c396 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -1380,9 +1380,24 @@ (assert (= (len "ℵℵℵ♥♥♥\t♥♥\r\n") 11))) -(defn test-keyword-dict-access [] - "NATIVE: test keyword dict access" - (assert (= "test" (:foo {:foo "test"})))) +(defn test-keyword-get [] + + (assert (= (:foo {:foo "test"}) "test")) + (setv f :foo) + (assert (= (f {:foo "test"}) "test")) + + (with [(pytest.raises KeyError)] (:foo {:a 1 :b 2})) + (assert (= (:foo {:a 1 :b 2} 3) 3)) + (assert (= (:foo {:a 1 :b 2 :foo 5} 3) 5)) + + (with [(pytest.raises TypeError)] (:foo "Hello World")) + (with [(pytest.raises TypeError)] (:foo (object))) + + ; The default argument should work regardless of the collection type. + (defclass G [object] + (defn __getitem__ [self k] + (raise KeyError))) + (assert (= (:foo (G) 15) 15))) (defn test-break-breaking []