diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc81f24..66983ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: reorder-python-imports args: [--py37-plus, --add-import, "from __future__ import annotations"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.6.3 hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -29,7 +29,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: diff --git a/pyproject.toml b/pyproject.toml index 5b35d3b..a8ce34c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,7 @@ ignore = [ "D203", # one-blank-line-before-class "D212", # multi-line-summary-first-line "D401", # non-imperative-mood + "D417", ] [tool.ruff.format] @@ -159,7 +160,8 @@ docstring-code-line-length = 100 # https://github.com/astral-sh/ruff/issues/4368 [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = [ - "S101", # assert - "E501", # line-too-long - "INP001", # implicit-namespace-package + "S101", # assert + "E501", # line-too-long + "INP001", # implicit-namespace-package + "PLR2004", ] diff --git a/src/dev_toolbox/cli/version_bump.py b/src/dev_toolbox/cli/version_bump.py index 29c5a85..bce548f 100644 --- a/src/dev_toolbox/cli/version_bump.py +++ b/src/dev_toolbox/cli/version_bump.py @@ -97,7 +97,7 @@ def main(argv: Sequence[str] | None = None) -> int: print(f"Old version: {latest_tag}") print(f"New version: {_new_version_tag}") - if input("Should we tag and push the new version?").lower() not in {"yes", "y"}: + if input("Should we tag and push the new version? ").lower() not in {"yes", "y"}: return 0 quick_run(("git", "tag", _new_version_tag)) diff --git a/src/dev_toolbox/great_value/functional.py b/src/dev_toolbox/great_value/functional.py index 5b98469..ca02a9b 100644 --- a/src/dev_toolbox/great_value/functional.py +++ b/src/dev_toolbox/great_value/functional.py @@ -47,6 +47,18 @@ def __init__(self, items: Iterable[_T_co]) -> None: self.__items = items def map(self, func: Callable[[_T_co], _R]) -> Stream[_R]: + """ + Apply a function to each item in the stream and return a new stream. + + Args: + ---- + func (Callable): The function to apply to each item in the stream. + + Returns: + ------- + Stream: A new stream containing the results of applying the function to each item. + + """ # Lazy return Stream(func(x) for x in self.__items) @@ -59,10 +71,36 @@ def filter(self: Stream[_U], predicate: Callable[[_U], TypeIs[_V]]) -> Stream[_V @overload def filter(self: Stream[_U], predicate: Callable[[_U], Any]) -> Stream[_U]: ... def filter(self, predicate=None): # type: ignore[no-untyped-def] + """ + Filters the items in the stream based on the given predicate. + + Args: + ---- + predicate (callable): A function that takes an item from the stream as input and returns + a boolean value indicating whether the item should be included in the filtered stream. + If no predicate is provided, all items will be included. + + Returns: + ------- + Stream: A new stream containing only the items that satisfy the predicate. + + """ # Lazy return Stream(filter(predicate, self.__items)) def type_is(self: Stream[_U], cls: type[_V]) -> Stream[_V]: + """ + Filters the stream to include only items that are instances of the specified class. + + Parameters + ---------- + cls (type[_V]): The class to filter by. + + Returns + ------- + Stream[_V]: A new stream containing only items that are instances of the specified class. + + """ # noqa: E501 # Lazy return Stream(x for x in self.__items if isinstance(x, cls)) @@ -82,14 +120,54 @@ def unique(self: Stream[_HashableT], key: None = ...) -> Stream[_HashableT]: ... @overload def unique(self: Stream[_U], key: Callable[[_U], _HashableT]) -> Stream[_U]: ... def unique(self, key=None): # type: ignore[no-untyped-def] + """ + Returns a new Stream object containing only the unique elements from the original Stream. + + Parameters + ---------- + key (function, optional): A function that takes an element from the Stream and returns + a value to compare for uniqueness. If not provided, the elements themselves will be + compared. + + Returns + ------- + Stream: A new Stream object containing only the unique elements. + + """ # Lazy return Stream(self.__unique_helper(items=self.__items, key=(key or (lambda x: x)))) def enumerate(self, start: int = 0) -> Stream[tuple[int, _T_co]]: + """ + Lazily enumerates the items in the stream. + + Args: + ---- + start (int, optional): The starting index for enumeration. Defaults to 0. + + Returns: + ------- + Stream[tuple[int, _T_co]]: A stream of tuples containing the index and item. + + """ # Lazy return Stream(enumerate(self.__items, start=start)) def peek(self: Stream[_U], func: Callable[[_U], Any]) -> Stream[_U]: + """ + Apply a function to each item in the stream, while keeping the original item unchanged. + + Args: + ---- + self (Stream[_U]): The stream object. + func (Callable[[_U], Any]): The function to apply to each item. + + Returns: + ------- + Stream[_U]: A new stream object with the same items as the original stream. + + """ + # Lazy def func_and_return(item: _U) -> _U: func(item) @@ -98,10 +176,33 @@ def func_and_return(item: _U) -> _U: return Stream(func_and_return(x) for x in self.__items) def flatten(self: Stream[Iterable[_U]]) -> Stream[_U]: + """ + Flattens a stream of iterables into a single stream. + + Returns + ------- + Stream[_U]: A new stream containing all the elements from the original stream's + iterables. + + """ # Lazy return Stream(item for sub in self.__items for item in sub) def flat_map(self, func: Callable[[_T_co], Iterable[_U]]) -> Stream[_U]: + """ + Applies the given function to each element in the stream and flattens the result. + + Args: + ---- + func (Callable[[_T_co], Iterable[_U]]): A function that takes an element of type _T_co + and returns an iterable of type _U. + + Returns: + ------- + Stream[_U]: A new stream containing the flattened result of applying the function to + each element. + + """ # Lazy return Stream(item for x in self.__items for item in func(x)) @@ -116,10 +217,31 @@ def islice( self, start: int | None, stop: int | None, step: int | None = ..., / ) -> Stream[_T_co]: ... def islice(self, *args) -> Stream[_T_co]: # type: ignore[no-untyped-def] + """ + Return an iterator whose next() method returns selected values from an iterable. + + If start is specified, will skip all preceding elements; + otherwise, start defaults to zero. Step defaults to one. If + specified as another value, step determines how many values are + skipped between successive calls. Works like a slice() on a list + but returns an iterator. + """ # Lazy return Stream(islice(self.__items, *args)) def batched(self, size: int) -> Stream[tuple[_T_co, ...]]: + """ + Returns a stream of batches, where each batch contains 'size' number of items from the original stream. + + Parameters + ---------- + size (int): The number of items in each batch. + + Returns + ------- + Stream[tuple[_T_co, ...]]: A stream of batches, where each batch is a tuple of 'size' number of items. + + """ # noqa: E501 # Lazy items = iter(self.__items) return Stream(iter(lambda: tuple(islice(items, size)), ())) @@ -148,6 +270,19 @@ def min( self: Stream[_U], /, *, key: Callable[[_U], SupportsRichComparison], default: _V ) -> _U | _V: ... def min(self, /, *, key=None, **kwargs): # type: ignore[no-untyped-def] + """ + Returns the minimum value from the items in the object. + + Args: + ---- + key: A function that serves as a key for the comparison. Defaults to None. + **kwargs: Additional keyword arguments to be passed to the `min` function. + + Returns: + ------- + The minimum value from the items in the object. + + """ # Eager return min(self.__items, key=key, **kwargs) @@ -166,6 +301,23 @@ def max( self: Stream[_U], /, *, key: Callable[[_U], SupportsRichComparison], default: _V ) -> _U | _V: ... def max(self, /, *, key=None, **kwargs): # type: ignore[no-untyped-def] + """ + Return the maximum value in the collection. + + Args: + ---- + key (callable, optional): A function to serve as the key for comparison. Defaults to None. + **kwargs: Additional keyword arguments to be passed to the max function. + + Returns: + ------- + The maximum value in the collection. + + Raises: + ------ + TypeError: If the collection is empty and no default value is provided. + + """ # noqa: E501 # Eager return max(self.__items, key=key, **kwargs) @@ -182,6 +334,19 @@ def sorted( reverse: bool = False, ) -> Stream[_T_co]: ... def sorted(self, /, *, key=None, reverse=False): # type: ignore[no-untyped-def] + """ + Returns a new Stream containing the items of the current Stream, sorted in ascending order. + + Parameters + ---------- + - key: A function that will be used to extract a comparison key from each item. Defaults to None. + - reverse: A boolean value indicating whether the items should be sorted in descending order. Defaults to False. + + Returns + ------- + - A new Stream containing the sorted items. + + """ # noqa: E501 # Eager return Stream(sorted(self.__items, key=key, reverse=reverse)) @@ -194,6 +359,22 @@ def first(self, /) -> _T_co: ... @overload def first(self: Stream[_U], default: _V, /) -> _U | _V: ... def first(self, *args): # type: ignore[no-untyped-def] + """ + Returns the first item in the collection. + + Parameters + ---------- + *args: Optional arguments to be passed to the `next` function. + + Returns + ------- + The first item in the collection. + + Raises + ------ + StopIteration: If the collection is empty and no default value is provided. + + """ # Eager return next(iter(self.__items), *args) @@ -201,12 +382,46 @@ def first(self, *args): # type: ignore[no-untyped-def] # region: Custom methods def find(self, func: Callable[[_T_co], object]) -> _T_co | type[NoItem]: + """ + Find the first item in the collection that satisfies the given function. + + Parameters + ---------- + func (Callable): A function that takes an item from the collection as input and returns a boolean value. + + Returns + ------- + The first item in the collection that satisfies the given function, or `NoItem` if no item is found. + + """ # noqa: E501 # Eager return next((item for item in self.__items if func(item)), NoItem) def group_by( self, key: Callable[[_T_co], _HashableT] ) -> Stream[tuple[_HashableT, list[_T_co]]]: + """ + Groups the items in the stream by the given key function. + + Args: + ---- + key: A callable that takes an item from the stream and returns a hashable key. + + Returns: + ------- + A Stream of tuples, where each tuple contains a key and a list of items that have that key. + + Example: + ------- + stream = Stream([1, 2, 3, 4, 5]) + result = stream.group_by(lambda x: x % 2 == 0) + for key, items in result: + print(key, items) + Output: + True [2, 4] + False [1, 3, 5]. + + """ # noqa: E501 # Eager dct: dict[_HashableT, list[_T_co]] = defaultdict(list) for item in self.__items: @@ -219,6 +434,14 @@ def for_each(self, func: Callable[[_T_co], Any]) -> None: func(item) def cache(self) -> Stream[_T_co]: + """ + Returns a cached version of the stream. + + Returns + ------- + Stream: A new Stream object containing the items from the original stream. + + """ # Eager return Stream(tuple(self.__items)) @@ -226,31 +449,77 @@ def cache(self) -> Stream[_T_co]: # region: Collectors def to_list(self) -> list[_T_co]: + """ + Convert the items in the object to a list. + + Returns + ------- + list[_T_co]: A list containing the items in the object. + + """ # Eager return list(self.__items) def to_tuple(self) -> tuple[_T_co, ...]: + """ + Converts the items in the object to a tuple. + + Returns + ------- + tuple: A tuple containing the items in the object. + + """ # Eager return tuple(self.__items) def to_set(self) -> set[_T_co]: + """ + Convert the items in the object to a set. + + Returns + ------- + set[_T_co]: A set containing the items in the object. + + """ # Eager return set(self.__items) def to_dict(self: Stream[tuple[_HashableT, _V]]) -> dict[_HashableT, _V]: + """ + Converts the stream of tuples into a dictionary. + + Returns + ------- + dict[_HashableT, _V]: The resulting dictionary. + + """ # Eager return dict(self.__items) # endregion: Collectors def len(self) -> int: + """ + Returns the length of the object. + + :return: The length of the object. + :rtype: int + """ # Eager return len(self) def __len__(self) -> int: - # Eager - if not hasattr(self.__items, "__len__"): - self.__items = tuple(self.__items) + """ + Returns the length of the object. + + Note: This method may fail if `self.__items` is a generator. In that case, you should call + the `cache` method first and then call `len` on the stream. + + Returns + ------- + int: The length of the object. + + """ return len(self.__items) # type: ignore[arg-type] ############################################################################ @@ -258,10 +527,24 @@ def __len__(self) -> int: ############################################################################ def __iter__(self) -> Iterator[_T_co]: + """ + Returns an iterator over the items in the object. + + :return: An iterator over the items. + :rtype: Iterator[_T_co] + """ # Lazy return iter(self.__items) def __repr__(self) -> str: + """ + Return a string representation of the object. + + Returns + ------- + str: The string representation of the object. + + """ # Lazy return f"{self.__class__.__name__}({self.__items})" @@ -296,22 +579,88 @@ def __sections_helper( yield tuple(buffer) def sections(self, predicate: Callable[[_T_co], object]) -> Stream[tuple[_T_co, ...]]: + """ + Returns a Stream of tuples, where each tuple represents a section of consecutive elements from the original Stream + that satisfy the given predicate. + + Parameters + ---------- + predicate (Callable[[_T_co], object]): A function that takes an element from the Stream and returns a boolean + value indicating whether the element satisfies the condition. + + Returns + ------- + Stream[tuple[_T_co, ...]]: A Stream of tuples, where each tuple represents a section of consecutive elements + from the original Stream that satisfy the given predicate. + + """ # noqa: D205, E501 # Lazy return Stream(self.__sections_helper(self.__items, predicate)) def take(self, n: int) -> Stream[_T_co]: + """ + Returns a new Stream containing the first `n` elements of the current Stream. + + Parameters + ---------- + n (int): The number of elements to take from the Stream. + + Returns + ------- + Stream[_T_co]: A new Stream containing the first `n` elements. + + """ # Lazy return Stream(itertools.islice(self.__items, n)) def drop(self, n: int) -> Stream[_T_co]: + """ + Drops the first `n` elements from the stream and returns a new stream. + + Parameters + ---------- + - n: An integer representing the number of elements to drop from the stream. + + Returns + ------- + - Stream[_T_co]: A new stream containing the remaining elements after dropping `n` elements. + + """ # Lazy return Stream(itertools.islice(self.__items, n, None)) def dropwhile(self, predicate: Callable[[_T_co], object]) -> Stream[_T_co]: + """ + Lazily drops elements from the stream while the predicate is true. + + Args: + ---- + predicate: A callable that takes an element from the stream as input and returns a boolean value. + + Returns: + ------- + A new Stream object containing the remaining elements after the predicate becomes false. + + """ # noqa: E501 # Lazy return Stream(itertools.dropwhile(predicate, self.__items)) def takewhile(self, predicate: Callable[[_T_co], object]) -> Stream[_T_co]: + """ + Returns a new Stream containing elements from the original Stream that satisfy the given predicate function. + + Parameters + ---------- + predicate (Callable[[_T_co], object]): A function that takes an element of the Stream + as input and returns a boolean value indicating whether the element should be included + in the new Stream. + + Returns + ------- + Stream[_T_co]: A new Stream containing elements from the original Stream that satisfy + the given predicate function. + + """ # noqa: E501 # Lazy return Stream(itertools.takewhile(predicate, self.__items)) @@ -320,6 +669,19 @@ def sum(self: Stream[int], start: int = 0) -> int: ... @overload def sum(self: Stream[float], start: int = 0) -> float: ... def sum(self: Stream[int | float], start: int = 0) -> int | float: + """ + Calculate the sum of the elements in the stream. + + Args: + ---- + self (Stream[int | float]): The stream of elements. + start (int, optional): The starting value for the sum. Defaults to 0. + + Returns: + ------- + int | float: The sum of the elements in the stream. + + """ # Eager return sum(self.__items, start=start) @@ -332,10 +694,36 @@ def zip( self, iter1: Iterable[_U], iter2: Iterable[_V], iter3: Iterable[_W], / ) -> Stream[tuple[_T_co, _U, _V, _W]]: ... def zip(self: Stream[_U], *iterables: Iterable[_U]) -> Stream[tuple[_U, ...]]: + """ + Lazily zips the elements of the stream with the corresponding elements from the given iterables. + + Args: + ---- + self (Stream[_U]): The stream object. + *iterables (Iterable[_U]): The iterables to zip with the stream. + + Returns: + ------- + Stream[tuple[_U, ...]]: A new stream containing tuples of the zipped elements. + + """ # noqa: E501 # Lazy return Stream(zip(self.__items, *iterables)) def chain(self: Stream[_U], iterable: Iterable[_U], *iterables: Iterable[_U]) -> Stream[_U]: + """ + Chains the items of the current stream with the items from the given iterable(s). + + Args: + ---- + iterable (Iterable[_U]): The iterable to chain with the current stream. + *iterables (Iterable[_U]): Additional iterables to chain with the current stream. + + Returns: + ------- + Stream[_U]: A new stream containing the chained items. + + """ # Lazy return Stream(itertools.chain(self.__items, iterable, *iterables)) @@ -360,6 +748,23 @@ def filterfalse(self: Stream[_U], predicate: None = ...) -> Stream[_U]: ... @overload def filterfalse(self: Stream[_U], predicate: Callable[[_U], Any]) -> Stream[_U]: ... def filterfalse(self, predicate=None): # type: ignore[no-untyped-def] + """ + Return a new Stream object containing the elements from the original stream + for which the predicate function returns False. + + Args: + ---- + predicate (Callable): A function that takes an item from the stream as + input and returns a boolean value indicating whether the item should + be included in the new stream. If no predicate is provided, all items + will be included in the new stream. + + Returns: + ------- + Stream: A new Stream object containing the elements from the original + stream for which the predicate function returns False. + + """ # noqa: D205 return Stream(itertools.filterfalse(predicate, self.__items)) @overload @@ -373,6 +778,20 @@ def zip_longest( self, iter1: Iterable[_U], iter2: Iterable[_V], iter3: Iterable[_W], / ) -> Stream[tuple[_T_co, _U, _V, _W]]: ... def zip_longest(self: Stream[_U], *iterables: Iterable[_U]) -> Stream[tuple[_U, ...]]: + """ + Returns a new Stream object that iterates over tuples containing elements from the input iterables. + The iteration stops when the longest input iterable is exhausted. + + Parameters + ---------- + self (Stream[_U]): The Stream object. + *iterables (Iterable[_U]): The input iterables to be zipped. + + Returns + ------- + Stream[tuple[_U, ...]]: A new Stream object that iterates over tuples containing elements from the input iterables. + + """ # noqa: D205, E501 # Lazy return Stream(itertools.zip_longest(self.__items, *iterables)) @@ -389,43 +808,173 @@ def accumulate(self, func=None, initial=None): # type: ignore[no-untyped-def] # Lazy return Stream(itertools.accumulate(self.__items, func, initial=initial)) + def typing_cast(self, _: type[_U], /) -> Stream[_U]: + """ + Casts the items in the stream to the specified type. + + Parameters + ---------- + _: The type to cast the items to. + + Returns + ------- + Stream[_U]: A new stream with the items casted to the specified type. + + """ + return Stream(self.__items) # type: ignore[arg-type] + + def collect(self, func: Callable[[Iterable[_T_co]], _R]) -> _R: + """ + Collects the items in the collection and applies the given function to them. + + Args: + ---- + func (Callable[[Iterable[_T_co]], _R]): The function to apply to the items. + + Returns: + ------- + _R: The result of applying the function to the items. + + """ + return func(self.__items) + + @staticmethod + def range(start: int, stop: int, step: int = 1) -> Stream[int]: + """ + Generate a stream of integers within a specified range. + + Args: + ---- + start (int): The starting value of the range. + stop (int): The ending value of the range (exclusive). + step (int, optional): The step size between values (default is 1). + + Returns: + ------- + Stream[int]: A stream of integers within the specified range. + + """ + return Stream(range(start, stop, step)) + + def reverse(self) -> Stream[_T_co]: + """ + Reverses the order of the items in the stream. + + Returns + ------- + Stream: A new stream with the items in reverse order. + + """ + if hasattr(self.__items, "__reversed__") or ( + hasattr(self.__items, "__len__") and hasattr(self.__items, "__getitem__") + ): + return Stream(reversed(self.__items)) # type: ignore[call-overload] + return Stream(reversed(tuple(self.__items))) + + @staticmethod + def __subprocess_run( + cmd: tuple[str, ...], pipe_in: Iterable[str] | None = None + ) -> Generator[str, None, None]: + import subprocess + + process = subprocess.Popen( + args=cmd, + stdin=subprocess.PIPE if pipe_in else None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + errors="ignore", + universal_newlines=True, + ) + with process: + if pipe_in and process.stdin: + with process.stdin as stdin: + for line in pipe_in: + stdin.write(line) + if process.stdout is None: + return + with process.stdout as stdout: + yield from iter(stdout.readline, "") + + @staticmethod + def subprocess_run(command: tuple[str, ...]) -> Stream[str]: + """ + Executes the given command using the `subprocess.run` function and returns a `Stream` object. + + Args: + ---- + command (tuple[str, ...]): The command to be executed as a tuple of strings. + + Returns: + ------- + Stream[str]: A `Stream` object that represents the output of the command. + + """ # noqa: E501 + return Stream(Stream.__subprocess_run(command)) + + def pipe(self: Stream[str], command: tuple[str, ...]) -> Stream[str]: + """ + Executes a command using subprocess and pipes the input from the current stream. + + Args: + ---- + command (tuple[str, ...]): The command to be executed. + + Returns: + ------- + Stream[str]: A new stream containing the output of the command. + + """ + return Stream(self.__subprocess_run(command, pipe_in=self.__items)) + if __name__ == "__main__": - s = Stream([1, 2, 3]) - _1 = s.map(lambda x: x + 1) - _2 = s.filter() - _3 = s.type_is(int) - _4 = s.unique() - _5 = s.enumerate() - _6 = s.peek(print) - _7 = s.map(lambda x: (x, x)).flatten() - _8 = s.flat_map(lambda x: (x, x)) - _9 = s.islice(2) - _10 = s.batched(2) - _11 = s.min() - _12 = s.max() - _13 = s.sorted() - _14 = s.map(str).join(",") - _15 = s.first() - _16 = s.find(lambda x: x == 2) # noqa: PLR2004 - _17 = s.group_by(lambda x: x % 2 == 0) - _18 = s.for_each(print) # type: ignore[func-returns-value] - _19 = s.cache() - _20 = s.to_list() - _21 = s.to_tuple() - _22 = s.to_set() - _23 = s.map(lambda x: (x, x)).to_dict() - _24 = s.len() - _25 = s.from_io(open("file.txt")) # noqa: SIM115 - _26 = s.sections(lambda x: x == 2) # noqa: PLR2004 - _27 = s.take(2) - _28 = s.drop(2) - _29 = s.dropwhile(lambda x: x == 1) - _30 = s.takewhile(lambda x: x == 1) - _31 = s.sum() - _32 = s.zip("range(10)") - _33 = s.chain(range(10)) # Fix this - _34 = s.reduce(lambda x, y: x + y, 0.1) - _35 = s.filterfalse() - _36 = s.zip_longest("") - _37 = s.accumulate() + + def test() -> None: + s = Stream([1, 2, 3]) + _1 = s.map(lambda x: x + 1) + _2 = s.filter() + _3 = s.type_is(int) + _4 = s.unique() + _5 = s.enumerate() + _6 = s.peek(print) + _7 = s.map(lambda x: (x, x)).flatten() + _8 = s.flat_map(lambda x: (x, x)) + _9 = s.islice(2) + _10 = s.batched(2) + _11 = s.min() + _12 = s.max() + _13 = s.sorted() + _14 = s.map(str).join(",") + _15 = s.first() + _16 = s.find(lambda x: x == 2) # noqa: PLR2004 + _17 = s.group_by(lambda x: x % 2 == 0) + _18 = s.for_each(print) # type: ignore[func-returns-value] + _19 = s.cache() + _20 = s.to_list() + _21 = s.to_tuple() + _22 = s.to_set() + _23 = s.map(lambda x: (x, x)).to_dict() + _24 = s.len() + _25 = s.from_io(open("file.txt")) # noqa: SIM115 + _26 = s.sections(lambda x: x == 2) # noqa: PLR2004 + _27 = s.take(2) + _28 = s.drop(2) + _29 = s.dropwhile(lambda x: x == 1) + _30 = s.takewhile(lambda x: x == 1) + _31 = s.sum() + _32 = s.zip("range(10)") + _33 = s.chain(range(10)) # Fix this + _34 = s.reduce(lambda x, y: x + y, 0.1) + _35 = s.filterfalse() + _36 = s.zip_longest("") + _37 = s.accumulate() + + def main() -> None: + Stream.subprocess_run(("seq", "10000")).pipe(("grep", "--color=always", "10")).for_each( + lambda x: print(x, end="") + ) + # for i in Stream.range(0, 10).map(lambda x: x).reverse(): + # print(i) + + # main() diff --git a/tests/functional_test.py b/tests/functional_test.py new file mode 100644 index 0000000..7e9c3b6 --- /dev/null +++ b/tests/functional_test.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dev_toolbox.great_value.functional import NoItem +from dev_toolbox.great_value.functional import Stream + +if TYPE_CHECKING: + from pathlib import Path + + +def test_initialization() -> None: + s = Stream([1, 2, 3]) + assert list(s) == [1, 2, 3] + + +def test_map() -> None: + s = Stream([1, 2, 3]).map(lambda x: x + 1) + assert list(s) == [2, 3, 4] + + +def test_filter() -> None: + s = Stream([1, 2, 3, 4]).filter(lambda x: x % 2 == 0) + assert list(s) == [2, 4] + + +def test_type_is() -> None: + s = Stream([1, "a", 2, "b"]).type_is(int) + assert list(s) == [1, 2] + + +def test_unique() -> None: + s = Stream([1, 2, 2, 3, 1]).unique() + assert list(s) == [1, 2, 3] + + +def test_enumerate() -> None: + s = Stream(["a", "b", "c"]).enumerate() + assert list(s) == [(0, "a"), (1, "b"), (2, "c")] + + +def test_peek() -> None: + result: list[int] = [] + s = Stream([1, 2, 3]).peek(result.append) + assert list(s) == [1, 2, 3] + assert result == [1, 2, 3] + + +def test_flatten() -> None: + s = Stream([[1, 2], [3, 4]]).flatten() + assert list(s) == [1, 2, 3, 4] + + +def test_flat_map() -> None: + s = Stream([1, 2, 3]).flat_map(lambda x: [x, x]) + assert list(s) == [1, 1, 2, 2, 3, 3] + + +def test_islice() -> None: + s = Stream([1, 2, 3, 4]).islice(2) + assert list(s) == [1, 2] + + +def test_batched() -> None: + s = Stream([1, 2, 3, 4]).batched(2) + assert list(s) == [(1, 2), (3, 4)] + + +def test_min() -> None: + s = Stream([3, 1, 2]) + assert s.min() == 1 + + +def test_max() -> None: + s = Stream([3, 1, 2]) + assert s.max() == 3 + + +def test_sorted() -> None: + s = Stream([3, 1, 2]).sorted() + assert list(s) == [1, 2, 3] + + +def test_join() -> None: + s = Stream(["a", "b", "c"]) + assert s.join(",") == "a,b,c" + + +def test_first() -> None: + s = Stream([1, 2, 3]) + assert s.first() == 1 + + +def test_find() -> None: + s = Stream([1, 2, 3]) + assert s.find(lambda x: x == 2) == 2 + assert s.find(lambda x: x == 4) == NoItem + + +def test_group_by() -> None: + s = Stream([1, 2, 3, 4, 5]).group_by(lambda x: x % 2 == 0) + assert list(s) == [(False, [1, 3, 5]), (True, [2, 4])] + + +def test_for_each() -> None: + result: list[int] = [] + s = Stream([1, 2, 3]) + s.for_each(result.append) + assert result == [1, 2, 3] + + +def test_cache() -> None: + s = Stream([1, 2, 3]).cache() + assert list(s) == [1, 2, 3] + + +def test_to_list() -> None: + s = Stream([1, 2, 3]) + assert s.to_list() == [1, 2, 3] + + +def test_to_tuple() -> None: + s = Stream([1, 2, 3]) + assert s.to_tuple() == (1, 2, 3) + + +def test_to_set() -> None: + s = Stream([1, 2, 2, 3]) + assert s.to_set() == {1, 2, 3} + + +def test_to_dict() -> None: + s = Stream([("a", 1), ("b", 2)]) + assert s.to_dict() == {"a": 1, "b": 2} + + +def test_len() -> None: + s = Stream([1, 2, 3]) + assert s.len() == 3 + + +def test_sum() -> None: + s = Stream([1, 2, 3]) + assert s.sum() == 6 + + +def test_reduce() -> None: + s = Stream([1, 2, 3]) + assert s.reduce(lambda x, y: x + y) == 6 + + +def test_reverse() -> None: + s = Stream([1, 2, 3]).reverse() + assert list(s) == [3, 2, 1] + + +def test_from_io(tmp_path: Path) -> None: + p = tmp_path / "test.txt" + p.write_text("line1\nline2\nline3") + s = Stream.from_io(open(p)) # noqa: SIM115 + assert list(s) == ["line1\n", "line2\n", "line3"] + + +def test_range() -> None: + s = Stream.range(0, 3) + assert list(s) == [0, 1, 2] + + +def test_sections() -> None: + s = Stream([1, 2, 3, 4, 1, 2, 3, 4]).sections(lambda x: x == 1) + assert list(s) == [(1, 2, 3, 4), (1, 2, 3, 4)] + + +def test_take() -> None: + s = Stream([1, 2, 3, 4]).take(2) + assert list(s) == [1, 2] + + +def test_drop() -> None: + s = Stream([1, 2, 3, 4]).drop(2) + assert list(s) == [3, 4] + + +def test_dropwhile() -> None: + s = Stream([1, 2, 3, 4]).dropwhile(lambda x: x < 3) + assert list(s) == [3, 4] + + +def test_takewhile() -> None: + s = Stream([1, 2, 3, 4]).takewhile(lambda x: x < 3) + assert list(s) == [1, 2] + + +def test_zip() -> None: + s = Stream([1, 2, 3]).zip([4, 5, 6]) + assert list(s) == [(1, 4), (2, 5), (3, 6)] + + +def test_chain() -> None: + s = Stream([1, 2, 3]).chain([4, 5, 6]) + assert list(s) == [1, 2, 3, 4, 5, 6] + + +def test_filterfalse() -> None: + s = Stream([1, 2, 3, 4]).filterfalse(lambda x: x % 2 == 0) + assert list(s) == [1, 3] + + +def test_zip_longest() -> None: + s = Stream([1, 2, 3]).zip_longest([4, 5]) + assert list(s) == [(1, 4), (2, 5), (3, None)] + + +def test_accumulate() -> None: + s = Stream([1, 2, 3]).accumulate() + assert list(s) == [1, 3, 6] + + +def test_typing_cast() -> None: + s = Stream([1, 2, 3]).typing_cast(str) + assert list(s) == [1, 2, 3] + + +def test_collect() -> None: + s = Stream([1, 2, 3]) + assert s.collect(sum) == 6