Skip to content

Issue #18 enhancements: O(1) peek + next_if, Rust-staple methods, repeat(1) fix#19

Merged
virgesmith merged 6 commits into
mainfrom
feat/issue-18-enhancements
Jul 6, 2026
Merged

Issue #18 enhancements: O(1) peek + next_if, Rust-staple methods, repeat(1) fix#19
virgesmith merged 6 commits into
mainfrom
feat/issue-18-enhancements

Conversation

@virgesmith

@virgesmith virgesmith commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Implements the remaining suggestions from #18 (item 1, value_counts via Counter, was already done in #17), plus a next_if method.

peek (#18 item 2): measured, and left tee-based

The suggested lookahead buffer was implemented two ways (an always-on _Peekable wrapper, then a lazy variant with unpeek()) and benchmarked — and then removed, because the premise of the suggestion turns out to be false in CPython: itertools.tee() re-tees an already-teed iterator via its cheap __copy__ rather than nesting, so repeated peek() calls are already flat and ~constant time (measured linear at ~0.22 µs/peek over 160k peeks). The buffer only bought ~2x on rare peek-heavy loops, while the always-on form cost 2.5–7.5x on ordinary pipelines (every item paid a Python-level __next__ instead of staying on the C iterator fast path) and the lazy form kept the machinery plus a public unpeek() API for marginal benefit. See the commit history for both variants and the revert rationale.

next_if

next_if(predicate) consumes and returns the next item only if it satisfies the predicate; otherwise the item stays in place (a subsequent peek/next sees it) and None is returned (also None on exhaustion) — mirroring Rust's Peekable::next_if. It is built on peek + next and needs no buffer.

Rust-trait staples (#18 item 3)

  • sum() / prod() — terminal aggregations (0 / 1 on empty input)
  • dedup() — lazy removal of adjacent duplicates; works on infinite iterators and unhashable items
  • zip_longest(other, fillvalue=...) — like zip but pads the shorter input
  • sorted_by(key, reverse=...) — eager stable sort by key

repeat(1) aliasing (#18 item 4)

Breaking: repeat(1) now returns a new Itr and leaves the original exhausted, consistent with every other n (previously it returned self).

Dev tooling

Dev dependencies are unpinned back to ranges: ty>=0.0.56, ruff>=0.15.20, pytest>=9.1.1.

All quality gates pass: ruff check/format, ty, pytest (181 tests, 100% coverage). README, apidoc and relnotes updated.

Closes #18.

🤖 Generated with Claude Code

virgesmith and others added 4 commits July 5, 2026 09:21
…eat(1) fix

- peek() now uses a single-item lookahead buffer (like Rust's Peekable)
  instead of stacking an itertools.tee layer per call, making repeated
  peeks O(1). Adaptors flush the buffered item back onto the underlying
  iterator before consuming it, so nothing is lost. The flush helper is
  a module-level function because ty 0.0.37 mis-infers generics at some
  call sites when it is a method or property.
- next_if(predicate): consume the next item only if it satisfies the
  predicate, else leave it peekable and return None (Peekable::next_if).
- New methods: sum(), prod(), dedup(), zip_longest(other, fillvalue=...),
  sorted_by(key, reverse=...).
- repeat(1) no longer returns self; it behaves like every other n,
  returning a new Itr and leaving the original exhausted.

Closes #18.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The peek buffer now lives in a private _Peekable iterator wrapper
(mirroring Rust, where Peekable is its own adapter type) instead of on
Itr itself. Since the buffered item is yielded by _Peekable.__next__,
adaptors consuming self._it automatically see the full sequence and no
flush accessor is needed - removing the module-level _flushed() helper
that existed only because ty mis-infers generics when the accessor is
a method or property (still broken in ty 0.0.56, but now irrelevant).

Dev deps are ranges again instead of the ty==0.0.37 pin: ty>=0.0.56,
ruff>=0.15.20, pytest>=9.1.1. All gates pass on the new versions.
Also ignore .claude/ in .gitignore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrapping every Itr's source in _Peekable unconditionally put a Python-level
__next__ frame between every stage of a pipeline, costing 2.5-7.5x on bulk
operations vs the raw C iterator path. The buffer is now installed only on
the first peek()/next_if() call, restoring parity with the pre-_Peekable
performance for pipelines that never peek.

For iterators that have been peeked, the new unpeek() method removes the
buffer again (prepending any pending item via itertools.chain, so nothing
is lost), recovering full iteration speed when no further lookahead is
needed.

_Peekable also gains __slots__.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Supersedes both the eager _Peekable wrapper and the lazy-wrap/unpeek
variant, following the benchmark discussion on the PR: the premise of
issue #18 item 2 is false in CPython, because itertools.tee() re-tees
an already-teed iterator via its cheap __copy__ rather than nesting.
Repeated peeks on the tee-based implementation are already flat and
~constant time (measured linear at ~0.22us/peek over 160k peeks), so
the buffer only bought ~2x on rare peek-heavy loops while adding
machinery, a public unpeek() API, and (in the eager form) a 2.5-7.5x
penalty on ordinary pipelines.

peek() is copy().next() again, next_if is built on peek + next with
identical semantics, and unpeek()/_Peekable/_MISSING are removed. All
other #18 enhancements are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
virgesmith and others added 2 commits July 6, 2026 08:20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@virgesmith virgesmith merged commit b4dc4e4 into main Jul 6, 2026
9 checks passed
@virgesmith virgesmith deleted the feat/issue-18-enhancements branch July 6, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement suggestions from repo review

1 participant