Added in_callback() to hooke.util.callback and pulled out is_iterable().
authorW. Trevor King <wking@drexel.edu>
Sun, 25 Jul 2010 16:40:50 +0000 (12:40 -0400)
committerW. Trevor King <wking@drexel.edu>
Sun, 25 Jul 2010 16:40:50 +0000 (12:40 -0400)
Also added hooke.util.caller with caller_name() which is useful for
in_callback().

hooke/util/callback.py
hooke/util/caller.py [new file with mode: 0644]

index 865ff38aca14917c658af7d2406b1ef37268c1cf..ec7895c6569c48fa63148ec0a4b3dd1336231d8b 100644 (file)
@@ -5,6 +5,31 @@
 See :pep:`318` for an introduction to decorators.
 """
 
+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 +85,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 +107,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]
@@ -93,14 +124,107 @@ def callback(method):
         result = method(self, *args, **kwargs)
         callback = self._callbacks.get(method.func_name, None)
         nm = getattr(self, method.func_name)
-        try:
+        if is_iterable(callback):
             for cb in callback:
                 cb(self, nm, result)
-        except TypeError:
-            if callback != None:
-                callback(self, nm, result)
+        elif callback != None:
+            callback(self, nm, 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)
+    nm = getattr(self, method_name)
+    if is_iterable(callback):
+        for cb in callback:
+            cb(self, nm, *args, **kwargs)
+    elif callback != None:
+        callback(self, nm, *args, **kwargs)
diff --git a/hooke/util/caller.py b/hooke/util/caller.py
new file mode 100644 (file)
index 0000000..e2e592d
--- /dev/null
@@ -0,0 +1,64 @@
+# Copyright
+
+"""Define :func:`caller_name`.
+
+This is useful, for example, to declare the `@callback` decorator for
+making GUI writing less tedious.  See :mod:`hooke.util.callback` and
+:mod:`hooke.ui.gui` for examples.
+"""
+
+import sys
+
+
+def frame(depth=1):
+    """Return the frame for the function `depth` up the call stack.
+
+    Notes
+    -----
+    The `ZeroDivisionError` trick is from stdlib's traceback.py.  See
+    the Python Refrence Manual on `traceback objects`_ and `frame
+    objects`_.
+
+    .. _traceback objects:
+      http://docs.python.org/reference/datamodel.html#index-873
+    .. _frame objects:
+      http://docs.python.org/reference/datamodel.html#index-870
+    """
+    try:
+        raise ZeroDivisionError
+    except ZeroDivisionError:
+        traceback = sys.exc_info()[2]
+    f = traceback.tb_frame
+    for i in range(depth):
+        f = f.f_back
+    return f
+
+def caller_name(depth=1):
+    """Return the name of the function `depth` up the call stack.
+
+    Examples
+    --------
+
+    >>> def x(depth):
+    ...     y(depth)
+    >>> def y(depth):
+    ...     print caller_name(depth)
+    >>> x(1)
+    y
+    >>> x(2)
+    x
+    >>> x(0)
+    caller_name
+
+    Notes
+    -----
+    See the Python Refrence manual on `frame objects`_ and
+    `code objects`_.
+
+    .. _frame objects:
+      http://docs.python.org/reference/datamodel.html#index-870
+    .. _code objects:
+      http://docs.python.org/reference/datamodel.html#index-866
+    """
+    f = frame(depth=depth+1)
+    return f.f_code.co_name