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