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\)')
387 kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
390 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
392 This class provides a modular and extensible dispatching mechanism. You
393 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
395 The command can have arguments, separated by (any number of) spaces and
396 keyword arguments (see parse_command for more details).
398 The dispatcher tries to route the command as deeply as it can, passing
399 the other "path" components as arguments to the callable. To route the
400 command it inspects the callable attributes to find a suitable callable
401 attribute to handle the command in a more specific way, and so on.
404 >>> d = Dispatcher(dict(handler=some_handler))
405 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
407 If 'some_handler' is an object with an 'attribute' that is another
408 object which has a method named 'method', then
409 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
410 If some_handler is a function, then some_handler('attribute', 'method',
411 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
412 complex and deep as you want.
414 If some command can't be dispatched, a CommandError subclass is raised.
417 def __init__(self, root):
418 r"""Initialize the Dispatcher object.
420 See Dispatcher class documentation for more info.
424 def dispatch(self, route):
425 r"""dispatch(route) -> None :: Dispatch a command string.
427 This method searches for a suitable callable object in the routes
428 "tree" and call it, or raises a CommandError subclass if the command
431 route - *unicode* string with the command route.
434 (route, kwargs) = parse_command(route)
436 raise CommandNotSpecifiedError()
438 while not is_handler(handler):
440 if isinstance(handler, Handler):
441 raise CommandIsAHandlerError(command)
442 raise CommandNotFoundError(command)
443 command.append(route[0])
444 if not hasattr(handler, route[0].encode('utf-8')):
445 if isinstance(handler, Handler) and len(command) > 1:
446 raise CommandNotInHandlerError(command)
447 raise CommandNotFoundError(command)
448 handler = getattr(handler, route[0].encode('utf-8'))
451 return handler(*route, **kwargs)
453 m = args_re.match(unicode(e))
455 (quant, n_ok, n_bad) = m.groups()
463 raise WrongArgumentsError(
464 u'Command "%s" takes %s %s argument%s, %s given.'
465 % (handler.__name__, quant, n_ok, pl, n_bad))
466 m = kw_re.match(unicode(e))
469 raise WrongArgumentsError(
470 u'Command "%s" got an unexpected keyword argument %s.'
471 % (handler.__name__, kw))
475 if __name__ == '__main__':
477 @handler(u"test: Print all the arguments, return nothing")
478 def test_func(*args):
481 class TestClassSubHandler(Handler):
482 @handler(u"subcmd: Print all the arguments, return nothing")
483 def subcmd(self, *args):
484 print 'class.subclass.subcmd:', args
486 class TestClass(Handler):
487 @handler(u"cmd1: Print all the arguments, return nothing")
488 def cmd1(self, *args):
489 print 'class.cmd1:', args
490 @handler(u"cmd2: Print all the arguments, return nothing")
491 def cmd2(self, *args):
492 print 'class.cmd2:', args
493 subclass = TestClassSubHandler()
495 class RootHandler(Handler):
496 func = staticmethod(test_func)
499 d = Dispatcher(RootHandler())
501 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
502 print 'inst commands:', tuple(d.dispatch('inst commands'))
503 print 'inst help:', d.dispatch('inst help')
504 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
505 d.dispatch('inst cmd2 arg1 arg2')
506 print 'inst subclass help:', d.dispatch('inst subclass help')
507 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
510 except CommandNotSpecifiedError, e:
511 print 'Not found:', e
513 d.dispatch('sucutrule piquete culete')
514 except CommandNotFoundError, e:
515 print 'Not found:', e
517 d.dispatch('inst cmd3 arg1 arg2 arg3')
518 except CommandNotInHandlerError, e:
519 print 'Not found:', e
522 except CommandIsAHandlerError, e:
523 print 'Not found:', e
528 p = parse_command('hello world')
529 assert p == ([u'hello', u'world'], {}), p
530 p = parse_command('hello planet=earth')
531 assert p == ([u'hello'], {'planet': u'earth'}), p
532 p = parse_command('hello planet="third rock from the sun"')
533 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
534 p = parse_command(u' planet="third rock from the sun" hello ')
535 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
536 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
538 assert p == ([u'hi, hello', u'how are you'],
539 {'planet': u'third rock from the sun'}), p
540 p = parse_command(u'one two three "fourth number"=four')
541 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
542 p = parse_command(u'one two three "fourth number=four"')
543 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
544 p = parse_command(u'one two three fourth\=four')
545 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
546 p = parse_command(u'one two three fourth=four=five')
547 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
548 p = parse_command(ur'nice\nlong\n\ttext')
549 assert p == ([u'nice\nlong\n\ttext'], {}), p
550 p = parse_command('=hello')
551 assert p == ([u'=hello'], {}), p
552 p = parse_command(r'\thello')
553 assert p == ([u'\thello'], {}), p
554 p = parse_command(r'hello \n')
555 assert p == ([u'hello', u'\n'], {}), p
556 p = parse_command(r'hello \nmundo')
557 assert p == ([u'hello', u'\nmundo'], {}), p
558 p = parse_command(r'test \N')
559 assert p == ([u'test', None], {}), p
560 p = parse_command(r'\N')
561 assert p == ([None], {}), p
562 p = parse_command(r'none=\N')
563 assert p == ([], {'none': None}), p
564 p = parse_command(r'\N=none')
565 assert p == ([], {'\\N': 'none'}), p
566 p = parse_command(r'Not\N')
567 assert p == ([u'Not\\N'], {}), p
568 p = parse_command(r'\None')
569 assert p == ([u'\\None'], {}), p
571 p = parse_command('hello=')
572 except ParseError, e:
575 assert False, p + ' should raised a ParseError'
577 p = parse_command('"hello')
578 except ParseError, e:
581 assert False, p + ' should raised a ParseError'