diff --git a/NEWS.rst b/NEWS.rst index 232e986..d210e71 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -15,6 +15,8 @@ Other Breaking Changes instead of ignoring it. This change increases consistency a bit and makes accidental unary uses easier to notice. * `hy-repr` uses registered functions instead of methods +* `HyKeyword` no longer inherits from the string type and has been + made into its own object type. New Features ------------------------------ diff --git a/docs/language/core.rst b/docs/language/core.rst index 3b089ea..4df50c8 100644 --- a/docs/language/core.rst +++ b/docs/language/core.rst @@ -581,10 +581,10 @@ objects with the `__name__` magic will work. .. code-block:: hy => (keyword "foo") - u'\ufdd0:foo' + HyKeyword('foo') => (keyword 1) - u'\ufdd0:1' + HyKeyword('foo') .. _keyword?-fn: diff --git a/docs/language/syntax.rst b/docs/language/syntax.rst index fa1e7b6..0bcde89 100644 --- a/docs/language/syntax.rst +++ b/docs/language/syntax.rst @@ -72,9 +72,7 @@ of bytes. So when running under Python 3, Hy translates ``"foo"`` and keywords -------- -An identifier headed by a colon, such as ``:foo``, is a keyword. Keywords -evaluate to a string preceded by the Unicode non-character code point U+FDD0, -like ``"\ufdd0:foo"``, so ``:foo`` and ``":foo"`` aren't equal. However, if a +An identifier headed by a colon, such as ``:foo``, is a keyword. If a literal keyword appears in a function call, it's used to indicate a keyword argument rather than passed in as a value. For example, ``(f :foo 3)`` calls the function ``f`` with the keyword argument named ``foo`` set to ``3``. Hence, diff --git a/hy/compiler.py b/hy/compiler.py index 6f42e2c..b1ee4f2 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -500,19 +500,18 @@ class HyASTCompiler(object): except StopIteration: raise HyTypeError(expr, "Keyword argument {kw} needs " - "a value.".format(kw=str(expr[1:]))) + "a value.".format(kw=expr)) + + if not expr: + raise HyTypeError(expr, "Can't call a function with the " + "empty keyword") compiled_value = self.compile(value) ret += compiled_value - keyword = expr[2:] - if not keyword: - raise HyTypeError(expr, "Can't call a function with the " - "empty keyword") - keyword = ast_str(keyword) - + arg = str_type(expr)[1:] keywords.append(asty.keyword( - expr, arg=keyword, value=compiled_value.force_expr)) + expr, arg=ast_str(arg), value=compiled_value.force_expr)) else: ret += self.compile(expr) @@ -742,10 +741,13 @@ class HyASTCompiler(object): return imports, HyExpression([HySymbol(name), HyString(form)]).replace(form), False + elif isinstance(form, HyKeyword): + return imports, form, False + elif isinstance(form, HyString): x = [HySymbol(name), form] if form.brackets is not None: - x.extend([HyKeyword(":brackets"), form.brackets]) + x.extend([HyKeyword("brackets"), form.brackets]) return imports, HyExpression(x).replace(form), False return imports, HyExpression([HySymbol(name), @@ -809,7 +811,7 @@ class HyASTCompiler(object): ret += self.compile(expr.pop(0)) cause = None - if len(expr) == 2 and expr[0] == HyKeyword(":from"): + if len(expr) == 2 and expr[0] == HyKeyword("from"): if not PY3: raise HyCompileError( "raise from only supported in python 3") @@ -1168,7 +1170,7 @@ class HyASTCompiler(object): if isinstance(iexpr, HyList) and iexpr: module = iexpr.pop(0) entry = iexpr[0] - if isinstance(entry, HyKeyword) and entry == HyKeyword(":as"): + if entry == HyKeyword("as"): if not len(iexpr) == 2: raise HyTypeError(iexpr, "garbage after aliased import") @@ -1462,7 +1464,7 @@ class HyASTCompiler(object): else: assignments = {} while names: - if len(names) > 1 and names[1] == HyKeyword(":as"): + if len(names) > 1 and names[1] == HyKeyword("as"): k, _, v = names[:3] del names[:3] assignments[k] = v @@ -1471,7 +1473,7 @@ class HyASTCompiler(object): assignments[symbol] = symbol require(module, self.module_name, assignments=assignments) elif (isinstance(entry, HyList) and len(entry) == 3 - and entry[1] == HyKeyword(":as")): + and entry[1] == HyKeyword("as")): # e.g., (require [foo :as bar]) module, _, prefix = entry __import__(module) @@ -1764,7 +1766,7 @@ class HyASTCompiler(object): # An exception for pulling together keyword args is if we're doing # a typecheck, eg (type :foo) with_kwargs = fn not in ( - "type", "HyKeyword", "keyword", "name", "keyword?") + "type", "HyKeyword", "keyword", "name", "keyword?", "identity") args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect( expression[1:], with_kwargs, oldpy_unpack=True) @@ -2187,7 +2189,18 @@ class HyASTCompiler(object): return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load()) - @builds(HyString, HyKeyword, HyBytes) + @builds(HyKeyword) + def compile_keyword(self, obj): + ret = Result() + ret += asty.Call( + obj, + func=asty.Name(obj, id="HyKeyword", ctx=ast.Load()), + args=[asty.Str(obj, s=obj.name)], + keywords=[]) + ret.add_imports("hy", {"HyKeyword"}) + return ret + + @builds(HyString, HyBytes) def compile_string(self, string, building): node = asty.Bytes if PY3 and building is HyBytes else asty.Str f = bytes_type if building is HyBytes else str_type diff --git a/hy/contrib/hy_repr.hy b/hy/contrib/hy_repr.hy index ce2e83b..42ff62c 100644 --- a/hy/contrib/hy_repr.hy +++ b/hy/contrib/hy_repr.hy @@ -32,7 +32,7 @@ (global -quoting) (setv started-quoting False) - (when (and (not -quoting) (instance? HyObject obj)) + (when (and (not -quoting) (instance? HyObject obj) (not (instance? HyKeyword obj))) (setv -quoting True) (setv started-quoting True)) @@ -82,10 +82,8 @@ (+ (get syntax (first x)) (hy-repr (second x))) (+ "(" (-cat x) ")")))) -(hy-repr-register HySymbol str) -(hy-repr-register [str-type bytes-type HyKeyword] (fn [x] - (if (and (instance? str-type x) (.startswith x HyKeyword.PREFIX)) - (return (cut x 1))) +(hy-repr-register [HySymbol HyKeyword] str) +(hy-repr-register [str-type bytes-type] (fn [x] (setv r (.lstrip (-base-repr x) "ub")) (+ (if (instance? bytes-type x) "b" "") diff --git a/hy/core/language.hy b/hy/core/language.hy index f973342..0904b22 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -65,8 +65,7 @@ (defn keyword? [k] "Check whether `k` is a keyword." - (and (instance? (type :foo) k) - (.startswith k (get :foo 0)))) + (instance? HyKeyword k)) (defn dec [n] "Decrement `n` by 1." @@ -460,26 +459,26 @@ as EOF (defaults to an empty string)." "Create a keyword from `value`. Strings numbers and even objects with the __name__ magic will work." - (if (and (string? value) (value.startswith HyKeyword.PREFIX)) - (unmangle value) - (if (string? value) - (HyKeyword (+ ":" (unmangle value))) - (try - (unmangle (.__name__ value)) - (except [] (HyKeyword (+ ":" (string value)))))))) + (if (keyword? value) + (HyKeyword (unmangle value.name)) + (if (string? value) + (HyKeyword (unmangle value)) + (try + (unmangle (.__name__ value)) + (except [] (HyKeyword (string value))))))) (defn name [value] "Convert `value` to a string. Keyword special character will be stripped. String will be used as is. Even objects with the __name__ magic will work." - (if (and (string? value) (value.startswith HyKeyword.PREFIX)) - (unmangle (cut value 2)) - (if (string? value) - (unmangle value) - (try - (unmangle (. value __name__)) - (except [] (string value)))))) + (if (keyword? value) + (unmangle (cut (str value) 1)) + (if (string? value) + (unmangle value) + (try + (unmangle (. value __name__)) + (except [] (string value)))))) (defn xor [a b] "Perform exclusive or between `a` and `b`." diff --git a/hy/lex/parser.py b/hy/lex/parser.py index 1f5b2d8..e94e5a4 100755 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -380,7 +380,7 @@ def symbol_like(obj): pass if obj.startswith(":") and "." not in obj: - return HyKeyword(obj) + return HyKeyword(obj[1:]) @pg.error diff --git a/hy/models.py b/hy/models.py index cef2fe8..5a16c02 100644 --- a/hy/models.py +++ b/hy/models.py @@ -121,22 +121,35 @@ _wrappers[bool] = lambda x: HySymbol("True") if x else HySymbol("False") _wrappers[type(None)] = lambda foo: HySymbol("None") -class HyKeyword(HyObject, str_type): - """Generic Hy Keyword object. It's either a ``str`` or a ``unicode``, - depending on the Python version. - """ +class HyKeyword(HyObject): + """Generic Hy Keyword object.""" - PREFIX = "\uFDD0" + __slots__ = ['name'] - def __new__(cls, value): - if not value.startswith(cls.PREFIX): - value = cls.PREFIX + value - - obj = str_type.__new__(cls, value) - return obj + def __init__(self, value): + self.name = value def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self[1:])) + return "%s(%r)" % (self.__class__.__name__, self.name) + + def __str__(self): + return ":%s" % self.name + + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, HyKeyword): + return NotImplemented + return self.name == other.name + + def __ne__(self, other): + if not isinstance(other, HyKeyword): + return NotImplemented + return self.name != other.name + + def __bool__(self): + return bool(self.name) def strip_digit_separators(number): diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy index 4ccd7ce..cdea7c4 100644 --- a/tests/native_tests/contrib/hy_repr.hy +++ b/tests/native_tests/contrib/hy_repr.hy @@ -79,14 +79,6 @@ (assert (is (type (get orig 1)) float)) (assert (is (type (get result 1)) HyFloat))) -(when PY3 (defn test-bytes-keywords [] - ; Make sure that keyword-like bytes objects aren't hy-repred as if - ; they were real keywords. - (setv kw :mykeyword) - (assert (= (hy-repr kw) ":mykeyword")) - (assert (= (hy-repr (str ':mykeyword)) ":mykeyword")) - (assert (= (hy-repr (.encode kw "UTF-8") #[[b"\xef\xb7\x90:hello"]]))))) - (when PY3 (defn test-dict-views [] (assert (= (hy-repr (.keys {1 2})) "(dict-keys [1])")) (assert (= (hy-repr (.values {1 2})) "(dict-values [2])")) diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 47be07d..34d8561 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -554,7 +554,7 @@ (assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) (assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar")) - (assert (is (.isdigit :foo) False))) + (assert (= (.__str__ :foo) ":foo"))) (defn test-do [] @@ -1221,9 +1221,12 @@ "NATIVE: test if keywords are recognised" (assert (= :foo :foo)) + (assert (= :foo ':foo)) + (assert (is (type :foo) (type ':foo))) (assert (= (get {:foo "bar"} :foo) "bar")) (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux"))) + (defn test-keyword-clash [] "NATIVE: test that keywords do not clash with normal strings" @@ -1649,11 +1652,6 @@ macros() (setv (. foo [1] test) "hello") (assert (= (getattr (. foo [1]) "test") "hello"))) -(defn test-keyword-quoting [] - "NATIVE: test keyword quoting magic" - (assert (= :foo "\ufdd0:foo")) - (assert (= `:foo "\ufdd0:foo"))) - (defn test-only-parse-lambda-list-in-defn [] "NATIVE: test lambda lists are only parsed in defn" (try diff --git a/tests/test_lex.py b/tests/test_lex.py index 3fb94fd..ca06560 100644 --- a/tests/test_lex.py +++ b/tests/test_lex.py @@ -434,9 +434,9 @@ def test_discard(): assert tokenize("(#_foo)") == [HyExpression()] assert tokenize("(#_foo bar)") == [HyExpression([HySymbol("bar")])] assert tokenize("(foo #_bar)") == [HyExpression([HySymbol("foo")])] - assert tokenize("(foo :bar 1)") == [HyExpression([HySymbol("foo"), HyKeyword(":bar"), HyInteger(1)])] + assert tokenize("(foo :bar 1)") == [HyExpression([HySymbol("foo"), HyKeyword("bar"), HyInteger(1)])] assert tokenize("(foo #_:bar 1)") == [HyExpression([HySymbol("foo"), HyInteger(1)])] - assert tokenize("(foo :bar #_1)") == [HyExpression([HySymbol("foo"), HyKeyword(":bar")])] + assert tokenize("(foo :bar #_1)") == [HyExpression([HySymbol("foo"), HyKeyword("bar")])] # discard term with nesting assert tokenize("[1 2 #_[a b c [d e [f g] h]] 3 4]") == [ HyList([HyInteger(1), HyInteger(2), HyInteger(3), HyInteger(4)])