Skip to content

Commit c45d916

Browse files
committed
pythongh-106016: Support customizing of module attributes access with __setattr__/__delattr__
1 parent 58f9c63 commit c45d916

10 files changed

Lines changed: 141 additions & 18 deletions

File tree

‎Doc/reference/datamodel.rst‎

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,51 +1695,67 @@ Customizing module attribute access
16951695

16961696
.. index::
16971697
single: __getattr__ (module attribute)
1698+
single: __setattr__ (module attribute)
1699+
single: __delattr__ (module attribute)
16981700
single: __dir__ (module attribute)
16991701
single: __class__ (module attribute)
17001702

1701-
Special names ``__getattr__`` and ``__dir__`` can be also used to customize
1702-
access to module attributes. The ``__getattr__`` function at the module level
1703+
Special names ``__getattr__``, ``__setattr__``, ``__delattr__`` and ``__dir__``
1704+
can be also used to customize access to module attributes.
1705+
1706+
The ``__getattr__`` function at the module level
17031707
should accept one argument which is the name of an attribute and return the
17041708
computed value or raise an :exc:`AttributeError`. If an attribute is
17051709
not found on a module object through the normal lookup, i.e.
17061710
:meth:`object.__getattribute__`, then ``__getattr__`` is searched in
17071711
the module ``__dict__`` before raising an :exc:`AttributeError`. If found,
17081712
it is called with the attribute name and the result is returned.
17091713

1714+
The ``__setattr__`` function should accept two arguments, respectively, the
1715+
name of an attribute and the value to be assigned and return :const:`None` or
1716+
raise an :exc:`AttributeError`. If present, this function overrides the
1717+
standard :func:`setattr` behaviour on a module. For example::
1718+
1719+
def __setattr__(name, value):
1720+
print(f'Setting {name} to {value}...')
1721+
globals()[name] = value
1722+
1723+
The ``__delattr__`` function should accept one argument which is the name of an
1724+
attribute and return :const:`None` or raise an :exc:`AttributeError`. If present,
1725+
this function overrides the standard :func:`delattr` behaviour on a module.
1726+
17101727
The ``__dir__`` function should accept no arguments, and return a sequence of
17111728
strings that represents the names accessible on module. If present, this
17121729
function overrides the standard :func:`dir` search on a module.
17131730

1714-
For a more fine grained customization of the module behavior (setting
1715-
attributes, properties, etc.), one can set the ``__class__`` attribute of
1716-
a module object to a subclass of :class:`types.ModuleType`. For example::
1731+
For a more fine grained customization of the module behavior, one can set the
1732+
``__class__`` attribute of a module object to a subclass of
1733+
:class:`types.ModuleType`. For example::
17171734

17181735
import sys
17191736
from types import ModuleType
17201737

1721-
class VerboseModule(ModuleType):
1722-
def __repr__(self):
1723-
return f'Verbose {self.__name__}'
1724-
1725-
def __setattr__(self, attr, value):
1726-
print(f'Setting {attr}...')
1727-
super().__setattr__(attr, value)
1738+
class CallableModule(ModuleType):
1739+
def __call__(self, *args, **kwargs):
1740+
raise RuntimeError("NO-body expects the Spanish Inquisition!")
17281741

1729-
sys.modules[__name__].__class__ = VerboseModule
1742+
sys.modules[__name__].__class__ = CallableModule
17301743

17311744
.. note::
1732-
Defining module ``__getattr__`` and setting module ``__class__`` only
1733-
affect lookups made using the attribute access syntax -- directly accessing
1734-
the module globals (whether by code within the module, or via a reference
1735-
to the module's globals dictionary) is unaffected.
1745+
Defining module ``__getattr__``/``__setattr__``/``__delattr__`` and setting
1746+
module ``__class__`` only affect lookups made using the attribute access
1747+
syntax -- directly accessing the module globals (whether by code within the
1748+
module, or via a reference to the module's globals dictionary) is unaffected.
17361749

17371750
.. versionchanged:: 3.5
17381751
``__class__`` module attribute is now writable.
17391752

17401753
.. versionadded:: 3.7
17411754
``__getattr__`` and ``__dir__`` module attributes.
17421755

1756+
.. versionadded:: 3.13
1757+
``__setattr__`` and ``__delattr__`` module attributes.
1758+
17431759
.. seealso::
17441760

17451761
:pep:`562` - Module __getattr__ and __dir__

‎Lib/test/bad_delattr.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
foo = 1
2+
__delattr__ = "Oops"

‎Lib/test/bad_delattr2.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
foo = 1
2+
3+
def __delattr__():
4+
"Bad function signature"

‎Lib/test/bad_setattr.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
foo = 1
2+
__setattr__ = "Oops"

‎Lib/test/bad_setattr2.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
foo = 1
2+
3+
def __setattr__():
4+
"Bad function signature"

‎Lib/test/good_delattr.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
foo = 1
2+
3+
def __delattr__(name):
4+
if name == 'foo':
5+
raise AttributeError("Read-only attribute")
6+
del globals()[name]

‎Lib/test/good_setattr.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
foo = 1
2+
3+
def __setattr__(name, value):
4+
if name == 'foo':
5+
raise AttributeError("Read-only attribute")
6+
globals()[name] = value

‎Lib/test/test_module.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,53 @@ def test_module_getattr_errors(self):
150150
if 'test.bad_getattr2' in sys.modules:
151151
del sys.modules['test.bad_getattr2']
152152

153+
def test_module_setattr(self):
154+
import test.good_setattr as gsa
155+
self.assertEqual(gsa.foo, 1)
156+
with self.assertRaises(AttributeError):
157+
gsa.bar
158+
with self.assertRaisesRegex(AttributeError, "Read-only attribute"):
159+
gsa.foo = 2
160+
gsa.bar = 3
161+
self.assertEqual(gsa.bar, 3)
162+
163+
def test_module_setattr_errors(self):
164+
import test.bad_setattr as bsa
165+
from test import bad_setattr2
166+
self.assertEqual(bsa.foo, 1)
167+
self.assertEqual(bad_setattr2.foo, 1)
168+
with self.assertRaises(TypeError):
169+
bsa.foo = 2
170+
with self.assertRaises(TypeError):
171+
bad_setattr2.foo = 2
172+
del sys.modules['test.bad_setattr']
173+
del sys.modules['test.bad_setattr2']
174+
175+
def test_module_delattr(self):
176+
import test.good_delattr as gda
177+
self.assertEqual(gda.foo, 1)
178+
with self.assertRaises(AttributeError):
179+
gda.bar
180+
with self.assertRaisesRegex(AttributeError, "Read-only attribute"):
181+
del gda.foo
182+
gda.bar = 3
183+
self.assertEqual(gda.bar, 3)
184+
del gda.bar
185+
with self.assertRaises(AttributeError):
186+
gda.bar
187+
188+
def test_module_delattr_errors(self):
189+
import test.bad_delattr as bda
190+
from test import bad_delattr2
191+
self.assertEqual(bda.foo, 1)
192+
self.assertEqual(bad_delattr2.foo, 1)
193+
with self.assertRaises(TypeError):
194+
del bda.foo
195+
with self.assertRaises(TypeError):
196+
del bad_delattr2.foo
197+
del sys.modules['test.bad_delattr']
198+
del sys.modules['test.bad_delattr2']
199+
153200
def test_module_dir(self):
154201
import test.good_getattr as gga
155202
self.assertEqual(dir(gga), ['a', 'b', 'c'])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for module ``__setattr__`` and ``__delattr__``. Patch by Sergey
2+
B Kirpichev.

‎Objects/moduleobject.c‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,40 @@ _Py_module_getattro(PyModuleObject *m, PyObject *name)
866866
return _Py_module_getattro_impl(m, name, 0);
867867
}
868868

869+
static int
870+
module_setattro(PyModuleObject *m, PyObject *name, PyObject *value)
871+
{
872+
PyObject *setattr, *delattr;
873+
assert(m->md_dict != NULL);
874+
PyErr_Clear();
875+
if (value == NULL) {
876+
delattr = PyDict_GetItemWithError(m->md_dict, &_Py_ID(__delattr__));
877+
if (PyErr_Occurred()) {
878+
return -1;
879+
}
880+
if (delattr) {
881+
PyObject_CallFunctionObjArgs(delattr, name, NULL);
882+
if (PyErr_Occurred()) {
883+
return -1;
884+
}
885+
return 0;
886+
}
887+
} else {
888+
setattr = PyDict_GetItemWithError(m->md_dict, &_Py_ID(__setattr__));
889+
if (PyErr_Occurred()) {
890+
return -1;
891+
}
892+
if (setattr) {
893+
PyObject_CallFunctionObjArgs(setattr, name, value, NULL);
894+
if (PyErr_Occurred()) {
895+
return -1;
896+
}
897+
return 0;
898+
}
899+
}
900+
return PyObject_GenericSetAttr((PyObject *)m, name, value);
901+
}
902+
869903
static int
870904
module_traverse(PyModuleObject *m, visitproc visit, void *arg)
871905
{
@@ -1018,7 +1052,7 @@ PyTypeObject PyModule_Type = {
10181052
0, /* tp_call */
10191053
0, /* tp_str */
10201054
(getattrofunc)_Py_module_getattro, /* tp_getattro */
1021-
PyObject_GenericSetAttr, /* tp_setattro */
1055+
(setattrofunc)module_setattro, /* tp_setattro */
10221056
0, /* tp_as_buffer */
10231057
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
10241058
Py_TPFLAGS_BASETYPE, /* tp_flags */

0 commit comments

Comments
 (0)