diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 8cc1b00..bf83e96 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -276,6 +276,22 @@ class BuiltinTest(unittest.TestCase): c3 = C3() self.assertTrue(callable(c3)) + # C4 instances are not actually callable. + class C4(object): + @property + def __call__(self): + raise AttributeError('Go away!') + + self.assertFalse(callable(C4())) + + # C5 instances are callable because the descriptor returns a func. + class C5(object): + @property + def __call__(self): + return lambda: None + + self.assertTrue(callable(C5())) + def test_chr(self): self.assertEqual(chr(32), ' ') self.assertEqual(chr(65), 'A') diff --git a/Misc/NEWS b/Misc/NEWS index b7d9607..973c6e5 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,8 @@ Core and Builtins - Issue #23275: Allow assigning to an empty target list in round brackets: () = iterable. +- Issue #23990: Makes the callable builtin function respect the descriptor + protocol. Library ------- diff --git a/Objects/object.c b/Objects/object.c index cc1b2ff..9619382 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -13,6 +13,7 @@ _Py_IDENTIFIER(__bytes__); _Py_IDENTIFIER(__dir__); _Py_IDENTIFIER(__isabstractmethod__); _Py_IDENTIFIER(builtins); +_Py_IDENTIFIER(__call__); #ifdef Py_REF_DEBUG Py_ssize_t _Py_RefTotal; @@ -1264,7 +1265,17 @@ PyCallable_Check(PyObject *x) { if (x == NULL) return 0; - return x->ob_type->tp_call != NULL; + + if (Py_TYPE(x)->tp_call == NULL) + return 0; + + if (_PyObject_LookupSpecial(x, &PyId___call__) == NULL) + return 0; + + if (PyErr_ExceptionMatches(PyExc_AttributeError)) + PyErr_Clear(); + + return 1; }