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 is_handler(h) or isinstance(h, Handler):
206 d[a] = h.handler_help
208 # A command was specified
209 if not hasattr(self, command.encode('utf-8')):
210 raise HelpNotFoundError(command)
211 handler = getattr(self, command.encode('utf-8'))
212 if not is_handler(handler) and not hasattr(handler):
213 raise HelpNotFoundError(command)
214 return handler.handler_help
216 def parse_command(command):
217 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
219 This function parses a command and split it into a list of parameters. It
220 has a similar to bash commandline parser. Spaces are the basic token
221 separator but you can group several tokens into one by using (single or
222 double) quotes. You can escape the quotes with a backslash (\' and \"),
223 express a backslash literal using a double backslash (\\), use special
224 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
225 single quotes inside a double quoted token or vice-versa. A special escape
226 sequence is provided to express a NULL/None value: \N and it should appear
227 always as a separated token.
229 Additionally it accepts keyword arguments. When an (not-escaped) equal
230 sign (=) is found, the argument is considered a keyword, and the next
231 argument it's interpreted as its value.
233 This function returns a tuple containing a list and a dictionary. The
234 first has the positional arguments, the second, the keyword arguments.
236 There is no restriction about the order, a keyword argument can be
237 followed by a positional argument and vice-versa. All type of arguments
238 are grouped in the list/dict returned. The order of the positional
239 arguments is preserved and if there are multiple keyword arguments with
240 the same key, the last value is the winner (all other values are lost).
242 The command should be a unicode string.
246 >>> parse_command('hello world')
247 ([u'hello', u'world'], {})
248 >>> parse_command('hello planet=earth')
249 ([u'hello'], {'planet': u'earth'})
250 >>> parse_command('hello planet="third rock from the sun"')
251 ([u'hello'], {'planet': u'third rock from the sun'})
252 >>> parse_command(u' planet="third rock from the sun" hello ')
253 ([u'hello'], {'planet': u'third rock from the sun'})
254 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
256 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
257 >>> parse_command(u'one two three "fourth number"=four')
258 ([u'one', u'two', u'three'], {'fourth number': u'four'})
259 >>> parse_command(u'one two three "fourth number=four"')
260 ([u'one', u'two', u'three', u'fourth number=four'], {})
261 >>> parse_command(u'one two three fourth\=four')
262 ([u'one', u'two', u'three', u'fourth=four'], {})
263 >>> parse_command(u'one two three fourth=four=five')
264 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
265 >>> parse_command(ur'nice\nlong\n\ttext')
266 ([u'nice\nlong\n\ttext'], {})
267 >>> parse_command('=hello')
269 >>> parse_command(r'\thello')
271 >>> parse_command(r'hello \n')
272 ([u'hello', u'\n'], {})
273 >>> parse_command(r'hello \nmundo')
274 ([u'hello', u'\nmundo'], {})
275 >>> parse_command(r'test \N')
276 ([u'test', None], {})
277 >>> parse_command(r'\N')
279 >>> parse_command(r'none=\N')
281 >>> parse_command(r'\N=none')
282 ([], {'\\N': 'none'})
283 >>> parse_command(r'Not\N')
285 >>> parse_command(r'\None')
288 This examples are syntax errors:
289 Missing quote: "hello world
290 Missing value: hello=
292 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
293 separators = (u' ', u'\t', u'\v', u'\n') # token separators
294 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
301 def register_token(buff, keyword, seq, dic):
304 if keyword is not None:
305 dic[keyword.encode('utf-8')] = buff
310 return (buff, keyword)
311 for n, c in enumerate(command):
314 # Not yet registered the token
315 if state == SEP and buff:
316 (buff, keyword) = register_token(buff, keyword, seq, dic)
318 for e in escaped_chars:
320 buff += eval(u'"\\' + e + u'"')
329 # Escaped sequence start
337 if buff and n != 2: # Not the first item (even if was a escape seq)
338 if c == EQUAL: # Keyword found
342 (buff, keyword) = register_token(buff, keyword, seq, dic)
352 # Check if a keyword is added
353 if c == EQUAL and keyword is None and buff:
363 # Inside a double quote
370 # Inside a single quote
377 assert 0, u'Unexpected state'
378 if state == DQUOTE or state == SQUOTE:
379 raise ParseError(command, u'missing closing quote (%s)' % state)
380 if not buff and keyword is not None:
381 raise ParseError(command,
382 u'keyword argument (%s) without value' % keyword)
384 register_token(buff, keyword, seq, dic)
387 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
388 kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
391 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
393 This class provides a modular and extensible dispatching mechanism. You
394 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
396 The command can have arguments, separated by (any number of) spaces and
397 keyword arguments (see parse_command for more details).
399 The dispatcher tries to route the command as deeply as it can, passing
400 the other "path" components as arguments to the callable. To route the
401 command it inspects the callable attributes to find a suitable callable
402 attribute to handle the command in a more specific way, and so on.
405 >>> d = Dispatcher(dict(handler=some_handler))
406 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
408 If 'some_handler' is an object with an 'attribute' that is another
409 object which has a method named 'method', then
410 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
411 If some_handler is a function, then some_handler('attribute', 'method',
412 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
413 complex and deep as you want.
415 If some command can't be dispatched, a CommandError subclass is raised.
418 def __init__(self, root):
419 r"""Initialize the Dispatcher object.
421 See Dispatcher class documentation for more info.
425 def dispatch(self, route):
426 r"""dispatch(route) -> None :: Dispatch a command string.
428 This method searches for a suitable callable object in the routes
429 "tree" and call it, or raises a CommandError subclass if the command
432 route - *unicode* string with the command route.
435 (route, kwargs) = parse_command(route)
437 raise CommandNotSpecifiedError()
439 while not is_handler(handler):
441 if isinstance(handler, Handler):
442 raise CommandIsAHandlerError(command)
443 raise CommandNotFoundError(command)
444 command.append(route[0])
445 if not hasattr(handler, route[0].encode('utf-8')):
446 if isinstance(handler, Handler) and len(command) > 1:
447 raise CommandNotInHandlerError(command)
448 raise CommandNotFoundError(command)
449 handler = getattr(handler, route[0].encode('utf-8'))
452 return handler(*route, **kwargs)
454 m = args_re.match(unicode(e))
456 (quant, n_ok, n_bad) = m.groups()
464 raise WrongArgumentsError(handler, u'takes %s %s argument%s, '
465 '%s given' % (quant, n_ok, pl, n_bad))
466 m = kw_re.match(unicode(e))
469 raise WrongArgumentsError(handler,
470 u'got an unexpected keyword argument %s' % kw)
474 if __name__ == '__main__':
476 @handler(u"test: Print all the arguments, return nothing")
477 def test_func(*args):
480 class TestClassSubHandler(Handler):
481 @handler(u"subcmd: Print all the arguments, return nothing")
482 def subcmd(self, *args):
483 print 'class.subclass.subcmd:', args
485 class TestClass(Handler):
486 @handler(u"cmd1: Print all the arguments, return nothing")
487 def cmd1(self, *args):
488 print 'class.cmd1:', args
489 @handler(u"cmd2: Print all the arguments, return nothing")
490 def cmd2(self, *args):
491 print 'class.cmd2:', args
492 subclass = TestClassSubHandler()
494 class RootHandler(Handler):
495 func = staticmethod(test_func)
498 d = Dispatcher(RootHandler())
500 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
501 print 'inst commands:', tuple(d.dispatch('inst commands'))
502 print 'inst help:', d.dispatch('inst help')
503 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
504 d.dispatch('inst cmd2 arg1 arg2')
505 print 'inst subclass help:', d.dispatch('inst subclass help')
506 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
509 except CommandNotSpecifiedError, e:
510 print 'Not found:', e
512 d.dispatch('sucutrule piquete culete')
513 except CommandNotFoundError, e:
514 print 'Not found:', e
516 d.dispatch('inst cmd3 arg1 arg2 arg3')
517 except CommandNotInHandlerError, e:
518 print 'Not found:', e
521 except CommandIsAHandlerError, e:
522 print 'Not found:', e
527 p = parse_command('hello world')
528 assert p == ([u'hello', u'world'], {}), p
529 p = parse_command('hello planet=earth')
530 assert p == ([u'hello'], {'planet': u'earth'}), p
531 p = parse_command('hello planet="third rock from the sun"')
532 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
533 p = parse_command(u' planet="third rock from the sun" hello ')
534 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
535 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
537 assert p == ([u'hi, hello', u'how are you'],
538 {'planet': u'third rock from the sun'}), p
539 p = parse_command(u'one two three "fourth number"=four')
540 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
541 p = parse_command(u'one two three "fourth number=four"')
542 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
543 p = parse_command(u'one two three fourth\=four')
544 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
545 p = parse_command(u'one two three fourth=four=five')
546 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
547 p = parse_command(ur'nice\nlong\n\ttext')
548 assert p == ([u'nice\nlong\n\ttext'], {}), p
549 p = parse_command('=hello')
550 assert p == ([u'=hello'], {}), p
551 p = parse_command(r'\thello')
552 assert p == ([u'\thello'], {}), p
553 p = parse_command(r'hello \n')
554 assert p == ([u'hello', u'\n'], {}), p
555 p = parse_command(r'hello \nmundo')
556 assert p == ([u'hello', u'\nmundo'], {}), p
557 p = parse_command(r'test \N')
558 assert p == ([u'test', None], {}), p
559 p = parse_command(r'\N')
560 assert p == ([None], {}), p
561 p = parse_command(r'none=\N')
562 assert p == ([], {'none': None}), p
563 p = parse_command(r'\N=none')
564 assert p == ([], {'\\N': 'none'}), p
565 p = parse_command(r'Not\N')
566 assert p == ([u'Not\\N'], {}), p
567 p = parse_command(r'\None')
568 assert p == ([u'\\None'], {}), p
570 p = parse_command('hello=')
571 except ParseError, e:
574 assert False, p + ' should raised a ParseError'
576 p = parse_command('"hello')
577 except ParseError, e:
580 assert False, p + ' should raised a ParseError'