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.
12 __ALL__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
13 'Dispatcher', 'handler', 'is_handler', 'get_help')
15 class Error(RuntimeError):
16 r"""Error(command) -> Error instance :: Base dispatching exceptions class.
18 All exceptions raised by the Dispatcher inherits from this one, so you can
19 easily catch any dispatching exception.
21 command - is the command that raised the exception, expressed as a list of
22 paths (or subcommands).
25 def __init__(self, message):
26 r"Initialize the Error object. See class documentation for more info."
27 self.message = message
29 def __unicode__(self):
33 return unicode(self).encode('utf-8')
35 class HandlerError(Error):
36 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
38 All exceptions raised by the handlers should inherit from this one, so
39 dispatching errors could be separated from real programming errors (bugs).
43 class CommandError(Error):
44 r"""CommandError(command) -> CommandError instance :: Base command error.
46 This exception is raised when there's a problem with the command itself.
47 It's the base class for all command (as a string) related error.
50 def __init__(self, command):
51 r"Initialize the object, see class documentation for more info."
52 self.command = command
54 def __unicode__(self):
55 return u'Error in command "%s".' % u' '.join(self.command)
57 class WrongArgumentsError(CommandError):
58 r"""WrongArgumentsError() -> WrongArgumentsError instance.
60 This exception is raised when an empty command string is received.
63 def __init__(self, message):
64 r"Initialize the object, see class documentation for more info."
65 self.message = message
67 def __unicode__(self):
70 class CommandNotSpecifiedError(CommandError):
71 r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
73 This exception is raised when an empty command string is received.
77 r"Initialize the object, see class documentation for more info."
80 def __unicode__(self):
81 return u'Command not specified.'
83 class CommandIsAHandlerError(CommandError):
84 r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
86 This exception is raised when a command is a handler containing commands
87 instead of a command itself.
90 def __unicode__(self):
91 command = ' '.join(self.command)
92 return u'"%s" is a handler, not a command (type "%s help" for help).' \
95 class CommandNotInHandlerError(CommandError):
96 r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
98 This exception is raised when a command parent is a hanlder containing
99 commands, but the command itself is not found.
102 def __unicode__(self):
103 return u'Command "%(c)s" not found in handler "%(h)s" ' \
104 u'(type "%(h)s help" for help).' \
105 % dict(c=u' '.join(self.command[-1:]),
106 h=u' '.join(self.command[0:-1]))
108 class CommandNotFoundError(CommandError):
109 r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
111 This exception is raised when the command received can't be dispatched
112 because there is no handlers to process it.
115 def __unicode__(self):
116 return u'Command "%s" not found.' % u' '.join(self.command)
118 class ParseError(CommandError):
119 r"""ParseError(command[, desc]) -> ParseError instance
121 This exception is raised when there is an error parsing a command.
123 command - Command that can't be parsed.
125 desc - Description of the error.
128 def __init__(self, command, desc="can't parse"):
129 r"""Initialize the object.
131 See class documentation for more info.
133 self.command = command
136 def __unicode__(self):
137 return u'Syntax error, %s: %s' % (self.desc, self.command)
139 class HelpNotFoundError(Error):
140 r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
142 This exception is raised when a help command can't find the command
146 def __init__(self, command):
147 r"""Initialize the object.
149 See class documentation for more info.
151 self.command = command
153 def __unicode__(self):
154 return u"Can't get help for '%s', command not found." % self.command
158 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
160 This is a decorator to mark a callable object as a dispatcher handler.
162 help - Help string for the handler.
166 raise TypeError("'help' should not be empty")
167 f._dispatcher_handler = True
168 f.handler_help = help
172 def is_handler(handler):
173 r"is_handler(handler) -> bool :: Tell if a object is a handler."
174 return callable(handler) and hasattr(handler, '_dispatcher_handler') \
175 and handler._dispatcher_handler
178 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
180 All dispatcher handlers should inherit from this class to have some extra
181 commands, like help. You should override the 'handler_help' attribute to a
182 nice help message describing the handler.
185 handler_help = u'Undocumented handler'
187 @handler(u'List available commands')
189 r"""commands() -> generator :: List the available commands."""
190 return (a for a in dir(self) if is_handler(getattr(self, a)))
192 @handler(u'Show available commands with their help')
193 def help(self, command=None):
194 r"""help([command]) -> unicode/dict :: Show help on available commands.
196 If command is specified, it returns the help of that particular command.
197 If not, it returns a dictionary which keys are the available commands
198 and values are the help strings.
204 if is_handler(h) or isinstance(h, Handler):
205 d[a] = h.handler_help
207 # A command was specified
208 if not hasattr(self, command.encode('utf-8')):
209 raise HelpNotFoundError(command)
210 handler = getattr(self, command.encode('utf-8'))
211 if not is_handler(handler) and not hasattr(handler):
212 raise HelpNotFoundError(command)
213 return handler.handler_help
215 def parse_command(command):
216 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
218 This function parses a command and split it into a list of parameters. It
219 has a similar to bash commandline parser. Spaces are the basic token
220 separator but you can group several tokens into one by using (single or
221 double) quotes. You can escape the quotes with a backslash (\' and \"),
222 express a backslash literal using a double backslash (\\), use special
223 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
224 single quotes inside a double quoted token or vice-versa. A special escape
225 sequence is provided to express a NULL/None value: \N and it should appear
226 always as a separated token.
228 Additionally it accepts keyword arguments. When an (not-escaped) equal
229 sign (=) is found, the argument is considered a keyword, and the next
230 argument it's interpreted as its value.
232 This function returns a tuple containing a list and a dictionary. The
233 first has the positional arguments, the second, the keyword arguments.
235 There is no restriction about the order, a keyword argument can be
236 followed by a positional argument and vice-versa. All type of arguments
237 are grouped in the list/dict returned. The order of the positional
238 arguments is preserved and if there are multiple keyword arguments with
239 the same key, the last value is the winner (all other values are lost).
241 The command should be a unicode string.
245 >>> parse_command('hello world')
246 ([u'hello', u'world'], {})
247 >>> parse_command('hello planet=earth')
248 ([u'hello'], {'planet': u'earth'})
249 >>> parse_command('hello planet="third rock from the sun"')
250 ([u'hello'], {'planet': u'third rock from the sun'})
251 >>> parse_command(u' planet="third rock from the sun" hello ')
252 ([u'hello'], {'planet': u'third rock from the sun'})
253 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
255 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
256 >>> parse_command(u'one two three "fourth number"=four')
257 ([u'one', u'two', u'three'], {'fourth number': u'four'})
258 >>> parse_command(u'one two three "fourth number=four"')
259 ([u'one', u'two', u'three', u'fourth number=four'], {})
260 >>> parse_command(u'one two three fourth\=four')
261 ([u'one', u'two', u'three', u'fourth=four'], {})
262 >>> parse_command(u'one two three fourth=four=five')
263 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
264 >>> parse_command(ur'nice\nlong\n\ttext')
265 ([u'nice\nlong\n\ttext'], {})
266 >>> parse_command('=hello')
268 >>> parse_command(r'\thello')
270 >>> parse_command(r'hello \n')
271 ([u'hello', u'\n'], {})
272 >>> parse_command(r'hello \nmundo')
273 ([u'hello', u'\nmundo'], {})
274 >>> parse_command(r'test \N')
275 ([u'test', None], {})
276 >>> parse_command(r'\N')
278 >>> parse_command(r'none=\N')
280 >>> parse_command(r'\N=none')
281 ([], {'\\N': 'none'})
282 >>> parse_command(r'Not\N')
284 >>> parse_command(r'\None')
287 This examples are syntax errors:
288 Missing quote: "hello world
289 Missing value: hello=
291 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
292 separators = (u' ', u'\t', u'\v', u'\n') # token separators
293 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
300 def register_token(buff, keyword, seq, dic):
303 if keyword is not None:
304 dic[keyword.encode('utf-8')] = buff
309 return (buff, keyword)
310 for n, c in enumerate(command):
313 # Not yet registered the token
314 if state == SEP and buff:
315 (buff, keyword) = register_token(buff, keyword, seq, dic)
317 for e in escaped_chars:
319 buff += eval(u'"\\' + e + u'"')
328 # Escaped sequence start
336 if buff and n != 2: # Not the first item (even if was a escape seq)
337 if c == EQUAL: # Keyword found
341 (buff, keyword) = register_token(buff, keyword, seq, dic)
351 # Check if a keyword is added
352 if c == EQUAL and keyword is None and buff:
362 # Inside a double quote
369 # Inside a single quote
376 assert 0, u'Unexpected state'
377 if state == DQUOTE or state == SQUOTE:
378 raise ParseError(command, u'missing closing quote (%s)' % state)
379 if not buff and keyword is not None:
380 raise ParseError(command,
381 u'keyword argument (%s) without value' % keyword)
383 register_token(buff, keyword, seq, dic)
386 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
389 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
391 This class provides a modular and extensible dispatching mechanism. You
392 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
394 The command can have arguments, separated by (any number of) spaces and
395 keyword arguments (see parse_command for more details).
397 The dispatcher tries to route the command as deeply as it can, passing
398 the other "path" components as arguments to the callable. To route the
399 command it inspects the callable attributes to find a suitable callable
400 attribute to handle the command in a more specific way, and so on.
403 >>> d = Dispatcher(dict(handler=some_handler))
404 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
406 If 'some_handler' is an object with an 'attribute' that is another
407 object which has a method named 'method', then
408 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
409 If some_handler is a function, then some_handler('attribute', 'method',
410 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
411 complex and deep as you want.
413 If some command can't be dispatched, a CommandError subclass is raised.
416 def __init__(self, root):
417 r"""Initialize the Dispatcher object.
419 See Dispatcher class documentation for more info.
423 def dispatch(self, route):
424 r"""dispatch(route) -> None :: Dispatch a command string.
426 This method searches for a suitable callable object in the routes
427 "tree" and call it, or raises a CommandError subclass if the command
430 route - *unicode* string with the command route.
433 (route, kwargs) = parse_command(route)
435 raise CommandNotSpecifiedError()
437 while not is_handler(handler):
439 if isinstance(handler, Handler):
440 raise CommandIsAHandlerError(command)
441 raise CommandNotFoundError(command)
442 command.append(route[0])
443 if not hasattr(handler, route[0].encode('utf-8')):
444 if isinstance(handler, Handler) and len(command) > 1:
445 raise CommandNotInHandlerError(command)
446 raise CommandNotFoundError(command)
447 handler = getattr(handler, route[0].encode('utf-8'))
450 return handler(*route, **kwargs)
452 m = args_re.match(unicode(e))
454 (quant, n_ok, n_bad) = m.groups()
462 raise WrongArgumentsError(
463 u'Command "%s" takes %s %s argument%s, %s given.'
464 % (handler.__name__, quant, n_ok, pl, n_bad))
468 if __name__ == '__main__':
470 @handler(u"test: Print all the arguments, return nothing")
471 def test_func(*args):
474 class TestClassSubHandler(Handler):
475 @handler(u"subcmd: Print all the arguments, return nothing")
476 def subcmd(self, *args):
477 print 'class.subclass.subcmd:', args
479 class TestClass(Handler):
480 @handler(u"cmd1: Print all the arguments, return nothing")
481 def cmd1(self, *args):
482 print 'class.cmd1:', args
483 @handler(u"cmd2: Print all the arguments, return nothing")
484 def cmd2(self, *args):
485 print 'class.cmd2:', args
486 subclass = TestClassSubHandler()
488 class RootHandler(Handler):
489 func = staticmethod(test_func)
492 d = Dispatcher(RootHandler())
494 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
495 print 'inst commands:', tuple(d.dispatch('inst commands'))
496 print 'inst help:', d.dispatch('inst help')
497 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
498 d.dispatch('inst cmd2 arg1 arg2')
499 print 'inst subclass help:', d.dispatch('inst subclass help')
500 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
503 except CommandNotSpecifiedError, e:
504 print 'Not found:', e
506 d.dispatch('sucutrule piquete culete')
507 except CommandNotFoundError, e:
508 print 'Not found:', e
510 d.dispatch('inst cmd3 arg1 arg2 arg3')
511 except CommandNotInHandlerError, e:
512 print 'Not found:', e
515 except CommandIsAHandlerError, e:
516 print 'Not found:', e
521 p = parse_command('hello world')
522 assert p == ([u'hello', u'world'], {}), p
523 p = parse_command('hello planet=earth')
524 assert p == ([u'hello'], {'planet': u'earth'}), p
525 p = parse_command('hello planet="third rock from the sun"')
526 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
527 p = parse_command(u' planet="third rock from the sun" hello ')
528 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
529 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
531 assert p == ([u'hi, hello', u'how are you'],
532 {'planet': u'third rock from the sun'}), p
533 p = parse_command(u'one two three "fourth number"=four')
534 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
535 p = parse_command(u'one two three "fourth number=four"')
536 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
537 p = parse_command(u'one two three fourth\=four')
538 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
539 p = parse_command(u'one two three fourth=four=five')
540 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
541 p = parse_command(ur'nice\nlong\n\ttext')
542 assert p == ([u'nice\nlong\n\ttext'], {}), p
543 p = parse_command('=hello')
544 assert p == ([u'=hello'], {}), p
545 p = parse_command(r'\thello')
546 assert p == ([u'\thello'], {}), p
547 p = parse_command(r'hello \n')
548 assert p == ([u'hello', u'\n'], {}), p
549 p = parse_command(r'hello \nmundo')
550 assert p == ([u'hello', u'\nmundo'], {}), p
551 p = parse_command(r'test \N')
552 assert p == ([u'test', None], {}), p
553 p = parse_command(r'\N')
554 assert p == ([None], {}), p
555 p = parse_command(r'none=\N')
556 assert p == ([], {'none': None}), p
557 p = parse_command(r'\N=none')
558 assert p == ([], {'\\N': 'none'}), p
559 p = parse_command(r'Not\N')
560 assert p == ([u'Not\\N'], {}), p
561 p = parse_command(r'\None')
562 assert p == ([u'\\None'], {}), p
564 p = parse_command('hello=')
565 except ParseError, e:
568 assert False, p + ' should raised a ParseError'
570 p = parse_command('"hello')
571 except ParseError, e:
574 assert False, p + ' should raised a ParseError'