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