Repository: MatterMiners/cobald

Issue:
Allow `.s` syntax for pools as well
Controller and decorator types both have a ``.s`` classmethod to allow binding them with ``>>``. Pools do not, as they are always concrete and mark the end of a binding chain.

```python3
LinearController.s() >> Pool()
```

The use of ``.s`` or not might be confusing without understanding how this work. For usability, it might be advantageous to have a dummy ``.s`` method for pools as well.

```python3
LinearController.s() >> Pool.s()
```

This might also be advantageous for "templating" Pools (as also intended for the others):

```python3
DemoPool = MyDronePool.s(collector="cobald.demo.edu")

LinearController.s() >> DemoPool(size=8)
LinearController.s() >> DemoPool(size=4)
```

Comments:

Patch:
diff --git a/src/cobald/controller/stepwise.py b/src/cobald/controller/stepwise.py
index 3b4241a..1e79f09 100644
--- a/src/cobald/controller/stepwise.py
+++ b/src/cobald/controller/stepwise.py
@@ -187,7 +187,7 @@ class UnboundStepwise(object):
         :note: The partial rules are sealed, and :py:meth:`~.UnboundStepwise.add`
                cannot be called on it.
         """
-        return Partial(Stepwise, self.base, *self.rules, *args, **kwargs)
+        return Partial(Stepwise, self.base, *self.rules, *args, __leaf__=True, **kwargs)
 
     def __call__(self, target: Pool, interval: float = None):
         if interval is None:
diff --git a/src/cobald/interfaces/_controller.py b/src/cobald/interfaces/_controller.py
index 8dcd617..e9da6c3 100644
--- a/src/cobald/interfaces/_controller.py
+++ b/src/cobald/interfaces/_controller.py
@@ -28,4 +28,4 @@ class Controller(metaclass=abc.ABCMeta):
 
             pipeline = controller(rate=10) >> pool
         """
-        return Partial(cls, *args, **kwargs)
+        return Partial(cls, *args, __leaf__=False, **kwargs)
diff --git a/src/cobald/interfaces/_partial.py b/src/cobald/interfaces/_partial.py
index 9eaab6d..7057ba9 100644
--- a/src/cobald/interfaces/_partial.py
+++ b/src/cobald/interfaces/_partial.py
@@ -1,11 +1,12 @@
 from inspect import Signature, BoundArguments
-from typing import Type, Generic, TypeVar, Tuple, Dict, TYPE_CHECKING, Union, overload
+from typing import Type, Generic, TypeVar, TYPE_CHECKING, Union, overload
 
-from ._pool import Pool
+from . import _pool
 
 if TYPE_CHECKING:
     from ._controller import Controller
     from ._proxy import PoolDecorator
+    from ._pool import Pool
     Owner = Union[Controller, PoolDecorator]
     C_co = TypeVar('C_co', bound=Owner)
 else:
@@ -31,20 +32,24 @@ class Partial(Generic[C_co]):
         # apply target by chaining
         pipeline = control >> Decorator() >> Pool()
 
+    :note: The keyword argument ``__leaf__`` is reserved for internal usage.
+
     :note: Binding :py:class:`~.Controller`\ s and :py:class:`~.Decorator`\ s
            creates a temporary :py:class:`~.PartialBind`. Only binding to a
            :py:class:`~.Pool` as the last element creates a concrete binding.
     """
-    __slots__ = ('ctor', 'args', 'kwargs')
+    __slots__ = ('ctor', 'args', 'kwargs', 'leaf')
 
-    def __init__(self, ctor: Type[C_co], *args, **kwargs):
+    def __init__(self, ctor: Type[C_co], *args, __leaf__, **kwargs):
         self.ctor = ctor
         self.args = args
         self.kwargs = kwargs
-        self._check_signature(args, kwargs)
+        self.leaf = __leaf__
+        self._check_signature()
 
-    def _check_signature(self, args: Tuple, kwargs: Dict):
-        if 'target' in kwargs or (args and isinstance(args[0], Pool)):
+    def _check_signature(self):
+        args, kwargs = self.args, self.kwargs
+        if 'target' in kwargs or (args and isinstance(args[0], _pool.Pool)):
             raise TypeError(
                 "%s[%s] cannot bind 'target' by calling. "
                 "Use `this >> target` instead." % (
@@ -52,8 +57,10 @@ class Partial(Generic[C_co]):
                 )
             )
         try:
+            if not self.leaf:
+                args = None, *args
             _ = Signature.from_callable(self.ctor).bind_partial(
-                None, *args, **kwargs
+                *args, **kwargs
             )  # type: BoundArguments
         except TypeError as err:
             message = err.args[0]
@@ -62,21 +69,38 @@ class Partial(Generic[C_co]):
             ) from err
 
     def __call__(self, *args, **kwargs) -> 'Partial[C_co]':
-        return Partial(self.ctor, *self.args, *args, **self.kwargs, **kwargs)
+        return Partial(
+            self.ctor,
+            *self.args, *args,
+            __leaf__=self.leaf,
+            **self.kwargs, **kwargs
+        )
+
+    def __construct__(self, *args, **kwargs):
+        return self.ctor(*args, *self.args, **kwargs, **self.kwargs)
 
     @overload  # noqa: F811
-    def __rshift__(self, other: 'Union[Partial, PartialBind]') -> 'PartialBind[C_co]':
+    def __rshift__(self, other: 'Union[Owner, Pool, PartialBind[Pool]]') -> 'C_co':
         ...
 
     @overload  # noqa: F811
-    def __rshift__(self, other: 'Union[Owner, Pool]') -> 'C_co':
+    def __rshift__(self, other: 'Union[Partial, PartialBind]') -> 'PartialBind[C_co]':
         ...
 
     def __rshift__(self, other):  # noqa: F811
-        if isinstance(other, (Partial, PartialBind)):
+        if isinstance(other, PartialBind):
+            return PartialBind(self, other.parent, *other.targets)
+        elif isinstance(other, Partial):
+            if other.leaf:
+                return self >> other.__construct__()
             return PartialBind(self, other)
         else:
-            return self.ctor(other, *self.args, **self.kwargs)
+            return self.__construct__(other)
+
+    def __repr__(self):
+        return '{self.__class__.__name__}(ctor={self.ctor.__name__}'.format(self=self)\
+               + ', args={self.args}, kwargs={self.kwargs}'.format(self=self) \
+               + ', leaf={self.leaf})'.format(self=self)
 
 
 class PartialBind(Generic[C_co]):
@@ -89,7 +113,11 @@ class PartialBind(Generic[C_co]):
     """
     __slots__ = ('parent', 'targets')
 
-    def __init__(self, parent: Partial[C_co], *targets: Partial[Owner]):
+    def __init__(
+            self,
+            parent: Partial[C_co],
+            *targets: 'Union[Partial[Owner], PartialBind[Owner]]'
+    ):
         self.parent = parent
         self.targets = targets
 
@@ -98,14 +126,16 @@ class PartialBind(Generic[C_co]):
         ...
 
     @overload  # noqa: F811
-    def __rshift__(self, other: Pool) -> 'C_co':
+    def __rshift__(self, other: 'Pool') -> 'C_co':
         ...
 
-    def __rshift__(self, other: Union[Pool, Partial[Owner]]):  # noqa: F811
-        if isinstance(other, Pool):
+    def __rshift__(self, other: 'Union[Pool, Partial[Owner]]'):  # noqa: F811
+        if isinstance(other, _pool.Pool):
             pool = self.targets[-1] >> other
             for owner in reversed(self.targets[:-1]):
                 pool = owner >> pool
             return self.parent >> pool
+        elif isinstance(other, Partial) and other.leaf:
+            return self >> other.__construct__()
         else:
             return PartialBind(self.parent, *self.targets, other)
diff --git a/src/cobald/interfaces/_pool.py b/src/cobald/interfaces/_pool.py
index 4f957d3..e6612de 100644
--- a/src/cobald/interfaces/_pool.py
+++ b/src/cobald/interfaces/_pool.py
@@ -1,4 +1,10 @@
 import abc
+from typing import TypeVar, Type
+
+from ._partial import Partial
+
+
+C = TypeVar('C', bound='Controller')
 
 
 class Pool(metaclass=abc.ABCMeta):
@@ -33,3 +39,16 @@ class Pool(metaclass=abc.ABCMeta):
     def allocation(self) -> float:
         """Fraction of the provided resources which are assigned for usage"""
         raise NotImplementedError
+
+    @classmethod
+    def s(cls: Type[C], *args, **kwargs) -> Partial[C]:
+        """
+        Create an unbound prototype of this class, partially applying arguments
+
+        .. code:: python
+
+            pool = RemotePool.s(port=1337)
+
+            pipeline = controller >> pool(host='localhost')
+        """
+        return Partial(cls, *args, __leaf__=True, **kwargs)
diff --git a/src/cobald/interfaces/_proxy.py b/src/cobald/interfaces/_proxy.py
index 5b81d8a..64735e5 100644
--- a/src/cobald/interfaces/_proxy.py
+++ b/src/cobald/interfaces/_proxy.py
@@ -28,7 +28,7 @@ class PoolDecorator(Pool):
 
             pipeline = controller >> decorator >> pool
         """
-        return Partial(cls, *args, **kwargs)
+        return Partial(cls, *args, __leaf__=False, **kwargs)
 
     @property
     def supply(self):


Test patch:
diff --git a/cobald_tests/interfaces/test_partial.py b/cobald_tests/interfaces/test_partial.py
index e8d42bf..d5eb642 100644
--- a/cobald_tests/interfaces/test_partial.py
+++ b/cobald_tests/interfaces/test_partial.py
@@ -45,6 +45,32 @@ class TestPartial(object):
         pipeline = partial_control >> FullMockPool()
         assert isinstance(pipeline, MockController)
 
+    def test_pool_curry_bind(self):
+        """Curry and bind the last element of a pipeline"""
+        partial_pool = FullMockPool.s()
+        assert isinstance(partial_pool, Partial)
+        partial_pool = partial_pool(demand=10)
+        assert isinstance(partial_pool, Partial)
+        partial_control = MockController.s()
+        pipeline = partial_control >> partial_pool
+        assert isinstance(pipeline, MockController)
+        assert isinstance(pipeline.target, FullMockPool)
+        assert pipeline.target.demand == 10
+
+    def test_pool_recursive(self):
+        """Curry and bind the last element of a long pipeline"""
+        partial_pool = FullMockPool.s(demand=10)
+        for _ in range(3):
+            partial_pool = partial_pool()
+            assert isinstance(partial_pool, Partial)
+        pipeline = MockController.s() >> MockDecorator.s() >> MockDecorator.s()\
+            >> partial_pool
+        assert isinstance(pipeline, MockController)
+        assert isinstance(pipeline.target, MockDecorator)
+        assert isinstance(pipeline.target.target, MockDecorator)
+        assert isinstance(pipeline.target.target.target, FullMockPool)
+        assert pipeline.target.target.target.demand == 10
+
     def test_signature_check(self):
         class ArgController(Controller):
             def __init__(self, target, a, b, c=3, *, kwa=2, kwb=3):