1 # vim: set et sts=4 sw=4 encoding=utf-8 :
6 This module provides a convenient and extensible command dispatching mechanism.
7 It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
8 and translates commands to functions/objects/methods.
11 class Error(RuntimeError):
13 Error(command) -> Error instance :: Base dispatching exceptions class.
15 All exceptions raised by the Dispatcher inherits from this one, so you can
16 easily catch any dispatching exception.
18 command - is the command that raised the exception, expressed as a list of
19 paths (or subcommands).
23 class HandlerError(Error):
25 HandlerError(command) -> HandlerError instance :: Base handlers exception.
27 All exceptions raised by the handlers should inherit from this one, so
28 dispatching errors could be separated from real programming errors (bugs).
32 class CommandNotFoundError(Error):
34 CommandNotFoundError(command) -> CommandNotFoundError instance
36 This exception is raised when the command received can't be dispatched
37 because there is no handlers to process it.
40 def __init__(self, command):
41 r"""Initialize the Error object.
43 See Error class documentation for more info.
45 self.command = command
48 return 'Command not found: "%s"' % ' '.join(self.command)
51 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
53 This is a decorator to mark a callable object as a dispatcher handler.
55 help - Help string for the handler.
59 raise TypeError("'help' should not be empty")
60 f._dispatcher_help = help
64 def is_handler(handler):
65 r"is_handler(handler) -> bool :: Tell if a object is a handler."
66 return callable(handler) and hasattr(handler, '_dispatcher_help')
68 def get_help(handler):
69 r"get_help(handler) -> unicode :: Get a handler's help string."
70 if not is_handler(handler):
71 raise TypeError("'%s' should be a handler" % handler.__name__)
72 return handler._dispatcher_help
75 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
77 All dispatcher handlers should inherit from this class to have some extra
81 @handler(u'List available commands.')
83 r"""commands() -> generator :: List the available commands."""
84 return (a for a in dir(self) if is_handler(getattr(self, a)))
86 @handler(u'Show available commands with their help.')
87 def help(self, command=None):
88 r"""help([command]) -> unicode/dict :: Show help on available commands.
90 If command is specified, it returns the help of that particular command.
91 If not, it returns a dictionary which keys are the available commands
92 and values are the help strings.
95 return dict((a, get_help(getattr(self, a)))
96 for a in dir(self) if is_handler(getattr(self, a)))
97 if not hasattr(self, command):
98 raise CommandNotFoundError(command)
99 handler = getattr(self, command)
100 if not is_handler(handler):
101 raise CommandNotFoundError(command)
102 return get_help(handler)
105 r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
107 This class provides a modular and extensible dispatching mechanism. You
108 can specify root 'routes' (as a dict where the key is the string of the
109 root command and the value is a callable object to handle that command,
110 or a subcommand if the callable is an instance and the command can be
113 The command can have arguments, separated by (any number of) spaces.
115 The dispatcher tries to route the command as deeply as it can, passing
116 the other "path" components as arguments to the callable. To route the
117 command it inspects the callable attributes to find a suitable callable
118 attribute to handle the command in a more specific way, and so on.
121 >>> d = Dispatcher(dict(handler=some_handler))
122 >>> d.dispatch('handler attribute method arg1 arg2')
124 If 'some_handler' is an object with an 'attribute' that is another
125 object which has a method named 'method', then
126 some_handler.attribute.method('arg1', 'arg2') will be called. If
127 some_handler is a function, then some_handler('attribute', 'method',
128 'arg1', 'arg2') will be called. The handler "tree" can be as complex
129 and deep as you want.
131 If some command can't be dispatched (because there is no root handler or
132 there is no matching callable attribute), a CommandNotFoundError is raised.
135 def __init__(self, routes=dict()):
136 r"""Initialize the Dispatcher object.
138 See Dispatcher class documentation for more info.
142 def dispatch(self, route):
143 r"""dispatch(route) -> None :: Dispatch a command string.
145 This method searches for a suitable callable object in the routes
146 "tree" and call it, or raises a CommandNotFoundError if the command
150 route = route.split() # TODO support "" and keyword arguments
152 raise CommandNotFoundError(command)
153 command.append(route[0])
154 handler = self.routes.get(route[0], None)
156 raise CommandNotFoundError(command)
158 while not is_handler(handler):
160 raise CommandNotFoundError(command)
161 command.append(route[0])
162 if not hasattr(handler, route[0]):
163 raise CommandNotFoundError(command)
164 handler = getattr(handler, route[0])
166 return handler(*route)
169 if __name__ == '__main__':
171 @handler(u"test: Print all the arguments, return nothing.")
172 def test_func(*args):
175 class TestClassSubHandler(Handler):
176 @handler(u"subcmd: Print all the arguments, return nothing.")
177 def subcmd(self, *args):
178 print 'class.subclass.subcmd:', args
180 class TestClass(Handler):
181 @handler(u"cmd1: Print all the arguments, return nothing.")
182 def cmd1(self, *args):
183 print 'class.cmd1:', args
184 @handler(u"cmd2: Print all the arguments, return nothing.")
185 def cmd2(self, *args):
186 print 'class.cmd2:', args
187 subclass = TestClassSubHandler()
189 test_class = TestClass()
196 d.dispatch('func arg1 arg2 arg3')
197 print 'inst commands:', tuple(d.dispatch('inst commands'))
198 print 'inst help:', d.dispatch('inst help')
199 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
200 d.dispatch('inst cmd2 arg1 arg2')
201 print 'inst subclass help:', d.dispatch('inst subclass help')
202 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
205 except CommandNotFoundError, e:
206 print 'Not found:', e
208 d.dispatch('sucutrule piquete culete')
209 except CommandNotFoundError, e:
210 print 'Not found:', e
212 d.dispatch('inst cmd3 arg1 arg2 arg3')
213 except CommandNotFoundError, e:
214 print 'Not found:', e