diff options
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | decorators.py | 15 | ||||
-rw-r--r-- | test/unittest_decorators.py | 34 |
3 files changed, 50 insertions, 6 deletions
@@ -1,11 +1,10 @@ ChangeLog for logilab.common ============================ - -- - * modutils: allow overriding of _getobj by suppressing mangling - -- - * daemon: call initgroups/setgid before setuid + * daemon: call initgroups/setgid before setuid (closes #74173) + * decorators: @monkeypatch should produce a method object (closes #73920) + * modutils: allow overriding of _getobj by suppressing mangling 2011-08-05 -- 0.56.1 * clcommands: #72450 --rc-file option doesn't work diff --git a/decorators.py b/decorators.py index 39144d1..7bb08fc 100644 --- a/decorators.py +++ b/decorators.py @@ -209,7 +209,7 @@ def locked(acquire, release): def monkeypatch(klass, methodname=None): - """Decorator extending class with the decorated function + """Decorator extending class with the decorated callable >>> class A: ... pass >>> @monkeypatch(A) @@ -227,6 +227,17 @@ def monkeypatch(klass, methodname=None): 12 """ def decorator(func): - setattr(klass, methodname or func.__name__, func) + try: + name = methodname or func.__name__ + except AttributeError: + raise AttributeError('%s has no __name__ attribute: ' + 'you should provide an explicit `methodname`' + % func) + if callable(func): + setattr(klass, name, types.MethodType(func, None, klass)) + else: + # likely a property + # this is quite borderline but usage already in the wild ... + setattr(klass, name, func) return func return decorator diff --git a/test/unittest_decorators.py b/test/unittest_decorators.py index 75261c9..355e934 100644 --- a/test/unittest_decorators.py +++ b/test/unittest_decorators.py @@ -17,12 +17,46 @@ # with logilab-common. If not, see <http://www.gnu.org/licenses/>. """unit tests for the decorators module """ +import types from logilab.common.testlib import TestCase, unittest_main from logilab.common.decorators import monkeypatch, cached, clear_cache, copy_cache class DecoratorsTC(TestCase): + def test_monkeypatch_instance_method(self): + class MyClass: pass + @monkeypatch(MyClass) + def meth1(self): + return 12 + class XXX(object): + @monkeypatch(MyClass) + def meth2(self): + return 12 + self.assertTrue(isinstance(MyClass.meth1, types.MethodType)) + self.assertTrue(isinstance(MyClass.meth2, types.MethodType)) + + def test_monkeypatch_callable_non_callable(self): + tester = self + class MyClass: pass + @monkeypatch(MyClass, methodname='prop1') + @property + def meth1(self): + return 12 + class XXX(object): + def __call__(self, other): + tester.assertTrue(isinstance(other, MyClass)) + return 12 + try: + monkeypatch(MyClass)(XXX()) + except AttributeError, err: + self.assertTrue(str(err).endswith('has no __name__ attribute: you should provide an explicit `methodname`')) + monkeypatch(MyClass, 'foo')(XXX()) + self.assertTrue(isinstance(MyClass.prop1, property)) + self.assertTrue(callable(MyClass.foo)) + self.assertEqual(MyClass().prop1, 12) + self.assertEqual(MyClass().foo(), 12) + def test_monkeypatch_with_same_name(self): class MyClass: pass @monkeypatch(MyClass) |