diff --git a/.gitignore b/.gitignore index b18f122..16ac5e5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ coverage.info .python-version .coverage -htmlcov/ \ No newline at end of file +htmlcov/ +.claude \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1887c5a..503b792 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/README.md b/README.md index dc1a770..b7cdbaa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/apidoc.md b/doc/apidoc.md index 2401bda..85b9a6b 100644 --- a/doc/apidoc.md +++ b/doc/apidoc.md @@ -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 @@ -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 @@ -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. @@ -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` @@ -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` @@ -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` @@ -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. @@ -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 @@ -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, '-')) + diff --git a/pyproject.toml b/pyproject.toml index 1ddd434..081b916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "itrx" -version = "0.3.0" +version = "0.4.0" description = "A chainable iterator adapter" readme = "README.md" authors = [ @@ -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] diff --git a/relnotes.md b/relnotes.md index 53a4477..a8b5af8 100644 --- a/relnotes.md +++ b/relnotes.md @@ -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 diff --git a/src/itrx/itr.py b/src/itrx/itr.py index cd1ad2e..b56f243 100644 --- a/src/itrx/itr.py +++ b/src/itrx/itr.py @@ -1,4 +1,5 @@ import itertools +import math from collections import Counter, deque from collections.abc import Callable, Generator, Iterable, Iterator from typing import Any, TypeVar, cast, overload @@ -50,9 +51,7 @@ def accumulate(self, func: Callable[[T, T], T] | None = None, *, initial: T | No >>> list(Itr([2, 3, 4]).accumulate(lambda x, y: x * y)) [2, 6, 24] """ - return Itr( - itertools.accumulate(self._it, func, initial=initial) - ) # if func else itertools.accumulate(self._it)) + return Itr(itertools.accumulate(self._it, func, initial=initial)) def all(self, predicate: Predicate[T]) -> bool: """Return True if all elements in the iterator satisfy the predicate. @@ -172,6 +171,21 @@ def cycle(self) -> "Itr[T]": """ return Itr(itertools.cycle(self._it)) + def dedup(self) -> "Itr[T]": + """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) + """ + return Itr(k for k, _ in itertools.groupby(self._it)) + def enumerate(self, *, start: int = 0) -> "Itr[tuple[int, T]]": """Yield pairs of (index, item) for each item in the iterator, where index starts at 0 or the value provided @@ -477,6 +491,37 @@ def next_chunk(self, n: int) -> tuple[T, ...]: """ return self.take(n).collect() + def next_if(self, predicate: Predicate[T]) -> T | None: + """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 + """ + try: + item = self.peek() + except StopIteration: + return None + if predicate(item): + return next(self._it) + return None + def nth(self, n: int) -> T: """Return the n-th item (0-based) from the iterator, consuming the preceding items. @@ -531,9 +576,21 @@ def peek(self) -> T: 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 """ return self.copy().next() @@ -552,6 +609,20 @@ def position(self, predicate: Predicate[T]) -> int: """ return self.enumerate().skip_while(lambda x: not predicate(x[1])).next()[0] + def prod(self) -> T: + """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 + """ + return cast("T", math.prod(cast("Iterable[Any]", self._it))) + def product[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]": """ Creates a new iterator over the cartesian product of self and the other iterator @@ -590,8 +661,6 @@ def repeat(self, n: int) -> "Itr[T]": Note: This implementation creates `n` independent iterators using `itertools.tee`, which may be inefficient for large `n` or large input iterators. """ - if n == 1: - return self # this creates n iterators so may be inefficient return Itr(itertools.chain(*itertools.tee(self._it, n))) @@ -641,6 +710,25 @@ def skip_while(self, predicate: Predicate[T]) -> "Itr[T]": """ return Itr(itertools.dropwhile(predicate, self._it)) + def sorted_by(self, key: Callable[[T], Any], *, reverse: bool = False) -> "Itr[T]": + """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') + """ + return Itr(sorted(self._it, key=key, reverse=reverse)) + def starmap[U](self, func: Callable[..., U]) -> "Itr[U]": """ Applies a function to the elements of the iterator, unpacking the elements as arguments. @@ -670,6 +758,20 @@ def step_by(self, n: int) -> "Itr[T]": """ return Itr(itertools.islice(self._it, 0, None, n)) + def sum(self) -> T: + """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 + """ + return cast("T", sum(cast("Iterable[Any]", self._it))) + def take(self, n: int) -> "Itr[T]": """Return an iterator over the next n items from the iterator. @@ -717,7 +819,7 @@ def tee(self, n: int = 2) -> tuple["Itr[T]", ...]: 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 @@ -780,3 +882,27 @@ def zip[U](self, other: Iterable[U]) -> "Itr[tuple[T, U]]": """ return cast("Itr[tuple[T, U]]", Itr(zip(self._it, other, strict=False))) + + def zip_longest[U, V]( + self, other: Iterable[U], *, fillvalue: V | None = None + ) -> "Itr[tuple[T | V | None, U | V | None]]": + """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, '-')) + """ + return cast( + "Itr[tuple[T | V | None, U | V | None]]", + Itr(itertools.zip_longest(self._it, other, fillvalue=fillvalue)), + ) diff --git a/src/test/test_aggregation.py b/src/test/test_aggregation.py index ba1ec49..5848fe1 100644 --- a/src/test/test_aggregation.py +++ b/src/test/test_aggregation.py @@ -185,3 +185,32 @@ def test_value_counts_consumes_iterator() -> None: _ = itr.value_counts() with pytest.raises(StopIteration): itr.next() + + +def test_sum() -> None: + assert Itr([1, 2, 3]).sum() == 6 + assert Itr([1.5, 2.5]).sum() == 4.0 + + +def test_sum_empty() -> None: + assert Itr[int]([]).sum() == 0 + + +def test_sum_consumes_iterator() -> None: + it = Itr([1, 2, 3]) + assert it.sum() == 6 + with pytest.raises(StopIteration): + it.next() + + +def test_prod() -> None: + assert Itr([2, 3, 4]).prod() == 24 + assert Itr([0.5, 4.0]).prod() == 2.0 + + +def test_prod_empty() -> None: + assert Itr[int]([]).prod() == 1 + + +def test_prod_with_zero() -> None: + assert Itr([1, 0, 5]).prod() == 0 diff --git a/src/test/test_collection.py b/src/test/test_collection.py index f655a5a..55b02e7 100644 --- a/src/test/test_collection.py +++ b/src/test/test_collection.py @@ -87,3 +87,69 @@ def test_peek_on_empty_iterator_raises() -> None: it = Itr[bool]([]) with pytest.raises(StopIteration): it.peek() + + +def test_peek_repeated_is_stable() -> None: + it = Itr([1, 2]) + assert it.peek() == 1 + assert it.peek() == 1 + assert it.next() == 1 + assert it.peek() == 2 + + +def test_peek_repeated_does_not_degrade() -> None: + # tee re-tees an already-teed iterator without nesting, so many peeks stay cheap and lossless + it = Itr([1, 2]) + for _ in range(10000): + assert it.peek() == 1 + assert it.collect() == (1, 2) + + +def test_peek_handles_none_values() -> None: + it = Itr([None, 1]) + assert it.peek() is None + assert it.next() is None + assert it.next() == 1 + + +def test_peeked_item_seen_by_adaptors() -> None: + # the buffered item must be pushed back before any adaptor consumes the underlying iterator + it = Itr([1, 2, 3]) + assert it.peek() == 1 + assert it.map(lambda x: x * 10).collect() == (10, 20, 30) + + +def test_peeked_item_seen_by_copy() -> None: + it = Itr([1, 2, 3]) + assert it.peek() == 1 + copied = it.copy() + assert copied.collect() == (1, 2, 3) + assert it.collect() == (1, 2, 3) + + +def test_peeked_item_seen_by_for_loop() -> None: + it = Itr([1, 2, 3]) + assert it.peek() == 1 + assert list(it) == [1, 2, 3] + + +def test_next_if_consumes_on_match() -> None: + it = Itr([1, 2, 3]) + assert it.next_if(lambda x: x < 3) == 1 + assert it.next_if(lambda x: x < 3) == 2 + assert it.next_if(lambda x: x < 3) is None + # the non-matching item is not consumed + assert it.next() == 3 + + +def test_next_if_exhausted_returns_none() -> None: + it = Itr[int]([]) + assert it.next_if(lambda _: True) is None + + +def test_next_if_after_peek() -> None: + it = Itr([5, 6]) + assert it.peek() == 5 + assert it.next_if(lambda x: x == 5) == 5 + assert it.next_if(lambda x: x == 5) is None + assert it.peek() == 6 diff --git a/src/test/test_combine_split.py b/src/test/test_combine_split.py index f0d5209..f7fbb61 100644 --- a/src/test/test_combine_split.py +++ b/src/test/test_combine_split.py @@ -170,6 +170,15 @@ def test_repeat_one() -> None: assert it.collect() == (4, 5) +def test_repeat_one_returns_new_itr() -> None: + # repeat(1) behaves like any other n: a new Itr is returned and the original is left exhausted + it = Itr([4, 5]) + repeated = it.repeat(1) + assert repeated is not it + assert repeated.collect() == (4, 5) + assert it.collect() == () + + def test_repeat_empty_iterable() -> None: it: Itr[int] = Itr([]).repeat(3) # Repeating an empty iterable should yield nothing @@ -422,3 +431,19 @@ def test_tee_iterators_are_distinct_objects() -> None: # ensure both still yield the same items assert list(a) == [7, 8, 9] assert list(b) == [7, 8, 9] + + +def test_zip_longest_equal_length() -> None: + assert Itr([1, 2]).zip_longest("ab").collect() == ((1, "a"), (2, "b")) + + +def test_zip_longest_left_longer() -> None: + assert Itr([1, 2, 3]).zip_longest("a").collect() == ((1, "a"), (2, None), (3, None)) + + +def test_zip_longest_right_longer_with_fillvalue() -> None: + assert Itr([1]).zip_longest("abc", fillvalue=0).collect() == ((1, "a"), (0, "b"), (0, "c")) + + +def test_zip_longest_both_empty() -> None: + assert Itr[int]([]).zip_longest([]).collect() == () diff --git a/src/test/test_transform_filter.py b/src/test/test_transform_filter.py index 5eab93d..61bd33a 100644 --- a/src/test/test_transform_filter.py +++ b/src/test/test_transform_filter.py @@ -206,3 +206,41 @@ def test_chunk_by_lazy_on_infinite() -> None: # chunk_by is lazy, so it works on unbounded iterators counts = Itr(itertools.count()).chunk_by(lambda n: n // 2).take(3).collect() assert counts == ((0, (0, 1)), (1, (2, 3)), (2, (4, 5))) + + +def test_dedup() -> None: + assert Itr([1, 1, 2, 2, 2, 3, 1]).dedup().collect() == (1, 2, 3, 1) + + +def test_dedup_no_duplicates() -> None: + assert Itr([1, 2, 3]).dedup().collect() == (1, 2, 3) + + +def test_dedup_empty() -> None: + assert Itr[int]([]).dedup().collect() == () + + +def test_dedup_unhashable_items() -> None: + assert Itr([[1], [1], [2]]).dedup().collect() == ([1], [2]) + + +def test_dedup_lazy_on_infinite() -> None: + it = Itr(itertools.count()).flat_map(lambda x: (x, x)).dedup() + assert it.take(3).collect() == (0, 1, 2) + + +def test_sorted_by() -> None: + assert Itr(["ccc", "a", "bb"]).sorted_by(len).collect() == ("a", "bb", "ccc") + + +def test_sorted_by_reverse() -> None: + assert Itr([1, 3, 2]).sorted_by(lambda x: x, reverse=True).collect() == (3, 2, 1) + + +def test_sorted_by_is_stable() -> None: + data = [(1, "b"), (0, "a"), (1, "a"), (0, "b")] + assert Itr(data).sorted_by(lambda x: x[0]).collect() == ((0, "a"), (0, "b"), (1, "b"), (1, "a")) + + +def test_sorted_by_empty() -> None: + assert Itr[int]([]).sorted_by(lambda x: x).collect() == () diff --git a/uv.lock b/uv.lock index e227335..2207d13 100644 --- a/uv.lock +++ b/uv.lock @@ -323,7 +323,7 @@ wheels = [ [[package]] name = "itrx" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } [package.optional-dependencies] @@ -347,10 +347,10 @@ provides-extras = ["examples"] [package.metadata.requires-dev] dev = [ { name = "pre-commit", specifier = ">=4.5.1" }, - { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-cov", specifier = ">=6.2.1" }, - { name = "ruff", specifier = ">=0.15.13" }, - { name = "ty", specifier = "==0.0.37" }, + { name = "ruff", specifier = ">=0.15.20" }, + { name = "ty", specifier = ">=0.0.56" }, ] [[package]] @@ -566,7 +566,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -575,9 +575,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -710,27 +710,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, - { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, - { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, - { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, - { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, - { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, - { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, - { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, - { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, - { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -784,27 +784,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.37" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/c3/60bc4829e0c1a8ff80b592067e1185a7b5ea64608acb0c676c44d5137d52/ty-0.0.37.tar.gz", hash = "sha256:f873f69627bd7f4ef8d57f716c63e5c63d7d1b7327ab3de185c7287a75223011", size = 5655422, upload-time = "2026-05-16T05:57:21.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/fe/180dd6914f9db33ad0200fbeaa429dd1fb0a4e6d98320dc1775f100a91af/ty-0.0.37-py3-none-linux_armv6l.whl", hash = "sha256:66cf7310189856e15f690559ddf37735476d2644db917d92f7cef13e5c834adf", size = 11246028, upload-time = "2026-05-16T05:57:41.744Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a2/fa0cfd31467ad99b2db8c81ee9e2b4574589974a3eb9723be825e15b300c/ty-0.0.37-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2048f3c44ee6c7dde6e0ca064f99c6cada8f6de8ccdcfad2d856a429f8a4ac82", size = 11001460, upload-time = "2026-05-16T05:57:35.27Z" }, - { url = "https://files.pythonhosted.org/packages/10/3f/db60ba9be8b95a464ece0ba103e534047c34b49fee12f5e101f83f8d66db/ty-0.0.37-py3-none-macosx_11_0_arm64.whl", hash = "sha256:32c7b9b5b626aacdec334b44a2698e5f7b80df55bf7338267084d00d4b9546b3", size = 10446549, upload-time = "2026-05-16T05:57:37.252Z" }, - { url = "https://files.pythonhosted.org/packages/56/6f/11dd7174b20ebcb37a3d3b68f60b3940e37e4356e0accd03e2d7f9f70690/ty-0.0.37-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9fba1bebccf1e656bc5e3787acc5a191c491041ee4d12fe8fe2eff64e7b190d", size = 10961016, upload-time = "2026-05-16T05:57:16.394Z" }, - { url = "https://files.pythonhosted.org/packages/65/dd/3c17ce2860c525817c42c82d7075391b1f5615d36c03aa2d26647a224e8a/ty-0.0.37-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f987c5fb59aa5017ee8e8c5b57a07390f584e58e572255acd0fa44b3e0b238df", size = 11022093, upload-time = "2026-05-16T05:57:32.741Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a8/e7a40b0b57660921dd3482d219add963973b52ae8507abd88f48439704b5/ty-0.0.37-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4168f53146e7a3f52560ff433f238352591c9b1a9ed09397fbb776ddef4f89c", size = 11486333, upload-time = "2026-05-16T05:57:18.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/2c406b98244bc1ad42afdd35f466bcef88664210957dcbb5172254ff2462/ty-0.0.37-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e487eafdb80a48223ce68a01f9287528216ffe0126d1629ff11e4f7c1dd3cf", size = 12093526, upload-time = "2026-05-16T05:57:04.456Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3c/5c492a38e1b21a26370727dd4b77a53f05262e53e3be232047f22e7fa1b3/ty-0.0.37-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b49f388d063668676daaa7eef57385089d1b844279c0185bd84d4dbc3bcede6", size = 11725957, upload-time = "2026-05-16T05:57:23.356Z" }, - { url = "https://files.pythonhosted.org/packages/b2/00/8a3d9ba265cd0582342c14e4980cc0351aaaa45c6305712d398c9e2446c7/ty-0.0.37-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b96bfc1cc725d9d859abef4e3aa32a6da0f7472eaaafae2d9a6cffd729c7c61", size = 11610336, upload-time = "2026-05-16T05:57:27.888Z" }, - { url = "https://files.pythonhosted.org/packages/91/4b/6ee172935cb842f5c1553b0d37215b45e9dde05a4c74fdb47fd271907122/ty-0.0.37-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c55f39b519107cf234b794718793e11793c055e89028a282a309f690def48117", size = 11797856, upload-time = "2026-05-16T05:57:11.109Z" }, - { url = "https://files.pythonhosted.org/packages/34/ef/75a7425bf9fe74483404ff11a8cbe3aa307354e0801697d6063384157776/ty-0.0.37-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c79204350de060a077bff7f027a1d53e216cad147d826ec9862be0af2f9c3c1e", size = 10941848, upload-time = "2026-05-16T05:57:30.653Z" }, - { url = "https://files.pythonhosted.org/packages/e0/2c/7ea9dccd55961375067f99ed00fb8eabb491f6a06d0e5f09c797d2b900a6/ty-0.0.37-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:49a21b4dcb2cd94cd0298c96dfb71a2dd25f08bf7e6eefd0c33c519d058908c6", size = 11058248, upload-time = "2026-05-16T05:57:01.785Z" }, - { url = "https://files.pythonhosted.org/packages/98/d7/848fde96c6610b2b1fd75823d44d8977a4525c4397f27332f054ccd6cf9c/ty-0.0.37-py3-none-musllinux_1_2_i686.whl", hash = "sha256:119332095c5974fe1dabfe4fd00c6759eeec5b99f7d7a80b2833feee5a58abdb", size = 11168423, upload-time = "2026-05-16T05:57:39.297Z" }, - { url = "https://files.pythonhosted.org/packages/29/11/c1613ac4b64357b9067df68bac97bcb458cc426cd468a2782847238c539b/ty-0.0.37-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ac5dc593675414f68862c2f71cc04912b0e5ec5520a9c49fc71ed79205b95c33", size = 11698565, upload-time = "2026-05-16T05:57:14.206Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ac/961205863903881996adb5a6f9cfe570c132882922ac226540346f15df20/ty-0.0.37-py3-none-win32.whl", hash = "sha256:33b57e4095179f06c2ae01c334833645cad94bf7d7467e073cdc3aaabea565d3", size = 10518308, upload-time = "2026-05-16T05:57:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/39/cd/f308edd0cd86e402fe3a1b5c54e0a0dfa0177d80c1557c4849510bb2a147/ty-0.0.37-py3-none-win_amd64.whl", hash = "sha256:3b159351e99cf6eed7aacfb69ae8437725d15599ac4f21c8b2e909b300498b6c", size = 11607159, upload-time = "2026-05-16T05:57:06.76Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ed/5ec4b501479bc5dad55467e2fe72e797cb9c178468c0d1a514536872ebc5/ty-0.0.37-py3-none-win_arm64.whl", hash = "sha256:6c3c2b997f68c71e14242b96d48cba3c086439556af02bb4613aa458950d5c23", size = 10958817, upload-time = "2026-05-16T05:57:08.907Z" }, +version = "0.0.56" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, ] [[package]]