]> git.llucax.com Git - software/pymin.git/blob - dispatcher.py
473c886c080b21ed62c1d5814d37d817deaa4e81
[software/pymin.git] / dispatcher.py
1 # vim: set et sts=4 sw=4 encoding=utf-8 :
2
3 r"""
4 Command dispatcher.
5
6 This module provides a convenient and extensible command dispatching mechanism.
7 It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
8 and translates commands to functions/objects/methods.
9 """
10
11 class Error(RuntimeError):
12     r"""
13     Error(command) -> Error instance :: Base dispatching exceptions class.
14
15     All exceptions raised by the Dispatcher inherits from this one, so you can
16     easily catch any dispatching exception.
17
18     command - is the command that raised the exception, expressed as a list of
19               paths (or subcommands).
20     """
21     pass
22
23 class HandlerError(Error):
24     r"""
25     HandlerError(command) -> HandlerError instance :: Base handlers exception.
26
27     All exceptions raised by the handlers should inherit from this one, so
28     dispatching errors could be separated from real programming errors (bugs).
29     """
30     pass
31
32 class CommandNotFoundError(Error):
33     r"""
34     CommandNotFoundError(command) -> CommandNotFoundError instance
35
36     This exception is raised when the command received can't be dispatched
37     because there is no handlers to process it.
38     """
39
40     def __init__(self, command):
41         r"""Initialize the Error object.
42
43         See Error class documentation for more info.
44         """
45         self.command = command
46
47     def __str__(self):
48         return 'Command not found: "%s"' % ' '.join(self.command)
49
50 def handler(help):
51     r"""handler(help) -> function wrapper :: Mark a callable as a handler.
52
53     This is a decorator to mark a callable object as a dispatcher handler.
54
55     help - Help string for the handler.
56     """
57     def wrapper(f):
58         if not help:
59             raise TypeError("'help' should not be empty")
60         f._dispatcher_help = help
61         return f
62     return wrapper
63
64 def is_handler(handler):
65     r"is_handler(handler) -> bool :: Tell if a object is a handler."
66     return callable(handler) and hasattr(handler, '_dispatcher_help')
67
68 def get_help(handler):
69     r"get_help(handler) -> unicode :: Get a handler's help string."
70     if not is_handler(handler):
71         raise TypeError("'%s' should be a handler" % handler.__name__)
72     return handler._dispatcher_help
73
74 class Handler:
75     r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
76
77     All dispatcher handlers should inherit from this class to have some extra
78     commands, like help.
79     """
80
81     @handler(u'List available commands.')
82     def commands(self):
83         r"""commands() -> generator :: List the available commands."""
84         return (a for a in dir(self) if is_handler(getattr(self, a)))
85
86     @handler(u'Show available commands with their help.')
87     def help(self, command=None):
88         r"""help([command]) -> unicode/dict :: Show help on available commands.
89
90         If command is specified, it returns the help of that particular command.
91         If not, it returns a dictionary which keys are the available commands
92         and values are the help strings.
93         """
94         if command is None:
95             return dict((a, get_help(getattr(self, a)))
96                         for a in dir(self) if is_handler(getattr(self, a)))
97         if not hasattr(self, command):
98             raise CommandNotFoundError(command)
99         handler = getattr(self, command)
100         if not is_handler(handler):
101             raise CommandNotFoundError(command)
102         return get_help(handler)
103
104 class Dispatcher:
105     r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
106
107     This class provides a modular and extensible dispatching mechanism. You
108     can specify root 'routes' (as a dict where the key is the string of the
109     root command and the value is a callable object to handle that command,
110     or a subcommand if the callable is an instance and the command can be
111     sub-routed).
112
113     The command can have arguments, separated by (any number of) spaces.
114
115     The dispatcher tries to route the command as deeply as it can, passing
116     the other "path" components as arguments to the callable. To route the
117     command it inspects the callable attributes to find a suitable callable
118     attribute to handle the command in a more specific way, and so on.
119
120     Example:
121     >>> d = Dispatcher(dict(handler=some_handler))
122     >>> d.dispatch('handler attribute method arg1 arg2')
123
124     If 'some_handler' is an object with an 'attribute' that is another
125     object which has a method named 'method', then
126     some_handler.attribute.method('arg1', 'arg2') will be called. If
127     some_handler is a function, then some_handler('attribute', 'method',
128     'arg1', 'arg2') will be called. The handler "tree" can be as complex
129     and deep as you want.
130
131     If some command can't be dispatched (because there is no root handler or
132     there is no matching callable attribute), a CommandNotFoundError is raised.
133     """
134
135     def __init__(self, routes=dict()):
136         r"""Initialize the Dispatcher object.
137
138         See Dispatcher class documentation for more info.
139         """
140         self.routes = routes
141
142     def dispatch(self, route):
143         r"""dispatch(route) -> None :: Dispatch a command string.
144
145         This method searches for a suitable callable object in the routes
146         "tree" and call it, or raises a CommandNotFoundError if the command
147         can't be dispatched.
148         """
149         command = list()
150         route = route.split() # TODO support "" and keyword arguments
151         if not route:
152             raise CommandNotFoundError(command)
153         command.append(route[0])
154         handler = self.routes.get(route[0], None)
155         if handler is None:
156             raise CommandNotFoundError(command)
157         route = route[1:]
158         while not is_handler(handler):
159             if len(route) is 0:
160                 raise CommandNotFoundError(command)
161             command.append(route[0])
162             if not hasattr(handler, route[0]):
163                 raise CommandNotFoundError(command)
164             handler = getattr(handler, route[0])
165             route = route[1:]
166         return handler(*route)
167
168
169 if __name__ == '__main__':
170
171     @handler(u"test: Print all the arguments, return nothing.")
172     def test_func(*args):
173         print 'func:', args
174
175     class TestClassSubHandler(Handler):
176         @handler(u"subcmd: Print all the arguments, return nothing.")
177         def subcmd(self, *args):
178             print 'class.subclass.subcmd:', args
179
180     class TestClass(Handler):
181         @handler(u"cmd1: Print all the arguments, return nothing.")
182         def cmd1(self, *args):
183             print 'class.cmd1:', args
184         @handler(u"cmd2: Print all the arguments, return nothing.")
185         def cmd2(self, *args):
186             print 'class.cmd2:', args
187         subclass = TestClassSubHandler()
188
189     test_class = TestClass()
190
191     d = Dispatcher(dict(
192             func=test_func,
193             inst=test_class,
194     ))
195
196     d.dispatch('func arg1 arg2 arg3')
197     print 'inst commands:', tuple(d.dispatch('inst commands'))
198     print 'inst help:', d.dispatch('inst help')
199     d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
200     d.dispatch('inst cmd2 arg1 arg2')
201     print 'inst subclass help:', d.dispatch('inst subclass help')
202     d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
203     try:
204         d.dispatch('')
205     except CommandNotFoundError, e:
206         print 'Not found:', e
207     try:
208         d.dispatch('sucutrule piquete culete')
209     except CommandNotFoundError, e:
210         print 'Not found:', e
211     try:
212         d.dispatch('inst cmd3 arg1 arg2 arg3')
213     except CommandNotFoundError, e:
214         print 'Not found:', e
215