• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

basilisp-lang / basilisp / 12298226251

12 Dec 2024 02:16PM CUT coverage: 98.738%. Remained the same
12298226251

Pull #1175

github

web-flow
Merge b0568f916 into 2dffcc004
Pull Request #1175: Fix `ns` macro metadata regression

1030 of 1037 branches covered (99.32%)

Branch coverage included in aggregate %.

8825 of 8944 relevant lines covered (98.67%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.89
/src/basilisp/lang/seq.py
1
import functools
1✔
2
import threading
1✔
3
from collections.abc import Iterable
1✔
4
from typing import Callable, Optional, TypeVar, overload
1✔
5

6
from basilisp.lang.interfaces import (
1✔
7
    IPersistentMap,
8
    ISeq,
9
    ISeqable,
10
    ISequential,
11
    IWithMeta,
12
)
13
from basilisp.util import Maybe
1✔
14

15
T = TypeVar("T")
1✔
16

17

18
class _EmptySequence(IWithMeta, ISequential, ISeq[T]):
1✔
19
    __slots__ = ("_meta",)
1✔
20

21
    def __init__(self, meta: Optional[IPersistentMap] = None):
1✔
22
        self._meta = meta
1✔
23

24
    def __repr__(self):
25
        return "()"
26

27
    def __bool__(self):
1✔
28
        return True
1✔
29

30
    def seq(self) -> Optional[ISeq[T]]:
1✔
31
        return None
×
32

33
    @property
1✔
34
    def meta(self) -> Optional[IPersistentMap]:
1✔
35
        return self._meta
×
36

37
    def with_meta(self, meta: Optional[IPersistentMap]) -> "_EmptySequence[T]":
1✔
38
        return _EmptySequence(meta=meta)
×
39

40
    @property
1✔
41
    def is_empty(self) -> bool:
1✔
42
        return True
1✔
43

44
    @property
1✔
45
    def first(self) -> Optional[T]:
1✔
46
        return None
1✔
47

48
    @property
1✔
49
    def rest(self) -> ISeq[T]:
1✔
50
        return self
×
51

52
    def cons(self, *elems: T) -> ISeq[T]:  # type: ignore[override]
1✔
53
        l: ISeq = self
1✔
54
        for elem in elems:
1✔
55
            l = Cons(elem, l)
1✔
56
        return l
1✔
57

58
    def empty(self):
1✔
59
        return EMPTY
×
60

61

62
EMPTY: ISeq = _EmptySequence()
1✔
63

64

65
class Cons(ISeq[T], ISequential, IWithMeta):
1✔
66
    __slots__ = ("_first", "_rest", "_meta")
1✔
67

68
    def __init__(
1✔
69
        self,
70
        first: T,
71
        seq: Optional[ISeq[T]] = None,
72
        meta: Optional[IPersistentMap] = None,
73
    ) -> None:
74
        self._first = first
1✔
75
        self._rest = Maybe(seq).or_else_get(EMPTY)
1✔
76
        self._meta = meta
1✔
77

78
    @property
1✔
79
    def is_empty(self) -> bool:
1✔
80
        return False
1✔
81

82
    @property
1✔
83
    def first(self) -> Optional[T]:
1✔
84
        return self._first
1✔
85

86
    @property
1✔
87
    def rest(self) -> ISeq[T]:
1✔
88
        return self._rest
1✔
89

90
    def cons(self, *elems: T) -> "Cons[T]":
1✔
91
        l = self
1✔
92
        for elem in elems:
1✔
93
            l = Cons(elem, l)
1✔
94
        return l
1✔
95

96
    def empty(self):
1✔
97
        return EMPTY
1✔
98

99
    @property
1✔
100
    def meta(self) -> Optional[IPersistentMap]:
1✔
101
        return self._meta
1✔
102

103
    def with_meta(self, meta: Optional[IPersistentMap]) -> "Cons[T]":
1✔
104
        return Cons(self._first, seq=self._rest, meta=meta)
1✔
105

106

107
LazySeqGenerator = Callable[[], Optional[ISeq[T]]]
1✔
108

109

110
class LazySeq(IWithMeta, ISequential, ISeq[T]):
1✔
111
    """LazySeqs are wrappers for delaying sequence computation. Create a LazySeq
112
    with a function that can either return None or a Seq. If a Seq is returned,
113
    the LazySeq is a proxy to that Seq.
114

115
    Callers should never provide the `seq` argument -- this is provided only to
116
    support `with_meta` returning a new LazySeq instance."""
117

118
    __slots__ = ("_gen", "_obj", "_seq", "_lock", "_meta")
1✔
119

120
    def __init__(
1✔
121
        self,
122
        gen: Optional[LazySeqGenerator],
123
        seq: Optional[ISeq[T]] = None,
124
        *,
125
        meta: Optional[IPersistentMap] = None,
126
    ) -> None:
127
        self._gen: Optional[LazySeqGenerator] = gen
1✔
128
        self._obj: Optional[ISeq[T]] = None
1✔
129
        self._seq: Optional[ISeq[T]] = seq
1✔
130
        self._lock = threading.RLock()
1✔
131
        self._meta = meta
1✔
132

133
    @property
1✔
134
    def meta(self) -> Optional[IPersistentMap]:
1✔
135
        return self._meta
1✔
136

137
    def with_meta(self, meta: Optional[IPersistentMap]) -> "LazySeq[T]":
1✔
138
        return LazySeq(None, seq=self.seq(), meta=meta)
1✔
139

140
    # LazySeqs have a fairly complex inner state, in spite of the simple interface.
141
    # Calls from Basilisp code should be providing the only generator seed function.
142
    # Calls to `(seq ...)` cause the LazySeq to cache the generator function locally
143
    # (as explained in _compute_seq), clear the _gen attribute, and cache the results
144
    # of that generator function call as _obj. _obj may be None, some other ISeq, or
145
    # perhaps another LazySeq. Finally, the LazySeq attempts to consume all returned
146
    # LazySeq objects before calling `(seq ...)` on the result, which is cached in the
147
    # _seq attribute.
148

149
    def _compute_seq(self) -> Optional[ISeq[T]]:
1✔
150
        if self._gen is not None:
1✔
151
            # This local caching of the generator function and clearing of self._gen
152
            # is absolutely critical for supporting co-recursive lazy sequences.
153
            #
154
            # The original example that prompted this change is below:
155
            #
156
            #   (def primes (remove
157
            #                (fn [x] (some #(zero? (mod x %)) primes))
158
            #                (iterate inc 2)))
159
            #
160
            #   (take 5 primes)  ;; => stack overflow
161
            #
162
            # If we don't clear self._gen, each successive call to (some ... primes)
163
            # will end up forcing the primes LazySeq object to call self._gen, rather
164
            # than caching the results, allowing examination of the partial seq
165
            # computed up to that point.
166
            gen = self._gen
1✔
167
            self._gen = None
1✔
168
            self._obj = gen()
1✔
169
        return self._obj if self._obj is not None else self._seq
1✔
170

171
    def seq(self) -> Optional[ISeq[T]]:
1✔
172
        with self._lock:
1✔
173
            self._compute_seq()
1✔
174
            if self._obj is not None:
1✔
175
                o = self._obj
1✔
176
                self._obj = None
1✔
177
                # Consume any additional lazy sequences returned immediately, so we
178
                # have a "real" concrete sequence to proxy to.
179
                #
180
                # The common idiom with LazySeqs is to return
181
                # (cons value (lazy-seq ...)) from the generator function, so this will
182
                # only result in evaluating away instances where _another_ LazySeq is
183
                # returned rather than a cons cell with a concrete first value. This
184
                # loop will not consume the LazySeq in the rest position of the cons.
185
                while isinstance(o, LazySeq):
1✔
186
                    o = o._compute_seq()  # type: ignore
1✔
187
                self._seq = to_seq(o)
1✔
188
            return self._seq
1✔
189

190
    @property
1✔
191
    def is_empty(self) -> bool:
1✔
192
        return self.seq() is None
1✔
193

194
    @property
1✔
195
    def first(self) -> Optional[T]:
1✔
196
        if self.is_empty:
1✔
197
            return None
1✔
198
        return self.seq().first  # type: ignore[union-attr]
1✔
199

200
    @property
1✔
201
    def rest(self) -> "ISeq[T]":
1✔
202
        if self.is_empty:
1✔
203
            return EMPTY
1✔
204
        return self.seq().rest  # type: ignore[union-attr]
1✔
205

206
    def cons(self, *elems: T) -> ISeq[T]:  # type: ignore[override]
1✔
207
        l: ISeq = self
1✔
208
        for elem in elems:
1✔
209
            l = Cons(elem, l)
1✔
210
        return l
1✔
211

212
    @property
1✔
213
    def is_realized(self):
1✔
214
        with self._lock:
1✔
215
            return self._gen is None
1✔
216

217
    def empty(self):
1✔
218
        return EMPTY
1✔
219

220

221
def sequence(s: Iterable[T]) -> ISeq[T]:
1✔
222
    """Create a Sequence from Iterable s."""
223
    i = iter(s)
1✔
224

225
    def _next_elem() -> ISeq[T]:
1✔
226
        try:
1✔
227
            e = next(i)
1✔
228
        except StopIteration:
1✔
229
            return EMPTY
1✔
230
        else:
231
            return Cons(e, LazySeq(_next_elem))
1✔
232

233
    return LazySeq(_next_elem)
1✔
234

235

236
@overload
1✔
237
def _seq_or_nil(s: None) -> None: ...
1✔
238

239

240
@overload
1✔
241
def _seq_or_nil(s: ISeq) -> Optional[ISeq]: ...
1✔
242

243

244
def _seq_or_nil(s):
1✔
245
    """Return None if a ISeq is empty, the ISeq otherwise."""
246
    if s is None or s.is_empty:
1✔
247
        return None
1✔
248
    return s
1✔
249

250

251
@functools.singledispatch
1✔
252
def to_seq(o) -> Optional[ISeq]:
1✔
253
    """Coerce the argument o to a ISeq. If o is None, return None."""
254
    return _seq_or_nil(sequence(o))
1✔
255

256

257
@to_seq.register(type(None))
1✔
258
def _to_seq_none(_) -> None:
1✔
259
    return None
1✔
260

261

262
@to_seq.register(ISeq)
1✔
263
def _to_seq_iseq(o: ISeq) -> Optional[ISeq]:
1✔
264
    return _seq_or_nil(o)
1✔
265

266

267
@to_seq.register(LazySeq)
1✔
268
def _to_seq_lazyseq(o: LazySeq) -> Optional[ISeq]:
1✔
269
    # Force evaluation of the LazySeq by calling o.seq() directly.
270
    return o.seq()
1✔
271

272

273
@to_seq.register(ISeqable)
1✔
274
def _to_seq_iseqable(o: ISeqable) -> Optional[ISeq]:
1✔
275
    return _seq_or_nil(o.seq())
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc