Skip to content

WIP gh-103741 - PEP 713 - Callable modules #103742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
6 changes: 6 additions & 0 deletions Doc/c-api/module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in Include/cpython/*.h or in an #ifndef Py_LIMITED_API block.
Or, if you want to add it to the limited API, see docs in the devguide.


#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000
/* New in 3.5 */
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/callable_module_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
PEP 713 Callable Module - def __call__()
"""

def __call__(value: int = 42, *, power: int = 1):
return value ** power
8 changes: 8 additions & 0 deletions Lib/test/callable_module_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
PEP 713 Callable Module - __call__ = func
"""

def func(value: int = 42, *, power: int = 1):
return value ** power

__call__ = func
11 changes: 11 additions & 0 deletions Lib/test/callable_module_c.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer inlining these instead of polluting the Lib/test namespace with the added files. Alternatively, put them in a subdirectory.


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()
45 changes: 43 additions & 2 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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},
Expand All @@ -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 */
Expand Down
3 changes: 3 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,9 @@ PyCallable_Check(PyObject *x)
{
if (x == NULL)
return 0;
if (PyModule_CheckExact(x)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This excludes subclasses of ModuleType.

return PyModule_Callable(x);
}
return Py_TYPE(x)->tp_call != NULL;
}

Expand Down