Skip to content

Commit

Permalink
feat(task): add a new task, sghi.task.Supplier (#37)
Browse files Browse the repository at this point in the history
Add a new `Task` type, `sghi.task.Supplier`. This is a specialized
`Task` that always supplies a result without requiring any input.
  • Loading branch information
kennedykori committed Apr 11, 2024
1 parent 53ca0fb commit d06824f
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 4 deletions.
58 changes: 57 additions & 1 deletion src/sghi/task/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,6 @@ class Consume(Task[_IT, _IT], Generic[_IT]):
.. deprecated:: 1.4
To be removed in v2.
"""

__slots__ = ("_accept",)
Expand Down Expand Up @@ -475,12 +474,66 @@ def execute(self, an_input: _IT) -> _OT:
)


@final
class Supplier(Task[None, _OT], Generic[_OT]):
"""A specialized :class:`Task` that supplies/provides a result.
This subclass of ``Task`` represents a task that provides a result without
needing any input value. It wraps/decorates a callable that returns the
desired output.
.. versionadded:: 1.4
"""

__slots__ = ("_source_callable", "__dict__")

def __init__(self, source_callable: Callable[[], _OT]):
"""Create a ``Supplier`` instance that wraps the provided callable.
:param source_callable: A callable object that takes no arguments and
returns the desired result.
:raise TypeError: If ``source_callable`` is not a callable object.
"""
super().__init__()
ensure_callable(
source_callable,
message="'source_callable' MUST be a callable object.",
)
self._source_callable: Callable[[], _OT]
self._source_callable = source_callable
update_wrapper(self, self._source_callable)

@override
def __call__(self, an_input: None = None) -> _OT:
return self.execute()

@override
def execute(self, an_input: None = None) -> _OT:
"""Retrieve and return a result/value.
This method overrides the base class ``execute`` and simply calls the
wrapped callable to retrieve the result. Since this is a ``Supplier``,
the argument to this method is ignored.
:param an_input: Unused parameter. Defaults to ``None``. This is only
here to maintain compatibility with the ``Task`` interface.
:return: The retrieved/supplied result.
"""
return self._source_callable()


chain = Chain

consume = Consume # type: ignore[reportDeprecated]

pipe = Pipe

supplier = Supplier

supply = Supplier


# =============================================================================
# CONCURRENT EXECUTOR
Expand Down Expand Up @@ -728,10 +781,13 @@ def execute(self, an_input: _IT) -> _OT:
"ConcurrentExecutorDisposedError",
"Consume",
"Pipe",
"Supplier",
"Task",
"chain",
"consume",
"execute_concurrently",
"pipe",
"supplier",
"supply",
"task",
]
48 changes: 45 additions & 3 deletions test/sghi/task_tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import operator
import time
from collections.abc import Sequence
from collections.abc import Iterable, Sequence
from concurrent.futures import wait
from functools import partial
from typing import TYPE_CHECKING
Expand All @@ -15,6 +15,7 @@
chain,
consume,
pipe,
supplier,
task,
)
from sghi.utils import ensure_greater_than, future_succeeded
Expand Down Expand Up @@ -335,10 +336,10 @@ def test_and_then_method_returns_expected_value(self) -> None:
# Should delegate to `Task.and_then` when given a `Task` instance.
assert isinstance(collector.and_then(task(collection2.add)), Task)

def test_instantiation_with_a_none_input_fails(self) -> None:
def test_instantiation_with_a_none_callable_input_fails(self) -> None:
"""
:class:`consume` constructor should raise ``ValueError`` when given a
``None`` input.
none-callable input.
"""
with pytest.raises(ValueError, match="MUST be a callable") as exc_info:
consume(accept=None) # type: ignore
Expand Down Expand Up @@ -487,6 +488,47 @@ def test_tasks_property_return_value_has_tasks_only(self) -> None:
assert isinstance(_task, Task)


class TestSupplier(TestCase):
"""Tests for the :class:`supplier` ``Task``."""

def test_instantiation_with_a_non_callable_input_fails(self) -> None:
"""
:class:`consume` constructor should raise ``ValueError`` when given a
none-callable input.
"""
with pytest.raises(ValueError, match="MUST be a callable") as exc_info:
supplier(source_callable=None) # type: ignore

assert (
exc_info.value.args[0]
== "'source_callable' MUST be a callable object."
)

def test_usage_as_a_decorator_produces_the_expected_results(self) -> None:
"""
:class:`supplier` should make a callable it decorates a ``Task``.
"""

@supplier
def ints_supplier() -> Iterable[int]:
yield from range(5)

assert isinstance(ints_supplier, supplier)
assert tuple(ints_supplier()) == (0, 1, 2, 3, 4)

def test_execute_returns_the_expected_value(self) -> None:
"""
:meth:`supplier.execute` should return the same value as it's wrapped
callable.
"""

def ints_supplier() -> Iterable[int]:
yield from range(5)

_supplier = supplier(ints_supplier)
assert tuple(_supplier()) == tuple(ints_supplier()) == (0, 1, 2, 3, 4)


class TestTask(TestCase):
"""Tests of the :class:`Task` interface default method implementations."""

Expand Down

0 comments on commit d06824f

Please sign in to comment.