Merge pull request #1543 from vodik/keywords-as-objects

Make HyKeyword a first class objects
This commit is contained in:
Kodi Arfer 2018-04-07 22:37:48 -07:00 committed by GitHub
commit 096aac244f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 83 additions and 70 deletions

View File

@ -15,6 +15,8 @@ Other Breaking Changes
instead of ignoring it. This change increases consistency a bit instead of ignoring it. This change increases consistency a bit
and makes accidental unary uses easier to notice. and makes accidental unary uses easier to notice.
* `hy-repr` uses registered functions instead of methods * `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 New Features
------------------------------ ------------------------------

View File

@ -581,10 +581,10 @@ objects with the `__name__` magic will work.
.. code-block:: hy .. code-block:: hy
=> (keyword "foo") => (keyword "foo")
u'\ufdd0:foo' HyKeyword('foo')
=> (keyword 1) => (keyword 1)
u'\ufdd0:1' HyKeyword('foo')
.. _keyword?-fn: .. _keyword?-fn:

View File

@ -72,9 +72,7 @@ of bytes. So when running under Python 3, Hy translates ``"foo"`` and
keywords keywords
-------- --------
An identifier headed by a colon, such as ``:foo``, is a keyword. Keywords An identifier headed by a colon, such as ``:foo``, is a keyword. If a
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
literal keyword appears in a function call, it's used to indicate a keyword 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 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, the function ``f`` with the keyword argument named ``foo`` set to ``3``. Hence,

View File

@ -500,19 +500,18 @@ class HyASTCompiler(object):
except StopIteration: except StopIteration:
raise HyTypeError(expr, raise HyTypeError(expr,
"Keyword argument {kw} needs " "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) compiled_value = self.compile(value)
ret += compiled_value ret += compiled_value
keyword = expr[2:] arg = str_type(expr)[1:]
if not keyword:
raise HyTypeError(expr, "Can't call a function with the "
"empty keyword")
keyword = ast_str(keyword)
keywords.append(asty.keyword( keywords.append(asty.keyword(
expr, arg=keyword, value=compiled_value.force_expr)) expr, arg=ast_str(arg), value=compiled_value.force_expr))
else: else:
ret += self.compile(expr) ret += self.compile(expr)
@ -742,10 +741,13 @@ class HyASTCompiler(object):
return imports, HyExpression([HySymbol(name), return imports, HyExpression([HySymbol(name),
HyString(form)]).replace(form), False HyString(form)]).replace(form), False
elif isinstance(form, HyKeyword):
return imports, form, False
elif isinstance(form, HyString): elif isinstance(form, HyString):
x = [HySymbol(name), form] x = [HySymbol(name), form]
if form.brackets is not None: 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(x).replace(form), False
return imports, HyExpression([HySymbol(name), return imports, HyExpression([HySymbol(name),
@ -809,7 +811,7 @@ class HyASTCompiler(object):
ret += self.compile(expr.pop(0)) ret += self.compile(expr.pop(0))
cause = None cause = None
if len(expr) == 2 and expr[0] == HyKeyword(":from"): if len(expr) == 2 and expr[0] == HyKeyword("from"):
if not PY3: if not PY3:
raise HyCompileError( raise HyCompileError(
"raise from only supported in python 3") "raise from only supported in python 3")
@ -1168,7 +1170,7 @@ class HyASTCompiler(object):
if isinstance(iexpr, HyList) and iexpr: if isinstance(iexpr, HyList) and iexpr:
module = iexpr.pop(0) module = iexpr.pop(0)
entry = iexpr[0] entry = iexpr[0]
if isinstance(entry, HyKeyword) and entry == HyKeyword(":as"): if entry == HyKeyword("as"):
if not len(iexpr) == 2: if not len(iexpr) == 2:
raise HyTypeError(iexpr, raise HyTypeError(iexpr,
"garbage after aliased import") "garbage after aliased import")
@ -1462,7 +1464,7 @@ class HyASTCompiler(object):
else: else:
assignments = {} assignments = {}
while names: while names:
if len(names) > 1 and names[1] == HyKeyword(":as"): if len(names) > 1 and names[1] == HyKeyword("as"):
k, _, v = names[:3] k, _, v = names[:3]
del names[:3] del names[:3]
assignments[k] = v assignments[k] = v
@ -1471,7 +1473,7 @@ class HyASTCompiler(object):
assignments[symbol] = symbol assignments[symbol] = symbol
require(module, self.module_name, assignments=assignments) require(module, self.module_name, assignments=assignments)
elif (isinstance(entry, HyList) and len(entry) == 3 elif (isinstance(entry, HyList) and len(entry) == 3
and entry[1] == HyKeyword(":as")): and entry[1] == HyKeyword("as")):
# e.g., (require [foo :as bar]) # e.g., (require [foo :as bar])
module, _, prefix = entry module, _, prefix = entry
__import__(module) __import__(module)
@ -1764,7 +1766,7 @@ class HyASTCompiler(object):
# An exception for pulling together keyword args is if we're doing # An exception for pulling together keyword args is if we're doing
# a typecheck, eg (type :foo) # a typecheck, eg (type :foo)
with_kwargs = fn not in ( 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( args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect(
expression[1:], with_kwargs, oldpy_unpack=True) 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()) 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): def compile_string(self, string, building):
node = asty.Bytes if PY3 and building is HyBytes else asty.Str node = asty.Bytes if PY3 and building is HyBytes else asty.Str
f = bytes_type if building is HyBytes else str_type f = bytes_type if building is HyBytes else str_type

View File

@ -32,7 +32,7 @@
(global -quoting) (global -quoting)
(setv started-quoting False) (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 -quoting True)
(setv started-quoting True)) (setv started-quoting True))
@ -82,10 +82,8 @@
(+ (get syntax (first x)) (hy-repr (second x))) (+ (get syntax (first x)) (hy-repr (second x)))
(+ "(" (-cat x) ")")))) (+ "(" (-cat x) ")"))))
(hy-repr-register HySymbol str) (hy-repr-register [HySymbol HyKeyword] str)
(hy-repr-register [str-type bytes-type HyKeyword] (fn [x] (hy-repr-register [str-type bytes-type] (fn [x]
(if (and (instance? str-type x) (.startswith x HyKeyword.PREFIX))
(return (cut x 1)))
(setv r (.lstrip (-base-repr x) "ub")) (setv r (.lstrip (-base-repr x) "ub"))
(+ (+
(if (instance? bytes-type x) "b" "") (if (instance? bytes-type x) "b" "")

View File

@ -65,8 +65,7 @@
(defn keyword? [k] (defn keyword? [k]
"Check whether `k` is a keyword." "Check whether `k` is a keyword."
(and (instance? (type :foo) k) (instance? HyKeyword k))
(.startswith k (get :foo 0))))
(defn dec [n] (defn dec [n]
"Decrement `n` by 1." "Decrement `n` by 1."
@ -460,26 +459,26 @@ as EOF (defaults to an empty string)."
"Create a keyword from `value`. "Create a keyword from `value`.
Strings numbers and even objects with the __name__ magic will work." Strings numbers and even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX)) (if (keyword? value)
(unmangle value) (HyKeyword (unmangle value.name))
(if (string? value) (if (string? value)
(HyKeyword (+ ":" (unmangle value))) (HyKeyword (unmangle value))
(try (try
(unmangle (.__name__ value)) (unmangle (.__name__ value))
(except [] (HyKeyword (+ ":" (string value)))))))) (except [] (HyKeyword (string value)))))))
(defn name [value] (defn name [value]
"Convert `value` to a string. "Convert `value` to a string.
Keyword special character will be stripped. String will be used as is. Keyword special character will be stripped. String will be used as is.
Even objects with the __name__ magic will work." Even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX)) (if (keyword? value)
(unmangle (cut value 2)) (unmangle (cut (str value) 1))
(if (string? value) (if (string? value)
(unmangle value) (unmangle value)
(try (try
(unmangle (. value __name__)) (unmangle (. value __name__))
(except [] (string value)))))) (except [] (string value))))))
(defn xor [a b] (defn xor [a b]
"Perform exclusive or between `a` and `b`." "Perform exclusive or between `a` and `b`."

View File

@ -380,7 +380,7 @@ def symbol_like(obj):
pass pass
if obj.startswith(":") and "." not in obj: if obj.startswith(":") and "." not in obj:
return HyKeyword(obj) return HyKeyword(obj[1:])
@pg.error @pg.error

View File

@ -121,22 +121,35 @@ _wrappers[bool] = lambda x: HySymbol("True") if x else HySymbol("False")
_wrappers[type(None)] = lambda foo: HySymbol("None") _wrappers[type(None)] = lambda foo: HySymbol("None")
class HyKeyword(HyObject, str_type): class HyKeyword(HyObject):
"""Generic Hy Keyword object. It's either a ``str`` or a ``unicode``, """Generic Hy Keyword object."""
depending on the Python version.
"""
PREFIX = "\uFDD0" __slots__ = ['name']
def __new__(cls, value): def __init__(self, value):
if not value.startswith(cls.PREFIX): self.name = value
value = cls.PREFIX + value
obj = str_type.__new__(cls, value)
return obj
def __repr__(self): 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): def strip_digit_separators(number):

View File

@ -79,14 +79,6 @@
(assert (is (type (get orig 1)) float)) (assert (is (type (get orig 1)) float))
(assert (is (type (get result 1)) HyFloat))) (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 [] (when PY3 (defn test-dict-views []
(assert (= (hy-repr (.keys {1 2})) "(dict-keys [1])")) (assert (= (hy-repr (.keys {1 2})) "(dict-keys [1])"))
(assert (= (hy-repr (.values {1 2})) "(dict-values [2])")) (assert (= (hy-repr (.values {1 2})) "(dict-values [2])"))

View File

@ -554,7 +554,7 @@
(assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) (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 (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar"))
(assert (is (.isdigit :foo) False))) (assert (= (.__str__ :foo) ":foo")))
(defn test-do [] (defn test-do []
@ -1221,9 +1221,12 @@
"NATIVE: test if keywords are recognised" "NATIVE: test if keywords are recognised"
(assert (= :foo :foo)) (assert (= :foo :foo))
(assert (= :foo ':foo))
(assert (is (type :foo) (type ':foo)))
(assert (= (get {:foo "bar"} :foo) "bar")) (assert (= (get {:foo "bar"} :foo) "bar"))
(assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux"))) (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux")))
(defn test-keyword-clash [] (defn test-keyword-clash []
"NATIVE: test that keywords do not clash with normal strings" "NATIVE: test that keywords do not clash with normal strings"
@ -1649,11 +1652,6 @@ macros()
(setv (. foo [1] test) "hello") (setv (. foo [1] test) "hello")
(assert (= (getattr (. 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 [] (defn test-only-parse-lambda-list-in-defn []
"NATIVE: test lambda lists are only parsed in defn" "NATIVE: test lambda lists are only parsed in defn"
(try (try

View File

@ -434,9 +434,9 @@ def test_discard():
assert tokenize("(#_foo)") == [HyExpression()] assert tokenize("(#_foo)") == [HyExpression()]
assert tokenize("(#_foo bar)") == [HyExpression([HySymbol("bar")])] assert tokenize("(#_foo bar)") == [HyExpression([HySymbol("bar")])]
assert tokenize("(foo #_bar)") == [HyExpression([HySymbol("foo")])] 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"), 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 # discard term with nesting
assert tokenize("[1 2 #_[a b c [d e [f g] h]] 3 4]") == [ assert tokenize("[1 2 #_[a b c [d e [f g] h]] 3 4]") == [
HyList([HyInteger(1), HyInteger(2), HyInteger(3), HyInteger(4)]) HyList([HyInteger(1), HyInteger(2), HyInteger(3), HyInteger(4)])