itrx 0.1.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.
- itrx/__init__.py +7 -0
- itrx/itr.py +612 -0
- itrx/py.typed +0 -0
- itrx-0.1.0.dist-info/METADATA +122 -0
- itrx-0.1.0.dist-info/RECORD +7 -0
- itrx-0.1.0.dist-info/WHEEL +4 -0
- itrx-0.1.0.dist-info/licenses/LICENCE.md +21 -0
itrx/__init__.py
ADDED
itrx/itr.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from collections import deque
|
|
3
|
+
from collections.abc import Generator, Iterable
|
|
4
|
+
from itertools import pairwise
|
|
5
|
+
from typing import Callable, Iterator, Self, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
_CollectT = TypeVar("_CollectT") # General item type for collected containers
|
|
9
|
+
|
|
10
|
+
Predicate = Callable[[T], bool]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Itr[T](Iterable[T]):
|
|
14
|
+
"""A generic iterator wrapper class inspired by Rust's Iterator trait, providing a composable API for
|
|
15
|
+
functional-style iteration and transformation over Python iterables.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, it: Iterable[T]) -> None:
|
|
19
|
+
"""Initialize the Itr with an iterable.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
it (Iterable[T]): The iterable to wrap.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
self._it = iter(it)
|
|
26
|
+
|
|
27
|
+
def __iter__(self) -> Iterator[T]:
|
|
28
|
+
"Implement the iter method of the Iterator protocol"
|
|
29
|
+
return self._it
|
|
30
|
+
|
|
31
|
+
def __next__(self) -> T:
|
|
32
|
+
"Implement the next method of the Iterator protocol"
|
|
33
|
+
return next(self._it)
|
|
34
|
+
|
|
35
|
+
def all(self, predicate: Predicate[T]) -> bool:
|
|
36
|
+
"""Return True if all elements in the iterator satisfy the predicate.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
predicate (Callable[[T], bool]): A function to test each element.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool: True if all elements satisfy the predicate, False otherwise.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
return all(predicate(item) for item in self._it)
|
|
46
|
+
|
|
47
|
+
def any(self, predicate: Predicate[T]) -> bool:
|
|
48
|
+
"""Return True if any element in the iterator satisfies the predicate.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
predicate (Callable[[T], bool]): A function to test each element.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
bool: True if any element satisfies the predicate, False otherwise.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
return any(predicate(item) for item in self._it)
|
|
58
|
+
|
|
59
|
+
def batched(self, n: int) -> "Itr[tuple[T, ...]]":
|
|
60
|
+
"""
|
|
61
|
+
Groups the elements of the iterator into batches of size `n`.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
n (int): The size of each batch. Must be at least 1.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Itr[tuple[T, ...]]: An iterator yielding tuples of up to `n` elements from the original iterator.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If `n` is less than 1.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> list(Itr(range(7)).batched(3))
|
|
74
|
+
[(0, 1, 2), (3, 4, 5), (6,)]
|
|
75
|
+
"""
|
|
76
|
+
if n < 1:
|
|
77
|
+
raise ValueError("Batch size must be at least 1")
|
|
78
|
+
return Itr(itertools.batched(self._it, n))
|
|
79
|
+
|
|
80
|
+
def chain[U](self, other: Iterable[U]) -> "Itr[T | U]":
|
|
81
|
+
"""Chain this iterator with another iterable, yielding all items from self followed by all items from other.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
other (Iterable[U]): Another iterable to chain.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Itr[T | U]: A new iterator yielding items from both iterables.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
return Itr(itertools.chain(self._it, other))
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def collect(self, container: type[tuple[T, ...]] = tuple) -> tuple[T, ...]: ...
|
|
94
|
+
@overload
|
|
95
|
+
def collect(self, container: type[list[T]]) -> list[T]: ...
|
|
96
|
+
@overload
|
|
97
|
+
def collect(self, container: type[set[T]]) -> set[T]: ...
|
|
98
|
+
@overload
|
|
99
|
+
def collect[K, V](self, container: type[dict[K, V]]) -> dict[K, V]: ...
|
|
100
|
+
|
|
101
|
+
def collect(self, container: type[_CollectT] = tuple) -> _CollectT: # type: ignore[assignment]
|
|
102
|
+
"""Collect all remaining items from the iterator into a sequence (tuple by default).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple[T]: A list of all remaining items.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
return container(self._it) # type: ignore[call-arg]
|
|
109
|
+
|
|
110
|
+
def copy(self) -> "Itr[T]":
|
|
111
|
+
"""Splits the iterator at its *current state* into two independent iterators.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Itr[T]: A new Itr instance wrapping one copy of the original iterator.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
self._it, it = itertools.tee(self._it)
|
|
118
|
+
return Itr(it)
|
|
119
|
+
|
|
120
|
+
def count(self) -> int:
|
|
121
|
+
"""Count the number of remaining items in the iterator. NB Consumes the iterator.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
int: The number of remaining items.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
return sum(1 for _ in self._it)
|
|
128
|
+
|
|
129
|
+
def cycle(self) -> "Itr[T]":
|
|
130
|
+
"""
|
|
131
|
+
Returns a new iterator that cycles indefinitely over the elements of the current iterator.
|
|
132
|
+
|
|
133
|
+
Yields:
|
|
134
|
+
Itr[T]: An iterator that repeats the elements of the original iterator endlessly.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> itr = Itr([1, 2, 3])
|
|
138
|
+
>>> cycler = itr.cycle()
|
|
139
|
+
>>> cycler.take(5).collect()
|
|
140
|
+
(1, 2, 3, 1, 2)
|
|
141
|
+
"""
|
|
142
|
+
return Itr(itertools.cycle(self._it))
|
|
143
|
+
|
|
144
|
+
def enumerate(self, *, start: int = 0) -> "Itr[tuple[int, T]]":
|
|
145
|
+
"""Yield pairs of (index, item) for each item in the iterator, where index starts at 0 or the value provided
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Itr[tuple[int, T]]: An iterator of (index, item) pairs.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
return Itr(enumerate(self._it, start))
|
|
152
|
+
|
|
153
|
+
def filter(self, predicate: Predicate[T]) -> "Itr[T]":
|
|
154
|
+
"""Yield only items that satisfy the predicate.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
predicate (Callable[[T], bool]): A function to test each element.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Itr[T]: An iterator of filtered items.
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
return Itr(filter(predicate, self._it))
|
|
164
|
+
|
|
165
|
+
def find(self, predicate: Predicate[T]) -> T | None:
|
|
166
|
+
"""Return the first item in the iterator that satisfies the predicate, or None if not found.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
predicate (Callable[[T], bool]): A function to test each element.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
T | None: The first matching item, or None.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
while True:
|
|
177
|
+
item = next(self._it)
|
|
178
|
+
if predicate(item):
|
|
179
|
+
return item
|
|
180
|
+
except StopIteration:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
# TODO fix the type annotations
|
|
184
|
+
def flat_map[U, V](self, mapper: Callable[[U], V]) -> "Itr[V]":
|
|
185
|
+
"""Map each item to an iterable and flatten the results. Each item must itself be iterable.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
mapper (Callable[[U], V]): A function mapping each item to an iterable.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Itr[V]: An iterator over the mapped and flattened items.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def flat_mapper() -> Generator[V, None, None]:
|
|
196
|
+
try:
|
|
197
|
+
while True:
|
|
198
|
+
seq = next(self._it)
|
|
199
|
+
# TODO fix type annotations are remove this
|
|
200
|
+
assert isinstance(seq, Iterable)
|
|
201
|
+
iseq = iter(seq)
|
|
202
|
+
try:
|
|
203
|
+
while True:
|
|
204
|
+
yield mapper(next(iseq))
|
|
205
|
+
except StopIteration:
|
|
206
|
+
pass
|
|
207
|
+
except StopIteration:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
return Itr(flat_mapper())
|
|
211
|
+
|
|
212
|
+
def flatten[U](self) -> "Itr[U]":
|
|
213
|
+
"""Flatten one level of nesting in the iterator. Each item must itself be iterable.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Itr[U]: An iterator over the flattened items.
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
return Itr(itertools.chain.from_iterable(self._it)) # type: ignore[arg-type]
|
|
220
|
+
|
|
221
|
+
def fold[U](self, init: U, func: Callable[[U, T], U]) -> U:
|
|
222
|
+
"""Reduce the iterator to a single value using a function and an initial value.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
init (U): The initial value.
|
|
226
|
+
func (Callable[[U, T], U]): The function to combine values.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
U: The final reduced value.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
result = init
|
|
233
|
+
for item in self._it:
|
|
234
|
+
result = func(result, item)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
def for_each(self, func: Callable[[T], None]) -> None:
|
|
238
|
+
"""Apply a function to each item in the iterator.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
func (Callable[[T], None]): The function to apply.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
for item in self._it:
|
|
245
|
+
func(item)
|
|
246
|
+
|
|
247
|
+
def groupby[U](self, grouper: Callable[[T], U]) -> "Itr[tuple[U, tuple[T,...]]]":
|
|
248
|
+
"""
|
|
249
|
+
Sort and then group an iterable by the supplied key function. Note the following differences from itertools:
|
|
250
|
+
- The iterable is pre-sorted because itertools.groupby only works correctly on sorted sequences
|
|
251
|
+
- The resulting groupby objects are realised into tuples
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Itr[tuple[U, tuple[T,...]]]: An iterator over the keys and tuples of values
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
return Itr(itertools.groupby(sorted(self._it, key=grouper), key=grouper)).map(lambda g: (g[0], tuple(g[1]))) # type: ignore[arg-type]
|
|
258
|
+
|
|
259
|
+
def intersperse[U](self, item: U) -> "Itr[T | U]":
|
|
260
|
+
"""
|
|
261
|
+
Yield items from the iterator, inserting the given item between each pair of items.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
item (U): The item to intersperse.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Itr[T | U]: An iterator with the item interspersed.
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def intersperser(item: U) -> Generator[T | U, None, None]:
|
|
272
|
+
try:
|
|
273
|
+
current = next(self._it)
|
|
274
|
+
while True:
|
|
275
|
+
yield current
|
|
276
|
+
current = next(self._it)
|
|
277
|
+
yield item
|
|
278
|
+
except StopIteration:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
return Itr(intersperser(item))
|
|
282
|
+
|
|
283
|
+
def interleave[U](self, other: Iterable[U]) -> "Itr[T | U]":
|
|
284
|
+
"""
|
|
285
|
+
Interleaves elements from this iterator with elements from another iterator.
|
|
286
|
+
Stops when either iterator is exhausted.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
other (Itr[U]): Another iterator to interleave with.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Itr[T | U]: A new iterator yielding elements alternately from self and other.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
itr1 = Itr([1, 3, 5])
|
|
296
|
+
itr2 = Itr([2, 4, 6])
|
|
297
|
+
result = itr1.interleave(itr2)
|
|
298
|
+
list(result) # [1, 2, 3, 4, 5, 6]
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
return Itr(self.zip(other).flatten())
|
|
302
|
+
|
|
303
|
+
def last(self) -> T | None:
|
|
304
|
+
"""Return the last item from the iterator. Do not use on an open-ended Iterable
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
T: The last item.
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
last_item = None
|
|
311
|
+
for item in self._it:
|
|
312
|
+
last_item = item
|
|
313
|
+
return last_item
|
|
314
|
+
|
|
315
|
+
def map[U](self, mapper: Callable[[T], U]) -> "Itr[U]":
|
|
316
|
+
"""Map each item in the iterator using the given function.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
mapper (Callable[[T], U]): The function to apply.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Itr[U]: An iterator of mapped items.
|
|
323
|
+
|
|
324
|
+
"""
|
|
325
|
+
return Itr(map(mapper, self._it))
|
|
326
|
+
|
|
327
|
+
def max(self) -> T:
|
|
328
|
+
"""
|
|
329
|
+
Returns the maximum element from the underlying iterable.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
T: The maximum element in the iterable.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValueError: If the iterable is empty.
|
|
336
|
+
"""
|
|
337
|
+
# TODO T should have a "comparable" bound
|
|
338
|
+
return max(self._it) # type: ignore[type-var]
|
|
339
|
+
|
|
340
|
+
def min(self) -> T:
|
|
341
|
+
"""
|
|
342
|
+
Returns the minimum element from the underlying iterable.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
T_co: The smallest element in the iterable.
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
ValueError: If the iterable is empty.
|
|
349
|
+
"""
|
|
350
|
+
# TODO T should have a "comparable" bound
|
|
351
|
+
return min(self._it) # type: ignore[type-var]
|
|
352
|
+
|
|
353
|
+
def next(self) -> T:
|
|
354
|
+
"""Return the next item from the iterator, if available. Otherwise raises StopIteration
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
T: The next item.
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
return next(self._it)
|
|
361
|
+
|
|
362
|
+
def next_chunk(self, n: int) -> tuple[T, ...]:
|
|
363
|
+
"""Return a list of the next n items from the iterator.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
n (int): The number of items to yield.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
tuple[T, ...]: The next n items (or fewer if the iterator is exhausted).
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
return self.take(n).collect()
|
|
373
|
+
|
|
374
|
+
def nth(self, n: int) -> T | None:
|
|
375
|
+
"""Return the n-th item (1-based) from the iterator, or None if out of range.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
n (int): The index (1-based) of the item to return.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
T | None: The n-th item, or None.
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
# TODO Rust impl returns None if overruns
|
|
385
|
+
try:
|
|
386
|
+
for _ in range(n - 1):
|
|
387
|
+
next(self._it)
|
|
388
|
+
return next(self._it)
|
|
389
|
+
except StopIteration:
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
def pairwise(self) -> "Itr[tuple[T, T]]":
|
|
393
|
+
"""Returns an iterator that yields consecutive pairs of elements from the iterable.
|
|
394
|
+
|
|
395
|
+
Each item produced is a tuple containing two consecutive elements from the original iterable.
|
|
396
|
+
For example, given [1, 2, 3, 4], the `Itr` returned from this method yields (1, 2), (2, 3), (3, 4).
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Itr[tuple[T, T]]: An iterator over consecutive pairs from the original iterable.
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
return Itr(pairwise(self._it))
|
|
403
|
+
|
|
404
|
+
def partition(self, predicate: Predicate[T]) -> tuple["Itr[T]", "Itr[T]"]:
|
|
405
|
+
"""
|
|
406
|
+
Splits the elements of the iterator into two separate iterators based on a predicate.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
predicate (Callable[[T], bool]): A function that takes an element and returns True or False.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
tuple[Itr[T], Itr[T]]: A tuple containing two iterators:
|
|
413
|
+
- The first iterator yields elements for which the predicate returns True.
|
|
414
|
+
- The second iterator yields elements for which the predicate returns False.
|
|
415
|
+
"""
|
|
416
|
+
copy = self.copy()
|
|
417
|
+
return self.filter(predicate), copy.filter(lambda i: not predicate(i))
|
|
418
|
+
|
|
419
|
+
def peek(self) -> T:
|
|
420
|
+
"""Returns the next element in the sequence without advancing the iterator.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
T: The next element in the sequence.
|
|
424
|
+
|
|
425
|
+
Note:
|
|
426
|
+
This method creates a copy of the iterator to avoid modifying the original iterator's state.
|
|
427
|
+
|
|
428
|
+
"""
|
|
429
|
+
return self.copy().next()
|
|
430
|
+
|
|
431
|
+
def product[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]":
|
|
432
|
+
"""
|
|
433
|
+
Creates a new iterator over tuples of the combinations of self and the other iterator
|
|
434
|
+
"""
|
|
435
|
+
return Itr(itertools.product(self._it, other))
|
|
436
|
+
|
|
437
|
+
def reduce(self, func: Callable[[T, T], T]) -> T:
|
|
438
|
+
"""Reduce the iterator to a single value using a function.
|
|
439
|
+
Will raise StopIteration if the iterator is exhausted.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
func (Callable[[T, T], T]): The function to combine values.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
T: The final reduced value.
|
|
446
|
+
|
|
447
|
+
"""
|
|
448
|
+
result = next(self._it)
|
|
449
|
+
for item in self._it:
|
|
450
|
+
result = func(result, item)
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
def repeat(self, n: int) -> "Itr[T]":
|
|
454
|
+
"""
|
|
455
|
+
Returns a new iterator that repeats the elements of the current iterator `n` times.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
n (int): The number of times to repeat the elements.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Itr[T]: An iterator yielding the elements of the original iterator repeated `n` times.
|
|
462
|
+
|
|
463
|
+
Note:
|
|
464
|
+
This implementation creates `n` independent iterators using `itertools.tee`, which may be inefficient for large `n` or large input iterators.
|
|
465
|
+
"""
|
|
466
|
+
# this creates n iterators so may be inefficient
|
|
467
|
+
return Itr(itertools.chain(*itertools.tee(self._it, n)))
|
|
468
|
+
|
|
469
|
+
def rev(self) -> "Itr[T]":
|
|
470
|
+
"""Return a reversed iterator over the remaining items (materializes the sequence).
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Itr[T]: A reversed iterator.
|
|
474
|
+
|
|
475
|
+
"""
|
|
476
|
+
# it's generally impossible to do this without materialising the entire sequence
|
|
477
|
+
return Itr(tuple(self._it)[::-1])
|
|
478
|
+
|
|
479
|
+
def rolling(self, n: int) -> "Itr[tuple[T, ...]]":
|
|
480
|
+
"""
|
|
481
|
+
Rolling window (generalisation of pairwise)
|
|
482
|
+
Rather than copying the iterator multiple times, collect n, yield the sequence and incrementally drop/add
|
|
483
|
+
"""
|
|
484
|
+
if n < 1:
|
|
485
|
+
raise ValueError(f"Invalid rolling window {n} (must be at least 1)")
|
|
486
|
+
|
|
487
|
+
def roller(it: "Itr[T]") -> Generator[tuple[T, ...], None, None]:
|
|
488
|
+
try:
|
|
489
|
+
window = deque(it.take(n).collect(), maxlen=n)
|
|
490
|
+
while True:
|
|
491
|
+
if len(window) == n:
|
|
492
|
+
yield tuple(window)
|
|
493
|
+
window.append(it.next())
|
|
494
|
+
except StopIteration:
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
return Itr(roller(self))
|
|
498
|
+
|
|
499
|
+
def skip(self, n: int) -> Self:
|
|
500
|
+
"""Skip the next n items in the iterator.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
n (int): The number of items to skip.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Self: The iterator itself.
|
|
507
|
+
|
|
508
|
+
"""
|
|
509
|
+
for _ in range(n):
|
|
510
|
+
next(self._it)
|
|
511
|
+
return self
|
|
512
|
+
|
|
513
|
+
def skip_while(self, predicate: Predicate[T]) -> "Itr[T]":
|
|
514
|
+
"""Skip items in the iterator as long as the predicate is true, returning self.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
predicate (Callable[[T], bool]): A function to test each element.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Itr[T]: The iterator itself after skipping items.
|
|
521
|
+
|
|
522
|
+
"""
|
|
523
|
+
return Itr(itertools.dropwhile(predicate, self._it))
|
|
524
|
+
|
|
525
|
+
def starmap[U](self, func: Callable[..., U]) -> "Itr[U]":
|
|
526
|
+
"""
|
|
527
|
+
Applies a function to the elements of the iterator, unpacking the elements as arguments.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
func (Callable[[T], U]): A function to apply to each element. Each element is expected to be an iterable of arguments for the function.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Itr[U]: A new iterator with the results of applying the function to each unpacked element.
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
>>> itr = Itr([(1, 2), (3, 4)])
|
|
537
|
+
>>> list(itr.starmap(lambda x, y: x + y))
|
|
538
|
+
[3, 7]
|
|
539
|
+
"""
|
|
540
|
+
return Itr(itertools.starmap(func, self._it)) # type: ignore[arg-type]
|
|
541
|
+
|
|
542
|
+
def step_by(self, n: int) -> "Itr[T]":
|
|
543
|
+
"""Yield every n-th item from the iterator.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
n (int): The step size.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Itr[T]: An iterator yielding every n-th item.
|
|
550
|
+
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
def stepper(n: int) -> Generator[T, None, None]:
|
|
554
|
+
try:
|
|
555
|
+
while True:
|
|
556
|
+
yield next(self._it)
|
|
557
|
+
for _ in range(n - 1):
|
|
558
|
+
next(self._it)
|
|
559
|
+
except StopIteration:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
return Itr(stepper(n))
|
|
563
|
+
|
|
564
|
+
def take(self, n: int) -> "Itr[T]":
|
|
565
|
+
"""Return an iterator over the next n items from the iterator.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
n (int): The number of items to take.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Itr[T]: An iterator over the next n items.
|
|
572
|
+
|
|
573
|
+
"""
|
|
574
|
+
return Itr(itertools.islice(self._it, n))
|
|
575
|
+
|
|
576
|
+
def take_while(self, predicate: Predicate[T]) -> "Itr[T]":
|
|
577
|
+
"""Collects and returns items from the iterator as long as the given predicate is true.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
predicate (Callable[[T], bool]): A function that takes an item and returns True to continue taking items, or False to stop.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Self: A new Itr instance containing the items taken while the predicate was true.
|
|
584
|
+
|
|
585
|
+
"""
|
|
586
|
+
return Itr(itertools.takewhile(predicate, self._it))
|
|
587
|
+
|
|
588
|
+
def unzip[U, V](self) -> tuple["Itr[U]", "Itr[V]"]:
|
|
589
|
+
"""Splits the iterator of pairs into two separate iterators, each containing the elements from one position of the pairs.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
tuple[Itr[T], Itr[U]]: A tuple containing two Itr instances. The first contains all first elements, and the second contains all second elements from the original iterator of pairs.
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
ValueError: If the underlying iterator does not yield pairs of equal length (enforced by strict=True).
|
|
596
|
+
|
|
597
|
+
"""
|
|
598
|
+
# TODO express that T is tuple[U, V]
|
|
599
|
+
it1, it2 = zip(*self._it, strict=True)
|
|
600
|
+
return Itr(it1), Itr(it2)
|
|
601
|
+
|
|
602
|
+
def zip[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]":
|
|
603
|
+
"""Yield pairs of items from this iterator and another iterable.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
other (Iterable[U]): The other iterable.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Itr[tuple[T, U]]: An iterator of paired items.
|
|
610
|
+
|
|
611
|
+
"""
|
|
612
|
+
return Itr(zip(self._it, other, strict=False))
|
itrx/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: itrx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A chainable iterable wrapper
|
|
5
|
+
Author-email: virgesmith <andrew@friarswood.net>
|
|
6
|
+
License-File: LICENCE.md
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# itrx - A Powerful Iterable Wrapper
|
|
11
|
+
|
|
12
|
+
[](https://opensource.org/licenses/MIT)
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
`itrx` is a powerful Python library that wraps iterators, iterables, and generators, providing a Rust-inspired `Iterator` trait experience with added Pythonic conveniences. It enables developers to build complex data processing pipelines with a fluent, chainable, and lazy API. In most cases, it simply wraps itertools in syntactic sugar.
|
|
17
|
+
|
|
18
|
+
Heavily inspired by Rust's [Iterator trait](https://doc.rust-lang.org/std/iter/trait.Iterator.html), `itrx` offers a familiar and robust pattern for sequence manipulation.
|
|
19
|
+
|
|
20
|
+
## Key Features & Why `Itr`?
|
|
21
|
+
|
|
22
|
+
* **Compatibility:** `Itr` is designed to work seamlessly with various Python iterable types, including sequences, ranges, custom iterators, and generators.
|
|
23
|
+
* **Fluent & Chainable API:** Write expressive, left-to-right sequences of operations on your data.
|
|
24
|
+
* **Lazy Evaluation:** Operations are only performed when their results are needed, improving efficiency for large or infinite sequences.
|
|
25
|
+
* **Rust-Inspired Patterns:** Bring the power and clarity of Rust's `Iterator` design to your Python projects.
|
|
26
|
+
* **Pythonic Additions:** Includes convenient methods like `starmap` that integrate smoothly with Python's ecosystem.
|
|
27
|
+
* **Simplifies Complex Logic:** Often provides a more readable and concise alternative to nested `itertools` or built-in functions.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install itrx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start & Core Functionality
|
|
37
|
+
|
|
38
|
+
### Chaining
|
|
39
|
+
|
|
40
|
+
`Itr` wraps Python Iterators, Iterables, and Generators, allowing efficient chaining methods for data transformation.
|
|
41
|
+
In this completely arbitrary example we take some integers, reverse them, discard some, take every 4th value and
|
|
42
|
+
print it if its square ends with the digit 9:
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
>>> from itrx import Itr
|
|
46
|
+
>>> Itr(range(100)).rev().step_by(4).skip(10).map(lambda x: x * x).filter(lambda x: x % 10 == 9).for_each(print)
|
|
47
|
+
2209
|
|
48
|
+
1849
|
|
49
|
+
729
|
|
50
|
+
529
|
|
51
|
+
49
|
|
52
|
+
9
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For reference, the equivalent expression using built-ins and `itertools` can be significantly less readable:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from itertools import islice
|
|
60
|
+
|
|
61
|
+
for item in filter(
|
|
62
|
+
lambda x: x % 10 == 9,
|
|
63
|
+
map(lambda x: x * x, islice(islice(reversed(range(100)), None, None, 4), 10, None)),
|
|
64
|
+
):
|
|
65
|
+
print(item)
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Outputs
|
|
70
|
+
|
|
71
|
+
While `Itr` methods typically return `self` or another `Itr` instance, the `collect()` method allows you to materialize the results into various Python collections: `tuple` (default), `list`, `set`, or `dict`.
|
|
72
|
+
|
|
73
|
+
Here's how to group words by their length into a dictionary:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
>>> from itrx import Itr
|
|
77
|
+
>>> Itr(("apple", "banana", "carrot")).groupby(len).collect(dict)
|
|
78
|
+
{5: ('apple',), 6: ('banana', 'carrot')}
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For reference, an equivalent using `itertools` directly:
|
|
83
|
+
|
|
84
|
+
```py
|
|
85
|
+
>>> import itertools
|
|
86
|
+
>>> {k: tuple(v) for k, v in itertools.groupby(("apple", "banana", "carrot"), key=len)}
|
|
87
|
+
{5: ('apple',), 6: ('banana', 'carrot')}
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
*Note: Using `collect(dict)` requires an iterable that produces 2-tuples (key-value pairs).*
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## How `Itr` Works: Lazy vs. Eager
|
|
95
|
+
|
|
96
|
+
Most `Itr` methods are **lazy transformations**, meaning they return a new `Itr` instance without immediately processing any data. This allows for arbitrary chaining and efficient memory usage, as items are only processed as they are requested. In most cases, `Itr` simply acts as a convenient wrapper around `itertools`, enabling this left-to-right chaining syntax.
|
|
97
|
+
|
|
98
|
+
- **Combining and splitting:** `partition`, `copy`, `batched`, `pairwise`, `rolling`, `chain`, `cycle`, `repeat`, `product`
|
|
99
|
+
- **Transformation and filtering:** `filter`, `map`, `starmap`, `flatten`, `flat_map`, `skip_while`, `take_while`, `groupby`
|
|
100
|
+
|
|
101
|
+
However, some methods are **eager consumers**. These methods iterate over and consume the underlying data, returning concrete values, collections, or aggregates. Examples include:
|
|
102
|
+
|
|
103
|
+
* **Collection methods:** `collect`, `next`, `next_chunk`, `nth`
|
|
104
|
+
* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `all`, `any`, `last`, `find`, `fold`
|
|
105
|
+
|
|
106
|
+
### Important Considerations
|
|
107
|
+
|
|
108
|
+
When working with `Itr`, keep these points in mind:
|
|
109
|
+
|
|
110
|
+
* **Single-Pass Iterators:** Like all Python iterators, `Itr` instances (and their underlying iterators) can generally only be consumed once. If you need to process the same sequence multiple times, use methods like `copy()`, `cycle()`, or `repeat()` as necessary.
|
|
111
|
+
* **No Rewinding:** It's not possible to rewind an `Itr` to an earlier state. You can "preview" the next value using the `peek()` method, but be aware that `peek()` often copies the iterator internally, which can be inefficient if used excessively.
|
|
112
|
+
* **Infinite Iterators:** Be cautious with open-ended iterators (e.g., those from `itertools.count()` or custom generators). Eager evaluation methods (like `collect()`, `count()`, `reduce()`) will attempt to consume the entire sequence, potentially leading to infinite loops or out-of-memory errors if applied to an infinite source.
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
`Itr` provides a comprehensive set of methods for various iterable operations. For a complete list of methods and their detailed descriptions, please refer to the [API documentation](./doc/apidoc.md).
|
|
117
|
+
|
|
118
|
+
*Note: `apidoc.md` is auto-generated using `Itr` - see [introspect.py](src/scripts/introspect.py).*
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
Some worked examples can be found [in this notebook](./doc/examples.ipynb).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
itrx/__init__.py,sha256=KohYUJt2bFjCNtmXSD06UbOpcvCosGMia7U62W96TJc,117
|
|
2
|
+
itrx/itr.py,sha256=MEYT0O1pSPm0NXK33m70O9xyc4hIKjuBm6c1qW4VdA8,19865
|
|
3
|
+
itrx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
itrx-0.1.0.dist-info/METADATA,sha256=-r7pc6oNTPZ7IDpKsJ4XymnXrYKXQLLh7o8XXOQER9U,5826
|
|
5
|
+
itrx-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
itrx-0.1.0.dist-info/licenses/LICENCE.md,sha256=8RqQ0hi29zjY0keH0jaItdtWSpxCnqErjIHBoDSgSV0,1078
|
|
7
|
+
itrx-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT Licence
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Andrew Smith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicence, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.**
|