diff --git a/CHANGELOG.md b/CHANGELOG.md index f9796eca..8406e253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Removed implicit support for single-use iterables in sequences, and introduced `iterator-seq` to expliciltly handle them (#1192) + * `basilisp.core/str` now delegates to the builtin Python `str` in all cases except for customizing the string output for builtin Python types (#1237) ### Fixed * Fix a bug where protocols with methods with leading hyphens in the could not be defined (#1230) diff --git a/src/basilisp/contrib/nrepl_server.lpy b/src/basilisp/contrib/nrepl_server.lpy index 8d058312..cbc15ec4 100644 --- a/src/basilisp/contrib/nrepl_server.lpy +++ b/src/basilisp/contrib/nrepl_server.lpy @@ -192,7 +192,7 @@ (read reader))} (catch python/Exception e (debug :symbol-identify-reader-error :input symbol-str :exception e) - {:error (str e)}))] + {:error (repr e)}))] (cond error @@ -210,7 +210,7 @@ :else (let [{:keys [var error]} (try {:var (ns-resolve resolve-ns form)} (catch python/Exception e - {:error (str e)}))] + {:error (repr e)}))] (cond var [:var var] error [:error error] diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index dec4a753..05e8a1e6 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -4394,8 +4394,9 @@ (defn pr "Print the arguments to the stream bound to :lpy:var:`*out*` in a format which is - readable by the Basilisp reader. Multiple arguments will be separated by the string - value bound to :lpy:var:`*print-sep*` (default is an ASCII space). + readable by the Basilisp reader (as by :lpy:fn:`repr`). Multiple arguments will be + separated by the string value bound to :lpy:var:`*print-sep*` (default is an ASCII + space). Note that some dynamically created Basilisp forms (such keywords and symbols) and Python objects may not be readable again. @@ -4461,13 +4462,13 @@ :lpy:var:`*print-sep*` (default is an ASCII space)." ([] (print "")) ([x] - (.write *out* (basilisp.lang.runtime/lstr x)) + (.write *out* (basilisp.lang.runtime/lrepr x ** :human-readable true)) nil) ([x & args] (let [stdout *out* sep *print-sep* - repr-args (interpose sep (map basilisp.lang.runtime/lstr args))] - (.write stdout (basilisp.lang.runtime/lstr x)) + repr-args (interpose sep (map #(basilisp.lang.runtime/lrepr % ** :human-readable true) args))] + (.write stdout (basilisp.lang.runtime/lrepr x ** :human-readable true)) (.write stdout sep) (.write stdout (apply str repr-args)) nil))) diff --git a/src/basilisp/lang/obj.py b/src/basilisp/lang/obj.py index 14972539..fecef8bd 100644 --- a/src/basilisp/lang/obj.py +++ b/src/basilisp/lang/obj.py @@ -1,4 +1,5 @@ import datetime +import fractions import math import re import uuid @@ -123,6 +124,49 @@ def seq_lrepr( return f"{start}{seq_lrepr}{end}" +@singledispatch +def lstr(o: Any) -> str: + return str(o) + + +@lstr.register(type(re.compile(""))) +def _lstr_pattern(o: Pattern) -> str: + return o.pattern + + +@lstr.register(bool) +@lstr.register(bytes) +@lstr.register(type(None)) +@lstr.register(str) +@lstr.register(list) +@lstr.register(dict) +@lstr.register(set) +@lstr.register(tuple) +@lstr.register(complex) +@lstr.register(float) +@lstr.register(datetime.datetime) +@lstr.register(Decimal) +@lstr.register(fractions.Fraction) +@lstr.register(Path) +def _lstr_lrepr(o) -> str: + """For built-in types, we want the `str()` representation to match the `lrepr()`. + + User types can be customized to their liking. + + This function intentionally does not capture the runtime values of the lrepr + keyword arguments.""" + return lrepr( + o, + human_readable=True, + print_dup=PRINT_DUP, + print_length=PRINT_LENGTH, + print_level=PRINT_LEVEL, + print_meta=PRINT_META, + print_namespace_maps=PRINT_NAMESPACE_MAPS, + print_readably=PRINT_READABLY, + ) + + # pylint: disable=unused-argument @singledispatch def lrepr( # pylint: disable=too-many-arguments diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index 3da62282..ea629fc3 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -2011,8 +2011,8 @@ def lrepr(o, human_readable: bool = False) -> str: def lstr(o) -> str: - """Produce a human readable string representation of an object.""" - return lrepr(o, human_readable=True) + """Produce a human-readable string representation of an object.""" + return lobj.lstr(o) __NOT_COMPLETEABLE = re.compile(r"^[0-9].*") diff --git a/tests/basilisp/lrepr_test.py b/tests/basilisp/lrepr_test.py index aba0968e..255cbc9b 100644 --- a/tests/basilisp/lrepr_test.py +++ b/tests/basilisp/lrepr_test.py @@ -168,6 +168,10 @@ def test_print_readably(lcompile: CompileFn): ("##Inf", "(pr-str ##Inf)"), ("##-Inf", "(pr-str ##-Inf)"), ('"hi"', '(pr-str "hi")'), + ("sym", "(pr-str 'sym)"), + ("some.ns/sym", "(pr-str 'some.ns/sym)"), + (":kw", "(pr-str :kw)"), + (":some.ns/kw", "(pr-str :some.ns/kw)"), ('"Hello\\nworld!"', '(pr-str "Hello\nworld!")'), (r'"\"Hello world!\""', r'(pr-str "\"Hello world!\"")'), ( @@ -196,6 +200,8 @@ def test_print_readably(lcompile: CompileFn): ("#py #{:a}", "(pr-str #py #{:a})"), ("#py ()", "(pr-str #py ())"), ('#py (:a 1 "s")', '(pr-str #py (:a 1 "s"))'), + ("#'basilisp.core/pr-str", "(pr-str #'pr-str)"), + ("basilisp.lrepr-test", "(pr-str *ns*)"), ], ) def test_lrepr(lcompile: CompileFn, repr: str, code: str): @@ -267,6 +273,10 @@ def test_lrepr_round_trip_special_cases(lcompile: CompileFn): ("##NaN", "(print-str ##NaN)"), ("##Inf", "(print-str ##Inf)"), ("##-Inf", "(print-str ##-Inf)"), + ("sym", "(print-str 'sym)"), + ("some.ns/sym", "(print-str 'some.ns/sym)"), + (":kw", "(print-str :kw)"), + (":some.ns/kw", "(print-str :some.ns/kw)"), ("hi", '(print-str "hi")'), ("Hello\nworld!", '(print-str "Hello\nworld!")'), ('"Hello world!"', r'(print-str "\"Hello world!\"")'), @@ -277,14 +287,8 @@ def test_lrepr_round_trip_special_cases(lcompile: CompileFn): ), # In Clojure, (print-str #uuid "...") produces '#uuid "..."' but in Basilisp # print-str is tied directly to str (which in Clojure simply returns the string - # part of the UUID). - # - # I'm not sure which one is right, but it feels a little inconsistent on the - # Clojure side. For now, I'm erring on the side of preferring for str to return - # only the string portion of the UUID and have print-str be slightly wrong. - # - # Maybe at some point, we can deep dive on whether Clojure is in error or if - # Basilisp should just try harder to match the Clojure side regardless. + # part of the UUID). I believe the more correct approach is Basilisp's, so + # I am leaving this as it is. ( "81f35603-0408-4b3d-bbc0-462e3702747f", '(print-str #uuid "81f35603-0408-4b3d-bbc0-462e3702747f")', @@ -298,6 +302,8 @@ def test_lrepr_round_trip_special_cases(lcompile: CompileFn): ("#py ()", "(print-str #py ())"), ("#py {}", "(print-str #py {})"), ("#py #{}", "(print-str #py #{})"), + ("#'basilisp.core/print-str", "(print-str #'print-str)"), + ("basilisp.lrepr-test", "(print-str *ns*)"), ], ) def test_lstr(lcompile: CompileFn, s: str, code: str): @@ -320,6 +326,10 @@ def test_lstr(lcompile: CompileFn, s: str, code: str): ("##NaN", "(str ##NaN)"), ("##Inf", "(str ##Inf)"), ("##-Inf", "(str ##-Inf)"), + ("sym", "(str 'sym)"), + ("some.ns/sym", "(str 'some.ns/sym)"), + (":kw", "(str :kw)"), + (":some.ns/kw", "(str :some.ns/kw)"), ("hi", '(str "hi")'), ("Hello\nworld!", '(str "Hello\nworld!")'), ('"Hello world!"', r'(str "\"Hello world!\"")'), @@ -332,7 +342,7 @@ def test_lstr(lcompile: CompileFn, s: str, code: str): "81f35603-0408-4b3d-bbc0-462e3702747f", '(str #uuid "81f35603-0408-4b3d-bbc0-462e3702747f")', ), - ('#"\\s"', '(str #"\\s")'), + ("\\s", '(str #"\\s")'), ( '#inst "2018-11-28T12:43:25.477000+00:00"', '(str #inst "2018-11-28T12:43:25.477-00:00")', @@ -341,6 +351,8 @@ def test_lstr(lcompile: CompileFn, s: str, code: str): ('#py ("a" 1 false)', '(str #py ("a" 1 false))'), ('#py {"a" "b"}', '(str #py {"a" "b"})'), ("#py #{}", "(str #py #{})"), + ("#'basilisp.core/str", "(str #'str)"), + ("basilisp.lrepr-test", "(str *ns*)"), ("abc", '(str "abc")'), ("false", "(str false)"), ("123", "(str 123)"), diff --git a/tests/basilisp/test_defrecord.lpy b/tests/basilisp/test_defrecord.lpy index 9a268e3e..dde951da 100644 --- a/tests/basilisp/test_defrecord.lpy +++ b/tests/basilisp/test_defrecord.lpy @@ -199,7 +199,7 @@ "#tests.basilisp.test-defrecord.Point{:y 2 :x 1 :z 3}" "#tests.basilisp.test-defrecord.Point{:x 1 :z 3 :y 2}" "#tests.basilisp.test-defrecord.Point{:z 3 :y 2 :x 1}"} - (str p))) + (repr p))) (is (contains? #{"^{:interesting :yes} #tests.basilisp.test-defrecord.Point{:x 1 :y 2 :z 3}" "^{:interesting :yes} #tests.basilisp.test-defrecord.Point{:y 2 :z 3 :x 1}" "^{:interesting :yes} #tests.basilisp.test-defrecord.Point{:z 3 :x 1 :y 2}" @@ -207,7 +207,7 @@ "^{:interesting :yes} #tests.basilisp.test-defrecord.Point{:x 1 :z 3 :y 2}" "^{:interesting :yes} #tests.basilisp.test-defrecord.Point{:z 3 :y 2 :x 1}"} (binding [*print-meta* true] - (str (with-meta p {:interesting :yes})))))))) + (repr (with-meta p {:interesting :yes})))))))) (definterface Shape (area [])) @@ -215,7 +215,10 @@ (defrecord Circle [radius] Shape (area [self] - (* 3.14 radius radius))) + (* 3.14 radius radius)) + + (__str__ [self] + (str "radius: " radius))) (defrecord InvokeConstructorsRecord [] (__call__ [self ctor] @@ -251,13 +254,18 @@ (testing "repr" (is (= "#tests.basilisp.test-defrecord.Circle{:radius 1}" - (str c))) + (repr c))) (is (= "^{:interesting :yes} #tests.basilisp.test-defrecord.Circle{:radius 1}" (binding [*print-meta* true] - (str (with-meta c {:interesting :yes}))))) + (pr-str (with-meta c {:interesting :yes}))))) (is (contains? #{"#tests.basilisp.test-defrecord.Circle{:radius 1 :name \"Kurt\"}" "#tests.basilisp.test-defrecord.Circle{:name \"Kurt\" :radius 1}"} - (str c1)))))) + (repr c1)))) + + (testing "str" + (is (= "radius: 1" (str c))) + (is (= "radius: 1" (str c1))) + (is (= "radius: 2" (str (assoc c :radius 2))))))) (defrecord ?Name [? x? ?x ?? ?x? ??x ?x- x?x]) @@ -279,19 +287,19 @@ p1 (assoc p :w 0) p2 (assoc p1 :new-key "some-value") p3 (dissoc p :y)] - (is (= p (eval (read-string (str p))))) - (is (= p1 (eval (read-string (str p1))))) - (is (= p2 (eval (read-string (str p2))))) - (is (= p3 (eval (read-string (str p3))))) + (is (= p (eval (read-string (repr p))))) + (is (= p1 (eval (read-string (repr p1))))) + (is (= p2 (eval (read-string (repr p2))))) + (is (= p3 (eval (read-string (repr p3))))) (is (= p #tests.basilisp.test-defrecord.Point[1 2 3])))) (testing "record with methods" (let [c (->Circle 1) c1 (assoc c :name "Kurt") c2 (assoc c1 :radius 3)] - (is (= c (eval (read-string (str c))))) - (is (= c1 (eval (read-string (str c1))))) - (is (= c2 (eval (read-string (str c2))))) + (is (= c (eval (read-string (repr c))))) + (is (= c1 (eval (read-string (repr c1))))) + (is (= c2 (eval (read-string (repr c2))))) (is (= c #tests.basilisp.test-defrecord.Circle[1])))) (testing "illegal other forms"