]> git.llucax.com Git - software/pymin.git/blobdiff - dispatcher.py
Update TODO (add logging).
[software/pymin.git] / dispatcher.py
index 78ae2460d4c7b1c6e2074a26e3148ba7d864f864..b78db93a3e0f042c273821dda82c44450aec8403 100644 (file)
 # 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).
+    """
+    pass
 
-       def __str__(self):
-               return repr(cmd)
+class HandlerError(Error):
+    r"""
+    HandlerError(command) -> HandlerError instance :: Base handlers exception.
 
-class Dispatcher:
-
-       def __init__(self, routes=dict()):
-               self.routes = routes
+    All exceptions raised by the handlers should inherit from this one, so
+    dispatching errors could be separated from real programming errors (bugs).
+    """
+    pass
 
-       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
+class CommandNotFoundError(Error):
+    r"""
+    CommandNotFoundError(command) -> CommandNotFoundError instance
 
+    This exception is raised when the command received can't be dispatched
+    because there is no handlers to process it.
+    """
 
-def test_func(*args):
-       print 'func:', args
+    def __init__(self, command):
+        r"""Initialize the Error object.
 
+        See Error class documentation for more info.
+        """
+        self.command = command
 
-class TestClassSubHandler:
+    def __str__(self):
+        return 'Command not found: "%s"' % ' '.join(self.command)
 
-       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,
@@ -70,20 +149,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')
-
-# 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