Skip to content

Commit

Permalink
move internal method
Browse files Browse the repository at this point in the history
Signed-off-by: Wolfgang Hoschek <wolfgang.hoschek@mac.com>
  • Loading branch information
whoschek committed Feb 12, 2025
1 parent 4d360f0 commit a1656d0
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 49 deletions.
44 changes: 44 additions & 0 deletions bzfs/bzfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4690,6 +4690,50 @@ def getenv_bool(key: str, default: bool = False) -> bool:
return getenv_any(key, str(default).lower()).strip().lower() == "true"


P = TypeVar("P")


def find_match(
seq: Sequence[P],
predicate: Callable[[P], bool],
start: Optional[int] = None,
end: Optional[int] = None,
reverse: bool = False,
raises: Union[bool, str, Callable[[], str]] = False, # raises: bool | str | Callable = False, # python >= 3.10
) -> int:
"""Returns the integer index within seq of the first item (or last item if reverse==True) that matches the given
predicate condition. If no matching item is found returns -1 or ValueError, depending on the raises parameter,
which is a bool indicating whether to raise an error, or a string containing the error message, but can also be a
Callable/lambda in order to support efficient deferred generation of error messages.
Analog to str.find(), including slicing semantics with parameters start and end.
For example, seq can be a list, tuple or str.
Example usage:
lst = ["a", "b", "-c", "d"]
i = find_match(lst, lambda arg: arg.startswith("-"), start=1, end=3, reverse=True)
if i >= 0:
...
i = find_match(lst, lambda arg: arg.startswith("-"), raises=f"Tag {tag} not found in {file}")
i = find_match(lst, lambda arg: arg.startswith("-"), raises=lambda: f"Tag {tag} not found in {file}")
"""
offset = 0 if start is None else start if start >= 0 else len(seq) + start
if start is not None or end is not None:
seq = seq[start:end]
for i, item in enumerate(reversed(seq) if reverse else seq):
if predicate(item):
if reverse:
return len(seq) - i - 1 + offset
else:
return i + offset
if raises is False or raises is None:
return -1
if raises is True:
raise ValueError("No matching item found in sequence")
if callable(raises):
raises = raises()
raise ValueError(raises)


def xappend(lst, *items) -> List[str]:
"""Appends each of the items to the given list if the item is "truthy", e.g. not None and not an empty string.
If an item is an iterable does so recursively, flattening the output."""
Expand Down
2 changes: 1 addition & 1 deletion bzfs_docs/update_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import subprocess

from bzfs import bzfs
from bzfs_tests.test_units import find_match
from bzfs.bzfs import find_match


def main():
Expand Down
4 changes: 2 additions & 2 deletions bzfs_tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
from unittest.mock import patch

from bzfs import bzfs
from bzfs.bzfs import die_status, getenv_any, getenv_bool
from bzfs_tests.test_units import TestIncrementalSendSteps, find_match, stop_on_failure_subtest
from bzfs.bzfs import die_status, find_match, getenv_any, getenv_bool
from bzfs_tests.test_units import TestIncrementalSendSteps, stop_on_failure_subtest
from bzfs_tests.zfs_util import (
bookmark_name,
bookmarks,
Expand Down
47 changes: 1 addition & 46 deletions bzfs_tests/test_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from unittest.mock import patch, mock_open

from bzfs import bzfs
from bzfs.bzfs import getenv_any, Remote
from bzfs.bzfs import find_match, getenv_any, Remote
from bzfs_tests.zfs_util import is_solaris_zfs

test_mode = getenv_any("test_mode", "") # Consider toggling this when testing isolated code changes
Expand Down Expand Up @@ -2718,51 +2718,6 @@ def test_close_fds(self):
print(f"close_fds={close_fds}: Took {secs:.1f} seconds, iters/sec: {iters/secs:.1f}")


#############################################################################
T = TypeVar("T")


def find_match(
seq: Sequence[T],
predicate: Callable[[T], bool],
start: Optional[int] = None,
end: Optional[int] = None,
reverse: bool = False,
raises: Union[bool, str, Callable[[], str]] = False, # raises: bool | str | Callable = False, # python >= 3.10
) -> int:
"""Returns the integer index within seq of the first item (or last item if reverse==True) that matches the given
predicate condition. If no matching item is found returns -1 or ValueError, depending on the raises parameter,
which is a bool indicating whether to raise an error, or a string containing the error message, but can also be a
Callable/lambda in order to support efficient deferred generation of error messages.
Analog to str.find(), including slicing semantics with parameters start and end.
For example, seq can be a list, tuple or str.
Example usage:
lst = ["a", "b", "-c", "d"]
i = find_match(lst, lambda arg: arg.startswith("-"), start=1, end=3, reverse=True)
if i >= 0:
...
i = find_match(lst, lambda arg: arg.startswith("-"), raises=f"Tag {tag} not found in {file}")
i = find_match(lst, lambda arg: arg.startswith("-"), raises=lambda: f"Tag {tag} not found in {file}")
"""
offset = 0 if start is None else start if start >= 0 else len(seq) + start
if start is not None or end is not None:
seq = seq[start:end]
for i, item in enumerate(reversed(seq) if reverse else seq):
if predicate(item):
if reverse:
return len(seq) - i - 1 + offset
else:
return i + offset
if raises is False or raises is None:
return -1
if raises is True:
raise ValueError("No matching item found in sequence")
if callable(raises):
raises = raises()
raise ValueError(raises)


@contextmanager
def stop_on_failure_subtest(**params):
"""Context manager to mimic UnitTest.subTest() but stop on first failure"""
Expand Down

0 comments on commit a1656d0

Please sign in to comment.