Skip to content

Commit eecfe28

Browse files
committed
add pure Python fallback
1 parent 6fcb64e commit eecfe28

File tree

4 files changed

+125
-16
lines changed

4 files changed

+125
-16
lines changed

Lib/test/test_types.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ def clear_typing_caches():
4141
class TypesTests(unittest.TestCase):
4242

4343
def test_names(self):
44-
c_only_names = {'CapsuleType', 'LazyImportType',
45-
'lookup_special_method'}
44+
c_only_names = {'CapsuleType', 'LazyImportType'}
4645
ignored = {'new_class', 'resolve_bases', 'prepare_class',
47-
'get_original_bases', 'DynamicClassAttribute', 'coroutine'}
46+
'get_original_bases', 'DynamicClassAttribute', 'coroutine',
47+
'lookup_special_method'}
4848

4949
for name in c_types.__all__:
5050
if name not in c_only_names | ignored:
@@ -727,7 +727,7 @@ def test_frame_locals_proxy_type(self):
727727
self.assertIsNotNone(frame)
728728
self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType)
729729

730-
def test_lookup_special_method(self):
730+
def _test_lookup_special_method(self, lookup):
731731
class CM1:
732732
def __enter__(self):
733733
return "__enter__ from class __dict__"
@@ -745,29 +745,36 @@ def __enter__(self):
745745
return "__enter__ from __slots__"
746746
self.__enter__ = __enter__
747747
cm1 = CM1()
748-
meth = types.lookup_special_method(cm1, "__enter__")
748+
meth = lookup(cm1, "__enter__")
749749
self.assertIsNotNone(meth)
750750
self.assertEqual(meth(cm1), "__enter__ from class __dict__")
751751

752-
meth = types.lookup_special_method(cm1, "__missing__")
752+
meth = lookup(cm1, "__missing__")
753753
self.assertIsNone(meth)
754754

755755
with self.assertRaises(TypeError):
756-
types.lookup_special_method(cm1, 123)
756+
lookup(cm1, 123)
757757

758758
cm2 = CM2()
759-
meth = types.lookup_special_method(cm2, "__enter__")
759+
meth = lookup(cm2, "__enter__")
760760
self.assertIsNone(meth)
761761

762762
cm3 = CM3()
763-
meth = types.lookup_special_method(cm3, "__enter__")
763+
meth = lookup(cm3, "__enter__")
764764
self.assertIsNotNone(meth)
765765
self.assertEqual(meth(cm3), "__enter__ from __slots__")
766766

767-
meth = types.lookup_special_method([], "__len__")
767+
meth = lookup([], "__len__")
768768
self.assertIsNotNone(meth)
769769
self.assertEqual(meth([]), 0)
770770

771+
def test_lookup_special_method(self):
772+
c_lookup = getattr(c_types, "lookup_special_method")
773+
py_lookup = getattr(types, "lookup_special_method")
774+
self._test_lookup_special_method(c_lookup)
775+
self._test_lookup_special_method(py_lookup)
776+
777+
771778
class UnionTests(unittest.TestCase):
772779

773780
def test_or_types_operator(self):

Lib/types.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,54 @@ def _m(self): pass
8181

8282
del sys, _f, _g, _C, _c, _ag, _cell_factory # Not for export
8383

84+
def lookup_special_method(obj, attr, /):
85+
"""Lookup special method name `attr` on `obj`.
86+
87+
Lookup method `attr` on `obj` without looking in the instance
88+
dictionary. For methods defined in class `__dict__` or `__slots__`, it
89+
returns the unbound function (descriptor), not a bound method. The
90+
caller is responsible for passing the object as the first argument when
91+
calling it:
92+
93+
class A:
94+
def __enter__(self):
95+
pass
96+
97+
class B:
98+
__slots__ = ("__enter__",)
99+
100+
def __init__(self):
101+
def __enter__(self):
102+
pass
103+
self.__enter__ = __enter__
104+
105+
a = A()
106+
b = B()
107+
enter_a = types.lookup_special_method(a, "__enter__")
108+
enter_b = types.lookup_special_method(b, "__enter__")
109+
110+
result_a = enter_a(a)
111+
result_b = enter_b(b)
112+
113+
For other descriptors (property, etc.), it returns the result of the
114+
descriptor's `__get__` method. Returns `None` if the method is not
115+
found.
116+
"""
117+
from inspect import getattr_static, isfunction, ismethoddescriptor
118+
cls = type(obj)
119+
try:
120+
descr = getattr_static(cls, attr)
121+
except AttributeError:
122+
return None
123+
if hasattr(descr, "__get__"):
124+
if isfunction(descr) or ismethoddescriptor(descr):
125+
# do not create bound method to mimic the behavior of
126+
# _PyObject_LookupSpecialMethod
127+
return descr
128+
else:
129+
return descr.__get__(obj, cls)
130+
return descr
131+
84132

85133
# Provide a PEP 3115 compliant mechanism for class creation
86134
def new_class(name, bases=(), kwds=None, exec_body=None):

Modules/_typesmodule.c

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,41 @@ _types.lookup_special_method
2424
2525
Lookup special method name `attr` on `obj`.
2626
27-
Lookup method `attr` on `obj` without looking in the instance dictionary.
28-
Returns `None` if the method is not found.
27+
Lookup method `attr` on `obj` without looking in the instance
28+
dictionary. For methods defined in class `__dict__` or `__slots__`, it
29+
returns the unbound function (descriptor), not a bound method. The
30+
caller is responsible for passing the object as the first argument when
31+
calling it:
32+
33+
class A:
34+
def __enter__(self):
35+
pass
36+
37+
class B:
38+
__slots__ = ("__enter__",)
39+
40+
def __init__(self):
41+
def __enter__(self):
42+
pass
43+
self.__enter__ = __enter__
44+
45+
a = A()
46+
b = B()
47+
enter_a = types.lookup_special_method(a, "__enter__")
48+
enter_b = types.lookup_special_method(b, "__enter__")
49+
50+
result_a = enter_a(a)
51+
result_b = enter_b(b)
52+
53+
For other descriptors (property, etc.), it returns the result of the
54+
descriptor's `__get__` method. Returns `None` if the method is not
55+
found.
2956
[clinic start generated code]*/
3057

3158
static PyObject *
3259
_types_lookup_special_method_impl(PyObject *module, PyObject *obj,
3360
PyObject *attr)
34-
/*[clinic end generated code: output=890e22cc0b8e0d34 input=f26012b0c90b81cd]*/
61+
/*[clinic end generated code: output=890e22cc0b8e0d34 input=fca9cb0e313a7848]*/
3562
{
3663
if (!PyUnicode_Check(attr)) {
3764
PyErr_Format(PyExc_TypeError,

Modules/clinic/_typesmodule.c.h

Lines changed: 30 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)