Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1a237da
expose _PyObject_LookupSpecialMethod to types.lookup_special_method
tanloong Feb 19, 2026
d00b90e
blurb
tanloong Feb 19, 2026
be4bcc2
Add signature; Fix test_inspect.test_inspect error
tanloong Feb 19, 2026
62b5d21
adjust indent width as 4
tanloong Feb 19, 2026
6f8d459
remove unnecessary Py_BuildValue()
tanloong Feb 19, 2026
f6c716e
versionadded:: next
tanloong Feb 19, 2026
db62648
surround None with backticks
tanloong Feb 19, 2026
49f891c
add :func: in news entry
tanloong Feb 19, 2026
05d25e4
add whats new in python3.15
tanloong Feb 19, 2026
00582fe
add positional-only marker
tanloong Feb 19, 2026
25af12d
use Argument Clinic intead of PyArg_ParseTuple
tanloong Feb 19, 2026
6fcb64e
add _typesmodule.c.h
tanloong Feb 19, 2026
eecfe28
add pure Python fallback
tanloong Feb 19, 2026
e722141
improve code example in the docstring
tanloong Feb 19, 2026
f43112a
fix py:func reference target not found
tanloong Feb 19, 2026
6650498
fix typo in test
tanloong Feb 19, 2026
d3f6bda
add attr check in Python fallback
tanloong Feb 19, 2026
da5dc81
ensure Python fallback has same behavior as _PyObject_LookupSpecialMe…
tanloong Feb 20, 2026
97a6eb8
replace inspect.isfunction with isinstance(descr, FunctionType)
tanloong Feb 20, 2026
671185e
1. expose _PyObject_LookupSpecial instead of _PyObject_LookupSpecialM…
tanloong Feb 22, 2026
98735f6
fix signature error in test_inspect.py
tanloong Feb 22, 2026
f0f26e4
fix whatsnew
tanloong Feb 22, 2026
10ff83f
Merge branch 'main' into expose-lookupspeicalmethod
tanloong Feb 23, 2026
9f66dc9
Added `default` parameter to `lookup_special` in `types.py` (#5)
tanloong Feb 23, 2026
5cf9c4c
Removed duplicated definition of `lookup_special` in `types.py`. (#6)
JoBeGaming Feb 23, 2026
8806877
remove extra 'del inspect'
tanloong Feb 23, 2026
c521b76
Revert "remove extra 'del inspect'"; add missing comma
tanloong Feb 23, 2026
e939d08
del enclose_lookup_special; fix 'inspect' not found
tanloong Feb 23, 2026
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
10 changes: 10 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,16 @@ Additional Utility Classes and Functions

.. versionadded:: 3.4

.. function:: lookup_special(object, name, /)
lookup_special(object, name, default, /)

Lookup method name *name* on *object* skipping the instance dictionary.
*name* must be a string. If the named special attribute does not exist,
*default* is returned if provided, otherwise :exc:`AttributeError` is
raised.

.. versionadded:: next


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

* Expose ``_PyObject_LookupSpecial`` as :func:`types.lookup_special`.

unicodedata
-----------
Expand Down
6 changes: 5 additions & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -6155,14 +6155,18 @@ def test_builtins_have_signatures(self):
methods_no_signature, methods_unsupported_signature)

def test_types_module_has_signatures(self):
no_signature = set()
# These need PEP 457 groups
needs_groups = {'lookup_special'}
no_signature |= needs_groups
unsupported_signature = {'CellType'}
methods_no_signature = {
'AsyncGeneratorType': {'athrow'},
'CoroutineType': {'throw'},
'GeneratorType': {'throw'},
'FrameLocalsProxyType': {'setdefault', 'pop', 'get'},
}
self._test_module_has_signatures(types,
self._test_module_has_signatures(types, no_signature,
unsupported_signature=unsupported_signature,
methods_no_signature=methods_no_signature)

Expand Down
63 changes: 61 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'}

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',
}
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,64 @@ def test_frame_locals_proxy_type(self):
self.assertIsNotNone(frame)
self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType)

def _test_lookup_special(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()
with self.assertRaisesRegex(TypeError, "attribute name must be string"):
lookup(cm1, 123)
with self.assertRaises(AttributeError):
lookup(cm1, "__missing__")
self.assertEqual(lookup(cm1, "__missing__", "default"), "default")
meth = lookup(cm1, "__enter__")
self.assertEqual(meth(), "__enter__ from class __dict__")

cm2 = CM2()
with self.assertRaises(AttributeError):
lookup(cm2, "__enter__")

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

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

class Person:
@classmethod
def hi(cls):
return f"hi from {cls.__name__}"
@staticmethod
def hello():
return "hello from static method"
@property
def name(self):
return "name from property"
p = Person()
self.assertEqual(lookup(p, "hi")(), "hi from Person")
self.assertEqual(lookup(p, "hello")(), "hello from static method")
self.assertEqual(lookup(p, "name"), "name from property")

def test_lookup_special(self):
c_lookup = getattr(c_types, "lookup_special")
py_lookup = getattr(py_types, "lookup_special")
self._test_lookup_special(c_lookup)
self._test_lookup_special(py_lookup)


class UnionTests(unittest.TestCase):

Expand Down
33 changes: 32 additions & 1 deletion Lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,38 @@ def _m(self): pass
# LazyImportType in pure Python cannot be guaranteed
# without overriding the filter, so there is no fallback.

del sys, _f, _g, _C, _c, _ag, _cell_factory # Not for export
def enclose_lookup_special():
_sentinel = object()

def lookup_special(object, name, default=_sentinel):
"""Lookup method name `name` on `object` skipping the instance
dictionary.

`name` must be a string. If the named special attribute does not exist,
`default` is returned if provided, otherwise AttributeError is raised.
"""
import inspect

cls = type(object)
if not isinstance(name, str):
raise TypeError(
f"attribute name must be string, not '{type(name).__name__}'"
)
try:
descr = inspect.getattr_static(cls, name)
except AttributeError:
if default is not _sentinel:
return default
raise
if hasattr(descr, "__get__"):
return descr.__get__(object, cls)
return descr

return lookup_special

lookup_special = enclose_lookup_special()

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


# Provide a PEP 3115 compliant mechanism for class creation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose ``_PyObject_LookupSpecial`` as :func:`types.lookup_special`.
45 changes: 45 additions & 0 deletions Modules/_typesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,45 @@
#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_LookupSpecial
#include "pycore_modsupport.h" // _PyArg_CheckPositional

PyDoc_STRVAR(lookup_special_doc,
"lookup_special(object, name[, default], /)\n\
\n\
Lookup method name `name` on `object` skipping the instance dictionary.\n\
`name` must be a string. If the named special attribute does not\n\
exist,`default` is returned if provided, otherwise `AttributeError` is raised.");

static PyObject *
_types_lookup_special_impl(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *v, *name, *result;

if (!_PyArg_CheckPositional("lookup_special", nargs, 2, 3))
return NULL;

v = args[0];
name = args[1];
if (!PyUnicode_Check(name)) {
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
Py_TYPE(name)->tp_name);
return NULL;
}
result = _PyObject_LookupSpecial(v, name);
if (result == NULL) {
if (nargs > 2) {
PyObject *dflt = args[2];
return Py_NewRef(dflt);
} else {
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no special attribute '%U'",
Py_TYPE(v)->tp_name, name);
}
}
return result;
}

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

static PyMethodDef _typesmodule_methods[] = {
{"lookup_special", _PyCFunction_CAST(_types_lookup_special_impl),
METH_FASTCALL, lookup_special_doc},
{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
Loading