1 # vim: set et sts=4 sw=4 encoding=utf-8 :
3 r"""Command dispatcher.
5 This module provides a convenient and extensible command dispatching mechanism.
6 It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
7 and translates commands to functions/objects/methods.
10 __ALL__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
11 'Dispatcher', 'handler', 'is_handler', 'get_help')
13 class Error(RuntimeError):
14 r"""Error(command) -> Error instance :: Base dispatching exceptions class.
16 All exceptions raised by the Dispatcher inherits from this one, so you can
17 easily catch any dispatching exception.
19 command - is the command that raised the exception, expressed as a list of
20 paths (or subcommands).
24 class HandlerError(Error):
25 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
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 CommandError(Error):
33 r"""CommandError(command) -> CommandError instance :: Base command error.
35 This exception is raised when there's a problem with the command itself.
36 It's the base class for all command (as a string) related error.
39 def __init__(self, command):
40 r"""Initialize the object.
42 See class documentation for more info.
44 self.command = command
47 return 'Command error: "%s"' % self.command
49 class CommandNotFoundError(CommandError):
50 r"""CommandNotFoundError(command) -> CommandNotFoundError instance.
52 This exception is raised when the command received can't be dispatched
53 because there is no handlers to process it.
57 return 'Command not found: "%s"' % ' '.join(
58 repr(c) for c in self.command)
60 class ParseError(CommandError):
61 r"""ParseError(command[, desc]) -> ParseError instance
63 This exception is raised when there is an error parsing a command.
65 command - Command that can't be parsed.
67 desc - Description of the error.
70 def __init__(self, command, desc="can't parse"):
71 r"""Initialize the object.
73 See class documentation for more info.
75 self.command = command
79 return 'Syntax error, %s: %s' % (self.desc, self.command)
82 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
84 This is a decorator to mark a callable object as a dispatcher handler.
86 help - Help string for the handler.
90 raise TypeError("'help' should not be empty")
91 f._dispatcher_help = help
95 def is_handler(handler):
96 r"is_handler(handler) -> bool :: Tell if a object is a handler."
97 return callable(handler) and hasattr(handler, '_dispatcher_help')
99 def get_help(handler):
100 r"get_help(handler) -> unicode :: Get a handler's help string."
101 if not is_handler(handler):
102 raise TypeError("'%s' should be a handler" % handler.__name__)
103 return handler._dispatcher_help
106 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
108 All dispatcher handlers should inherit from this class to have some extra
112 @handler(u'List available commands.')
114 r"""commands() -> generator :: List the available commands."""
115 return (a for a in dir(self) if is_handler(getattr(self, a)))
117 @handler(u'Show available commands with their help.')
118 def help(self, command=None):
119 r"""help([command]) -> unicode/dict :: Show help on available commands.
121 If command is specified, it returns the help of that particular command.
122 If not, it returns a dictionary which keys are the available commands
123 and values are the help strings.
126 return dict((a, get_help(getattr(self, a)))
127 for a in dir(self) if is_handler(getattr(self, a)))
128 if not hasattr(self, command):
129 raise CommandNotFoundError(command)
130 handler = getattr(self, command)
131 if not is_handler(handler):
132 raise CommandNotFoundError(command)
133 return get_help(handler)
135 def parse_command(command):
136 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
138 This function parses a command and split it into a list of parameters. It
139 has a similar to bash commandline parser. Spaces are the basic token
140 separator but you can group several tokens into one by using (single or
141 double) quotes. You can escape the quotes with a backslash (\' and \"),
142 express a backslash literal using a double backslash (\\), use special
143 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
144 single quotes inside a double quoted token or vice-versa.
146 Additionally it accepts keyword arguments. When an (not-escaped) equal
147 sign (=) is found, the argument is considered a keyword, and the next
148 argument it's interpreted as its value.
150 This function returns a tuple containing a list and a dictionary. The
151 first has the positional arguments, the second, the keyword arguments.
153 There is no restriction about the order, a keyword argument can be
154 followed by a positional argument and vice-versa. All type of arguments
155 are grouped in the list/dict returned. The order of the positional
156 arguments is preserved and if there are multiple keyword arguments with
157 the same key, the last value is the winner (all other values are lost).
161 >>> parse_command('hello world')
162 ([u'hello', u'world'], {})
163 >>> parse_command('hello planet=earth')
164 ([u'hello'], {u'planet': u'earth'})
165 >>> parse_command('hello planet="third rock from the sun"')
166 ([u'hello'], {u'planet': u'third rock from the sun'})
167 >>> parse_command(u' planet="third rock from the sun" hello ')
168 ([u'hello'], {u'planet': u'third rock from the sun'})
169 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
171 ([u'hi, hello', u'how are you'], {u'planet': u'third rock from the sun'})
172 >>> parse_command(u'one two three "fourth number"=four')
173 ([u'one', u'two', u'three'], {u'fourth number': u'four'})
174 >>> parse_command(u'one two three "fourth number=four"')
175 ([u'one', u'two', u'three', u'fourth number=four'], {})
176 >>> parse_command(u'one two three fourth\=four')
177 ([u'one', u'two', u'three', u'fourth=four'], {})
178 >>> parse_command(u'one two three fourth=four=five')
179 ([u'one', u'two', u'three'], {u'fourth': u'four=five'})
180 >>> parse_command(ur'nice\nlong\n\ttext')
181 ([u'nice\nlong\n\ttext'], {})
182 >>> parse_command('=hello')
185 This examples are syntax errors:
186 Missing quote: "hello world
187 Missing value: hello=
189 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
190 separators = (u' ', u'\t', u'\v', u'\n') # token separators
191 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
201 for e in escaped_chars:
203 buff += eval(u'"\\' + e + u'"')
209 # Escaped sequence start
218 if c == EQUAL: # Keyword found
222 if keyword is not None: # Value found
223 dic[str(keyword)] = buff
225 else: # Normal parameter found
237 # Check if a keyword is added
238 if c == EQUAL and keyword is None and buff:
248 # Inside a double quote
255 # Inside a single quote
262 assert 0, u'Unexpected state'
263 if state == DQUOTE or state == SQUOTE:
264 raise ParseError(command, u'missing closing quote (%s)' % state)
265 if not buff and keyword is not None:
266 raise ParseError(command,
267 u'keyword argument (%s) without value' % keyword)
269 if keyword is not None:
270 dic[str(keyword)] = buff
276 r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
278 This class provides a modular and extensible dispatching mechanism. You
279 can specify root 'routes' (as a dict where the key is the string of the
280 root command and the value is a callable object to handle that command,
281 or a subcommand if the callable is an instance and the command can be
284 The command can have arguments, separated by (any number of) spaces.
286 The dispatcher tries to route the command as deeply as it can, passing
287 the other "path" components as arguments to the callable. To route the
288 command it inspects the callable attributes to find a suitable callable
289 attribute to handle the command in a more specific way, and so on.
292 >>> d = Dispatcher(dict(handler=some_handler))
293 >>> d.dispatch('handler attribute method arg1 arg2 "third argument"')
295 If 'some_handler' is an object with an 'attribute' that is another
296 object which has a method named 'method', then
297 some_handler.attribute.method('arg1', 'arg2') will be called. If
298 some_handler is a function, then some_handler('attribute', 'method',
299 'arg1', 'arg2') will be called. The handler "tree" can be as complex
300 and deep as you want.
302 If some command can't be dispatched (because there is no root handler or
303 there is no matching callable attribute), a CommandNotFoundError is raised.
306 def __init__(self, routes=dict()):
307 r"""Initialize the Dispatcher object.
309 See Dispatcher class documentation for more info.
313 def dispatch(self, route):
314 r"""dispatch(route) -> None :: Dispatch a command string.
316 This method searches for a suitable callable object in the routes
317 "tree" and call it, or raises a CommandNotFoundError if the command
321 (route, kwargs) = parse_command(route)
323 raise CommandNotFoundError(command)
324 command.append(route[0])
325 handler = self.routes.get(route[0], None)
327 raise CommandNotFoundError(command)
329 while not is_handler(handler):
331 raise CommandNotFoundError(command)
332 command.append(route[0])
333 if not hasattr(handler, route[0]):
334 raise CommandNotFoundError(command)
335 handler = getattr(handler, route[0])
337 return handler(*route, **kwargs)
340 if __name__ == '__main__':
342 @handler(u"test: Print all the arguments, return nothing.")
343 def test_func(*args):
346 class TestClassSubHandler(Handler):
347 @handler(u"subcmd: Print all the arguments, return nothing.")
348 def subcmd(self, *args):
349 print 'class.subclass.subcmd:', args
351 class TestClass(Handler):
352 @handler(u"cmd1: Print all the arguments, return nothing.")
353 def cmd1(self, *args):
354 print 'class.cmd1:', args
355 @handler(u"cmd2: Print all the arguments, return nothing.")
356 def cmd2(self, *args):
357 print 'class.cmd2:', args
358 subclass = TestClassSubHandler()
360 test_class = TestClass()
367 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
368 print 'inst commands:', tuple(d.dispatch('inst commands'))
369 print 'inst help:', d.dispatch('inst help')
370 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
371 d.dispatch('inst cmd2 arg1 arg2')
372 print 'inst subclass help:', d.dispatch('inst subclass help')
373 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
376 except CommandNotFoundError, e:
377 print 'Not found:', e
379 d.dispatch('sucutrule piquete culete')
380 except CommandNotFoundError, e:
381 print 'Not found:', e
383 d.dispatch('inst cmd3 arg1 arg2 arg3')
384 except CommandNotFoundError, e:
385 print 'Not found:', e
388 print parse_command('hello world')
389 print parse_command('hello planet=earth')
390 print parse_command('hello planet="third rock from the sun"')
391 print parse_command(u' planet="third rock from the sun" hello ')
392 print parse_command(u' planet="third rock from the sun" "hi, hello"'
394 print parse_command(u'one two three "fourth number"=four')
395 print parse_command(u'one two three "fourth number=four"')
396 print parse_command(u'one two three fourth\=four')
397 print parse_command(u'one two three fourth=four=five')
398 print parse_command(ur'nice\nlong\n\ttext')
399 print parse_command('=hello')
401 parse_command('hello=')
402 except ParseError, e:
405 parse_command('"hello')
406 except ParseError, e: