summaryrefslogtreecommitdiff
blob: c3e85e09b660d53d8351cfc888b157d81ef8a8e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
From 33708e76578c173333d1879a4a21baddf8fcdb6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny@gentoo.org>
Date: Fri, 29 May 2020 16:06:07 +0200
Subject: [PATCH] Update for fixed outer @classmethod behavior in Python 3.9

Fixes #160
---
 docs/decorators.rst             | 18 ++++++-------
 tests/test_outer_classmethod.py | 45 +++++++++++++++++++++------------
 tests/test_synchronized_lock.py | 22 ++++++++--------
 3 files changed, 49 insertions(+), 36 deletions(-)

diff --git a/docs/decorators.rst b/docs/decorators.rst
index b8200d6..94201de 100644
--- a/docs/decorators.rst
+++ b/docs/decorators.rst
@@ -641,15 +641,15 @@ When calling the wrapped function in the decorator wrapper function, the
 instance is already bound to ``wrapped`` and will be passed automatically
 as the first argument to the original wrapped function.
 
-Note that due to a bug in Python ``classmethod.__get__()``, whereby it does
-not apply the descriptor protocol to the function wrapped by ``@classmethod``,
-the above only applies where the decorator wraps the ``@classmethod``
-decorator. If the decorator is placed inside of the ``@classmethod``
-decorator, then ``instance`` will be ``None`` and the decorator wrapper
-function will see the call as being the same as a normal function. As a
-result, always place any decorator outside of the ``@classmethod``
-decorator. Hopefully this issue in Python can be addressed in a future
-Python version.
+Note that due to a bug in Python prior to 3.9 ``classmethod.__get__()``,
+whereby it does not apply the descriptor protocol to the function
+wrapped by ``@classmethod``, the above only applies where the decorator
+wraps the ``@classmethod`` decorator. If the decorator is placed inside
+of the ``@classmethod`` decorator, then ``instance`` will be ``None``
+and the decorator wrapper function will see the call as being the same
+as a normal function. As a result, always place any decorator outside of
+the ``@classmethod`` decorator if you need to support earlier Python
+versions.
 
 Decorating Static Methods
 -------------------------
diff --git a/tests/test_outer_classmethod.py b/tests/test_outer_classmethod.py
index 6b4af4f..9c2fcb8 100644
--- a/tests/test_outer_classmethod.py
+++ b/tests/test_outer_classmethod.py
@@ -3,6 +3,7 @@ from __future__ import print_function
 import unittest
 import inspect
 import imp
+import sys
 
 import wrapt
 
@@ -121,20 +122,26 @@ class TestNamingOuterClassMethod(unittest.TestCase):
 class TestCallingOuterClassMethod(unittest.TestCase):
 
     def test_class_call_function(self):
-        # Test calling classmethod. The instance and class passed to the
-        # wrapper will both be None because our decorator is surrounded
-        # by the classmethod decorator. The classmethod decorator
-        # doesn't bind the method and treats it like a normal function,
-        # explicitly passing the class as the first argument with the
-        # actual arguments following that.
+        # Test calling classmethod. In Python 3.9, the class will be
+        # passed as instance.  In older versions of Python, the instance
+        # and class passed to the wrapper will both be None because our
+        # decorator is surrounded by the classmethod decorator.
+        # The classmethod decorator doesn't bind the method and treats
+        # it like a normal function, explicitly passing the class
+        # as the first argument with the actual arguments following
+        # that.
 
         _args = (1, 2)
         _kwargs = {'one': 1, 'two': 2}
 
         @wrapt.decorator
         def _decorator(wrapped, instance, args, kwargs):
-            self.assertEqual(instance, None)
-            self.assertEqual(args, (Class,)+_args)
+            if sys.hexversion >= 0x03090000:
+                self.assertEqual(instance, Class)
+                self.assertEqual(args, _args)
+            else:
+                self.assertEqual(instance, None)
+                self.assertEqual(args, (Class,)+_args)
             self.assertEqual(kwargs, _kwargs)
             self.assertEqual(wrapped.__module__, _function.__module__)
             self.assertEqual(wrapped.__name__, _function.__name__)
@@ -155,20 +162,26 @@ class TestCallingOuterClassMethod(unittest.TestCase):
         self.assertEqual(result, (_args, _kwargs))
 
     def test_instance_call_function(self):
-        # Test calling classmethod via class instance. The instance
-        # and class passed to the wrapper will both be None because our
-        # decorator is surrounded by the classmethod decorator. The
-        # classmethod decorator doesn't bind the method and treats it
-        # like a normal function, explicitly passing the class as the
-        # first argument with the actual arguments following that.
+        # Test calling classmethod via class instance. In Python 3.9,
+        # the class will be passed as instance.  In older versions
+        # of Python, the instance and class passed to the wrapper will
+        # both be None because our decorator is surrounded
+        # by the classmethod decorator. The classmethod decorator
+        # doesn't bind the method and treats it like a normal function,
+        # explicitly passing the class as the first argument with
+        # the actual arguments following that.
 
         _args = (1, 2)
         _kwargs = {'one': 1, 'two': 2}
 
         @wrapt.decorator
         def _decorator(wrapped, instance, args, kwargs):
-            self.assertEqual(instance, None)
-            self.assertEqual(args, (Class,)+_args)
+            if sys.hexversion >= 0x03090000:
+                self.assertEqual(instance, Class)
+                self.assertEqual(args, _args)
+            else:
+                self.assertEqual(instance, None)
+                self.assertEqual(args, (Class,)+_args)
             self.assertEqual(kwargs, _kwargs)
             self.assertEqual(wrapped.__module__, _function.__module__)
             self.assertEqual(wrapped.__name__, _function.__name__)
diff --git a/tests/test_synchronized_lock.py b/tests/test_synchronized_lock.py
index 6e7eb12..b8f60f3 100644
--- a/tests/test_synchronized_lock.py
+++ b/tests/test_synchronized_lock.py
@@ -1,5 +1,6 @@
 from __future__ import print_function
 
+import sys
 import unittest
 
 import wrapt
@@ -157,34 +158,33 @@ class TestSynchronized(unittest.TestCase):
         self.assertEqual(_lock3, _lock2)
 
     def test_synchronized_outer_classmethod(self):
-        # XXX If all was good, this would be detected as a class
+        # Bug in Python < 3.9:
+        # If all was good, this would be detected as a class
         # method call, but the classmethod decorator doesn't bind
         # the wrapped function to the class before calling and
         # just calls it direct, explicitly passing the class as
-        # first argument. This screws things up. Would be nice if
-        # Python were fixed, but that isn't likely to happen.
+        # first argument. This screws things up.
 
-        #_lock0 = getattr(C4, '_synchronized_lock', None)
-        _lock0 = getattr(C4.function2, '_synchronized_lock', None)
+        lock_target = (C4 if sys.hexversion >= 0x03090000
+                       else C4.function2)
+
+        _lock0 = getattr(lock_target, '_synchronized_lock', None)
         self.assertEqual(_lock0, None)
 
         c4.function2()
 
-        #_lock1 = getattr(C4, '_synchronized_lock', None)
-        _lock1 = getattr(C4.function2, '_synchronized_lock', None)
+        _lock1 = getattr(lock_target, '_synchronized_lock', None)
         self.assertNotEqual(_lock1, None)
 
         C4.function2()
 
-        #_lock2 = getattr(C4, '_synchronized_lock', None)
-        _lock2 = getattr(C4.function2, '_synchronized_lock', None)
+        _lock2 = getattr(lock_target, '_synchronized_lock', None)
         self.assertNotEqual(_lock2, None)
         self.assertEqual(_lock2, _lock1)
 
         C4.function2()
 
-        #_lock3 = getattr(C4, '_synchronized_lock', None)
-        _lock3 = getattr(C4.function2, '_synchronized_lock', None)
+        _lock3 = getattr(lock_target, '_synchronized_lock', None)
         self.assertNotEqual(_lock3, None)
         self.assertEqual(_lock3, _lock2)
 
-- 
2.26.2