# 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,
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