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).
23 def __init__(self, message):
24 r"Initialize the Error object. See class documentation for more info."
25 self.message = message
27 def __unicode__(self):
31 return unicode(self).encode('utf-8')
33 class HandlerError(Error):
34 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
36 All exceptions raised by the handlers should inherit from this one, so
37 dispatching errors could be separated from real programming errors (bugs).
41 class CommandError(Error):
42 r"""CommandError(command) -> CommandError instance :: Base command error.
44 This exception is raised when there's a problem with the command itself.
45 It's the base class for all command (as a string) related error.
48 def __init__(self, command):
49 r"Initialize the object, see class documentation for more info."
50 self.command = command
52 def __unicode__(self):
53 return u'Error in command "%s".' % u' '.join(self.command)
55 class CommandNotSpecifiedError(CommandError):
56 r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
58 This exception is raised when an empty command string is received.
62 r"Initialize the object, see class documentation for more info."
65 def __unicode__(self):
66 return u'Command not specified.'
68 class CommandIsAHandlerError(CommandError):
69 r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
71 This exception is raised when a command is a handler containing commands
72 instead of a command itself.
75 def __unicode__(self):
76 command = ' '.join(self.command)
77 return u'"%s" is a handler, not a command (type "%s help" for help).' \
80 class CommandNotInHandlerError(CommandError):
81 r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
83 This exception is raised when a command parent is a hanlder containing
84 commands, but the command itself is not found.
87 def __unicode__(self):
88 return u'Command "%(c)s" not found in handler "%(h)s" ' \
89 u'(type "%(h)s help" for help).' \
90 % dict(c=u' '.join(self.command[-1:]),
91 h=u' '.join(self.command[0:-1]))
93 class CommandNotFoundError(CommandError):
94 r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
96 This exception is raised when the command received can't be dispatched
97 because there is no handlers to process it.
100 def __unicode__(self):
101 return u'Command "%s" not found.' % u' '.join(self.command)
103 class ParseError(CommandError):
104 r"""ParseError(command[, desc]) -> ParseError instance
106 This exception is raised when there is an error parsing a command.
108 command - Command that can't be parsed.
110 desc - Description of the error.
113 def __init__(self, command, desc="can't parse"):
114 r"""Initialize the object.
116 See class documentation for more info.
118 self.command = command
121 def __unicode__(self):
122 return u'Syntax error, %s: %s' % (self.desc, self.command)
124 class HelpNotFoundError(Error):
125 r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
127 This exception is raised when a help command can't find the command
131 def __init__(self, command):
132 r"""Initialize the object.
134 See class documentation for more info.
136 self.command = command
138 def __unicode__(self):
139 return u"Can't get help for '%s', command not found." % self.command
143 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
145 This is a decorator to mark a callable object as a dispatcher handler.
147 help - Help string for the handler.
151 raise TypeError("'help' should not be empty")
152 f._dispatcher_handler = True
153 f.handler_help = help
157 def is_handler(handler):
158 r"is_handler(handler) -> bool :: Tell if a object is a handler."
159 return callable(handler) and hasattr(handler, '_dispatcher_handler') \
160 and handler._dispatcher_handler
163 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
165 All dispatcher handlers should inherit from this class to have some extra
166 commands, like help. You should override the 'handler_help' attribute to a
167 nice help message describing the handler.
170 handler_help = u'Undocumented handler'
172 @handler(u'List available commands')
174 r"""commands() -> generator :: List the available commands."""
175 return (a for a in dir(self) if is_handler(getattr(self, a)))
177 @handler(u'Show available commands with their help')
178 def help(self, command=None):
179 r"""help([command]) -> unicode/dict :: Show help on available commands.
181 If command is specified, it returns the help of that particular command.
182 If not, it returns a dictionary which keys are the available commands
183 and values are the help strings.
189 if is_handler(h) or isinstance(h, Handler):
190 d[a] = h.handler_help
192 # A command was specified
193 if not hasattr(self, command.encode('utf-8')):
194 raise HelpNotFoundError(command)
195 handler = getattr(self, command.encode('utf-8'))
196 if not is_handler(handler) and not hasattr(handler):
197 raise HelpNotFoundError(command)
198 return handler.handler_help
200 def parse_command(command):
201 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
203 This function parses a command and split it into a list of parameters. It
204 has a similar to bash commandline parser. Spaces are the basic token
205 separator but you can group several tokens into one by using (single or
206 double) quotes. You can escape the quotes with a backslash (\' and \"),
207 express a backslash literal using a double backslash (\\), use special
208 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
209 single quotes inside a double quoted token or vice-versa. A special escape
210 sequence is provided to express a NULL/None value: \N and it should appear
211 always as a separated token.
213 Additionally it accepts keyword arguments. When an (not-escaped) equal
214 sign (=) is found, the argument is considered a keyword, and the next
215 argument it's interpreted as its value.
217 This function returns a tuple containing a list and a dictionary. The
218 first has the positional arguments, the second, the keyword arguments.
220 There is no restriction about the order, a keyword argument can be
221 followed by a positional argument and vice-versa. All type of arguments
222 are grouped in the list/dict returned. The order of the positional
223 arguments is preserved and if there are multiple keyword arguments with
224 the same key, the last value is the winner (all other values are lost).
228 >>> parse_command('hello world')
229 ([u'hello', u'world'], {})
230 >>> parse_command('hello planet=earth')
231 ([u'hello'], {'planet': u'earth'})
232 >>> parse_command('hello planet="third rock from the sun"')
233 ([u'hello'], {'planet': u'third rock from the sun'})
234 >>> parse_command(u' planet="third rock from the sun" hello ')
235 ([u'hello'], {'planet': u'third rock from the sun'})
236 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
238 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
239 >>> parse_command(u'one two three "fourth number"=four')
240 ([u'one', u'two', u'three'], {'fourth number': u'four'})
241 >>> parse_command(u'one two three "fourth number=four"')
242 ([u'one', u'two', u'three', u'fourth number=four'], {})
243 >>> parse_command(u'one two three fourth\=four')
244 ([u'one', u'two', u'three', u'fourth=four'], {})
245 >>> parse_command(u'one two three fourth=four=five')
246 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
247 >>> parse_command(ur'nice\nlong\n\ttext')
248 ([u'nice\nlong\n\ttext'], {})
249 >>> parse_command('=hello')
251 >>> parse_command(r'\thello')
253 >>> parse_command(r'\N')
255 >>> parse_command(r'none=\N')
257 >>> parse_command(r'\N=none')
258 ([], {'\\N': 'none'})
259 >>> parse_command(r'Not\N')
261 >>> parse_command(r'\None')
264 This examples are syntax errors:
265 Missing quote: "hello world
266 Missing value: hello=
268 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
269 separators = (u' ', u'\t', u'\v', u'\n') # token separators
270 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
277 for n, c in enumerate(command):
280 for e in escaped_chars:
282 buff += eval(u'"\\' + e + u'"')
291 # Escaped sequence start
299 if buff and n != 2: # Not the first item (even if was a escape seq)
300 if c == EQUAL: # Keyword found
306 if keyword is not None: # Value found
307 dic[str(keyword)] = buff
309 else: # Normal parameter found
321 # Check if a keyword is added
322 if c == EQUAL and keyword is None and buff:
332 # Inside a double quote
339 # Inside a single quote
346 assert 0, u'Unexpected state'
347 if state == DQUOTE or state == SQUOTE:
348 raise ParseError(command, u'missing closing quote (%s)' % state)
349 if not buff and keyword is not None:
350 raise ParseError(command,
351 u'keyword argument (%s) without value' % keyword)
355 if keyword is not None:
356 dic[str(keyword)] = buff
362 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
364 This class provides a modular and extensible dispatching mechanism. You
365 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
367 The command can have arguments, separated by (any number of) spaces and
368 keyword arguments (see parse_command for more details).
370 The dispatcher tries to route the command as deeply as it can, passing
371 the other "path" components as arguments to the callable. To route the
372 command it inspects the callable attributes to find a suitable callable
373 attribute to handle the command in a more specific way, and so on.
376 >>> d = Dispatcher(dict(handler=some_handler))
377 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
379 If 'some_handler' is an object with an 'attribute' that is another
380 object which has a method named 'method', then
381 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
382 If some_handler is a function, then some_handler('attribute', 'method',
383 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
384 complex and deep as you want.
386 If some command can't be dispatched, a CommandError subclass is raised.
389 def __init__(self, root):
390 r"""Initialize the Dispatcher object.
392 See Dispatcher class documentation for more info.
396 def dispatch(self, route):
397 r"""dispatch(route) -> None :: Dispatch a command string.
399 This method searches for a suitable callable object in the routes
400 "tree" and call it, or raises a CommandError subclass if the command
403 route - *unicode* string with the command route.
406 (route, kwargs) = parse_command(route)
408 raise CommandNotSpecifiedError()
410 while not is_handler(handler):
412 if isinstance(handler, Handler):
413 raise CommandIsAHandlerError(command)
414 raise CommandNotFoundError(command)
415 command.append(route[0])
416 if not hasattr(handler, route[0].encode('utf-8')):
417 if isinstance(handler, Handler) and len(command) > 1:
418 raise CommandNotInHandlerError(command)
419 raise CommandNotFoundError(command)
420 handler = getattr(handler, route[0].encode('utf-8'))
422 return handler(*route, **kwargs)
425 if __name__ == '__main__':
427 @handler(u"test: Print all the arguments, return nothing")
428 def test_func(*args):
431 class TestClassSubHandler(Handler):
432 @handler(u"subcmd: Print all the arguments, return nothing")
433 def subcmd(self, *args):
434 print 'class.subclass.subcmd:', args
436 class TestClass(Handler):
437 @handler(u"cmd1: Print all the arguments, return nothing")
438 def cmd1(self, *args):
439 print 'class.cmd1:', args
440 @handler(u"cmd2: Print all the arguments, return nothing")
441 def cmd2(self, *args):
442 print 'class.cmd2:', args
443 subclass = TestClassSubHandler()
445 class RootHandler(Handler):
446 func = staticmethod(test_func)
449 d = Dispatcher(RootHandler())
451 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
452 print 'inst commands:', tuple(d.dispatch('inst commands'))
453 print 'inst help:', d.dispatch('inst help')
454 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
455 d.dispatch('inst cmd2 arg1 arg2')
456 print 'inst subclass help:', d.dispatch('inst subclass help')
457 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
460 except CommandNotSpecifiedError, e:
461 print 'Not found:', e
463 d.dispatch('sucutrule piquete culete')
464 except CommandNotFoundError, e:
465 print 'Not found:', e
467 d.dispatch('inst cmd3 arg1 arg2 arg3')
468 except CommandNotInHandlerError, e:
469 print 'Not found:', e
472 except CommandIsAHandlerError, e:
473 print 'Not found:', e
478 p = parse_command('hello world')
479 assert p == ([u'hello', u'world'], {}), p
480 p = parse_command('hello planet=earth')
481 assert p == ([u'hello'], {'planet': u'earth'}), p
482 p = parse_command('hello planet="third rock from the sun"')
483 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
484 p = parse_command(u' planet="third rock from the sun" hello ')
485 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
486 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
488 assert p == ([u'hi, hello', u'how are you'],
489 {'planet': u'third rock from the sun'}), p
490 p = parse_command(u'one two three "fourth number"=four')
491 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
492 p = parse_command(u'one two three "fourth number=four"')
493 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
494 p = parse_command(u'one two three fourth\=four')
495 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
496 p = parse_command(u'one two three fourth=four=five')
497 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
498 p = parse_command(ur'nice\nlong\n\ttext')
499 assert p == ([u'nice\nlong\n\ttext'], {}), p
500 p = parse_command('=hello')
501 assert p == ([u'=hello'], {}), p
502 p = parse_command(r'\thello')
503 assert p == ([u'\thello'], {}), p
504 p = parse_command(r'\N')
505 assert p == ([None], {}), p
506 p = parse_command(r'none=\N')
507 assert p == ([], {'none': None}), p
508 p = parse_command(r'\N=none')
509 assert p == ([], {'\\N': 'none'}), p
510 p = parse_command(r'Not\N')
511 assert p == ([u'Not\\N'], {}), p
512 p = parse_command(r'\None')
513 assert p == ([u'\\None'], {}), p
515 p = parse_command('hello=')
516 except ParseError, e:
519 assert False, p + ' should raised a ParseError'
521 p = parse_command('"hello')
522 except ParseError, e:
525 assert False, p + ' should raised a ParseError'