diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6974f86..73b3bc3 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -19,6 +19,6 @@ jobs: - run: uv sync - run: uv run ruff check pyenumerable test - run: uv run pyright pyenumerable test - - run: uv run pytest -v --cov=pyenumerable.implementations --cov-report term-missing --cov-report term:skip-covered + - run: uv run pytest -v --cov=pyenumerable.implementations --cov-report term-missing - run: uv build - run: uv publish --trusted-publishing always diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4a2abd..10b0555 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,4 +42,4 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: uv sync - - run: uv run pytest -v --cov=pyenumerable.implementations --cov-report term-missing --cov-report term:skip-covered + - run: uv run pytest -v --cov=pyenumerable.implementations --cov-report term-missing diff --git a/documentation.md b/documentation.md index 1d7d150..2cf9c84 100644 --- a/documentation.md +++ b/documentation.md @@ -4,7 +4,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ ## Architecture & Design -PyEnumerable follows a relatively simple architecture, mainly because there isn't any reason to do otherwise! +PyEnumerable follows a relatively simple architecture, mainly because there isn't any reason to do otherwise!
Extension methods defined by `IEnumerable` interface are grouped by their functionality under protocols located `pyenumerable.protocol` package; The main advantage provided by protocols over ABCs (abstract base classes) is the ability to define overloads w/ different signatures. ### Protocols @@ -23,7 +23,7 @@ A callable which accepts two arguments of type `TSource` & returns a `bool` valu #### `Enumerable` -This protocol consolidates all other protocols into a single one, allowing implementations to reference it instead of listing each individual protocol. This approach minimizes the risk of omitting any methods due to oversight. +This protocol consolidates all other protocols into a single one, allowing implementations to reference it instead of listing each individual protocol. This approach minimizes the risk of omitting any methods due to oversight.
It also enforces the presence of a property called `source` which can be used to access actual items inside an instance of a particular implementation. #### `Associable` @@ -242,8 +242,7 @@ assert one.group_join( two, lambda x: x, lambda point: point.y, - lambda x, - points: (x, points.source) + lambda x, points: (x, points.source) ).source == ( (1, Point(1, 1), Point(2, 1)), (2, Point(3, 2), Point(4, 2), Point(5, 2)) @@ -707,7 +706,8 @@ type parameters: #### `PurePythonEnumerable` -A basic implementation of Enumerable; Written without the assumption of `TSource` conforming to `collections.abc.Hashable` or being immutable; preserves order. +Basic implementation of `pyenumerable.Enumerable`; Assumes that `TSource` conforms to `collections.abc.Hashable` & is immutable.
+Violating this assumption may lead to unpredictable behaviour. usage: ```py diff --git a/pyenumerable/__init__.py b/pyenumerable/__init__.py index 8fa30a5..1a97d3a 100644 --- a/pyenumerable/__init__.py +++ b/pyenumerable/__init__.py @@ -9,7 +9,7 @@ __all__ = ["Enumerable", "PurePythonEnumerable", "pp_enumerable"] __author__ = "AmirHossein Ahmadi" __license__ = "WTFPL" -__version__ = "1.1.5" +__version__ = "2.0.0" __maintainer__ = "AmirHossein Ahmadi" __email__ = "amirthehossein@gmail.com" __documentation__ = "https://github.com/amirongit/PyEnumerable/blob/master/documentation.md" diff --git a/pyenumerable/implementation_utility.py b/pyenumerable/implementation_utility.py new file mode 100644 index 0000000..47d0275 --- /dev/null +++ b/pyenumerable/implementation_utility.py @@ -0,0 +1,9 @@ +from typing import Any + +from pyenumerable.protocol._enumerable import Enumerable + + +def assume_not_empty(instance: Enumerable[Any]) -> None: + if instance.count_() == 0: + msg = "Enumerable is empty" + raise ValueError(msg) diff --git a/pyenumerable/implementations/pure_python.py b/pyenumerable/implementations/pure_python.py index df0b4ec..bb49877 100644 --- a/pyenumerable/implementations/pure_python.py +++ b/pyenumerable/implementations/pure_python.py @@ -1,14 +1,22 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Sequence -from itertools import chain, islice -from typing import Any, Protocol +from collections.abc import Callable, Hashable, Iterable +from contextlib import suppress +from functools import cache +from itertools import chain +from pyenumerable.implementation_utility import assume_not_empty from pyenumerable.protocol import Associable, Enumerable from pyenumerable.typing_utility import Comparer -class PurePythonEnumerable[TSource](Enumerable[TSource]): +class PurePythonEnumerable[TSource: Hashable](Enumerable[TSource]): + """ + Basic implementation of `pyenumerable.Enumerable`; Assumes that `TSource` + conforms to `collections.abc.Hashable` & is immutable. + Violating this assumption may lead to unpredictable behaviour. + """ + def __init__( self, *items: TSource, @@ -28,6 +36,11 @@ def select[TResult]( selector: Callable[[int, TSource], TResult], /, ) -> Enumerable[TResult]: + """ + Uses `PurePythonEnumerable.__init__` + O(n) + """ + return PurePythonEnumerable( *tuple(selector(i, v) for i, v in enumerate(self.source)), ) @@ -37,6 +50,11 @@ def select_many[TResult]( selector: Callable[[int, TSource], Iterable[TResult]], /, ) -> Enumerable[TResult]: + """ + Uses `PurePythonEnumerable.__init__` + O(n) + """ + return PurePythonEnumerable( from_iterable=[selector(i, v) for i, v in enumerate(self.source)], ) @@ -46,6 +64,10 @@ def concat( other: Enumerable[TSource], /, ) -> Enumerable[TSource]: + """ + Wraps `PurePythonEnumerable.__init__` + """ + return PurePythonEnumerable(from_iterable=(self.source, other.source)) def max_( @@ -54,7 +76,17 @@ def max_( *, comparer: Comparer[TSource] | None = None, ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) + """ + If comparer is given + O(n) + Else + Assumes that `TSource` conforms + `pyenumerable.typing_utility.Comparable` + Wraps `builtins.max` + """ + + assume_not_empty(self) + if comparer is not None: out = self.source[0] for item in self.source[1:]: @@ -62,14 +94,7 @@ def max_( out = item return out - try: - return max(self.source) # type: ignore - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable" - ) - raise TypeError(msg) from te + return max(self.source) # type: ignore def max_by[TKey]( self, @@ -78,8 +103,20 @@ def max_by[TKey]( *, comparer: Comparer[TKey] | None = None, ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) + """ + Uses `builtins.enumerate` + If comparer is given + O(n) + Else + Assumes that `TKey` conforms to + `pyenumerable.typing_utility.Comparable` + Wraps `builtins.max` + """ + + assume_not_empty(self) + enumerated = enumerate(key_selector(i) for i in self.source) + if comparer is not None: max_key = next(iterable := iter(enumerated)) for index, key in iterable: @@ -87,13 +124,7 @@ def max_by[TKey]( max_key = (index, key) return self.source[max_key[0]] - try: - return self.source[max(enumerated, key=lambda e: e[1])[0]] # type: ignore - except TypeError as te: - msg = ( - "TKey doesn't implement pyenumerable.typing_utility.Comparable" - ) - raise TypeError(msg) from te + return self.source[max(enumerated, key=lambda e: e[1])[0]] # type: ignore def min_( self, @@ -101,7 +132,17 @@ def min_( *, comparer: Comparer[TSource] | None = None, ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) + """ + If comparer is given + O(n) + Else + Assumes that `TSource` conforms to + `pyenumerable.typing_utility.Comparable` + Wraps `builtins.min` + """ + + assume_not_empty(self) + if comparer is not None: out = self.source[0] for item in self.source[1:]: @@ -109,14 +150,7 @@ def min_( out = item return out - try: - return min(self.source) # type: ignore - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable" - ) - raise TypeError(msg) from te + return min(self.source) # type: ignore def min_by[TKey]( self, @@ -125,8 +159,20 @@ def min_by[TKey]( *, comparer: Comparer[TKey] | None = None, ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) + """ + Uses `builtins.enumerate` + If comparer is given + O(n) + Else + Assumes that `TKey` conforms to + `pyenumerable.typing_utility.Comparable` + Wraps `builtins.min` + """ + + assume_not_empty(self) + enumerated = enumerate(key_selector(i) for i in self.source) + if comparer is not None: min_key = next(iterable := iter(enumerated)) for index, key in iterable: @@ -134,13 +180,7 @@ def min_by[TKey]( min_key = (index, key) return self.source[min_key[0]] - try: - return self.source[min(enumerated, key=lambda e: e[1])[0]] # type: ignore - except TypeError as te: - msg = ( - "TKey doesn't implement pyenumerable.typing_utility.Comparable" - ) - raise TypeError(msg) from te + return self.source[min(enumerated, key=lambda e: e[1])[0]] # type: ignore def contains( self, @@ -149,6 +189,14 @@ def contains( *, comparer: Comparer[TSource] | None = None, ) -> bool: + """ + If comparer is given + Wraps `builtins.any` + O(n) + Else + Wraps `in` + """ + return ( (any(comparer(item, i) for i in self.source)) if comparer is not None @@ -160,6 +208,14 @@ def count_( predicate: Callable[[TSource], bool] | None = None, /, ) -> int: + """ + If predicate is given + Wraps `builtins.sum` + O(n) + Else + Wraps `builtins.len` + """ + return ( sum(1 for i in self.source if predicate(i)) if predicate is not None @@ -171,13 +227,18 @@ def single( predicate: Callable[[TSource], bool] | None = None, /, ) -> TSource: + """ + Uses `builtins.len` + If predicate is given + Uses `builtins.filter` + O(n) + """ + if ( len( - items := tuple( - filter(predicate, self.source), - ) + items := tuple(filter(predicate, self.source)) if predicate is not None - else self.source, + else self.source ) != 1 ): @@ -194,13 +255,18 @@ def single_or_deafult( predicate: Callable[[TSource], bool] | None = None, /, ) -> TSource: + """ + Uses `builtins.len` + If predicate is given + Uses `builtins.filter` + O(n) + """ + if ( length := len( - items := self.source - if predicate is None - else tuple( - filter(predicate, self.source), - ), + items := tuple(filter(predicate, self.source)) + if predicate is not None + else self.source, ) ) > 1: msg = ( @@ -217,6 +283,10 @@ def skip( end: int | None = None, /, ) -> Enumerable[TSource]: + """ + Wraps slicing + """ + return PurePythonEnumerable( *( self.source[:start_or_count] + self.source[end:] @@ -226,6 +296,10 @@ def skip( ) def skip_last(self, count: int, /) -> Enumerable[TSource]: + """ + Wraps slicing + """ + return PurePythonEnumerable(*self.source[:-count]) def skip_while( @@ -233,6 +307,10 @@ def skip_while( predicate: Callable[[int, TSource], bool], /, ) -> Enumerable[TSource]: + """ + O(n) + """ + start = 0 for index, item in enumerate(self.source): start = index @@ -248,15 +326,23 @@ def take( end: int | None = None, /, ) -> Enumerable[TSource]: + """ + Wraps slicing + """ + return PurePythonEnumerable( *( - islice(self.source, start_or_count, end) + self.source[start_or_count:end] if (end is not None) - else islice(self.source, start_or_count) + else self.source[:start_or_count] ), ) def take_last(self, count: int, /) -> Enumerable[TSource]: + """ + Wraps slicing + """ + return PurePythonEnumerable(*self.source[-count:]) def take_while( @@ -264,6 +350,10 @@ def take_while( predicate: Callable[[int, TSource], bool], /, ) -> Enumerable[TSource]: + """ + O(n) + """ + stop = 0 for index, item in enumerate(self.source): stop = index @@ -278,44 +368,66 @@ def of_type[TResult]( type_: type[TResult], /, ) -> Enumerable[TResult]: + """ + Wraps `builtins.filter` + """ + return PurePythonEnumerable( # type: ignore *filter(lambda i: isinstance(i, type_), self.source), ) - def all( + def all_( self, predicate: Callable[[TSource], bool] | None = None, /, ) -> bool: + """ + Wraps `builtins.all` + If predicate is given + O(n) + """ + return all( (predicate(i) for i in self.source) if (predicate is not None) else self.source, ) - def any( + def any_( self, predicate: Callable[[TSource], bool] | None = None, /, ) -> bool: + """ + Wraps `builtins.any` + If predicate is given + O(n) + """ + return any( (predicate(i) for i in self.source) if (predicate is not None) else self.source, ) - def sum(self, /) -> TSource: - try: - return sum(self.source) # type: ignore - except TypeError as te: - msg = "TSource can't be passed to bultins.sum" - raise TypeError(msg) from te + def sum_(self, /) -> TSource: + """ + Wraps `builtins.sum` + `TSource` should be usable as arugment of `builtins.sum` + """ + + return sum(self.source) # type: ignore def where( self, predicate: Callable[[int, TSource], bool], /, ) -> Enumerable[TSource]: + """ + Wraps `builtins.filter` + O(n) + """ + return PurePythonEnumerable( *( en[1] @@ -331,9 +443,17 @@ def prepend( element: TSource, /, ) -> Enumerable[TSource]: + """ + Wraps `PurePythonEnumerable.__init__` + """ + return PurePythonEnumerable(element, *self.source) def append(self, element: TSource, /) -> Enumerable[TSource]: + """ + Wraps `PurePythonEnumerable.__init__` + """ + return PurePythonEnumerable(*self.source, element) def distinct( @@ -342,8 +462,12 @@ def distinct( *, comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + If comparer is given + O(n^2) + Else + Wraps `builtins.dict.fromkeys` + """ if comparer is not None: out: list[TSource] = [] @@ -355,11 +479,7 @@ def distinct( out.append(item) return PurePythonEnumerable(*out) - try: - return PurePythonEnumerable(*dict.fromkeys(self.source).keys()) - except TypeError as te: - msg = "TSource doesn't implement __hash__; Comparer isn't given" - raise TypeError(msg) from te + return PurePythonEnumerable(*dict.fromkeys(self.source).keys()) def distinct_by[TKey]( self, @@ -368,28 +488,31 @@ def distinct_by[TKey]( *, comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + If comparer is given + O(n^2) + Else + Assumes that `TKey` conforms to `collections.abc.Hashable` + Uses `builtins.dict.fromkeys` + O(n) + """ + + captured_dict: dict[TKey, TSource] = {} if comparer is not None: - captured_list: list[TSource] = [] for item in self.source: - for captured in captured_list: - if comparer(key_selector(item), key_selector(captured)): + item_key = key_selector(item) + for captured_key in captured_dict: + if comparer(item_key, captured_key): break else: - captured_list.append(item) - return PurePythonEnumerable(*captured_list) - - try: - captured_dict: dict[TKey, TSource] = {} - for item in self.source: - if (k := key_selector(item)) not in captured_dict: - captured_dict[k] = item + captured_dict[item_key] = item return PurePythonEnumerable(*captured_dict.values()) - except TypeError as te: - msg = "TKey doesn't implement __hash__; Comparer isn't given" - raise TypeError(msg) from te + + for item in self.source: + if (item_key := key_selector(item)) not in captured_dict: + captured_dict[item_key] = item + return PurePythonEnumerable(*captured_dict.values()) def order( self, @@ -397,8 +520,14 @@ def order( *, comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + Uses `builtins.sorted` + If comparer is given + O(n^2) + Else + Assumes that `TSource` conforms + `pyenumerable.typing_utility.Comparable` + """ if comparer is not None: rank_table: dict[int, list[TSource]] = {} @@ -408,21 +537,13 @@ def order( if comparer(compared, item): rank += 1 rank_table.setdefault(rank, []).append(item) - return PurePythonEnumerable( from_iterable=[ rank_table[key] for key in sorted(rank_table.keys()) ] ) - try: - return PurePythonEnumerable(*sorted(self.source)) # type: ignore - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable; Comparer isn't given" - ) - raise TypeError(msg) from te + return PurePythonEnumerable(*sorted(self.source)) # type: ignore def order_descending( self, @@ -430,8 +551,14 @@ def order_descending( *, comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + Uses `builtins.sorted` + If comparer is given + O(n^2) + Else + Assumes that `TSource` conforms to + `pyenumerable.typing_utility.Comparable` + """ if comparer is not None: rank_table: dict[int, list[TSource]] = {} @@ -441,58 +568,51 @@ def order_descending( if not comparer(compared, item): rank += 1 rank_table.setdefault(rank, []).append(item) - return PurePythonEnumerable( from_iterable=[ rank_table[key] for key in sorted(rank_table.keys()) ] ) - try: - return PurePythonEnumerable(*sorted(self.source, reverse=True)) # type: ignore - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable; Comparer isn't given" - ) - raise TypeError(msg) from te + return PurePythonEnumerable(*sorted(self.source, reverse=True)) # type: ignore - def order_by[TKey]( + def order_by[TKey: Hashable]( self, key_selector: Callable[[TSource], TKey], /, *, comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + Uses `builtins.sorted` + If comparer is given + O(n^2) + Else + Assumes that `TKey` conforms to + `pyenumerable.typing_utility.Comparable` + """ + + cached_selector = cache(key_selector) if comparer is not None: rank_table: dict[int, list[TSource]] = {} for item in self.source: rank = 0 - item_key = key_selector(item) for compared in self.source: - if comparer(key_selector(compared), item_key): + if comparer( + cached_selector(compared), cached_selector(item) + ): rank += 1 rank_table.setdefault(rank, []).append(item) - return PurePythonEnumerable( from_iterable=[ rank_table[key] for key in sorted(rank_table.keys()) ] ) - try: - return PurePythonEnumerable( - *sorted(self.source, key=key_selector) # type: ignore - ) - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable; Comparer isn't given" - ) - raise TypeError(msg) from te + return PurePythonEnumerable( + *sorted(self.source, key=cached_selector) # type: ignore + ) def order_by_descending[TKey]( self, @@ -501,44 +621,53 @@ def order_by_descending[TKey]( *, comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() + """ + Uses `builtins.sorted` + If comparer is given + O(n^2) + Else + Assumes that `TKey` conforms to + `pyenumerable.typing_utility.Comparable` + """ + + cached_selector = cache(key_selector) if comparer is not None: rank_table: dict[int, list[TSource]] = {} for item in self.source: rank = 0 - item_key = key_selector(item) for compared in self.source: - if not comparer(key_selector(compared), item_key): + if not comparer( + cached_selector(compared), cached_selector(item) + ): rank += 1 rank_table.setdefault(rank, []).append(item) - return PurePythonEnumerable( from_iterable=[ rank_table[key] for key in sorted(rank_table.keys()) ] ) - try: - return PurePythonEnumerable( - *sorted(self.source, key=key_selector, reverse=True) # type: ignore - ) - except TypeError as te: - msg = ( - "TSource doesn't implement " - "pyenumerable.typing_utility.Comparable; Comparer isn't given" - ) - raise TypeError(msg) from te + return PurePythonEnumerable( + *sorted(self.source, key=cached_selector, reverse=True) # type: ignore + ) def zip[TSecond]( self, second: Enumerable[TSecond], /, ) -> Enumerable[tuple[TSource, TSecond]]: + """ + Wraps `builtins.zip` + """ + return PurePythonEnumerable(*zip(self.source, second.source)) def reverse(self, /) -> Enumerable[TSource]: + """ + Wraps `builtins.reversed` + """ + return PurePythonEnumerable(*reversed(self.source)) def intersect( @@ -546,20 +675,34 @@ def intersect( second: Enumerable[TSource], /, *, - comparer: Comparer[TSource] = lambda in_, out: in_ == out, + comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 0: + """ + If comparer is given + O(n*m) + Else + > DOESN'T PRESERVE ORDER < + Uses `builtins.set.intersect` + """ + + if self.count_() == 0 or second.count_() == 0: return PurePythonEnumerable() - out: list[TSource] = [] - for inner in self.source: - for outer in second.source: - if comparer(inner, outer): - for captured in out: - if comparer(inner, captured): - break - else: - out.append(inner) - return PurePythonEnumerable(*out) + + if comparer is not None: + out: list[TSource] = [] + for inner in self.source: + for outer in second.source: + if comparer(inner, outer): + for captured in out: + if comparer(inner, captured): + break + else: + out.append(inner) + return PurePythonEnumerable(*out) + + return PurePythonEnumerable( + *set(self.source).intersection(second.source) + ) def intersect_by[TKey]( self, @@ -567,22 +710,42 @@ def intersect_by[TKey]( key_selector: Callable[[TSource], TKey], /, *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 0: + """ + If comparer is given + O(n*m) + Else + Assumes that `TKey` conforms to `collections.abc.Hashable` + Uses `builtins.dict` + O(n) + """ + + if self.count_() == 0 or second.count_() == 0: return PurePythonEnumerable() - out: list[TSource] = [] - for inner in self.source: - inner_key = key_selector(inner) - for outer_key in second.source: - if comparer(inner_key, outer_key): - for captured in out: - captured_key = key_selector(captured) - if comparer(inner_key, captured_key): - break - else: - out.append(inner) - return PurePythonEnumerable(*out) + + cached_selector = cache(key_selector) + + if comparer is not None: + out: list[TSource] = [] + for inner in self.source: + inner_key = cached_selector(inner) + for outer_key in second.source: + if comparer(inner_key, outer_key): + for captured in out: + if comparer(inner_key, cached_selector(captured)): + break + else: + out.append(inner) + return PurePythonEnumerable(*out) + + inner_source_table = {cached_selector(i): i for i in self.source} + return PurePythonEnumerable( + *( + inner_source_table[k] + for k in (inner_source_table and second.source) + ) + ) def sequence_equal( self, @@ -591,8 +754,14 @@ def sequence_equal( *, comparer: Comparer[TSource] = lambda in_, out: in_ == out, ) -> bool: - if len(self.source) != len(other.source): + """ + Wraps `builtins.all` + O(max(n, m)) + """ + + if self.count_() != other.count_(): return False + return all( comparer(inner, outer) for inner, outer in zip(self.source, other.source) @@ -603,16 +772,28 @@ def except_( other: Enumerable[TSource], /, *, - comparer: Comparer[TSource] = lambda in_, out: in_ == out, + comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: - out: list[TSource] = [] - for inner in self.source: - for outer in other.source: - if comparer(inner, outer): - break - else: - out.append(inner) - return PurePythonEnumerable(*out) + """ + If comparer is given + O(n*m) + Else + > DOESN'T PRESERVE ORDER < + > REMOVES DUPLICATES < + Wraps `builtins.set` + """ + + if comparer is not None: + out: list[TSource] = [] + for inner in self.source: + for outer in other.source: + if comparer(inner, outer): + break + else: + out.append(inner) + return PurePythonEnumerable(*out) + + return PurePythonEnumerable(*set(self.source) - set(other.source)) def except_by[TKey]( self, @@ -620,26 +801,57 @@ def except_by[TKey]( key_selector: Callable[[TSource], TKey], /, *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - out: list[TSource] = [] - for inner in self.source: - inner_key = key_selector(inner) - for outer in other.source: - if comparer(inner_key, key_selector(outer)): - break - else: - out.append(inner) - return PurePythonEnumerable(*out) + """ + If comparer is given + O(n*m) + Else + Assumes that `TKey` conforms to `collections.abc.Hashable` + Uses `builtins.dict` + O(max(n, m)) + """ + + if comparer is not None: + out: list[TSource] = [] + key_table: dict[TSource, TKey] = {} + for inner in self.source: + if (inner_key := key_table.get(inner)) is None: + inner_key = key_selector(inner) + key_table[inner] = inner_key + for outer in other.source: + if (outer_key := key_table.get(outer)) is None: + outer_key = key_selector(outer) + key_table[outer] = outer_key + if comparer(inner_key, outer_key): + break + else: + out.append(inner) + return PurePythonEnumerable(*out) + + inner_key_table = {key_selector(i): i for i in self.source} + return PurePythonEnumerable( + *( + inner_key_table[k] + for k in inner_key_table.keys() + - (key_selector(o) for o in other.source) + ) + ) def average(self, /) -> float: - try: - return sum(self.source) / len(self.source) # type: ignore - except TypeError as te: - msg = "Average can't be executed on TSource" - raise TypeError(msg) from te + """ + Wraps `builtins.sum` & `builtins.len` + `TSource` should be usable as arugment of `builtins.sum` + """ + + return sum(self.source) / len(self.source) # type: ignore def chunk(self, size: int, /) -> tuple[PurePythonEnumerable[TSource], ...]: + """ + Uses slicing + O(n) + """ + return tuple( PurePythonEnumerable(*c) for c in ( @@ -655,9 +867,16 @@ def aggregate( *, seed: TSource | None = None, ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) - curr, start = (seed, 0) if seed is not None else (self.source[0], 1) - for item in self.source[start:]: + """ + O(n) + """ + + assume_not_empty(self) + + curr, start_idx = ( + (seed, 0) if seed is not None else (self.source[0], 1) + ) + for item in self.source[start_idx:]: curr = func(curr, item) return curr @@ -668,6 +887,13 @@ def union( *, comparer: Comparer[TSource] | None = None, ) -> Enumerable[TSource]: + """ + If comparer is given + O(max(n, m)^2) + Else + Wraps `builtins.dict.fromkeys` + """ + if comparer is not None: out: list[TSource] = [] for inner in self.source: @@ -683,13 +909,10 @@ def union( else: out.append(outer) return PurePythonEnumerable(*out) - try: - return PurePythonEnumerable( - *dict.fromkeys((*self.source, *second.source)).keys() - ) - except TypeError as te: - msg = "TSource doesn't implement __hash__; Comparer isn't given" - raise TypeError(msg) from te + + return PurePythonEnumerable( + *dict.fromkeys((*self.source, *second.source)).keys() + ) def union_by[TKey]( self, @@ -697,45 +920,88 @@ def union_by[TKey]( key_selector: Callable[[TSource], TKey], /, *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[TSource]: - out: list[TSource] = [] - for inner in self.source: - inner_key = key_selector(inner) - for captured in out: - if comparer(inner_key, key_selector(captured)): - break - else: - out.append(inner) - for outer in second.source: - outer_key = key_selector(outer) - for captured in out: - if comparer(outer_key, key_selector(captured)): - break - else: - out.append(outer) - return PurePythonEnumerable(*out) + """ + If comparer is given + O(max(n, m)^2) + Else + Uses `builtins.dict` + O(max(n, m)) + """ + + if comparer is not None: + out: list[TSource] = [] + key_table: dict[TSource, TKey] = {} + for inner in self.source: + if (inner_key := key_table.get(inner)) is None: + inner_key = key_selector(inner) + key_table[inner] = inner_key + for captured in out: + if comparer(inner_key, key_selector(captured)): + break + else: + out.append(inner) + for outer in second.source: + if (outer_key := key_table.get(outer)) is None: + outer_key = key_selector(outer) + key_table[outer] = outer_key + for captured in out: + if comparer(outer_key, key_selector(captured)): + break + else: + out.append(outer) + return PurePythonEnumerable(*out) + + (source_table := {key_selector(i): i for i in self.source}).update( + {key_selector(o): o for o in second.source} + ) + return PurePythonEnumerable(*source_table.values()) def group_by[TKey]( self, key_selector: Callable[[TSource], TKey], /, *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[Associable[TKey, TSource]]: - keys: list[TKey] = [] - values: dict[int, list[TSource]] = {} + """ + If comparer is given + O(n^2) + Else + Assumes that `TKey` conforms to `collections.abc.Hashable` + O(n) + """ + + cached_selector = cache(key_selector) + + if comparer is not None: + keys: list[TKey] = [] + values: dict[int, list[TSource]] = {} + for item in self.source: + item_key = cached_selector(item) + for index, k in enumerate(keys): + if comparer(k, item_key): + values[index].append(item) + break + else: + keys.append(item_key) + values[len(keys) - 1] = [item] + return PurePythonEnumerable( + *( + PurePythonAssociable(keys[kid], *v) + for kid, v in values.items() + ) + ) + + group_table: dict[TKey, list[TSource]] = {} for item in self.source: - item_key = key_selector(item) - for index, k in enumerate(keys): - if comparer(k, item_key): - values[index].append(item) - break - else: - keys.append(item_key) - values[len(keys) - 1] = [item] + group_table.setdefault(cached_selector(item), []).append(item) return PurePythonEnumerable( - *(PurePythonAssociable(keys[kid], *v) for kid, v in values.items()) + *( + PurePythonAssociable(k, *values) + for k, values in group_table.items() + ) ) def join[TInner, TKey, TResult]( @@ -746,15 +1012,51 @@ def join[TInner, TKey, TResult]( result_selector: Callable[[TSource, TInner], TResult], /, *, - comparer: Comparer[TKey] = lambda out, in_: out == in_, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[TResult]: - out: list[TResult] = [] + """ + O(n*m) + If comparer isn't given + Assumes that `TKey` conforms to `collections.abc.Hashable` + Uses `builtins.dict` & `builtins.set.intersection` + """ + + cached_outer_selector = cache(outer_key_selector) + cached_inner_selector = cache(inner_key_selector) + + if comparer is not None: + return PurePythonEnumerable( + *( + result_selector(outer_item, inner_item) + for outer_item in self.source + for inner_item in inner.source + if comparer( + cached_outer_selector(outer_item), + cached_inner_selector(inner_item), + ) + ) + ) + + inner_table: dict[TKey, list[TInner]] = {} + for inner_item in inner.source: + inner_table.setdefault( + cached_inner_selector(inner_item), [] + ).append(inner_item) + outer_table: dict[TKey, list[TSource]] = {} for outer_item in self.source: - outer_key = outer_key_selector(outer_item) - for inner_item in inner.source: - if comparer(outer_key, inner_key_selector(inner_item)): - out.append(result_selector(outer_item, inner_item)) # noqa: PERF401 - return PurePythonEnumerable(*out) + outer_table.setdefault( + cached_outer_selector(outer_item), [] + ).append(outer_item) + return PurePythonEnumerable( + *( + result_selector(outer_item, inner_item) + for k in set(inner_table.keys()).intersection( + outer_table.keys() + ) + for outer_item in outer_table[k] + for inner_item in inner_table[k] + ) + ) def group_join[TInner, TKey, TResult]( self, @@ -764,36 +1066,56 @@ def group_join[TInner, TKey, TResult]( result_selector: Callable[[TSource, Enumerable[TInner]], TResult], /, *, - comparer: Comparer[TKey] = lambda out, in_: out == in_, + comparer: Comparer[TKey] | None = None, ) -> Enumerable[TResult]: - keys: list[tuple[TKey, TSource]] = [] - values: dict[int, list[TInner]] = {} - for outer_item in self.source: - outer_key = outer_key_selector(outer_item) - for index, kpair in enumerate(keys): - if comparer(outer_key, kpair[0]): - break - else: - keys.append((outer_key, outer_item)) - values[len(keys) - 1] = [] + """ + If comparer is given + O(n^2+(n*m)) + Else + Assumes that `TKey` conforms to `collections.abc.Hashable` + O(max(n, m)) + """ + + if comparer is not None: + keys: list[tuple[TKey, TSource]] = [] + values: dict[int, list[TInner]] = {} + for outer_item in self.source: + outer_key = outer_key_selector(outer_item) + for index, kpair in enumerate(keys): + if comparer(outer_key, kpair[0]): + break + else: + keys.append((outer_key, outer_item)) + values[len(keys) - 1] = [] + for inner_item in inner.source: + inner_key = inner_key_selector(inner_item) + for index, kpair in enumerate(keys): + if comparer(kpair[0], inner_key): + values[index].append(inner_item) + return PurePythonEnumerable( + *( + result_selector( + kpair[1], PurePythonEnumerable(*values[index]) + ) + for index, kpair in enumerate(keys) + ) + ) + + group_table: dict[TKey, tuple[TSource, list[TInner]]] = { + outer_key_selector(o): (o, []) for o in self.source + } for inner_item in inner.source: - inner_key = inner_key_selector(inner_item) - for index, kpair in enumerate(keys): - if comparer(kpair[0], inner_key): - values[index].append(inner_item) + with suppress(KeyError): + group_table[inner_key_selector(inner_item)][1].append( + inner_item + ) return PurePythonEnumerable( - *[ - result_selector(kpair[1], PurePythonEnumerable(*values[index])) - for index, kpair in enumerate(keys) - ] + *( + result_selector(values[0], PurePythonEnumerable(*values[1])) + for values in group_table.values() + ) ) - @staticmethod - def _assume_not_empty(instance: PurePythonEnumerable[Any]) -> None: - if len(instance.source) == 0: - msg = "Enumerable (self) is empty" - raise ValueError(msg) - class PurePythonAssociable[TKey, TSource]( Associable[TKey, TSource], diff --git a/pyenumerable/protocol/_enumerable.py b/pyenumerable/protocol/_enumerable.py index 2e1b17f..d620280 100644 --- a/pyenumerable/protocol/_enumerable.py +++ b/pyenumerable/protocol/_enumerable.py @@ -72,4 +72,4 @@ def __str__(self) -> str: return f"Enumerable(*{self.source})" def __repr__(self) -> str: - return f"{self.__class__.__name__}(*{self.source})" + return f"{self.__class__.__name__}: {self.source}" diff --git a/pyenumerable/protocol/_supports_all.py b/pyenumerable/protocol/_supports_all.py index f6670b0..fa84e09 100644 --- a/pyenumerable/protocol/_supports_all.py +++ b/pyenumerable/protocol/_supports_all.py @@ -4,7 +4,7 @@ class SupportsAll[TSource](Protocol): @overload - def all(self, /) -> bool: ... + def all_(self, /) -> bool: ... @overload - def all(self, predicate: Callable[[TSource], bool], /) -> bool: ... + def all_(self, predicate: Callable[[TSource], bool], /) -> bool: ... diff --git a/pyenumerable/protocol/_supports_any.py b/pyenumerable/protocol/_supports_any.py index 13aea76..52ea375 100644 --- a/pyenumerable/protocol/_supports_any.py +++ b/pyenumerable/protocol/_supports_any.py @@ -4,7 +4,7 @@ class SupportsAny[TSource](Protocol): @overload - def any(self, /) -> bool: ... + def any_(self, /) -> bool: ... @overload - def any(self, predicate: Callable[[TSource], bool], /) -> bool: ... + def any_(self, predicate: Callable[[TSource], bool], /) -> bool: ... diff --git a/pyenumerable/protocol/_supports_max.py b/pyenumerable/protocol/_supports_max.py index 746673a..af3247f 100644 --- a/pyenumerable/protocol/_supports_max.py +++ b/pyenumerable/protocol/_supports_max.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Any, Protocol, overload +from typing import Protocol, overload from pyenumerable.typing_utility import Comparer diff --git a/pyenumerable/protocol/_supports_sum.py b/pyenumerable/protocol/_supports_sum.py index fd8ae3f..2cf58d6 100644 --- a/pyenumerable/protocol/_supports_sum.py +++ b/pyenumerable/protocol/_supports_sum.py @@ -2,4 +2,4 @@ class SupportsSum[TSource](Protocol): - def sum(self, /) -> TSource: ... + def sum_(self, /) -> TSource: ... diff --git a/pyenumerable/typing_utility.py b/pyenumerable/typing_utility.py index 40ec766..7246ffd 100644 --- a/pyenumerable/typing_utility.py +++ b/pyenumerable/typing_utility.py @@ -5,6 +5,10 @@ class Comparable(Protocol): + """ + mimics `_typeshed.SupportsRichComparisonT` + """ + def __eq__[T](self: T, other: T, /) -> bool: ... def __lt__[T](self: T, other: T, /) -> bool: ... diff --git a/pyproject.toml b/pyproject.toml index e720eb3..3714426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyenumerable" -version = "1.1.5" +version = "2.0.0" description = "Implementation of .net's IEnumerable interface in python W/ support for generics." readme = "readme.md" license = "WTFPL" @@ -61,7 +61,7 @@ lint.select = [ "PERF", "RUF", ] -lint.ignore = ["F401", "COM812"] +lint.ignore = ["F401", "COM812", "PLW1641"] lint.fixable = ["ALL"] extend-exclude = ["__init__.py"] diff --git a/test/unit/pure_python/enumerable/all/test_all_method.py b/test/unit/pure_python/enumerable/all/test_all_method.py index c37bfff..fa01056 100644 --- a/test/unit/pure_python/enumerable/all/test_all_method.py +++ b/test/unit/pure_python/enumerable/all/test_all_method.py @@ -6,35 +6,35 @@ class TestAllMethod: def test_without_predicate_when_empty(self) -> None: obj: PurePythonEnumerable[int] = PurePythonEnumerable() - res = obj.all() + res = obj.all_() assert res is True def test_pass_without_predicate(self) -> None: obj = PurePythonEnumerable(True, True, True) # noqa: FBT003 - res = obj.all() + res = obj.all_() assert res is True def test_fail_without_predicate(self) -> None: obj = PurePythonEnumerable(True, None, False) # noqa: FBT003 - res = obj.all() + res = obj.all_() assert res is False def test_with_predicate_when_empty(self) -> None: obj: PurePythonEnumerable[int] = PurePythonEnumerable() - res = obj.all(lambda _: False) + res = obj.all_(lambda _: False) assert res is True def test_pass_with_predicate(self) -> None: obj = PurePythonEnumerable(*range(threshold := 7)) - res = obj.all(lambda x: x <= threshold) + res = obj.all_(lambda x: x <= threshold) assert res is True @@ -46,6 +46,6 @@ def test_fail_with_predicate(self) -> None: Person("jane doe", 6, Person("harry doe", 28)), ) - res = obj.all(lambda person: person.parent is not None) + res = obj.all_(lambda person: person.parent is not None) assert res is False diff --git a/test/unit/pure_python/enumerable/any/test_any_method.py b/test/unit/pure_python/enumerable/any/test_any_method.py index 46eeb7f..3f8dbe2 100644 --- a/test/unit/pure_python/enumerable/any/test_any_method.py +++ b/test/unit/pure_python/enumerable/any/test_any_method.py @@ -6,35 +6,35 @@ class TestAnyMethod: def test_without_predicate_when_empty(self) -> None: obj: PurePythonEnumerable[int] = PurePythonEnumerable() - res = obj.any() + res = obj.any_() assert res is False def test_pass_without_predicate(self) -> None: obj = PurePythonEnumerable(False, False, True) # noqa: FBT003 - res = obj.any() + res = obj.any_() assert res is True def test_fail_without_predicate(self) -> None: obj = PurePythonEnumerable(None, False, False) # noqa: FBT003 - res = obj.any() + res = obj.any_() assert res is False def test_with_predicate_when_empty(self) -> None: obj: PurePythonEnumerable[int] = PurePythonEnumerable() - res = obj.any(lambda _: True) + res = obj.any_(lambda _: True) assert res is False def test_pass_with_predicate(self) -> None: obj = PurePythonEnumerable(*range((threshold := 7) + 1)) - res = obj.any(lambda x: x == threshold) + res = obj.any_(lambda x: x == threshold) assert res is True @@ -46,7 +46,7 @@ def test_fail_with_predicate(self) -> None: Person("jane doe", 6, Person("harry doe", 28)), ) - res = obj.any( + res = obj.any_( lambda person: ( person.parent is not None and person.parent.parent is not None ), diff --git a/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py b/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py index 8da118e..f7e0ba4 100644 --- a/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py +++ b/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py @@ -12,16 +12,6 @@ def test_when_empty(self) -> None: assert res.source == () - def test_exc_raise_when_unhashable(self) -> None: - obj = PurePythonEnumerable( - Person("john doe", 12), - Person("jane doe", 12, Person("marry doe", 36)), - Person("junior doe", 12, Person("larry doe", 28)), - ) - - with pytest.raises(TypeError): - obj.distinct_by(lambda person: person.parent) - def test_without_comparer(self) -> None: obj = PurePythonEnumerable( from_iterable=(items := tuple(range(7)), [-i for i in items]), diff --git a/test/unit/pure_python/enumerable/distinct/test_distinct_method.py b/test/unit/pure_python/enumerable/distinct/test_distinct_method.py index f51e2d2..ed191c9 100644 --- a/test/unit/pure_python/enumerable/distinct/test_distinct_method.py +++ b/test/unit/pure_python/enumerable/distinct/test_distinct_method.py @@ -12,15 +12,6 @@ def test_when_empty(self) -> None: assert res.source == () - def test_exc_raise_when_unhashable(self) -> None: - obj = PurePythonEnumerable( - Person("john doe", 12), - Person("jane doe", 12), - ) - - with pytest.raises(TypeError): - obj.distinct() - def test_without_comparer(self) -> None: obj = PurePythonEnumerable( from_iterable=(items := tuple(range(7)), items), diff --git a/test/unit/pure_python/enumerable/except_/test_except_by_method.py b/test/unit/pure_python/enumerable/except_/test_except_by_method.py index 1181391..7268852 100644 --- a/test/unit/pure_python/enumerable/except_/test_except_by_method.py +++ b/test/unit/pure_python/enumerable/except_/test_except_by_method.py @@ -3,7 +3,26 @@ class TestExceptByMethod: - def test_except_by(self) -> None: + def test_with_comparer(self) -> None: + first_object = PurePythonEnumerable( + Point(0, 1), + first := Point(3, 2), + Point(4, 5), + second := Point(7, 6), + ) + second_object = PurePythonEnumerable( + Point(3, -5), Point(8, 9), Point(-1, -1), Point(4, 7) + ) + + res = first_object.except_by( + second_object, + lambda point: point.y, + comparer=lambda first_y, second_y: abs(first_y) == abs(second_y), + ) + + assert res.source == (first, second) + + def test_without_comparer(self) -> None: first_object = PurePythonEnumerable( Point(0, 1), first := Point(3, 2), diff --git a/test/unit/pure_python/enumerable/except_/test_except_method.py b/test/unit/pure_python/enumerable/except_/test_except_method.py index e381a18..f241eda 100644 --- a/test/unit/pure_python/enumerable/except_/test_except_method.py +++ b/test/unit/pure_python/enumerable/except_/test_except_method.py @@ -3,7 +3,7 @@ class TestExceptMethod: - def test_except(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable( Point(0, 1), first := Point(1, 3), @@ -21,3 +21,18 @@ def test_except(self) -> None: ) assert res.source == (first, second) + + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable( + Point(0, 1), + first := Point(1, 3), + Point(3, 5), + second := Point(8, 12), + ) + second_object = PurePythonEnumerable( + Point(4, 18), Point(0, 1), Point(0, 0), Point(3, 5) + ) + + res = first_object.except_(second_object) + + assert res.source == (first, second) diff --git a/test/unit/pure_python/enumerable/group_by/test_group_by_method.py b/test/unit/pure_python/enumerable/group_by/test_group_by_method.py index d4afdc3..312ce42 100644 --- a/test/unit/pure_python/enumerable/group_by/test_group_by_method.py +++ b/test/unit/pure_python/enumerable/group_by/test_group_by_method.py @@ -3,7 +3,7 @@ class TestGroupByMethod: - def test_group_by(self) -> None: + def test_without_comparer(self) -> None: number_of_groups = 2 first_group_key, number_of_first_group_members = 1, 3 second_group_key, number_of_second_group_members = 2, 2 @@ -40,3 +40,72 @@ def test_group_by(self) -> None: ) assert res.source[first_group_index].key == first_group_key assert res.source[second_group_index].key == second_group_key + + def test_with_comparer(self) -> None: + number_of_groups = 2 + ( + first_group_key, + number_of_first_half_of_first_group_members, + number_of_second_half_of_first_group_members, + ) = 1, 3, 3 + ( + second_group_key, + number_of_first_half_of_second_group_members, + number_of_second_half_of_second_group_members, + ) = 2, 2, 2 + obj = PurePythonEnumerable( + *( + tuple( + Point(i, first_group_key) + for i in range(number_of_first_half_of_first_group_members) + ) + + tuple( + Point(i, -first_group_key) + for i in range( + number_of_second_half_of_first_group_members + ) + ) + ), + *( + tuple( + Point(i, second_group_key) + for i in range( + number_of_first_half_of_second_group_members + ) + ) + + tuple( + Point(i, -second_group_key) + for i in range( + number_of_second_half_of_second_group_members + ) + ) + ), + ) + + res = obj.group_by( + lambda point: point.y, + comparer=lambda first_y, second_y: abs(first_y) == abs(second_y), + ) + + first_group_index, second_group_index = 0, 1 + assert len(res.source) == number_of_groups + assert ( + len(res.source[first_group_index].source) + == number_of_first_half_of_first_group_members + + number_of_second_half_of_first_group_members + ) + assert ( + len(res.source[second_group_index].source) + == number_of_first_half_of_second_group_members + + number_of_second_half_of_second_group_members + ) + assert all( + abs(p.y) == abs(first_group_key) + for p in res.source[first_group_index].source + ) + assert all( + abs(p.y) == abs(second_group_key) + for p in res.source[second_group_index].source + ) + assert res.source[first_group_index].key == first_group_key + assert res.source[second_group_index].key == second_group_key diff --git a/test/unit/pure_python/enumerable/group_join/test_group_join_method.py b/test/unit/pure_python/enumerable/group_join/test_group_join_method.py index ce0b317..8657fbc 100644 --- a/test/unit/pure_python/enumerable/group_join/test_group_join_method.py +++ b/test/unit/pure_python/enumerable/group_join/test_group_join_method.py @@ -3,7 +3,7 @@ class TestGroupJoinMethod: - def test_group_join(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable( first_parent := Person("john doe", 32), second_parent := Person("jane doe", 28), @@ -23,6 +23,7 @@ def test_group_join(self) -> None: if child.parent is not None else None, lambda parent, children: (parent, children.source), + comparer=lambda first_name, second_name: first_name == second_name, ) assert res.source == ( @@ -47,6 +48,34 @@ def test_overlap_remove(self) -> None: if child.parent is not None else None, lambda parent, children: (parent.age, children.source), + comparer=lambda first_age, second_age: first_age == second_age, ) assert len(res.source) == 1 + + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable( + first_parent := Person("john doe", 32), + second_parent := Person("jane doe", 28), + ) + second_object = PurePythonEnumerable( + first_child := Person("larry doe", 12, first_parent), + second_child := Person("jerry doe", 13, first_parent), + third_child := Person("marry doe", 14, second_parent), + fourth_child := Person("james doe", 15, second_parent), + fifth_child := Person("james doe", 16, second_parent), + ) + + res = first_object.group_join( + second_object, + lambda parent: parent.name, + lambda child: child.parent.name + if child.parent is not None + else None, + lambda parent, children: (parent, children.source), + ) + + assert res.source == ( + (first_parent, (first_child, second_child)), + (second_parent, (third_child, fourth_child, fifth_child)), + ) diff --git a/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py b/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py index 0c8b628..890be4d 100644 --- a/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py +++ b/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py @@ -19,7 +19,7 @@ def test_when_second_empty(self) -> None: assert res.source == () - def test_intersect_by(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable( first := Point(5, 1), Point(3, 3), @@ -37,6 +37,20 @@ def test_intersect_by(self) -> None: assert res.source == (first, second, third) + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable( + first := Point(5, 1), + Point(3, 3), + Point(4, 5), + second := Point(2, 7), + third := Point(3, 9), + ) + second_object = PurePythonEnumerable(1, 7, 9) + + res = first_object.intersect_by(second_object, lambda point: point.y) + + assert res.source == (first, second, third) + def test_overlap_remove(self) -> None: first_object = PurePythonEnumerable( first := Point(5, 1), diff --git a/test/unit/pure_python/enumerable/intersect/test_intersect_method.py b/test/unit/pure_python/enumerable/intersect/test_intersect_method.py index 5b99343..1acd6cb 100644 --- a/test/unit/pure_python/enumerable/intersect/test_intersect_method.py +++ b/test/unit/pure_python/enumerable/intersect/test_intersect_method.py @@ -19,7 +19,7 @@ def test_when_second_empty(self) -> None: assert res.source == () - def test_intersect(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable( first := Point(0, 1), Point(0, 3), @@ -43,6 +43,26 @@ def test_intersect(self) -> None: assert res.source == (first, second, third) + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable( + first := Point(0, 1), + Point(0, 3), + Point(0, 4), + second := Point(0, 7), + third := Point(0, 9), + ) + second_object = PurePythonEnumerable( + Point(0, -2), + Point(0, 1), + Point(0, 5), + Point(0, 7), + Point(0, 9), + ) + + res = first_object.intersect(second_object) + + assert res.source == (first, second, third) + def test_overlap_remove(self) -> None: first_object = PurePythonEnumerable( first := Point(0, 1), diff --git a/test/unit/pure_python/enumerable/join/test_join_method.py b/test/unit/pure_python/enumerable/join/test_join_method.py index a30b1a7..a5fbc60 100644 --- a/test/unit/pure_python/enumerable/join/test_join_method.py +++ b/test/unit/pure_python/enumerable/join/test_join_method.py @@ -3,7 +3,7 @@ class TestJoinMethod: - def test_without_outcaster(self) -> None: + def test_with_comparer_without_outcaster(self) -> None: first_group_key = 1 second_group_key = 3 @@ -23,6 +23,7 @@ def test_without_outcaster(self) -> None: lambda point: point.y, lambda point: point.x, lambda outer_point, inner_point: (outer_point, inner_point), + comparer=lambda first_y, second_x: first_y == second_x, ) assert res.source == ( @@ -32,7 +33,41 @@ def test_without_outcaster(self) -> None: (second_group_second_outer, second_group_first_inner), ) - def test_with_outcaster(self) -> None: + def test_with_comparer_with_outcaster(self) -> None: + first_group_key = 1 + second_group_key = 3 + + first_object = PurePythonEnumerable( + first_group_first_outer := Point(0, first_group_key), + Point(0, 2), + Point(1, 2), + second_group_first_outer := Point(0, second_group_key), + second_group_second_outer := Point(1, second_group_key), + ) + second_object = PurePythonEnumerable( + first_group_first_inner := Point(first_group_key, 0), + first_group_second_inner := Point(first_group_key, 1), + Point(4, 0), + Point(4, 1), + second_group_first_inner := Point(second_group_key, 0), + ) + + res = first_object.join( + second_object, + lambda point: point.y, + lambda point: point.x, + lambda outer_point, inner_point: (outer_point, inner_point), + comparer=lambda first_y, second_x: first_y == second_x, + ) + + assert res.source == ( + (first_group_first_outer, first_group_first_inner), + (first_group_first_outer, first_group_second_inner), + (second_group_first_outer, second_group_first_inner), + (second_group_second_outer, second_group_first_inner), + ) + + def test_without_comparer(self) -> None: first_group_key = 1 second_group_key = 3 diff --git a/test/unit/pure_python/enumerable/sum/test_sum_method.py b/test/unit/pure_python/enumerable/sum/test_sum_method.py index be1cff7..7465f23 100644 --- a/test/unit/pure_python/enumerable/sum/test_sum_method.py +++ b/test/unit/pure_python/enumerable/sum/test_sum_method.py @@ -8,11 +8,11 @@ def test_exc_raise_with_bad_type(self) -> None: obj = PurePythonEnumerable("should", "not", "work") with pytest.raises(TypeError): - obj.sum() + obj.sum_() def test_sum(self) -> None: obj = PurePythonEnumerable(*(items := tuple(range(7)))) - res = obj.sum() + res = obj.sum_() assert res == sum(items) diff --git a/test/unit/pure_python/enumerable/union/test_union_by_method.py b/test/unit/pure_python/enumerable/union/test_union_by_method.py index 0945c94..8d2b575 100644 --- a/test/unit/pure_python/enumerable/union/test_union_by_method.py +++ b/test/unit/pure_python/enumerable/union/test_union_by_method.py @@ -3,7 +3,7 @@ class TestUnionByMethod: - def test_union_by(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable( *( first_items := ( @@ -31,6 +31,30 @@ def test_union_by(self) -> None: assert res.source == first_items + second_items + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable( + *( + first_items := ( + Point(0, 1), + Point(0, 2), + Point(0, 3), + ) + ) + ) + second_object = PurePythonEnumerable( + *( + second_items := ( + Point(0, -4), + Point(0, -5), + Point(0, -6), + ) + ) + ) + + res = first_object.union_by(second_object, lambda point: point.y) + + assert res.source == first_items + second_items + def test_overlap_remove_for_self(self) -> None: first_object = PurePythonEnumerable( first := Point(0, 1), @@ -42,7 +66,11 @@ def test_overlap_remove_for_self(self) -> None: fourth := Point(6, 7), ) - res = first_object.union_by(second_object, lambda point: point.y) + res = first_object.union_by( + second_object, + lambda point: point.y, + comparer=lambda first_y, second_y: first_y == second_y, + ) assert res.source == (first, second, third, fourth) @@ -57,6 +85,10 @@ def test_overlap_remove_for_second(self) -> None: fourth := Point(6, 7), ) - res = first_object.union_by(second_object, lambda point: point.y) + res = first_object.union_by( + second_object, + lambda point: point.y, + comparer=lambda first_y, second_y: first_y == second_y, + ) assert res.source == (first, second, third, fourth) diff --git a/test/unit/pure_python/enumerable/union/test_union_method.py b/test/unit/pure_python/enumerable/union/test_union_method.py index e1d7756..8af7600 100644 --- a/test/unit/pure_python/enumerable/union/test_union_method.py +++ b/test/unit/pure_python/enumerable/union/test_union_method.py @@ -5,14 +5,7 @@ class TestUnionMethod: - def test_exc_raise_when_unhashable(self) -> None: - first_object = PurePythonEnumerable(Point(0, 1), Point(1, 0)) - second_object = PurePythonEnumerable(Point(1, 0), Point(0, 1)) - - with pytest.raises(TypeError): - first_object.union(second_object) - - def test_union(self) -> None: + def test_with_comparer(self) -> None: first_object = PurePythonEnumerable(*(items := tuple(range(7)))) second_object = PurePythonEnumerable(*(-i for i in items)) @@ -22,6 +15,16 @@ def test_union(self) -> None: assert res.source == items + def test_without_comparer(self) -> None: + first_object = PurePythonEnumerable(*tuple(range(half := 7))) + second_object = PurePythonEnumerable( + *(items := tuple(range(half * 2))) + ) + + res = first_object.union(second_object) + + assert res.source == items + def test_overlap_remove_for_self(self) -> None: first_object = PurePythonEnumerable(first := Point(0, 1), Point(1, 1)) second_object = PurePythonEnumerable( diff --git a/test/unit/pure_python/test_utility.py b/test/unit/pure_python/test_utility.py index c37e359..237fa77 100644 --- a/test/unit/pure_python/test_utility.py +++ b/test/unit/pure_python/test_utility.py @@ -9,14 +9,14 @@ def generate_random_args(length: int, range_: range) -> tuple[int, ...]: return tuple(choice(range_) for _ in range(length)) -@dataclass +@dataclass(frozen=True, eq=True) class Person: name: str age: int parent: Person | None = None -@dataclass +@dataclass(frozen=True, eq=True) class Point: x: int y: int diff --git a/uv.lock b/uv.lock index 71c7749..3efae46 100644 --- a/uv.lock +++ b/uv.lock @@ -13,31 +13,33 @@ wheels = [ [[package]] name = "coverage" -version = "7.7.1" +version = "7.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332, upload_time = "2025-03-21T17:23:58.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload_time = "2025-06-13T13:02:28.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277, upload_time = "2025-03-21T17:23:04.822Z" }, - { url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551, upload_time = "2025-03-21T17:23:06.256Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068, upload_time = "2025-03-21T17:23:08.462Z" }, - { url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109, upload_time = "2025-03-21T17:23:10.208Z" }, - { url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129, upload_time = "2025-03-21T17:23:11.83Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201, upload_time = "2025-03-21T17:23:13.667Z" }, - { url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282, upload_time = "2025-03-21T17:23:15.454Z" }, - { url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570, upload_time = "2025-03-21T17:23:16.902Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772, upload_time = "2025-03-21T17:23:18.3Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575, upload_time = "2025-03-21T17:23:19.664Z" }, - { url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113, upload_time = "2025-03-21T17:23:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333, upload_time = "2025-03-21T17:23:22.474Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566, upload_time = "2025-03-21T17:23:24.492Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276, upload_time = "2025-03-21T17:23:26.245Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616, upload_time = "2025-03-21T17:23:28.183Z" }, - { url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707, upload_time = "2025-03-21T17:23:29.578Z" }, - { url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876, upload_time = "2025-03-21T17:23:31.554Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687, upload_time = "2025-03-21T17:23:33.406Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486, upload_time = "2025-03-21T17:23:35.035Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647, upload_time = "2025-03-21T17:23:36.572Z" }, - { url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006, upload_time = "2025-03-21T17:23:56.378Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload_time = "2025-06-13T13:01:30.909Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload_time = "2025-06-13T13:01:32.256Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload_time = "2025-06-13T13:01:33.948Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload_time = "2025-06-13T13:01:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload_time = "2025-06-13T13:01:36.712Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload_time = "2025-06-13T13:01:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload_time = "2025-06-13T13:01:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload_time = "2025-06-13T13:01:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload_time = "2025-06-13T13:01:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload_time = "2025-06-13T13:01:45.772Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload_time = "2025-06-13T13:01:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload_time = "2025-06-13T13:01:48.554Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload_time = "2025-06-13T13:01:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload_time = "2025-06-13T13:01:51.314Z" }, + { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload_time = "2025-06-13T13:01:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload_time = "2025-06-13T13:01:56.769Z" }, + { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload_time = "2025-06-13T13:01:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload_time = "2025-06-13T13:01:59.645Z" }, + { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload_time = "2025-06-13T13:02:01.37Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload_time = "2025-06-13T13:02:02.905Z" }, + { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload_time = "2025-06-13T13:02:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload_time = "2025-06-13T13:02:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload_time = "2025-06-13T13:02:27.173Z" }, ] [[package]] @@ -60,25 +62,25 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload_time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload_time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload_time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload_time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload_time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload_time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pyenumerable" -version = "1.1.4" +version = "2.0.0" source = { virtual = "." } [package.dev-dependencies] @@ -99,77 +101,88 @@ dev = [ { name = "ruff", specifier = ">=0.9.2" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload_time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload_time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pyright" -version = "1.1.398" +version = "1.1.402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675, upload_time = "2025-03-26T10:06:06.063Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload_time = "2025-06-11T08:48:35.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235, upload_time = "2025-03-26T10:06:03.994Z" }, + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload_time = "2025-06-11T08:48:33.998Z" }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload_time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload_time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload_time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload_time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945, upload_time = "2024-10-29T20:13:35.363Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload_time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949, upload_time = "2024-10-29T20:13:33.215Z" }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload_time = "2025-06-12T10:47:45.932Z" }, ] [[package]] name = "ruff" -version = "0.11.2" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511, upload_time = "2025-03-21T13:31:17.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload_time = "2025-06-17T15:19:26.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146, upload_time = "2025-03-21T13:30:26.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092, upload_time = "2025-03-21T13:30:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082, upload_time = "2025-03-21T13:30:39.962Z" }, - { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818, upload_time = "2025-03-21T13:30:42.551Z" }, - { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251, upload_time = "2025-03-21T13:30:45.196Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566, upload_time = "2025-03-21T13:30:47.516Z" }, - { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721, upload_time = "2025-03-21T13:30:49.56Z" }, - { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274, upload_time = "2025-03-21T13:30:52.055Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284, upload_time = "2025-03-21T13:30:54.24Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861, upload_time = "2025-03-21T13:30:56.757Z" }, - { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560, upload_time = "2025-03-21T13:30:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091, upload_time = "2025-03-21T13:31:01.45Z" }, - { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133, upload_time = "2025-03-21T13:31:04.013Z" }, - { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514, upload_time = "2025-03-21T13:31:06.166Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835, upload_time = "2025-03-21T13:31:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713, upload_time = "2025-03-21T13:31:13.148Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990, upload_time = "2025-03-21T13:31:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload_time = "2025-06-17T15:18:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload_time = "2025-06-17T15:18:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload_time = "2025-06-17T15:18:51.341Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload_time = "2025-06-17T15:18:53.29Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload_time = "2025-06-17T15:18:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload_time = "2025-06-17T15:18:58.906Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload_time = "2025-06-17T15:19:01.316Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload_time = "2025-06-17T15:19:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload_time = "2025-06-17T15:19:05.875Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload_time = "2025-06-17T15:19:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload_time = "2025-06-17T15:19:10.37Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload_time = "2025-06-17T15:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload_time = "2025-06-17T15:19:15.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload_time = "2025-06-17T15:19:17.6Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload_time = "2025-06-17T15:19:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload_time = "2025-06-17T15:19:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload_time = "2025-06-17T15:19:23.952Z" }, ] [[package]] name = "typing-extensions" -version = "4.13.0" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520, upload_time = "2025-03-26T03:49:41.628Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload_time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683, upload_time = "2025-03-26T03:49:40.35Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload_time = "2025-06-02T14:52:10.026Z" }, ]