From 246feda8324f79709af3aa2619ae15844c80fa3d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 25 Jul 2010 12:40:50 -0400 Subject: [PATCH] Added in_callback() to hooke.util.callback and pulled out is_iterable(). Also added hooke.util.caller with caller_name() which is useful for in_callback(). --- hooke/util/callback.py | 134 +++++++++++++++++++++++++++++++++++++++-- hooke/util/caller.py | 64 ++++++++++++++++++++ 2 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 hooke/util/caller.py diff --git a/hooke/util/callback.py b/hooke/util/callback.py index 865ff38..ec7895c 100644 --- a/hooke/util/callback.py +++ b/hooke/util/callback.py @@ -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: + method: > + 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: + method: > + 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: + method: > + 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 index 0000000..e2e592d --- /dev/null +++ b/hooke/util/caller.py @@ -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 -- 2.26.2