Skip to content

Commit 7be69bb

Browse files
authored
*print-namespace-maps* support (#883)
Hi, could you please review draft support for the *print-namespace-maps* dynamic variable. It resolves #882. I implemented the support at the lowest `obj.py` level, though this required to bring in the `keyword.py` and `symbol.py` modules resulting to cyclic dependencies. Not sure how else to implement it, so any other suggestions are most welcome. I've also blindly followed the example of *print-dup* on how I ended setup the variable in the cli/runtime/object .py files. In addition, I've used a set to compare the printed output of the map result with multiple k/v, exhaustively covering the possible orderings, is there perhaps a better simpler way to do this? This currently is prune to user errors. Thanks --------- Co-authored-by: ikappaki <[email protected]>
1 parent 37de5a6 commit 7be69bb

File tree

6 files changed

+179
-60
lines changed

6 files changed

+179
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* Added `merge-with` core function (#860)
1818
* Added `fnext` core function (#879)
1919
* Added `INamed` interface for Keywords and Symbols (#884)
20+
* Added `*print-namespace-maps*` dynamic var support (#882)
2021

2122
### Changed
2223
* Cause exceptions arising from compilation issues during macroexpansion will no longer be nested for each level of macroexpansion (#852)

src/basilisp/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ def repl(
422422
runtime.PRINT_READABLY_VAR_NAME,
423423
runtime.PRINT_LEVEL_VAR_NAME,
424424
runtime.PRINT_META_VAR_NAME,
425+
runtime.PRINT_NAMESPACE_MAPS_VAR_NAME,
425426
],
426427
)
427428
}

src/basilisp/lang/map.py

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
from builtins import map as pymap
2-
from typing import Callable, Iterable, Mapping, Optional, Tuple, TypeVar, Union, cast
2+
from itertools import islice
3+
from typing import (
4+
Any,
5+
Callable,
6+
Iterable,
7+
Mapping,
8+
Optional,
9+
Tuple,
10+
TypeVar,
11+
Union,
12+
cast,
13+
)
314

415
from immutables import Map as _Map
516
from immutables import MapMutation
@@ -9,14 +20,21 @@
920
IEvolveableCollection,
1021
ILispObject,
1122
IMapEntry,
23+
INamed,
1224
IPersistentMap,
1325
IPersistentVector,
1426
ISeq,
1527
ITransientMap,
1628
IWithMeta,
1729
)
18-
from basilisp.lang.obj import PrintSettings
19-
from basilisp.lang.obj import map_lrepr as _map_lrepr
30+
from basilisp.lang.obj import (
31+
PRINT_SEPARATOR,
32+
SURPASSED_PRINT_LENGTH,
33+
SURPASSED_PRINT_LEVEL,
34+
PrintSettings,
35+
lrepr,
36+
process_lrepr_kwargs,
37+
)
2038
from basilisp.lang.seq import sequence
2139
from basilisp.lang.vector import MapEntry
2240
from basilisp.util import partition
@@ -107,6 +125,91 @@ def to_persistent(self) -> "PersistentMap[K, V]":
107125
return PersistentMap(self._inner.finish())
108126

109127

128+
def map_lrepr( # pylint: disable=too-many-locals
129+
entries: Callable[[], Iterable[Tuple[Any, Any]]],
130+
start: str,
131+
end: str,
132+
meta: Optional[IPersistentMap] = None,
133+
**kwargs: Unpack[PrintSettings],
134+
) -> str:
135+
"""Produce a Lisp representation of an associative collection, bookended
136+
with the start and end string supplied. The entries argument must be a
137+
callable which will produce tuples of key-value pairs.
138+
139+
If the keyword argument print_namespace_maps is True and all keys
140+
share the same namespace, then print the namespace of the keys at
141+
the beginning of the map instead of beside the keys.
142+
143+
The keyword arguments will be passed along to lrepr for the sequence
144+
elements.
145+
146+
"""
147+
print_level = kwargs["print_level"]
148+
if isinstance(print_level, int) and print_level < 1:
149+
return SURPASSED_PRINT_LEVEL
150+
151+
kwargs = process_lrepr_kwargs(**kwargs)
152+
153+
def check_same_ns():
154+
"""Check whether all keys in entries belong to the same
155+
namespace. If they do, return the namespace name; otherwise,
156+
return None.
157+
"""
158+
nses = set()
159+
for k, _ in entries():
160+
if isinstance(k, INamed):
161+
nses.add(k.ns)
162+
else:
163+
nses.add(None)
164+
if len(nses) > 1:
165+
break
166+
return next(iter(nses)) if len(nses) == 1 else None
167+
168+
ns_name_shared = check_same_ns() if kwargs["print_namespace_maps"] else None
169+
170+
entries_updated = entries
171+
if ns_name_shared:
172+
173+
def entries_ns_remove():
174+
for k, v in entries():
175+
yield (k.with_name(k.name), v)
176+
177+
entries_updated = entries_ns_remove
178+
179+
kw_items = kwargs.copy()
180+
kw_items["human_readable"] = False
181+
182+
def entry_reprs():
183+
for k, v in entries_updated():
184+
yield f"{lrepr(k, **kw_items)} {lrepr(v, **kw_items)}"
185+
186+
trailer = []
187+
print_dup = kwargs["print_dup"]
188+
print_length = kwargs["print_length"]
189+
if not print_dup and isinstance(print_length, int):
190+
items = list(islice(entry_reprs(), print_length + 1))
191+
if len(items) > print_length:
192+
items.pop()
193+
trailer.append(SURPASSED_PRINT_LENGTH)
194+
else:
195+
items = list(entry_reprs())
196+
197+
seq_lrepr = PRINT_SEPARATOR.join(items + trailer)
198+
199+
ns_prefix = ("#:" + ns_name_shared) if ns_name_shared else ""
200+
if kwargs["print_meta"] and meta:
201+
kwargs_meta = kwargs
202+
kwargs_meta["print_level"] = None
203+
return f"^{lrepr(meta,**kwargs_meta)} {ns_prefix}{start}{seq_lrepr}{end}"
204+
205+
return f"{ns_prefix}{start}{seq_lrepr}{end}"
206+
207+
208+
@lrepr.register(dict)
209+
def _lrepr_py_dict(o: dict, **kwargs: Unpack[PrintSettings]) -> str:
210+
return f"#py {map_lrepr(o.items, '{', '}', **kwargs)}"
211+
212+
110213
class PersistentMap(
111214
IPersistentMap[K, V], IEvolveableCollection[TransientMap], ILispObject, IWithMeta
112215
):
@@ -163,8 +266,12 @@ def __len__(self):
163266
return len(self._inner)
164267

165268
def _lrepr(self, **kwargs: Unpack[PrintSettings]):
166-
return _map_lrepr(
167-
self._inner.items, start="{", end="}", meta=self._meta, **kwargs
269+
return map_lrepr(
270+
self._inner.items,
271+
start="{",
272+
end="}",
273+
meta=self._meta,
274+
**kwargs,
168275
)
169276

170277
@property

src/basilisp/lang/obj.py

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from functools import singledispatch
99
from itertools import islice
1010
from pathlib import Path
11-
from typing import Any, Callable, Iterable, Pattern, Tuple, Union, cast
11+
from typing import Any, Iterable, Pattern, Union, cast
1212

1313
from typing_extensions import TypedDict, Unpack
1414

@@ -21,6 +21,7 @@
2121
PRINT_LENGTH: PrintCountSetting = 50
2222
PRINT_LEVEL: PrintCountSetting = None
2323
PRINT_META = False
24+
PRINT_NAMESPACE_MAPS = False
2425
PRINT_READABLY = True
2526
PRINT_SEPARATOR = " "
2627

@@ -31,6 +32,7 @@ class PrintSettings(TypedDict, total=False):
3132
print_length: PrintCountSetting
3233
print_level: PrintCountSetting
3334
print_meta: bool
35+
print_namespace_maps: bool
3436
print_readably: bool
3537

3638

@@ -41,7 +43,7 @@ def _dec_print_level(lvl: PrintCountSetting) -> PrintCountSetting:
4143
return lvl
4244

4345

44-
def _process_kwargs(**kwargs: Unpack[PrintSettings]) -> PrintSettings:
46+
def process_lrepr_kwargs(**kwargs: Unpack[PrintSettings]) -> PrintSettings:
4547
"""Process keyword arguments, decreasing the print-level. Should be called
4648
after examining the print level for the current level."""
4749
return cast(
@@ -78,52 +80,6 @@ def lrepr(self, **kwargs: Unpack[PrintSettings]) -> str:
7880
return lrepr(self, **kwargs)
7981

8082

81-
def map_lrepr(
82-
entries: Callable[[], Iterable[Tuple[Any, Any]]],
83-
start: str,
84-
end: str,
85-
meta=None,
86-
**kwargs: Unpack[PrintSettings],
87-
) -> str:
88-
"""Produce a Lisp representation of an associative collection, bookended
89-
with the start and end string supplied. The entries argument must be a
90-
callable which will produce tuples of key-value pairs.
91-
92-
The keyword arguments will be passed along to lrepr for the sequence
93-
elements."""
94-
print_level = kwargs["print_level"]
95-
if isinstance(print_level, int) and print_level < 1:
96-
return SURPASSED_PRINT_LEVEL
97-
98-
kwargs = _process_kwargs(**kwargs)
99-
100-
kw_items = kwargs.copy()
101-
kw_items["human_readable"] = False
102-
103-
def entry_reprs():
104-
for k, v in entries():
105-
yield f"{lrepr(k, **kw_items)} {lrepr(v, **kw_items)}"
106-
107-
trailer = []
108-
print_dup = kwargs["print_dup"]
109-
print_length = kwargs["print_length"]
110-
if not print_dup and isinstance(print_length, int):
111-
items = list(islice(entry_reprs(), print_length + 1))
112-
if len(items) > print_length:
113-
items.pop()
114-
trailer.append(SURPASSED_PRINT_LENGTH)
115-
else:
116-
items = list(entry_reprs())
117-
118-
seq_lrepr = PRINT_SEPARATOR.join(items + trailer)
119-
120-
print_meta = kwargs["print_meta"]
121-
if print_meta and meta:
122-
return f"^{lrepr(meta, **kwargs)} {start}{seq_lrepr}{end}"
123-
124-
return f"{start}{seq_lrepr}{end}"
125-
126-
12783
def seq_lrepr(
12884
iterable: Iterable[Any],
12985
start: str,
@@ -138,7 +94,7 @@ def seq_lrepr(
13894
if isinstance(print_level, int) and print_level < 1:
13995
return SURPASSED_PRINT_LEVEL
14096

141-
kwargs = _process_kwargs(**kwargs)
97+
kwargs = process_lrepr_kwargs(**kwargs)
14298

14399
trailer = []
144100
print_dup = kwargs["print_dup"]
@@ -172,6 +128,7 @@ def lrepr( # pylint: disable=too-many-arguments
172128
print_length: PrintCountSetting = PRINT_LENGTH,
173129
print_level: PrintCountSetting = PRINT_LEVEL,
174130
print_meta: bool = PRINT_META,
131+
print_namespace_maps: bool = PRINT_NAMESPACE_MAPS,
175132
print_readably: bool = PRINT_READABLY,
176133
) -> str:
177134
"""Return a string representation of a Lisp object.
@@ -185,6 +142,10 @@ def lrepr( # pylint: disable=too-many-arguments
185142
or no limit if bound to a logical falsey value (default: 50)
186143
- print_level: the depth of the object graph to print, starting with 0, or
187144
no limit if bound to a logical falsey value (default: nil)
145+
- print_namespace_maps: if logical true, and the object is a map consisting
146+
with keys belonging to the same namespace, print the
147+
namespace at the beginning of the map instead of
148+
beside the keys (default: false)
188149
- print_meta: if logical true, print objects meta in a way that can be
189150
read back by the reader (default: false)
190151
- print_readably: if logical false, print strings and characters with
@@ -206,6 +167,7 @@ def _lrepr_lisp_obj( # pylint: disable=too-many-arguments
206167
print_length: PrintCountSetting = PRINT_LENGTH,
207168
print_level: PrintCountSetting = PRINT_LEVEL,
208169
print_meta: bool = PRINT_META,
170+
print_namespace_maps: bool = PRINT_NAMESPACE_MAPS,
209171
print_readably: bool = PRINT_READABLY,
210172
) -> str: # pragma: no cover
211173
return o._lrepr(
@@ -214,6 +176,7 @@ def _lrepr_lisp_obj( # pylint: disable=too-many-arguments
214176
print_length=print_length,
215177
print_level=print_level,
216178
print_meta=print_meta,
179+
print_namespace_maps=print_namespace_maps,
217180
print_readably=print_readably,
218181
)
219182

@@ -250,11 +213,6 @@ def _lrepr_py_list(o: list, **kwargs: Unpack[PrintSettings]) -> str:
250213
return f"#py {seq_lrepr(o, '[', ']', **kwargs)}"
251214

252215

253-
@lrepr.register(dict)
254-
def _lrepr_py_dict(o: dict, **kwargs: Unpack[PrintSettings]) -> str:
255-
return f"#py {map_lrepr(o.items, '{', '}', **kwargs)}"
256-
257-
258216
@lrepr.register(set)
259217
def _lrepr_py_set(o: set, **kwargs: Unpack[PrintSettings]) -> str:
260218
return f"#py {seq_lrepr(o, '#{', '}', **kwargs)}"

src/basilisp/lang/runtime.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
PRINT_LENGTH_VAR_NAME = "*print-length*"
9292
PRINT_LEVEL_VAR_NAME = "*print-level*"
9393
PRINT_META_VAR_NAME = "*print-meta*"
94+
PRINT_NAMESPACE_MAPS_VAR_NAME = "*print-namespace-maps*"
9495
PRINT_READABLY_VAR_NAME = "*print-readably*"
9596
PYTHON_VERSION_VAR_NAME = "*python-version*"
9697
BASILISP_VERSION_VAR_NAME = "*basilisp-version*"
@@ -1636,6 +1637,7 @@ def lrepr(o, human_readable: bool = False) -> str:
16361637
sym.symbol(PRINT_LEVEL_VAR_NAME)
16371638
).value,
16381639
print_meta=core_ns.find(sym.symbol(PRINT_META_VAR_NAME)).value, # type: ignore
1640+
print_namespace_maps=core_ns.find(sym.symbol(PRINT_NAMESPACE_MAPS_VAR_NAME)).value, # type: ignore
16391641
print_readably=core_ns.find( # type: ignore
16401642
sym.symbol(PRINT_READABLY_VAR_NAME)
16411643
).value,
@@ -2191,6 +2193,21 @@ def in_ns(s: sym.Symbol):
21912193
Var.intern(
21922194
CORE_NS_SYM, sym.symbol(PRINT_META_VAR_NAME), lobj.PRINT_META, dynamic=True
21932195
)
2196+
Var.intern(
2197+
CORE_NS_SYM,
2198+
sym.symbol(PRINT_NAMESPACE_MAPS_VAR_NAME),
2199+
lobj.PRINT_NAMESPACE_MAPS,
2200+
dynamic=True,
2201+
meta=lmap.map(
2202+
{
2203+
_DOC_META_KEY: (
2204+
"Indicates to print the namespace of keys in a map belonging to the same"
2205+
" namespace, at the beginning of the map instead of beside the keys."
2206+
" Defaults to false."
2207+
)
2208+
}
2209+
),
2210+
)
21942211
Var.intern(
21952212
CORE_NS_SYM,
21962213
sym.symbol(PRINT_READABLY_VAR_NAME),

tests/basilisp/test_core_fns.lpy

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2156,7 +2156,42 @@
21562156

21572157
(deftest pr-test
21582158
(testing "is dynamic"
2159-
(is (= '(1) (binding [pr (fn [& more] more)] (pr 1))))))
2159+
(is (= '(1) (binding [pr (fn [& more] more)] (pr 1)))))
2160+
2161+
(testing "with *print-namespace-maps*"
2162+
(is (= "#py {:a/x 5}" (binding [*print-namespace-maps* false] (pr-str #py {:a/x 5}))))
2163+
(is (= "#py #:a{:x 5}" (binding [*print-namespace-maps* true] (pr-str #py {:a/x 5}))))
2164+
2165+
(are [res m b] (contains? res (binding [*print-namespace-maps* b] (pr-str m)))
2166+
#{"{:a/x 5}"} {:a/x 5} false
2167+
#{"#:a{:x 5}"} {:a/x 5} true
2168+
#{"{:x 5}"} {:x 5} true
2169+
#{"{1 2}"} {1 2} false
2170+
#{"{1 2}"} {1 2} true
2171+
#{"{:a/y 6 :a/x 5}" "{:a/x 5 :a/y 6}"} {:a/x 5 :a/y 6} false
2172+
#{"#:a{:y 6 :x 5}" "#:a{:x 5 :y 6}"} {:a/x 5 :a/y 6} true
2173+
#{"{6 6 :a/x 5}" "{:a/x 5 6 6}"} {:a/x 5 6 6} false
2174+
#{"{6 6 :a/x 5}" "{:a/x 5 6 6}"} {:a/x 5 6 6} true
2175+
#{"{:a/x 5 :b/y 6}" "{:b/y 6 :a/x 5}"} {:a/x 5 :b/y 6} true
2176+
#{"#:a{y 6 x 5}" "#:a{x 5 y 6}"} {'a/x 5 'a/y 6} true
2177+
#{"#:a{y 6 :x 5}" "#:a{:x 5 y 6}"} {:a/x 5 'a/y 6} true
2178+
#{"{y 6 :a/x 5}" "{:a/x 5 y 6}"} {:a/x 5 'y 6} true)
2179+
2180+
(are [res m pnm pm] (= res (binding [*print-namespace-maps* pnm
2181+
*print-meta* pm]
2182+
(pr-str m)))
2183+
"^{:m 1} {:a/x 5}" ^{:m 1} {:a/x 5} false true
2184+
"^{:m 1} #:a{:x 5}" ^{:m 1} {:a/x 5} true true
2185+
"^#:l{:m 1} #:a{:x 5}" ^{:l/m 1} {:a/x 5} true true)
2186+
2187+
(are [res m pl] (= res (binding [*print-namespace-maps* true
2188+
*print-meta* true
2189+
*print-level* pl]
2190+
(pr-str m)))
2191+
"#" ^{:m 1} {:a/x 5} 0
2192+
"^{:m 1} #:a{:x 5}" ^{:m 1} {:a/x 5} 1
2193+
"^{:m 1} #:a{:x #}" ^{:m 1} {:a/x ^{:x 22} {:b 36}} 1
2194+
"^{:m 1} #:a{:x ^{:x 22} {:b 36}}" ^{:m 1} {:a/x ^{:x 22} {:b 36}} 2)))
21602195

21612196
(defn- bio-write
21622197
"Helper fn to write the ``strings`` to the ByteIO buffer ``bio``

0 commit comments

Comments
 (0)