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