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([root]) -> Dispatcher instance :: Command dispatcher.
299 This class provides a modular and extensible dispatching mechanism. You
300 specify a root handler (probably as a pymin.dispatcher.Handler subclass),
302 The command can have arguments, separated by (any number of) spaces and
303 keyword arguments (see parse_command for more details).
305 The dispatcher tries to route the command as deeply as it can, passing
306 the other "path" components as arguments to the callable. To route the
307 command it inspects the callable attributes to find a suitable callable
308 attribute to handle the command in a more specific way, and so on.
311 >>> d = Dispatcher(dict(handler=some_handler))
312 >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
314 If 'some_handler' is an object with an 'attribute' that is another
315 object which has a method named 'method', then
316 some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
317 If some_handler is a function, then some_handler('attribute', 'method',
318 'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
319 complex and deep as you want.
321 If some command can't be dispatched (because there is no root handler
322 or there is no matching callable attribute), a CommandNotFoundError
326 def __init__(self, root):
327 r"""Initialize the Dispatcher object.
329 See Dispatcher class documentation for more info.
333 def dispatch(self, route):
334 r"""dispatch(route) -> None :: Dispatch a command string.
336 This method searches for a suitable callable object in the routes
337 "tree" and call it, or raises a CommandNotFoundError if the command
341 (route, kwargs) = parse_command(route)
343 raise CommandNotFoundError(command)
345 while not is_handler(handler):
347 raise CommandNotFoundError(command)
348 command.append(route[0])
349 if not hasattr(handler, route[0]):
350 raise CommandNotFoundError(command)
351 handler = getattr(handler, route[0])
353 return handler(*route, **kwargs)
356 if __name__ == '__main__':
358 @handler(u"test: Print all the arguments, return nothing.")
359 def test_func(*args):
362 class TestClassSubHandler(Handler):
363 @handler(u"subcmd: Print all the arguments, return nothing.")
364 def subcmd(self, *args):
365 print 'class.subclass.subcmd:', args
367 class TestClass(Handler):
368 @handler(u"cmd1: Print all the arguments, return nothing.")
369 def cmd1(self, *args):
370 print 'class.cmd1:', args
371 @handler(u"cmd2: Print all the arguments, return nothing.")
372 def cmd2(self, *args):
373 print 'class.cmd2:', args
374 subclass = TestClassSubHandler()
376 class RootHandler(Handler):
377 func = staticmethod(test_func)
380 d = Dispatcher(RootHandler())
382 d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
383 print 'inst commands:', tuple(d.dispatch('inst commands'))
384 print 'inst help:', d.dispatch('inst help')
385 d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
386 d.dispatch('inst cmd2 arg1 arg2')
387 print 'inst subclass help:', d.dispatch('inst subclass help')
388 d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
391 except CommandNotFoundError, e:
392 print 'Not found:', e
394 d.dispatch('sucutrule piquete culete')
395 except CommandNotFoundError, e:
396 print 'Not found:', e
398 d.dispatch('inst cmd3 arg1 arg2 arg3')
399 except CommandNotFoundError, e:
400 print 'Not found:', e
405 p = parse_command('hello world')
406 assert p == ([u'hello', u'world'], {}), p
407 p = parse_command('hello planet=earth')
408 assert p == ([u'hello'], {'planet': u'earth'}), p
409 p = parse_command('hello planet="third rock from the sun"')
410 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
411 p = parse_command(u' planet="third rock from the sun" hello ')
412 assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
413 p = parse_command(u' planet="third rock from the sun" "hi, hello" '
415 assert p == ([u'hi, hello', u'how are you'],
416 {'planet': u'third rock from the sun'}), p
417 p = parse_command(u'one two three "fourth number"=four')
418 assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
419 p = parse_command(u'one two three "fourth number=four"')
420 assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
421 p = parse_command(u'one two three fourth\=four')
422 assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
423 p = parse_command(u'one two three fourth=four=five')
424 assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
425 p = parse_command(ur'nice\nlong\n\ttext')
426 assert p == ([u'nice\nlong\n\ttext'], {}), p
427 p = parse_command('=hello')
428 assert p == ([u'=hello'], {}), p
429 p = parse_command(r'\thello')
430 assert p == ([u'\thello'], {}), p
431 p = parse_command(r'\N')
432 assert p == ([None], {}), p
433 p = parse_command(r'none=\N')
434 assert p == ([], {'none': None}), p
435 p = parse_command(r'\N=none')
436 assert p == ([], {'\\N': 'none'}), p
437 p = parse_command(r'Not\N')
438 assert p == ([u'Not\\N'], {}), p
439 p = parse_command(r'\None')
440 assert p == ([u'\\None'], {}), p
442 p = parse_command('hello=')
443 except ParseError, e:
446 assert False, p + ' should raised a ParseError'
448 p = parse_command('"hello')
449 except ParseError, e:
452 assert False, p + ' should raised a ParseError'