commit 4cc4ed3bb411feb458d54f34d18f41d8479005c8 Author: Tommy Wang Date: Thu Nov 13 14:11:45 2014 -0600 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1e689f --- /dev/null +++ b/.gitignore @@ -0,0 +1,99 @@ +# Created by https://www.gitignore.io + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +### vim ### +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst new file mode 100644 index 0000000..54f6704 --- /dev/null +++ b/DESCRIPTION.rst @@ -0,0 +1,8 @@ +Python 3.4 include a ``WeakMethod`` class, for storing bound methods using weak references +(see the `Python weakref module `_). + +This project is a backport of the WeakMethod class, and tests, for Python 2.6. The tests +require the `unittest2 package `_. + +* Github repository & issue tracker: + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b7f84d5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include DESCRIPTION.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..67e3469 --- /dev/null +++ b/README.rst @@ -0,0 +1,6 @@ +weakrefmethod +============= + +Backport of WeakMethod from Python 3.4 to Python 2.6+ + +`docs `_ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5196478 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[sdist] +force-manifest = 1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e1cca93 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup +from codecs import open +from os import path +from weakrefmethod import __version__ + +here = path.abspath(path.dirname(__file__)) +with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f: + long_description = f.read() + +URL = 'http://pypi.python.org/pypi/weakrefmethod' + +setup( + name='weakrefmethod', + version=__version__, + description='A WeakMethod class for storing bound methods using weak references.', + long_description=long_description, + py_modules=['weakrefmethod'], + author='Tommy Wang', + author_email='twang@august8.net', + license='PSF', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Python Software Foundation License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + keywords='weakref WeakMethod', + url='http://pypi.python.org/pypi/weakrefmethod', + tests_require=['unittest2'], + test_suite='test_weakmethod', +) diff --git a/test_weakmethod.py b/test_weakmethod.py new file mode 100644 index 0000000..c0ea81f --- /dev/null +++ b/test_weakmethod.py @@ -0,0 +1,163 @@ +import unittest2 as unittest +import gc +import weakref +import weakrefmethod + +class Object: + def __init__(self, arg): + self.arg = arg + def __repr__(self): + return "" % self.arg + def __eq__(self, other): + if isinstance(other, Object): + return self.arg == other.arg + return NotImplemented + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + return not result + def __lt__(self, other): + if isinstance(other, Object): + return self.arg < other.arg + return NotImplemented + def __hash__(self): + return hash(self.arg) + def some_method(self): + return 4 + def other_method(self): + return 5 + + +class WeakMethodTestCase(unittest.TestCase): + def _subclass(self): + """Return an Object subclass overriding `some_method`.""" + class C(Object): + def some_method(self): + return 6 + return C + + def test_alive(self): + o = Object(1) + r = weakrefmethod.WeakMethod(o.some_method) + self.assertIsInstance(r, weakref.ReferenceType) + self.assertIsInstance(r(), type(o.some_method)) + self.assertIs(r().__self__, o) + self.assertIs(r().__func__, o.some_method.__func__) + self.assertEqual(r()(), 4) + + def test_object_dead(self): + o = Object(1) + r = weakrefmethod.WeakMethod(o.some_method) + self.assertIsInstance(r, weakref.ReferenceType) + self.assertIsInstance(r(), type(o.some_method)) + self.assertIs(r().__self__, o) + self.assertIs(r().__func__, o.some_method.__func__) + self.assertEqual(r()(), 4) + + def test_method_dead(self): + C = self._subclass() + o = C(1) + r = weakrefmethod.WeakMethod(o.some_method) + del C.some_method + gc.collect() + self.assertIs(r(), None) + + def test_callback_when_object_dead(self): + # Test callback behavior when object dies first. + C = self._subclass() + calls = [] + def cb(arg): + calls.append(arg) + o = C(1) + r = weakrefmethod.WeakMethod(o.some_method, cb) + del o + gc.collect() + self.assertEqual(calls, [r]) + # Callback is only called once. + C.some_method = Object.some_method + gc.collect() + self.assertEqual(calls, [r]) + + def test_callback_when_method_dead(self): + # Test callback behavior when method dies first. + C = self._subclass() + calls = [] + def cb(arg): + calls.append(arg) + o = C(1) + r = weakrefmethod.WeakMethod(o.some_method, cb) + del C.some_method + gc.collect() + self.assertEqual(calls, [r]) + # Callback is only called once. + del o + gc.collect() + self.assertEqual(calls, [r]) + + def test_no_cycles(self): + # A WeakMethod doesn't create any reference cycle to itself. + o = Object(1) + def cb(_): + pass + r = weakrefmethod.WeakMethod(o.some_method, cb) + wr = weakref.ref(r) + del r + self.assertIs(wr(), None) + + def test_equality(self): + def _eq(a, b): + self.assertTrue(a == b) + self.assertFalse(a != b) + def _ne(a, b): + self.assertTrue(a != b) + self.assertFalse(a == b) + x = Object(1) + y = Object(1) + a = weakrefmethod.WeakMethod(x.some_method) + b = weakrefmethod.WeakMethod(y.some_method) + c = weakrefmethod.WeakMethod(x.other_method) + d = weakrefmethod.WeakMethod(y.other_method) + # Objects equal, same method + _eq(a, b) + _eq(c, d) + # Objects equal, different method + _ne(a, c) + _ne(a, d) + _ne(b, c) + _ne(b, d) + # Objects unequal, same or different method + z = Object(2) + e = weakrefmethod.WeakMethod(z.some_method) + f = weakrefmethod.WeakMethod(z.other_method) + _ne(a, e) + _ne(a, f) + _ne(b, e) + _ne(b, f) + del x, y, z + gc.collect() + # Dead WeakMethod compare by identity + refs = a, b, c, d, e, f + for q in refs: + for r in refs: + self.assertEqual(q == r, q is r) + self.assertEqual(q != r, q is not r) + + def test_hashing(self): + # Alive WeakMethods are hashable if the underlying object is + # hashable. + x = Object(1) + y = Object(1) + a = weakrefmethod.WeakMethod(x.some_method) + b = weakrefmethod.WeakMethod(y.some_method) + c = weakrefmethod.WeakMethod(y.other_method) + # Since WeakMethod objects are equal, the hashes should be equal. + self.assertEqual(hash(a), hash(b)) + ha = hash(a) + # Dead WeakMethods retain their old hash value + del x, y + gc.collect() + self.assertEqual(hash(a), ha) + self.assertEqual(hash(b), ha) + # If it wasn't hashed when alive, a dead WeakMethod cannot be hashed. + self.assertRaises(TypeError, hash, c) diff --git a/weakrefmethod.py b/weakrefmethod.py new file mode 100644 index 0000000..1b8e73b --- /dev/null +++ b/weakrefmethod.py @@ -0,0 +1,58 @@ +import weakref + +__all__ = ['WeakMethod'] + +__version__ = '1.0.0' + + +class WeakMethod(weakref.ref): + """ + A custom 'weakref.ref' subclass which simulates a weak reference to + a bound method, working around the lifetime problem of bound methods + """ + + __slots__ = '_func_ref', '_meth_type', '_alive', '__weakref__' + + def __new__(cls, meth, callback=None): + try: + obj = meth.__self__ + func = meth.__func__ + except AttributeError: + raise TypeError('argument should be a bound method, not {}'.format(type(meth))) + + def _cb(arg): + # The self-weakref trick is needed to avoid creating a reference cycle. + self = self_wr() + if self._alive: + self._alive = False + if callback is not None: + callback(self) + self = weakref.ref.__new__(cls, obj, _cb) + self._func_ref = weakref.ref(func, _cb) + self._meth_type = type(meth) + self._alive = True + self_wr = weakref.ref(self) + return self + + def __call__(self): + obj = super(WeakMethod, self).__call__() + func = self._func_ref() + if obj is None or func is None: + return None + return self._meth_type(func, obj) + + def __eq__(self, other): + if isinstance(other, WeakMethod): + if not self._alive or not other._alive: + return self is other + return weakref.ref.__eq__(self, other) and self._func_ref == other._func_ref + return False + + def __ne__(self, other): + if isinstance(other, WeakMethod): + if not self._alive or not other._alive: + return self is not other + return weakref.ref.__ne__(self, other) or self._func_ref != other._func_ref + return True + + __hash__ = weakref.ref.__hash__