diff --git a/pyenumerable/implementations/pure_python/__init__.py b/pyenumerable/implementations/pure_python/__init__.py index 1df0692..0cb5026 100644 --- a/pyenumerable/implementations/pure_python/__init__.py +++ b/pyenumerable/implementations/pure_python/__init__.py @@ -1 +1,782 @@ -from ._enumerable import PurePythonEnumerable +from __future__ import annotations + +from collections.abc import Callable, Hashable, Iterable +from contextlib import suppress +from itertools import chain +from typing import Any, Protocol + +from pyenumerable.typing_utility import Comparable, Comparer + + +class PurePythonEnumerable[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], + /, + ) -> PurePythonEnumerable[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]], + /, + ) -> PurePythonEnumerable[TResult]: + return PurePythonEnumerable( + from_iterable=[selector(i, v) for i, v in enumerate(self.source)], + ) + + def concat( + self, + other: PurePythonEnumerable[TSource], + /, + ) -> PurePythonEnumerable[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, + /, + ) -> PurePythonEnumerable[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, /) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable(*self.source[:-count]) + + def skip_while( + self, + predicate: Callable[[int, TSource], bool], + /, + ) -> PurePythonEnumerable[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, + /, + ) -> PurePythonEnumerable[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, /) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable(*self.source[-count:]) + + def take_while( + self, + predicate: Callable[[int, TSource], bool], + /, + ) -> PurePythonEnumerable[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], + /, + ) -> PurePythonEnumerable[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], + /, + ) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable( + *( + en[1] + for en in filter( + lambda i: predicate(i[0], i[1]), + enumerate(self.source), + ) + ), + ) + + def prepend( + self, + element: TSource, + /, + ) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable(element, *self.source) + + def append(self, element: TSource, /) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable(*self.source, element) + + def distinct( + self, + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> PurePythonEnumerable[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, + ) -> PurePythonEnumerable[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, + ) -> PurePythonEnumerable[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, + ) -> PurePythonEnumerable[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, + ) -> PurePythonEnumerable[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, + ) -> PurePythonEnumerable[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: PurePythonEnumerable[TSecond], + /, + ) -> PurePythonEnumerable[tuple[TSource, TSecond]]: + return PurePythonEnumerable(*zip(self.source, second.source)) + + def reverse(self, /) -> PurePythonEnumerable[TSource]: + return PurePythonEnumerable(*reversed(self.source)) + + def intersect( + self, + second: PurePythonEnumerable[TSource], + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> PurePythonEnumerable[TSource]: + if len(self.source) == 0 or len(second.source) == 0: + return PurePythonEnumerable() + comparer_: Comparer[TSource] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + 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: PurePythonEnumerable[TSource], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> PurePythonEnumerable[TSource]: + if len(self.source) == 0 or len(second.source) == 0: + return PurePythonEnumerable() + comparer_: Comparer[TKey] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + out: list[TSource] = [] + for inner in self.source: + inner_key = key_selector(inner) + for outer in second.source: + outer_key = key_selector(outer) + 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: PurePythonEnumerable[TSource], + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> bool: + if len(self.source) != len(other.source): + return False + comparer_: Comparer[TSource] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + return all( + comparer_(inner, outer) + for inner, outer in zip(self.source, other.source) + ) + + def except_( + self, + other: PurePythonEnumerable[TSource], + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> PurePythonEnumerable[TSource]: + comparer_: Comparer[TSource] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + 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: PurePythonEnumerable[TSource], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> PurePythonEnumerable[TSource]: + comparer_: Comparer[TKey] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + 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: PurePythonEnumerable[TSource], + /, + *, + comparer: Comparer[TSource] | None = None, + ) -> PurePythonEnumerable[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: PurePythonEnumerable[TSource], + key_selector: Callable[[TSource], TKey], + /, + *, + comparer: Comparer[TKey] | None = None, + ) -> PurePythonEnumerable[TSource]: + comparer_: Comparer[TKey] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + 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] | None = None, + ) -> PurePythonEnumerable[PurePythonAssociable[TKey, TSource]]: + comparer_: Comparer[TKey] = ( + comparer if comparer is not None else lambda i, o: i == o + ) + 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()) + ) + + @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](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 diff --git a/pyenumerable/implementations/pure_python/_enumerable.py b/pyenumerable/implementations/pure_python/_enumerable.py deleted file mode 100644 index f25c229..0000000 --- a/pyenumerable/implementations/pure_python/_enumerable.py +++ /dev/null @@ -1,742 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Hashable, Iterable -from contextlib import suppress -from itertools import chain -from typing import Any, Protocol - -from pyenumerable.typing_utility import Comparable, Comparer - - -class PurePythonEnumerable[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], - /, - ) -> PurePythonEnumerable[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]], - /, - ) -> PurePythonEnumerable[TResult]: - return PurePythonEnumerable( - from_iterable=[selector(i, v) for i, v in enumerate(self.source)], - ) - - def concat( - self, - other: PurePythonEnumerable[TSource], - /, - ) -> PurePythonEnumerable[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, - /, - ) -> PurePythonEnumerable[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, /) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable(*self.source[:-count]) - - def skip_while( - self, - predicate: Callable[[int, TSource], bool], - /, - ) -> PurePythonEnumerable[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, - /, - ) -> PurePythonEnumerable[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, /) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable(*self.source[-count:]) - - def take_while( - self, - predicate: Callable[[int, TSource], bool], - /, - ) -> PurePythonEnumerable[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], - /, - ) -> PurePythonEnumerable[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], - /, - ) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable( - *( - en[1] - for en in filter( - lambda i: predicate(i[0], i[1]), - enumerate(self.source), - ) - ), - ) - - def prepend( - self, - element: TSource, - /, - ) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable(element, *self.source) - - def append(self, element: TSource, /) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable(*self.source, element) - - def distinct( - self, - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> PurePythonEnumerable[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, - ) -> PurePythonEnumerable[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, - ) -> PurePythonEnumerable[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, - ) -> PurePythonEnumerable[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, - ) -> PurePythonEnumerable[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, - ) -> PurePythonEnumerable[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: PurePythonEnumerable[TSecond], - /, - ) -> PurePythonEnumerable[tuple[TSource, TSecond]]: - return PurePythonEnumerable(*zip(self.source, second.source)) - - def reverse(self, /) -> PurePythonEnumerable[TSource]: - return PurePythonEnumerable(*reversed(self.source)) - - def intersect( - self, - second: PurePythonEnumerable[TSource], - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> PurePythonEnumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 0: - return PurePythonEnumerable() - comparer_: Comparer[TSource] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - 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: PurePythonEnumerable[TSource], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> PurePythonEnumerable[TSource]: - if len(self.source) == 0 or len(second.source) == 0: - return PurePythonEnumerable() - comparer_: Comparer[TKey] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - out: list[TSource] = [] - for inner in self.source: - inner_key = key_selector(inner) - for outer in second.source: - outer_key = key_selector(outer) - 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: PurePythonEnumerable[TSource], - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> bool: - if len(self.source) != len(other.source): - return False - comparer_: Comparer[TSource] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - return all( - comparer_(inner, outer) - for inner, outer in zip(self.source, other.source) - ) - - def except_( - self, - other: PurePythonEnumerable[TSource], - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> PurePythonEnumerable[TSource]: - comparer_: Comparer[TSource] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - 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: PurePythonEnumerable[TSource], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> PurePythonEnumerable[TSource]: - comparer_: Comparer[TKey] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - 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: PurePythonEnumerable[TSource], - /, - *, - comparer: Comparer[TSource] | None = None, - ) -> PurePythonEnumerable[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: PurePythonEnumerable[TSource], - key_selector: Callable[[TSource], TKey], - /, - *, - comparer: Comparer[TKey] | None = None, - ) -> PurePythonEnumerable[TSource]: - comparer_: Comparer[TKey] = ( - comparer if comparer is not None else lambda i, o: i == o - ) - 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) - - @staticmethod - def _assume_not_empty(instance: PurePythonEnumerable[Any]) -> None: - if len(instance.source) == 0: - msg = "Enumerable (self) is empty" - raise ValueError(msg) diff --git a/pyenumerable/protocol/_supports_group_by.py b/pyenumerable/protocol/_supports_group_by.py index 9618d3e..5b9cf69 100644 --- a/pyenumerable/protocol/_supports_group_by.py +++ b/pyenumerable/protocol/_supports_group_by.py @@ -9,65 +9,6 @@ class SupportsGroupBy[TSource](Protocol): - @overload - def group_by[TKey: Comparable, TElement, TResult]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - element_selector: Callable[[TSource], TElement], - result_selector: Callable[[TKey, "Enumerable[TElement]"], TResult], - ) -> "Enumerable[TResult]": ... - - @overload - def group_by[TKey, TElement, TResult]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - element_selector: Callable[[TSource], TElement], - result_selector: Callable[[TKey, "Enumerable[TElement]"], TResult], - comparer: Comparer[TKey], - ) -> "Enumerable[TResult]": ... - - @overload - def group_by[TKey: Comparable, TElement]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - element_selector: Callable[[TSource], TElement], - ) -> "Enumerable[Associable[TKey, TElement]]": ... - - @overload - def group_by[TKey, TElement]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - element_selector: Callable[[TSource], TElement], - comparer: Comparer[TKey], - ) -> "Enumerable[Associable[TKey, TElement]]": ... - - @overload - def group_by[TKey: Comparable, TResult]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - result_selector: Callable[[TKey, "Enumerable[TSource]"], TResult], - ) -> "Enumerable[TResult]": ... - - @overload - def group_by[TKey, TResult]( - self, - key_selector: Callable[[TSource], TKey], - /, - *, - result_selector: Callable[[TKey, "Enumerable[TSource]"], TResult], - comparer: Comparer[TKey], - ) -> "Enumerable[TResult]": ... - @overload def group_by[TKey: Comparable]( self, diff --git a/pyproject.toml b/pyproject.toml index aa88466..ceaeec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,7 @@ lint.fixable = ["ALL"] typeCheckingMode = "strict" reportUnusedImport = false reportUnusedVariable = false + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["test/unit"] diff --git a/readme.md b/readme.md index 531d12d..c277d90 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ - [ ] Join - [x] Intersect - [ ] Group join - - [ ] Group by + - [x] Group by - [x] Prepend - [x] Order - [x] Min diff --git a/test/unit/pure_python/__init__.py b/test/unit/pure_python/associable/__init__.py similarity index 100% rename from test/unit/pure_python/__init__.py rename to test/unit/pure_python/associable/__init__.py diff --git a/test/unit/pure_python/aggregate/__init__.py b/test/unit/pure_python/associable/constructor/__init__.py similarity index 100% rename from test/unit/pure_python/aggregate/__init__.py rename to test/unit/pure_python/associable/constructor/__init__.py diff --git a/test/unit/pure_python/associable/constructor/test_constructor.py b/test/unit/pure_python/associable/constructor/test_constructor.py new file mode 100644 index 0000000..f4f1051 --- /dev/null +++ b/test/unit/pure_python/associable/constructor/test_constructor.py @@ -0,0 +1,10 @@ +from pyenumerable.implementations.pure_python import PurePythonAssociable + + +class TestConstructor: + def test_initialization(self) -> None: + obj: PurePythonAssociable[str, int] = PurePythonAssociable( + key := "Key" + ) + + assert obj.key == key diff --git a/test/unit/pure_python/all/__init__.py b/test/unit/pure_python/enumerable/__init__.py similarity index 100% rename from test/unit/pure_python/all/__init__.py rename to test/unit/pure_python/enumerable/__init__.py diff --git a/test/unit/pure_python/any/__init__.py b/test/unit/pure_python/enumerable/aggregate/__init__.py similarity index 100% rename from test/unit/pure_python/any/__init__.py rename to test/unit/pure_python/enumerable/aggregate/__init__.py diff --git a/test/unit/pure_python/aggregate/test_aggregate_method.py b/test/unit/pure_python/enumerable/aggregate/test_aggregate_method.py similarity index 100% rename from test/unit/pure_python/aggregate/test_aggregate_method.py rename to test/unit/pure_python/enumerable/aggregate/test_aggregate_method.py diff --git a/test/unit/pure_python/append/__init__.py b/test/unit/pure_python/enumerable/all/__init__.py similarity index 100% rename from test/unit/pure_python/append/__init__.py rename to test/unit/pure_python/enumerable/all/__init__.py diff --git a/test/unit/pure_python/all/test_all_method.py b/test/unit/pure_python/enumerable/all/test_all_method.py similarity index 100% rename from test/unit/pure_python/all/test_all_method.py rename to test/unit/pure_python/enumerable/all/test_all_method.py diff --git a/test/unit/pure_python/average/__init__.py b/test/unit/pure_python/enumerable/any/__init__.py similarity index 100% rename from test/unit/pure_python/average/__init__.py rename to test/unit/pure_python/enumerable/any/__init__.py diff --git a/test/unit/pure_python/any/test_any_method.py b/test/unit/pure_python/enumerable/any/test_any_method.py similarity index 100% rename from test/unit/pure_python/any/test_any_method.py rename to test/unit/pure_python/enumerable/any/test_any_method.py diff --git a/test/unit/pure_python/chunk_/__init__.py b/test/unit/pure_python/enumerable/append/__init__.py similarity index 100% rename from test/unit/pure_python/chunk_/__init__.py rename to test/unit/pure_python/enumerable/append/__init__.py diff --git a/test/unit/pure_python/append/test_append_method.py b/test/unit/pure_python/enumerable/append/test_append_method.py similarity index 100% rename from test/unit/pure_python/append/test_append_method.py rename to test/unit/pure_python/enumerable/append/test_append_method.py diff --git a/test/unit/pure_python/concat/__init__.py b/test/unit/pure_python/enumerable/average/__init__.py similarity index 100% rename from test/unit/pure_python/concat/__init__.py rename to test/unit/pure_python/enumerable/average/__init__.py diff --git a/test/unit/pure_python/average/test_average_method.py b/test/unit/pure_python/enumerable/average/test_average_method.py similarity index 100% rename from test/unit/pure_python/average/test_average_method.py rename to test/unit/pure_python/enumerable/average/test_average_method.py diff --git a/test/unit/pure_python/contains/__init__.py b/test/unit/pure_python/enumerable/chunk_/__init__.py similarity index 100% rename from test/unit/pure_python/contains/__init__.py rename to test/unit/pure_python/enumerable/chunk_/__init__.py diff --git a/test/unit/pure_python/chunk_/test_chunk_method.py b/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py similarity index 100% rename from test/unit/pure_python/chunk_/test_chunk_method.py rename to test/unit/pure_python/enumerable/chunk_/test_chunk_method.py diff --git a/test/unit/pure_python/count/__init__.py b/test/unit/pure_python/enumerable/concat/__init__.py similarity index 100% rename from test/unit/pure_python/count/__init__.py rename to test/unit/pure_python/enumerable/concat/__init__.py diff --git a/test/unit/pure_python/concat/test_concat_method.py b/test/unit/pure_python/enumerable/concat/test_concat_method.py similarity index 100% rename from test/unit/pure_python/concat/test_concat_method.py rename to test/unit/pure_python/enumerable/concat/test_concat_method.py diff --git a/test/unit/pure_python/distinct/__init__.py b/test/unit/pure_python/enumerable/constructor/__init__.py similarity index 100% rename from test/unit/pure_python/distinct/__init__.py rename to test/unit/pure_python/enumerable/constructor/__init__.py diff --git a/test/unit/pure_python/source_property/test_source_property.py b/test/unit/pure_python/enumerable/constructor/test_constructor.py similarity index 100% rename from test/unit/pure_python/source_property/test_source_property.py rename to test/unit/pure_python/enumerable/constructor/test_constructor.py diff --git a/test/unit/pure_python/except_/__init__.py b/test/unit/pure_python/enumerable/contains/__init__.py similarity index 100% rename from test/unit/pure_python/except_/__init__.py rename to test/unit/pure_python/enumerable/contains/__init__.py diff --git a/test/unit/pure_python/contains/test_contains_method.py b/test/unit/pure_python/enumerable/contains/test_contains_method.py similarity index 59% rename from test/unit/pure_python/contains/test_contains_method.py rename to test/unit/pure_python/enumerable/contains/test_contains_method.py index 1848153..b7ca819 100644 --- a/test/unit/pure_python/contains/test_contains_method.py +++ b/test/unit/pure_python/enumerable/contains/test_contains_method.py @@ -3,17 +3,7 @@ class TestContainsMethod: - def test_positive_without_comparer(self) -> None: - existing_item = "existing-item" - obj = PurePythonEnumerable( - "not-being-tested", - "also-not-being-tested", - existing_item, - ) - - assert obj.contains(existing_item) is True - - def test_positive_with_comparer(self) -> None: + def test_positive(self) -> None: obj = PurePythonEnumerable( Person("john doe", 14), Person("jane doe", 12), @@ -28,15 +18,7 @@ def test_positive_with_comparer(self) -> None: is True ) - def test_negative_without_comparer(self) -> None: - obj = PurePythonEnumerable( - "not-being-tested", - "also-not-being-tested", - ) - - assert obj.contains("non-existing-item") is False - - def test_negative_with_comparer(self) -> None: + def test_negative(self) -> None: obj = PurePythonEnumerable( Person("john doe", 14), Person("jane doe", 12), diff --git a/test/unit/pure_python/intersect/__init__.py b/test/unit/pure_python/enumerable/count/__init__.py similarity index 100% rename from test/unit/pure_python/intersect/__init__.py rename to test/unit/pure_python/enumerable/count/__init__.py diff --git a/test/unit/pure_python/count/test_count_method.py b/test/unit/pure_python/enumerable/count/test_count_method.py similarity index 100% rename from test/unit/pure_python/count/test_count_method.py rename to test/unit/pure_python/enumerable/count/test_count_method.py diff --git a/test/unit/pure_python/max/__init__.py b/test/unit/pure_python/enumerable/distinct/__init__.py similarity index 100% rename from test/unit/pure_python/max/__init__.py rename to test/unit/pure_python/enumerable/distinct/__init__.py diff --git a/test/unit/pure_python/distinct/test_distinct_by_method.py b/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py similarity index 100% rename from test/unit/pure_python/distinct/test_distinct_by_method.py rename to test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py diff --git a/test/unit/pure_python/distinct/test_distinct_method.py b/test/unit/pure_python/enumerable/distinct/test_distinct_method.py similarity index 100% rename from test/unit/pure_python/distinct/test_distinct_method.py rename to test/unit/pure_python/enumerable/distinct/test_distinct_method.py diff --git a/test/unit/pure_python/min/__init__.py b/test/unit/pure_python/enumerable/except_/__init__.py similarity index 100% rename from test/unit/pure_python/min/__init__.py rename to test/unit/pure_python/enumerable/except_/__init__.py 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 new file mode 100644 index 0000000..1181391 --- /dev/null +++ b/test/unit/pure_python/enumerable/except_/test_except_by_method.py @@ -0,0 +1,19 @@ +from pyenumerable.implementations.pure_python import PurePythonEnumerable +from test.unit.pure_python.test_utility import Point + + +class TestExceptByMethod: + def test_except_by(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) + + assert res.source == (first, second) diff --git a/test/unit/pure_python/except_/test_except_method.py b/test/unit/pure_python/enumerable/except_/test_except_method.py similarity index 68% rename from test/unit/pure_python/except_/test_except_method.py rename to test/unit/pure_python/enumerable/except_/test_except_method.py index 10ee67a..e381a18 100644 --- a/test/unit/pure_python/except_/test_except_method.py +++ b/test/unit/pure_python/enumerable/except_/test_except_method.py @@ -3,15 +3,7 @@ class TestExceptMethod: - def test_without_comparer(self) -> None: - first_object = PurePythonEnumerable(*range(7)) - second_object = PurePythonEnumerable(*range(start := 3, 9)) - - res = first_object.except_(second_object) - - assert res.source == tuple(range(start)) - - def test_with_comparer(self) -> None: + def test_except(self) -> None: first_object = PurePythonEnumerable( Point(0, 1), first := Point(1, 3), diff --git a/test/unit/pure_python/of_type/__init__.py b/test/unit/pure_python/enumerable/group_by/__init__.py similarity index 100% rename from test/unit/pure_python/of_type/__init__.py rename to test/unit/pure_python/enumerable/group_by/__init__.py 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 new file mode 100644 index 0000000..539911a --- /dev/null +++ b/test/unit/pure_python/enumerable/group_by/test_group_by_method.py @@ -0,0 +1,40 @@ +from pyenumerable.implementations.pure_python import PurePythonEnumerable +from test.unit.pure_python.test_utility import Point + + +class TestGroupByMethod: + def test_group_by(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 + obj = PurePythonEnumerable( + *( + Point(i, first_group_key) + for i in range(number_of_first_group_members) + ), + *( + Point(i, second_group_key) + for i in range(number_of_second_group_members) + ), + ) + + res = obj.group_by(lambda point: point.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_group_members + ) + assert ( + len(res.source[second_group_index].source) + == number_of_second_group_members + ) + assert all( + p.y == first_group_key + for p in res.source[first_group_index].source + ) + assert all( + p.y == second_group_key + for p in res.source[second_group_index].source + ) diff --git a/test/unit/pure_python/order/__init__.py b/test/unit/pure_python/enumerable/intersect/__init__.py similarity index 100% rename from test/unit/pure_python/order/__init__.py rename to test/unit/pure_python/enumerable/intersect/__init__.py diff --git a/test/unit/pure_python/intersect/test_intersect_by_method.py b/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py similarity index 66% rename from test/unit/pure_python/intersect/test_intersect_by_method.py rename to test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py index b1d5841..8ef0650 100644 --- a/test/unit/pure_python/intersect/test_intersect_by_method.py +++ b/test/unit/pure_python/enumerable/intersect/test_intersect_by_method.py @@ -19,31 +19,7 @@ def test_when_second_empty(self) -> None: assert res.source == () - def test_without_comparer(self) -> None: - first_object = PurePythonEnumerable( - first := Person("john doe", 12, Person("marray doe", 27)), - Person("jane doe", 10, Person("larry doe", 31)), - Person("james doe", 11, None), - second := Person("jacob doe", 17, Person("harry doe", 41)), - third := Person(" doe", 14, Person("jerry doe", 34)), - ) - second_object = PurePythonEnumerable( - Person("john doe", 12, Person("arry doe", 27)), - Person("jane doe", 10, Person("curry doe", 35)), - Person("jacob doe", 17, Person("harry doe", 41)), - Person(" doe", 14, Person("jerry doe", 34)), - ) - - res = first_object.intersect_by( - second_object, - lambda person: None - if person.parent is None - else person.parent.age, - ) - - assert res.source == (first, second, third) - - def test_with_comparer(self) -> None: + def test_intersect_by(self) -> None: first_object = PurePythonEnumerable( first := Point(5, 1), Point(3, 3), diff --git a/test/unit/pure_python/intersect/test_intersect_method.py b/test/unit/pure_python/enumerable/intersect/test_intersect_method.py similarity index 85% rename from test/unit/pure_python/intersect/test_intersect_method.py rename to test/unit/pure_python/enumerable/intersect/test_intersect_method.py index 7613d62..5b99343 100644 --- a/test/unit/pure_python/intersect/test_intersect_method.py +++ b/test/unit/pure_python/enumerable/intersect/test_intersect_method.py @@ -19,15 +19,7 @@ def test_when_second_empty(self) -> None: assert res.source == () - def test_without_comparer(self) -> None: - first_object = PurePythonEnumerable(*range(end := 7)) - second_object = PurePythonEnumerable(*range(start := 3, 9)) - - res = first_object.intersect(second_object) - - assert res.source == tuple(range(start, end)) - - def test_with_comparer(self) -> None: + def test_intersect(self) -> None: first_object = PurePythonEnumerable( first := Point(0, 1), Point(0, 3), diff --git a/test/unit/pure_python/prepend/__init__.py b/test/unit/pure_python/enumerable/max/__init__.py similarity index 100% rename from test/unit/pure_python/prepend/__init__.py rename to test/unit/pure_python/enumerable/max/__init__.py diff --git a/test/unit/pure_python/max/test_max_by_method.py b/test/unit/pure_python/enumerable/max/test_max_by_method.py similarity index 78% rename from test/unit/pure_python/max/test_max_by_method.py rename to test/unit/pure_python/enumerable/max/test_max_by_method.py index a9d5617..14c8f3f 100644 --- a/test/unit/pure_python/max/test_max_by_method.py +++ b/test/unit/pure_python/enumerable/max/test_max_by_method.py @@ -20,7 +20,7 @@ def test_exc_raise_when_uncomparable(self) -> None: with pytest.raises(TypeError): obj.max_by(lambda person: person.parent) - def test_with_comparer(self) -> None: + def test_max_by(self) -> None: obj = PurePythonEnumerable( Person("jane doe", 12, Person("james doe", 34)), maximum := Person("john doe", 4, Person("jessie doe", 40)), @@ -33,16 +33,6 @@ def test_with_comparer(self) -> None: assert maximum is res - def test_without_comparer(self) -> None: - obj = PurePythonEnumerable( - maximum := Person("jane doe", 12, Person("james doe", 34)), - Person("john doe", 4, Person("jessie doe", 40)), - ) - - res = obj.max_by(lambda person: person.age) - - assert res is maximum - @staticmethod def age_comparer( first: Person | None, diff --git a/test/unit/pure_python/max/test_max_method.py b/test/unit/pure_python/enumerable/max/test_max_method.py similarity index 77% rename from test/unit/pure_python/max/test_max_method.py rename to test/unit/pure_python/enumerable/max/test_max_method.py index 6e6574c..e7e8180 100644 --- a/test/unit/pure_python/max/test_max_method.py +++ b/test/unit/pure_python/enumerable/max/test_max_method.py @@ -17,16 +17,9 @@ def test_exc_raise_when_uncomparable(self) -> None: with pytest.raises(TypeError): obj.max_() - def test_with_comparer(self) -> None: + def test_max(self) -> None: obj = PurePythonEnumerable(Point(4, 5), maximum := Point(6, 3)) res = obj.max_(comparer=lambda first, second: first.x > second.x) assert res is maximum - - def test_without_comparer(self) -> None: - obj = PurePythonEnumerable(6, 8, maximum := 10, 9, 7) - - res = obj.max_() - - assert res == maximum diff --git a/test/unit/pure_python/reverse/__init__.py b/test/unit/pure_python/enumerable/min/__init__.py similarity index 100% rename from test/unit/pure_python/reverse/__init__.py rename to test/unit/pure_python/enumerable/min/__init__.py diff --git a/test/unit/pure_python/min/test_min_by_method.py b/test/unit/pure_python/enumerable/min/test_min_by_method.py similarity index 78% rename from test/unit/pure_python/min/test_min_by_method.py rename to test/unit/pure_python/enumerable/min/test_min_by_method.py index 0c7973a..62462d7 100644 --- a/test/unit/pure_python/min/test_min_by_method.py +++ b/test/unit/pure_python/enumerable/min/test_min_by_method.py @@ -20,7 +20,7 @@ def test_exc_raise_when_uncomparable(self) -> None: with pytest.raises(TypeError): obj.min_by(lambda person: person.parent) - def test_with_comparer(self) -> None: + def test_min_by(self) -> None: obj = PurePythonEnumerable( Person("john doe", 4, Person("jessie doe", 40)), minimum := Person("jane doe", 12, Person("james doe", 34)), @@ -33,16 +33,6 @@ def test_with_comparer(self) -> None: assert minimum is res - def test_without_comparer(self) -> None: - obj = PurePythonEnumerable( - Person("jane doe", 12, Person("james doe", 34)), - minimum := Person("john doe", 4, Person("jessie doe", 40)), - ) - - res = obj.min_by(lambda person: person.age) - - assert res is minimum - @staticmethod def age_comparer( first: Person | None, diff --git a/test/unit/pure_python/min/test_min_method.py b/test/unit/pure_python/enumerable/min/test_min_method.py similarity index 77% rename from test/unit/pure_python/min/test_min_method.py rename to test/unit/pure_python/enumerable/min/test_min_method.py index 67a5006..b2c898f 100644 --- a/test/unit/pure_python/min/test_min_method.py +++ b/test/unit/pure_python/enumerable/min/test_min_method.py @@ -17,16 +17,9 @@ def test_exc_raise_when_uncomparable(self) -> None: with pytest.raises(TypeError): obj.min_() - def test_with_comparer(self) -> None: + def test_min(self) -> None: obj = PurePythonEnumerable(Point(6, 3), minimum := Point(4, 5)) res = obj.min_(comparer=lambda first, second: first.x < second.x) assert res is minimum - - def test_without_comparer(self) -> None: - obj = PurePythonEnumerable(minimum := 6, 8, 10, 9, 7) - - res = obj.min_() - - assert res == minimum diff --git a/test/unit/pure_python/select_/__init__.py b/test/unit/pure_python/enumerable/of_type/__init__.py similarity index 100% rename from test/unit/pure_python/select_/__init__.py rename to test/unit/pure_python/enumerable/of_type/__init__.py diff --git a/test/unit/pure_python/of_type/test_of_type_method.py b/test/unit/pure_python/enumerable/of_type/test_of_type_method.py similarity index 100% rename from test/unit/pure_python/of_type/test_of_type_method.py rename to test/unit/pure_python/enumerable/of_type/test_of_type_method.py diff --git a/test/unit/pure_python/sequence_equal/__init__.py b/test/unit/pure_python/enumerable/order/__init__.py similarity index 100% rename from test/unit/pure_python/sequence_equal/__init__.py rename to test/unit/pure_python/enumerable/order/__init__.py diff --git a/test/unit/pure_python/order/test_order_by_descending_method.py b/test/unit/pure_python/enumerable/order/test_order_by_descending_method.py similarity index 100% rename from test/unit/pure_python/order/test_order_by_descending_method.py rename to test/unit/pure_python/enumerable/order/test_order_by_descending_method.py diff --git a/test/unit/pure_python/order/test_order_by_method.py b/test/unit/pure_python/enumerable/order/test_order_by_method.py similarity index 100% rename from test/unit/pure_python/order/test_order_by_method.py rename to test/unit/pure_python/enumerable/order/test_order_by_method.py diff --git a/test/unit/pure_python/order/test_order_descending_method.py b/test/unit/pure_python/enumerable/order/test_order_descending_method.py similarity index 100% rename from test/unit/pure_python/order/test_order_descending_method.py rename to test/unit/pure_python/enumerable/order/test_order_descending_method.py diff --git a/test/unit/pure_python/order/test_order_method.py b/test/unit/pure_python/enumerable/order/test_order_method.py similarity index 100% rename from test/unit/pure_python/order/test_order_method.py rename to test/unit/pure_python/enumerable/order/test_order_method.py diff --git a/test/unit/pure_python/single/__init__.py b/test/unit/pure_python/enumerable/prepend/__init__.py similarity index 100% rename from test/unit/pure_python/single/__init__.py rename to test/unit/pure_python/enumerable/prepend/__init__.py diff --git a/test/unit/pure_python/prepend/test_prepend_method.py b/test/unit/pure_python/enumerable/prepend/test_prepend_method.py similarity index 100% rename from test/unit/pure_python/prepend/test_prepend_method.py rename to test/unit/pure_python/enumerable/prepend/test_prepend_method.py diff --git a/test/unit/pure_python/skip/__init__.py b/test/unit/pure_python/enumerable/reverse/__init__.py similarity index 100% rename from test/unit/pure_python/skip/__init__.py rename to test/unit/pure_python/enumerable/reverse/__init__.py diff --git a/test/unit/pure_python/reverse/test_reverse_method.py b/test/unit/pure_python/enumerable/reverse/test_reverse_method.py similarity index 100% rename from test/unit/pure_python/reverse/test_reverse_method.py rename to test/unit/pure_python/enumerable/reverse/test_reverse_method.py diff --git a/test/unit/pure_python/source_property/__init__.py b/test/unit/pure_python/enumerable/select_/__init__.py similarity index 100% rename from test/unit/pure_python/source_property/__init__.py rename to test/unit/pure_python/enumerable/select_/__init__.py diff --git a/test/unit/pure_python/select_/test_select_many_method.py b/test/unit/pure_python/enumerable/select_/test_select_many_method.py similarity index 100% rename from test/unit/pure_python/select_/test_select_many_method.py rename to test/unit/pure_python/enumerable/select_/test_select_many_method.py diff --git a/test/unit/pure_python/select_/test_select_method.py b/test/unit/pure_python/enumerable/select_/test_select_method.py similarity index 100% rename from test/unit/pure_python/select_/test_select_method.py rename to test/unit/pure_python/enumerable/select_/test_select_method.py diff --git a/test/unit/pure_python/sum/__init__.py b/test/unit/pure_python/enumerable/sequence_equal/__init__.py similarity index 100% rename from test/unit/pure_python/sum/__init__.py rename to test/unit/pure_python/enumerable/sequence_equal/__init__.py diff --git a/test/unit/pure_python/sequence_equal/test_sequence_equal_method.py b/test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py similarity index 100% rename from test/unit/pure_python/sequence_equal/test_sequence_equal_method.py rename to test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py diff --git a/test/unit/pure_python/take/__init__.py b/test/unit/pure_python/enumerable/single/__init__.py similarity index 100% rename from test/unit/pure_python/take/__init__.py rename to test/unit/pure_python/enumerable/single/__init__.py diff --git a/test/unit/pure_python/single/test_single_method.py b/test/unit/pure_python/enumerable/single/test_single_method.py similarity index 100% rename from test/unit/pure_python/single/test_single_method.py rename to test/unit/pure_python/enumerable/single/test_single_method.py diff --git a/test/unit/pure_python/single/test_single_or_default_method.py b/test/unit/pure_python/enumerable/single/test_single_or_default_method.py similarity index 100% rename from test/unit/pure_python/single/test_single_or_default_method.py rename to test/unit/pure_python/enumerable/single/test_single_or_default_method.py diff --git a/test/unit/pure_python/union/__init__.py b/test/unit/pure_python/enumerable/skip/__init__.py similarity index 100% rename from test/unit/pure_python/union/__init__.py rename to test/unit/pure_python/enumerable/skip/__init__.py diff --git a/test/unit/pure_python/skip/test_skip_last_method.py b/test/unit/pure_python/enumerable/skip/test_skip_last_method.py similarity index 100% rename from test/unit/pure_python/skip/test_skip_last_method.py rename to test/unit/pure_python/enumerable/skip/test_skip_last_method.py diff --git a/test/unit/pure_python/skip/test_skip_method.py b/test/unit/pure_python/enumerable/skip/test_skip_method.py similarity index 100% rename from test/unit/pure_python/skip/test_skip_method.py rename to test/unit/pure_python/enumerable/skip/test_skip_method.py diff --git a/test/unit/pure_python/skip/test_skip_while_method.py b/test/unit/pure_python/enumerable/skip/test_skip_while_method.py similarity index 100% rename from test/unit/pure_python/skip/test_skip_while_method.py rename to test/unit/pure_python/enumerable/skip/test_skip_while_method.py diff --git a/test/unit/pure_python/where/__init__.py b/test/unit/pure_python/enumerable/sum/__init__.py similarity index 100% rename from test/unit/pure_python/where/__init__.py rename to test/unit/pure_python/enumerable/sum/__init__.py diff --git a/test/unit/pure_python/sum/test_sum_method.py b/test/unit/pure_python/enumerable/sum/test_sum_method.py similarity index 100% rename from test/unit/pure_python/sum/test_sum_method.py rename to test/unit/pure_python/enumerable/sum/test_sum_method.py diff --git a/test/unit/pure_python/zip/__init__.py b/test/unit/pure_python/enumerable/take/__init__.py similarity index 100% rename from test/unit/pure_python/zip/__init__.py rename to test/unit/pure_python/enumerable/take/__init__.py diff --git a/test/unit/pure_python/take/test_take_last_method.py b/test/unit/pure_python/enumerable/take/test_take_last_method.py similarity index 100% rename from test/unit/pure_python/take/test_take_last_method.py rename to test/unit/pure_python/enumerable/take/test_take_last_method.py diff --git a/test/unit/pure_python/take/test_take_method.py b/test/unit/pure_python/enumerable/take/test_take_method.py similarity index 100% rename from test/unit/pure_python/take/test_take_method.py rename to test/unit/pure_python/enumerable/take/test_take_method.py diff --git a/test/unit/pure_python/take/test_take_while_method.py b/test/unit/pure_python/enumerable/take/test_take_while_method.py similarity index 100% rename from test/unit/pure_python/take/test_take_while_method.py rename to test/unit/pure_python/enumerable/take/test_take_while_method.py diff --git a/test/unit/pure_python/enumerable/union/__init__.py b/test/unit/pure_python/enumerable/union/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/pure_python/union/test_union_by_method.py b/test/unit/pure_python/enumerable/union/test_union_by_method.py similarity index 72% rename from test/unit/pure_python/union/test_union_by_method.py rename to test/unit/pure_python/enumerable/union/test_union_by_method.py index adcf256..0945c94 100644 --- a/test/unit/pure_python/union/test_union_by_method.py +++ b/test/unit/pure_python/enumerable/union/test_union_by_method.py @@ -3,31 +3,7 @@ class TestUnionByMethod: - 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_with_comparer(self) -> None: + def test_union_by(self) -> None: first_object = PurePythonEnumerable( *( first_items := ( diff --git a/test/unit/pure_python/union/test_union_method.py b/test/unit/pure_python/enumerable/union/test_union_method.py similarity index 81% rename from test/unit/pure_python/union/test_union_method.py rename to test/unit/pure_python/enumerable/union/test_union_method.py index 05b01cf..e1d7756 100644 --- a/test/unit/pure_python/union/test_union_method.py +++ b/test/unit/pure_python/enumerable/union/test_union_method.py @@ -12,17 +12,7 @@ def test_exc_raise_when_unhashable(self) -> None: with pytest.raises(TypeError): first_object.union(second_object) - def test_without_comparer(self) -> None: - first_object = PurePythonEnumerable(*(first_items := tuple(range(3)))) - second_object = PurePythonEnumerable( - *(second_items := tuple(range(7, 10))) - ) - - res = first_object.union(second_object) - - assert res.source == first_items + second_items - - def test_with_comparer(self) -> None: + def test_union(self) -> None: first_object = PurePythonEnumerable(*(items := tuple(range(7)))) second_object = PurePythonEnumerable(*(-i for i in items)) diff --git a/test/unit/pure_python/enumerable/where/__init__.py b/test/unit/pure_python/enumerable/where/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/pure_python/where/test_where_method.py b/test/unit/pure_python/enumerable/where/test_where_method.py similarity index 100% rename from test/unit/pure_python/where/test_where_method.py rename to test/unit/pure_python/enumerable/where/test_where_method.py diff --git a/test/unit/pure_python/enumerable/zip/__init__.py b/test/unit/pure_python/enumerable/zip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/pure_python/zip/test_zip_method.py b/test/unit/pure_python/enumerable/zip/test_zip_method.py similarity index 100% rename from test/unit/pure_python/zip/test_zip_method.py rename to test/unit/pure_python/enumerable/zip/test_zip_method.py diff --git a/test/unit/pure_python/except_/test_except_by_method.py b/test/unit/pure_python/except_/test_except_by_method.py deleted file mode 100644 index c448a96..0000000 --- a/test/unit/pure_python/except_/test_except_by_method.py +++ /dev/null @@ -1,38 +0,0 @@ -from pyenumerable.implementations.pure_python import PurePythonEnumerable -from test.unit.pure_python.test_utility import Point - - -class TestExceptByMethod: - 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) - - assert res.source == (first, second) - - def test_without_comparer(self) -> None: - first_object = PurePythonEnumerable( - first := Point(0, 2), - Point(9, 7), - second := Point(3, 1), - Point(4, -4), - ) - second_object = PurePythonEnumerable( - Point(5, 4), Point(8, 9), Point(-1, 3), 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)