Skip to content

Commit 671185e

Browse files
committed
1. expose _PyObject_LookupSpecial instead of _PyObject_LookupSpecialMethod;
2. raise AttributeError instead of returning None; 3. add optional `default` parameter, similar to `getattr`: lookup_special(object, name[, default]) 4. There are different opinions on which module should the function be in. Leave it in `types` until there is consensus.
1 parent 97a6eb8 commit 671185e

File tree

6 files changed

+72
-245
lines changed

6 files changed

+72
-245
lines changed

Doc/library/types.rst

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -521,41 +521,13 @@ Additional Utility Classes and Functions
521521

522522
.. versionadded:: 3.4
523523

524-
.. function:: lookup_special_method(obj, attr, /)
525-
526-
Lookup special method name ``attr`` on ``obj``.
527-
528-
Lookup method ``attr`` on ``obj`` without looking in the instance
529-
dictionary. For methods defined in class ``__dict__`` or ``__slots__``, it
530-
returns the unbound function (descriptor), not a bound method. The
531-
caller is responsible for passing the object as the first argument when
532-
calling it:
533-
534-
.. code-block:: python
535-
536-
>>> class A:
537-
... def __enter__(self):
538-
... return "A.__enter__"
539-
...
540-
>>> class B:
541-
... __slots__ = ("__enter__",)
542-
... def __init__(self):
543-
... def __enter__(self):
544-
... return "B.__enter__"
545-
... self.__enter__ = __enter__
546-
...
547-
>>> a = A()
548-
>>> b = B()
549-
>>> enter_a = types.lookup_special_method(a, "__enter__")
550-
>>> enter_b = types.lookup_special_method(b, "__enter__")
551-
>>> enter_a(a)
552-
'A.__enter__'
553-
>>> enter_b(b)
554-
'B.__enter__'
555-
556-
For other descriptors (property, etc.), it returns the result of the
557-
descriptor's ``__get__`` method. Returns ``None`` if the method is not
558-
found.
524+
.. function:: lookup_special(object, name, /)
525+
lookup_special(object, name, default, /)
526+
527+
Lookup method name *name* on *object* skipping the instance dictionary.
528+
*name* must be a string. If the named special attribute does not exist,
529+
*default* is returned if provided, otherwise :exc:`AttributeError` is
530+
raised.
559531

560532
.. versionadded:: next
561533

Lib/test/test_types.py

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_names(self):
4444
c_only_names = {'CapsuleType', 'LazyImportType'}
4545
ignored = {'new_class', 'resolve_bases', 'prepare_class',
4646
'get_original_bases', 'DynamicClassAttribute', 'coroutine',
47-
'lookup_special_method'}
47+
'lookup_special'}
4848

4949
for name in c_types.__all__:
5050
if name not in c_only_names | ignored:
@@ -60,7 +60,7 @@ def test_names(self):
6060
'MemberDescriptorType', 'MethodDescriptorType', 'MethodType',
6161
'MethodWrapperType', 'ModuleType', 'NoneType',
6262
'NotImplementedType', 'SimpleNamespace', 'TracebackType',
63-
'UnionType', 'WrapperDescriptorType', 'lookup_special_method',
63+
'UnionType', 'WrapperDescriptorType', 'lookup_special',
6464
}
6565
self.assertEqual(all_names, set(c_types.__all__))
6666
self.assertEqual(all_names - c_only_names, set(py_types.__all__))
@@ -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, lookup):
730+
def _test_lookup_special(self, lookup):
731731
class CM1:
732732
def __enter__(self):
733733
return "__enter__ from class __dict__"
@@ -745,31 +745,24 @@ def __enter__(self):
745745
return "__enter__ from __slots__"
746746
self.__enter__ = __enter__
747747
cm1 = CM1()
748-
meth = lookup(cm1, "__enter__")
749-
self.assertIsNotNone(meth)
750-
with self.assertRaisesRegex(
751-
TypeError, "missing 1 required positional argument") as cm:
752-
meth()
753-
self.assertEqual(meth(cm1), "__enter__ from class __dict__")
754-
755-
meth = lookup(cm1, "__missing__")
756-
self.assertIsNone(meth)
757-
758748
with self.assertRaisesRegex(TypeError, "attribute name must be string"):
759749
lookup(cm1, 123)
750+
with self.assertRaises(AttributeError):
751+
lookup(cm1, "__missing__")
752+
self.assertEqual(lookup(cm1, "__missing__", "default"), "default")
753+
meth = lookup(cm1, "__enter__")
754+
self.assertEqual(meth(), "__enter__ from class __dict__")
760755

761756
cm2 = CM2()
762-
meth = lookup(cm2, "__enter__")
763-
self.assertIsNone(meth)
757+
with self.assertRaises(AttributeError):
758+
lookup(cm2, "__enter__")
764759

765760
cm3 = CM3()
766761
meth = lookup(cm3, "__enter__")
767-
self.assertIsNotNone(meth)
768762
self.assertEqual(meth(cm3), "__enter__ from __slots__")
769763

770764
meth = lookup([], "__len__")
771-
self.assertIsNotNone(meth)
772-
self.assertEqual(meth([]), 0)
765+
self.assertEqual(meth(), 0)
773766

774767
class Person:
775768
@classmethod
@@ -782,21 +775,15 @@ def hello():
782775
def name(self):
783776
return "name from property"
784777
p = Person()
785-
meth = lookup(p, "hi")
786-
self.assertIsNotNone(meth)
787-
self.assertEqual(meth(), "hi from Person")
788-
789-
meth = lookup(p, "hello")
790-
self.assertIsNotNone(meth)
791-
self.assertEqual(meth(), "hello from static method")
792-
778+
self.assertEqual(lookup(p, "hi")(), "hi from Person")
779+
self.assertEqual(lookup(p, "hello")(), "hello from static method")
793780
self.assertEqual(lookup(p, "name"), "name from property")
794781

795-
def test_lookup_special_method(self):
796-
c_lookup = getattr(c_types, "lookup_special_method")
797-
py_lookup = getattr(py_types, "lookup_special_method")
798-
self._test_lookup_special_method(c_lookup)
799-
self._test_lookup_special_method(py_lookup)
782+
def test_lookup_special(self):
783+
c_lookup = getattr(c_types, "lookup_special")
784+
py_lookup = getattr(py_types, "lookup_special")
785+
self._test_lookup_special(c_lookup)
786+
self._test_lookup_special(py_lookup)
800787

801788

802789
class UnionTests(unittest.TestCase):

Lib/types.py

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -81,57 +81,27 @@ 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-
... return "A.__enter__"
96-
...
97-
>>> class B:
98-
... __slots__ = ("__enter__",)
99-
... def __init__(self):
100-
... def __enter__(self):
101-
... return "B.__enter__"
102-
... self.__enter__ = __enter__
103-
...
104-
>>> a = A()
105-
>>> b = B()
106-
>>> enter_a = types.lookup_special_method(a, "__enter__")
107-
>>> enter_b = types.lookup_special_method(b, "__enter__")
108-
>>> enter_a(a)
109-
'A.__enter__'
110-
>>> enter_b(b)
111-
'B.__enter__'
112-
113-
For other descriptors (classmethod, staticmethod, property, etc.), it
114-
returns the result of the descriptor's `__get__` method. Returns `None`
115-
if the method is not found.
84+
def lookup_special(object, name, *args):
85+
"""Lookup method name `name` on `object` skipping the instance
86+
dictionary.
87+
88+
`name` must be a string. If the named special attribute does not exist,
89+
`default` is returned if provided, otherwise AttributeError is raised.
11690
"""
11791
from inspect import getattr_static
118-
cls = type(obj)
119-
if not isinstance(attr, str):
92+
cls = type(object)
93+
if not isinstance(name, str):
12094
raise TypeError(
121-
f"attribute name must be string, not '{type(attr).__name__}'"
95+
f"attribute name must be string, not '{type(name).__name__}'"
12296
)
12397
try:
124-
descr = getattr_static(cls, attr)
98+
descr = getattr_static(cls, name)
12599
except AttributeError:
126-
return None
100+
if args:
101+
return args[0]
102+
raise
127103
if hasattr(descr, "__get__"):
128-
if isinstance(descr, (
129-
FunctionType, MethodDescriptorType, WrapperDescriptorType)):
130-
# do not create bound method to mimic the behavior of
131-
# _PyObject_LookupSpecialMethod
132-
return descr
133-
else:
134-
return descr.__get__(obj, cls)
104+
return descr.__get__(object, cls)
135105
return descr
136106

137107

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
Expose ``_PyObject_LookupSpecialMethod`` as
2-
:func:`types.lookup_special_method`.
1+
Expose ``_PyObject_LookupSpecial`` as :func:`types.lookup_special`.

Modules/_typesmodule.c

Lines changed: 32 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,78 +6,44 @@
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-
#include "clinic/_typesmodule.c.h"
9+
#include "pycore_typeobject.h" // _PyObject_LookupSpecial
10+
#include "pycore_modsupport.h" // _PyArg_CheckPositional
1211

13-
/*[clinic input]
14-
module _types
15-
[clinic start generated code]*/
16-
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=530308b1011b659d]*/
17-
18-
/*[clinic input]
19-
_types.lookup_special_method
20-
21-
obj: 'O'
22-
attr: 'O'
23-
/
24-
25-
Lookup special method name `attr` on `obj`.
26-
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-
... return "A.__enter__"
36-
...
37-
>>> class B:
38-
... __slots__ = ("__enter__",)
39-
... def __init__(self):
40-
... def __enter__(self):
41-
... return "B.__enter__"
42-
... self.__enter__ = __enter__
43-
...
44-
>>> a = A()
45-
>>> b = B()
46-
>>> enter_a = types.lookup_special_method(a, "__enter__")
47-
>>> enter_b = types.lookup_special_method(b, "__enter__")
48-
>>> enter_a(a)
49-
'A.__enter__'
50-
>>> enter_b(b)
51-
'B.__enter__'
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.
56-
[clinic start generated code]*/
12+
PyDoc_STRVAR(lookup_special_doc,
13+
"lookup_special(object, name[, default], /)\n\
14+
\n\
15+
Lookup method name `name` on `object` skipping the instance dictionary.\n\
16+
`name` must be a string. If the named special attribute does not\n\
17+
exist,`default` is returned if provided, otherwise AttributeError is raised.");
5718

5819
static PyObject *
59-
_types_lookup_special_method_impl(PyObject *module, PyObject *obj,
60-
PyObject *attr)
61-
/*[clinic end generated code: output=890e22cc0b8e0d34 input=e317288370125cd5]*/
20+
_types_lookup_special_impl(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
6221
{
63-
if (!PyUnicode_Check(attr)) {
22+
PyObject *v, *name, *result;
23+
24+
if (!_PyArg_CheckPositional("lookup_special", nargs, 2, 3))
25+
return NULL;
26+
27+
v = args[0];
28+
name = args[1];
29+
if (!PyUnicode_Check(name)) {
6430
PyErr_Format(PyExc_TypeError,
6531
"attribute name must be string, not '%.200s'",
66-
Py_TYPE(attr)->tp_name);
32+
Py_TYPE(name)->tp_name);
6733
return NULL;
6834
}
69-
_PyStackRef method_and_self[2];
70-
method_and_self[0] = PyStackRef_NULL;
71-
method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj);
72-
int result = _PyObject_LookupSpecialMethod(attr, method_and_self);
73-
if (result == -1) {
74-
return NULL;
75-
}
76-
if (result == 0) {
77-
Py_RETURN_NONE;
35+
result = _PyObject_LookupSpecial(v, name);
36+
if (result == NULL) {
37+
if (nargs > 2) {
38+
PyObject *dflt = args[2];
39+
return Py_NewRef(dflt);
40+
} else {
41+
PyErr_Format(PyExc_AttributeError,
42+
"'%.50s' object has no special attribute '%U'",
43+
Py_TYPE(v)->tp_name, name);
44+
}
7845
}
79-
PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]);
80-
return method;
46+
return result;
8147
}
8248

8349
static int
@@ -134,9 +100,9 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = {
134100
};
135101

136102
static PyMethodDef _typesmodule_methods[] = {
137-
_TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF
138-
{NULL, NULL, 0, NULL}
139-
};
103+
{"lookup_special", _PyCFunction_CAST(_types_lookup_special_impl),
104+
METH_FASTCALL, lookup_special_doc},
105+
{NULL, NULL, 0, NULL}};
140106

141107
static struct PyModuleDef typesmodule = {
142108
.m_base = PyModuleDef_HEAD_INIT,

0 commit comments

Comments
 (0)