diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c9229fc2..509042eb7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -593,6 +593,7 @@ pep-0709.rst @carljm pep-0710.rst @dstufft pep-0711.rst @njsmith pep-0712.rst @ericvsmith +pep-0713.rst @ambv # ... # pep-0754.txt # ... diff --git a/AUTHOR_OVERRIDES.csv b/AUTHOR_OVERRIDES.csv index 72289408f..ba4e681a2 100644 --- a/AUTHOR_OVERRIDES.csv +++ b/AUTHOR_OVERRIDES.csv @@ -10,3 +10,4 @@ Martin v. Löwis,"von Löwis, Martin",von Löwis Nathaniel Smith,"Smith, Nathaniel J.",Smith P.J. Eby,"Eby, Phillip J.",Eby Germán Méndez Bravo,"Méndez Bravo, Germán",Méndez Bravo +Amethyst Reese,"Reese, Amethyst",Amethyst diff --git a/pep-0713.rst b/pep-0713.rst new file mode 100644 index 000000000..e3c1bdd8c --- /dev/null +++ b/pep-0713.rst @@ -0,0 +1,187 @@ +PEP: 713 +Title: Callable Modules +Author: Amethyst Reese +Sponsor: Łukasz Langa +Status: Draft +Type: Standards Track +Content-Type: text/x-rst +Created: 20-Apr-2023 +Python-Version: 3.12 + + +Abstract +======== + +Modules are currently not directly callable. Classes can define a ``__call__`` +method that makes instance objects callable, but defining a similarly named +function in the global module scope has no effect, and that function can +only be called by importing or referencing it directly as ``module.__call__``. +:pep:`562` added support for :meth:`~object.__getattr__` and :meth:`~object.__dir__` for modules, but +defining ``__getattr__`` to return a value for ``__call__`` still does not +make a module callable. + +This PEP proposes support for making modules directly callable by defining +a ``__call__`` object in the module's global namespace, either as a standard +function, or an arbitrary callable object. + + +Motivation +========== + +Many modules have only a single primary interface to their functionality. +In many cases, that interface is a single callable object, where being able +to import and use the module directly as a callable provides a more "Pythonic" +interface for users:: + + # user.py + + import fancy + + @fancy + def func(...): + ... + +Currently, providing this style of interface requires modifying the module +object at runtime to make it callable. + +This is commonly done by replacing the module object in ``sys.modules`` with +a callable alternative (such as a function or class instance):: + + # fancy.py + + def fancy(...): + ... + + sys.modules[__name__] = fancy + +This has the effect of making the original module effectively unreachable +without further hooks from the author, even with ``from module import member``. +It also results in a "module" object that is missing all of the special module +attributes, including ``__doc__``, ``__package__``, ``__path__``, etc. + +Alteratively, a module author can choose to override the module's ``__class__`` +property with a custom type that provides a callable interface:: + + # fancy.py + + def fancy(...): + ... + + class FancyModule(types.ModuleType): + def __call__(self, ...): + return fancy(...) + + sys.modules[__name__].__class__ = FancyModule + +The downside of either approach is that it not only results in extra +boilerplate, but also results in type checker failures because they don't +recognize that the module is callable at runtime: + +.. code-block:: console + + $ mypy user.py + user.py:3: error: Module not callable [operator] + Found 1 error in 1 file (checked 1 source file) + + +Specification +============= + +When a module object is called, and a ``__call__`` object is found (either +as the result of a ``__getattr__`` or ``__dict__`` lookup), then that object +will be called with the given arguments. + +If a ``__call__`` object is not found, then a ``TypeError`` will be raised, +matching the existing behavior. + +All of these examples would be considered valid, callable modules: + +.. code-block:: python + + # hello.py + + def __call__(...): + pass + +.. code-block:: python + + # hello.py + + class Hello: + pass + + __call__ = Hello + +.. code-block:: python + + # hello.py + + def hello(): + pass + + def __getattr__(name): + if name == "__call__": + return hello + +The first two styles should generally be preferred, as it allows for easier +static analysis from tools like type checkers, though the third form would be +allowed in order to make the implementation more consistent. + +The intent is to allow arbitrary callable object to be assigned to the module's +``__call__`` property or returned by the module's ``__getattr__`` method, +enabling module authors to pick the most suitable mechanism for making their +module callable by users. + + +Backwards Compatibility and Impact on Performance +================================================= + +This PEP is not expected to cause any backwards incompatibility. Any modules +that already contain a ``__call__`` object will continue to function the same +as before, though with the additional ability to be called directly. It is +considered unlikely that modules with an existing ``__call__`` object would +depend on the existing behavior of raising ``TypeError`` when called. + +Performance implications of this PEP are minimal, as it defines a new interface. +Calling a module would trigger a lookup for the name ``__call__`` on a module +object. Existing workarounds for creating callable modules already depend on +this behavior for generic objects, resulting in similar performance for these +callable modules. + +Type checkers will likely need to be updated accordingly to treat modules with +a ``__call__`` object as callable. This should be possible to support in type +checkers when checking code targeted at older Python versions that do not +support callable modules, with the expectation that these modules would also +include one of the workarounds mentioned earlier to make the module callable. + + +How to Teach This +================= + +The documentation for :external+python:ref:`callable types ` will +include modules in the list, with a link to :meth:`~object.__call__`. +The :external+python:ref:`callable-types` documentation will include a section +covering callable modules, with example code, similar to the section for +`customizing module attribute access`__. + +__ https://docs.python.org/3/reference/datamodel.html#customizing-module-attribute-access + + +Rejected Ideas +============== + +Given the introduction of ``__getattr__`` and ``__dir__``, and the proposal +to enable use of ``__call__``, it was considered if it was worth allowing use +of *all* :external+python:ref:`specialnames` for modules, such as ``__or__`` +and ``__iter__``. While this would not be completely undesired, it increases +the potential for backward compatibility concerns, and these other special +methods are likely to provide less utility to library authors in comparison +to ``__call__``. + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. +