From 5c7a68d43ee71f3a6b73c9452a20eb0296917e0d Mon Sep 17 00:00:00 2001 From: Bhargav Andhe Date: Tue, 18 Aug 2026 22:23:00 +0530 Subject: [PATCH] feat: add Seq.try_find --- expression/collections/seq.py | 30 ++++++++++++++++++++ tests/test_seq.py | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/expression/collections/seq.py b/expression/collections/seq.py index f5c1c8a..8b428d9 100644 --- a/expression/collections/seq.py +++ b/expression/collections/seq.py @@ -328,6 +328,10 @@ def try_find_index(self, predicate: Callable[[_TSource], bool]) -> Option[int]: """Return the index of the first element matching the predicate, if any.""" return pipe(self, try_find_index(predicate)) + def try_find(self, predicate: Callable[[_TSource], bool]) -> Option[_TSource]: + """Return the first element matching the predicate, if any.""" + return pipe(self, try_find(predicate)) + def dict(self) -> Iterable[_TSource]: """Returns a json serializable representation of the list.""" @@ -974,6 +978,31 @@ def try_find_index(source: Iterable[_TSource], predicate: Callable[[_TSource], b return Nothing +@curry_flip(1) +def try_find(source: Iterable[_TSource], predicate: Callable[[_TSource], bool]) -> Option[_TSource]: + """Return the first element matching the predicate, if any. + + Evaluation stops as soon as the predicate returns `True`. + + Args: + source: The input sequence. + predicate: A function to test each element. + + Returns: + The first matching element wrapped in `Some`, or `Nothing` when no + element matches. + + Example: + >>> pipe([1, 2, 3], try_find(lambda value: value % 2 == 0)) + Some 2 + """ + for value in source: + if predicate(value): + return Some(value) + + return Nothing + + @curry_flip(1) def unfold(state: _TState, generator: Callable[[_TState], Option[tuple[_TSource, _TState]]]) -> Iterable[_TSource]: """Unfold sequence. @@ -1067,6 +1096,7 @@ def _zip(source2: Iterable[_TResult]) -> Iterable[tuple[_TSource, _TResult]]: "sum_by", "tail", "take", + "try_find", "try_find_index", "unfold", "zip", diff --git a/tests/test_seq.py b/tests/test_seq.py index a013e55..130bf5e 100644 --- a/tests/test_seq.py +++ b/tests/test_seq.py @@ -370,6 +370,13 @@ def test_seq_try_find_index_pipe_returns_first_matching_index(): assert result == Some(2) +def test_seq_try_find_pipe_returns_first_match(): + is_even: Callable[[int], bool] = lambda value: value % 2 == 0 + result = pipe([1, 2, 4], seq.try_find(is_even)) + + assert result == Some(2) + + def test_seq_try_find_index_fluent(): source = Seq[int].of_iterable([1, 3, 4, 6]) @@ -391,6 +398,21 @@ def test_seq_try_find_index_returns_nothing_without_match(): assert pipe([1, 3], seq.try_find_index(is_even)) is Nothing +def test_seq_try_find_fluent(): + source = Seq[int].of_iterable([1, 2, 4]) + + assert source.try_find(lambda value: value % 2 == 0) == Some(2) + + +def test_seq_try_find_returns_nothing_without_match(): + empty: list[int] = [] + always_true: Callable[[int], bool] = lambda _: True + is_even: Callable[[int], bool] = lambda value: value % 2 == 0 + + assert pipe(empty, seq.try_find(always_true)) is Nothing + assert pipe([1, 3], seq.try_find(is_even)) is Nothing + + def test_seq_try_find_index_stops_after_first_match(): consumed: list[int] = [] is_even: Callable[[int], bool] = lambda value: value % 2 == 0 @@ -414,6 +436,37 @@ def predicate(_: int) -> bool: pipe([1], seq.try_find_index(predicate)) +def test_seq_try_find_can_match_none(): + source: list[int | None] = [1, None, 2] + is_none: Callable[[int | None], bool] = lambda value: value is None + result = pipe(source, seq.try_find(is_none)) + + assert result == Some(None) + + +def test_seq_try_find_stops_after_first_match(): + consumed: list[int] = [] + is_two: Callable[[int], bool] = lambda value: value == 2 + + def source() -> Iterable[int]: + for value in [1, 2, 3]: + consumed.append(value) + yield value + + result = pipe(source(), seq.try_find(is_two)) + + assert result == Some(2) + assert consumed == [1, 2] + + +def test_seq_try_find_propagates_predicate_exceptions(): + def predicate(_: int) -> bool: + raise ValueError("predicate failed") + + with pytest.raises(ValueError, match="predicate failed"): + pipe([1], seq.try_find(predicate)) + + rtn: Callable[[int], Seq[int]] = seq.singleton empty: Seq[int] = seq.empty