pyochain 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyochain might be problematic. Click here for more details.

@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import statistics
5
+ from collections.abc import Callable, Iterable
6
+ from typing import TYPE_CHECKING, Literal, NamedTuple
7
+
8
+ import cytoolz as cz
9
+ import more_itertools as mit
10
+
11
+ from .._core import IterWrapper
12
+
13
+ if TYPE_CHECKING:
14
+ from ._main import Iter
15
+
16
+
17
+ class Unzipped[T, V](NamedTuple):
18
+ first: Iter[T]
19
+ second: Iter[V]
20
+
21
+
22
+ class BaseAgg[T](IterWrapper[T]):
23
+ def unzip[U, V](self: IterWrapper[tuple[U, V]]) -> Unzipped[U, V]:
24
+ """
25
+ Converts an iterator of pairs into a pair of iterators.
26
+
27
+ `Iter.unzip()` consumes the iterator of pairs.
28
+
29
+ Returns an Unzipped NamedTuple, containing two iterators:
30
+
31
+ - one from the left elements of the pairs
32
+ - one from the right elements.
33
+ This function is, in some sense, the opposite of zip.
34
+ ```python
35
+ >>> import pyochain as pc
36
+ >>> data = [(1, "a"), (2, "b"), (3, "c")]
37
+ >>> unzipped = pc.Iter.from_(data).unzip()
38
+ >>> unzipped.first.into(list)
39
+ [1, 2, 3]
40
+ >>> unzipped.second.into(list)
41
+ ['a', 'b', 'c']
42
+
43
+ ```
44
+ """
45
+ from ._main import Iter
46
+
47
+ def _unzip(data: Iterable[tuple[U, V]]) -> Unzipped[U, V]:
48
+ d: list[tuple[U, V]] = list(data)
49
+ return Unzipped(Iter(x[0] for x in d), Iter(x[1] for x in d))
50
+
51
+ return self.into(_unzip)
52
+
53
+ def reduce(self, func: Callable[[T, T], T]) -> T:
54
+ """
55
+ Apply a function of two arguments cumulatively to the items of an iterable, from left to right.
56
+
57
+ Args:
58
+ func: Function to apply cumulatively to the items of the iterable.
59
+
60
+ This effectively reduces the iterable to a single value.
61
+
62
+ If initial is present, it is placed before the items of the iterable in the calculation.
63
+
64
+ It then serves as a default when the iterable is empty.
65
+ ```python
66
+ >>> import pyochain as pc
67
+ >>> pc.Iter.from_([1, 2, 3]).reduce(lambda a, b: a + b)
68
+ 6
69
+
70
+ ```
71
+ """
72
+ return self.into(functools.partial(functools.reduce, func))
73
+
74
+ def combination_index(self, r: Iterable[T]) -> int:
75
+ """
76
+ Equivalent to list(combinations(iterable, r)).index(element).
77
+
78
+ Args:
79
+ r: The combination to find the index of.
80
+
81
+ The subsequences of iterable that are of length r can be ordered lexicographically.
82
+
83
+ combination_index computes the index of the first element, without computing the previous combinations.
84
+
85
+ ValueError will be raised if the given element isn't one of the combinations of iterable.
86
+ ```python
87
+ >>> import pyochain as pc
88
+ >>> pc.Iter.from_("abcdefg").combination_index("adf")
89
+ 10
90
+
91
+ ```
92
+ """
93
+ return self.into(functools.partial(mit.combination_index, r))
94
+
95
+ def first(self) -> T:
96
+ """
97
+ Return the first element.
98
+ ```python
99
+ >>> import pyochain as pc
100
+ >>> pc.Iter.from_([9]).first()
101
+ 9
102
+
103
+ ```
104
+ """
105
+ return self.into(cz.itertoolz.first)
106
+
107
+ def second(self) -> T:
108
+ """
109
+ Return the second element.
110
+ ```python
111
+ >>> import pyochain as pc
112
+ >>> pc.Iter.from_([9, 8]).second()
113
+ 8
114
+
115
+ ```
116
+ """
117
+ return self.into(cz.itertoolz.second)
118
+
119
+ def last(self) -> T:
120
+ """
121
+ Return the last element.
122
+ ```python
123
+ >>> import pyochain as pc
124
+ >>> pc.Iter.from_([7, 8, 9]).last()
125
+ 9
126
+
127
+ ```
128
+ """
129
+ return self.into(cz.itertoolz.last)
130
+
131
+ def count(self) -> int:
132
+ """
133
+ Return the length of the sequence.
134
+ Like the builtin len but works on lazy sequences.
135
+ ```python
136
+ >>> import pyochain as pc
137
+ >>> pc.Iter.from_([1, 2]).count()
138
+ 2
139
+
140
+ ```
141
+ """
142
+ return self.into(cz.itertoolz.count)
143
+
144
+ def item(self, index: int) -> T:
145
+ """
146
+ Return item at index.
147
+
148
+ Args:
149
+ index: The index of the item to retrieve.
150
+
151
+ ```python
152
+ >>> import pyochain as pc
153
+ >>> pc.Iter.from_([10, 20]).item(1)
154
+ 20
155
+
156
+ ```
157
+ """
158
+ return self.into(functools.partial(cz.itertoolz.nth, index))
159
+
160
+ def argmax[U](self, key: Callable[[T], U] | None = None) -> int:
161
+ """
162
+ Index of the first occurrence of a maximum value in an iterable.
163
+
164
+ Args:
165
+ key: Optional function to determine the value for comparison.
166
+
167
+ ```python
168
+ >>> import pyochain as pc
169
+ >>> pc.Iter.from_("abcdefghabcd").argmax()
170
+ 7
171
+ >>> pc.Iter.from_([0, 1, 2, 3, 3, 2, 1, 0]).argmax()
172
+ 3
173
+
174
+ ```
175
+ For example, identify the best machine learning model:
176
+ ```python
177
+ >>> models = pc.Iter.from_(["svm", "random forest", "knn", "naïve bayes"])
178
+ >>> accuracy = pc.Seq([68, 61, 84, 72])
179
+ >>> # Most accurate model
180
+ >>> models.item(accuracy.argmax())
181
+ 'knn'
182
+ >>>
183
+ >>> # Best accuracy
184
+ >>> accuracy.into(max)
185
+ 84
186
+
187
+ ```
188
+ """
189
+ return self.into(mit.argmax, key=key)
190
+
191
+ def argmin[U](self, key: Callable[[T], U] | None = None) -> int:
192
+ """
193
+ Index of the first occurrence of a minimum value in an iterable.
194
+
195
+ Args:
196
+ key: Optional function to determine the value for comparison.
197
+
198
+ ```python
199
+ >>> import pyochain as pc
200
+ >>> pc.Iter.from_("efghabcdijkl").argmin()
201
+ 4
202
+ >>> pc.Iter.from_([3, 2, 1, 0, 4, 2, 1, 0]).argmin()
203
+ 3
204
+
205
+ ```
206
+
207
+ For example, look up a label corresponding to the position of a value that minimizes a cost function:
208
+ ```python
209
+ >>> def cost(x):
210
+ ... "Days for a wound to heal given a subject's age."
211
+ ... return x**2 - 20 * x + 150
212
+ >>> labels = pc.Iter.from_(["homer", "marge", "bart", "lisa", "maggie"])
213
+ >>> ages = pc.Seq([35, 30, 10, 9, 1])
214
+ >>> # Fastest healing family member
215
+ >>> labels.item(ages.argmin(key=cost))
216
+ 'bart'
217
+ >>> # Age with fastest healing
218
+ >>> ages.into(min, key=cost)
219
+ 10
220
+
221
+ ```
222
+ """
223
+ return self.into(mit.argmin, key=key)
224
+
225
+ def sum[U: int | float](self: IterWrapper[U]) -> U | Literal[0]:
226
+ """
227
+ Return the sum of the sequence.
228
+ ```python
229
+ >>> import pyochain as pc
230
+ >>> pc.Iter.from_([1, 2, 3]).sum()
231
+ 6
232
+
233
+ ```
234
+ """
235
+ return self.into(sum)
236
+
237
+ def min[U: int | float](self: IterWrapper[U]) -> U:
238
+ """
239
+ Return the minimum of the sequence.
240
+ ```python
241
+ >>> import pyochain as pc
242
+ >>> pc.Iter.from_([3, 1, 2]).min()
243
+ 1
244
+
245
+ ```
246
+ """
247
+ return self.into(min)
248
+
249
+ def max[U: int | float](self: IterWrapper[U]) -> U:
250
+ """
251
+ Return the maximum of the sequence.
252
+ ```python
253
+ >>> import pyochain as pc
254
+ >>> pc.Iter.from_([3, 1, 2]).max()
255
+ 3
256
+
257
+ ```
258
+ """
259
+ return self.into(max)
260
+
261
+ def mean[U: int | float](self: IterWrapper[U]) -> float:
262
+ """
263
+ Return the mean of the sequence.
264
+ ```python
265
+ >>> import pyochain as pc
266
+ >>> pc.Iter.from_([1, 2, 3]).mean()
267
+ 2
268
+
269
+ ```
270
+ """
271
+ return self.into(statistics.mean)
272
+
273
+ def median[U: int | float](self: IterWrapper[U]) -> float:
274
+ """
275
+ Return the median of the sequence.
276
+ ```python
277
+ >>> import pyochain as pc
278
+ >>> pc.Iter.from_([1, 3, 2]).median()
279
+ 2
280
+
281
+ ```
282
+ """
283
+ return self.into(statistics.median)
284
+
285
+ def mode[U: int | float](self: IterWrapper[U]) -> U:
286
+ """
287
+ Return the mode of the sequence.
288
+ ```python
289
+ >>> import pyochain as pc
290
+ >>> pc.Iter.from_([1, 2, 2, 3]).mode()
291
+ 2
292
+
293
+ ```
294
+ """
295
+ return self.into(statistics.mode)
296
+
297
+ def stdev[U: int | float](
298
+ self: IterWrapper[U],
299
+ ) -> float:
300
+ """
301
+ Return the standard deviation of the sequence.
302
+ ```python
303
+ >>> import pyochain as pc
304
+ >>> pc.Iter.from_([1, 2, 3]).stdev()
305
+ 1.0
306
+
307
+ ```
308
+ """
309
+ return self.into(statistics.stdev)
310
+
311
+ def variance[U: int | float](
312
+ self: IterWrapper[U],
313
+ ) -> float:
314
+ """
315
+ Return the variance of the sequence.
316
+ ```python
317
+ >>> import pyochain as pc
318
+ >>> pc.Iter.from_([1, 2, 3, 7, 8]).variance()
319
+ 9.7
320
+
321
+ ```
322
+ """
323
+ return self.into(statistics.variance)
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from typing import overload
5
+
6
+ import cytoolz as cz
7
+ import more_itertools as mit
8
+
9
+ from .._core import IterWrapper
10
+
11
+
12
+ class BaseBool[T](IterWrapper[T]):
13
+ def all(self, predicate: Callable[[T], bool] = lambda x: bool(x)) -> bool:
14
+ """
15
+ Tests if every element of the iterator matches a predicate.
16
+
17
+ `Iter.all()` takes a closure that returns true or false.
18
+
19
+ It applies this closure to each element of the iterator, and if they all return true, then so does `Iter.all()`.
20
+
21
+ If any of them return false, it returns false.
22
+
23
+ An empty iterator returns true.
24
+ Args:
25
+ predicate: Function to evaluate each item. Defaults to checking truthiness.
26
+ Example:
27
+ ```python
28
+ >>> import pyochain as pc
29
+ >>> pc.Iter.from_([1, True]).all()
30
+ True
31
+ >>> pc.Iter.from_([]).all()
32
+ True
33
+ >>> pc.Iter.from_([1, 0]).all()
34
+ False
35
+ >>> def is_even(x: int) -> bool:
36
+ ... return x % 2 == 0
37
+ >>> pc.Iter.from_([2, 4, 6]).all(is_even)
38
+ True
39
+
40
+ ```
41
+ """
42
+
43
+ def _all(data: Iterable[T]) -> bool:
44
+ return all(predicate(x) for x in data)
45
+
46
+ return self.into(_all)
47
+
48
+ def any(self, predicate: Callable[[T], bool] = lambda x: bool(x)) -> bool:
49
+ """
50
+ Tests if any element of the iterator matches a predicate.
51
+
52
+
53
+ `Iter.any()` takes a closure that returns true or false.
54
+
55
+ It applies this closure to each element of the iterator, and if any of them return true, then so does `Iter.any()`.
56
+
57
+ If they all return false, it returns false.
58
+
59
+ An empty iterator returns false.
60
+ Args:
61
+ predicate: Function to evaluate each item. Defaults to checking truthiness.
62
+ Example:
63
+ ```python
64
+ >>> import pyochain as pc
65
+ >>> pc.Iter.from_([0, 1]).any()
66
+ True
67
+ >>> pc.Iter.from_(range(0)).any()
68
+ False
69
+ >>> def is_even(x: int) -> bool:
70
+ ... return x % 2 == 0
71
+ >>> pc.Iter.from_([1, 3, 4]).any(is_even)
72
+ True
73
+
74
+ ```
75
+ """
76
+
77
+ def _any(data: Iterable[T]) -> bool:
78
+ return any(predicate(x) for x in data)
79
+
80
+ return self.into(_any)
81
+
82
+ def is_distinct(self) -> bool:
83
+ """
84
+ Return True if all items are distinct.
85
+ ```python
86
+ >>> import pyochain as pc
87
+ >>> pc.Iter.from_([1, 2]).is_distinct()
88
+ True
89
+
90
+ ```
91
+ """
92
+ return self.into(cz.itertoolz.isdistinct)
93
+
94
+ def all_equal[U](self, key: Callable[[T], U] | None = None) -> bool:
95
+ """
96
+ Return True if all items are equal.
97
+
98
+ Args:
99
+ key: Function to transform items before comparison. Defaults to None.
100
+ Example:
101
+ ```python
102
+ >>> import pyochain as pc
103
+ >>> pc.Iter.from_([1, 1, 1]).all_equal()
104
+ True
105
+
106
+ ```
107
+ A function that accepts a single argument and returns a transformed version of each input item can be specified with key:
108
+ ```python
109
+ >>> pc.Iter.from_("AaaA").all_equal(key=str.casefold)
110
+ True
111
+ >>> pc.Iter.from_([1, 2, 3]).all_equal(key=lambda x: x < 10)
112
+ True
113
+
114
+ ```
115
+ """
116
+ return self.into(mit.all_equal, key=key)
117
+
118
+ def all_unique[U](self, key: Callable[[T], U] | None = None) -> bool:
119
+ """
120
+ Returns True if all the elements of iterable are unique.
121
+
122
+ Args:
123
+ key: Function to transform items before comparison. Defaults to None.
124
+ Example:
125
+ ```python
126
+ >>> import pyochain as pc
127
+ >>> pc.Iter.from_("ABCB").all_unique()
128
+ False
129
+
130
+ ```
131
+ If a key function is specified, it will be used to make comparisons.
132
+ ```python
133
+ >>> pc.Iter.from_("ABCb").all_unique()
134
+ True
135
+ >>> pc.Iter.from_("ABCb").all_unique(str.lower)
136
+ False
137
+
138
+ ```
139
+ The function returns as soon as the first non-unique element is encountered.
140
+
141
+ Iterables with a mix of hashable and unhashable items can be used, but the function will be slower for unhashable items
142
+
143
+ """
144
+ return self.into(mit.all_unique, key=key)
145
+
146
+ def is_sorted[U](
147
+ self,
148
+ key: Callable[[T], U] | None = None,
149
+ reverse: bool = False,
150
+ strict: bool = False,
151
+ ) -> bool:
152
+ """
153
+ Returns True if the items of iterable are in sorted order.
154
+
155
+ Args:
156
+ key: Function to transform items before comparison. Defaults to None.
157
+ reverse: Whether to check for descending order. Defaults to False.
158
+ strict: Whether to enforce strict sorting (no equal elements). Defaults to False.
159
+ Example:
160
+ ```python
161
+ >>> import pyochain as pc
162
+ >>> pc.Iter.from_(["1", "2", "3", "4", "5"]).is_sorted(key=int)
163
+ True
164
+ >>> pc.Iter.from_([5, 4, 3, 1, 2]).is_sorted(reverse=True)
165
+ False
166
+
167
+ If strict, tests for strict sorting, that is, returns False if equal elements are found:
168
+ ```python
169
+ >>> pc.Iter.from_([1, 2, 2]).is_sorted()
170
+ True
171
+ >>> pc.Iter.from_([1, 2, 2]).is_sorted(strict=True)
172
+ False
173
+
174
+ ```
175
+
176
+ The function returns False after encountering the first out-of-order item.
177
+
178
+ This means it may produce results that differ from the built-in sorted function for objects with unusual comparison dynamics (like math.nan).
179
+
180
+ If there are no out-of-order items, the iterable is exhausted.
181
+ """
182
+ return self.into(mit.is_sorted, key=key, reverse=reverse, strict=strict)
183
+
184
+ @overload
185
+ def find(
186
+ self, default: None = None, predicate: Callable[[T], bool] | None = ...
187
+ ) -> T | None: ...
188
+ @overload
189
+ def find(self, default: T, predicate: Callable[[T], bool] | None = ...) -> T: ...
190
+
191
+ def find[U](
192
+ self, default: U = None, predicate: Callable[[T], bool] | None = None
193
+ ) -> U | T:
194
+ """
195
+ Searches for an element of an iterator that satisfies a `predicate`, by:
196
+
197
+ - Taking a closure that returns true or false as `predicate` (optional).
198
+ - Using the identity function if no `predicate` is provided.
199
+ - Applying this closure to each element of the iterator.
200
+ - Returning the first element that satisfies the `predicate`.
201
+
202
+ If all the elements return false, `Iter.find()` returns the default value.
203
+ Args:
204
+ default: Value to return if no element satisfies the predicate. Defaults to None.
205
+ predicate: Function to evaluate each item. Defaults to checking truthiness.
206
+ Example:
207
+ ```python
208
+ >>> import pyochain as pc
209
+ >>> def gt_five(x: int) -> bool:
210
+ ... return x > 5
211
+ >>>
212
+ >>> def gt_nine(x: int) -> bool:
213
+ ... return x > 9
214
+ >>>
215
+ >>> pc.Iter.from_(range(10)).find()
216
+ 1
217
+ >>> pc.Iter.from_(range(10)).find(predicate=gt_five)
218
+ 6
219
+ >>> pc.Iter.from_(range(10)).find(default="missing", predicate=gt_nine)
220
+ 'missing'
221
+
222
+ ```
223
+ """
224
+ return self.into(mit.first_true, default, predicate)
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ from collections.abc import Callable, Iterable, Iterator
5
+ from typing import TYPE_CHECKING
6
+
7
+ import cytoolz as cz
8
+
9
+ if TYPE_CHECKING:
10
+ from ._main import Iter
11
+
12
+
13
+ class IterConstructors:
14
+ @staticmethod
15
+ def from_count(start: int = 0, step: int = 1) -> Iter[int]:
16
+ """
17
+ Create an infinite iterator of evenly spaced values.
18
+
19
+ **Warning** ⚠️
20
+ This creates an infinite iterator.
21
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken.
22
+
23
+ Args:
24
+ start: Starting value of the sequence. Defaults to 0.
25
+ step: Difference between consecutive values. Defaults to 1.
26
+ Example:
27
+ ```python
28
+ >>> import pyochain as pc
29
+ >>> pc.Iter.from_count(10, 2).take(3).into(list)
30
+ [10, 12, 14]
31
+
32
+ ```
33
+ """
34
+ from ._main import Iter
35
+
36
+ return Iter(itertools.count(start, step))
37
+
38
+ @staticmethod
39
+ def from_func[U](func: Callable[[U], U], input: U) -> Iter[U]:
40
+ """
41
+ Create an infinite iterator by repeatedly applying a function on an original input.
42
+
43
+ **Warning** ⚠️
44
+ This creates an infinite iterator.
45
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken.
46
+
47
+ Args:
48
+ func: Function to apply repeatedly.
49
+ input: Initial value to start the iteration.
50
+
51
+ Example:
52
+ ```python
53
+ >>> import pyochain as pc
54
+ >>> pc.Iter.from_func(lambda x: x + 1, 0).take(3).into(list)
55
+ [0, 1, 2]
56
+
57
+ ```
58
+ """
59
+ from ._main import Iter
60
+
61
+ return Iter(cz.itertoolz.iterate(func, input))
62
+
63
+ @staticmethod
64
+ def from_[U](data: Iterable[U]) -> Iter[U]:
65
+ """
66
+ Create an iterator from any Iterable.
67
+
68
+ - An Iterable is any object capable of returning its members one at a time, permitting it to be iterated over in a for-loop.
69
+ - An Iterator is an object representing a stream of data; returned by calling `iter()` on an Iterable.
70
+ - Once an Iterator is exhausted, it cannot be reused or reset.
71
+
72
+ If you need to reuse the data, consider collecting it into a list first with `.collect()`.
73
+
74
+ In general, avoid intermediate references when dealing with lazy iterators, and prioritize method chaining instead.
75
+ Args:
76
+ data: Iterable to convert into an iterator.
77
+ Example:
78
+ ```python
79
+ >>> import pyochain as pc
80
+ >>> data: tuple[int, ...] = (1, 2, 3)
81
+ >>> iterator = pc.Iter.from_(data)
82
+ >>> iterator.unwrap().__class__.__name__
83
+ 'tuple_iterator'
84
+ >>> mapped = iterator.map(lambda x: x * 2)
85
+ >>> mapped.unwrap().__class__.__name__
86
+ 'map'
87
+ >>> mapped.collect(tuple).unwrap()
88
+ (2, 4, 6)
89
+ >>> # iterator is now exhausted
90
+ >>> iterator.collect().unwrap()
91
+ []
92
+
93
+ ```
94
+ """
95
+ from ._main import Iter
96
+
97
+ return Iter(iter(data))
98
+
99
+ @staticmethod
100
+ def unfold[S, V](seed: S, generator: Callable[[S], tuple[V, S] | None]) -> Iter[V]:
101
+ """
102
+ Create an iterator by repeatedly applying a generator function to an initial state.
103
+
104
+ The `generator` function takes the current state and must return:
105
+ - A tuple `(value, new_state)` to emit the `value` and continue with the `new_state`.
106
+ - `None` to stop the generation.
107
+
108
+ This is functionally equivalent to a state-based `while` loop.
109
+
110
+ **Warning** ⚠️
111
+ If the `generator` function never returns `None`, it creates an infinite iterator.
112
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken if necessary.
113
+
114
+ Args:
115
+ seed: Initial state for the generator.
116
+ generator: Function that generates the next value and state.
117
+
118
+ Example:
119
+ ```python
120
+ >>> import pyochain as pc
121
+ >>> # Example 1: Simple counter up to 5
122
+ >>> def counter_generator(state: int) -> tuple[int, int] | None:
123
+ ... if state < 5:
124
+ ... return (state * 10, state + 1)
125
+ ... return None
126
+ >>> pc.Iter.unfold(seed=0, generator=counter_generator).into(list)
127
+ [0, 10, 20, 30, 40]
128
+ >>> # Example 2: Fibonacci sequence up to 100
129
+ >>> type FibState = tuple[int, int]
130
+ >>> def fib_generator(state: FibState) -> tuple[int, FibState] | None:
131
+ ... a, b = state
132
+ ... if a > 100:
133
+ ... return None
134
+ ... return (a, (b, a + b))
135
+ >>> pc.Iter.unfold(seed=(0, 1), generator=fib_generator).into(list)
136
+ [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
137
+ >>> # Example 3: Infinite iterator (requires take())
138
+ >>> pc.Iter.unfold(seed=1, generator=lambda s: (s, s * 2)).take(5).into(list)
139
+ [1, 2, 4, 8, 16]
140
+
141
+ ```
142
+ """
143
+ from ._main import Iter
144
+
145
+ def _unfold() -> Iterator[V]:
146
+ current_seed: S = seed
147
+ while True:
148
+ result: tuple[V, S] | None = generator(current_seed)
149
+ if result is None:
150
+ break
151
+ value, next_seed = result
152
+ yield value
153
+ current_seed = next_seed
154
+
155
+ return Iter(_unfold())