309 lines
9.0 KiB
ReStructuredText
309 lines
9.0 KiB
ReStructuredText
PEP: 726
|
|
Title: Module ``__setattr__`` and ``__delattr__``
|
|
Author: Sergey B Kirpichev <skirpichev@gmail.com>
|
|
Sponsor: Adam Turner <python@quite.org.uk>
|
|
Discussions-To: https://discuss.python.org/t/32640/
|
|
Status: Rejected
|
|
Type: Standards Track
|
|
Content-Type: text/x-rst
|
|
Created: 24-Aug-2023
|
|
Python-Version: 3.13
|
|
Post-History: `06-Apr-2023 <https://discuss.python.org/t/25506/>`__,
|
|
`31-Aug-2023 <https://discuss.python.org/t/32640/>`__,
|
|
Resolution: https://discuss.python.org/t/32640/32
|
|
|
|
|
|
Abstract
|
|
========
|
|
|
|
This PEP proposes supporting user-defined ``__setattr__``
|
|
and ``__delattr__`` methods on modules to extend customization
|
|
of module attribute access beyond :pep:`562`.
|
|
|
|
Motivation
|
|
==========
|
|
|
|
There are several potential uses of a module ``__setattr__``:
|
|
|
|
1. To prevent setting an attribute at all (i.e. make it read-only)
|
|
2. To validate the value to be assigned
|
|
3. To intercept setting an attribute and update some other state
|
|
|
|
Proper support for read-only attributes would also require adding the
|
|
``__delattr__`` function to prevent their deletion.
|
|
|
|
It would be convenient to directly support such customization, by recognizing
|
|
``__setattr__`` and ``__delattr__`` methods defined in a module that would act
|
|
like normal :py:meth:`python:object.__setattr__` and
|
|
:py:meth:`python:object.__delattr__` methods, except that they will be defined
|
|
on module *instances*. Together with existing ``__getattr__`` and ``__dir__``
|
|
methods this will streamline all variants of customizing module attribute access.
|
|
|
|
For example
|
|
|
|
.. code:: python
|
|
|
|
# mplib.py
|
|
|
|
CONSTANT = 3.14
|
|
prec = 53
|
|
dps = 15
|
|
|
|
def dps_to_prec(n):
|
|
"""Return the number of bits required to represent n decimals accurately."""
|
|
return max(1, int(round((int(n)+1)*3.3219280948873626)))
|
|
|
|
def prec_to_dps(n):
|
|
"""Return the number of accurate decimals that can be represented with n bits."""
|
|
return max(1, int(round(int(n)/3.3219280948873626)-1))
|
|
|
|
def validate(n):
|
|
n = int(n)
|
|
if n <= 0:
|
|
raise ValueError('Positive integer expected')
|
|
return n
|
|
|
|
def __setattr__(name, value):
|
|
if name == 'CONSTANT':
|
|
raise AttributeError('Read-only attribute!')
|
|
if name == 'dps':
|
|
value = validate(value)
|
|
globals()['dps'] = value
|
|
globals()['prec'] = dps_to_prec(value)
|
|
return
|
|
if name == 'prec':
|
|
value = validate(value)
|
|
globals()['prec'] = value
|
|
globals()['dps'] = prec_to_dps(value)
|
|
return
|
|
globals()[name] = value
|
|
|
|
def __delattr__(name):
|
|
if name in ('CONSTANT', 'dps', 'prec'):
|
|
raise AttributeError('Read-only attribute!')
|
|
del globals()[name]
|
|
|
|
.. code:: pycon
|
|
|
|
>>> import mplib
|
|
>>> mplib.foo = 'spam'
|
|
>>> mplib.CONSTANT = 42
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: Read-only attribute!
|
|
>>> del mplib.foo
|
|
>>> del mplib.CONSTANT
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: Read-only attribute!
|
|
>>> mplib.prec
|
|
53
|
|
>>> mplib.dps
|
|
15
|
|
>>> mplib.dps = 5
|
|
>>> mplib.prec
|
|
20
|
|
>>> mplib.dps = 0
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Positive integer expected
|
|
|
|
|
|
Existing Options
|
|
================
|
|
|
|
The current workaround is assigning the ``__class__`` of a module object to a
|
|
custom subclass of :py:class:`python:types.ModuleType` (see [1]_).
|
|
|
|
For example, to prevent modification or deletion of an attribute we could use:
|
|
|
|
.. code:: python
|
|
|
|
# mod.py
|
|
|
|
import sys
|
|
from types import ModuleType
|
|
|
|
CONSTANT = 3.14
|
|
|
|
class ImmutableModule(ModuleType):
|
|
def __setattr__(name, value):
|
|
raise AttributeError('Read-only attribute!')
|
|
|
|
def __delattr__(name):
|
|
raise AttributeError('Read-only attribute!')
|
|
|
|
sys.modules[__name__].__class__ = ImmutableModule
|
|
|
|
But this variant is slower (~2x) than the proposed solution. More
|
|
importantly, it also brings a noticeable speed regression (~2-3x) for
|
|
attribute *access*.
|
|
|
|
Specification
|
|
=============
|
|
|
|
The ``__setattr__`` function at the module level should accept two
|
|
arguments, the name of an attribute and the value to be assigned,
|
|
and return :py:obj:`None` or raise an :exc:`AttributeError`.
|
|
|
|
.. code:: python
|
|
|
|
def __setattr__(name: str, value: typing.Any, /) -> None: ...
|
|
|
|
The ``__delattr__`` function should accept one argument,
|
|
the name of an attribute, and return :py:obj:`None` or raise an
|
|
:py:exc:`AttributeError`:
|
|
|
|
.. code:: python
|
|
|
|
def __delattr__(name: str, /) -> None: ...
|
|
|
|
The ``__setattr__`` and ``__delattr__`` functions are looked up in the
|
|
module ``__dict__``. If present, the appropriate function is called to
|
|
customize setting the attribute or its deletion, else the normal
|
|
mechanism (storing/deleting the value in the module dictionary) will work.
|
|
|
|
Defining module ``__setattr__`` or ``__delattr__`` only affects lookups made
|
|
using the attribute access syntax --- directly accessing the module globals
|
|
(whether by ``globals()`` within the module, or via a reference to the module's
|
|
globals dictionary) is unaffected. For example:
|
|
|
|
.. code:: pycon
|
|
|
|
>>> import mod
|
|
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
|
|
|
|
or
|
|
|
|
.. code:: python
|
|
|
|
# mod.py
|
|
|
|
def __setattr__(name, value):
|
|
...
|
|
|
|
foo = 'spam' # bypasses __setattr__
|
|
globals()['bar'] = 'spam' # here too
|
|
|
|
def f():
|
|
global x
|
|
x = 123
|
|
|
|
f() # and here
|
|
|
|
To use a module global and trigger ``__setattr__`` (or ``__delattr__``),
|
|
one can access it via ``sys.modules[__name__]`` within the module's code:
|
|
|
|
.. code:: python
|
|
|
|
# mod.py
|
|
|
|
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
|
|
|
|
def __setattr__(name, value):
|
|
...
|
|
|
|
sys.modules[__name__].bar = 'spam' # triggers __setattr__
|
|
|
|
This limitation is intentional (just as for the :pep:`562`), because the
|
|
interpreter highly optimizes access to module globals and disabling all that
|
|
and going through special methods written in Python would slow down the code
|
|
unacceptably.
|
|
|
|
|
|
How to Teach This
|
|
=================
|
|
|
|
The "Customizing module attribute access" [1]_ section of the documentation
|
|
will be expanded to include new functions.
|
|
|
|
|
|
Reference Implementation
|
|
========================
|
|
|
|
The reference implementation for this PEP can be found in `CPython PR #108261
|
|
<https://github.com/python/cpython/pull/108261>`__.
|
|
|
|
|
|
Backwards compatibility
|
|
=======================
|
|
|
|
This PEP may break code that uses module level (global) names
|
|
``__setattr__`` and ``__delattr__``, but the language reference
|
|
explicitly reserves *all* undocumented dunder names, and allows
|
|
"breakage without warning" [2]_.
|
|
|
|
The performance implications of this PEP are small, since additional
|
|
dictionary lookup is much cheaper than storing/deleting the value in
|
|
the dictionary. Also it is hard to imagine a module that expects the
|
|
user to set (and/or delete) attributes enough times to be a
|
|
performance concern. On another hand, proposed mechanism allows to
|
|
override setting/deleting of attributes without affecting speed of
|
|
attribute access, which is much more likely scenario to get a
|
|
performance penalty.
|
|
|
|
|
|
Discussion
|
|
==========
|
|
|
|
As pointed out by Victor Stinner, the proposed API could be useful already in
|
|
the stdlib, for example to ensure that :py:obj:`sys.modules` type is always a
|
|
:py:class:`dict`:
|
|
|
|
.. code:: pycon
|
|
|
|
>>> import sys
|
|
>>> sys.modules = 123
|
|
>>> import asyncio
|
|
Traceback (most recent call last):
|
|
File "<stdin>", line 1, in <module>
|
|
File "<frozen importlib._bootstrap>", line 1260, in _find_and_load
|
|
AttributeError: 'int' object has no attribute 'get'
|
|
|
|
or to prevent deletion of critical :py:mod:`sys` attributes, which makes the
|
|
code more complicated. For example, code using :py:obj:`sys.stderr` has to
|
|
check if the attribute exists and if it's not :py:obj:`None`. Currently, it's
|
|
possible to remove any :py:mod:`sys` attribute, including functions:
|
|
|
|
.. code:: pycon
|
|
|
|
>>> import sys
|
|
>>> del sys.excepthook
|
|
>>> 1+ # notice the next line
|
|
sys.excepthook is missing
|
|
File "<stdin>", line 1
|
|
1+
|
|
^
|
|
SyntaxError: invalid syntax
|
|
|
|
See `related issue
|
|
<https://github.com/python/cpython/issues/106016#issue-1771174774>`__ for
|
|
other details.
|
|
|
|
Other stdlib modules also come with attributes which can be overridden (as a
|
|
feature) and some input validation here could be helpful. Examples:
|
|
:py:obj:`threading.excepthook`, :py:obj:`warnings.showwarning`,
|
|
:py:obj:`io.DEFAULT_BUFFER_SIZE` or :py:obj:`os.SEEK_SET`.
|
|
|
|
Also a typical use case for customizing module attribute access is managing
|
|
deprecation warnings. But the :pep:`562` accomplishes this scenario only
|
|
partially: e.g. it's impossible to issue a warning during an attempt to
|
|
*change* a renamed attribute.
|
|
|
|
|
|
Footnotes
|
|
=========
|
|
|
|
.. [1] Customizing module attribute access
|
|
(https://docs.python.org/3.11/reference/datamodel.html#customizing-module-attribute-access)
|
|
|
|
.. [2] Reserved classes of identifiers
|
|
(https://docs.python.org/3.11/reference/lexical_analysis.html#reserved-classes-of-identifiers)
|
|
|
|
|
|
Copyright
|
|
=========
|
|
|
|
This document is placed in the public domain or under the
|
|
CC0-1.0-Universal license, whichever is more permissive.
|