
After discussion on python-ideas, it looks this PEP moves towards a favorable decision. For a recent discussion see https://mail.python.org/pipermail/python-ideas/2017-November/047806.html. The PEP is available at https://www.python.org/dev/peps/pep-0562/ The most important recent change is the addition of __dir__, as proposed by Guido.
Here is the full text:
+++++++++++++++++++++
PEP: 562 Title: Module __getattr__ and __dir__ Author: Ivan Levkivskyi levkivskyi@gmail.com Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 09-Sep-2017 Python-Version: 3.7 Post-History: 09-Sep-2017
Abstract ========
It is proposed to support a ``__getattr__`` and ``__dir__`` functions defined +on modules to provide basic customization of module attribute access.
Rationale =========
It is sometimes convenient to customize or otherwise have control over access to module attributes. A typical example is managing deprecation warnings. Typical workarounds are assigning ``__class__`` of a module object to a custom subclass of ``types.ModuleType`` or replacing the ``sys.modules`` item with a custom wrapper instance. It would be convenient to simplify this procedure by recognizing ``__getattr__`` defined directly in a module that would act like a normal ``__getattr__`` method, except that it will be defined on module *instances*. For example::
# lib.py
from warnings import warn
deprecated_names = ["old_function", ...]
def _deprecated_old_function(arg, other): ...
def __getattr__(name): if name in deprecated_names: warn(f"{name} is deprecated", DeprecationWarning) return globals()[f"_deprecated_{name}"] raise AttributeError(f"module {__name__} has no attribute {name}")
# main.py
from lib import old_function # Works, but emits the warning
Another widespread use case for ``__getattr__`` would be lazy submodule imports. Consider a simple example::
# lib/__init__.py
import importlib
__all__ = ['submod', ...]
def __getattr__(name): if name in __all__: return importlib.import_module("." + name, __name__) raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# lib/submod.py
print("Submodule loaded") class HeavyClass: ...
# main.py
import lib lib.submodule.HeavyClass # prints "Submodule loaded"
There is a related proposal PEP 549 that proposes to support instance properties for a similar functionality. The difference is this PEP proposes a faster and simpler mechanism, but provides more basic customization. An additional motivation for this proposal is that PEP 484 already defines the use of module ``__getattr__`` for this purpose in Python stub files, see [1]_.
In addition, to allow modifying result of a ``dir()`` call on a module to show deprecated and other dynamically generated attributes, it is proposed to support module level ``__dir__`` function. For example::
# lib.py
deprecated_names = ["old_function", ...] __all__ = ["new_function_one", "new_function_two", ...]
def new_function_one(arg, other): ... def new_function_two(arg, other): ...
def __dir__(): return sorted(__all__ + deprecated_names)
# main.py
import lib
dir(lib) # prints ["new_function_one", "new_function_two", "old_function", ...]
Specification =============
The ``__getattr__`` function at the module level should accept one argument which is the name of an attribute and return the computed value or raise an ``AttributeError``::
def __getattr__(name: str) -> Any: ...
This function will be called only if ``name`` is not found in the module through the normal attribute lookup.
The ``__dir__`` function should accept no arguments, and return a list of strings that represents the names accessible on module::
def __dir__() -> List[str]: ...
If present, this function overrides the standard ``dir()`` search on a module.
The reference implementation for this PEP can be found in [2]_.
Backwards compatibility and impact on performance =================================================
This PEP may break code that uses module level (global) names ``__getattr__`` and ``__dir__``. The performance implications of this PEP are minimal, since ``__getattr__`` is called only for missing attributes.
Discussion ==========
Note that the use of module ``__getattr__`` requires care to keep the referred objects pickleable. For example, the ``__name__`` attribute of a function should correspond to the name with which it is accessible via ``__getattr__``::
def keep_pickleable(func): func.__name__ = func.__name__.replace('_deprecated_', '') func.__qualname__ = func.__qualname__.replace('_deprecated_', '') return func
@keep_pickleable def _deprecated_old_function(arg, other): ...
One should be also careful to avoid recursion as one would do with a class level ``__getattr__``.
References ==========
.. [1] PEP 484 section about ``__getattr__`` in stub files (https://www.python.org/dev/peps/pep-0484/#stub-files)
.. [2] The reference implementation (https://github.com/ilevkivskyi/cpython/pull/3/files)
Copyright =========
This document has been placed in the public domain.
.. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End: