]> git.llucax.com Git - software/pymin.git/blob - dispatcher.py
Update TODO list.
[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
22     def __init__(self, command):
23         r"""Initialize the Error object.
24
25         See Error class documentation for more info.
26         """
27         self.command = command
28
29     def __str__(self):
30         return 'Command not found: "%s"' % ' '.join(self.command)
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     pass
40
41 def handler(f):
42     f._dispatcher_handler = True
43     return f
44
45 def is_handler(handler):
46     return callable(handler) and hasattr(handler, '_dispatcher_handler') \
47             and handler._dispatcher_handler
48
49 class Dispatcher:
50     r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
51
52     This class provides a modular and extensible dispatching mechanism. You
53     can specify root 'routes' (as a dict where the key is the string of the
54     root command and the value is a callable object to handle that command,
55     or a subcommand if the callable is an instance and the command can be
56     sub-routed).
57
58     The command can have arguments, separated by (any number of) spaces.
59
60     The dispatcher tries to route the command as deeply as it can, passing
61     the other "path" components as arguments to the callable. To route the
62     command it inspects the callable attributes to find a suitable callable
63     attribute to handle the command in a more specific way, and so on.
64
65     Example:
66     >>> d = Dispatcher(dict(handler=some_handler))
67     >>> d.dispatch('handler attribute method arg1 arg2')
68
69     If 'some_handler' is an object with an 'attribute' that is another
70     object which has a method named 'method', then
71     some_handler.attribute.method('arg1', 'arg2') will be called. If
72     some_handler is a function, then some_handler('attribute', 'method',
73     'arg1', 'arg2') will be called. The handler "tree" can be as complex
74     and deep as you want.
75
76     If some command can't be dispatched (because there is no root handler or
77     there is no matching callable attribute), a CommandNotFoundError is raised.
78     """
79
80     def __init__(self, routes=dict()):
81         r"""Initialize the Dispatcher object.
82
83         See Dispatcher class documentation for more info.
84         """
85         self.routes = routes
86
87     def dispatch(self, route):
88         r"""dispatch(route) -> None :: Dispatch a command string.
89
90         This method searches for a suitable callable object in the routes
91         "tree" and call it, or raises a CommandNotFoundError if the command
92         can't be dispatched.
93         """
94         command = list()
95         route = route.split() # TODO support "" and keyword arguments
96         if not route:
97             raise CommandNotFoundError(command)
98         command.append(route[0])
99         handler = self.routes.get(route[0], None)
100         if handler is None:
101             raise CommandNotFoundError(command)
102         route = route[1:]
103         while not is_handler(handler):
104             if len(route) is 0:
105                 raise CommandNotFoundError(command)
106             command.append(route[0])
107             if not hasattr(handler, route[0]):
108                 raise CommandNotFoundError(command)
109             handler = getattr(handler, route[0])
110             route = route[1:]
111         return handler(*route)
112
113
114 if __name__ == '__main__':
115
116     @handler
117     def test_func(*args):
118           print 'func:', args
119
120     class TestClassSubHandler:
121         @handler
122         def subcmd(self, *args):
123             print 'class.subclass.subcmd:', args
124
125     class TestClass:
126         @handler
127         def cmd1(self, *args):
128             print 'class.cmd1:', args
129         @handler
130         def cmd2(self, *args):
131             print 'class.cmd2:', args
132         subclass = TestClassSubHandler()
133
134     d = Dispatcher(dict(
135             func=test_func,
136             inst=TestClass(),
137     ))
138
139     d.dispatch('func arg1 arg2 arg3')
140     d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
141     d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
142     try:
143         d.dispatch('')
144     except CommandNotFoundError, e:
145         print 'Not found:', e
146     try:
147         d.dispatch('sucutrule piquete culete')
148     except CommandNotFoundError, e:
149         print 'Not found:', e
150     try:
151         d.dispatch('inst cmd3 arg1 arg2 arg3')
152     except CommandNotFoundError, e:
153         print 'Not found:', e
154