[py3k @cached] fix compat of dark corners for the @monkeypatch decorator, making tests pass. Closes #104047

authorSylvain Thénault <sylvain.thenault@logilab.fr>
changeset8d13747da834
branchstable
phasepublic
hiddenno
parent revision#42985b1dff16 [decorators test] use assertIsInstance as expected
child revision#f8fb4a6d9249 py3k cached wip
files modified by this revision
decorators.py
test/unittest_decorators.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1350981176 -7200
# Tue Oct 23 10:32:56 2012 +0200
# Branch stable
# Node ID 8d13747da8344f78a8a25c757a4bdd41692ac2dc
# Parent 42985b1dff16372e73939c6cde5ca00b768cbf60
[py3k @cached] fix compat of dark corners for the @monkeypatch decorator, making tests pass. Closes #104047

diff --git a/decorators.py b/decorators.py
@@ -1,6 +1,6 @@
1 -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2 +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
3  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
4  #
5  # This file is part of logilab-common.
6  #
7  # logilab-common is free software: you can redistribute it and/or modify it under
@@ -17,10 +17,11 @@
8  # with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
9  """ A few useful function/method decorators. """
10  __docformat__ = "restructuredtext en"
11 
12  import sys
13 +import types
14  from time import clock, time
15 
16  from logilab.common.compat import callable, method_type
17 
18  # XXX rewrite so we can use the decorator syntax when keyarg has to be specified
@@ -271,13 +272,35 @@
19              name = methodname or func.__name__
20          except AttributeError:
21              raise AttributeError('%s has no __name__ attribute: '
22                                   'you should provide an explicit `methodname`'
23                                   % func)
24 -        if callable(func) and sys.version_info < (3, 0):
25 -            setattr(klass, name, method_type(func, None, klass))
26 +        if callable(func):
27 +            if sys.version_info < (3, 0):
28 +                setattr(klass, name, method_type(func, None, klass))
29 +            elif isinstance(func, types.FunctionType):
30 +                setattr(klass, name, func)
31 +            else:
32 +                setattr(klass, name, UnboundMethod(func))
33          else:
34              # likely a property
35              # this is quite borderline but usage already in the wild ...
36              setattr(klass, name, func)
37          return func
38      return decorator
39 +
40 +if sys.version_info >= (3, 0):
41 +    class UnboundMethod(object):
42 +        """unbound method wrapper necessary for python3 where we can't turn
43 +        arbitrary object (eg class implementing __call__) into a method, as
44 +        there is no more unbound method and only function are turned
45 +        automatically to method when accessed through an instance.
46 +        """
47 +        __slots__ = ('_callable',)
48 +
49 +        def __init__(self, callable):
50 +            self._callable = callable
51 +
52 +        def __get__(self, instance, objtype):
53 +            if instance is None:
54 +                return self._callable
55 +            return types.MethodType(self._callable, instance)
diff --git a/test/unittest_decorators.py b/test/unittest_decorators.py
@@ -15,10 +15,11 @@
56  #
57  # You should have received a copy of the GNU Lesser General Public License along
58  # with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
59  """unit tests for the decorators module
60  """
61 +import sys
62  import types
63 
64  from logilab.common.testlib import TestCase, unittest_main
65  from logilab.common.decorators import (monkeypatch, cached, clear_cache,
66                                         copy_cache, cachedproperty)
@@ -32,12 +33,19 @@
67              return 12
68          class XXX(object):
69              @monkeypatch(MyClass)
70              def meth2(self):
71                  return 12
72 -        self.assertTrue(isinstance(MyClass.meth1, types.MethodType))
73 -        self.assertTrue(isinstance(MyClass.meth2, types.MethodType))
74 +        if sys.version_info < (3, 0):
75 +            # with python3, unbound method are functions
76 +            self.assertIsInstance(MyClass.meth1, types.MethodType)
77 +            self.assertIsInstance(MyClass.meth2, types.MethodType)
78 +        else:
79 +            self.assertIsInstance(MyClass.meth1, types.FunctionType)
80 +            self.assertIsInstance(MyClass.meth2, types.FunctionType)
81 +        self.assertEqual(MyClass().meth1(), 12)
82 +        self.assertEqual(MyClass().meth2(), 12)
83 
84      def test_monkeypatch_callable_non_callable(self):
85          tester = self
86          class MyClass: pass
87          @monkeypatch(MyClass, methodname='prop1')