From 851e6d1a96d6092ca0f86bd3dc44b622317b2ec9 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Thu, 24 Apr 2025 21:43:09 +0330 Subject: [PATCH 01/13] add hashed pure python module --- pyenumerable/implementations/hashed_pure_python.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pyenumerable/implementations/hashed_pure_python.py diff --git a/pyenumerable/implementations/hashed_pure_python.py b/pyenumerable/implementations/hashed_pure_python.py new file mode 100644 index 0000000..741ad0b --- /dev/null +++ b/pyenumerable/implementations/hashed_pure_python.py @@ -0,0 +1,4 @@ +from pyenumerable.implementations.pure_python import PurePythonEnumerable + + +class HashedPurePythonEnumerable(PurePythonEnumerable): ... From 3936a66bf4ce89c9730fcadc53e8f53827abcdf7 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 25 Apr 2025 23:35:14 +0330 Subject: [PATCH 02/13] add generic to hashed pp --- pyenumerable/implementations/hashed_pure_python.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyenumerable/implementations/hashed_pure_python.py b/pyenumerable/implementations/hashed_pure_python.py index 741ad0b..29b296a 100644 --- a/pyenumerable/implementations/hashed_pure_python.py +++ b/pyenumerable/implementations/hashed_pure_python.py @@ -1,4 +1,8 @@ +from collections.abc import Hashable + from pyenumerable.implementations.pure_python import PurePythonEnumerable -class HashedPurePythonEnumerable(PurePythonEnumerable): ... +class HashedPurePythonEnumerable[TSource: Hashable]( + PurePythonEnumerable[TSource] +): ... From 7afe8b55254a801637309280edbd5ee6b814091e Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:01:49 +0330 Subject: [PATCH 03/13] move assume not empty util to a separate file and change its logic --- pyenumerable/implementation_utility.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 pyenumerable/implementation_utility.py 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) From cb699a90cac47c89afb0e526fe2bb50f6fbf6095 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:02:03 +0330 Subject: [PATCH 04/13] remove separate hashed pp impl --- pyenumerable/implementations/hashed_pure_python.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 pyenumerable/implementations/hashed_pure_python.py diff --git a/pyenumerable/implementations/hashed_pure_python.py b/pyenumerable/implementations/hashed_pure_python.py deleted file mode 100644 index 29b296a..0000000 --- a/pyenumerable/implementations/hashed_pure_python.py +++ /dev/null @@ -1,8 +0,0 @@ -from collections.abc import Hashable - -from pyenumerable.implementations.pure_python import PurePythonEnumerable - - -class HashedPurePythonEnumerable[TSource: Hashable]( - PurePythonEnumerable[TSource] -): ... From fb2bda404ca2cd8c06c2a32901ce6cf181b1c0f5 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:02:39 +0330 Subject: [PATCH 05/13] reimplement most of pp with the assumption of tsource being hashable and immutable --- pyenumerable/implementations/pure_python.py | 832 ++++++++++++++------ 1 file changed, 577 insertions(+), 255 deletions(-) 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], From 1910e40d707881fa3a8b59008b07d6d5fc689a69 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:03:10 +0330 Subject: [PATCH 06/13] modify tests to be compatible with the assumption about tsource being hashable and immutable --- .../enumerable/all/test_all_method.py | 12 ++-- .../enumerable/any/test_any_method.py | 12 ++-- .../distinct/test_distinct_by_method.py | 10 --- .../distinct/test_distinct_method.py | 9 --- .../except_/test_except_by_method.py | 21 +++++- .../enumerable/except_/test_except_method.py | 17 ++++- .../group_by/test_group_by_method.py | 71 ++++++++++++++++++- .../group_join/test_group_join_method.py | 31 +++++++- .../intersect/test_intersect_by_method.py | 16 ++++- .../intersect/test_intersect_method.py | 22 +++++- .../enumerable/join/test_join_method.py | 39 +++++++++- .../enumerable/sum/test_sum_method.py | 4 +- .../enumerable/union/test_union_by_method.py | 38 +++++++++- .../enumerable/union/test_union_method.py | 19 ++--- test/unit/pure_python/test_utility.py | 4 +- 15 files changed, 271 insertions(+), 54 deletions(-) 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 From 07ef25b11ab182b1ee46dc7a210a711bb3c30483 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:03:19 +0330 Subject: [PATCH 07/13] update documentation --- documentation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From 64cd9c3331fa40a9e2164c0f0623e7aebbae738b Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:03:36 +0330 Subject: [PATCH 08/13] update protocols to not shadow builtins --- pyenumerable/protocol/_supports_all.py | 4 ++-- pyenumerable/protocol/_supports_any.py | 4 ++-- pyenumerable/protocol/_supports_max.py | 2 +- pyenumerable/protocol/_supports_sum.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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: ... From 3890fbf4675e17dbdddfd128317aa93ec247ccda Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:03:53 +0330 Subject: [PATCH 09/13] add doc string to Comparable --- pyenumerable/typing_utility.py | 4 ++++ 1 file changed, 4 insertions(+) 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: ... From 0e5aba395db6df71a499c8484f4dfa13649bafb8 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:05:14 +0330 Subject: [PATCH 10/13] modify repr of base enumerable --- pyenumerable/protocol/_enumerable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}" From 120167d33ab581f428e9471326f55ad23d66056c Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:09:47 +0330 Subject: [PATCH 11/13] update version & lock file --- pyenumerable/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 135 +++++++++++++++++++++------------------ 3 files changed, 76 insertions(+), 63 deletions(-) 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/pyproject.toml b/pyproject.toml index e720eb3..58e45be 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" 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" }, ] From 9aa6c0271a3da9ed2237842820219e4e2e9341c3 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:11:32 +0330 Subject: [PATCH 12/13] update ci cd to show tested modules --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 67fe248fafd8db1b89f9acd04f371b9b96693291 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Tue, 24 Jun 2025 19:23:51 +0330 Subject: [PATCH 13/13] update ruff ignored rules --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 58e45be..3714426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ lint.select = [ "PERF", "RUF", ] -lint.ignore = ["F401", "COM812"] +lint.ignore = ["F401", "COM812", "PLW1641"] lint.fixable = ["ALL"] extend-exclude = ["__init__.py"]