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.
11 import logging ; log = logging.getLogger('pymin.dispatcher')
13 __ALL__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
14 'Dispatcher', 'handler', 'is_handler', 'get_help')
16 class Error(RuntimeError):
17 r"""Error(command) -> Error instance :: Base dispatching exceptions class.
19 All exceptions raised by the Dispatcher inherits from this one, so you can
20 easily catch any dispatching exception.
22 command - is the command that raised the exception, expressed as a list of
23 paths (or subcommands).
26 def __init__(self, message):
27 r"Initialize the Error object. See class documentation for more info."
28 self.message = message
30 def __unicode__(self):
34 return unicode(self).encode('utf-8')
36 class HandlerError(Error):
37 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
39 All exceptions raised by the handlers should inherit from this one, so
40 dispatching errors could be separated from real programming errors (bugs).
44 class CommandError(Error):
45 r"""CommandError(command) -> CommandError instance :: Base command error.
47 This exception is raised when there's a problem with the command itself.
48 It's the base class for all command (as a string) related error.
51 def __init__(self, command):
52 r"Initialize the object, see class documentation for more info."
53 self.command = command
55 def __unicode__(self):
56 return u'Error in command "%s".' % u' '.join(self.command)
58 class WrongArgumentsError(CommandError):
59 r"""WrongArgumentsError(handler, message) -> WrongArgumentsError instance.
61 This exception is raised when an empty command string is received.
64 def __init__(self, handler, message):
65 r"Initialize the object, see class documentation for more info."
66 self.handler = handler
67 self.message = message
69 def __unicode__(self):
70 return u'Command "%s" %s.' % (self.handler.__name__, self.message)
72 class CommandNotSpecifiedError(CommandError):
73 r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
75 This exception is raised when an empty command string is received.
79 r"Initialize the object, see class documentation for more info."
82 def __unicode__(self):
83 return u'Command not specified.'
85 class CommandIsAHandlerError(CommandError):
86 r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
88 This exception is raised when a command is a handler containing commands
89 instead of a command itself.
92 def __unicode__(self):
93 command = ' '.join(self.command)
94 return u'"%s" is a handler, not a command (type "%s help" for help).' \
97 class CommandNotInHandlerError(CommandError):
98 r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
100 This exception is raised when a command parent is a hanlder containing
101 commands, but the command itself is not found.
104 def __unicode__(self):
105 return u'Command "%(c)s" not found in handler "%(h)s" ' \
106 u'(type "%(h)s help" for help).' \
107 % dict(c=u' '.join(self.command[-1:]),
108 h=u' '.join(self.command[0:-1]))
110 class CommandNotFoundError(CommandError):
111 r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
113 This exception is raised when the command received can't be dispatched
114 because there is no handlers to process it.
117 def __unicode__(self):
118 return u'Command "%s" not found.' % u' '.join(self.command)
120 class ParseError(CommandError):
121 r"""ParseError(command[, desc]) -> ParseError instance
123 This exception is raised when there is an error parsing a command.
125 command - Command that can't be parsed.
127 desc - Description of the error.
130 def __init__(self, command, desc="can't parse"):
131 r"""Initialize the object.
133 See class documentation for more info.
135 self.command = command
138 def __unicode__(self):
139 return u'Syntax error, %s: %s' % (self.desc, self.command)
141 class HelpNotFoundError(Error):
142 r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
144 This exception is raised when a help command can't find the command
148 def __init__(self, command):
149 r"""Initialize the object.
151 See class documentation for more info.
153 self.command = command
155 def __unicode__(self):
156 return u"Can't get help for '%s', command not found." % self.command
160 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
162 This is a decorator to mark a callable object as a dispatcher handler.
164 help - Help string for the handler.
168 raise TypeError("'help' should not be empty")
169 f._dispatcher_handler = True
170 f.handler_help = help
174 def is_handler(handler):
175 r"is_handler(handler) -> bool :: Tell if a object is a handler."
176 return callable(handler) and hasattr(handler, '_dispatcher_handler') \
177 and handler._dispatcher_handler
180 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
182 All dispatcher handlers should inherit from this class to have some extra
183 commands, like help. You should override the 'handler_help' attribute to a
184 nice help message describing the handler.
187 handler_help = u'Undocumented handler'
189 @handler(u'List available commands')
191 r"""commands() -> generator :: List the available commands."""
192 return (a for a in dir(self) if is_handler(getattr(self, a)))
194 @handler(u'Show available commands with their help')
195 def help(self, command=None):
196 r"""help([command]) -> unicode/dict :: Show help on available commands.
198 If command is specified, it returns the help of that particular command.
199 If not, it returns a dictionary which keys are the available commands
200 and values are the help strings.
206 if a == 'parent': continue # Skip parents in SubHandlers
207 if is_handler(h) or isinstance(h, Handler):
208 d[a] = h.handler_help
210 # A command was specified
211 if command == 'parent': # Skip parents in SubHandlers
212 raise HelpNotFoundError(command)
213 if not hasattr(self, command.encode('utf-8')):
214 raise HelpNotFoundError(command)
215 handler = getattr(self, command.encode('utf-8'))
216 if not is_handler(handler) and not hasattr(handler, 'handler_help'):
217 raise HelpNotFoundError(command)
218 return handler.handler_help
220 def handle_timer(self):
221 r"""handle_timer() -> None :: Do periodic tasks.
223 By default we do nothing but calling handle_timer() on subhandlers.
226 if a == 'parent': continue # Skip parents in SubHandlers
228 if isinstance(h, Handler):
231 def parse_command(command):
232 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
234 This function parses a command and split it into a list of parameters. It
235 has a similar to bash commandline parser. Spaces are the basic token
236 separator but you can group several tokens into one by using (single or
237 double) quotes. You can escape the quotes with a backslash (\' and \"),
238 express a backslash literal using a double backslash (\\), use special
239 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
240 single quotes inside a double quoted token or vice-versa. A special escape
241 sequence is provided to express a NULL/None value: \N and it should appear
242 always as a separated token.
244 Additionally it accepts keyword arguments. When an (not-escaped) equal
245 sign (=) is found, the argument is considered a keyword, and the next
246 argument it's interpreted as its value.
248 This function returns a tuple containing a list and a dictionary. The
249 first has the positional arguments, the second, the keyword arguments.
251 There is no restriction about the order, a keyword argument can be
252 followed by a positional argument and vice-versa. All type of arguments
253 are grouped in the list/dict returned. The order of the positional
254 arguments is preserved and if there are multiple keyword arguments with
255 the same key, the last value is the winner (all other values are lost).
257 The command should be a unicode string.
261 >>> parse_command('hello world')
262 ([u'hello', u'world'], {})
263 >>> parse_command('hello planet=earth')
264 ([u'hello'], {'planet': u'earth'})
265 >>> parse_command('hello planet="third rock from the sun"')
266 ([u'hello'], {'planet': u'third rock from the sun'})
267 >>> parse_command(u' planet="third rock from the sun" hello ')
268 ([u'hello'], {'planet': u'third rock from the sun'})
269 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
271 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
272 >>> parse_command(u'one two three "fourth number"=four')
273 ([u'one', u'two', u'three'], {'fourth number': u'four'})
274 >>> parse_command(u'one two three "fourth number=four"')
275 ([u'one', u'two', u'three', u'fourth number=four'], {})
276 >>> parse_command(u'one two three fourth\=four')
277 ([u'one', u'two', u'three', u'fourth=four'], {})
278 >>> parse_command(u'one two three fourth=four=five')
279 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
280 >>> parse_command(ur'nice\nlong\n\ttext')
281 ([u'nice\nlong\n\ttext'], {})
282 >>> parse_command('=hello')
284 >>> parse_command(r'\thello')
286 >>> parse_command(r'hello \n')
287 ([u'hello', u'\n'], {})
288 >>> parse_command(r'hello \nmundo')
289 ([u'hello', u'\nmundo'], {})
290 >>> parse_command(r'test \N')
291 ([u'test', None], {})
292 >>> parse_command(r'\N')
294 >>> parse_command(r'none=\N')
296 >>> parse_command(r'\N=none')
297 ([], {'\\N': 'none'})
298 >>> parse_command(r'Not\N')
300 >>> parse_command(r'\None')
303 This examples are syntax errors:
304 Missing quote: "hello world
305 Missing value: hello=
307 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
308 separators = (u' ', u'\t', u'\v', u'\n') # token separators
309 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
316 def register_token(buff, keyword, seq, dic):
319 if keyword is not None:
320 dic[keyword.encode('utf-8')] = buff
325 return (buff, keyword)
326 for n, c in enumerate(command):
329 # Not yet registered the token
330 if state == SEP and buff:
331 (buff, keyword) = register_token(buff, keyword, seq, dic)
333 for e in escaped_chars:
335 buff += eval(u'"\\' + e + u'"')
344 # Escaped sequence start
352 if buff and n != 2: # Not the first item (even if was a escape seq)
353 if c == EQUAL: # Keyword found
357 (buff, keyword) = register_token(buff, keyword, seq, dic)
367 # Check if a keyword is added
368 if c == EQUAL and keyword is None and buff:
378 # Inside a double quote
385 # Inside a single quote
392 assert 0, u'Unexpected state'
393 if state == DQUOTE or state == SQUOTE:
394 raise ParseError(command, u'missing closing quote (%s)' % state)
395 if not buff and keyword is not None:
396 raise ParseError(command,
397 u'keyword argument (%s) without value' % keyword)
399 register_token(buff, keyword, seq, dic)
402 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
403 kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
406 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
408 This class provides a modular and extensible dispatching mechanism. You
409 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
411 The command can have arguments, separated by (any number of) spaces and
412 keyword arguments (see parse_command for more details).
414 The dispatcher tries to route the command as deeply as it can, passing
415 the other "path" components as arguments to the callable. To route the
416 command it inspects the callable attributes to find a suitable callable
417 attribute to handle the command in a more specific way, and so on.
420 >>> d = Dispatcher(dict(handler=some_handler))
421 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
423 If 'some_handler' is an object with an 'attribute' that is another
424 object which has a method named 'method', then
425 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
426 If some_handler is a function, then some_handler('attribute', 'method',
427 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
428 complex and deep as you want.
430 If some command can't be dispatched, a CommandError subclass is raised.
433 def __init__(self, root):
434 r"""Initialize the Dispatcher object.
436 See Dispatcher class documentation for more info.
438 log.debug(u'Dispatcher(%r)', root)
441 def dispatch(self, route):
442 r"""dispatch(route) -> None :: Dispatch a command string.
444 This method searches for a suitable callable object in the routes
445 "tree" and call it, or raises a CommandError subclass if the command
448 route - *unicode* string with the command route.
450 log.debug('Dispatcher.dispatch(%r)', route)
452 (route, kwargs) = parse_command(route)
453 log.debug(u'Dispatcher.dispatch: route=%r, kwargs=%r', route, kwargs)
455 log.debug(u'Dispatcher.dispatch: command not specified')
456 raise CommandNotSpecifiedError()
458 while not is_handler(handler):
459 log.debug(u'Dispatcher.dispatch: handler=%r, route=%r',
462 if isinstance(handler, Handler):
463 log.debug(u'Dispatcher.dispatch: command is a handler')
464 raise CommandIsAHandlerError(command)
465 log.debug(u'Dispatcher.dispatch: command not found')
466 raise CommandNotFoundError(command)
467 command.append(route[0])
468 log.debug(u'Dispatcher.dispatch: command=%r', command)
469 if route[0] == 'parent':
470 log.debug(u'Dispatcher.dispatch: is parent => not found')
471 raise CommandNotFoundError(command)
472 if not hasattr(handler, route[0].encode('utf-8')):
473 if isinstance(handler, Handler) and len(command) > 1:
474 log.debug(u'Dispatcher.dispatch: command not in handler')
475 raise CommandNotInHandlerError(command)
476 log.debug(u'Dispatcher.dispatch: command not found')
477 raise CommandNotFoundError(command)
478 handler = getattr(handler, route[0].encode('utf-8'))
480 log.debug(u'Dispatcher.dispatch: %r is a handler, calling it with '
481 u'route=%r, kwargs=%r', handler, route, kwargs)
483 r = handler(*route, **kwargs)
484 log.debug(u'Dispatcher.dispatch: handler returned %s', r)
485 return handler(*route, **kwargs)
487 log.debug(u'Dispatcher.dispatch: type error (%r)', e)
488 m = args_re.match(unicode(e))
490 (quant, n_ok, n_bad) = m.groups()
498 e = WrongArgumentsError(handler, u'takes %s %s argument%s, '
499 '%s given' % (quant, n_ok, pl, n_bad))
500 log.debug(u'Dispatcher.dispatch: wrong arguments (%r)', e)
502 m = kw_re.match(unicode(e))
505 e = WrongArgumentsError(handler,
506 u'got an unexpected keyword argument %s' % kw)
507 log.debug(u'Dispatcher.dispatch: wrong arguments (%r)', e)
509 log.debug(u'Dispatcher.dispatch: some other TypeError, re-raising')
513 if __name__ == '__main__':
516 level = logging.DEBUG,
517 format = '%(asctime)s %(levelname)-8s %(message)s',
518 datefmt = '%H:%M:%S',
521 @handler(u"test: Print all the arguments, return nothing")
522 def test_func(*args):
525 class TestClassSubHandler(Handler):
526 @handler(u"subcmd: Print all the arguments, return nothing")
527 def subcmd(self, *args):
528 print 'class.subclass.subcmd:', args
530 class TestClass(Handler):
531 @handler(u"cmd1: Print all the arguments, return nothing")
532 def cmd1(self, *args):
533 print 'class.cmd1:', args
534 @handler(u"cmd2: Print all the arguments, return nothing")
535 def cmd2(self, *args):
536 print 'class.cmd2:', args
537 subclass = TestClassSubHandler()
539 class RootHandler(Handler):
540 func = staticmethod(test_func)
543 d = Dispatcher(RootHandler())
545 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
546 print 'inst commands:', tuple(d.dispatch('inst commands'))
547 print 'inst help:', d.dispatch('inst help')
548 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
549 d.dispatch('inst cmd2 arg1 arg2')
550 print 'inst subclass help:', d.dispatch('inst subclass help')
551 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
554 except CommandNotSpecifiedError, e:
555 print 'Not found:', e
557 d.dispatch('sucutrule piquete culete')
558 except CommandNotFoundError, e:
559 print 'Not found:', e
561 d.dispatch('inst cmd3 arg1 arg2 arg3')
562 except CommandNotInHandlerError, e:
563 print 'Not found:', e
566 except CommandIsAHandlerError, e:
567 print 'Not found:', e
572 p = parse_command('hello world')
573 assert p == ([u'hello', u'world'], {}), p
574 p = parse_command('hello planet=earth')
575 assert p == ([u'hello'], {'planet': u'earth'}), p
576 p = parse_command('hello planet="third rock from the sun"')
577 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
578 p = parse_command(u' planet="third rock from the sun" hello ')
579 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
580 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
582 assert p == ([u'hi, hello', u'how are you'],
583 {'planet': u'third rock from the sun'}), p
584 p = parse_command(u'one two three "fourth number"=four')
585 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
586 p = parse_command(u'one two three "fourth number=four"')
587 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
588 p = parse_command(u'one two three fourth\=four')
589 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
590 p = parse_command(u'one two three fourth=four=five')
591 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
592 p = parse_command(ur'nice\nlong\n\ttext')
593 assert p == ([u'nice\nlong\n\ttext'], {}), p
594 p = parse_command('=hello')
595 assert p == ([u'=hello'], {}), p
596 p = parse_command(r'\thello')
597 assert p == ([u'\thello'], {}), p
598 p = parse_command(r'hello \n')
599 assert p == ([u'hello', u'\n'], {}), p
600 p = parse_command(r'hello \nmundo')
601 assert p == ([u'hello', u'\nmundo'], {}), p
602 p = parse_command(r'test \N')
603 assert p == ([u'test', None], {}), p
604 p = parse_command(r'\N')
605 assert p == ([None], {}), p
606 p = parse_command(r'none=\N')
607 assert p == ([], {'none': None}), p
608 p = parse_command(r'\N=none')
609 assert p == ([], {'\\N': 'none'}), p
610 p = parse_command(r'Not\N')
611 assert p == ([u'Not\\N'], {}), p
612 p = parse_command(r'\None')
613 assert p == ([u'\\None'], {}), p
615 p = parse_command('hello=')
616 except ParseError, e:
619 assert False, p + ' should raised a ParseError'
621 p = parse_command('"hello')
622 except ParseError, e:
625 assert False, p + ' should raised a ParseError'