PEP 713: Callable Modules (#3117)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
parent
8c61071df1
commit
fc60c98e22
|
@ -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
|
||||
# ...
|
||||
|
|
|
@ -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
|
||||
|
|
|
|
@ -0,0 +1,187 @@
|
|||
PEP: 713
|
||||
Title: Callable Modules
|
||||
Author: Amethyst Reese <amethyst at n7.gg>
|
||||
Sponsor: Łukasz Langa <lukasz at python.org>
|
||||
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 <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.
|
||||
|
Loading…
Reference in New Issue