0e76e815001cc1026784ca071850645a90f5489b
[hooke.git] / hooke / util / callback.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Define the `@callback` decorator.
20
21 See :pep:`318` for an introduction to decorators.
22 """
23
24 import logging
25
26 from .caller import caller_name
27
28
29 def is_iterable(x):
30     """Return `True` if `x` is iterable.
31
32     Examples
33     --------
34     >>> is_iterable('abc')
35     True
36     >>> is_iterable((1,2,3))
37     True
38     >>> is_iterable(5)
39     False
40     >>> def c():
41     ...     for i in range(5):
42     ...         yield i
43     >>> is_iterable(c())
44     True
45     """
46     try:
47         iter(x)
48         return True
49     except TypeError:
50         return False
51
52 def callback(method):
53     """Enable callbacks on `method`.
54
55     This decorator should make it easy to setup callbacks in a rich
56     GUI.  You only need to decorate potential hooks, and maintain a
57     single dict with all the callbacks for the class.  This beats
58     passing each of the callbacks into the class' `__init__` function
59     individually.
60
61     Examples
62     --------
63
64     Callbacks are called with the class instance, method instance, and
65     returned arguments of the method they're attached to.
66
67     >>> def c(self, method, *args):
68     ...     print '\\n  '.join([
69     ...             'callback:',
70     ...             'class:    %s' % self,
71     ...             'method:   %s' % method,
72     ...             'returned: %s' % args])
73
74     For some class, decorate any functions you're interested in
75     attaching callbacks too.  Also, add a `_callbacks` attribute
76     holding the callbacks, keyed by function name.
77
78     >>> class X (object):
79     ...     def __init__(self):
80     ...         self._callbacks = {'xyz': c}
81     ...
82     ...     @callback
83     ...     def xyz(self):
84     ...         "xyz's docstring"
85     ...         print 'usual xyz business'
86     ...         return (0, 1, 1, 2, 3, 5)
87     ...
88     ...     @callback
89     ...     def abc(self):
90     ...         "abc's docstring"
91     ...         print 'usual abc business'
92     ...
93     >>> x = X()
94
95     Here's our callback on `xyz`.
96
97     >>> r = x.xyz()  # doctest: +ELLIPSIS
98     usual xyz business
99     callback:
100       class:    <hooke.util.callback.X object at 0x...>
101       method:   <bound method X.xyz of <hooke.util.callback.X object at 0x...>>
102       returned: (0, 1, 1, 2, 3, 5)
103     >>> r
104     (0, 1, 1, 2, 3, 5)
105
106     The decorated method preserves the original docstring.
107
108     >>> print x.xyz.__doc__
109     xyz's docstring
110
111     So far, we haven't attached a callback to `abc`.
112
113     >>> r = x.abc()
114     usual abc business
115
116     Now we attach the callback to `abc`.
117
118     >>> x._callbacks['abc'] = c
119     >>> r = x.abc()  # doctest: +ELLIPSIS
120     usual abc business
121     callback:
122       class:    <hooke.util.callback.X object at 0x...>
123       method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
124       returned: None
125
126     You can also place an iterable in the `_callbacks` dict to run an
127     array of callbacks in series.
128
129     >>> def d(self, method, *args):
130     ...     print 'callback d'
131     >>> x._callbacks['abc'] = [d, c, d]
132     >>> r = x.abc()  # doctest: +ELLIPSIS
133     usual abc business
134     callback d
135     callback:
136       class:    <hooke.util.callback.X object at 0x...>
137       method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
138       returned: None
139     callback d
140     """
141     def new_m(self, *args, **kwargs):
142         result = method(self, *args, **kwargs)
143         callback = self._callbacks.get(method.func_name, None)
144         mn = getattr(self, method.func_name)
145         log = logging.getLogger('hooke')
146         log.debug('callback: %s (%s) calling %s' % (method.func_name, mn, callback))
147         if is_iterable(callback):
148             for cb in callback:
149                 cb(self, mn, result)
150         elif callback != None:
151             callback(self, mn, result)
152         return result
153     new_m.func_name = method.func_name
154     new_m.func_doc = method.func_doc
155     new_m.original_method = method
156     return new_m
157
158 def in_callback(self, *args, **kwargs):
159     """Enable callbacks inside methods.
160
161     Sometimes :func:`callback` isn't granular enough.  This function
162     can accomplish the same thing from inside your method, giving you
163     control over the arguments passed and the time at which the call
164     is made.  It draws from the same `._callbacks` dictionary.
165
166     Examples
167     --------
168
169     Callbacks are called with the class instance, method instance, and
170     returned arguments of the method they're attached to.
171
172     >>> def c(self, method, *args, **kwargs):
173     ...     print '\\n  '.join([
174     ...             'callback:',
175     ...             'class:    %s' % self,
176     ...             'method:   %s' % method,
177     ...             'args:     %s' % (args,),
178     ...             'kwargs:   %s' % kwargs])
179
180     Now place `in_callback` calls inside any interesting methods.
181
182     >>> class X (object):
183     ...     def __init__(self):
184     ...         self._callbacks = {'xyz': c}
185     ...
186     ...     def xyz(self):
187     ...         "xyz's docstring"
188     ...         print 'usual xyz business'
189     ...         in_callback(self, 5, my_kw=17)
190     ...         return (0, 1, 1, 2, 3, 5)
191     ...
192     ...     def abc(self):
193     ...         "abc's docstring"
194     ...         in_callback(self, p1=3.14, p2=159)
195     ...         print 'usual abc business'
196     ...
197     >>> x = X()
198
199     Here's our callback in `xyz`.
200
201     >>> r = x.xyz()  # doctest: +ELLIPSIS
202     usual xyz business
203     callback:
204       class:    <hooke.util.callback.X object at 0x...>
205       method:   <bound method X.xyz of <hooke.util.callback.X object at 0x...>>
206       args:     (5,)
207       kwargs:   {'my_kw': 17}
208     >>> r
209     (0, 1, 1, 2, 3, 5)
210
211     Note that we haven't attached a callback to `abc`.
212
213     >>> r = x.abc()
214     usual abc business
215
216     Now we attach the callback to `abc`.
217
218     >>> x._callbacks['abc'] = c
219     >>> r = x.abc()  # doctest: +ELLIPSIS
220     callback:
221       class:    <hooke.util.callback.X object at 0x...>
222       method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
223       args:     ()
224       kwargs:   {'p2': 159, 'p1': 3.1400000000000001}
225     usual abc business
226
227     You can also place an iterable in the `_callbacks` dict to run an
228     array of callbacks in series.
229
230     >>> def d(self, method, *args, **kwargs):
231     ...     print 'callback d'
232     >>> x._callbacks['abc'] = [d, c, d]
233     >>> r = x.abc()  # doctest: +ELLIPSIS
234     callback d
235     callback:
236       class:    <hooke.util.callback.X object at 0x...>
237       method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
238       args:     ()
239       kwargs:   {'p2': 159, 'p1': 3.14...}
240     callback d
241     usual abc business
242     """
243     method_name = caller_name(depth=2)
244     callback = self._callbacks.get(method_name, None)
245     mn = getattr(self, method_name)
246     log = logging.getLogger('hooke')
247     log.debug('callback: %s (%s) calling %s' % (method_name, mn, callback))
248     if is_iterable(callback):
249         for cb in callback:
250             cb(self, mn, *args, **kwargs)
251     elif callback != None:
252         callback(self, mn, *args, **kwargs)