diff options
author | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2013-01-21 18:33:04 +0100 |
---|---|---|
committer | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2013-01-21 18:33:04 +0100 |
commit | 0a0ba64e9b8f6720c96faa3fa34735ddbc758caf (patch) | |
tree | 1154c454cb04bba5165b7a68bb6834da77e42fcf | |
parent | 0d6e3833d6bdc9f48f75abe07a8ad457349aa6ca (diff) | |
download | logilab-common-0a0ba64e9b8f6720c96faa3fa34735ddbc758caf.tar.gz |
Properly fix @monkeypatch by changing its contract. Actually closes #104047logilab-common-version-0.59.0
This follows 8d13747da834 and f8fb4a6d9249 which should not have been commited
before this cleanup.
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | decorators.py | 36 | ||||
-rw-r--r-- | test/unittest_decorators.py | 32 |
3 files changed, 27 insertions, 48 deletions
@@ -21,7 +21,7 @@ ChangeLog for logilab.common - use register_all when no registration callback defined (closes #111011) - * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436) + * logging_ext: on windows, use colorama to display colored logs, if available (closes #107436) * packaging: remove references to ftp at logilab @@ -35,7 +35,10 @@ ChangeLog for logilab.common * configuration: enhance merge_options function (closes #113458) - + * decorators: fix @monkeypatch decorator contract for dark corner + cases such as monkeypatching of a callable instance: no more + turned into an unbound method, which was broken in python 3 and + probably not used anywhere (actually closes #104047). 2012-11-14 -- 0.58.3 * date: fix ustrftime() impl. for python3 (closes #82161, patch by Arfrever diff --git a/decorators.py b/decorators.py index 5b21cd1..34bbd3a 100644 --- a/decorators.py +++ b/decorators.py @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. @@ -250,7 +250,9 @@ def locked(acquire, release): def monkeypatch(klass, methodname=None): - """Decorator extending class with the decorated callable + """Decorator extending class with the decorated callable. This is basically + a syntactic sugar vs class assignment. + >>> class A: ... pass >>> @monkeypatch(A) @@ -274,34 +276,6 @@ def monkeypatch(klass, methodname=None): raise AttributeError('%s has no __name__ attribute: ' 'you should provide an explicit `methodname`' % func) - if callable(func): - if sys.version_info < (3, 0): - setattr(klass, name, method_type(func, None, klass)) - #elif isinstance(func, types.FunctionType): - else: - setattr(klass, name, func) - #else: - # setattr(klass, name, UnboundMethod(func)) - else: - # likely a property - # this is quite borderline but usage already in the wild ... - setattr(klass, name, func) + setattr(klass, name, func) return func return decorator - -if sys.version_info >= (3, 0): - class UnboundMethod(object): - """unbound method wrapper necessary for python3 where we can't turn - arbitrary object (eg class implementing __call__) into a method, as - there is no more unbound method and only function are turned - automatically to method when accessed through an instance. - """ - __slots__ = ('_callable',) - - def __init__(self, callable): - self._callable = callable - - def __get__(self, instance, objtype): - if instance is None: - return self._callable - return types.MethodType(self._callable, instance) diff --git a/test/unittest_decorators.py b/test/unittest_decorators.py index f532890..688d837 100644 --- a/test/unittest_decorators.py +++ b/test/unittest_decorators.py @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. @@ -45,26 +45,28 @@ class DecoratorsTC(TestCase): self.assertEqual(MyClass().meth1(), 12) self.assertEqual(MyClass().meth2(), 12) - def test_monkeypatch_callable_non_callable(self): - tester = self + def test_monkeypatch_property(self): class MyClass: pass @monkeypatch(MyClass, methodname='prop1') @property def meth1(self): return 12 - # class XXX(object): - # def __call__(self, other): - # tester.assertIsInstance(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.assertIsInstance(MyClass.prop1, property) - # self.assertTrue(callable(MyClass.foo)) + self.assertIsInstance(MyClass.prop1, property) self.assertEqual(MyClass().prop1, 12) - # self.assertEqual(MyClass().foo(), 12) + + def test_monkeypatch_arbitrary_callable(self): + class MyClass: pass + class ArbitraryCallable(object): + def __call__(self): + return 12 + # ensure it complains about missing __name__ + with self.assertRaises(AttributeError) as cm: + monkeypatch(MyClass)(ArbitraryCallable()) + self.assertTrue(str(cm.exception).endswith('has no __name__ attribute: you should provide an explicit `methodname`')) + # ensure no black magic under the hood + monkeypatch(MyClass, 'foo')(ArbitraryCallable()) + self.assertTrue(callable(MyClass.foo)) + self.assertEqual(MyClass().foo(), 12) def test_monkeypatch_with_same_name(self): class MyClass: pass |