Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,44 @@ Additional Utility Classes and Functions

.. versionadded:: 3.4

.. function:: lookup_special_method(obj, attr, /)

Lookup special method name ``attr`` on ``obj``.

Lookup method ``attr`` on ``obj`` without looking in the instance
dictionary. For methods defined in class ``__dict__`` or ``__slots__``, it
returns the unbound function (descriptor), not a bound method. The
caller is responsible for passing the object as the first argument when
calling it:

.. code-block:: python
>>> class A:
... def __enter__(self):
... return "A.__enter__"
...
>>> class B:
... __slots__ = ("__enter__",)
... def __init__(self):
... def __enter__(self):
... return "B.__enter__"
... self.__enter__ = __enter__
...
>>> a = A()
>>> b = B()
>>> enter_a = types.lookup_special_method(a, "__enter__")
>>> enter_b = types.lookup_special_method(b, "__enter__")
>>> enter_a(a)
'A.__enter__'
>>> enter_b(b)
'B.__enter__'
For other descriptors (property, etc.), it returns the result of the
descriptor's ``__get__`` method. Returns ``None`` if the method is not
found.

.. versionadded:: next


Coroutine Utility Functions
---------------------------
Expand Down
2 changes: 2 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,8 @@ types
This represents the type of the :attr:`frame.f_locals` attribute,
as described in :pep:`667`.

* Expose ``_PyObject_LookupSpecialMethod`` as
:func:`types.lookup_special_method`.

unicodedata
-----------
Expand Down
52 changes: 50 additions & 2 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class TypesTests(unittest.TestCase):
def test_names(self):
c_only_names = {'CapsuleType', 'LazyImportType'}
ignored = {'new_class', 'resolve_bases', 'prepare_class',
'get_original_bases', 'DynamicClassAttribute', 'coroutine'}
'get_original_bases', 'DynamicClassAttribute', 'coroutine',
'lookup_special_method'}

for name in c_types.__all__:
if name not in c_only_names | ignored:
Expand All @@ -59,7 +60,7 @@ def test_names(self):
'MemberDescriptorType', 'MethodDescriptorType', 'MethodType',
'MethodWrapperType', 'ModuleType', 'NoneType',
'NotImplementedType', 'SimpleNamespace', 'TracebackType',
'UnionType', 'WrapperDescriptorType',
'UnionType', 'WrapperDescriptorType', 'lookup_special_method',
}
self.assertEqual(all_names, set(c_types.__all__))
self.assertEqual(all_names - c_only_names, set(py_types.__all__))
Expand Down Expand Up @@ -726,6 +727,53 @@ def test_frame_locals_proxy_type(self):
self.assertIsNotNone(frame)
self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType)

def _test_lookup_special_method(self, lookup):
class CM1:
def __enter__(self):
return "__enter__ from class __dict__"

class CM2:
def __init__(self):
def __enter__(self):
return "__enter__ from instance __dict__"
self.__enter__ = __enter__

class CM3:
__slots__ = ("__enter__",)
def __init__(self):
def __enter__(self):
return "__enter__ from __slots__"
self.__enter__ = __enter__
cm1 = CM1()
meth = lookup(cm1, "__enter__")
self.assertIsNotNone(meth)
self.assertEqual(meth(cm1), "__enter__ from class __dict__")

meth = lookup(cm1, "__missing__")
self.assertIsNone(meth)

with self.assertRaises(TypeError):
lookup(cm1, 123)

cm2 = CM2()
meth = lookup(cm2, "__enter__")
self.assertIsNone(meth)

cm3 = CM3()
meth = lookup(cm3, "__enter__")
self.assertIsNotNone(meth)
self.assertEqual(meth(cm3), "__enter__ from __slots__")

meth = lookup([], "__len__")
self.assertIsNotNone(meth)
self.assertEqual(meth([]), 0)

def test_lookup_special_method(self):
c_lookup = getattr(c_types, "lookup_special_method")
py_lookup = getattr(py_types, "lookup_special_method")
self._test_lookup_special_method(c_lookup)
self._test_lookup_special_method(py_lookup)


class UnionTests(unittest.TestCase):

Expand Down
52 changes: 52 additions & 0 deletions Lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,58 @@ def _m(self): pass

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

def lookup_special_method(obj, attr, /):
"""Lookup special method name `attr` on `obj`.
Lookup method `attr` on `obj` without looking in the instance
dictionary. For methods defined in class `__dict__` or `__slots__`, it
returns the unbound function (descriptor), not a bound method. The
caller is responsible for passing the object as the first argument when
calling it:
>>> class A:
... def __enter__(self):
... return "A.__enter__"
...
>>> class B:
... __slots__ = ("__enter__",)
... def __init__(self):
... def __enter__(self):
... return "B.__enter__"
... self.__enter__ = __enter__
...
>>> a = A()
>>> b = B()
>>> enter_a = types.lookup_special_method(a, "__enter__")
>>> enter_b = types.lookup_special_method(b, "__enter__")
>>> enter_a(a)
'A.__enter__'
>>> enter_b(b)
'B.__enter__'
For other descriptors (property, etc.), it returns the result of the
descriptor's `__get__` method. Returns `None` if the method is not
found.
"""
from inspect import getattr_static, isfunction, ismethoddescriptor
cls = type(obj)
if not isinstance(attr, str):
raise TypeError(
f"attribute name must be string, not '{type(attr).__name__}'"
)
try:
descr = getattr_static(cls, attr)
except AttributeError:
return None
if hasattr(descr, "__get__"):
if isfunction(descr) or ismethoddescriptor(descr):
# do not create bound method to mimic the behavior of
# _PyObject_LookupSpecialMethod
return descr
else:
return descr.__get__(obj, cls)
return descr


# Provide a PEP 3115 compliant mechanism for class creation
def new_class(name, bases=(), kwds=None, exec_body=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Expose ``_PyObject_LookupSpecialMethod`` as
:func:`types.lookup_special_method`.
79 changes: 79 additions & 0 deletions Modules/_typesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,79 @@
#include "pycore_namespace.h" // _PyNamespace_Type
#include "pycore_object.h" // _PyNone_Type, _PyNotImplemented_Type
#include "pycore_unionobject.h" // _PyUnion_Type
#include "pycore_typeobject.h" // _PyObject_LookupSpecialMethod
#include "pycore_stackref.h" // _PyStackRef
#include "clinic/_typesmodule.c.h"

/*[clinic input]
module _types
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=530308b1011b659d]*/

/*[clinic input]
_types.lookup_special_method

obj: 'O'
attr: 'O'
/

Lookup special method name `attr` on `obj`.

Lookup method `attr` on `obj` without looking in the instance
dictionary. For methods defined in class `__dict__` or `__slots__`, it
returns the unbound function (descriptor), not a bound method. The
caller is responsible for passing the object as the first argument when
calling it:

>>> class A:
... def __enter__(self):
... return "A.__enter__"
...
>>> class B:
... __slots__ = ("__enter__",)
... def __init__(self):
... def __enter__(self):
... return "B.__enter__"
... self.__enter__ = __enter__
...
>>> a = A()
>>> b = B()
>>> enter_a = types.lookup_special_method(a, "__enter__")
>>> enter_b = types.lookup_special_method(b, "__enter__")
>>> enter_a(a)
'A.__enter__'
>>> enter_b(b)
'B.__enter__'

For other descriptors (property, etc.), it returns the result of the
descriptor's `__get__` method. Returns `None` if the method is not
found.
[clinic start generated code]*/

static PyObject *
_types_lookup_special_method_impl(PyObject *module, PyObject *obj,
PyObject *attr)
/*[clinic end generated code: output=890e22cc0b8e0d34 input=e317288370125cd5]*/
{
if (!PyUnicode_Check(attr)) {
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
Py_TYPE(attr)->tp_name);
return NULL;
}
_PyStackRef method_and_self[2];
method_and_self[0] = PyStackRef_NULL;
method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj);
int result = _PyObject_LookupSpecialMethod(attr, method_and_self);
if (result == -1) {
return NULL;
}
if (result == 0) {
Py_RETURN_NONE;
}
PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]);
return method;
}

static int
_types_exec(PyObject *m)
Expand Down Expand Up @@ -60,12 +133,18 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = {
{0, NULL}
};

static PyMethodDef _typesmodule_methods[] = {
_TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef typesmodule = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_types",
.m_doc = "Define names for built-in types.",
.m_size = 0,
.m_slots = _typesmodule_slots,
.m_methods = _typesmodule_methods,
};

PyMODINIT_FUNC
Expand Down
67 changes: 67 additions & 0 deletions Modules/clinic/_typesmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading