From 3ed910831e9ee65b436a80ab9573dcf3c140131c Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 27 Apr 2023 07:09:30 +0300 Subject: [PATCH 01/10] PEP 726: Module __setattr__ and __delattr__ --- .github/CODEOWNERS | 1 + pep-0726.rst | 171 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 pep-0726.rst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 53755f393c0..f040e08ac0b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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-0754.txt # ... diff --git a/pep-0726.rst b/pep-0726.rst new file mode 100644 index 00000000000..d81ce1f92ab --- /dev/null +++ b/pep-0726.rst @@ -0,0 +1,171 @@ +PEP: 726 +Title: Module __setattr__ and __delattr__ +Author: Sergey B Kirpichev +Sponsor: Adam Turner +Status: Draft +Type: Standards Track +Content-Type: text/x-rst +Created: 24-Aug-2023 +Python-Version: 3.13 +Post-History: `06-Apr-2023 `__ + + +Abstract +======== + +It is proposed to support ``__setattr__`` and ``__delattr__`` methods +defined 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 attribute at all (make one 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 +``__delattr__`` helper function to prevent their deletion as well. + +Typical workaround is assigning ``__class__`` of a module object to a +custom subclass of :py:class:`python:types.ModuleType` (see [1]_). +Unfortunately, this also coming with a noticiable speed regression +(~2-3x) for attribute *access*. It would be convenient to directly +support such customizations, e.g. having a clear way to make module +attributes effectively read-only, by recognizing +``__setattr__``/``__delattr__`` methods defined in a module that would +act like a normal +:py:meth:`python:object.__setattr__`/:py:meth:`python:object.__delattr__` +methods, except that they will be defined on module *instances*. + +For example:: + + # cat 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] + + # python -q + >>> 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, respectively, the name of an attribute and the value to be +assigned, and return :py:obj:`None` or raise an :exc:`AttributeError`:: + + def __setattr__(name: str, value: typing.Any) -> None: ... + +The ``__delattr__`` function should accept one argument which is the +name of an attribute and return :py:obj:`None` or raise an :exc:`AttributeError`:: + + def __delattr__(name: str): -> None: ... + +The ``__setattr__``/``__delattr__`` functions are searched in the +module ``__dict__``. If present, suitable function is called to +customize setting of the attribute or it's deletion, else the normal +mechanism (storing/deleting the value in the module dictionary) will work. + +Defining ``__setattr__``/``__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'``. + + +Reference Implementation +======================== + +The reference implementation for this PEP can be found in `CPython PR #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 has been placed in the public domain. From 8f6afd3cc5baac152aabbca65f533572ca7e6a89 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 24 Aug 2023 07:31:01 +0300 Subject: [PATCH 02/10] PEP 726: Fix some typos --- pep-0726.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pep-0726.rst b/pep-0726.rst index d81ce1f92ab..8c229f162d5 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -22,7 +22,7 @@ Motivation There are several potential uses of a module ``__setattr__``: -1. To prevent setting attribute at all (make one read-only) +1. To prevent setting an attribute at all (make one read-only) 2. To validate the value to be assigned 3. To intercept setting an attribute and update some other state @@ -31,7 +31,7 @@ Proper support for read-only attributes would also require adding Typical workaround is assigning ``__class__`` of a module object to a custom subclass of :py:class:`python:types.ModuleType` (see [1]_). -Unfortunately, this also coming with a noticiable speed regression +Unfortunately, this also coming with a noticeable speed regression (~2-3x) for attribute *access*. It would be convenient to directly support such customizations, e.g. having a clear way to make module attributes effectively read-only, by recognizing From b11629d8c4fe9f2c6a8d4cc1b63b0648e3da9293 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 31 Aug 2023 02:54:40 +0100 Subject: [PATCH 03/10] Editorial review --- pep-0726.rst | 144 +++++++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/pep-0726.rst b/pep-0726.rst index 8c229f162d5..de20e8e3828 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -1,87 +1,88 @@ PEP: 726 -Title: Module __setattr__ and __delattr__ +Title: Module ``__setattr__`` and ``__delattr__`` Author: Sergey B Kirpichev Sponsor: Adam Turner +Discussions-To: Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 24-Aug-2023 Python-Version: 3.13 -Post-History: `06-Apr-2023 `__ +Post-History: `06-Apr-2023 `__ Abstract ======== -It is proposed to support ``__setattr__`` and ``__delattr__`` methods -defined on modules to extend customization of module attribute access -beyond :pep:`562`. +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 (make one read-only) +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 -``__delattr__`` helper function to prevent their deletion as well. +Proper support for read-only attributes would also require adding the +``__delattr__`` function to prevent their deletion. -Typical workaround is assigning ``__class__`` of a module object to a +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 coming with a noticeable speed regression +Unfortunately, this also brings a noticeable speed regression (~2-3x) for attribute *access*. It would be convenient to directly -support such customizations, e.g. having a clear way to make module -attributes effectively read-only, by recognizing -``__setattr__``/``__delattr__`` methods defined in a module that would -act like a normal -:py:meth:`python:object.__setattr__`/:py:meth:`python:object.__delattr__` +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:: - - # cat 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] - - # python -q +For example + +.. code:: python + + 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 @@ -110,23 +111,28 @@ Specification ============= The ``__setattr__`` function at the module level should accept two -arguments, respectively, the name of an attribute and the value to be -assigned, and return :py:obj:`None` or raise an :exc:`AttributeError`:: +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: ... - 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`: -The ``__delattr__`` function should accept one argument which is the -name of an attribute and return :py:obj:`None` or raise an :exc:`AttributeError`:: +.. code:: python - def __delattr__(name: str): -> None: ... + def __delattr__(name: str, /): -> None: ... -The ``__setattr__``/``__delattr__`` functions are searched in the -module ``__dict__``. If present, suitable function is called to -customize setting of the attribute or it's deletion, else the normal +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__``/``__delattr__`` only affect lookups made -using the attribute access syntax --- directly accessing the module +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'``. @@ -134,7 +140,7 @@ Reference Implementation ======================== The reference implementation for this PEP can be found in `CPython PR #108261 -`_. +`__. Backwards compatibility From 68578f6694c460586557c1ea5fdf945f80318cad Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 31 Aug 2023 05:21:11 +0300 Subject: [PATCH 04/10] Update Copyright section --- pep-0726.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pep-0726.rst b/pep-0726.rst index de20e8e3828..139ee8880fb 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -174,4 +174,5 @@ Footnotes Copyright ========= -This document has been placed in the public domain. +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From be382d69b0fe4c679c7ed4b184f8e31c3211bf2f Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 31 Aug 2023 06:04:35 +0300 Subject: [PATCH 05/10] Added "How to Teach This" section --- pep-0726.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pep-0726.rst b/pep-0726.rst index 139ee8880fb..34bfa43ec24 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -136,6 +136,13 @@ 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 ======================== From 2d066781a883711e99e024e70627a4d574d99f70 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 31 Aug 2023 06:10:37 +0300 Subject: [PATCH 06/10] Restore comment in the example --- pep-0726.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pep-0726.rst b/pep-0726.rst index 34bfa43ec24..2243f85ca39 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -43,6 +43,8 @@ For example .. code:: python + # mlib.py + CONSTANT = 3.14 prec = 53 dps = 15 From f1b05e41231a89cadb71566e4fcbdbaf2ca64a7c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 31 Aug 2023 04:29:04 +0100 Subject: [PATCH 07/10] Syntax --- pep-0726.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pep-0726.rst b/pep-0726.rst index 2243f85ca39..5ac855f07dd 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -126,7 +126,7 @@ the name of an attribute, and return :py:obj:`None` or raise an .. code:: python - def __delattr__(name: str, /): -> None: ... + 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 From 452612921ac29c6e4a092e0df92f167232ad421f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 31 Aug 2023 04:29:44 +0100 Subject: [PATCH 08/10] Synchronise module names --- pep-0726.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pep-0726.rst b/pep-0726.rst index 5ac855f07dd..a7629228281 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -43,7 +43,7 @@ For example .. code:: python - # mlib.py + # mplib.py CONSTANT = 3.14 prec = 53 From 7d9e8b2e2b85d559ee9d4477b62d7e6735572eaf Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 31 Aug 2023 06:37:37 +0300 Subject: [PATCH 09/10] Update Discussions-To & Post-History --- pep-0726.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pep-0726.rst b/pep-0726.rst index a7629228281..f74919f939e 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -2,13 +2,14 @@ PEP: 726 Title: Module ``__setattr__`` and ``__delattr__`` Author: Sergey B Kirpichev Sponsor: Adam Turner -Discussions-To: +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 `__ +Post-History: `06-Apr-2023 `__, + `31-Aug-2023 `__ Abstract From 3ba3b3755d4749aa4cdb3d3fb2491cf3914a421a Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 31 Aug 2023 04:39:48 +0100 Subject: [PATCH 10/10] Add trailing comma --- pep-0726.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pep-0726.rst b/pep-0726.rst index f74919f939e..20b821ad386 100644 --- a/pep-0726.rst +++ b/pep-0726.rst @@ -9,7 +9,7 @@ Content-Type: text/x-rst Created: 24-Aug-2023 Python-Version: 3.13 Post-History: `06-Apr-2023 `__, - `31-Aug-2023 `__ + `31-Aug-2023 `__, Abstract