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
2 changes: 1 addition & 1 deletion .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: "Setup uv ${{ matrix.python-version }} / ${{ matrix.os }}"
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57
uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Build
Expand Down
36 changes: 10 additions & 26 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,27 @@
name: Publish to PyPI

on:
workflow_run:
workflows: [ "Lint and Test" ]
types:
- completed
branches:
- main
push:
tags:
# Publish on any tag starting with a `v`, e.g., v1.2.3
- v*

jobs:
deploy:
name: Publish to PyPI
runs-on: ubuntu-latest

# Optional: Define an environment for better secret management and auditing
# You would need to create an environment named 'pypi' in your repository settings
# and link the PYPI_API_TOKEN secret to it.
# environment: pypi

environment:
name: pypi
permissions:
# Required for pypa/gh-action-pypi-publish if using OIDC trusted publishing
# (highly recommended for security, but requires setup on PyPI side).
# If not using OIDC, 'id-token: write' is not strictly necessary for the action,
# but 'contents: read' is usually needed for checkout.
# id-token: write
contents: read # Required to checkout the repository code

id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Install prerequisites
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57
uses: astral-sh/setup-uv@v8.1.0
- name: Install project
run: uv sync --all-groups
- name: Build wheel
run: uv build --wheel
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
# This action automatically uses '__token__' as username
password: ${{ secrets.PYPI_API_TOKEN }}
# You can specify a different repository if needed (e.g., TestPyPI for testing)
# repository-url: https://test.pypi.org/legacy/
run: uv publish
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
repos:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
Expand Down
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "itrx"
version = "0.2.2"
version = "0.2.3"
description = "A chainable iterator adapter"
readme = "README.md"
authors = [
Expand Down Expand Up @@ -29,8 +29,8 @@ dev = [
"pre-commit>=4.5.1",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"ruff>=0.12.2",
"ty>=0.0.31",
"ruff>=0.15.13",
"ty==0.0.37",
]

[project.optional-dependencies]
Expand All @@ -47,6 +47,9 @@ testpaths = [
]
addopts = "--cov=src/itrx --cov-report html --cov-fail-under=100 --doctest-modules --doctest-glob=README.md"

[tool.ty.src]
exclude = ["doc/"]

[tool.ruff]
line-length = 120

Expand Down
57 changes: 31 additions & 26 deletions src/itrx/itr.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import itertools
from collections import deque
from collections.abc import Callable, Generator, Iterable, Iterator
from typing import TypeVar, overload
from typing import Any, TypeVar, cast, overload

T = TypeVar("T")
_CollectT = TypeVar("_CollectT") # General item type for collected containers
Expand Down Expand Up @@ -95,7 +95,7 @@ def batched(self, n: int) -> "Itr[tuple[T, ...]]":
>>> list(Itr(range(7)).batched(3))
[(0, 1, 2), (3, 4, 5), (6,)]
"""
return Itr(itertools.batched(self._it, n))
return cast("Itr[tuple[T, ...]]", Itr(itertools.batched(self._it, n)))

def chain[U](self, other: Iterable[U]) -> "Itr[T | U]":
"""Chain this iterator with another iterable, yielding all items from self followed by all items from other.
Expand All @@ -107,7 +107,8 @@ def chain[U](self, other: Iterable[U]) -> "Itr[T | U]":
Itr[T | U]: A new iterator yielding items from both iterables.

"""
return Itr(itertools.chain(self._it, other)) # ty: ignore[invalid-argument-type, invalid-return-type]

return cast("Itr[T | U]", Itr(itertools.chain(self._it, other)))

@overload
def collect(self, container: type[tuple[T, ...]] = tuple) -> tuple[T, ...]: ...
Expand Down Expand Up @@ -178,7 +179,7 @@ def enumerate(self, *, start: int = 0) -> "Itr[tuple[int, T]]":
Itr[tuple[int, T]]: An iterator of (index, item) pairs.

"""
return Itr(enumerate(self._it, start))
return cast("Itr[tuple[int, T]]", Itr(enumerate(self._it, start)))

def filter(self, predicate: Predicate[T]) -> "Itr[T]":
"""Yield only items that satisfy the predicate.
Expand Down Expand Up @@ -228,7 +229,7 @@ def flatten[U](self) -> "Itr[U]":
Itr[U]: An iterator over the flattened items.

"""
return Itr(itertools.chain.from_iterable(self._it)) # ty: ignore[invalid-argument-type]
return Itr(itertools.chain.from_iterable(cast("Iterable[Iterable[U]]", self._it)))

def fold[U](self, init: U, func: Callable[[U, T], U]) -> U:
"""Reduce the iterator to a single value using a function and an initial value.
Expand Down Expand Up @@ -266,7 +267,9 @@ def groupby[U](self, grouper: Callable[[T], U]) -> "Itr[tuple[U, tuple[T,...]]]"
Itr[tuple[U, tuple[T,...]]]: An iterator over the keys and tuples of values

"""
return Itr(itertools.groupby(sorted(self._it, key=grouper), key=grouper)).map(lambda g: (g[0], tuple(g[1]))) # ty: ignore[no-matching-overload]
key_fn = cast("Callable[[T], Any]", grouper)
groups = ((k, tuple(v)) for k, v in itertools.groupby(sorted(self._it, key=key_fn), key=key_fn))
return cast("Itr[tuple[U, tuple[T, ...]]]", Itr(groups))

def inspect(self, func: Callable[[T], None]) -> "Itr[T]":
"""
Expand Down Expand Up @@ -313,9 +316,9 @@ def intersperser(item: U) -> Generator[T | U, None, None]:
current = next(self._it)
yield item
except StopIteration:
return None
return

return Itr(intersperser(item)) # ty: ignore[invalid-return-type]
return cast("Itr[T | U]", Itr(intersperser(item)))

def interleave[U](self, other: Iterable[U]) -> "Itr[T | U]":
"""
Expand All @@ -335,14 +338,17 @@ def interleave[U](self, other: Iterable[U]) -> "Itr[T | U]":
list(result) # [1, 2, 3, 4, 5, 6]
"""

return Itr(self.zip(other).flatten()) # ty: ignore[invalid-return-type]
return cast("Itr[T | U]", Itr(self.zip(other).flatten()))

def last(self) -> T | None:
def last(self) -> T:
"""Return the last item from the iterator. Do not use on an open-ended Iterable

Returns:
T: The last item.

Raises:
ValueError: If the iterator is empty.

"""
*_, last_item = self._it
return last_item
Expand Down Expand Up @@ -384,37 +390,35 @@ def map_while[U](self, predicate: Predicate[T], mapper: Callable[[T], U]) -> "It
"""
return Itr(map(mapper, itertools.takewhile(predicate, self._it)))

def max[U](self, key: Callable[[T], object] | None = None) -> object:
def max(self, key: Callable[[T], Any] | None = None) -> T:
"""
Return the maximum element from the iterator, optionally using a key function.

Args:
key (Callable[[T], object] | None, optional): A function to extract a comparison key from each element. Defaults to None.
key (Callable[[T], Any] | None, optional): A function to extract a comparison key from each element. Defaults to None.

Returns:
T: The maximum element in the iterator.

Raises:
ValueError: If the iterator is empty.
"""
# TODO T or the return type of key should have a "comparable" bound
return max(self._it, key=key) # ty: ignore[no-matching-overload]
return max(self._it, key=key)

def min(self, key: Callable[[T], object] | None = None) -> object:
def min(self, key: Callable[[T], Any] | None = None) -> T:
"""
Return the minimum element from the iterator, optionally using a key function.

Args:
key (Callable[[T], object] | None, optional): A function to extract a comparison key from each element. Defaults to None.
key (Callable[[T], Any] | None, optional): A function to extract a comparison key from each element. Defaults to None.

Returns:
T: The minimum element in the iterator.

Raises:
ValueError: If the iterator is empty.
"""
# TODO T or the return type of key should have a "comparable" bound
return min(self._it, key=key) # ty: ignore[no-matching-overload]
return min(self._it, key=key)

def next(self) -> T:
"""Return the next item from the iterator, if available. Otherwise raises StopIteration
Expand Down Expand Up @@ -451,6 +455,8 @@ def nth(self, n: int) -> T:
ValueError: if n < 1

"""
if n < 1:
raise ValueError(f"nth index must be >= 1, got {n}")
return self.skip(n - 1).next()

def pairwise(self) -> "Itr[tuple[T, T]]":
Expand All @@ -463,7 +469,7 @@ def pairwise(self) -> "Itr[tuple[T, T]]":
Itr[tuple[T, T]]: An iterator over consecutive pairs from the original iterable.

"""
return Itr(itertools.pairwise(self._it))
return cast("Itr[tuple[T, T]]", Itr(itertools.pairwise(self._it)))

def partition(self, predicate: Predicate[T]) -> tuple["Itr[T]", "Itr[T]"]:
"""
Expand Down Expand Up @@ -517,7 +523,7 @@ def product[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]":
Returns:
Itr[tuple[T, U]]: Iterator of 2-tuples with elements from each input iterator.
"""
return Itr(itertools.product(self._it, other))
return cast("Itr[tuple[T, U]]", Itr(itertools.product(self._it, other)))

def reduce(self, func: Callable[[T, T], T]) -> T:
"""Reduce the iterator to a single value using a function.
Expand Down Expand Up @@ -570,7 +576,7 @@ def rolling(self, n: int) -> "Itr[tuple[T, ...]]":

iterators = itertools.tee(self._it, n)
shifted_iterators = (itertools.islice(it, i, None) for i, it in enumerate(iterators))
return Itr(zip(*shifted_iterators, strict=False))
return cast("Itr[tuple[T, ...]]", Itr(zip(*shifted_iterators, strict=False)))

def skip(self, n: int) -> "Itr[T]":
"""Skip the next n items in the iterator.
Expand Down Expand Up @@ -611,7 +617,7 @@ def starmap[U](self, func: Callable[..., U]) -> "Itr[U]":
>>> list(itr.starmap(lambda x, y: x + y))
[3, 7]
"""
return Itr(itertools.starmap(func, self._it)) # ty: ignore[invalid-argument-type]
return Itr(itertools.starmap(func, cast("Iterable[Iterable[Any]]", self._it)))

def step_by(self, n: int) -> "Itr[T]":
"""Yield every n-th item from the iterator.
Expand Down Expand Up @@ -688,7 +694,7 @@ def tee(self, n: int = 2) -> tuple["Itr[T]", ...]:
"""
return tuple(Itr(t) for t in itertools.tee(self._it, n))

def unzip[U, V](self) -> tuple["Itr[U]", "Itr[V]"]:
def unzip[U, V](self: "Itr[tuple[U, V]]") -> tuple["Itr[U]", "Itr[V]"]:
"""Splits the iterator of pairs into two separate iterators, each containing the elements from one position of
the pairs.

Expand All @@ -701,9 +707,8 @@ def unzip[U, V](self) -> tuple["Itr[U]", "Itr[V]"]:
and then maps over each to extract the respective elements.

"""
# TODO express that T is tuple[U, V]
it1, it2 = itertools.tee(self._it, 2)
return Itr(x[0] for x in it1), Itr(x[1] for x in it2) # type: ignore[index]
return Itr(x[0] for x in it1), Itr(x[1] for x in it2)

def value_counts(self) -> "Itr[tuple[T, int]]":
"""
Expand All @@ -727,4 +732,4 @@ def zip[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]":
Itr[tuple[T, U]]: An iterator of paired items.

"""
return Itr(zip(self._it, other, strict=False))
return cast("Itr[tuple[T, U]]", Itr(zip(self._it, other, strict=False)))
2 changes: 1 addition & 1 deletion src/test/test_transform_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_flat_map_empty() -> None:
def test_flat_map_invalid_mapper() -> None:
# mapper must return an iterable
with pytest.raises(TypeError):
Itr([1, 2, 3]).flat_map(lambda n: n * 2).collect() # type: ignore[arg-type, return-value]
Itr([1, 2, 3]).flat_map(lambda n: n * 2).collect() # ty: ignore[invalid-argument-type]


def test_map() -> None:
Expand Down
Loading
Loading