Properly fix @monkeypatch by changing its contract. Actually closes #104047

This follows 8d13747da834 and f8fb4a6d9249 which should not have been commited before this cleanup.

authorSylvain Thénault <sylvain.thenault@logilab.fr>
changeseta93679c86bfd
branchstable
phasepublic
hiddenno
parent revision#9f1a4c1593ee Added tag logilab-common-version-0.59.0 for changeset 555eda2edcc1
child revision#60a9c75910c3 Added tag logilab-common-version-0.59.0 for changeset a93679c86bfd
files modified by this revision
ChangeLog
decorators.py
test/unittest_decorators.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1358789584 -3600
# Mon Jan 21 18:33:04 2013 +0100
# Branch stable
# Node ID a93679c86bfd724bea7aeff098af51c18b6234c4
# Parent 9f1a4c1593eecf26ceec88d2126535613f6e27a5
Properly fix @monkeypatch by changing its contract. Actually closes #104047

This follows 8d13747da834 and f8fb4a6d9249 which should not have been commited
before this cleanup.

diff --git a/ChangeLog b/ChangeLog
@@ -19,11 +19,11 @@
1          __abstract__ explicitly is better and notion of registered object 'name'
2          is now somewhat fuzzy
3 
4        - use register_all when no registration callback defined (closes #111011)
5 
6 -    * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436)
7 +    * logging_ext: on windows, use colorama to display colored logs, if available (closes #107436)
8 
9      * packaging: remove references to ftp at logilab
10 
11      * deprecations: really check them
12 
@@ -33,11 +33,14 @@
13 
14      * packaging Update download and project urls (closes #113099)
15 
16      * configuration: enhance merge_options function (closes #113458)
17 
18 -
19 +    * decorators: fix @monkeypatch decorator contract for dark corner
20 +      cases such as monkeypatching of a callable instance: no more
21 +      turned into an unbound method, which was broken in python 3 and
22 +      probably not used anywhere (actually closes #104047).
23 
24  2012-11-14  --  0.58.3
25      * date: fix ustrftime() impl. for python3 (closes #82161, patch by Arfrever
26        Frehtes Taifersar Arahesis) and encoding detection for python2 (closes
27        #109740)
diff --git a/decorators.py b/decorators.py
@@ -1,6 +1,6 @@
28 -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
29 +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
30  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
31  #
32  # This file is part of logilab-common.
33  #
34  # logilab-common is free software: you can redistribute it and/or modify it under
@@ -248,11 +248,13 @@
35          return wrapper
36      return decorator
37 
38 
39  def monkeypatch(klass, methodname=None):
40 -    """Decorator extending class with the decorated callable
41 +    """Decorator extending class with the decorated callable. This is basically
42 +    a syntactic sugar vs class assignment.
43 +
44      >>> class A:
45      ...     pass
46      >>> @monkeypatch(A)
47      ... def meth(self):
48      ...     return 12
@@ -272,36 +274,8 @@
49              name = methodname or func.__name__
50          except AttributeError:
51              raise AttributeError('%s has no __name__ attribute: '
52                                   'you should provide an explicit `methodname`'
53                                   % func)
54 -        if callable(func):
55 -            if sys.version_info < (3, 0):
56 -                setattr(klass, name, method_type(func, None, klass))
57 -            #elif  isinstance(func, types.FunctionType):
58 -            else:
59 -                setattr(klass, name, func)
60 -            #else:
61 -            #    setattr(klass, name, UnboundMethod(func))
62 -        else:
63 -            # likely a property
64 -            # this is quite borderline but usage already in the wild ...
65 -            setattr(klass, name, func)
66 +        setattr(klass, name, func)
67          return func
68      return decorator
69 -
70 -if sys.version_info >= (3, 0):
71 -    class UnboundMethod(object):
72 -        """unbound method wrapper necessary for python3 where we can't turn
73 -        arbitrary object (eg class implementing __call__) into a method, as
74 -        there is no more unbound method and only function are turned
75 -        automatically to method when accessed through an instance.
76 -        """
77 -        __slots__ = ('_callable',)
78 -
79 -        def __init__(self, callable):
80 -            self._callable = callable
81 -
82 -        def __get__(self, instance, objtype):
83 -            if instance is None:
84 -                return self._callable
85 -            return types.MethodType(self._callable, instance)
diff --git a/test/unittest_decorators.py b/test/unittest_decorators.py
@@ -1,6 +1,6 @@
86 -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
87 +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
88  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
89  #
90  # This file is part of logilab-common.
91  #
92  # logilab-common is free software: you can redistribute it and/or modify it under
@@ -43,30 +43,32 @@
93              self.assertIsInstance(MyClass.meth1, types.FunctionType)
94              self.assertIsInstance(MyClass.meth2, types.FunctionType)
95          self.assertEqual(MyClass().meth1(), 12)
96          self.assertEqual(MyClass().meth2(), 12)
97 
98 -    def test_monkeypatch_callable_non_callable(self):
99 -        tester = self
100 +    def test_monkeypatch_property(self):
101          class MyClass: pass
102          @monkeypatch(MyClass, methodname='prop1')
103          @property
104          def meth1(self):
105              return 12
106 -        # class XXX(object):
107 -        #     def __call__(self, other):
108 -        #         tester.assertIsInstance(other, MyClass)
109 -        #         return 12
110 -        # try:
111 -        #     monkeypatch(MyClass)(XXX())
112 -        # except AttributeError, err:
113 -        #     self.assertTrue(str(err).endswith('has no __name__ attribute: you should provide an explicit `methodname`'))
114 -        # monkeypatch(MyClass, 'foo')(XXX())
115 -        # self.assertIsInstance(MyClass.prop1, property)
116 -        # self.assertTrue(callable(MyClass.foo))
117 +        self.assertIsInstance(MyClass.prop1, property)
118          self.assertEqual(MyClass().prop1, 12)
119 -        # self.assertEqual(MyClass().foo(), 12)
120 +
121 +    def test_monkeypatch_arbitrary_callable(self):
122 +        class MyClass: pass
123 +        class ArbitraryCallable(object):
124 +            def __call__(self):
125 +                return 12
126 +        # ensure it complains about missing __name__
127 +        with self.assertRaises(AttributeError) as cm:
128 +            monkeypatch(MyClass)(ArbitraryCallable())
129 +        self.assertTrue(str(cm.exception).endswith('has no __name__ attribute: you should provide an explicit `methodname`'))
130 +        # ensure no black magic under the hood
131 +        monkeypatch(MyClass, 'foo')(ArbitraryCallable())
132 +        self.assertTrue(callable(MyClass.foo))
133 +        self.assertEqual(MyClass().foo(), 12)
134 
135      def test_monkeypatch_with_same_name(self):
136          class MyClass: pass
137          @monkeypatch(MyClass)
138          def meth1(self):