Repository: twisted/tubes

Issue:
tubes Router is broken

Besides the broken docstring for the Router as reported in #21 the Router also generally appears not to work; or am I missing something simple here?

see my attempt at creating unit tests for Router:
https://github.com/david415/tubes/tree/add_routing_tests.0
https://github.com/david415/tubes/blob/add_routing_tests.0/tubes/test/test_routing.py

When the Router's newRoute() method is called this results in:

```
$ trial tubes.test.test_routing
tubes.test.test_routing
  TestBasicRouter
    test_basic_router ...                                               [ERROR]

===============================================================================
[ERROR]
Traceback (most recent call last):
  File "/home/user/tubes/tubes/test/test_routing.py", line 64, in test_basic_router
    evenOddTube.addRoutes()
  File "/home/user/tubes/tubes/test/test_routing.py", line 41, in addRoutes
    self.evenRoute = self.newRoute()
  File "/home/user/tubes/tubes/routing.py", line 179, in newRoute
    fount = self._out.newFount().flowTo(received)
  File "/home/user/tubes/tubes/fan.py", line 253, in flowTo
    return beginFlowingTo(self, drain)
  File "/home/user/tubes/tubes/kit.py", line 104, in beginFlowingTo
    return drain.flowingFrom(fount)
exceptions.AttributeError: '_Tubule' object has no attribute 'flowingFrom'

tubes.test.test_routing.TestBasicRouter.test_basic_router
-------------------------------------------------------------------------------
Ran 1 tests in 0.018s

FAILED (errors=1)
```


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 04f9ff1..6944375 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,6 @@ env:
     - secure: "CvFj8Df5OiDRrW7EsTGhkltdmNlYerx9hH/tSKxiNFVDBUUFaTN7rUr7kWcOKchzerGwk7zjZ4SRXyoSCs+Srht6GZxWHkNROwKpp5Xvf5clbLXbp7GO1X/L5rLgrXpGwtkhgNuHx0X2IUCDHUQAUSumPgZcNFu3emgVxEqabN0="
   matrix:
     - TOX_ENV=lint
-    - TOX_ENV=py26
     - TOX_ENV=py27
     - TOX_ENV=pypy
     - TOX_ENV=docs
diff --git a/tubes/fan.py b/tubes/fan.py
index 6322c99..67ad05f 100644
--- a/tubes/fan.py
+++ b/tubes/fan.py
@@ -355,7 +355,7 @@ class _OutDrain(object):
                 self._pause = None
             if p is not None:
                 p.unpause()
-        self.fount = fount
+        beginFlowingFrom(self, fount)
 
 
     def receive(self, item):
diff --git a/tubes/routing.py b/tubes/routing.py
index 7682fe5..725d868 100644
--- a/tubes/routing.py
+++ b/tubes/routing.py
@@ -1,4 +1,4 @@
-# -*- test-case-name: tubes.test.test_fan -*-
+# -*- test-case-name: tubes.test.test_routing -*-
 # Copyright (c) Twisted Matrix Laboratories.
 # See LICENSE for details.
 
@@ -8,87 +8,97 @@ an appropriate output, stripping the addressing information off.
 
 Use like so::
 
-    from tubes.routing import Router, Routed, to
+    from tubes.tube import receiver, series
+    from tubes.routing import Router, to
 
-    aRouter = Router(int)
+    aRouter = Router()
+    evens = aRouter.newRoute()
+    odds = aRouter.newRoute()
 
-    evens, evenFount = aRouter.newRoute()
-    odds, oddFount = aRouter.newRoute()
+    @receiver()
+    def evenOdd(item):
+        if (item % 2) == 0:
+            yield to(evens, item)
+        else:
+            yield to(odds, item)
 
-    @tube
-    class EvenOdd(object):
-        outputType = Routed(int)
-        def received(self, item):
-            if (item % 2) == 0:
-                yield to(evens, item)
-            else:
-                yield to(odds, item)
+    numbers.flowTo(series(evenOdd, aRouter.drain))
 
-    numbers.flowTo(aRouter)
+Assuming C{numbers} is a fount of counting integers, this creates two founts:
+C{evens} and C{odds}, whose outputs are even and odd integers, respectively.
+Note that C{evenOdd} also uses C{evens} and C{odds} as I{addresses}; the first
+argument to L{to} says I{where} the value will go.
 
-This creates a fount in evenFount and oddFount, which each have an outputType
-of "int".
-
-Why do this rather than just having C{EvenOdd} just call methods directly based
+Why do this rather than just having C{evenOdd} just call methods directly based
 on whether a number is even or odd?
 
 By using a L{Router}, flow control relationships are automatically preserved by
-the same mechanism that tubes usually use.  The distinct drains of evenFount
-and oddFount can both be independently paused, and the pause state will be
+the same mechanism that tubes usually use.  The distinct drains of C{evens} and
+C{odds} can both independently pause their founts, and the pause state will be
 propagated to the "numbers" fount.  If you want to send on outputs to multiple
 drains which may have complex flow-control interrelationships, you can't do
 that by calling the C{receive} method directly since any one of those methods
-might reentrantly pause you.
+might reentrantly pause its fount.
 """
 
-from .tube import tube, receiver
+from zope.interface import implementer
+
+from .tube import receiver, series
+from .itube import IDrain
 from .fan import Out
+from .kit import beginFlowingFrom
 
 if 0:
-    from zope.interface.interfaces import IInterface
-    IInterface
+    from zope.interface.interfaces import ISpecification
+    ISpecification
 
+__all__ = [
+    "Router",
+    "Routed",
+    "to",
+]
 
 
 class Routed(object):
     """
-    A L{Routed} is an interface describing another interface that has been
-    wrapped in a C{to}.  As such, it is an incomplete implementation of
-    L{IInterface}.
+    A L{Routed} is a specification describing another specification that has
+    been wrapped in a C{to}.  As such, it is an incomplete implementation of
+    L{ISpecification}.
     """
 
-    def __init__(self, interface=None):
+    def __init__(self, specification=None):
         """
-        Derive a L{Routed} version of C{interface}.
+        Derive a L{Routed} version of C{specification}.
 
-        @param interface: the interface that will be provided by the C{what}
-            attribute of providers of this interface.
-        @type interface: L{IInterface}
+        @param specification: the specification that will be provided by the
+            C{what} attribute of providers of this specification.
+        @type specification: L{ISpecification}
         """
-        self.interface = interface
+        self.specification = specification
 
 
     def isOrExtends(self, other):
         """
         Is this L{Routed} substitutable for the given specification?
 
-        @param other: Another L{Routed} or interface.
-        @type other: L{IInterface}
+        @param other: Another L{Routed} or specification.
+        @type other: L{ISpecification}
 
         @return: L{True} if so, L{False} if not.
         """
         if not isinstance(other, Routed):
             return False
-        if self.interface is None or other.interface is None:
+        if self.specification is None or other.specification is None:
             return True
-        return self.interface.isOrExtends(other.interface)
+        return self.specification.isOrExtends(other.specification)
 
 
     def providedBy(self, instance):
         """
         Is this L{Routed} provided by a particular value?
 
-        @param instance: an object which may or may not provide this interface.
+        @param instance: an object which may or may not provide this
+            specification.
         @type instance: L{object}
 
         @return: L{True} if so, L{False} if not.
@@ -96,9 +106,27 @@ class Routed(object):
         """
         if not isinstance(instance, _To):
             return False
-        if self.interface is None:
+        if self.specification is None:
             return True
-        return self.interface.providedBy(instance._what)
+        return self.specification.providedBy(instance._what)
+
+
+    def __eq__(self, other):
+        """
+        Routed(X) compares equal to Routed(X).
+        """
+        if not isinstance(other, Routed):
+            return NotImplemented
+        return self.specification == other.specification
+
+
+    def __ne__(self, other):
+        """
+        Routed(X) compares unequal to Routed(Y).
+        """
+        if not isinstance(other, Routed):
+            return NotImplemented
+        return self.specification != other.specification
 
 
 
@@ -119,6 +147,13 @@ class _To(object):
         self._what = what
 
 
+    def __repr__(self):
+        """
+        @return: an explanatory string.
+        """
+        return "to({!r}, {!r})".format(self._where, self._what)
+
+
 
 def to(where, what):
     """
@@ -138,7 +173,6 @@ def to(where, what):
 
 
 
-@tube
 class Router(object):
     """
     A drain with multiple founts that consumes L{Routed}C{(IX)} from its input
@@ -154,10 +188,21 @@ class Router(object):
     def __init__(self, outputType=None):
         self._out = Out()
         self._outputType = outputType
+        @implementer(IDrain)
+        class NullDrain(object):
+            inputType = outputType
+            fount = None
+            def flowingFrom(self, fount):
+                beginFlowingFrom(self, fount)
+            def receive(self, item):
+                pass
+            def flowStopped(self, reason):
+                pass
+        self.newRoute().flowTo(NullDrain())
         self.drain = self._out.drain
 
 
-    def newRoute(self):
+    def newRoute(self, name=None):
         """
         Create a new route.
 
@@ -168,13 +213,18 @@ class Router(object):
         to L{Router.drain} should be a L{to} constructed with a value returned
         from this method as the "where" parameter.
 
+        @param name: Give the route a name for debugging purposes.
+        @type name: native L{str}
+
         @return: L{IFount}
         """
         @receiver(inputType=Routed(self._outputType),
-                  outputType=self._outputType)
+                  outputType=self._outputType,
+                  name=name)
         def received(item):
-            if isinstance(item, to):
-                if item._where is fount:
-                    yield item._what
-        fount = self._out.newFount().flowTo(received)
+            if not isinstance(item, _To):
+                raise TypeError("{0} is not routed".format(item))
+            if item._where is fount:
+                yield item._what
+        fount = self._out.newFount().flowTo(series(received))
         return fount
diff --git a/tubes/tube.py b/tubes/tube.py
index fe23b49..39de857 100644
--- a/tubes/tube.py
+++ b/tubes/tube.py
@@ -123,17 +123,20 @@ class _Tubule(object):
     """
     A tube created for the C{@tube} decorator.
     """
-    def __init__(self, inputType, outputType, received):
+    def __init__(self, inputType, outputType, received, name):
         """
         @param inputType: An interface for the input type.
 
         @param outputType: an interface for the output type.
 
         @param received: a callable to implement C{received}.
+
+        @param name: a string describing this L{_Tubule}.
         """
         self.inputType = inputType
         self.outputType = outputType
         self.received = received
+        self._name = name
 
 
     def started(self):
@@ -156,8 +159,15 @@ class _Tubule(object):
         return ()
 
 
+    def __repr__(self):
+        """
+        @return: this L{_Tubule}'s name.
+        """
+        return self._name
 
-def receiver(inputType=None, outputType=None):
+
+
+def receiver(inputType=None, outputType=None, name=None):
     """
     Decorator for a stateless function which receives inputs.
 
@@ -171,12 +181,16 @@ def receiver(inputType=None, outputType=None):
 
     @param outputType: The C{outputType} attribute of the resulting L{ITube}.
 
+    @param name: a name describing the tubule for it to show as in a C{repr}.
+    @type name: native L{str}
+
     @return: a stateless tube with the decorated method as its C{received}
         method.
     @rtype: L{ITube}
     """
     def decorator(decoratee):
-        return _Tubule(inputType, outputType, decoratee)
+        return _Tubule(inputType, outputType, decoratee,
+                       name if name is not None else decoratee.__name__)
     return decorator
 
 


Test patch:
diff --git a/tubes/test/test_routing.py b/tubes/test/test_routing.py
new file mode 100644
index 0000000..f0b4225
--- /dev/null
+++ b/tubes/test/test_routing.py
@@ -0,0 +1,117 @@
+# -*- test-case-name: tubes.test.test_routing -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{tubes.routing}.
+"""
+
+from unittest import TestCase
+
+from ..routing import Router, to, Routed
+from ..tube import series, receiver
+from .util import FakeFount, FakeDrain, IFakeOutput, IFakeInput
+
+if 0:
+    # Names used by PyDoctor.
+    from ..itube import IFount
+    IFount
+
+
+
+class RouterTests(TestCase):
+    """
+    Tests for L{Router}.
+    """
+
+    def test_twoRoutes(self):
+        """
+        The L{IFount} feeding into a L{Router} may yield L{to} each route
+        returned from L{Router.newRoute}.
+        """
+        @receiver()
+        def chooser(item):
+            if item % 2:
+                yield to(odd, item)
+            else:
+                yield to(even, item)
+        router = Router()
+        even = router.newRoute("even")
+        evens = FakeDrain()
+        even.flowTo(evens)
+        odd = router.newRoute("odd")
+        odds = FakeDrain()
+        odd.flowTo(odds)
+        ff = FakeFount()
+        routeDrain = series(chooser, router.drain)
+        ff.flowTo(routeDrain)
+        for x in range(10):
+            ff.drain.receive(x)
+        self.assertEqual(odds.received, [1, 3, 5, 7, 9])
+        self.assertEqual(evens.received, [0, 2, 4, 6, 8])
+
+
+    def test_routeRepr(self):
+        """
+        It's useful to C{repr} a route for debugging purposes; if we give it a
+        name, its C{repr} will contain that name.
+        """
+        router = Router()
+        route = router.newRoute("hello")
+        self.assertTrue("hello" in repr(route))
+
+
+    def test_defaultTypeChecking(self):
+        """
+        L{Router}'s drain accepts only L{Routed} objects; if no other type is
+        specified, L{Routed}C{(None)}.
+        """
+        router = Router()
+        ff = FakeFount(IFakeOutput)
+        self.assertEqual(router.drain.inputType, Routed(None))
+        self.assertRaises(TypeError, ff.flowTo, router.drain)
+        self.assertEqual(router.newRoute().outputType, None)
+
+
+    def test_specifiedTypeChecking(self):
+        """
+        The C{outputType} argument to L{Router}'s constructor specifies the
+        type of output that its routes will provide, and also the routed type
+        required as an input.
+        """
+        router = Router(IFakeInput)
+        incorrect = FakeDrain(IFakeOutput)
+        correct = FakeDrain(IFakeInput)
+        self.assertEqual(router.drain.inputType, Routed(IFakeInput))
+        self.assertEqual(router.newRoute().outputType, IFakeInput)
+        self.assertRaises(TypeError, router.newRoute().flowTo, incorrect)
+        self.assertEqual(router.newRoute().flowTo(correct), None)
+        correctFount = FakeFount(Routed(IFakeInput))
+        incorrectFount = FakeFount(Routed(IFakeOutput))
+        self.assertRaises(TypeError, incorrectFount.flowTo, router.drain)
+        self.assertEquals(None, correctFount.flowTo(router.drain))
+
+
+
+class RoutedTests(TestCase):
+    """
+    Tests for L{Routed}.
+    """
+
+    def test_eq(self):
+        """
+        C{==} on L{Routed} is L{True} for equivalent ones, L{False} otherwise.
+        """
+        self.assertEqual(True, Routed(IFakeInput) == Routed(IFakeInput))
+        self.assertEqual(False, Routed(IFakeInput) == Routed(IFakeOutput))
+        self.assertEqual(False, Routed() == 7)
+
+
+    def test_ne(self):
+        """
+        C{==} on L{Routed} is L{False} for equivalent ones, L{True} otherwise.
+        """
+        self.assertEqual(False, Routed(IFakeInput) != Routed(IFakeInput))
+        self.assertEqual(True, Routed(IFakeInput) != Routed(IFakeOutput))
+        self.assertEqual(True, Routed() != 7)
+
diff --git a/tubes/test/util.py b/tubes/test/util.py
index dd36224..0ebf41b 100644
--- a/tubes/test/util.py
+++ b/tubes/test/util.py
@@ -77,13 +77,12 @@ class FakeDrain(object):
     @type stopped: L{list}
     """
 
-    inputType = None
-
     fount = None
 
-    def __init__(self):
+    def __init__(self, inputType=None):
         self.received = []
         self.stopped = []
+        self.inputType = inputType
 
 
     def flowingFrom(self, fount):