Skip to content

Commit

Permalink
PEP 726: Module __setattr__ and __delattr__ (#3301)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Turner <[email protected]>
  • Loading branch information
skirpichev and AA-Turner authored Aug 31, 2023
1 parent f70b3ba commit 43a28ef
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ pep-0721.rst @encukou
pep-0722.rst @pfmoore
pep-0723.rst @AA-Turner
pep-0725.rst @pradyunsg
pep-0726.rst @AA-Turner
pep-0727.rst @JelleZijlstra
# ...
# pep-0754.txt
Expand Down
188 changes: 188 additions & 0 deletions pep-0726.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
PEP: 726
Title: Module ``__setattr__`` and ``__delattr__``
Author: Sergey B Kirpichev <[email protected]>
Sponsor: Adam Turner <[email protected]>
Discussions-To: https://discuss.python.org/t/32640/
Status: Draft
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/>`__,


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.

A typical workaround is assigning the ``__class__`` of a module object to a
custom subclass of :py:class:`python:types.ModuleType` (see [1]_).
Unfortunately, this also brings a noticeable speed regression
(~2-3x) for attribute *access*. 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*.

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('non-negative 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: non-negative integer expected
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 ``__setattr__`` or ``__delattr__`` only affect lookups made
using the attribute access syntax---directly accessing the module
globals is unaffected, e.g. ``sys.modules[__name__].some_global = 'spam'``.


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.


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.

0 comments on commit 43a28ef

Please sign in to comment.