Skip to content

Commit 5e77692

Browse files
authored
Fix thread-safety for sequences wrapping Python Iterators (#937)
Fixes #936 Another fix related to #934
1 parent e719617 commit 5e77692

File tree

4 files changed

+35
-71
lines changed

4 files changed

+35
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
* Fix a bug where `basilisp.core/boolean` was returning the boolean coercions like Python rather than like Basilisp (#928)
2424
* Fix a bug where Basilisp vectors were not callable (#932)
2525
* Fix a bug where `basilisp.lang.seq.LazySeq` instances were not thread-safe (#934)
26+
* Fix a bug where Seqs wrapping Python Iterable instances were not thread-safe (#936)
2627

2728
### Other
2829
* Add several sections to Concepts documentation module (#666)

src/basilisp/lang/seq.py

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22
import threading
3-
from typing import Callable, Iterable, Iterator, Optional, TypeVar, overload
3+
from typing import Callable, Iterable, Optional, TypeVar, overload
44

55
from basilisp.lang.interfaces import (
66
IPersistentMap,
@@ -60,7 +60,7 @@ class Cons(ISeq[T], ISequential, IWithMeta):
6060

6161
def __init__(
6262
self,
63-
first,
63+
first: T,
6464
seq: Optional[ISeq[T]] = None,
6565
meta: Optional[IPersistentMap] = None,
6666
) -> None:
@@ -91,57 +91,6 @@ def with_meta(self, meta: Optional[IPersistentMap]) -> "Cons[T]":
9191
return Cons(self._first, seq=self._rest, meta=meta)
9292

9393

94-
class _Sequence(IWithMeta, ISequential, ISeq[T]):
95-
"""Sequences are a thin wrapper over Python Iterable values so they can
96-
satisfy the Basilisp `ISeq` interface.
97-
98-
Sequences are singly linked lists which lazily traverse the input Iterable.
99-
100-
Do not directly instantiate a Sequence. Instead use the `sequence` function
101-
below."""
102-
103-
__slots__ = ("_first", "_seq", "_rest", "_meta")
104-
105-
def __init__(
106-
self, s: Iterator[T], first: T, *, meta: Optional[IPersistentMap] = None
107-
) -> None:
108-
self._seq = s
109-
self._first = first
110-
self._rest: Optional[ISeq] = None
111-
self._meta = meta
112-
113-
@property
114-
def meta(self) -> Optional[IPersistentMap]:
115-
return self._meta
116-
117-
def with_meta(self, meta: Optional[IPersistentMap]) -> "_Sequence[T]":
118-
return _Sequence(self._seq, self._first, meta=meta)
119-
120-
@property
121-
def is_empty(self) -> bool:
122-
return False
123-
124-
@property
125-
def first(self) -> Optional[T]:
126-
return self._first
127-
128-
@property
129-
def rest(self) -> "ISeq[T]":
130-
if self._rest:
131-
return self._rest
132-
133-
try:
134-
n = next(self._seq)
135-
self._rest = _Sequence(self._seq, n)
136-
except StopIteration:
137-
self._rest = EMPTY
138-
139-
return self._rest
140-
141-
def cons(self, elem):
142-
return Cons(elem, self)
143-
144-
14594
LazySeqGenerator = Callable[[], Optional[ISeq[T]]]
14695

14796

@@ -150,21 +99,20 @@ class LazySeq(IWithMeta, ISequential, ISeq[T]):
15099
with a function that can either return None or a Seq. If a Seq is returned,
151100
the LazySeq is a proxy to that Seq.
152101
153-
Callers should never provide the `obj` or `seq` arguments -- these are provided
154-
only to support `with_meta` returning a new LazySeq instance."""
102+
Callers should never provide the `seq` argument -- this is provided only to
103+
support `with_meta` returning a new LazySeq instance."""
155104

156105
__slots__ = ("_gen", "_obj", "_seq", "_lock", "_meta")
157106

158107
def __init__(
159108
self,
160109
gen: Optional[LazySeqGenerator],
161-
obj: Optional[ISeq[T]] = None,
162110
seq: Optional[ISeq[T]] = None,
163111
*,
164112
meta: Optional[IPersistentMap] = None,
165113
) -> None:
166114
self._gen: Optional[LazySeqGenerator] = gen
167-
self._obj: Optional[ISeq[T]] = obj
115+
self._obj: Optional[ISeq[T]] = None
168116
self._seq: Optional[ISeq[T]] = seq
169117
self._lock = threading.RLock()
170118
self._meta = meta
@@ -174,7 +122,7 @@ def meta(self) -> Optional[IPersistentMap]:
174122
return self._meta
175123

176124
def with_meta(self, meta: Optional[IPersistentMap]) -> "LazySeq[T]":
177-
return LazySeq(self._gen, obj=self._obj, seq=self._seq, meta=meta)
125+
return LazySeq(None, seq=self.seq(), meta=meta)
178126

179127
# LazySeqs have a fairly complex inner state, in spite of the simple interface.
180128
# Calls from Basilisp code should be providing the only generator seed function.
@@ -255,11 +203,17 @@ def is_realized(self):
255203

256204
def sequence(s: Iterable[T]) -> ISeq[T]:
257205
"""Create a Sequence from Iterable s."""
258-
try:
259-
i = iter(s)
260-
return _Sequence(i, next(i))
261-
except StopIteration:
262-
return EMPTY
206+
i = iter(s)
207+
208+
def _next_elem() -> ISeq[T]:
209+
try:
210+
e = next(i)
211+
except StopIteration:
212+
return EMPTY
213+
else:
214+
return Cons(e, LazySeq(_next_elem))
215+
216+
return LazySeq(_next_elem)
263217

264218

265219
@overload

tests/basilisp/runtime_test.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,12 @@ def test_rest():
134134
assert lseq.EMPTY is runtime.rest(llist.l())
135135
assert lseq.EMPTY is runtime.rest(llist.l(1))
136136
assert llist.l(2, 3) == runtime.rest(llist.l(1, 2, 3))
137-
assert lseq.EMPTY is runtime.rest(vec.v(1).seq())
138-
assert lseq.EMPTY is runtime.rest(vec.v(1))
137+
v1 = runtime.rest(vec.v(1).seq())
138+
assert lseq.EMPTY == v1
139+
assert v1.is_empty
140+
v2 = runtime.rest(vec.v(1))
141+
assert lseq.EMPTY == v2
142+
assert v2.is_empty
139143
assert llist.l(2, 3) == runtime.rest(vec.v(1, 2, 3))
140144

141145

@@ -236,7 +240,8 @@ def test_to_seq():
236240

237241
def test_concat():
238242
s1 = runtime.concat()
239-
assert lseq.EMPTY is s1
243+
assert lseq.EMPTY == s1
244+
assert s1.is_empty
240245

241246
s1 = runtime.concat(llist.PersistentList.empty(), llist.PersistentList.empty())
242247
assert lseq.EMPTY == s1

tests/basilisp/seq_test.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
def test_to_sequence():
9-
assert lseq.EMPTY is lseq.sequence([])
9+
assert lseq.EMPTY == lseq.sequence([])
1010
assert lseq.sequence([]).is_empty
1111
assert llist.l(None) == lseq.sequence([None])
1212
assert not lseq.sequence([None]).is_empty
@@ -34,7 +34,8 @@ def test_lazy_sequence():
3434
s = lseq.LazySeq(lambda: lseq.sequence([1]))
3535
assert not s.is_empty
3636
assert 1 == s.first
37-
assert lseq.EMPTY is s.rest
37+
assert lseq.EMPTY == s.rest
38+
assert s.rest.is_empty
3839
assert s.is_realized
3940
assert not s.is_empty, "LazySeq has been realized and is not empty"
4041

@@ -64,7 +65,8 @@ def inner_inner_seq():
6465
t = r.rest
6566
assert not t.is_empty
6667
assert 3 == t.first
67-
assert lseq.EMPTY is t.rest
68+
assert lseq.EMPTY == t.rest
69+
assert t.rest.is_empty
6870
assert t.is_realized
6971
assert not t.is_empty, "LazySeq has been realized and is not empty"
7072

@@ -77,15 +79,17 @@ def test_empty_sequence():
7779
assert None is empty.first
7880
assert empty.rest == empty
7981
assert llist.l(1) == empty.cons(1)
80-
assert lseq.EMPTY is empty
82+
assert lseq.EMPTY == empty
83+
assert empty.is_empty
8184
assert True is bool(lseq.sequence([]))
8285

8386

8487
def test_sequence():
8588
s = lseq.sequence([1])
8689
assert not s.is_empty
8790
assert 1 == s.first
88-
assert lseq.EMPTY is s.rest
91+
assert lseq.EMPTY == s.rest
92+
assert s.rest.is_empty
8993
assert llist.l(2, 1) == s.cons(2)
9094
assert [1, 2, 3] == [e for e in lseq.sequence([1, 2, 3])]
9195
assert llist.l(1, 2, 3) == lseq.sequence([1, 2, 3])

0 commit comments

Comments
 (0)