diff --git a/Doc/c-api/module.rst b/Doc/c-api/module.rst index c0351c8a6c72aa..4ab5afe7ca3147 100644 --- a/Doc/c-api/module.rst +++ b/Doc/c-api/module.rst @@ -123,6 +123,12 @@ Module Objects unencodable filenames, use :c:func:`PyModule_GetFilenameObject` instead. +.. c:function:: int PyModule_Callable(PyObject *module) + + Return true if *module* is a module object, and has a + :attr:`~object.__call__` property that is also callable. + + .. _initializing-modules: Initializing C modules diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 555564ec73b4a2..1bb6adec66a0dd 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -34,6 +34,7 @@ PyAPI_FUNC(int) _PyModuleSpec_IsInitializing(PyObject *); #endif PyAPI_FUNC(PyModuleDef*) PyModule_GetDef(PyObject*); PyAPI_FUNC(void*) PyModule_GetState(PyObject*); +PyAPI_FUNC(int) PyModule_Callable(PyObject *); #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 /* New in 3.5 */ diff --git a/Lib/test/callable_module_a.py b/Lib/test/callable_module_a.py new file mode 100644 index 00000000000000..25e533091333fe --- /dev/null +++ b/Lib/test/callable_module_a.py @@ -0,0 +1,6 @@ +""" +PEP 713 Callable Module - def __call__() +""" + +def __call__(value: int = 42, *, power: int = 1): + return value ** power diff --git a/Lib/test/callable_module_b.py b/Lib/test/callable_module_b.py new file mode 100644 index 00000000000000..1ccf9b4210b086 --- /dev/null +++ b/Lib/test/callable_module_b.py @@ -0,0 +1,8 @@ +""" +PEP 713 Callable Module - __call__ = func +""" + +def func(value: int = 42, *, power: int = 1): + return value ** power + +__call__ = func diff --git a/Lib/test/callable_module_c.py b/Lib/test/callable_module_c.py new file mode 100644 index 00000000000000..4e6e8a20584eaf --- /dev/null +++ b/Lib/test/callable_module_c.py @@ -0,0 +1,11 @@ +""" +PEP 713 Callable Module - __call__ = func +""" + +def func(value: int = 42, *, power: int = 1): + return value ** power + +def __getattr__(name: str): + if name == "__call__": + return func + raise AttributeError diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index aab7b1580eaf35..dfc4f411b73ab0 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -945,5 +945,48 @@ def test_function_with_many_args(self): self.assertEqual(l['f'](*range(N)), N//2) +class TestPEP590(unittest.TestCase): + def test_sys_not_callable(self): + import sys + + self.assertFalse(callable(sys)) + self.assertFalse(hasattr(sys, "__call__")) + self.assertRaisesRegex(TypeError, "Module 'sys' is not callable", sys) + + def test_sys_add_callable(self): + try: + def exponent(value=42, *, power=1): + return value ** power + + sys.__call__ = exponent + + self.assertTrue(callable(sys)) + self.assertTrue(hasattr(sys, "__call__")) + + self.assertEqual(sys(), 42) + self.assertEqual(sys(3), 3) + self.assertEqual(sys(power=2), 1764) + + finally: + if hasattr(sys, "__call__"): + del sys.__call__ + + def test_callable_modules(self): + from . import callable_module_a, callable_module_b, callable_module_c + + for name, module in ( + ("via def __call__", callable_module_a), + ("via __call__ = func", callable_module_b), + ("via __getattr__", callable_module_c), + ): + with self.subTest(name): + self.assertTrue(callable(module)) + self.assertTrue(hasattr(module, "__call__")) + + self.assertEqual(module(), 42) + self.assertEqual(module(3), 3) + self.assertEqual(module(power=2), 1764) + + if __name__ == "__main__": unittest.main() diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index a0be19a3ca8ac8..e2bd1830e76151 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -743,7 +743,16 @@ module_getattro(PyModuleObject *m, PyObject *name) { PyObject *attr, *mod_name, *getattr; attr = PyObject_GenericGetAttr((PyObject *)m, name); - if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError)) { + if (attr) { + // If the module does not explicitly set __call__, do not return + // the generic method-wrapper for tp_call in its place + if (!(Py_TYPE(attr) == &_PyMethodWrapper_Type && + _PyUnicode_EqualToASCIIString(name, "__call__"))) + { + return attr; + } + } + else if (!PyErr_ExceptionMatches(PyExc_AttributeError)) { return attr; } PyErr_Clear(); @@ -928,6 +937,38 @@ module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignor return ret; } +int +PyModule_Callable(PyObject *mod) +{ + if (PyModule_CheckExact(mod)) { + PyObject *mod_call = PyObject_GetAttr(mod, &_Py_ID(__call__)); + if (mod_call) { + return PyCallable_Check(mod_call); + } + else if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + } + + return 0; +} + +static PyObject * +module_call(PyModuleObject *mod, PyObject *args, PyObject *kwargs) +{ + PyObject *callable = PyObject_GetAttr((PyObject *)mod, &_Py_ID(__call__)); + if (callable == NULL) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Format(PyExc_TypeError, + "Module '%.200s' is not callable", + PyModule_GetName((PyObject *)mod)); + } + return NULL; + } + + return PyObject_Call(callable, args, kwargs); +} static PyGetSetDef module_getsets[] = { {"__annotations__", (getter)module_get_annotations, (setter)module_set_annotations}, @@ -949,7 +990,7 @@ PyTypeObject PyModule_Type = { 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ - 0, /* tp_call */ + (ternaryfunc)module_call, /* tp_call */ 0, /* tp_str */ (getattrofunc)module_getattro, /* tp_getattro */ PyObject_GenericSetAttr, /* tp_setattro */ diff --git a/Objects/object.c b/Objects/object.c index 65c296e9340601..6996c1cd30bdca 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1668,6 +1668,9 @@ PyCallable_Check(PyObject *x) { if (x == NULL) return 0; + if (PyModule_CheckExact(x)) { + return PyModule_Callable(x); + } return Py_TYPE(x)->tp_call != NULL; }