Skip to content
This repository was archived by the owner on Mar 10, 2025. It is now read-only.
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ If `enumerator_without_func` is set, the decorator skips calling the decorated m

Relys on the enumerable's implementation of `__into__` and `__to_tuple__`.

#### [`any`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-any-3F)

Returns whether any element meets a given criterion.
With no arguments, the criterion is truthiness.
With a callable argument, returns whether it returns a truthy value for any element.
With a non-callable argument, treats it as a [pattern](#user-content-patterns).

#### [`all`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-all-3F)

Returns whether every element meets a given criterion.
With no arguments, the criterion is truthiness.
With a callable argument, returns whether it returns a truthy value for all elements.
With a non-callable argument, treats it as a [pattern](#user-content-patterns).

#### [`collect`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-collect), [`map`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-map)

Returns the result of mapping a function over the elements.
Expand Down Expand Up @@ -139,7 +153,19 @@ Performs a reduction operation much like `functools.reduce`.
If called with a single argument, treats it as the reduction function.
If called with two arguments, the first is treated as the initial value for the reduction and the second argument acts as the reduction function.

Also available as the alias `reduce`.
#### [`none`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-none-3F)

Returns whether no element meets a given criterion.
With no arguments, the criterion is truthiness.
With a callable argument, returns whether it returns a truthy value for no element.
With a non-callable argument, treats it as a [pattern](#user-content-patterns).

#### [`one`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-one-3F)

Returns whether exactly one element meets a given criterion.
With no arguments, the criterion is truthiness.
With a callable argument, returns whether it returns a truthy value for exactly one element.
With a non-callable argument, treats it as a [pattern](#user-content-patterns).

#### [`reject`](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-reject)

Expand Down Expand Up @@ -195,3 +221,13 @@ If going beyond the enumeration, `StopIteration` is raised.
Rewinds the enumeration sequence to the beginning.

_Note that this may not be possible to do for underlying iterables that can be exhausted._

---

### Patterns

Especially given Enumerable's [query methods](https://ruby-doc.org/core-3.1.2/Enumerable.html#module-Enumerable-label-Methods+for+Querying), the concept of patterns needs to be dealt with.

In Ruby, [`===`](https://ruby-doc.org/core-3.1.2/Object.html#method-i-3D-3D-3D) is used, but this is not easily ported to Python. For example, in the case of a callabel type like `bool` a decision must be made whether to check for type equality or call `bool` with the element to determine truthiness. Thus Pyby gives precedence to type checks.

In the case of regex patterns, they only attempt to match an element if that element is of a compatible type to the regex pattern, i.e. a string regex pattern is applied to string-like elements and a bytes regex pattern is applied to bytes-like elements.
70 changes: 69 additions & 1 deletion src/pyby/enumerable.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import functools
from importlib import import_module
from itertools import islice
from itertools import dropwhile, islice
from operator import truth
import re
from .object import RObject

EMPTY_REDUCE_ERRORS = [
"reduce() of empty iterable with no initial value",
"reduce() of empty sequence with no initial value",
]
NOT_FOUND = object()
NOT_USED = object()


Expand Down Expand Up @@ -50,6 +53,37 @@ def wrapper(self, *args, **kwargs):

return decorator

def any(self, compare_to=truth):
is_a = lambda item: isinstance(item, compare_to) # noqa
same = lambda item: item == compare_to # noqa
match = lambda item: isinstance(item, type(compare_to.pattern)) and bool( # noqa
compare_to.search(item)
)
comparison = compare_to
if isinstance(compare_to, type):
comparison = is_a
elif isinstance(compare_to, re.Pattern):
comparison = match
elif not callable(compare_to):
comparison = same

return any(comparison(item) for item in self.__each__())

def all(self, compare_to=truth):
is_a = lambda item: isinstance(item, compare_to) # noqa
same = lambda item: item == compare_to # noqa
match = lambda item: isinstance(item, type(compare_to.pattern)) and bool( # noqa
compare_to.search(item)
)
comparison = compare_to
if isinstance(compare_to, type):
comparison = is_a
elif isinstance(compare_to, re.Pattern):
comparison = match
elif not callable(compare_to):
comparison = same
return not self.any(inverse(comparison))

@configure()
def collect(self, into, to_tuple, func):
"""
Expand Down Expand Up @@ -147,6 +181,40 @@ def inject(self, func_or_initial, func=NOT_USED):
else:
raise

def none(self, compare_to=truth):
is_a = lambda item: isinstance(item, compare_to) # noqa
same = lambda item: item == compare_to # noqa
match = lambda item: isinstance(item, type(compare_to.pattern)) and bool( # noqa
compare_to.search(item)
)
comparison = compare_to
if isinstance(compare_to, type):
comparison = is_a
elif isinstance(compare_to, re.Pattern):
comparison = match
elif not callable(compare_to):
comparison = same
return not self.any(comparison)

def one(self, compare_to=truth):
is_a = lambda item: isinstance(item, compare_to) # noqa
same = lambda item: item == compare_to # noqa
match = lambda item: isinstance(item, type(compare_to.pattern)) and bool( # noqa
compare_to.search(item)
)
comparison = compare_to
if isinstance(compare_to, type):
comparison = is_a
elif isinstance(compare_to, re.Pattern):
comparison = match
elif not callable(compare_to):
comparison = same
tail = dropwhile(inverse(comparison), self.__each__())
if next(tail, NOT_FOUND) == NOT_FOUND:
return False
else:
return not any(comparison(item) for item in tail)

@configure(use_into=False, use_to_tuple=False)
def reject(self, predicate):
"""
Expand Down
Empty file added tests/enumerable/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions tests/enumerable/none_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest
import re
from pyby import EnumerableList


@pytest.fixture
def empty_list():
return EnumerableList()


@pytest.fixture
def numbers():
return EnumerableList([1, 2, 3])


def test_none(empty_list, numbers):
assert not numbers.none()
assert empty_list.none()
assert EnumerableList([False, None]).none()


def test_none_with_an_object(numbers):
assert not numbers.none(3)
assert numbers.none(4)


def test_none_with_a_predicate(empty_list, numbers):
assert not EnumerableList([0]).none(is_zero)
assert empty_list.none(is_zero)
assert numbers.none(is_zero)


def test_none_with_a_regex_pattern(numbers):
string_pattern = re.compile(r"\d")
assert numbers.none(string_pattern)
numbers.append("the number 69")
assert not numbers.none(string_pattern)
bytes_pattern = re.compile(r"\d".encode())
assert numbers.none(bytes_pattern)
numbers.append(b"binary 420")
assert not numbers.none(bytes_pattern)


def test_none_with_a_class(numbers):
assert not numbers.none(int)
assert numbers.none(str)


def is_zero(element):
return element == 0
54 changes: 54 additions & 0 deletions tests/enumerable/one_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest
import re
from pyby import EnumerableList


@pytest.fixture
def empty_list():
return EnumerableList()


@pytest.fixture
def numbers():
return EnumerableList([1, 2, 3])


def test_one(empty_list, numbers):
assert not numbers.one()
assert not empty_list.one()
assert not EnumerableList([False]).one()
assert EnumerableList([True]).one()


def test_one_with_an_object(numbers):
assert numbers.one(3)
assert not numbers.one(4)


def test_one_with_a_predicate(empty_list, numbers):
assert EnumerableList([0]).one(is_zero)
assert not empty_list.one(is_zero)
assert not numbers.one(is_zero)


def test_one_with_a_regex_pattern(numbers):
string_pattern = re.compile(r"\d")
assert not numbers.one(string_pattern)
numbers.append("the number 69")
assert numbers.one(string_pattern)
numbers.append("another number 69")
assert not numbers.one(string_pattern)
bytes_pattern = re.compile(r"\d".encode())
assert not numbers.one(bytes_pattern)
numbers.append(b"binary 420")
assert numbers.one(bytes_pattern)


def test_one_with_a_class(numbers):
assert not numbers.one(int)
numbers.append(1.23)
assert numbers.one(float)


def is_zero(element):
return element == 0
69 changes: 69 additions & 0 deletions tests/enumerable_list_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import re
from operator import add
from pyby import EnumerableList
from .test_helpers import assert_enumerable_list, assert_enumerator, pass_through
Expand Down Expand Up @@ -33,6 +34,74 @@ def test_repr(letters):
assert repr(letters) == "EnumerableList(['a', 'b', 'c'])"


def test_any(empty_list, numbers):
assert numbers.any()
assert not empty_list.any()
assert not EnumerableList([False, None]).any()


def test_any_with_an_object(numbers):
assert numbers.any(3)
assert not numbers.any(4)


def test_any_with_a_predicate(empty_list, numbers):
assert EnumerableList([0]).any(is_zero)
assert not empty_list.any(is_zero)
assert not numbers.any(is_zero)


def test_any_with_a_regex_pattern(numbers):
string_pattern = re.compile(r"\d")
assert not numbers.any(string_pattern)
numbers.append("the number 69")
assert numbers.any(string_pattern)
bytes_pattern = re.compile(r"\d".encode())
assert not numbers.any(bytes_pattern)
numbers.append(b"binary 420")
assert numbers.any(bytes_pattern)


def test_any_with_a_class(numbers):
assert numbers.any(int)
assert not numbers.any(str)


def test_all(empty_list, numbers):
assert numbers.all()
assert empty_list.all()
assert not EnumerableList([False, None]).all()


def test_all_with_an_object(numbers):
assert not numbers.all(3)
assert EnumerableList([4, 4, 4]).all(4)


def test_all_with_a_predicate(empty_list, numbers):
assert empty_list.all(is_zero)
assert EnumerableList([0]).all(is_zero)
assert not numbers.all(larger_than_one)


# def test_all_with_a_regex_pattern(empty_list, numbers):
# string_pattern = re.compile(r"\d")

# assert not numbers.all(string_pattern)
# assert numbers.all(string_pattern)

# bytes_pattern = re.compile(r"\d".encode())
# assert not numbers.any(bytes_pattern)
# numbers.append(b"420")
# assert numbers.any(bytes_pattern)


def test_all_with_a_class(numbers, list_with_a_tuple):
assert numbers.all(int)
assert not numbers.all(str)
assert not list_with_a_tuple.all(int)


def test_collect_with_a_function_maps_over_the_items_and_returns_an_enumerable_list(numbers):
assert_enumerable_list(numbers.collect(increment), [2, 3, 4])

Expand Down
4 changes: 4 additions & 0 deletions tests/enumerable_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def enumerable():
@pytest.mark.parametrize(
"method_name",
[
"any",
"all",
"each",
"collect",
"collect_concat",
Expand All @@ -26,6 +28,8 @@ def enumerable():
"inject",
"map",
"member",
"none",
"one",
"reduce",
"reject",
"select",
Expand Down