Merge pull request #1516 from Kodiologist/hy-repr-extensions

hy-repr improvements
This commit is contained in:
Kodi Arfer 2018-03-10 18:12:21 -08:00 committed by GitHub
commit 6d977ab541
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 307 additions and 110 deletions

View File

@ -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
==============================

View File

@ -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)"

View File

@ -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

View File

@ -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 <class 'int'> {\"a\" 8})"
"(defaultdict <type 'int'> {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")))