Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ coverage.info
.python-version

.coverage
htmlcov/
htmlcov/
.claude
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.11.5
rev: 0.11.26
hooks:
- id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
rev: v0.15.20
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/astral-sh/ty-pre-commit
# ty version.
rev: v0.0.49
rev: v0.0.56
hooks:
- id: ty
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,21 @@ Note:

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.

- **Combining and splitting:** `partition`, `copy`, `batched`, `pairwise`, `rolling`, `chain`, `cycle`, `repeat`, `product`, `inspect`, `intersperse`, `interleave`, `chunk_by`
- **Transformation and filtering:** `accumulate`, `filter`, `map`, `starmap`, `map_while`, `flatten`, `flat_map`, `skip_while`, `take_while`
- **Combining and splitting:** `partition`, `copy`, `batched`, `pairwise`, `rolling`, `chain`, `cycle`, `repeat`, `product`, `inspect`, `intersperse`, `interleave`, `chunk_by`, `zip_longest`
- **Transformation and filtering:** `accumulate`, `filter`, `map`, `starmap`, `map_while`, `flatten`, `flat_map`, `skip_while`, `take_while`, `dedup`

However, some methods are **eager consumers**. These methods iterate over and consume the underlying data, returning concrete values, collections, or aggregates. Examples include:

* **Collection methods:** `collect`, `last`, `next`, `next_chunk`, `nth`, `position`
* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `all`, `any`, `consume`, `find`, `fold`
* **Sorting/grouping:** `groupby` sorts the entire input up front, and `value_counts` counts it (most common first, like pandas), so both consume the whole iterator immediately and must not be used on infinite sources. Use the lazy `chunk_by` to group consecutive runs without sorting.
* **Collection methods:** `collect`, `last`, `next`, `next_chunk`, `next_if`, `nth`, `position`
* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `sum`, `prod`, `all`, `any`, `consume`, `find`, `fold`
* **Sorting/grouping:** `sorted_by` and `groupby` sort the entire input up front, and `value_counts` counts it (most common first, like pandas), so all three consume the whole iterator immediately and must not be used on infinite sources. Use the lazy `chunk_by` to group consecutive runs without sorting.

### Important Considerations

When working with `Itr`, keep these points in mind:

* **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.
* **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.
* **No Rewinding:** It's not possible to rewind an `Itr` to an earlier state. You can "preview" the next value using the `peek()` method, and conditionally consume it with `next_if()`.
* **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.

## API Reference
Expand Down
124 changes: 121 additions & 3 deletions doc/apidoc.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `Itr` v0.3.0 class documentation
# `Itr` v0.4.0 class documentation
A generic iterator adaptor class inspired by Rust's Iterator trait, providing a composable API for
functional-style iteration and transformation over Python iterables.
## Public methods
Expand Down Expand Up @@ -167,6 +167,21 @@ Example:
(1, 2, 3, 1, 2)


### `dedup`

Lazily remove *consecutive* duplicate items, keeping the first of each run (like Rust's `dedup`).

Only adjacent duplicates are removed, so the result may still contain repeated values if they are not
contiguous. Items are compared by equality and do not need to be hashable. Works on infinite iterators.

Returns:
Itr[T]: An iterator over the items with consecutive duplicates removed.

Example:
>>> Itr([1, 1, 2, 2, 2, 3, 1]).dedup().collect()
(1, 2, 3, 1)


### `enumerate`

Yield pairs of (index, item) for each item in the iterator, where index starts at 0 or the value provided
Expand Down Expand Up @@ -415,6 +430,31 @@ Returns:



### `next_if`

Consume and return the next item only if it satisfies the predicate (like Rust's `Peekable::next_if`).

If the next item fails the predicate it is left in place (retrievable by a subsequent `next` or `peek`),
and None is returned. None is also returned if the iterator is exhausted.

Args:
predicate (Callable[[T], bool]): A function to test the next item.

Returns:
T | None: The next item if it satisfies the predicate, otherwise None.

Example:
>>> it = Itr([1, 2, 3])
>>> it.next_if(lambda x: x < 3)
1
>>> it.next_if(lambda x: x < 3)
2
>>> it.next_if(lambda x: x < 3) is None
True
>>> it.next()
3


### `nth`

Return the n-th item (0-based) from the iterator, consuming the preceding items.
Expand Down Expand Up @@ -467,9 +507,21 @@ Returns the next element in the sequence without advancing the iterator.
Returns:
T: The next element in the sequence.

Raises:
StopIteration: If the iterator is exhausted.

Note:
This method creates a copy of the iterator to avoid modifying the original iterator's state.
This method copies the iterator to avoid modifying the original iterator's state. The copy is cheap
even for repeated peeks: `itertools.tee` re-tees an already-teed iterator without adding a layer.

Example:
>>> it = Itr([1, 2])
>>> it.peek(), it.peek()
(1, 1)
>>> it.next()
1
>>> it.peek()
2


### `position`
Expand All @@ -487,6 +539,20 @@ Raises:
StopIteration: If no element satisfies the predicate.


### `prod`

Return the product of all items in the iterator (1 if empty). NB Consumes the iterator.

The items must support multiplication (e.g. numbers).

Returns:
T: The product of all items.

Example:
>>> Itr([2, 3, 4]).prod()
24


### `product`


Expand Down Expand Up @@ -567,6 +633,25 @@ Returns:



### `sorted_by`

Return an iterator over the items sorted by the given key function.

This method is **eager**: it consumes and materialises the whole iterator immediately (so it must not be
used on an infinite iterator). The sort is stable: items that compare equal retain their relative order.

Args:
key (Callable[[T], Any]): A function to extract a comparison key from each item.
reverse (bool): If True, sort in descending order. Defaults to False.

Returns:
Itr[T]: An iterator over the sorted items.

Example:
>>> Itr(["ccc", "a", "bb"]).sorted_by(len).collect()
('a', 'bb', 'ccc')


### `starmap`


Expand Down Expand Up @@ -596,6 +681,20 @@ Returns:



### `sum`

Return the sum of all items in the iterator (0 if empty). NB Consumes the iterator.

The items must support addition with int (e.g. numbers; use `reduce` or `fold` for other types).

Returns:
T: The sum of all items.

Example:
>>> Itr([1, 2, 3]).sum()
6


### `take`

Return an iterator over the next n items from the iterator.
Expand Down Expand Up @@ -644,7 +743,7 @@ Notes:
that store items produced by the original iterator until all tees have consumed them.
If one or more returned iterators lag behind the others, buffered items will be
retained and memory usage can grow.
- After calling this method, avoid consuming the original wrapped iterator (`self._it`)
- After calling this method, avoid consuming the original wrapped iterator
directly; use the returned Itr objects to prevent surprising interactions with the
shared buffer.
- Creating the tees is inexpensive, but the memory characteristics depend on how the
Expand Down Expand Up @@ -703,3 +802,22 @@ Returns:
Itr[tuple[T, U]]: An iterator of paired items.



### `zip_longest`

Yield pairs of items from this iterator and another iterable, padding the shorter with `fillvalue`.

Unlike `zip`, iteration continues until the longer input is exhausted, with missing values replaced by
`fillvalue`.

Args:
other (Iterable[U]): The other iterable.
fillvalue (V | None): The value used to pad the shorter input. Defaults to None.

Returns:
Itr[tuple[T | V | None, U | V | None]]: An iterator of paired items.

Example:
>>> Itr([1, 2, 3]).zip_longest("ab", fillvalue="-").collect()
((1, 'a'), (2, 'b'), (3, '-'))

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "itrx"
version = "0.3.0"
version = "0.4.0"
description = "A chainable iterator adapter"
readme = "README.md"
authors = [
Expand All @@ -27,10 +27,10 @@ build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pre-commit>=4.5.1",
"pytest>=8.4.1",
"pytest>=9.1.1",
"pytest-cov>=6.2.1",
"ruff>=0.15.13",
"ty==0.0.37",
"ruff>=0.15.20",
"ty>=0.0.56",
]

[project.optional-dependencies]
Expand Down
11 changes: 10 additions & 1 deletion relnotes.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
## Unreleased
## 0.4.0

### Breaking changes

- `value_counts` now behaves like pandas' `value_counts`: results are ordered most-common-first (ties by first appearance) instead of sorted by key. Items now only need to be hashable rather than orderable, and counting is O(n) rather than O(n log n).
- `tee` now raises `ValueError` when `n < 1`, as its docstring has always stated (previously `tee(0)` silently discarded the iterator and returned an empty tuple).
- `repeat(1)` now returns a new `Itr` and leaves the original exhausted, consistent with every other value of `n` (previously it returned `self`, so consuming the result also consumed the original in that case only).

### New features

- `next_if(predicate)`: consume and return the next item only if it satisfies the predicate, otherwise leave it in place and return `None` (like Rust's `Peekable::next_if`).
- `sum()` / `prod()`: terminal aggregations over the remaining items.
- `dedup()`: lazily remove *consecutive* duplicates, keeping the first of each run (like Rust's `dedup`); works on infinite iterators.
- `zip_longest(other, fillvalue=...)`: like `zip`, but continues to the end of the longer input, padding with `fillvalue`.
- `sorted_by(key, reverse=...)`: eager stable sort by a key function.

### Bug fixes

Expand Down
Loading
Loading