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 parse_command(command):
220 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
222 This function parses a command and split it into a list of parameters. It
223 has a similar to bash commandline parser. Spaces are the basic token
224 separator but you can group several tokens into one by using (single or
225 double) quotes. You can escape the quotes with a backslash (\' and \"),
226 express a backslash literal using a double backslash (\\), use special
227 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
228 single quotes inside a double quoted token or vice-versa. A special escape
229 sequence is provided to express a NULL/None value: \N and it should appear
230 always as a separated token.
232 Additionally it accepts keyword arguments. When an (not-escaped) equal
233 sign (=) is found, the argument is considered a keyword, and the next
234 argument it's interpreted as its value.
236 This function returns a tuple containing a list and a dictionary. The
237 first has the positional arguments, the second, the keyword arguments.
239 There is no restriction about the order, a keyword argument can be
240 followed by a positional argument and vice-versa. All type of arguments
241 are grouped in the list/dict returned. The order of the positional
242 arguments is preserved and if there are multiple keyword arguments with
243 the same key, the last value is the winner (all other values are lost).
245 The command should be a unicode string.
249 >>> parse_command('hello world')
250 ([u'hello', u'world'], {})
251 >>> parse_command('hello planet=earth')
252 ([u'hello'], {'planet': u'earth'})
253 >>> parse_command('hello planet="third rock from the sun"')
254 ([u'hello'], {'planet': u'third rock from the sun'})
255 >>> parse_command(u' planet="third rock from the sun" hello ')
256 ([u'hello'], {'planet': u'third rock from the sun'})
257 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
259 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
260 >>> parse_command(u'one two three "fourth number"=four')
261 ([u'one', u'two', u'three'], {'fourth number': u'four'})
262 >>> parse_command(u'one two three "fourth number=four"')
263 ([u'one', u'two', u'three', u'fourth number=four'], {})
264 >>> parse_command(u'one two three fourth\=four')
265 ([u'one', u'two', u'three', u'fourth=four'], {})
266 >>> parse_command(u'one two three fourth=four=five')
267 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
268 >>> parse_command(ur'nice\nlong\n\ttext')
269 ([u'nice\nlong\n\ttext'], {})
270 >>> parse_command('=hello')
272 >>> parse_command(r'\thello')
274 >>> parse_command(r'hello \n')
275 ([u'hello', u'\n'], {})
276 >>> parse_command(r'hello \nmundo')
277 ([u'hello', u'\nmundo'], {})
278 >>> parse_command(r'test \N')
279 ([u'test', None], {})
280 >>> parse_command(r'\N')
282 >>> parse_command(r'none=\N')
284 >>> parse_command(r'\N=none')
285 ([], {'\\N': 'none'})
286 >>> parse_command(r'Not\N')
288 >>> parse_command(r'\None')
291 This examples are syntax errors:
292 Missing quote: "hello world
293 Missing value: hello=
295 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
296 separators = (u' ', u'\t', u'\v', u'\n') # token separators
297 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
304 def register_token(buff, keyword, seq, dic):
307 if keyword is not None:
308 dic[keyword.encode('utf-8')] = buff
313 return (buff, keyword)
314 for n, c in enumerate(command):
317 # Not yet registered the token
318 if state == SEP and buff:
319 (buff, keyword) = register_token(buff, keyword, seq, dic)
321 for e in escaped_chars:
323 buff += eval(u'"\\' + e + u'"')
332 # Escaped sequence start
340 if buff and n != 2: # Not the first item (even if was a escape seq)
341 if c == EQUAL: # Keyword found
345 (buff, keyword) = register_token(buff, keyword, seq, dic)
355 # Check if a keyword is added
356 if c == EQUAL and keyword is None and buff:
366 # Inside a double quote
373 # Inside a single quote
380 assert 0, u'Unexpected state'
381 if state == DQUOTE or state == SQUOTE:
382 raise ParseError(command, u'missing closing quote (%s)' % state)
383 if not buff and keyword is not None:
384 raise ParseError(command,
385 u'keyword argument (%s) without value' % keyword)
387 register_token(buff, keyword, seq, dic)
390 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
391 kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
394 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
396 This class provides a modular and extensible dispatching mechanism. You
397 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
399 The command can have arguments, separated by (any number of) spaces and
400 keyword arguments (see parse_command for more details).
402 The dispatcher tries to route the command as deeply as it can, passing
403 the other "path" components as arguments to the callable. To route the
404 command it inspects the callable attributes to find a suitable callable
405 attribute to handle the command in a more specific way, and so on.
408 >>> d = Dispatcher(dict(handler=some_handler))
409 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
411 If 'some_handler' is an object with an 'attribute' that is another
412 object which has a method named 'method', then
413 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
414 If some_handler is a function, then some_handler('attribute', 'method',
415 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
416 complex and deep as you want.
418 If some command can't be dispatched, a CommandError subclass is raised.
421 def __init__(self, root):
422 r"""Initialize the Dispatcher object.
424 See Dispatcher class documentation for more info.
428 def dispatch(self, route):
429 r"""dispatch(route) -> None :: Dispatch a command string.
431 This method searches for a suitable callable object in the routes
432 "tree" and call it, or raises a CommandError subclass if the command
435 route - *unicode* string with the command route.
438 (route, kwargs) = parse_command(route)
440 raise CommandNotSpecifiedError()
442 while not is_handler(handler):
444 if isinstance(handler, Handler):
445 raise CommandIsAHandlerError(command)
446 raise CommandNotFoundError(command)
447 command.append(route[0])
448 if route[0] == 'parent':
449 raise CommandNotFoundError(command)
450 if not hasattr(handler, route[0].encode('utf-8')):
451 if isinstance(handler, Handler) and len(command) > 1:
452 raise CommandNotInHandlerError(command)
453 raise CommandNotFoundError(command)
454 handler = getattr(handler, route[0].encode('utf-8'))
457 return handler(*route, **kwargs)
459 m = args_re.match(unicode(e))
461 (quant, n_ok, n_bad) = m.groups()
469 raise WrongArgumentsError(handler, u'takes %s %s argument%s, '
470 '%s given' % (quant, n_ok, pl, n_bad))
471 m = kw_re.match(unicode(e))
474 raise WrongArgumentsError(handler,
475 u'got an unexpected keyword argument %s' % kw)
479 if __name__ == '__main__':
481 @handler(u"test: Print all the arguments, return nothing")
482 def test_func(*args):
485 class TestClassSubHandler(Handler):
486 @handler(u"subcmd: Print all the arguments, return nothing")
487 def subcmd(self, *args):
488 print 'class.subclass.subcmd:', args
490 class TestClass(Handler):
491 @handler(u"cmd1: Print all the arguments, return nothing")
492 def cmd1(self, *args):
493 print 'class.cmd1:', args
494 @handler(u"cmd2: Print all the arguments, return nothing")
495 def cmd2(self, *args):
496 print 'class.cmd2:', args
497 subclass = TestClassSubHandler()
499 class RootHandler(Handler):
500 func = staticmethod(test_func)
503 d = Dispatcher(RootHandler())
505 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
506 print 'inst commands:', tuple(d.dispatch('inst commands'))
507 print 'inst help:', d.dispatch('inst help')
508 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
509 d.dispatch('inst cmd2 arg1 arg2')
510 print 'inst subclass help:', d.dispatch('inst subclass help')
511 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
514 except CommandNotSpecifiedError, e:
515 print 'Not found:', e
517 d.dispatch('sucutrule piquete culete')
518 except CommandNotFoundError, e:
519 print 'Not found:', e
521 d.dispatch('inst cmd3 arg1 arg2 arg3')
522 except CommandNotInHandlerError, e:
523 print 'Not found:', e
526 except CommandIsAHandlerError, e:
527 print 'Not found:', e
532 p = parse_command('hello world')
533 assert p == ([u'hello', u'world'], {}), p
534 p = parse_command('hello planet=earth')
535 assert p == ([u'hello'], {'planet': u'earth'}), p
536 p = parse_command('hello planet="third rock from the sun"')
537 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
538 p = parse_command(u' planet="third rock from the sun" hello ')
539 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
540 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
542 assert p == ([u'hi, hello', u'how are you'],
543 {'planet': u'third rock from the sun'}), p
544 p = parse_command(u'one two three "fourth number"=four')
545 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
546 p = parse_command(u'one two three "fourth number=four"')
547 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
548 p = parse_command(u'one two three fourth\=four')
549 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
550 p = parse_command(u'one two three fourth=four=five')
551 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
552 p = parse_command(ur'nice\nlong\n\ttext')
553 assert p == ([u'nice\nlong\n\ttext'], {}), p
554 p = parse_command('=hello')
555 assert p == ([u'=hello'], {}), p
556 p = parse_command(r'\thello')
557 assert p == ([u'\thello'], {}), p
558 p = parse_command(r'hello \n')
559 assert p == ([u'hello', u'\n'], {}), p
560 p = parse_command(r'hello \nmundo')
561 assert p == ([u'hello', u'\nmundo'], {}), p
562 p = parse_command(r'test \N')
563 assert p == ([u'test', None], {}), p
564 p = parse_command(r'\N')
565 assert p == ([None], {}), p
566 p = parse_command(r'none=\N')
567 assert p == ([], {'none': None}), p
568 p = parse_command(r'\N=none')
569 assert p == ([], {'\\N': 'none'}), p
570 p = parse_command(r'Not\N')
571 assert p == ([u'Not\\N'], {}), p
572 p = parse_command(r'\None')
573 assert p == ([u'\\None'], {}), p
575 p = parse_command('hello=')
576 except ParseError, e:
579 assert False, p + ' should raised a ParseError'
581 p = parse_command('"hello')
582 except ParseError, e:
585 assert False, p + ' should raised a ParseError'