From 54fc92c9bfd9471350c8ef4ceb45bc1139d77597 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 13 Sep 2013 16:39:11 +1000 Subject: [PATCH] Added a weakref proxy implementation that works with both normal functions and instance methods. --- src/__init__.py | 2 +- src/wrappers.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 2356f7a..dfdf254 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,5 @@ __version_info__ = ('1', '1', '0') __version__ = '.'.join(__version_info__) -from .wrappers import ObjectProxy, FunctionWrapper +from .wrappers import ObjectProxy, FunctionWrapper, WeakFunctionProxy from .decorators import decorator diff --git a/src/wrappers.py b/src/wrappers.py index f6fbe49..d1e35c8 100644 --- a/src/wrappers.py +++ b/src/wrappers.py @@ -2,6 +2,7 @@ from . import six import functools import operator +import weakref class _ObjectProxyMethods(object): @@ -511,3 +512,66 @@ try: from ._wrappers import ObjectProxy, FunctionWrapper except ImportError: pass + +def _weak_function_proxy_callback(ref, self, callback): + if self._self_expired: + return + + self._self_expired = True + + # This could raise an exception. We let it propagate back and let + # the weakref.proxy() deal with it, at which point it generally + # prints out a short error message direct to stderr and keeps going. + + if callback is not None: + callback(self) + +class WeakFunctionProxy(ObjectProxy): + + def __init__(self, wrapped, callback=None): + # We need to determine if the wrapped function is actually a + # bound method. In the case of a bound method, we need to keep a + # reference to the original unbound function and the instance. + # This is necessary because if we hold a reference to the bound + # function, it will be the only reference and given it is a + # temporary object, it will almost immediately expire and + # the weakref callback triggered. So what is done is that we + # hold a reference to the instance and unbound function and + # when called bind the function to the instance once again and + # then call it. Note that we avoid using a nested function for + # the callback here so as not to cause any odd reference cycles. + + _callback = callback and functools.partial( + _weak_function_proxy_callback, self=self, callback=callback) + + self._self_expired = False + + try: + self._self_instance = weakref.proxy(wrapped.__self__, _callback) + + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped.__func__, _callback)) + + except AttributeError: + self._self_instance = None + + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped, _callback)) + + def __call__(self, *args, **kwargs): + # We perform a boolean check here on the instance and wrapped + # function as that will trigger the reference error prior to + # calling if the reference had expired. + + instance = self._self_instance and self._self_instance + function = self._self_wrapped and self._self_wrapped + + # If the wrapped function was originally a bound function, for + # which we retained a reference to the instance and the unbound + # function we need to rebind the function and then call it. If + # not just called the wrapped function. + + if instance is None: + return self._self_wrapped(*args, **kwargs) + + return function.__get__(instance)(*args, **kwargs)