diff --git a/NEWS.rst b/NEWS.rst index 689dae5..beb22ed 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,14 @@ Bug Fixes ------------------------------ * Fix `(return)` so it works correctly to exit a Python 2 generator +Other Breaking Changes +----------------------------- +* `hy-repr` uses registered functions instead of methods + +Misc. Improvements +---------------------------- +* `hy-repr` supports more standard types + 0.14.0 ============================== diff --git a/docs/contrib/hy_repr.rst b/docs/contrib/hy_repr.rst index 51d8842..fae2625 100644 --- a/docs/contrib/hy_repr.rst +++ b/docs/contrib/hy_repr.rst @@ -4,10 +4,10 @@ Hy representations .. versionadded:: 0.13.0 -``hy.contrib.hy-repr`` is a module containing a single function. -To import it, say:: +``hy.contrib.hy-repr`` is a module containing two functions. +To import them, say:: - (import [hy.contrib.hy-repr [hy-repr]]) + (import [hy.contrib.hy-repr [hy-repr hy-repr-register]]) To make the Hy REPL use it for output, invoke Hy like so:: @@ -30,19 +30,47 @@ It returns a string representing the input object in Hy syntax. => (repr [1 2 3]) '[1, 2, 3]' -If the input object has a method ``__hy-repr__``, it will be called -instead of doing anything else. - -.. code-block:: hy - - => (defclass C [list] [__hy-repr__ (fn [self] "cuddles")]) - => (hy-repr (C)) - 'cuddles' - -When ``hy-repr`` doesn't know how to handle its input, it falls back -on ``repr``. - Like ``repr`` in Python, ``hy-repr`` can round-trip many kinds of values. Round-tripping implies that given an object ``x``, ``(eval (read-str (hy-repr x)))`` returns ``x``, or at least a value that's equal to ``x``. + +.. _hy-repr-register-fn: + +hy-repr-register +---------------- + +Usage: ``(hy-repr-register the-type fun)`` + +``hy-repr-register`` lets you set the function that ``hy-repr`` calls to +represent a type. + +.. code-block:: hy + + => (defclass C) + => (hy-repr-register C (fn [x] "cuddles")) + => (hy-repr [1 (C) 2]) + '[1 cuddles 2]' + +If the type of an object passed to ``hy-repr`` doesn't have a registered +function, ``hy-repr`` will search the type's method resolution order +(its ``__mro__`` attribute) for the first type that does. If ``hy-repr`` +doesn't find a candidate, it falls back on ``repr``. + +Registered functions often call ``hy-repr`` themselves. ``hy-repr`` will +automatically detect self-references, even deeply nested ones, and +output ``"..."`` for them instead of calling the usual registered +function. To use a placeholder other than ``"..."``, pass a string of +your choice to the keyword argument ``:placeholder`` of +``hy-repr-register``. + +.. code-block:: hy + + (defclass Container [object] + [__init__ (fn [self value] + (setv self.value value))]) + (hy-repr-register Container :placeholder "HY THERE" (fn [x] + (+ "(Container " (hy-repr x.value) ")"))) + (setv container (Container 5)) + (setv container.value container) + (print (hy-repr container)) ; Prints "(Container HY THERE)" diff --git a/hy/contrib/hy_repr.hy b/hy/contrib/hy_repr.hy index e47adcf..627649a 100644 --- a/hy/contrib/hy_repr.hy +++ b/hy/contrib/hy_repr.hy @@ -4,95 +4,161 @@ (import [math [isnan]] - [hy._compat [PY3 str-type bytes-type long-type]] + re + datetime + collections + [hy._compat [PY3 PY36 str-type bytes-type long-type]] [hy.models [HyObject HyExpression HySymbol HyKeyword HyInteger HyFloat HyComplex HyList HyDict HySet HyString HyBytes]]) -(defn hy-repr [obj] - (setv seen (set)) - ; We keep track of objects we've already seen, and avoid - ; redisplaying their contents, so a self-referential object - ; doesn't send us into an infinite loop. - (defn f [x q] - ; `x` is the current object being stringified. - ; `q` is True if we're inside a single quote, False otherwise. - (setv old? (in (id x) seen)) - (.add seen (id x)) - (setv t (type x)) - (defn catted [] - (if old? "..." (.join " " (list-comp (f it q) [it x])))) - (setv prefix "") - (if (and (not q) (instance? HyObject x)) - (setv prefix "'" q True)) - (+ prefix (if - (hasattr x "__hy_repr__") - (.__hy-repr__ x) - (is t HyExpression) - (if (and x (symbol? (first x))) - (if - (= (first x) 'quote) - (+ "'" (f (second x) True)) - (= (first x) 'quasiquote) - (+ "`" (f (second x) q)) - (= (first x) 'unquote) - (+ "~" (f (second x) q)) - (= (first x) 'unquote_splice) - (+ "~@" (f (second x) q)) - (= (first x) 'unpack_iterable) - (+ "#* " (f (second x) q)) - (= (first x) 'unpack_mapping) - (+ "#** " (f (second x) q)) - ; else - (+ "(" (catted) ")")) - (+ "(" (catted) ")")) - (is t tuple) - (+ "(," (if x " " "") (catted) ")") - (in t [list HyList]) - (+ "[" (catted) "]") - (is t HyDict) - (+ "{" (catted) "}") - (is t dict) - (+ - "{" - (if old? "..." (.join " " (list-comp - (+ (f k q) " " (f v q)) - [[k v] (.items x)]))) - "}") - (in t [set HySet]) - (+ "#{" (catted) "}") - (is t frozenset) - (+ "(frozenset #{" (catted) "})") - (is t HySymbol) - x - (or (is t HyKeyword) (and (is t str-type) (.startswith x HyKeyword.PREFIX))) - (cut x 1) - (in t [str-type HyString bytes-type HyBytes]) (do - (setv r (.lstrip (base-repr x) "ub")) - (+ (if (in t [bytes-type HyBytes]) "b" "") (if (.startswith "\"" r) - ; If Python's built-in repr produced a double-quoted string, use - ; that. - r - ; Otherwise, we have a single-quoted string, which isn't valid Hy, so - ; convert it. - (+ "\"" (.replace (cut r 1 -1) "\"" "\\\"") "\"")))) - (and (not PY3) (is t int)) - (.format "(int {})" (base-repr x)) - (and (not PY3) (in t [long_type HyInteger])) - (.rstrip (base-repr x) "L") - (and (in t [float HyFloat]) (isnan x)) - "NaN" - (and (in t [float HyFloat]) (= x Inf)) - "Inf" - (and (in t [float HyFloat]) (= x -Inf)) - "-Inf" - (in t [complex HyComplex]) - (.replace (.replace (.strip (base-repr x) "()") "inf" "Inf") "nan" "NaN") - (is t fraction) - (.format "{}/{}" (f x.numerator q) (f x.denominator q)) - ; else - (base-repr x)))) - (f obj False)) +(try + (import [_collections_abc [dict-keys dict-values dict-items]]) + (except [ImportError] + (defclass C) + (setv [dict-keys dict-values dict-items] [C C C]))) -(defn base-repr [x] +(setv -registry {}) +(defn hy-repr-register [types f &optional placeholder] + (for [typ (if (instance? list types) types [types])] + (setv (get -registry typ) (, f placeholder)))) + +(setv -quoting False) +(setv -seen (set)) +(defn hy-repr [obj] + (setv [f placeholder] (next + (genexpr (get -registry t) + [t (. (type obj) __mro__)] + (in t -registry)) + [-base-repr None])) + + (global -quoting) + (setv started-quoting False) + (when (and (not -quoting) (instance? HyObject obj)) + (setv -quoting True) + (setv started-quoting True)) + + (setv oid (id obj)) + (when (in oid -seen) + (return (if (none? placeholder) "..." placeholder))) + (.add -seen oid) + + (try + (+ (if started-quoting "'" "") (f obj)) + (finally + (.discard -seen oid) + (when started-quoting + (setv -quoting False))))) + +(hy-repr-register tuple (fn [x] + (if (hasattr x "_fields") + ; It's a named tuple. (We can't use `instance?` or so because + ; generated named-tuple classes don't actually inherit from + ; collections.namedtuple.) + (.format "({} {})" + (. (type x) __name__) + (.join " " (genexpr (+ ":" k " " (hy-repr v)) [[k v] (zip x._fields x)]))) + ; Otherwise, print it as a regular tuple. + (+ "(," (if x " " "") (-cat x) ")")))) +(hy-repr-register dict :placeholder "{...}" (fn [x] + (setv text (.join " " (genexpr + (+ (hy-repr k) " " (hy-repr v)) + [[k v] (.items x)]))) + (+ "{" text "}"))) +(hy-repr-register HyDict :placeholder "{...}" (fn [x] + (setv text (.join " " (genexpr + (+ (hy-repr k) " " (hy-repr v)) + [[k v] (partition x)]))) + (if (% (len x) 2) + (+= text (+ " " (hy-repr (get x -1))))) + (+ "{" text "}"))) +(hy-repr-register HyExpression (fn [x] + (setv syntax { + 'quote "'" + 'quasiquote "`" + 'unquote "~" + 'unquote_splice "~@" + 'unpack_iterable "#* " + 'unpack_mapping "#** "}) + (if (and x (symbol? (first x)) (in (first x) syntax)) + (+ (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))) + (setv r (.lstrip (-base-repr x) "ub")) + (+ + (if (instance? bytes-type x) "b" "") + (if (.startswith "\"" r) + ; If Python's built-in repr produced a double-quoted string, use + ; that. + r + ; Otherwise, we have a single-quoted string, which isn't valid Hy, so + ; convert it. + (+ "\"" (.replace (cut r 1 -1) "\"" "\\\"") "\""))))) +(hy-repr-register bool str) +(if (not PY3) (hy-repr-register int (fn [x] + (.format "(int {})" (-base-repr x))))) +(if (not PY3) (hy-repr-register long_type (fn [x] + (.rstrip (-base-repr x) "L")))) +(hy-repr-register float (fn [x] + (if + (isnan x) "NaN" + (= x Inf) "Inf" + (= x -Inf) "-Inf" + (-base-repr x)))) +(hy-repr-register complex (fn [x] + (.replace (.replace (.strip (-base-repr x) "()") "inf" "Inf") "nan" "NaN"))) +(hy-repr-register fraction (fn [x] + (.format "{}/{}" (hy-repr x.numerator) (hy-repr x.denominator)))) + +(setv -matchobject-type (type (re.match "" ""))) +(hy-repr-register -matchobject-type (fn [x] + (.format "<{}.{} object; :span {} :match {}>" + -matchobject-type.__module__ + -matchobject-type.__name__ + (hy-repr (.span x)) + (hy-repr (.group x 0))))) + +(hy-repr-register datetime.datetime (fn [x] + (.format "(datetime.datetime {}{})" + (.strftime x "%Y %-m %-d %-H %-M %-S") + (-repr-time-innards x)))) +(hy-repr-register datetime.date (fn [x] + (.strftime x "(datetime.date %Y %-m %-d)"))) +(hy-repr-register datetime.time (fn [x] + (.format "(datetime.time {}{})" + (.strftime x "%-H %-M %-S") + (-repr-time-innards x)))) +(defn -repr-time-innards [x] + (.rstrip (+ " " (.join " " (filter identity [ + (if x.microsecond (str-type x.microsecond)) + (if (not (none? x.tzinfo)) (+ ":tzinfo " (hy-repr x.tzinfo))) + (if (and PY36 (!= x.fold 0)) (+ ":fold " (hy-repr x.fold)))]))))) + +(hy-repr-register collections.Counter (fn [x] + (.format "(Counter {})" + (hy-repr (dict x))))) +(hy-repr-register collections.defaultdict (fn [x] + (.format "(defaultdict {} {})" + (hy-repr x.default-factory) + (hy-repr (dict x))))) + +(for [[types fmt] (partition [ + list "[...]" + [set HySet] "#{...}" + frozenset "(frozenset #{...})" + dict-keys "(dict-keys [...])" + dict-values "(dict-values [...])" + dict-items "(dict-items [...])"])] + (defn mkrepr [fmt] + (fn [x] (.replace fmt "..." (-cat x) 1))) + (hy-repr-register types :placeholder fmt (mkrepr fmt))) + +(defn -cat [obj] + (.join " " (map hy-repr obj))) + +(defn -base-repr [x] (unless (instance? HyObject x) (return (repr x))) ; Call (.repr x) using the first class of x that doesn't inherit from diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy index 3f40fcc..a896013 100644 --- a/tests/native_tests/contrib/hy_repr.hy +++ b/tests/native_tests/contrib/hy_repr.hy @@ -3,8 +3,9 @@ ;; license. See the LICENSE. (import + [hy._compat [PY3 PY36]] [math [isnan]] - [hy.contrib.hy-repr [hy-repr]]) + [hy.contrib.hy-repr [hy-repr hy-repr-register]]) (defn test-hy-repr-roundtrip-from-value [] ; Test that a variety of values round-trip properly. @@ -27,7 +28,7 @@ '(+ 1 2) [1 2 3] (, 1 2 3) #{1 2 3} (frozenset #{1 2 3}) '[1 2 3] '(, 1 2 3) '#{1 2 3} '(frozenset #{1 2 3}) - {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3} + {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3} [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"} '(f #* a #** b)] '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"} (f #* a #** b)]]) (for [original-val values] @@ -57,7 +58,7 @@ "'[1 `[~(+ 1 2) ~@(+ [1] [2])] 4]" "'[1 `[~(do (print x 'y) 1)] 4]" "{1 20}" - "'{1 10 1 20}" + "'{1 10 1 20}" "'asymbol" ":akeyword" "'(f #* args #** kwargs)"]) @@ -65,12 +66,83 @@ (setv rep (hy-repr (eval (read-str original-str)))) (assert (= rep original-str)))) +(defn test-hy-repr-no-roundtrip [] + ; Test one of the corner cases in which hy-repr doesn't + ; round-trip: when a HyObject contains a non-HyObject, we + ; promote the constituent to a HyObject. + + (setv orig `[a ~5.0]) + (setv reprd (hy-repr orig)) + (assert (= reprd "'[a 5.0]")) + (setv result (eval (read-str reprd))) + + (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])")) + (assert (= (hy-repr (.items {1 2})) "(dict-items [(, 1 2)])")))) + +(defn test-datetime [] + (import [datetime :as D]) + + (assert (= (hy-repr (D.datetime 2009 1 15 15 27 5 0)) + "(datetime.datetime 2009 1 15 15 27 5)")) + (assert (= (hy-repr (D.datetime 2009 1 15 15 27 5 123)) + "(datetime.datetime 2009 1 15 15 27 5 123)")) + (when PY3 + (assert (= (hy-repr (D.datetime 2009 1 15 15 27 5 123 :tzinfo D.timezone.utc)) + "(datetime.datetime 2009 1 15 15 27 5 123 :tzinfo datetime.timezone.utc)"))) + (when PY36 + (assert (= (hy-repr (D.datetime 2009 1 15 15 27 5 :fold 1)) + "(datetime.datetime 2009 1 15 15 27 5 :fold 1)")) + (assert (= (hy-repr (D.datetime 2009 1 15 15 27 5 :fold 1 :tzinfo D.timezone.utc)) + "(datetime.datetime 2009 1 15 15 27 5 :tzinfo datetime.timezone.utc :fold 1)"))) + + (assert (= (hy-repr (D.date 2015 11 3)) + "(datetime.date 2015 11 3)")) + + (assert (= (hy-repr (D.time 1 2 3)) + "(datetime.time 1 2 3)")) + (assert (= (hy-repr (D.time 1 2 3 4567)) + "(datetime.time 1 2 3 4567)")) + (when PY36 + (assert (= (hy-repr (D.time 1 2 3 4567 :fold 1 :tzinfo D.timezone.utc)) + "(datetime.time 1 2 3 4567 :tzinfo datetime.timezone.utc :fold 1)")))) + +(defn test-collections [] + (import collections) + (assert (= (hy-repr (collections.defaultdict :a 8)) + (if PY3 + "(defaultdict None {\"a\" 8})" + "(defaultdict None {b\"a\" 8})"))) + (assert (= (hy-repr (collections.defaultdict int :a 8)) + (if PY3 + "(defaultdict {\"a\" 8})" + "(defaultdict {b\"a\" 8})"))) + (assert (= (hy-repr (collections.Counter [15 15 15 15])) + (if PY3 + "(Counter {15 4})" + "(Counter {15 (int 4)})"))) + (setv C (collections.namedtuple "Fooey" ["cd" "a_b"])) + (assert (= (hy-repr (C 11 12)) + "(Fooey :cd 11 :a_b 12)"))) + (defn test-hy-model-constructors [] (import hy) (assert (= (hy-repr (hy.HyInteger 7)) "'7")) (assert (= (hy-repr (hy.HyString "hello")) "'\"hello\"")) (assert (= (hy-repr (hy.HyList [1 2 3])) "'[1 2 3]")) - (assert (= (hy-repr (hy.HyDict [1 2 3])) "'{1 2 3}"))) + (assert (= (hy-repr (hy.HyDict [1 2 3])) "'{1 2 3}"))) (defn test-hy-repr-self-reference [] @@ -83,13 +155,36 @@ (assert (in (hy-repr x) (list-comp ; The ordering of a dictionary isn't guaranteed, so we need ; to check for all possible orderings. - (+ "{" (.join " " p) "}") + (+ "{" (.join " " p) "}") [p (permutations ["1 2" "3 [4 {...}]" "6 7"])])))) -(defn test-hy-repr-dunder-method [] - (defclass C [list] [__hy-repr__ (fn [self] "cuddles")]) - (assert (= (hy-repr (C)) "cuddles"))) +(defn test-matchobject [] + (import re) + (setv mo (re.search "b+" "aaaabbbccc")) + (assert (= (hy-repr mo) + (.format + #[[<{}.SRE_Match object; :span {} :match "bbb">]] + (. (type mo) __module__) + (if PY3 "(, 4 7)" "(, (int 4) (int 7))"))))) + +(defn test-hy-repr-custom [] + + (defclass C [object]) + (hy-repr-register C (fn [x] "cuddles")) + (assert (= (hy-repr (C)) "cuddles")) + + (defclass Container [object] + [__init__ (fn [self value] + (setv self.value value))]) + (hy-repr-register Container :placeholder "(Container ...)" (fn [x] + (+ "(Container " (hy-repr x.value) ")"))) + (setv container (Container 5)) + (setv container.value container) + (assert (= (hy-repr container) "(Container (Container ...))")) + (setv container.value [1 container 3]) + (assert (= (hy-repr container) "(Container [1 (Container ...) 3])"))) (defn test-hy-repr-fallback [] - (defclass D [list] [__repr__ (fn [self] "cuddles")]) + (defclass D [object] + [__repr__ (fn [self] "cuddles")]) (assert (= (hy-repr (D)) "cuddles")))