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.
10 __ALL__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
11 'Dispatcher', 'handler', 'is_handler', 'get_help')
13 class Error(RuntimeError):
14 r"""Error(command) -> Error instance :: Base dispatching exceptions class.
16 All exceptions raised by the Dispatcher inherits from this one, so you can
17 easily catch any dispatching exception.
19 command - is the command that raised the exception, expressed as a list of
20 paths (or subcommands).
24 class HandlerError(Error):
25 r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
27 All exceptions raised by the handlers should inherit from this one, so
28 dispatching errors could be separated from real programming errors (bugs).
32 class CommandError(Error):
33 r"""CommandError(command) -> CommandError instance :: Base command error.
35 This exception is raised when there's a problem with the command itself.
36 It's the base class for all command (as a string) related error.
39 def __init__(self, command):
40 r"""Initialize the object.
42 See class documentation for more info.
44 self.command = command
47 return 'Command error: "%s"' % self.command
49 class CommandNotFoundError(CommandError):
50 r"""CommandNotFoundError(command) -> CommandNotFoundError instance.
52 This exception is raised when the command received can't be dispatched
53 because there is no handlers to process it.
57 return 'Command not found: "%s"' % ' '.join(
58 repr(c) for c in self.command)
60 class ParseError(CommandError):
61 r"""ParseError(command[, desc]) -> ParseError instance
63 This exception is raised when there is an error parsing a command.
65 command - Command that can't be parsed.
67 desc - Description of the error.
70 def __init__(self, command, desc="can't parse"):
71 r"""Initialize the object.
73 See class documentation for more info.
75 self.command = command
79 return 'Syntax error, %s: %s' % (self.desc, self.command)
82 r"""handler(help) -> function wrapper :: Mark a callable as a handler.
84 This is a decorator to mark a callable object as a dispatcher handler.
86 help - Help string for the handler.
90 raise TypeError("'help' should not be empty")
91 f._dispatcher_help = help
95 def is_handler(handler):
96 r"is_handler(handler) -> bool :: Tell if a object is a handler."
97 return callable(handler) and hasattr(handler, '_dispatcher_help')
99 def get_help(handler):
100 r"get_help(handler) -> unicode :: Get a handler's help string."
101 if not is_handler(handler):
102 raise TypeError("'%s' should be a handler" % handler.__name__)
103 return handler._dispatcher_help
106 r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
108 All dispatcher handlers should inherit from this class to have some extra
112 @handler(u'List available commands.')
114 r"""commands() -> generator :: List the available commands."""
115 return (a for a in dir(self) if is_handler(getattr(self, a)))
117 @handler(u'Show available commands with their help.')
118 def help(self, command=None):
119 r"""help([command]) -> unicode/dict :: Show help on available commands.
121 If command is specified, it returns the help of that particular command.
122 If not, it returns a dictionary which keys are the available commands
123 and values are the help strings.
126 return dict((a, get_help(getattr(self, a)))
127 for a in dir(self) if is_handler(getattr(self, a)))
128 if not hasattr(self, command):
129 raise CommandNotFoundError(command)
130 handler = getattr(self, command)
131 if not is_handler(handler):
132 raise CommandNotFoundError(command)
133 return get_help(handler)
135 def parse_command(command):
136 r"""parse_command(command) -> (args, kwargs) :: Parse a command.
138 This function parses a command and split it into a list of parameters. It
139 has a similar to bash commandline parser. Spaces are the basic token
140 separator but you can group several tokens into one by using (single or
141 double) quotes. You can escape the quotes with a backslash (\' and \"),
142 express a backslash literal using a double backslash (\\), use special
143 meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
144 single quotes inside a double quoted token or vice-versa. A special escape
145 sequence is provided to express a NULL/None value: \N and it should appear
146 always as a separated token.
148 Additionally it accepts keyword arguments. When an (not-escaped) equal
149 sign (=) is found, the argument is considered a keyword, and the next
150 argument it's interpreted as its value.
152 This function returns a tuple containing a list and a dictionary. The
153 first has the positional arguments, the second, the keyword arguments.
155 There is no restriction about the order, a keyword argument can be
156 followed by a positional argument and vice-versa. All type of arguments
157 are grouped in the list/dict returned. The order of the positional
158 arguments is preserved and if there are multiple keyword arguments with
159 the same key, the last value is the winner (all other values are lost).
163 >>> parse_command('hello world')
164 ([u'hello', u'world'], {})
165 >>> parse_command('hello planet=earth')
166 ([u'hello'], {'planet': u'earth'})
167 >>> parse_command('hello planet="third rock from the sun"')
168 ([u'hello'], {'planet': u'third rock from the sun'})
169 >>> parse_command(u' planet="third rock from the sun" hello ')
170 ([u'hello'], {'planet': u'third rock from the sun'})
171 >>> parse_command(u' planet="third rock from the sun" "hi, hello"'
173 ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
174 >>> parse_command(u'one two three "fourth number"=four')
175 ([u'one', u'two', u'three'], {'fourth number': u'four'})
176 >>> parse_command(u'one two three "fourth number=four"')
177 ([u'one', u'two', u'three', u'fourth number=four'], {})
178 >>> parse_command(u'one two three fourth\=four')
179 ([u'one', u'two', u'three', u'fourth=four'], {})
180 >>> parse_command(u'one two three fourth=four=five')
181 ([u'one', u'two', u'three'], {'fourth': u'four=five'})
182 >>> parse_command(ur'nice\nlong\n\ttext')
183 ([u'nice\nlong\n\ttext'], {})
184 >>> parse_command('=hello')
186 >>> parse_command(r'\thello')
188 >>> parse_command(r'\N')
190 >>> parse_command(r'none=\N')
192 >>> parse_command(r'\N=none')
193 ([], {'\\N': 'none'})
194 >>> parse_command(r'Not\N')
196 >>> parse_command(r'\None')
199 This examples are syntax errors:
200 Missing quote: "hello world
201 Missing value: hello=
203 SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
204 separators = (u' ', u'\t', u'\v', u'\n') # token separators
205 escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
212 for n, c in enumerate(command):
215 for e in escaped_chars:
217 buff += eval(u'"\\' + e + u'"')
226 # Escaped sequence start
234 if buff and n != 2: # Not the first item (even if was a escape seq)
235 if c == EQUAL: # Keyword found
241 if keyword is not None: # Value found
242 dic[str(keyword)] = buff
244 else: # Normal parameter found
256 # Check if a keyword is added
257 if c == EQUAL and keyword is None and buff:
267 # Inside a double quote
274 # Inside a single quote
281 assert 0, u'Unexpected state'
282 if state == DQUOTE or state == SQUOTE:
283 raise ParseError(command, u'missing closing quote (%s)' % state)
284 if not buff and keyword is not None:
285 raise ParseError(command,
286 u'keyword argument (%s) without value' % keyword)
290 if keyword is not None:
291 dic[str(keyword)] = buff
297 r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
299 This class provides a modular and extensible dispatching mechanism. You
300 can specify root 'routes' (as a dict where the key is the string of the
301 root command and the value is a callable object to handle that command,
302 or a subcommand if the callable is an instance and the command can be
305 The command can have arguments, separated by (any number of) spaces.
307 The dispatcher tries to route the command as deeply as it can, passing
308 the other "path" components as arguments to the callable. To route the
309 command it inspects the callable attributes to find a suitable callable
310 attribute to handle the command in a more specific way, and so on.
313 >>> d = Dispatcher(dict(handler=some_handler))
314 >>> d.dispatch('handler attribute method arg1 arg2 "third argument"')
316 If 'some_handler' is an object with an 'attribute' that is another
317 object which has a method named 'method', then
318 some_handler.attribute.method('arg1', 'arg2') will be called. If
319 some_handler is a function, then some_handler('attribute', 'method',
320 'arg1', 'arg2') will be called. The handler "tree" can be as complex
321 and deep as you want.
323 If some command can't be dispatched (because there is no root handler or
324 there is no matching callable attribute), a CommandNotFoundError is raised.
327 def __init__(self, routes=dict()):
328 r"""Initialize the Dispatcher object.
330 See Dispatcher class documentation for more info.
334 def dispatch(self, route):
335 r"""dispatch(route) -> None :: Dispatch a command string.
337 This method searches for a suitable callable object in the routes
338 "tree" and call it, or raises a CommandNotFoundError if the command
342 (route, kwargs) = parse_command(route)
344 raise CommandNotFoundError(command)
345 command.append(route[0])
346 handler = self.routes.get(route[0], None)
348 raise CommandNotFoundError(command)
350 while not is_handler(handler):
352 raise CommandNotFoundError(command)
353 command.append(route[0])
354 if not hasattr(handler, route[0]):
355 raise CommandNotFoundError(command)
356 handler = getattr(handler, route[0])
358 return handler(*route, **kwargs)
361 if __name__ == '__main__':
363 @handler(u"test: Print all the arguments, return nothing.")
364 def test_func(*args):
367 class TestClassSubHandler(Handler):
368 @handler(u"subcmd: Print all the arguments, return nothing.")
369 def subcmd(self, *args):
370 print 'class.subclass.subcmd:', args
372 class TestClass(Handler):
373 @handler(u"cmd1: Print all the arguments, return nothing.")
374 def cmd1(self, *args):
375 print 'class.cmd1:', args
376 @handler(u"cmd2: Print all the arguments, return nothing.")
377 def cmd2(self, *args):
378 print 'class.cmd2:', args
379 subclass = TestClassSubHandler()
381 test_class = TestClass()
388 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
389 print 'inst commands:', tuple(d.dispatch('inst commands'))
390 print 'inst help:', d.dispatch('inst help')
391 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
392 d.dispatch('inst cmd2 arg1 arg2')
393 print 'inst subclass help:', d.dispatch('inst subclass help')
394 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
397 except CommandNotFoundError, e:
398 print 'Not found:', e
400 d.dispatch('sucutrule piquete culete')
401 except CommandNotFoundError, e:
402 print 'Not found:', e
404 d.dispatch('inst cmd3 arg1 arg2 arg3')
405 except CommandNotFoundError, e:
406 print 'Not found:', e
409 p = parse_command('hello world')
410 assert p == ([u'hello', u'world'], {}), p
411 p = parse_command('hello planet=earth')
412 assert p == ([u'hello'], {'planet': u'earth'}), p
413 p = parse_command('hello planet="third rock from the sun"')
414 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
415 p = parse_command(u' planet="third rock from the sun" hello ')
416 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
417 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
419 assert p == ([u'hi, hello', u'how are you'],
420 {'planet': u'third rock from the sun'}), p
421 p = parse_command(u'one two three "fourth number"=four')
422 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
423 p = parse_command(u'one two three "fourth number=four"')
424 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
425 p = parse_command(u'one two three fourth\=four')
426 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
427 p = parse_command(u'one two three fourth=four=five')
428 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
429 p = parse_command(ur'nice\nlong\n\ttext')
430 assert p == ([u'nice\nlong\n\ttext'], {}), p
431 p = parse_command('=hello')
432 assert p == ([u'=hello'], {}), p
433 p = parse_command(r'\thello')
434 assert p == ([u'\thello'], {}), p
435 p = parse_command(r'\N')
436 assert p == ([None], {}), p
437 p = parse_command(r'none=\N')
438 assert p == ([], {'none': None}), p
439 p = parse_command(r'\N=none')
440 assert p == ([], {'\\N': 'none'}), p
441 p = parse_command(r'Not\N')
442 assert p == ([u'Not\\N'], {}), p
443 p = parse_command(r'\None')
444 assert p == ([u'\\None'], {}), p
446 p = parse_command('hello=')
447 except ParseError, e:
450 assert False, p + ' should raised a ParseError'
452 p = parse_command('"hello')
453 except ParseError, e:
456 assert False, p + ' should raised a ParseError'