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 import logging ; log = logging.getLogger('pymin.dispatcher')
14 __all__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
15 'Dispatcher', 'handler', 'is_handler', 'get_help')
17 class Error(RuntimeError):
18 r"""Error(command) -> Error instance :: Base dispatching exceptions class.
20 All exceptions raised by the Dispatcher inherits from this one, so you can
21 easily catch any dispatching exception.
23 command - is the command that raised the exception, expressed as a list of
24 paths (or subcommands).
27 def __init__(self, message):
28 r"Initialize the Error object. See class documentation for more info."
29 self.message = message
31 def __unicode__(self):
35 return unicode(self).encode('utf-8')
37 class HandlerError(Error):
38 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
40 All exceptions raised by the handlers should inherit from this one, so
41 dispatching errors could be separated from real programming errors (bugs).
45 class CommandError(Error):
46 r"""CommandError(command) -> CommandError instance :: Base command error.
48 This exception is raised when there's a problem with the command itself.
49 It's the base class for all command (as a string) related error.
52 def __init__(self, command):
53 r"Initialize the object, see class documentation for more info."
54 self.command = command
56 def __unicode__(self):
57 return u'Error in command "%s".' % u' '.join(self.command)
59 class WrongArgumentsError(CommandError):
60 r"""WrongArgumentsError(handler, error) -> WrongArgumentsError instance.
62 This exception is raised when an empty command string is received.
65 def __init__(self, handler, error):
66 r"Initialize the object, see class documentation for more info."
67 self.handler = handler
70 args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
72 extra_kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
74 dup_kw_re = re.compile(r'\w+\(\) got multiple values for keyword argument '
78 r"format() -> unicode - Format a TypeError to adapt it to a command."
79 m = self.args_re.match(unicode(self.error))
81 (quant, n_ok, n_bad) = m.groups()
89 return u'takes %s %s argument%s, %s given' \
90 % (quant, n_ok, pl, n_bad)
91 m = self.extra_kw_re.match(unicode(self.error))
94 return u'got an unexpected keyword argument %s' % kw
95 m = self.dup_kw_re.match(unicode(self.error))
98 return u'got multiple values for argument %s' % kw
99 return u'got wrong arguments'
101 def __unicode__(self):
102 return u'Command "%s" %s.' % (self.handler.__name__, self.format())
104 class CommandNotSpecifiedError(CommandError):
105 r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
107 This exception is raised when an empty command string is received.
111 r"Initialize the object, see class documentation for more info."
114 def __unicode__(self):
115 return u'Command not specified.'
117 class CommandIsAHandlerError(CommandError):
118 r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
120 This exception is raised when a command is a handler containing commands
121 instead of a command itself.
124 def __unicode__(self):
125 command = ' '.join(self.command)
126 return u'"%s" is a handler, not a command (type "%s help" for help).' \
129 class CommandNotInHandlerError(CommandError):
130 r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
132 This exception is raised when a command parent is a hanlder containing
133 commands, but the command itself is not found.
136 def __unicode__(self):
137 return u'Command "%(c)s" not found in handler "%(h)s" ' \
138 u'(type "%(h)s help" for help).' \
139 % dict(c=u' '.join(self.command[-1:]),
140 h=u' '.join(self.command[0:-1]))
142 class CommandNotFoundError(CommandError):
143 r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
145 This exception is raised when the command received can't be dispatched
146 because there is no handlers to process it.
149 def __unicode__(self):
150 return u'Command "%s" not found.' % u' '.join(self.command)
152 class ParseError(CommandError):
153 r"""ParseError(command[, desc]) -> ParseError instance
155 This exception is raised when there is an error parsing a command.
157 command - Command that can't be parsed.
159 desc - Description of the error.
162 def __init__(self, command, desc="can't parse"):
163 r"""Initialize the object.
165 See class documentation for more info.
167 self.command = command
170 def __unicode__(self):
171 return u'Syntax error, %s: %s' % (self.desc, self.command)
173 class HelpNotFoundError(Error):
174 r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
176 This exception is raised when a help command can't find the command
180 def __init__(self, command):
181 r"""Initialize the object.
183 See class documentation for more info.
185 self.command = command
187 def __unicode__(self):
188 return u"Can't get help for '%s', command not found." % self.command
192 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
194 This is a decorator to mark a callable object as a dispatcher handler.
196 help - Help string for the handler.
199 raise TypeError("'help' should not be empty")
201 log.debug('handler(): Decorating %s()', f.__name__)
202 # Here comes the tricky part:
203 # We need to make our wrapped function to accept any number of
204 # positional and keyword arguments, but checking for the correct
205 # arguments and raising an exception in case the arguments doesn't
207 # So we create a dummy function, with the same signature as the
208 # wrapped one, so we can check later (at "dispatch-time") if the
209 # real function call will be successful. If the dummy function don't
210 # raise a TypeError, the arguments are just fine.
212 argspec = inspect.getargspec(f)
213 signature = inspect.formatargspec(*argspec)
215 exec "def f%s: pass" % signature in env
216 signature_check = env['f']
217 # The wrapper to check the signature at "dispatch-time"
218 def wrapper(*args, **kwargs):
219 # First we check if the arguments passed are OK.
221 signature_check(*args, **kwargs)
223 # If not, we raise an appropriate error.
224 raise WrongArgumentsError(f, e)
225 # If they are fine, we call the real function
226 return f(*args, **kwargs)
227 # Some flag to mark our handlers for simple checks
228 wrapper._dispatcher_handler = True
229 # The help string we asked for in the first place =)
230 wrapper.handler_help = help
231 # We store the original signature for better help generation
232 wrapper.handler_argspec = argspec
233 # And some makeup, to make our wrapper look like the original function
234 wrapper.__name__ = f.__name__
235 wrapper.__dict__.update(f.__dict__)
236 # We add a hint in the documentation
237 wrapper.__doc__ = "Pymin handler with signature: %s%s" \
238 % (wrapper.__name__, signature)
239 if f.__doc__ is not None:
240 wrapper.__doc__ += "\n\n" + f.__doc__
241 wrapper.__module__ = f.__module__
245 def is_handler(handler):
246 r"is_handler(handler) -> bool :: Tell if a object is a handler."
247 return callable(handler) and hasattr(handler, '_dispatcher_handler') \
248 and handler._dispatcher_handler
251 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
253 All dispatcher handlers should inherit from this class to have some extra
254 commands, like help. You should override the 'handler_help' attribute to a
255 nice help message describing the handler.
258 handler_help = u'Undocumented handler'
260 @handler(u'List available commands')
262 r"""commands() -> generator :: List the available commands."""
263 return (a for a in dir(self) if is_handler(getattr(self, a)))
265 @handler(u'Show available commands with their help')
266 def help(self, command=None):
267 r"""help([command]) -> unicode/dict :: Show help on available commands.
269 If command is specified, it returns the help of that particular command.
270 If not, it returns a dictionary which keys are the available commands
271 and values are the help strings.
277 if a == 'parent': continue # Skip parents in SubHandlers
278 if is_handler(h) or isinstance(h, Handler):
279 d[a] = h.handler_help
281 # A command was specified
282 if command == 'parent': # Skip parents in SubHandlers
283 raise HelpNotFoundError(command)
284 if not hasattr(self, command.encode('utf-8')):
285 raise HelpNotFoundError(command)
286 handler = getattr(self, command.encode('utf-8'))
287 if not is_handler(handler) and not hasattr(handler, 'handler_help'):
288 raise HelpNotFoundError(command)
289 return handler.handler_help
291 def handle_timer(self):
292 r"""handle_timer() -> None :: Do periodic tasks.
294 By default we do nothing but calling handle_timer() on subhandlers.
297 if a == 'parent': continue # Skip parents in SubHandlers
299 if isinstance(h, Handler):
302 def parse_command(command):
303 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
305 This function parses a command and split it into a list of parameters. It
306 has a similar to bash commandline parser. Spaces are the basic token
307 separator but you can group several tokens into one by using (single or
308 double) quotes. You can escape the quotes with a backslash (\' and \"),
309 express a backslash literal using a double backslash (\\), use special
310 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
311 single quotes inside a double quoted token or vice-versa. A special escape
312 sequence is provided to express a NULL/None value: \N and it should appear
313 always as a separated token.
315 Additionally it accepts keyword arguments. When an (not-escaped) equal
316 sign (=) is found, the argument is considered a keyword, and the next
317 argument it's interpreted as its value.
319 This function returns a tuple containing a list and a dictionary. The
320 first has the positional arguments, the second, the keyword arguments.
322 There is no restriction about the order, a keyword argument can be
323 followed by a positional argument and vice-versa. All type of arguments
324 are grouped in the list/dict returned. The order of the positional
325 arguments is preserved and if there are multiple keyword arguments with
326 the same key, the last value is the winner (all other values are lost).
328 The command should be a unicode string.
332 >>> parse_command('hello world')
333 ([u'hello', u'world'], {})
334 >>> parse_command('hello planet=earth')
335 ([u'hello'], {'planet': u'earth'})
336 >>> parse_command('hello planet="third rock from the sun"')
337 ([u'hello'], {'planet': u'third rock from the sun'})
338 >>> parse_command(u' planet="third rock from the sun" hello ')
339 ([u'hello'], {'planet': u'third rock from the sun'})
340 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
342 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
343 >>> parse_command(u'one two three "fourth number"=four')
344 ([u'one', u'two', u'three'], {'fourth number': u'four'})
345 >>> parse_command(u'one two three "fourth number=four"')
346 ([u'one', u'two', u'three', u'fourth number=four'], {})
347 >>> parse_command(u'one two three fourth\=four')
348 ([u'one', u'two', u'three', u'fourth=four'], {})
349 >>> parse_command(u'one two three fourth=four=five')
350 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
351 >>> parse_command(ur'nice\nlong\n\ttext')
352 ([u'nice\nlong\n\ttext'], {})
353 >>> parse_command('=hello')
355 >>> parse_command(r'\thello')
357 >>> parse_command(r'hello \n')
358 ([u'hello', u'\n'], {})
359 >>> parse_command(r'hello \nmundo')
360 ([u'hello', u'\nmundo'], {})
361 >>> parse_command(r'test \N')
362 ([u'test', None], {})
363 >>> parse_command(r'\N')
365 >>> parse_command(r'none=\N')
367 >>> parse_command(r'\N=none')
368 ([], {'\\N': 'none'})
369 >>> parse_command(r'Not\N')
371 >>> parse_command(r'\None')
374 This examples are syntax errors:
375 Missing quote: "hello world
376 Missing value: hello=
378 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
379 separators = (u' ', u'\t', u'\v', u'\n') # token separators
380 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
387 def register_token(buff, keyword, seq, dic):
390 if keyword is not None:
391 dic[keyword.encode('utf-8')] = buff
396 return (buff, keyword)
397 for n, c in enumerate(command):
400 # Not yet registered the token
401 if state == SEP and buff:
402 (buff, keyword) = register_token(buff, keyword, seq, dic)
404 for e in escaped_chars:
406 buff += eval(u'"\\' + e + u'"')
415 # Escaped sequence start
423 if buff and n != 2: # Not the first item (even if was a escape seq)
424 if c == EQUAL: # Keyword found
428 (buff, keyword) = register_token(buff, keyword, seq, dic)
438 # Check if a keyword is added
439 if c == EQUAL and keyword is None and buff:
449 # Inside a double quote
456 # Inside a single quote
463 assert 0, u'Unexpected state'
464 if state == DQUOTE or state == SQUOTE:
465 raise ParseError(command, u'missing closing quote (%s)' % state)
466 if not buff and keyword is not None:
467 raise ParseError(command,
468 u'keyword argument (%s) without value' % keyword)
470 register_token(buff, keyword, seq, dic)
474 r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
476 This class provides a modular and extensible dispatching mechanism. You
477 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
479 The command can have arguments, separated by (any number of) spaces and
480 keyword arguments (see parse_command for more details).
482 The dispatcher tries to route the command as deeply as it can, passing
483 the other "path" components as arguments to the callable. To route the
484 command it inspects the callable attributes to find a suitable callable
485 attribute to handle the command in a more specific way, and so on.
488 >>> d = Dispatcher(dict(handler=some_handler))
489 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
491 If 'some_handler' is an object with an 'attribute' that is another
492 object which has a method named 'method', then
493 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
494 If some_handler is a function, then some_handler('attribute', 'method',
495 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
496 complex and deep as you want.
498 If some command can't be dispatched, a CommandError subclass is raised.
501 def __init__(self, root):
502 r"""Initialize the Dispatcher object.
504 See Dispatcher class documentation for more info.
506 log.debug(u'Dispatcher(%r)', root)
509 def dispatch(self, route):
510 r"""dispatch(route) -> None :: Dispatch a command string.
512 This method searches for a suitable callable object in the routes
513 "tree" and call it, or raises a CommandError subclass if the command
516 route - *unicode* string with the command route.
518 log.debug('Dispatcher.dispatch(%r)', route)
520 (route, kwargs) = parse_command(route)
521 log.debug(u'Dispatcher.dispatch: route=%r, kwargs=%r', route, kwargs)
523 log.debug(u'Dispatcher.dispatch: command not specified')
524 raise CommandNotSpecifiedError()
526 while not is_handler(handler):
527 log.debug(u'Dispatcher.dispatch: handler=%r, route=%r',
530 if isinstance(handler, Handler):
531 log.debug(u'Dispatcher.dispatch: command is a handler')
532 raise CommandIsAHandlerError(command)
533 log.debug(u'Dispatcher.dispatch: command not found')
534 raise CommandNotFoundError(command)
535 command.append(route[0])
536 log.debug(u'Dispatcher.dispatch: command=%r', command)
537 if route[0] == 'parent':
538 log.debug(u'Dispatcher.dispatch: is parent => not found')
539 raise CommandNotFoundError(command)
540 if not hasattr(handler, route[0].encode('utf-8')):
541 if isinstance(handler, Handler) and len(command) > 1:
542 log.debug(u'Dispatcher.dispatch: command not in handler')
543 raise CommandNotInHandlerError(command)
544 log.debug(u'Dispatcher.dispatch: command not found')
545 raise CommandNotFoundError(command)
546 handler = getattr(handler, route[0].encode('utf-8'))
548 log.debug(u'Dispatcher.dispatch: %r is a handler, calling it with '
549 u'route=%r, kwargs=%r', handler, route, kwargs)
550 r = handler(*route, **kwargs)
551 log.debug(u'Dispatcher.dispatch: handler returned %s', r)
555 if __name__ == '__main__':
558 level = logging.DEBUG,
559 format = '%(asctime)s %(levelname)-8s %(message)s',
560 datefmt = '%H:%M:%S',
563 @handler(u"test: Print all the arguments, return nothing")
564 def test_func(*args):
567 class TestClassSubHandler(Handler):
568 @handler(u"subcmd: Print all the arguments, return nothing")
569 def subcmd(self, *args):
570 print 'class.subclass.subcmd:', args
572 class TestClass(Handler):
573 @handler(u"cmd1: Print all the arguments, return nothing")
574 def cmd1(self, *args):
575 print 'class.cmd1:', args
576 @handler(u"cmd2: Print all the arguments, return nothing")
577 def cmd2(self, arg1, arg2):
578 print 'class.cmd2:', arg1, arg2
579 subclass = TestClassSubHandler()
581 class RootHandler(Handler):
582 func = staticmethod(test_func)
585 d = Dispatcher(RootHandler())
587 r = d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
589 r = list(d.dispatch('inst commands'))
591 assert r == ['cmd1', 'cmd2', 'commands', 'help']
592 print 'inst commands:', r
593 r = d.dispatch('inst help')
595 'commands': u'List available commands',
596 'subclass': u'Undocumented handler',
597 'cmd1': u'cmd1: Print all the arguments, return nothing',
598 'cmd2': u'cmd2: Print all the arguments, return nothing',
599 'help': u'Show available commands with their help'
601 print 'inst help:', r
602 r = d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
604 r = d.dispatch('inst cmd2 arg1 arg2')
606 r = d.dispatch('inst subclass help')
608 'subcmd': u'subcmd: Print all the arguments, return nothing',
609 'commands': u'List available commands',
610 'help': u'Show available commands with their help'
612 print 'inst subclass help:', r
613 r = d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
617 assert False, 'It should raised a CommandNotSpecifiedError'
618 except CommandNotSpecifiedError, e:
619 print 'Not found:', e
621 d.dispatch('sucutrule piquete culete')
622 assert False, 'It should raised a CommandNotFoundError'
623 except CommandNotFoundError, e:
624 print 'Not found:', e
626 d.dispatch('inst cmd3 arg1 arg2 arg3')
627 assert False, 'It should raised a CommandNotInHandlerError'
628 except CommandNotInHandlerError, e:
629 print 'Not found:', e
632 assert False, 'It should raised a CommandIsAHandlerError'
633 except CommandIsAHandlerError, e:
634 print 'Not found:', e
636 d.dispatch('inst cmd2 "just one arg"')
637 assert False, 'It should raised a WrongArgumentsError'
638 except WrongArgumentsError, e:
639 print 'Bad arguments:', e
641 d.dispatch('inst cmd2 arg1 arg2 "an extra argument"')
642 assert False, 'It should raised a WrongArgumentsError'
643 except WrongArgumentsError, e:
644 print 'Bad arguments:', e
646 d.dispatch('inst cmd2 arg1 arg2 arg3="unexpected keyword arg"')
647 assert False, 'It should raised a WrongArgumentsError'
648 except WrongArgumentsError, e:
649 print 'Bad arguments:', e
651 d.dispatch('inst cmd2 arg1 arg2 arg2="duplicated keyword arg"')
652 assert False, 'It should raised a WrongArgumentsError'
653 except WrongArgumentsError, e:
654 print 'Bad arguments:', e
659 p = parse_command('hello world')
660 assert p == ([u'hello', u'world'], {}), p
661 p = parse_command('hello planet=earth')
662 assert p == ([u'hello'], {'planet': u'earth'}), p
663 p = parse_command('hello planet="third rock from the sun"')
664 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
665 p = parse_command(u' planet="third rock from the sun" hello ')
666 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
667 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
669 assert p == ([u'hi, hello', u'how are you'],
670 {'planet': u'third rock from the sun'}), p
671 p = parse_command(u'one two three "fourth number"=four')
672 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
673 p = parse_command(u'one two three "fourth number=four"')
674 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
675 p = parse_command(u'one two three fourth\=four')
676 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
677 p = parse_command(u'one two three fourth=four=five')
678 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
679 p = parse_command(ur'nice\nlong\n\ttext')
680 assert p == ([u'nice\nlong\n\ttext'], {}), p
681 p = parse_command('=hello')
682 assert p == ([u'=hello'], {}), p
683 p = parse_command(r'\thello')
684 assert p == ([u'\thello'], {}), p
685 p = parse_command(r'hello \n')
686 assert p == ([u'hello', u'\n'], {}), p
687 p = parse_command(r'hello \nmundo')
688 assert p == ([u'hello', u'\nmundo'], {}), p
689 p = parse_command(r'test \N')
690 assert p == ([u'test', None], {}), p
691 p = parse_command(r'\N')
692 assert p == ([None], {}), p
693 p = parse_command(r'none=\N')
694 assert p == ([], {'none': None}), p
695 p = parse_command(r'\N=none')
696 assert p == ([], {'\\N': 'none'}), p
697 p = parse_command(r'Not\N')
698 assert p == ([u'Not\\N'], {}), p
699 p = parse_command(r'\None')
700 assert p == ([u'\\None'], {}), p
702 p = parse_command('hello=')
703 assert False, p + ' should raised a ParseError'
704 except ParseError, e:
707 p = parse_command('"hello')
708 assert False, p + ' should raised a ParseError'
709 except ParseError, e: