Skip to content

Commit 29da93b

Browse files
authored
Add support for namespaced map syntax (#577)
* Add support for namespaced map syntax * Shut up, Mypy!
1 parent 66c0009 commit 29da93b

File tree

3 files changed

+127
-5
lines changed

3 files changed

+127
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
* Added support for auto-resolving namespaces for keyword from the current namespace using the `::kw` syntax (#576)
10+
* Added support for namespaced map syntax (#577)
1011

1112
## [v0.1.dev14] - 2020-06-18
1213
### Added

src/basilisp/lang/reader.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Collection,
1515
Deque,
1616
Dict,
17+
Hashable,
1718
Iterable,
1819
List,
1920
MutableMapping,
@@ -470,9 +471,9 @@ def _with_loc(f: W) -> W:
470471
information along with relevant forms."""
471472

472473
@functools.wraps(f)
473-
def with_lineno_and_col(ctx):
474+
def with_lineno_and_col(ctx, **kwargs):
474475
line, col = ctx.reader.line, ctx.reader.col
475-
v = f(ctx)
476+
v = f(ctx, **kwargs) # type: ignore[call-arg]
476477
if isinstance(v, IWithMeta):
477478
new_meta = lmap.map({READER_LINE_KW: line, READER_COL_KW: col})
478479
old_meta = v.meta
@@ -643,15 +644,42 @@ def __read_map_elems(ctx: ReaderContext) -> Iterable[RawReaderForm]:
643644
yield v
644645

645646

647+
def _map_key_processor(namespace: Optional[str],) -> Callable[[Hashable], Hashable]:
648+
"""Return a map key processor.
649+
650+
If no `namespace` is provided, return an identity function. If a `namespace`
651+
is given, return a function that can apply namespaces to un-namespaced
652+
keyword and symbol values."""
653+
if namespace is None:
654+
return lambda v: v
655+
656+
def process_key(k: Any) -> Any:
657+
if isinstance(k, keyword.Keyword):
658+
if k.ns is None:
659+
return keyword.keyword(k.name, ns=namespace)
660+
if k.ns == "_":
661+
return keyword.keyword(k.name)
662+
if isinstance(k, symbol.Symbol):
663+
if k.ns is None:
664+
return symbol.symbol(k.name, ns=namespace)
665+
if k.ns == "_":
666+
return symbol.symbol(k.name)
667+
return k
668+
669+
return process_key
670+
671+
646672
@_with_loc
647-
def _read_map(ctx: ReaderContext) -> lmap.Map:
673+
def _read_map(ctx: ReaderContext, namespace: Optional[str] = None) -> lmap.Map:
648674
"""Return a map from the input stream."""
649675
reader = ctx.reader
650676
start = reader.advance()
651677
assert start == "{"
652678
d: MutableMapping[Any, Any] = {}
679+
process_key = _map_key_processor(namespace)
653680
try:
654681
for k, v in partition(list(__read_map_elems(ctx)), 2):
682+
k = process_key(k)
655683
if k in d:
656684
raise ctx.syntax_error(f"Duplicate key '{k}' in map literal")
657685
d[k] = v
@@ -661,6 +689,30 @@ def _read_map(ctx: ReaderContext) -> lmap.Map:
661689
return lmap.map(d)
662690

663691

692+
def _read_namespaced_map(ctx: ReaderContext) -> lmap.Map:
693+
"""Read a namespaced map from the input stream."""
694+
start = ctx.reader.peek()
695+
assert start == ":"
696+
ctx.reader.advance()
697+
if ctx.reader.peek() == ":":
698+
ctx.reader.advance()
699+
current_ns = get_current_ns()
700+
map_ns = current_ns.name
701+
else:
702+
kw_ns, map_ns = _read_namespaced(ctx)
703+
if kw_ns is not None:
704+
raise ctx.syntax_error(
705+
f"Invalid map namespace '{kw_ns}/{map_ns}'; namespaces for maps must "
706+
"be specified as keywords without namespaces"
707+
)
708+
709+
token = ctx.reader.peek()
710+
while whitespace_chars.match(token):
711+
token = ctx.reader.next_token()
712+
713+
return _read_map(ctx, namespace=map_ns)
714+
715+
664716
# Due to some ambiguities that arise in parsing symbols, numbers, and the
665717
# special keywords `true`, `false`, and `nil`, we have to have a looser
666718
# type defined for the return from these reader functions.
@@ -835,8 +887,7 @@ def _read_kw(ctx: ReaderContext) -> keyword.Keyword:
835887
"""Return a keyword from the input stream."""
836888
start = ctx.reader.advance()
837889
assert start == ":"
838-
token = ctx.reader.peek()
839-
if token == ":":
890+
if ctx.reader.peek() == ":":
840891
ctx.reader.advance()
841892
should_autoresolve = True
842893
else:
@@ -1290,6 +1341,8 @@ def _read_reader_macro(ctx: ReaderContext) -> LispReaderForm:
12901341
return _read_set(ctx)
12911342
elif token == "(":
12921343
return _read_function(ctx)
1344+
elif token == ":":
1345+
return _read_namespaced_map(ctx)
12931346
elif token == "'":
12941347
ctx.reader.advance()
12951348
s = _read_sym(ctx)

tests/basilisp/reader_test.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,74 @@ def test_map():
529529
read_str_first("{:a 1 :b}")
530530

531531

532+
def test_namespaced_map(test_ns: str, ns: runtime.Namespace):
533+
assert lmap.map(
534+
{
535+
kw.keyword("name", ns="member"): "Chris",
536+
kw.keyword("gender", ns="person"): "M",
537+
kw.keyword("id"): 15,
538+
}
539+
) == read_str_first('#:person {:member/name "Chris" :gender "M" :_/id 15}')
540+
assert lmap.map(
541+
{
542+
sym.symbol("name", ns="member"): "Chris",
543+
sym.symbol("gender", ns="person"): "M",
544+
sym.symbol("id"): 15,
545+
}
546+
) == read_str_first('#:person{member/name "Chris" gender "M" _/id 15}')
547+
548+
with pytest.raises(reader.SyntaxError):
549+
read_str_first('#:person/thing {member/name "Chris" gender "M" _/id 15}')
550+
551+
assert lmap.map(
552+
{
553+
kw.keyword("name", ns="member"): "Chris",
554+
kw.keyword("gender", ns=test_ns): "M",
555+
kw.keyword("id"): 15,
556+
}
557+
) == read_str_first('#:: {:member/name "Chris" :gender "M" :_/id 15}')
558+
assert lmap.map(
559+
{
560+
sym.symbol("name", ns="member"): "Chris",
561+
sym.symbol("gender", ns=test_ns): "M",
562+
sym.symbol("id"): 15,
563+
}
564+
) == read_str_first('#::{member/name "Chris" gender "M" _/id 15}')
565+
566+
assert lmap.map(
567+
{
568+
kw.keyword("name", ns="member"): "Chris",
569+
kw.keyword("gender", ns="person"): "M",
570+
kw.keyword("id"): 15,
571+
kw.keyword("address", ns="person"): lmap.map(
572+
{kw.keyword("city"): "New York"}
573+
),
574+
}
575+
) == read_str_first(
576+
"""
577+
#:person {:member/name "Chris"
578+
:gender "M"
579+
:_/id 15
580+
:address {:city "New York"}}"""
581+
)
582+
assert lmap.map(
583+
{
584+
kw.keyword("name", ns="member"): "Chris",
585+
kw.keyword("gender", ns="person"): "M",
586+
kw.keyword("id"): 15,
587+
kw.keyword("address", ns="person"): lmap.map(
588+
{kw.keyword("city", ns="address"): "New York"}
589+
),
590+
}
591+
) == read_str_first(
592+
"""
593+
#:person {:member/name "Chris"
594+
:gender "M"
595+
:_/id 15
596+
:address #:address{:city "New York"}}"""
597+
)
598+
599+
532600
def test_quoted():
533601
assert read_str_first("'a") == llist.l(sym.symbol("quote"), sym.symbol("a"))
534602
assert read_str_first("'some.ns/sym") == llist.l(

0 commit comments

Comments
 (0)