From 9add8a87f4d51177059f10d6a23d13b3845c9e84 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:23:13 +0330 Subject: [PATCH 1/6] update readme --- readme.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 220a4e5..f5c7ec1 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1?view=net-9.0) interface in python W/ support for generics. ## Issue tracker -### 1.0.0 +### v1.0.x - [x] Design protocols for each operation set - [x] Design & Implement `Enumerable` constructor(s) for PP implementation - [x] Add pure python implementation of `Enumerable` (assuming inputs aren't guaranteed to be `Hashable` or immutable & maintaining order) @@ -39,8 +39,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ - [x] Max - [x] remove `Comparable` bind from type variables - [x] Publish on pypi -### 1.0.1 -- [x] Add project links to `pyproject.toml` -### 1.1.0 +- [ ] Add external wrapper constructor +### v1.1.x - [ ] Improve test code quality - [ ] Add hashed pure python implementation of `Enumerable` (assuming inputs are guaranteed to be `Hashable` & immutable & not maintaining order) From 25fca3b30e94e134b72cd7621acc05d1926ab21d Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:23:40 +0330 Subject: [PATCH 2/6] clean up protocol imports --- pyenumerable/protocol/__init__.py | 32 +------------- pyenumerable/protocol/_enumerable.py | 62 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/pyenumerable/protocol/__init__.py b/pyenumerable/protocol/__init__.py index b9c2010..012e919 100644 --- a/pyenumerable/protocol/__init__.py +++ b/pyenumerable/protocol/__init__.py @@ -1,32 +1,4 @@ -from ._supports_aggregate import SupportsAggregate # noqa: I001 -from ._supports_all import SupportsAll -from ._supports_any import SupportsAny -from ._supports_append import SupportsAppend -from ._supports_average import SupportsAverage -from ._supports_chunk import SupportsChunk -from ._supports_concat import SupportsConcat -from ._supports_contains import SupportsContains -from ._supports_count import SupportsCount -from ._supports_distinct import SupportsDistinct -from ._supports_except import SupportsExcept -from ._supports_group_by import SupportsGroupBy -from ._supports_group_join import SupportsGroupJoin -from ._supports_intersect import SupportsIntersect -from ._supports_join import SupportsJoin -from ._supports_max import SupportsMax -from ._supports_min import SupportsMin -from ._supports_of_type import SupportsOfType -from ._supports_order import SupportsOrder -from ._supports_prepend import SupportsPrepend -from ._supports_reverse import SupportsReverse -from ._supports_select import SupportsSelect -from ._supports_sequence_equal import SupportsSequenceEqual -from ._supports_single import SupportsSingle -from ._supports_skip import SupportsSkip -from ._supports_sum import SupportsSum -from ._supports_take import SupportsTake -from ._supports_union import SupportsUnion -from ._supports_where import SupportsWhere -from ._supports_zip import SupportsZip from ._associable import Associable from ._enumerable import Enumerable + +__all__ = ["Associable", "Enumerable"] diff --git a/pyenumerable/protocol/_enumerable.py b/pyenumerable/protocol/_enumerable.py index aa5e093..01904db 100644 --- a/pyenumerable/protocol/_enumerable.py +++ b/pyenumerable/protocol/_enumerable.py @@ -1,37 +1,35 @@ from typing import Protocol -from . import ( - SupportsAggregate, - SupportsAll, - SupportsAny, - SupportsAppend, - SupportsAverage, - SupportsChunk, - SupportsConcat, - SupportsContains, - SupportsCount, - SupportsDistinct, - SupportsExcept, - SupportsGroupBy, - SupportsGroupJoin, - SupportsIntersect, - SupportsJoin, - SupportsMax, - SupportsMin, - SupportsOfType, - SupportsOrder, - SupportsPrepend, - SupportsReverse, - SupportsSelect, - SupportsSequenceEqual, - SupportsSingle, - SupportsSkip, - SupportsSum, - SupportsTake, - SupportsUnion, - SupportsWhere, - SupportsZip, -) +from ._supports_aggregate import SupportsAggregate +from ._supports_all import SupportsAll +from ._supports_any import SupportsAny +from ._supports_append import SupportsAppend +from ._supports_average import SupportsAverage +from ._supports_chunk import SupportsChunk +from ._supports_concat import SupportsConcat +from ._supports_contains import SupportsContains +from ._supports_count import SupportsCount +from ._supports_distinct import SupportsDistinct +from ._supports_except import SupportsExcept +from ._supports_group_by import SupportsGroupBy +from ._supports_group_join import SupportsGroupJoin +from ._supports_intersect import SupportsIntersect +from ._supports_join import SupportsJoin +from ._supports_max import SupportsMax +from ._supports_min import SupportsMin +from ._supports_of_type import SupportsOfType +from ._supports_order import SupportsOrder +from ._supports_prepend import SupportsPrepend +from ._supports_reverse import SupportsReverse +from ._supports_select import SupportsSelect +from ._supports_sequence_equal import SupportsSequenceEqual +from ._supports_single import SupportsSingle +from ._supports_skip import SupportsSkip +from ._supports_sum import SupportsSum +from ._supports_take import SupportsTake +from ._supports_union import SupportsUnion +from ._supports_where import SupportsWhere +from ._supports_zip import SupportsZip class Enumerable[TSource]( From b0437be497730bf6df2e95a7cbf0adfcabf9b715 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:24:04 +0330 Subject: [PATCH 3/6] add main implementation and protocol to init modules --- pyenumerable/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyenumerable/__init__.py b/pyenumerable/__init__.py index e69de29..25e4003 100644 --- a/pyenumerable/__init__.py +++ b/pyenumerable/__init__.py @@ -0,0 +1,14 @@ +""" +Implementation of .NET's IEnumerable interface in python W/ support for generics. +""" # noqa: E501 + + +from pyenumerable.implementations import PurePythonEnumerable +from pyenumerable.protocol import Enumerable + +__all__ = ["Enumerable", "PurePythonEnumerable"] +__author__ = "AmirHossein Ahmadi" +__license__ = "WTFPL" +__version__ = "1.0.2" +__maintainer__ = "AmirHossein Ahmadi" +__email__ = "amirthehossein@gmail.com" From dc81cc6d252f329b444ef92efbc32d078dae5f03 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:24:17 +0330 Subject: [PATCH 4/6] turn pp to module --- .../implementations/pure_python/__init__.py | 814 ------------------ 1 file changed, 814 deletions(-) delete mode 100644 pyenumerable/implementations/pure_python/__init__.py diff --git a/pyenumerable/implementations/pure_python/__init__.py b/pyenumerable/implementations/pure_python/__init__.py deleted file mode 100644 index b474bc6..0000000 --- a/pyenumerable/implementations/pure_python/__init__.py +++ /dev/null @@ -1,814 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Iterable, Sequence -from contextlib import suppress -from itertools import chain -from typing import Any, Protocol - -from pyenumerable.protocol import Associable, Enumerable -from pyenumerable.typing_utility import Comparer - - -class PurePythonEnumerable[TSource](Enumerable[TSource]): - def __init__( - self, - *items: TSource, - from_iterable: Iterable[Iterable[TSource]] | None = None, - ) -> None: - self._source: tuple[TSource, ...] = items - - if from_iterable is not None: - self._source += tuple(chain.from_iterable(from_iterable)) - - @property - def source(self) -> tuple[TSource, ...]: - return self._source - - def select[TResult]( - self, - selector: Callable[[int, TSource], TResult], - /, - ) -> Enumerable[TResult]: - return PurePythonEnumerable( - *tuple(selector(i, v) for i, v in enumerate(self.source)), - ) - - def select_many[TResult]( - self, - selector: Callable[[int, TSource], Iterable[TResult]], - /, - ) -> Enumerable[TResult]: - return PurePythonEnumerable( - from_iterable=[selector(i, v) for i, v in enumerate(self.source)], - ) - - def concat( - self, - other: Enumerable[TSource], - /, - ) -> Enumerable[TSource]: - return PurePythonEnumerable(from_iterable=(self.source, other.source)) - - def max_( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) - if comparer is not None: - out = self.source[0] - for item in self.source[1:]: - if comparer(item, out): - 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 - - def max_by[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> TSource: - PurePythonEnumerable._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: - if comparer(key, max_key[1]): - 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 - - def min_( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> TSource: - PurePythonEnumerable._assume_not_empty(self) - if comparer is not None: - out = self.source[0] - for item in self.source[1:]: - if comparer(item, out): - 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 - - def min_by[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> TSource: - PurePythonEnumerable._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: - if comparer(key, min_key[1]): - 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 - - def contains( - self, - item: TSource, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> bool: - return ( - (any(comparer(item, i) for i in self.source)) - if comparer is not None - else item in self.source - ) - - def count_( - self, - predicate: Callable[[TSource], bool] | None = None, - /, - ) -> int: - return ( - sum(1 for i in self.source if predicate(i)) - if predicate is not None - else len(self.source) - ) - - def single( - self, - predicate: Callable[[TSource], bool] | None = None, - /, - ) -> TSource: - if ( - len( - items := tuple( - filter(predicate, self.source), - ) - if predicate is not None - else self.source, - ) - != 1 - ): - msg = ( - "There are zero or more than exactly one item to return; If " - "predicate is given, make sure it filters exactly one item" - ) - raise ValueError(msg) - return items[0] - - def single_or_deafult( - self, - default: TSource, - predicate: Callable[[TSource], bool] | None = None, - /, - ) -> TSource: - if ( - length := len( - items := self.source - if predicate is None - else tuple( - filter(predicate, self.source), - ), - ) - ) > 1: - msg = ( - "There are more than one item to return or fall back to " - "default; If predicate is given, make sure it filters one or " - "zero item" - ) - raise ValueError(msg) - return items[0] if length == 1 else default - - def skip( - self, - start_or_count: int, - end: int | None = None, - /, - ) -> Enumerable[TSource]: - return PurePythonEnumerable( - *( - self.source[:start_or_count] + self.source[end:] - if (end is not None) - else self.source[start_or_count:] - ), - ) - - def skip_last(self, count: int, /) -> Enumerable[TSource]: - return PurePythonEnumerable(*self.source[:-count]) - - def skip_while( - self, - predicate: Callable[[int, TSource], bool], - /, - ) -> Enumerable[TSource]: - start = 0 - for index, item in enumerate(self.source): - start = index - if not predicate(index, item): - break - else: - start += 1 - return PurePythonEnumerable(*self.source[start:]) - - def take( - self, - start_or_count: int, - end: int | None = None, - /, - ) -> Enumerable[TSource]: - return PurePythonEnumerable( - *( - self.source[start_or_count:end] - if (end is not None) - else self.source[:start_or_count] - ), - ) - - def take_last(self, count: int, /) -> Enumerable[TSource]: - return PurePythonEnumerable(*self.source[-count:]) - - def take_while( - self, - predicate: Callable[[int, TSource], bool], - /, - ) -> Enumerable[TSource]: - stop = 0 - for index, item in enumerate(self.source): - stop = index - if not predicate(index, item): - break - else: - stop += 1 - return PurePythonEnumerable(*self.source[:stop]) - - def of_type[TResult]( - self, - type_: type[TResult], - /, - ) -> Enumerable[TResult]: - return PurePythonEnumerable( # type: ignore - *filter(lambda i: isinstance(i, type_), self.source), - ) - - def all( - self, - predicate: Callable[[TSource], bool] | None = None, - /, - ) -> bool: - return all( - (predicate(i) for i in self.source) - if (predicate is not None) - else self.source, - ) - - def any( - self, - predicate: Callable[[TSource], bool] | None = None, - /, - ) -> bool: - 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 where( - self, - predicate: Callable[[int, TSource], bool], - /, - ) -> Enumerable[TSource]: - return PurePythonEnumerable( - *( - en[1] - for en in filter( - lambda i: predicate(i[0], i[1]), - enumerate(self.source), - ) - ), - ) - - def prepend( - self, - element: TSource, - /, - ) -> Enumerable[TSource]: - return PurePythonEnumerable(element, *self.source) - - def append(self, element: TSource, /) -> Enumerable[TSource]: - return PurePythonEnumerable(*self.source, element) - - def distinct( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - if comparer is not None: - out: list[TSource] = [] - for item in self.source: - for captured in out: - if comparer(item, captured): - break - else: - 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 - - def distinct_by[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - 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)): - 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 - return PurePythonEnumerable(*captured_dict.values()) - except TypeError as te: - msg = "TKey doesn't implement __hash__; Comparer isn't given" - raise TypeError(msg) from te - - def order( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - if comparer is not None: - rank_table: dict[int, list[TSource]] = {} - for item in self.source: - rank = 0 - for compared in self.source: - 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 - - def order_descending( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - if comparer is not None: - rank_table: dict[int, list[TSource]] = {} - for item in self.source: - rank = 0 - for compared in self.source: - 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 - - def order_by[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - 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): - 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 - - def order_by_descending[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> Enumerable[TSource]: - if len(self.source) == 0: - return PurePythonEnumerable() - - 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): - 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 - - def zip[TSecond]( - self, - second: Enumerable[TSecond], - /, - ) -> Enumerable[tuple[TSource, TSecond]]: - return PurePythonEnumerable(*zip(self.source, second.source)) - - def reverse(self, /) -> Enumerable[TSource]: - return PurePythonEnumerable(*reversed(self.source)) - - def intersect( - self, - second: Enumerable[TSource], - /, - *, - comparer: Comparer[TSource] = lambda in_, out: in_ == out, - ) -> Enumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 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) - - def intersect_by[TKey]( - self, - second: Enumerable[TKey], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, - ) -> Enumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 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) - - def sequence_equal( - self, - other: Enumerable[TSource], - /, - *, - comparer: Comparer[TSource] = lambda in_, out: in_ == out, - ) -> bool: - if len(self.source) != len(other.source): - return False - return all( - comparer(inner, outer) - for inner, outer in zip(self.source, other.source) - ) - - def except_( - self, - other: Enumerable[TSource], - /, - *, - comparer: Comparer[TSource] = lambda in_, out: in_ == out, - ) -> 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) - - def except_by[TKey]( - self, - other: Enumerable[TSource], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, - ) -> 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) - - 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 - - def chunk(self, size: int, /) -> tuple[PurePythonEnumerable[TSource], ...]: - return tuple( - PurePythonEnumerable(*c) - for c in ( - self.source[i : i + size] - for i in range(0, len(self.source), size) - ) - ) - - def aggregate( - self, - func: Callable[[TSource, TSource], TSource], - /, - *, - 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:]: - curr = func(curr, item) - return curr - - def union( - self, - second: Enumerable[TSource], - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> Enumerable[TSource]: - if comparer is not None: - out: list[TSource] = [] - for inner in self.source: - for captured in out: - if comparer(inner, captured): - break - else: - out.append(inner) - for outer in second.source: - for captured in out: - if comparer(outer, captured): - break - 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 - - def union_by[TKey]( - self, - second: Enumerable[TSource], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, - ) -> 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) - - def group_by[TKey]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] = lambda in_, out: in_ == out, - ) -> Enumerable[Associable[TKey, TSource]]: - keys: list[TKey] = [] - values: dict[int, 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] - return PurePythonEnumerable( - *(PurePythonAssociable(keys[kid], *v) for kid, v in values.items()) - ) - - def join[TInner, TKey, TResult]( - self, - inner: Enumerable[TInner], - outer_key_selector: Callable[[TSource], TKey], - inner_key_selector: Callable[[TInner], TKey], - result_selector: Callable[[TSource, TInner], TResult], - /, - *, - comparer: Comparer[TKey] = lambda out, in_: out == in_, - ) -> Enumerable[TResult]: - out: list[TResult] = [] - 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) - - def group_join[TInner, TKey, TResult]( - self, - inner: Enumerable[TInner], - outer_key_selector: Callable[[TSource], TKey], - inner_key_selector: Callable[[TInner], TKey], - result_selector: Callable[[TSource, Enumerable[TInner]], TResult], - /, - *, - comparer: Comparer[TKey] = lambda out, in_: out == in_, - ) -> 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] = [] - 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) - ] - ) - - @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], - PurePythonEnumerable[TSource], -): - def __init__( - self, - key: TKey, - *items: TSource, - from_iterable: Iterable[Iterable[TSource]] | None = None, - ) -> None: - self._key = key - super().__init__(*items, from_iterable=from_iterable) - - @property - def key(self) -> TKey: - return self._key From b1b141c6b114d650dbf0567ffb7044cb3fb1609d Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:24:44 +0330 Subject: [PATCH 5/6] add pp to impl init --- pyenumerable/implementations/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyenumerable/implementations/__init__.py b/pyenumerable/implementations/__init__.py index e69de29..e210147 100644 --- a/pyenumerable/implementations/__init__.py +++ b/pyenumerable/implementations/__init__.py @@ -0,0 +1,3 @@ +from .pure_python import PurePythonEnumerable + +__all__ = ["PurePythonEnumerable"] From 4e7790018961307f18b864387328a60edd9f6149 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:25:01 +0330 Subject: [PATCH 6/6] turn pp to module --- pyenumerable/implementations/pure_python.py | 814 ++++++++++++++++++++ 1 file changed, 814 insertions(+) create mode 100644 pyenumerable/implementations/pure_python.py diff --git a/pyenumerable/implementations/pure_python.py b/pyenumerable/implementations/pure_python.py new file mode 100644 index 0000000..b474bc6 --- /dev/null +++ b/pyenumerable/implementations/pure_python.py @@ -0,0 +1,814 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable, Sequence +from contextlib import suppress +from itertools import chain +from typing import Any, Protocol + +from pyenumerable.protocol import Associable, Enumerable +from pyenumerable.typing_utility import Comparer + + +class PurePythonEnumerable[TSource](Enumerable[TSource]): + def __init__( + self, + *items: TSource, + from_iterable: Iterable[Iterable[TSource]] | None = None, + ) -> None: + self._source: tuple[TSource, ...] = items + + if from_iterable is not None: + self._source += tuple(chain.from_iterable(from_iterable)) + + @property + def source(self) -> tuple[TSource, ...]: + return self._source + + def select[TResult]( + self, + selector: Callable[[int, TSource], TResult], + /, + ) -> Enumerable[TResult]: + return PurePythonEnumerable( + *tuple(selector(i, v) for i, v in enumerate(self.source)), + ) + + def select_many[TResult]( + self, + selector: Callable[[int, TSource], Iterable[TResult]], + /, + ) -> Enumerable[TResult]: + return PurePythonEnumerable( + from_iterable=[selector(i, v) for i, v in enumerate(self.source)], + ) + + def concat( + self, + other: Enumerable[TSource], + /, + ) -> Enumerable[TSource]: + return PurePythonEnumerable(from_iterable=(self.source, other.source)) + + def max_( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> TSource: + PurePythonEnumerable._assume_not_empty(self) + if comparer is not None: + out = self.source[0] + for item in self.source[1:]: + if comparer(item, out): + 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 + + def max_by[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> TSource: + PurePythonEnumerable._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: + if comparer(key, max_key[1]): + 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 + + def min_( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> TSource: + PurePythonEnumerable._assume_not_empty(self) + if comparer is not None: + out = self.source[0] + for item in self.source[1:]: + if comparer(item, out): + 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 + + def min_by[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> TSource: + PurePythonEnumerable._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: + if comparer(key, min_key[1]): + 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 + + def contains( + self, + item: TSource, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> bool: + return ( + (any(comparer(item, i) for i in self.source)) + if comparer is not None + else item in self.source + ) + + def count_( + self, + predicate: Callable[[TSource], bool] | None = None, + /, + ) -> int: + return ( + sum(1 for i in self.source if predicate(i)) + if predicate is not None + else len(self.source) + ) + + def single( + self, + predicate: Callable[[TSource], bool] | None = None, + /, + ) -> TSource: + if ( + len( + items := tuple( + filter(predicate, self.source), + ) + if predicate is not None + else self.source, + ) + != 1 + ): + msg = ( + "There are zero or more than exactly one item to return; If " + "predicate is given, make sure it filters exactly one item" + ) + raise ValueError(msg) + return items[0] + + def single_or_deafult( + self, + default: TSource, + predicate: Callable[[TSource], bool] | None = None, + /, + ) -> TSource: + if ( + length := len( + items := self.source + if predicate is None + else tuple( + filter(predicate, self.source), + ), + ) + ) > 1: + msg = ( + "There are more than one item to return or fall back to " + "default; If predicate is given, make sure it filters one or " + "zero item" + ) + raise ValueError(msg) + return items[0] if length == 1 else default + + def skip( + self, + start_or_count: int, + end: int | None = None, + /, + ) -> Enumerable[TSource]: + return PurePythonEnumerable( + *( + self.source[:start_or_count] + self.source[end:] + if (end is not None) + else self.source[start_or_count:] + ), + ) + + def skip_last(self, count: int, /) -> Enumerable[TSource]: + return PurePythonEnumerable(*self.source[:-count]) + + def skip_while( + self, + predicate: Callable[[int, TSource], bool], + /, + ) -> Enumerable[TSource]: + start = 0 + for index, item in enumerate(self.source): + start = index + if not predicate(index, item): + break + else: + start += 1 + return PurePythonEnumerable(*self.source[start:]) + + def take( + self, + start_or_count: int, + end: int | None = None, + /, + ) -> Enumerable[TSource]: + return PurePythonEnumerable( + *( + self.source[start_or_count:end] + if (end is not None) + else self.source[:start_or_count] + ), + ) + + def take_last(self, count: int, /) -> Enumerable[TSource]: + return PurePythonEnumerable(*self.source[-count:]) + + def take_while( + self, + predicate: Callable[[int, TSource], bool], + /, + ) -> Enumerable[TSource]: + stop = 0 + for index, item in enumerate(self.source): + stop = index + if not predicate(index, item): + break + else: + stop += 1 + return PurePythonEnumerable(*self.source[:stop]) + + def of_type[TResult]( + self, + type_: type[TResult], + /, + ) -> Enumerable[TResult]: + return PurePythonEnumerable( # type: ignore + *filter(lambda i: isinstance(i, type_), self.source), + ) + + def all( + self, + predicate: Callable[[TSource], bool] | None = None, + /, + ) -> bool: + return all( + (predicate(i) for i in self.source) + if (predicate is not None) + else self.source, + ) + + def any( + self, + predicate: Callable[[TSource], bool] | None = None, + /, + ) -> bool: + 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 where( + self, + predicate: Callable[[int, TSource], bool], + /, + ) -> Enumerable[TSource]: + return PurePythonEnumerable( + *( + en[1] + for en in filter( + lambda i: predicate(i[0], i[1]), + enumerate(self.source), + ) + ), + ) + + def prepend( + self, + element: TSource, + /, + ) -> Enumerable[TSource]: + return PurePythonEnumerable(element, *self.source) + + def append(self, element: TSource, /) -> Enumerable[TSource]: + return PurePythonEnumerable(*self.source, element) + + def distinct( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + if comparer is not None: + out: list[TSource] = [] + for item in self.source: + for captured in out: + if comparer(item, captured): + break + else: + 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 + + def distinct_by[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + 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)): + 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 + return PurePythonEnumerable(*captured_dict.values()) + except TypeError as te: + msg = "TKey doesn't implement __hash__; Comparer isn't given" + raise TypeError(msg) from te + + def order( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + if comparer is not None: + rank_table: dict[int, list[TSource]] = {} + for item in self.source: + rank = 0 + for compared in self.source: + 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 + + def order_descending( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + if comparer is not None: + rank_table: dict[int, list[TSource]] = {} + for item in self.source: + rank = 0 + for compared in self.source: + 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 + + def order_by[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + 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): + 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 + + def order_by_descending[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> Enumerable[TSource]: + if len(self.source) == 0: + return PurePythonEnumerable() + + 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): + 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 + + def zip[TSecond]( + self, + second: Enumerable[TSecond], + /, + ) -> Enumerable[tuple[TSource, TSecond]]: + return PurePythonEnumerable(*zip(self.source, second.source)) + + def reverse(self, /) -> Enumerable[TSource]: + return PurePythonEnumerable(*reversed(self.source)) + + def intersect( + self, + second: Enumerable[TSource], + /, + *, + comparer: Comparer[TSource] = lambda in_, out: in_ == out, + ) -> Enumerable[TSource]: + if len(self.source) == 0 or len(second.source) == 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) + + def intersect_by[TKey]( + self, + second: Enumerable[TKey], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] = lambda in_, out: in_ == out, + ) -> Enumerable[TSource]: + if len(self.source) == 0 or len(second.source) == 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) + + def sequence_equal( + self, + other: Enumerable[TSource], + /, + *, + comparer: Comparer[TSource] = lambda in_, out: in_ == out, + ) -> bool: + if len(self.source) != len(other.source): + return False + return all( + comparer(inner, outer) + for inner, outer in zip(self.source, other.source) + ) + + def except_( + self, + other: Enumerable[TSource], + /, + *, + comparer: Comparer[TSource] = lambda in_, out: in_ == out, + ) -> 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) + + def except_by[TKey]( + self, + other: Enumerable[TSource], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] = lambda in_, out: in_ == out, + ) -> 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) + + 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 + + def chunk(self, size: int, /) -> tuple[PurePythonEnumerable[TSource], ...]: + return tuple( + PurePythonEnumerable(*c) + for c in ( + self.source[i : i + size] + for i in range(0, len(self.source), size) + ) + ) + + def aggregate( + self, + func: Callable[[TSource, TSource], TSource], + /, + *, + 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:]: + curr = func(curr, item) + return curr + + def union( + self, + second: Enumerable[TSource], + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> Enumerable[TSource]: + if comparer is not None: + out: list[TSource] = [] + for inner in self.source: + for captured in out: + if comparer(inner, captured): + break + else: + out.append(inner) + for outer in second.source: + for captured in out: + if comparer(outer, captured): + break + 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 + + def union_by[TKey]( + self, + second: Enumerable[TSource], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] = lambda in_, out: in_ == out, + ) -> 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) + + def group_by[TKey]( + self, + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] = lambda in_, out: in_ == out, + ) -> Enumerable[Associable[TKey, TSource]]: + keys: list[TKey] = [] + values: dict[int, 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] + return PurePythonEnumerable( + *(PurePythonAssociable(keys[kid], *v) for kid, v in values.items()) + ) + + def join[TInner, TKey, TResult]( + self, + inner: Enumerable[TInner], + outer_key_selector: Callable[[TSource], TKey], + inner_key_selector: Callable[[TInner], TKey], + result_selector: Callable[[TSource, TInner], TResult], + /, + *, + comparer: Comparer[TKey] = lambda out, in_: out == in_, + ) -> Enumerable[TResult]: + out: list[TResult] = [] + 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) + + def group_join[TInner, TKey, TResult]( + self, + inner: Enumerable[TInner], + outer_key_selector: Callable[[TSource], TKey], + inner_key_selector: Callable[[TInner], TKey], + result_selector: Callable[[TSource, Enumerable[TInner]], TResult], + /, + *, + comparer: Comparer[TKey] = lambda out, in_: out == in_, + ) -> 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] = [] + 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) + ] + ) + + @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], + PurePythonEnumerable[TSource], +): + def __init__( + self, + key: TKey, + *items: TSource, + from_iterable: Iterable[Iterable[TSource]] | None = None, + ) -> None: + self._key = key + super().__init__(*items, from_iterable=from_iterable) + + @property + def key(self) -> TKey: + return self._key