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 ADDED
@@ -0,0 +1,7 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version("itrx")
4
+
5
+ from .itr import Itr
6
+
7
+ __all__ = ["Itr"]
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
+ ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https://raw.githubusercontent.com/virgesmith/itrx/refs/heads/main/pyproject.toml)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ # MIT Licence
2
+
3
+ Copyright &copy; 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.**