From bf2f90a0d9d4d4ec53424d306a410653d01e7c86 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 20 Mar 2017 12:04:00 -0700 Subject: [PATCH] Add hy.contrib.hy-repr --- NEWS | 4 ++ docs/contrib/hy_repr.rst | 42 ++++++++++++++ docs/contrib/index.rst | 1 + hy/contrib/hy_repr.hy | 77 +++++++++++++++++++++++++ tests/__init__.py | 1 + tests/native_tests/contrib/hy_repr.hy | 82 +++++++++++++++++++++++++++ 6 files changed, 207 insertions(+) create mode 100644 docs/contrib/hy_repr.rst create mode 100644 hy/contrib/hy_repr.hy create mode 100644 tests/native_tests/contrib/hy_repr.hy diff --git a/NEWS b/NEWS index 7584628..b0d5d24 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,10 @@ Changes from 0.12.1 returns. * `setv` no longer unnecessarily tries to get attributes + [ Misc. Improvements ] + * New contrib module `hy-repr` + * Added a command-line option --hy-repr + Changes from 0.12.0 [ Bug Fixes ] diff --git a/docs/contrib/hy_repr.rst b/docs/contrib/hy_repr.rst new file mode 100644 index 0000000..c9ad777 --- /dev/null +++ b/docs/contrib/hy_repr.rst @@ -0,0 +1,42 @@ +================== +Hy representations +================== + +.. versionadded:: 0.13.0 + +``hy.contrib.hy-repr`` is a module containing a single function. +To import it, say ``(import [hy.contrib.hy-repr [hy-repr]])``. + +.. _hy-repr-fn: + +hy-repr +------- + +Usage: ``(hy-repr x)`` + +This function is Hy's equivalent of Python's built-in ``repr``. +It returns a string representing the input object in Hy syntax. + +.. code-block:: hy + + => (hy-repr [1 2 3]) + '[1 2 3]' + => (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``. diff --git a/docs/contrib/index.rst b/docs/contrib/index.rst index 67c8abe..917e12b 100644 --- a/docs/contrib/index.rst +++ b/docs/contrib/index.rst @@ -16,3 +16,4 @@ Contents: profile sequences walk + hy_repr diff --git a/hy/contrib/hy_repr.hy b/hy/contrib/hy_repr.hy new file mode 100644 index 0000000..e70a5a8 --- /dev/null +++ b/hy/contrib/hy_repr.hy @@ -0,0 +1,77 @@ +(import [hy._compat [PY3 str-type bytes-type long-type]]) +(import [hy.models [HyObject HyExpression HySymbol HyKeyword HyInteger 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)) + ; 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 (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 {})" (repr x)) + (and (not PY3) (in t [long_type HyInteger])) + (.rstrip (repr x) "L") + (is t complex) + (.strip (repr x) "()") + (is t fraction) + (.format "{}/{}" (f x.numerator q) (f x.denominator q)) + ; else + (repr x)))) + (f obj False)) diff --git a/tests/__init__.py b/tests/__init__.py index 88a1cad..74b8390 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,6 +19,7 @@ from .native_tests.contrib.loop import * # noqa from .native_tests.contrib.walk import * # noqa from .native_tests.contrib.multi import * # noqa from .native_tests.contrib.sequences import * # noqa +from .native_tests.contrib.hy_repr import * # noqa if PY3: from .native_tests.py3_only_tests import * # noqa diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy new file mode 100644 index 0000000..04dbce6 --- /dev/null +++ b/tests/native_tests/contrib/hy_repr.hy @@ -0,0 +1,82 @@ +(import + [hy.contrib.hy-repr [hy-repr]]) + +(defn test-hy-repr-roundtrip-from-value [] + ; Test that a variety of values round-trip properly. + (setv values [ + None False True + 5 5.1 '5 '5.1 + (int 5) + 1/2 + 5j 5.1j 2+1j 1.2+3.4j + "" b"" + '"" 'b"" + "apple bloom" b"apple bloom" "⚘" + '"apple bloom" 'b"apple bloom" '"⚘" + "single ' quotes" b"single ' quotes" + "\"double \" quotes\"" b"\"double \" quotes\"" + 'mysymbol :mykeyword + [] (,) #{} (frozenset #{}) + '[] '(,) '#{} '(frozenset #{}) + '['[]] + '(+ 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} + [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"}] + '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"}]]) + (for [original-val values] + (setv evaled (eval (read-str (hy-repr original-val)))) + (assert (= evaled original-val)) + (assert (is (type evaled) (type original-val))))) + +(defn test-hy-repr-roundtrip-from-str [] + (setv strs [ + "[1 2 3]" + "'[1 2 3]" + "[1 'a 3]" + "'[1 a 3]" + "'[1 'a 3]" + "[1 '[2 3] 4]" + "'[1 [2 3] 4]" + "'[1 '[2 3] 4]" + "'[1 `[2 3] 4]" + "'[1 `[~foo ~@bar] 4]" + "'[1 `[~(+ 1 2) ~@(+ [1] [2])] 4]" + "'[1 `[~(do (print x 'y) 1)] 4]" + "{1 20}" + "'{1 10 1 20}" + "'asymbol" + ":akeyword"]) + (for [original-str strs] + (setv rep (hy-repr (eval (read-str original-str)))) + (assert (= rep original-str)))) + +(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}"))) + +(defn test-hy-repr-self-reference [] + + (setv x [1 2 3]) + (setv (get x 1) x) + (assert (= (hy-repr x) "[1 [...] 3]")) + + (setv x {1 2 3 [4 5] 6 7}) + (setv (get x 3 1) x) + (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) "}") + [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-hy-repr-fallback [] + (defclass D [list] [__repr__ (fn [self] "cuddles")]) + (assert (= (hy-repr (D)) "cuddles")))