]> git.llucax.com Git - software/pymin.git/blobdiff - dispatcher.py
Improve error reporting.
[software/pymin.git] / dispatcher.py
index 78ae2460d4c7b1c6e2074a26e3148ba7d864f864..0284da5c499b03805babd97caec2dda8fa99c5ab 100644 (file)
 # vim: set et sts=4 sw=4 encoding=utf-8 :
 
 # vim: set et sts=4 sw=4 encoding=utf-8 :
 
-class BadRouteError(Exception):
+r"""
+Command dispatcher.
 
 
-       def __init__(self,cmd):
-               self.cmd = cmd
+This module provides a convenient and extensible command dispatching mechanism.
+It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
+and translates commands to functions/objects/methods.
+"""
 
 
-       def __str__(self):
-               return repr(cmd)
+class Error(RuntimeError):
+    r"""
+    Error(command) -> Error instance :: Base dispatching exceptions class.
 
 
-class CommandNotFoundError(Exception):
+    All exceptions raised by the Dispatcher inherits from this one, so you can
+    easily catch any dispatching exception.
 
 
-       def __init__(self,cmd):
-               self.cmd = cmd
+    command - is the command that raised the exception, expressed as a list of
+              paths (or subcommands).
+    """
 
 
-       def __str__(self):
-               return repr(cmd)
-
-class Dispatcher:
-
-       def __init__(self, routes=dict()):
-               self.routes = routes
-
-       def dispatch(self, route):
-               route = route.split() # TODO considerar comillas
-               try:
-                       handler = self.routes[route[0]]
-                       route = route[1:]
-                       while not callable(handler):
-                               handler = getattr(handler, route[0])
-                               route = route[1:]
-                       handler(*route)
-
-               except KeyError:
-                       raise CommandNotFoundError(route[0])
-               except AttributeError:
-                       raise BadRouteError(route[0])
-               except IndexError:
-                       pass
+    def __init__(self, command):
+        r"""Initialize the Error object.
 
 
+        See Error class documentation for more info.
+        """
+        self.command = command
 
 
-def test_func(*args):
-       print 'func:', args
+    def __str__(self):
+        return 'Command not found: "%s"' % ' '.join(self.command)
 
 
+class CommandNotFoundError(Error):
+    r"""
+    CommandNotFoundError(command) -> CommandNotFoundError instance
 
 
-class TestClassSubHandler:
+    This exception is raised when the command received can't be dispatched
+    because there is no handlers to process it.
+    """
+    pass
 
 
-       def subcmd(self, *args):
-               print 'class.subclass.subcmd:', args
+def handler(f):
+    f._dispatcher_handler = True
+    return f
 
 
+def is_handler(handler):
+    return callable(handler) and hasattr(handler, '_dispatcher_handler') \
+            and handler._dispatcher_handler
 
 
-class TestClass:
+class Dispatcher:
+    r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
+
+    This class provides a modular and extensible dispatching mechanism. You
+    can specify root 'routes' (as a dict where the key is the string of the
+    root command and the value is a callable object to handle that command,
+    or a subcommand if the callable is an instance and the command can be
+    sub-routed).
+
+    The command can have arguments, separated by (any number of) spaces.
+
+    The dispatcher tries to route the command as deeply as it can, passing
+    the other "path" components as arguments to the callable. To route the
+    command it inspects the callable attributes to find a suitable callable
+    attribute to handle the command in a more specific way, and so on.
+
+    Example:
+    >>> d = Dispatcher(dict(handler=some_handler))
+    >>> d.dispatch('handler attribute method arg1 arg2')
+
+    If 'some_handler' is an object with an 'attribute' that is another
+    object which has a method named 'method', then
+    some_handler.attribute.method('arg1', 'arg2') will be called. If
+    some_handler is a function, then some_handler('attribute', 'method',
+    'arg1', 'arg2') will be called. The handler "tree" can be as complex
+    and deep as you want.
+
+    If some command can't be dispatched (because there is no root handler or
+    there is no matching callable attribute), a CommandNotFoundError is raised.
+    """
+
+    def __init__(self, routes=dict()):
+        r"""Initialize the Dispatcher object.
+
+        See Dispatcher class documentation for more info.
+        """
+        self.routes = routes
+
+    def dispatch(self, route):
+        r"""dispatch(route) -> None :: Dispatch a command string.
+
+        This method searches for a suitable callable object in the routes
+        "tree" and call it, or raises a CommandNotFoundError if the command
+        can't be dispatched.
+        """
+        command = list()
+        route = route.split() # TODO support "" and keyword arguments
+        if not route:
+            raise CommandNotFoundError(command)
+        command.append(route[0])
+        handler = self.routes.get(route[0], None)
+        if handler is None:
+            raise CommandNotFoundError(command)
+        route = route[1:]
+        while not is_handler(handler):
+            if len(route) is 0:
+                raise CommandNotFoundError(command)
+            command.append(route[0])
+            if not hasattr(handler, route[0]):
+                raise CommandNotFoundError(command)
+            handler = getattr(handler, route[0])
+            route = route[1:]
+        return handler(*route)
 
 
-       def cmd1(self, *args):
-               print 'class.cmd1:', args
 
 
-       def cmd2(self, *args):
-               print 'class.cmd2:', args
+if __name__ == '__main__':
 
 
-       subclass = TestClassSubHandler()
+    @handler
+    def test_func(*args):
+          print 'func:', args
 
 
+    class TestClassSubHandler:
+        @handler
+        def subcmd(self, *args):
+            print 'class.subclass.subcmd:', args
 
 
-if __name__ == '__main__':
+    class TestClass:
+        @handler
+        def cmd1(self, *args):
+            print 'class.cmd1:', args
+        @handler
+        def cmd2(self, *args):
+            print 'class.cmd2:', args
+        subclass = TestClassSubHandler()
 
     d = Dispatcher(dict(
             func=test_func,
 
     d = Dispatcher(dict(
             func=test_func,
@@ -70,20 +139,16 @@ if __name__ == '__main__':
     d.dispatch('func arg1 arg2 arg3')
     d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
     d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
     d.dispatch('func arg1 arg2 arg3')
     d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
     d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
-
-# Ideas / TODO:
-#
-# * Soportar comillas para argumentos con espacios y otros caracteres, onda:
-#   'misc set motd "Hola!\nEste es el servidor de garombia"'
-#
-# * Soportar keyword arguments, onda que:
-#   'dns set pepe=10.10.10.1 juan=10.10.10.2'
-#   se mapee a algo como: dns.set(pepe='10.10.10.1', juan='10.10.10.2')
-#
-# Estas cosas quedan sujetas a necesitada y a definición del protocolo.
-# Para mí lo ideal es que el protocolo de red sea igual que la consola del
-# usuario, porque después de todo no va a ser más que eso, mandar comanditos.
-#
-# Por otro lado, el cliente de consola, por que no es el cliente web pero
-# accedido via ssh usando un navegador de texto como w3m???
+    try:
+        d.dispatch('')
+    except CommandNotFoundError, e:
+        print 'Not found:', e
+    try:
+        d.dispatch('sucutrule piquete culete')
+    except CommandNotFoundError, e:
+        print 'Not found:', e
+    try:
+        d.dispatch('inst cmd3 arg1 arg2 arg3')
+    except CommandNotFoundError, e:
+        print 'Not found:', e