Skip to content

Commit 1a237da

Browse files
committed
expose _PyObject_LookupSpecialMethod to types.lookup_special_method
1 parent 645f5c4 commit 1a237da

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

Doc/library/types.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,14 @@ Additional Utility Classes and Functions
521521

522522
.. versionadded:: 3.4
523523

524+
.. function:: lookup_special_method(obj, attr)
525+
526+
Do a method lookup in the type without looking in the instance dictionary
527+
but still binding it to the instance. Returns None if the method is not
528+
found.
529+
530+
.. versionadded:: 3.15
531+
524532

525533
Coroutine Utility Functions
526534
---------------------------

Lib/test/test_types.py

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

4343
def test_names(self):
44-
c_only_names = {'CapsuleType', 'LazyImportType'}
44+
c_only_names = {'CapsuleType', 'LazyImportType',
45+
'lookup_special_method'}
4546
ignored = {'new_class', 'resolve_bases', 'prepare_class',
4647
'get_original_bases', 'DynamicClassAttribute', 'coroutine'}
4748

@@ -59,7 +60,7 @@ def test_names(self):
5960
'MemberDescriptorType', 'MethodDescriptorType', 'MethodType',
6061
'MethodWrapperType', 'ModuleType', 'NoneType',
6162
'NotImplementedType', 'SimpleNamespace', 'TracebackType',
62-
'UnionType', 'WrapperDescriptorType',
63+
'UnionType', 'WrapperDescriptorType', 'lookup_special_method',
6364
}
6465
self.assertEqual(all_names, set(c_types.__all__))
6566
self.assertEqual(all_names - c_only_names, set(py_types.__all__))
@@ -726,6 +727,46 @@ def test_frame_locals_proxy_type(self):
726727
self.assertIsNotNone(frame)
727728
self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType)
728729

730+
def test_lookup_special_method(self):
731+
class CM1:
732+
def __enter__(self):
733+
return "__enter__ from class __dict__"
734+
735+
class CM2:
736+
def __init__(self):
737+
def __enter__(self):
738+
return "__enter__ from instance __dict__"
739+
self.__enter__ = __enter__
740+
741+
class CM3:
742+
__slots__ = ("__enter__",)
743+
def __init__(self):
744+
def __enter__(self):
745+
return "__enter__ from __slots__"
746+
self.__enter__ = __enter__
747+
cm1 = CM1()
748+
meth = types.lookup_special_method(cm1, "__enter__")
749+
self.assertIsNotNone(meth)
750+
self.assertEqual(meth(cm1), "__enter__ from class __dict__")
751+
752+
meth = types.lookup_special_method(cm1, "__missing__")
753+
self.assertIsNone(meth)
754+
755+
with self.assertRaises(TypeError):
756+
types.lookup_special_method(cm1, 123)
757+
758+
cm2 = CM2()
759+
meth = types.lookup_special_method(cm2, "__enter__")
760+
self.assertIsNone(meth)
761+
762+
cm3 = CM3()
763+
meth = types.lookup_special_method(cm3, "__enter__")
764+
self.assertIsNotNone(meth)
765+
self.assertEqual(meth(cm3), "__enter__ from __slots__")
766+
767+
meth = types.lookup_special_method([], "__len__")
768+
self.assertIsNotNone(meth)
769+
self.assertEqual(meth([]), 0)
729770

730771
class UnionTests(unittest.TestCase):
731772

Modules/_typesmodule.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,34 @@
66
#include "pycore_namespace.h" // _PyNamespace_Type
77
#include "pycore_object.h" // _PyNone_Type, _PyNotImplemented_Type
88
#include "pycore_unionobject.h" // _PyUnion_Type
9+
#include "pycore_typeobject.h" // _PyObject_LookupSpecialMethod
10+
#include "pycore_stackref.h" // _PyStackRef
11+
12+
static PyObject *
13+
_types_lookup_special_method_impl(PyObject *self, PyObject *args)
14+
{
15+
PyObject *obj, *attr;
16+
if (!PyArg_ParseTuple(args, "OO", &obj, &attr)) {
17+
return NULL;
18+
}
19+
if (!PyUnicode_Check(attr)) {
20+
PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'",
21+
Py_TYPE(attr)->tp_name);
22+
return NULL;
23+
}
24+
_PyStackRef method_and_self[2];
25+
method_and_self[0] = PyStackRef_NULL;
26+
method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj);
27+
int result = _PyObject_LookupSpecialMethod(attr, method_and_self);
28+
if (result == -1) {
29+
return NULL;
30+
}
31+
if (result == 0) {
32+
Py_RETURN_NONE;
33+
}
34+
PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]);
35+
return Py_BuildValue("O", method);
36+
}
937

1038
static int
1139
_types_exec(PyObject *m)
@@ -60,12 +88,20 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = {
6088
{0, NULL}
6189
};
6290

91+
static PyMethodDef _typesmodule_methods[] = {
92+
{"lookup_special_method", _types_lookup_special_method_impl, METH_VARARGS,
93+
"Do a method lookup in the type without looking in the instance "
94+
"dictionary but still binding it to the instance. Returns None if the "
95+
"method is not found."},
96+
{NULL, NULL, 0, NULL}};
97+
6398
static struct PyModuleDef typesmodule = {
6499
.m_base = PyModuleDef_HEAD_INIT,
65100
.m_name = "_types",
66101
.m_doc = "Define names for built-in types.",
67102
.m_size = 0,
68103
.m_slots = _typesmodule_slots,
104+
.m_methods = _typesmodule_methods,
69105
};
70106

71107
PyMODINIT_FUNC

0 commit comments

Comments
 (0)