1- """Tests for ``FunctionWrapper.__get__`` with native C descriptors (slot
2- wrappers, method descriptors, getset descriptors, member descriptors) and
3- Python descriptors (property, classmethod, staticmethod) at both class-level
4- and instance-level access.
1+ """Tests for ``FunctionWrapper.__get__`` against the full range of descriptor
2+ types found in CPython: the native C descriptor types (slot wrappers, method
3+ descriptors, classmethod descriptors, getset descriptors, member descriptors)
4+ as well as the Python-level descriptors (property, classmethod, staticmethod,
5+ and user-defined descriptors). Each is exercised at class-level access (where
6+ the C descriptor protocol passes ``NULL`` for the instance argument of the
7+ ``tp_descr_get`` slot) and at instance-level access where applicable.
8+
9+ These cases verify that ``FunctionWrapper.__get__`` forwards the instance
10+ argument it receives through to the wrapped descriptor's ``__get__`` slot
11+ without modification. Native CPython descriptor types other than the
12+ descriptor for ordinary Python functions distinguish between ``NULL`` and
13+ ``Py_None`` for the instance argument and raise ``TypeError`` if ``Py_None``
14+ is passed where ``NULL`` is expected, so any future change which substitutes
15+ ``Py_None`` for ``NULL`` before invoking the wrapped descriptor is detected
16+ here. The same tests are executed against the C and pure Python wrapper
17+ implementations.
518"""
619
720import io
1225import _pickle
1326
1427import wrapt
15- import wrapt .__wrapt__
16-
17- # ``wrapt.FunctionWrapper`` resolves to the C type when the extension is
18- # loaded, so the pure-Python class is only reachable via ``wrapt.wrappers``.
19- from wrapt .wrappers import FunctionWrapper as PurePythonFunctionWrapper
20-
21- USING_C_EXTENSION = wrapt .__wrapt__ ._using_c_extension
22-
23- c_extension_only = unittest .skipUnless (
24- USING_C_EXTENSION ,
25- "exercises a code path that only exists in the C extension" ,
26- )
2728
2829
2930def _wrapper (wrapped , instance , args , kwargs ):
@@ -44,18 +45,17 @@ def _find_member_descriptor():
4445
4546
4647class TestSlotWrapperDescriptor (unittest .TestCase ):
47- # ``wrapper_descriptor`` — the descriptor type for C-level type slots,
48- # e.g. ``_pickle.Unpickler.load`` (displayed as
49- # ``<slot wrapper 'load' of '_pickle.Unpickler' objects>``).
50- # A C extension type is used so the slot is not a plain Python function.
48+ # ``wrapper_descriptor`` is the descriptor type for C-level type slots
49+ # such as ``_pickle.Unpickler.load`` (displayed as
50+ # ``<slot wrapper 'load' of '_pickle.Unpickler' objects>``). A C
51+ # extension type is used so the slot wrapper is backed by a C function
52+ # rather than a Python function.
5153
52- @c_extension_only
5354 def test_class_level_get (self ):
5455 wrapped = _wrap (_pickle .Unpickler .load )
5556 result = wrapped .__get__ (None , _pickle .Unpickler )
5657 self .assertIsNotNone (result .__doc__ )
5758
58- @c_extension_only
5959 def test_subclass_class_level_get (self ):
6060 class Sub (_pickle .Unpickler ):
6161 pass
@@ -64,7 +64,6 @@ class Sub(_pickle.Unpickler):
6464 result = wrapped .__get__ (None , Sub )
6565 self .assertIsNotNone (result .__doc__ )
6666
67- @c_extension_only
6867 def test_instance_level_get (self ):
6968 wrapped = _wrap (_pickle .Unpickler .load )
7069 instance = _pickle .Unpickler (io .BytesIO ())
@@ -73,13 +72,14 @@ def test_instance_level_get(self):
7372
7473
7574class TestMethodDescriptor (unittest .TestCase ):
76- @c_extension_only
75+ # ``method_descriptor`` is the descriptor type for built-in methods
76+ # defined in C such as ``str.upper``.
77+
7778 def test_class_level_get (self ):
7879 wrapped = _wrap (str .upper )
7980 result = wrapped .__get__ (None , str )
8081 self .assertIsNotNone (result .__doc__ )
8182
82- @c_extension_only
8383 def test_subclass_class_level_get (self ):
8484 class SubStr (str ):
8585 pass
@@ -88,46 +88,53 @@ class SubStr(str):
8888 result = wrapped .__get__ (None , SubStr )
8989 self .assertIsNotNone (result .__doc__ )
9090
91- @c_extension_only
9291 def test_instance_level_call (self ):
9392 wrapped = _wrap (str .upper )
9493 bound = wrapped .__get__ ("hello" , str )
9594 self .assertEqual (bound (), "HELLO" )
9695
9796
9897class TestClassMethodDescriptor (unittest .TestCase ):
99- # ``classmethod_descriptor`` — classmethod descriptors ignore ``obj``
100- # entirely, so class-level and instance-level access behave identically.
98+ # ``classmethod_descriptor`` is the descriptor type for C-level
99+ # classmethods such as ``dict.fromkeys``. A classmethod descriptor
100+ # ignores the instance argument entirely, so class-level and
101+ # instance-level access behave identically.
101102
102- @c_extension_only
103103 def test_class_level_get (self ):
104104 wrapped = _wrap (dict .fromkeys )
105105 bound = wrapped .__get__ (None , dict )
106106 self .assertEqual (bound (["a" , "b" ], 1 ), {"a" : 1 , "b" : 1 })
107107
108108
109109class TestGetSetDescriptor (unittest .TestCase ):
110+ # ``getset_descriptor`` is the descriptor type generated for attributes
111+ # defined via ``PyGetSetDef`` in C, such as ``type.__name__``.
112+
110113 @staticmethod
111114 def _descriptor ():
112115 # Resolve via __dict__ so the lookup doesn't run the descriptor itself.
113116 descr = type .__dict__ ["__name__" ]
114117 assert isinstance (descr , types .GetSetDescriptorType )
115118 return descr
116119
117- @c_extension_only
118120 def test_class_level_get (self ):
121+ # Class-level access must return the descriptor itself rather than
122+ # invoking the getter.
119123 wrapped = _wrap (self ._descriptor ())
120124 result = wrapped .__get__ (None , type )
121125 self .assertIsInstance (result , types .GetSetDescriptorType )
122126
123- @c_extension_only
124127 def test_instance_level_get (self ):
128+ # Instance-level access must invoke the getter and return the value
129+ # of the attribute on the instance.
125130 wrapped = _wrap (self ._descriptor ())
126131 self .assertEqual (wrapped .__get__ (str , type ), "str" )
127132
128133
129134class TestMemberDescriptor (unittest .TestCase ):
130- @c_extension_only
135+ # ``member_descriptor`` is the descriptor type generated for slots
136+ # defined via ``PyMemberDef`` in C and for entries in ``__slots__``.
137+
131138 def test_class_level_get (self ):
132139 descr = _find_member_descriptor ()
133140 if descr is None :
@@ -138,6 +145,10 @@ def test_class_level_get(self):
138145
139146
140147class TestProperty (unittest .TestCase ):
148+ # ``property`` is the Python-level descriptor for read/write computed
149+ # attributes. Class-level access returns the property object itself
150+ # whereas instance-level access invokes the getter.
151+
141152 def test_class_level_get (self ):
142153 class C :
143154 @property
@@ -159,6 +170,11 @@ def value(self):
159170
160171
161172class TestClassMethod (unittest .TestCase ):
173+ # ``classmethod`` is the Python-level descriptor which binds the owning
174+ # class as the first argument. The wrapper must forward to the
175+ # classmethod's ``__get__`` so that calling the bound result invokes
176+ # the underlying function with the class.
177+
162178 def test_class_level_get (self ):
163179 class C :
164180 @classmethod
@@ -170,6 +186,11 @@ def make(cls):
170186
171187
172188class TestStaticMethod (unittest .TestCase ):
189+ # ``staticmethod`` is the Python-level descriptor which returns the
190+ # underlying function unchanged. The wrapper must forward to the
191+ # staticmethod's ``__get__`` so that calling the bound result invokes
192+ # the function with no implicit first argument.
193+
173194 def test_class_level_get (self ):
174195 class C :
175196 @staticmethod
@@ -181,6 +202,12 @@ def make():
181202
182203
183204class TestCustomDescriptor (unittest .TestCase ):
205+ # A user-defined Python descriptor receives the arguments
206+ # ``FunctionWrapper.__get__`` was called with. This case asserts on the
207+ # exact ``(instance, owner)`` tuple seen by the inner descriptor's
208+ # ``__get__`` so that any modification of the instance argument by the
209+ # wrapper between caller and wrapped descriptor is detected.
210+
184211 def test_class_level_get_forwards_to_descriptor_get (self ):
185212 calls = []
186213
@@ -197,7 +224,12 @@ def __get__(self, instance, owner):
197224
198225
199226class TestSubclassAttributeAccess (unittest .TestCase ):
200- @c_extension_only
227+ # A wrapped descriptor installed on a subclass must remain accessible
228+ # via class attribute access on that subclass. This is the realistic
229+ # monkey patching pattern used by instrumentation libraries: take an
230+ # existing descriptor off a parent class, wrap it, and install the
231+ # wrapper as an attribute of a subclass.
232+
201233 def test_wrapped_slot_wrapper_via_subclass (self ):
202234 class Sub (_pickle .Unpickler ):
203235 pass
@@ -206,7 +238,6 @@ class Sub(_pickle.Unpickler):
206238 Sub .load = wrapped
207239 self .assertIsNotNone (Sub .load .__doc__ )
208240
209- @c_extension_only
210241 def test_wrapped_dict_slot_via_subclass (self ):
211242 class SubDict (dict ):
212243 pass
@@ -227,17 +258,5 @@ class Child(Parent):
227258 self .assertEqual (Child ().method (), "parent" )
228259
229260
230- class TestPurePythonWraptPath (unittest .TestCase ):
231- def test_slot_wrapper (self ):
232- wrapped = PurePythonFunctionWrapper (_pickle .Unpickler .load , _wrapper )
233- result = wrapped .__get__ (None , _pickle .Unpickler )
234- self .assertIsNotNone (result .__doc__ )
235-
236- def test_method_descriptor (self ):
237- wrapped = PurePythonFunctionWrapper (str .upper , _wrapper )
238- result = wrapped .__get__ (None , str )
239- self .assertIsNotNone (result .__doc__ )
240-
241-
242261if __name__ == "__main__" :
243262 unittest .main ()
0 commit comments