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(handler, message) -> WrongArgumentsError instance.
60 This exception is raised when an empty command string is received.
63 def __init__(self, handler, message):
64 r"Initialize the object, see class documentation for more info."
65 self.handler = handler
66 self.message = message
68 def __unicode__(self):
69 return u'Command "%s" %s.' % (self.handler.__name__, self.message)
71 class CommandNotSpecifiedError(CommandError):
72 r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
74 This exception is raised when an empty command string is received.
78 r"Initialize the object, see class documentation for more info."
81 def __unicode__(self):
82 return u'Command not specified.'
84 class CommandIsAHandlerError(CommandError):
85 r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
87 This exception is raised when a command is a handler containing commands
88 instead of a command itself.
91 def __unicode__(self):
92 command = ' '.join(self.command)
93 return u'"%s" is a handler, not a command (type "%s help" for help).' \
96 class CommandNotInHandlerError(CommandError):
97 r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
99 This exception is raised when a command parent is a hanlder containing
100 commands, but the command itself is not found.
103 def __unicode__(self):
104 return u'Command "%(c)s" not found in handler "%(h)s" ' \
105 u'(type "%(h)s help" for help).' \
106 % dict(c=u' '.join(self.command[-1:]),
107 h=u' '.join(self.command[0:-1]))
109 class CommandNotFoundError(CommandError):
110 r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
112 This exception is raised when the command received can't be dispatched
113 because there is no handlers to process it.
116 def __unicode__(self):
117 return u'Command "%s" not found.' % u' '.join(self.command)
119 class ParseError(CommandError):
120 r"""ParseError(command[, desc]) -> ParseError instance
122 This exception is raised when there is an error parsing a command.
124 command - Command that can't be parsed.
126 desc - Description of the error.
129 def __init__(self, command, desc="can't parse"):
130 r"""Initialize the object.
132 See class documentation for more info.
134 self.command = command
137 def __unicode__(self):
138 return u'Syntax error, %s: %s' % (self.desc, self.command)
140 class HelpNotFoundError(Error):
141 r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
143 This exception is raised when a help command can't find the command
147 def __init__(self, command):
148 r"""Initialize the object.
150 See class documentation for more info.
152 self.command = command
154 def __unicode__(self):
155 return u"Can't get help for '%s', command not found." % self.command
159 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
161 This is a decorator to mark a callable object as a dispatcher handler.
163 help - Help string for the handler.
167 raise TypeError("'help' should not be empty")
168 f._dispatcher_handler = True
169 f.handler_help = help
173 def is_handler(handler):
174 r"is_handler(handler) -> bool :: Tell if a object is a handler."
175 return callable(handler) and hasattr(handler, '_dispatcher_handler') \
176 and handler._dispatcher_handler
179 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
181 All dispatcher handlers should inherit from this class to have some extra
182 commands, like help. You should override the 'handler_help' attribute to a
183 nice help message describing the handler.
186 handler_help = u'Undocumented handler'
188 @handler(u'List available commands')
190 r"""commands() -> generator :: List the available commands."""
191 return (a for a in dir(self) if is_handler(getattr(self, a)))
193 @handler(u'Show available commands with their help')
194 def help(self, command=None):
195 r"""help([command]) -> unicode/dict :: Show help on available commands.
197 If command is specified, it returns the help of that particular command.
198 If not, it returns a dictionary which keys are the available commands
199 and values are the help strings.
205 if a == 'parent': continue # Skip parents in SubHandlers
206 if is_handler(h) or isinstance(h, Handler):
207 d[a] = h.handler_help
209 # A command was specified
210 if command == 'parent': # Skip parents in SubHandlers
211 raise HelpNotFoundError(command)
212 if not hasattr(self, command.encode('utf-8')):
213 raise HelpNotFoundError(command)
214 handler = getattr(self, command.encode('utf-8'))
215 if not is_handler(handler) and not hasattr(handler, 'handler_help'):
216 raise HelpNotFoundError(command)
217 return handler.handler_help
219 def handle_timer(self):
220 r"""handle_timer() -> None :: Do periodic tasks.
222 By default we do nothing but calling handle_timer() on subhandlers.
225 if a == 'parent': continue # Skip parents in SubHandlers
227 if isinstance(h, Handler):
230 def parse_command(command):
231 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
233 This function parses a command and split it into a list of parameters. It
234 has a similar to bash commandline parser. Spaces are the basic token
235 separator but you can group several tokens into one by using (single or
236 double) quotes. You can escape the quotes with a backslash (\' and \"),
237 express a backslash literal using a double backslash (\\), use special
238 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
239 single quotes inside a double quoted token or vice-versa. A special escape
240 sequence is provided to express a NULL/None value: \N and it should appear
241 always as a separated token.
243 Additionally it accepts keyword arguments. When an (not-escaped) equal
244 sign (=) is found, the argument is considered a keyword, and the next
245 argument it's interpreted as its value.
247 This function returns a tuple containing a list and a dictionary. The
248 first has the positional arguments, the second, the keyword arguments.
250 There is no restriction about the order, a keyword argument can be
251 followed by a positional argument and vice-versa. All type of arguments
252 are grouped in the list/dict returned. The order of the positional
253 arguments is preserved and if there are multiple keyword arguments with
254 the same key, the last value is the winner (all other values are lost).
256 The command should be a unicode string.
260 >>> parse_command('hello world')
261 ([u'hello', u'world'], {})
262 >>> parse_command('hello planet=earth')
263 ([u'hello'], {'planet': u'earth'})
264 >>> parse_command('hello planet="third rock from the sun"')
265 ([u'hello'], {'planet': u'third rock from the sun'})
266 >>> parse_command(u' planet="third rock from the sun" hello ')
267 ([u'hello'], {'planet': u'third rock from the sun'})
268 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
270 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
271 >>> parse_command(u'one two three "fourth number"=four')
272 ([u'one', u'two', u'three'], {'fourth number': u'four'})
273 >>> parse_command(u'one two three "fourth number=four"')
274 ([u'one', u'two', u'three', u'fourth number=four'], {})
275 >>> parse_command(u'one two three fourth\=four')
276 ([u'one', u'two', u'three', u'fourth=four'], {})
277 >>> parse_command(u'one two three fourth=four=five')
278 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
279 >>> parse_command(ur'nice\nlong\n\ttext')
280 ([u'nice\nlong\n\ttext'], {})
281 >>> parse_command('=hello')
283 >>> parse_command(r'\thello')
285 >>> parse_command(r'hello \n')
286 ([u'hello', u'\n'], {})
287 >>> parse_command(r'hello \nmundo')
288 ([u'hello', u'\nmundo'], {})
289 >>> parse_command(r'test \N')
290 ([u'test', None], {})
291 >>> parse_command(r'\N')
293 >>> parse_command(r'none=\N')
295 >>> parse_command(r'\N=none')
296 ([], {'\\N': 'none'})
297 >>> parse_command(r'Not\N')
299 >>> parse_command(r'\None')
302 This examples are syntax errors:
303 Missing quote: "hello world
304 Missing value: hello=
306 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
307 separators = (u' ', u'\t', u'\v', u'\n') # token separators
308 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
315 def register_token(buff, keyword, seq, dic):
318 if keyword is not None:
319 dic[keyword.encode('utf-8')] = buff
324 return (buff, keyword)
325 for n, c in enumerate(command):
328 # Not yet registered the token
329 if state == SEP and buff:
330 (buff, keyword) = register_token(buff, keyword, seq, dic)
332 for e in escaped_chars:
334 buff += eval(u'"\\' + e + u'"')
343 # Escaped sequence start
351 if buff and n != 2: # Not the first item (even if was a escape seq)
352 if c == EQUAL: # Keyword found
356 (buff, keyword) = register_token(buff, keyword, seq, dic)
366 # Check if a keyword is added
367 if c == EQUAL and keyword is None and buff:
377 # Inside a double quote
384 # Inside a single quote
391 assert 0, u'Unexpected state'
392 if state == DQUOTE or state == SQUOTE:
393 raise ParseError(command, u'missing closing quote (%s)' % state)
394 if not buff and keyword is not None:
395 raise ParseError(command,
396 u'keyword argument (%s) without value' % keyword)
398 register_token(buff, keyword, seq, dic)
401 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
402 kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
405 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
407 This class provides a modular and extensible dispatching mechanism. You
408 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
410 The command can have arguments, separated by (any number of) spaces and
411 keyword arguments (see parse_command for more details).
413 The dispatcher tries to route the command as deeply as it can, passing
414 the other "path" components as arguments to the callable. To route the
415 command it inspects the callable attributes to find a suitable callable
416 attribute to handle the command in a more specific way, and so on.
419 >>> d = Dispatcher(dict(handler=some_handler))
420 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
422 If 'some_handler' is an object with an 'attribute' that is another
423 object which has a method named 'method', then
424 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
425 If some_handler is a function, then some_handler('attribute', 'method',
426 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
427 complex and deep as you want.
429 If some command can't be dispatched, a CommandError subclass is raised.
432 def __init__(self, root):
433 r"""Initialize the Dispatcher object.
435 See Dispatcher class documentation for more info.
439 def dispatch(self, route):
440 r"""dispatch(route) -> None :: Dispatch a command string.
442 This method searches for a suitable callable object in the routes
443 "tree" and call it, or raises a CommandError subclass if the command
446 route - *unicode* string with the command route.
449 (route, kwargs) = parse_command(route)
451 raise CommandNotSpecifiedError()
453 while not is_handler(handler):
455 if isinstance(handler, Handler):
456 raise CommandIsAHandlerError(command)
457 raise CommandNotFoundError(command)
458 command.append(route[0])
459 if route[0] == 'parent':
460 raise CommandNotFoundError(command)
461 if not hasattr(handler, route[0].encode('utf-8')):
462 if isinstance(handler, Handler) and len(command) > 1:
463 raise CommandNotInHandlerError(command)
464 raise CommandNotFoundError(command)
465 handler = getattr(handler, route[0].encode('utf-8'))
468 return handler(*route, **kwargs)
470 m = args_re.match(unicode(e))
472 (quant, n_ok, n_bad) = m.groups()
480 raise WrongArgumentsError(handler, u'takes %s %s argument%s, '
481 '%s given' % (quant, n_ok, pl, n_bad))
482 m = kw_re.match(unicode(e))
485 raise WrongArgumentsError(handler,
486 u'got an unexpected keyword argument %s' % kw)
490 if __name__ == '__main__':
492 @handler(u"test: Print all the arguments, return nothing")
493 def test_func(*args):
496 class TestClassSubHandler(Handler):
497 @handler(u"subcmd: Print all the arguments, return nothing")
498 def subcmd(self, *args):
499 print 'class.subclass.subcmd:', args
501 class TestClass(Handler):
502 @handler(u"cmd1: Print all the arguments, return nothing")
503 def cmd1(self, *args):
504 print 'class.cmd1:', args
505 @handler(u"cmd2: Print all the arguments, return nothing")
506 def cmd2(self, *args):
507 print 'class.cmd2:', args
508 subclass = TestClassSubHandler()
510 class RootHandler(Handler):
511 func = staticmethod(test_func)
514 d = Dispatcher(RootHandler())
516 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
517 print 'inst commands:', tuple(d.dispatch('inst commands'))
518 print 'inst help:', d.dispatch('inst help')
519 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
520 d.dispatch('inst cmd2 arg1 arg2')
521 print 'inst subclass help:', d.dispatch('inst subclass help')
522 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
525 except CommandNotSpecifiedError, e:
526 print 'Not found:', e
528 d.dispatch('sucutrule piquete culete')
529 except CommandNotFoundError, e:
530 print 'Not found:', e
532 d.dispatch('inst cmd3 arg1 arg2 arg3')
533 except CommandNotInHandlerError, e:
534 print 'Not found:', e
537 except CommandIsAHandlerError, e:
538 print 'Not found:', e
543 p = parse_command('hello world')
544 assert p == ([u'hello', u'world'], {}), p
545 p = parse_command('hello planet=earth')
546 assert p == ([u'hello'], {'planet': u'earth'}), p
547 p = parse_command('hello planet="third rock from the sun"')
548 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
549 p = parse_command(u' planet="third rock from the sun" hello ')
550 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
551 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
553 assert p == ([u'hi, hello', u'how are you'],
554 {'planet': u'third rock from the sun'}), p
555 p = parse_command(u'one two three "fourth number"=four')
556 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
557 p = parse_command(u'one two three "fourth number=four"')
558 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
559 p = parse_command(u'one two three fourth\=four')
560 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
561 p = parse_command(u'one two three fourth=four=five')
562 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
563 p = parse_command(ur'nice\nlong\n\ttext')
564 assert p == ([u'nice\nlong\n\ttext'], {}), p
565 p = parse_command('=hello')
566 assert p == ([u'=hello'], {}), p
567 p = parse_command(r'\thello')
568 assert p == ([u'\thello'], {}), p
569 p = parse_command(r'hello \n')
570 assert p == ([u'hello', u'\n'], {}), p
571 p = parse_command(r'hello \nmundo')
572 assert p == ([u'hello', u'\nmundo'], {}), p
573 p = parse_command(r'test \N')
574 assert p == ([u'test', None], {}), p
575 p = parse_command(r'\N')
576 assert p == ([None], {}), p
577 p = parse_command(r'none=\N')
578 assert p == ([], {'none': None}), p
579 p = parse_command(r'\N=none')
580 assert p == ([], {'\\N': 'none'}), p
581 p = parse_command(r'Not\N')
582 assert p == ([u'Not\\N'], {}), p
583 p = parse_command(r'\None')
584 assert p == ([u'\\None'], {}), p
586 p = parse_command('hello=')
587 except ParseError, e:
590 assert False, p + ' should raised a ParseError'
592 p = parse_command('"hello')
593 except ParseError, e:
596 assert False, p + ' should raised a ParseError'