Added logging to hooke.util.callback's callback routines.
[hooke.git] / hooke / util / callback.py
index 865ff38aca14917c658af7d2406b1ef37268c1cf..0e76e815001cc1026784ca071850645a90f5489b 100644 (file)
@@ -1,10 +1,53 @@
-# Copyright
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of Hooke.
+#
+# Hooke is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Hooke is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Hooke.  If not, see
+# <http://www.gnu.org/licenses/>.
 
 """Define the `@callback` decorator.
 
 See :pep:`318` for an introduction to decorators.
 """
 
+import logging
+
+from .caller import caller_name
+
+
+def is_iterable(x):
+    """Return `True` if `x` is iterable.
+
+    Examples
+    --------
+    >>> is_iterable('abc')
+    True
+    >>> is_iterable((1,2,3))
+    True
+    >>> is_iterable(5)
+    False
+    >>> def c():
+    ...     for i in range(5):
+    ...         yield i
+    >>> is_iterable(c())
+    True
+    """
+    try:
+        iter(x)
+        return True
+    except TypeError:
+        return False
 
 def callback(method):
     """Enable callbacks on `method`.
@@ -60,7 +103,12 @@ def callback(method):
     >>> r
     (0, 1, 1, 2, 3, 5)
 
-    Note that we haven't attached a callback to `abc`.
+    The decorated method preserves the original docstring.
+
+    >>> print x.xyz.__doc__
+    xyz's docstring
+
+    So far, we haven't attached a callback to `abc`.
 
     >>> r = x.abc()
     usual abc business
@@ -77,6 +125,7 @@ def callback(method):
 
     You can also place an iterable in the `_callbacks` dict to run an
     array of callbacks in series.
+
     >>> def d(self, method, *args):
     ...     print 'callback d'
     >>> x._callbacks['abc'] = [d, c, d]
@@ -92,15 +141,112 @@ def callback(method):
     def new_m(self, *args, **kwargs):
         result = method(self, *args, **kwargs)
         callback = self._callbacks.get(method.func_name, None)
-        nm = getattr(self, method.func_name)
-        try:
+        mn = getattr(self, method.func_name)
+        log = logging.getLogger('hooke')
+        log.debug('callback: %s (%s) calling %s' % (method.func_name, mn, callback))
+        if is_iterable(callback):
             for cb in callback:
-                cb(self, nm, result)
-        except TypeError:
-            if callback != None:
-                callback(self, nm, result)
+                cb(self, mn, result)
+        elif callback != None:
+            callback(self, mn, result)
         return result
     new_m.func_name = method.func_name
     new_m.func_doc = method.func_doc
     new_m.original_method = method
     return new_m
+
+def in_callback(self, *args, **kwargs):
+    """Enable callbacks inside methods.
+
+    Sometimes :func:`callback` isn't granular enough.  This function
+    can accomplish the same thing from inside your method, giving you
+    control over the arguments passed and the time at which the call
+    is made.  It draws from the same `._callbacks` dictionary.
+
+    Examples
+    --------
+
+    Callbacks are called with the class instance, method instance, and
+    returned arguments of the method they're attached to.
+
+    >>> def c(self, method, *args, **kwargs):
+    ...     print '\\n  '.join([
+    ...             'callback:',
+    ...             'class:    %s' % self,
+    ...             'method:   %s' % method,
+    ...             'args:     %s' % (args,),
+    ...             'kwargs:   %s' % kwargs])
+
+    Now place `in_callback` calls inside any interesting methods.
+
+    >>> class X (object):
+    ...     def __init__(self):
+    ...         self._callbacks = {'xyz': c}
+    ...
+    ...     def xyz(self):
+    ...         "xyz's docstring"
+    ...         print 'usual xyz business'
+    ...         in_callback(self, 5, my_kw=17)
+    ...         return (0, 1, 1, 2, 3, 5)
+    ...
+    ...     def abc(self):
+    ...         "abc's docstring"
+    ...         in_callback(self, p1=3.14, p2=159)
+    ...         print 'usual abc business'
+    ...
+    >>> x = X()
+
+    Here's our callback in `xyz`.
+
+    >>> r = x.xyz()  # doctest: +ELLIPSIS
+    usual xyz business
+    callback:
+      class:    <hooke.util.callback.X object at 0x...>
+      method:   <bound method X.xyz of <hooke.util.callback.X object at 0x...>>
+      args:     (5,)
+      kwargs:   {'my_kw': 17}
+    >>> r
+    (0, 1, 1, 2, 3, 5)
+
+    Note that we haven't attached a callback to `abc`.
+
+    >>> r = x.abc()
+    usual abc business
+
+    Now we attach the callback to `abc`.
+
+    >>> x._callbacks['abc'] = c
+    >>> r = x.abc()  # doctest: +ELLIPSIS
+    callback:
+      class:    <hooke.util.callback.X object at 0x...>
+      method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
+      args:     ()
+      kwargs:   {'p2': 159, 'p1': 3.1400000000000001}
+    usual abc business
+
+    You can also place an iterable in the `_callbacks` dict to run an
+    array of callbacks in series.
+
+    >>> def d(self, method, *args, **kwargs):
+    ...     print 'callback d'
+    >>> x._callbacks['abc'] = [d, c, d]
+    >>> r = x.abc()  # doctest: +ELLIPSIS
+    callback d
+    callback:
+      class:    <hooke.util.callback.X object at 0x...>
+      method:   <bound method X.abc of <hooke.util.callback.X object at 0x...>>
+      args:     ()
+      kwargs:   {'p2': 159, 'p1': 3.14...}
+    callback d
+    usual abc business
+    """
+    method_name = caller_name(depth=2)
+    callback = self._callbacks.get(method_name, None)
+    mn = getattr(self, method_name)
+    log = logging.getLogger('hooke')
+    log.debug('callback: %s (%s) calling %s' % (method_name, mn, callback))
+    if is_iterable(callback):
+        for cb in callback:
+            cb(self, mn, *args, **kwargs)
+    elif callback != None:
+        callback(self, mn, *args, **kwargs)