Skip to content

Commit d1dae79

Browse files
authored
Produce a Lisp string representation from LispObject ABC (#316)
* Produce a Lisp string representation from LispObject ABC * Fix linting and MyPy errors * Fix old references * Add some tests * More lrepr tests * Fix emergent MyPy error * repr/str support * Fix unicode * Refactor a bit * Test print-dup and print-readably * Reformat
1 parent 95ee3b0 commit d1dae79

File tree

17 files changed

+486
-87
lines changed

17 files changed

+486
-87
lines changed

src/basilisp/core/__init__.lpy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,13 @@
276276
(defn str
277277
"Create a string representation of o."
278278
([] "")
279-
([o] (builtins/str o))
279+
([o] (basilisp.lang.runtime/lstr o))
280280
([o & args]
281281
(let [coerce (fn [in out]
282282
(if (seq (rest in))
283283
(recur (rest in)
284-
(conj out (builtins/str (first in))))
285-
(conj out (builtins/str (first in)))))
284+
(conj out (basilisp.lang.runtime/lstr (first in))))
285+
(conj out (basilisp.lang.runtime/lstr (first in)))))
286286
strs (coerce (conj args o) [])]
287287
(.join "" strs))))
288288

@@ -1250,7 +1250,7 @@
12501250
(defn repr
12511251
"Return the reader representation of an object."
12521252
[x]
1253-
(basilisp.lang.util/lrepr x))
1253+
(basilisp.lang.runtime/lrepr x))
12541254

12551255
(defn flush
12561256
"Flush the buffer currently bound to *out*."

src/basilisp/lang/compiler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import basilisp.lang.seq as lseq
4242
import basilisp.lang.set as lset
4343
import basilisp.lang.symbol as sym
44-
import basilisp.lang.util
4544
import basilisp.lang.vector as vec
4645
from basilisp.lang.runtime import Var
4746
from basilisp.lang.typing import LispForm
@@ -2428,4 +2427,4 @@ def compile_bytecode(
24282427
exec(bytecode, module.__dict__)
24292428

24302429

2431-
lrepr = basilisp.lang.util.lrepr
2430+
lrepr = runtime.lrepr

src/basilisp/lang/exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import basilisp.lang.map as lmap
2-
from basilisp.lang.util import lrepr
2+
from basilisp.lang.obj import lrepr
33

44

55
class ExceptionInfo(Exception):

src/basilisp/lang/keyword.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
import basilisp.lang.associative as lassoc
66
import basilisp.lang.atom as atom
7+
from basilisp.lang.obj import LispObject
78

89
__INTERN = atom.Atom(pmap())
910

1011

11-
class Keyword:
12+
class Keyword(LispObject):
1213
__slots__ = ("_name", "_ns")
1314

1415
def __init__(self, name: str, ns: Optional[str] = None) -> None:
@@ -23,14 +24,11 @@ def name(self) -> str:
2324
def ns(self) -> Optional[str]:
2425
return self._ns
2526

26-
def __str__(self):
27+
def _lrepr(self, **kwargs) -> str:
2728
if self._ns is not None:
2829
return ":{ns}/{name}".format(ns=self._ns, name=self._name)
2930
return ":{name}".format(name=self._name)
3031

31-
def __repr__(self):
32-
return str(self)
33-
3432
def __eq__(self, other):
3533
return self is other
3634

src/basilisp/lang/list.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from basilisp.lang.collection import Collection
55
from basilisp.lang.meta import Meta
6+
from basilisp.lang.obj import LispObject
67
from basilisp.lang.seq import Seq, EMPTY
7-
from basilisp.lang.util import lrepr
88

99

1010
class List(Collection, Meta, Seq):
@@ -19,9 +19,6 @@ def __init__(self, wrapped: PList, meta=None) -> None:
1919
self._inner = wrapped
2020
self._meta = meta
2121

22-
def __repr__(self):
23-
return "({list})".format(list=" ".join(map(lrepr, self._inner)))
24-
2522
def __eq__(self, other):
2623
return self._inner == other
2724

@@ -36,6 +33,9 @@ def __hash__(self):
3633
def __len__(self):
3734
return len(self._inner)
3835

36+
def _lrepr(self, **kwargs) -> str:
37+
return LispObject.seq_lrepr(self._inner, "(", ")", meta=self._meta, **kwargs)
38+
3939
@property
4040
def meta(self):
4141
return self._meta

src/basilisp/lang/map.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from collections import Sequence
2-
from typing import Optional # noqa: F401
2+
from typing import Optional # noqa # pylint: disable=unused-import
33

44
from functional import seq
55
from pyrsistent import pmap, PMap
66

7+
import basilisp.lang.obj as lobj
78
import basilisp.lang.vector as vec
89
from basilisp.lang.associative import Associative
910
from basilisp.lang.collection import Collection
1011
from basilisp.lang.meta import Meta
12+
from basilisp.lang.obj import LispObject, lrepr
1113
from basilisp.lang.seq import Seqable, sequence, Seq
12-
from basilisp.lang.util import lrepr
1314
from basilisp.util import partition
1415

1516

@@ -60,7 +61,7 @@ def from_vec(v: Sequence) -> "MapEntry":
6061
return MapEntry(vec.vector(v))
6162

6263

63-
class Map(Associative, Collection, Meta, Seqable):
64+
class Map(Associative, Collection, LispObject, Meta, Seqable):
6465
"""Basilisp Map. Delegates internally to a pyrsistent.PMap object.
6566
Do not instantiate directly. Instead use the m() and map() factory
6667
methods below."""
@@ -71,12 +72,6 @@ def __init__(self, wrapped: PMap, meta=None) -> None:
7172
self._inner = wrapped
7273
self._meta = meta
7374

74-
def __repr__(self):
75-
kvs = [
76-
"{k} {v}".format(k=lrepr(k), v=lrepr(v)) for k, v in self._inner.iteritems()
77-
]
78-
return "{{{kvs}}}".format(kvs=" ".join(kvs))
79-
8075
def __call__(self, key, default=None):
8176
return self._inner.get(key, default)
8277

@@ -102,6 +97,36 @@ def __iter__(self):
10297
def __len__(self):
10398
return len(self._inner)
10499

100+
def _lrepr(self, **kwargs):
101+
print_level = kwargs["print_level"]
102+
if isinstance(print_level, int) and print_level < 1:
103+
return lobj.SURPASSED_PRINT_LEVEL
104+
105+
kwargs = LispObject._process_kwargs(**kwargs)
106+
107+
def entry_reprs():
108+
for k, v in self._inner.iteritems():
109+
yield "{k} {v}".format(k=lrepr(k, **kwargs), v=lrepr(v, **kwargs))
110+
111+
trailer = []
112+
print_dup = kwargs["print_dup"]
113+
print_length = kwargs["print_length"]
114+
if not print_dup and isinstance(print_length, int):
115+
items = seq(entry_reprs()).take(print_length + 1).to_list()
116+
if len(items) > print_length:
117+
items.pop()
118+
trailer.append(lobj.SURPASSED_PRINT_LENGTH)
119+
else:
120+
items = list(entry_reprs())
121+
122+
seq_lrepr = lobj.PRINT_SEPARATOR.join(items + trailer)
123+
124+
print_meta = kwargs["print_meta"]
125+
if print_meta and self._meta:
126+
return f"^{lrepr(self._meta, **kwargs)} {{{seq_lrepr}}}"
127+
128+
return f"{{{seq_lrepr}}}"
129+
105130
def items(self):
106131
return self._inner.items()
107132

src/basilisp/lang/meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
class Meta(ABC):
5-
slots = ()
5+
__slots__ = ()
66

77
@property
88
@abstractmethod

src/basilisp/lang/obj.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import datetime
2+
import uuid
3+
from abc import ABC, abstractmethod
4+
from decimal import Decimal
5+
from fractions import Fraction
6+
from typing import Union, Any, Pattern, Iterable
7+
8+
from functional import seq
9+
from pyrsistent import pmap
10+
11+
PrintCountSetting = Union[bool, int, None]
12+
13+
SURPASSED_PRINT_LENGTH = "..."
14+
SURPASSED_PRINT_LEVEL = "#"
15+
16+
PRINT_DUP = False
17+
PRINT_LENGTH: PrintCountSetting = 50
18+
PRINT_LEVEL: PrintCountSetting = None
19+
PRINT_META = False
20+
PRINT_READABLY = True
21+
PRINT_SEPARATOR = " "
22+
23+
24+
def _dec_print_level(lvl: PrintCountSetting):
25+
"""Decrement the print level if it is numeric."""
26+
if isinstance(lvl, int):
27+
return lvl - 1
28+
return lvl
29+
30+
31+
class LispObject(ABC):
32+
__slots__ = ()
33+
34+
def __repr__(self):
35+
return self.lrepr()
36+
37+
def __str__(self):
38+
return self.lrepr(human_readable=True)
39+
40+
@abstractmethod
41+
def _lrepr(self, **kwargs) -> str:
42+
"""Private Lisp representation method. Callers (including object
43+
internal callers) should not call this method directly, but instead
44+
should use the module function .lrepr()."""
45+
raise NotImplementedError()
46+
47+
def lrepr(self, **kwargs) -> str:
48+
"""Return a string representation of this Lisp object which can be
49+
read by the reader."""
50+
return lrepr(self, **kwargs)
51+
52+
@staticmethod
53+
def seq_lrepr(
54+
iterable: Iterable[Any], start: str, end: str, meta=None, **kwargs
55+
) -> str:
56+
"""Produce a Lisp representation of a sequential collection, bookended
57+
with the start and end string supplied. The keyword arguments will be
58+
passed along to lrepr for the sequence elements."""
59+
print_level = kwargs["print_level"]
60+
if isinstance(print_level, int) and print_level < 1:
61+
return SURPASSED_PRINT_LEVEL
62+
63+
kwargs = LispObject._process_kwargs(**kwargs)
64+
65+
trailer = []
66+
print_dup = kwargs["print_dup"]
67+
print_length = kwargs["print_length"]
68+
if not print_dup and isinstance(print_length, int):
69+
items = seq(iterable).take(print_length + 1).to_list()
70+
if len(items) > print_length:
71+
items.pop()
72+
trailer.append(SURPASSED_PRINT_LENGTH)
73+
else:
74+
items = iterable
75+
76+
items = seq(items).map(lambda o: lrepr(o, **kwargs)).to_list()
77+
seq_lrepr = PRINT_SEPARATOR.join(items + trailer)
78+
79+
print_meta = kwargs["print_meta"]
80+
if print_meta and meta:
81+
return f"^{lrepr(meta, **kwargs)} {start}{seq_lrepr}{end}"
82+
83+
return f"{start}{seq_lrepr}{end}"
84+
85+
@staticmethod
86+
def _process_kwargs(**kwargs):
87+
"""Process keyword arguments, decreasing the print-level. Should be called
88+
after examining the print level for the current level."""
89+
return pmap(initial=kwargs).transform(["print_level"], _dec_print_level)
90+
91+
92+
def lrepr( # pylint: disable=too-many-arguments
93+
o: Any,
94+
human_readable: bool = False,
95+
print_dup: bool = PRINT_DUP,
96+
print_length: PrintCountSetting = PRINT_LENGTH,
97+
print_level: PrintCountSetting = PRINT_LEVEL,
98+
print_meta: bool = PRINT_META,
99+
print_readably: bool = PRINT_READABLY,
100+
) -> str:
101+
"""Return a string representation of a Lisp object.
102+
103+
Permissible keyword arguments are:
104+
- human_readable: if logical True, print strings without quotations or
105+
escape sequences (default: false)
106+
- print_dup: if logical true, print objects in a way that preserves their
107+
types (default: false)
108+
- print_length: the number of items in a collection which will be printed,
109+
or no limit if bound to a logical falsey value (default: 50)
110+
- print_level: the depth of the object graph to print, starting with 0, or
111+
no limit if bound to a logical falsey value (default: nil)
112+
- print_meta: if logical true, print objects meta in a way that can be
113+
read back by the reader (default: false)
114+
- print_readably: if logical false, print strings and characters with
115+
non-alphanumeric characters converted to escape sequences
116+
(default: true)
117+
118+
Note that this function is not capable of capturing the values bound at
119+
runtime to the basilisp.core dynamic variables which correspond to each
120+
of the keyword arguments to this function. To use a version of lrepr
121+
which does capture those values, call basilisp.lang.runtime.lrepr directly."""
122+
kwargs = pmap(
123+
{
124+
"human_readable": human_readable,
125+
"print_dup": print_dup,
126+
"print_length": print_length,
127+
"print_level": print_level,
128+
"print_meta": print_meta,
129+
"print_readably": print_readably,
130+
}
131+
)
132+
if isinstance(o, LispObject):
133+
return o._lrepr(**kwargs)
134+
elif o is True:
135+
return "true"
136+
elif o is False:
137+
return "false"
138+
elif o is None:
139+
return "nil"
140+
elif isinstance(o, str):
141+
if human_readable:
142+
return o
143+
if print_readably is None or print_readably is False:
144+
return f'"{o}"'
145+
return f'"{o.encode("unicode_escape").decode("utf-8")}"'
146+
elif isinstance(o, complex):
147+
return repr(o).upper()
148+
elif isinstance(o, datetime.datetime):
149+
inst_str = o.isoformat()
150+
return f'#inst "{inst_str}"'
151+
elif isinstance(o, Decimal):
152+
if print_dup:
153+
return f"{str(o)}M"
154+
return str(o)
155+
elif isinstance(o, Fraction):
156+
return f"{o.numerator}/{o.denominator}"
157+
elif isinstance(o, Pattern):
158+
return f'#"{o.pattern}"'
159+
elif isinstance(o, uuid.UUID):
160+
uuid_str = str(o)
161+
return f'#uuid "{uuid_str}"'
162+
else:
163+
return repr(o)

0 commit comments

Comments
 (0)