diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 6438abbba10e37..b1f23fb953258e 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2007,11 +2007,15 @@ Customizing module attribute access .. index:: single: __getattr__ (module attribute) + single: __setattr__ (module attribute) + single: __delattr__ (module attribute) single: __dir__ (module attribute) single: __class__ (module attribute) -Special names ``__getattr__`` and ``__dir__`` can be also used to customize -access to module attributes. The ``__getattr__`` function at the module level +Special names ``__getattr__``, ``__setattr__``, ``__delattr__`` and ``__dir__`` +can be also used to customize access to module attributes. + +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 :exc:`AttributeError`. If an attribute is not found on a module object through the normal lookup, i.e. @@ -2019,32 +2023,41 @@ not found on a module object through the normal lookup, i.e. the module ``__dict__`` before raising an :exc:`AttributeError`. If found, it is called with the attribute name and the result is returned. +The ``__setattr__`` function should accept two arguments, respectively, the +name of an attribute and the value to be assigned. It can +raise an :exc:`AttributeError`. If present, this function overrides the +standard :func:`setattr` behaviour on a module. For example:: + + def __setattr__(name, value): + print(f'Setting {name} to {value}...') + globals()[name] = value + +The ``__delattr__`` function should accept one argument which is the name of an +attribute. It can raise an :exc:`AttributeError`. If present, +this function overrides the standard :func:`delattr` behaviour on a module. + The ``__dir__`` function should accept no arguments, and return an iterable of strings that represents the names accessible on module. If present, this function overrides the standard :func:`dir` search on a module. -For a more fine grained customization of the module behavior (setting -attributes, properties, etc.), one can set the ``__class__`` attribute of -a module object to a subclass of :class:`types.ModuleType`. For example:: +For a more fine grained customization of the module behavior, one can set the +``__class__`` attribute of a module object to a subclass of +:class:`types.ModuleType`. For example:: import sys from types import ModuleType - class VerboseModule(ModuleType): - def __repr__(self): - return f'Verbose {self.__name__}' + class CallableModule(ModuleType): + def __call__(self, *args, **kwargs): + raise RuntimeError("NO-body expects the Spanish Inquisition!") - def __setattr__(self, attr, value): - print(f'Setting {attr}...') - super().__setattr__(attr, value) - - sys.modules[__name__].__class__ = VerboseModule + sys.modules[__name__].__class__ = CallableModule .. note:: - Defining module ``__getattr__`` and setting module ``__class__`` only - affect lookups made using the attribute access syntax -- directly accessing - the module globals (whether by code within the module, or via a reference - to the module's globals dictionary) is unaffected. + Defining module ``__getattr__``, ``__setattr__``, ``__delattr__``, ``__dir__`` + and setting module ``__class__`` only affect lookups made using the attribute access + syntax -- directly accessing the module globals (whether by code within the + module, or via a reference to the module's globals dictionary) is unaffected. .. versionchanged:: 3.5 ``__class__`` module attribute is now writable. @@ -2052,11 +2065,17 @@ a module object to a subclass of :class:`types.ModuleType`. For example:: .. versionadded:: 3.7 ``__getattr__`` and ``__dir__`` module attributes. +.. versionadded:: 3.13 + ``__setattr__`` and ``__delattr__`` module attributes. + .. seealso:: :pep:`562` - Module __getattr__ and __dir__ Describes the ``__getattr__`` and ``__dir__`` functions on modules. + :pep:`726` - Module __setattr__ and __delattr__ + Proposes the ``__setattr__`` and ``__delattr__`` functions on modules. + .. _descriptors: diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 40823587fb9417..3f6dac66fc192d 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -131,6 +131,10 @@ Other Language Changes of the ``optimize`` argument. (Contributed by Irit Katriel in :gh:`108113`). +* Support customizing module attribute access with ``__setattr__`` and + ``__delattr__`` functions at the module level. (Contributed by + Sergey B Kirpichev in :gh:`108261`). + * :mod:`multiprocessing`, :mod:`concurrent.futures`, :mod:`compileall`: Replace :func:`os.cpu_count` with :func:`os.process_cpu_count` to select the default number of worker threads and processes. Get the CPU affinity diff --git a/Lib/test/test_module/__init__.py b/Lib/test/test_module/__init__.py index 98d1cbe824df12..38daad36adb0c3 100644 --- a/Lib/test/test_module/__init__.py +++ b/Lib/test/test_module/__init__.py @@ -151,6 +151,57 @@ def test_module_getattr_errors(self): if 'test.test_module.bad_getattr2' in sys.modules: del sys.modules['test.test_module.bad_getattr2'] + def test_module_setattr(self): + import test.test_module.good_setattr as gsa + self.assertEqual(gsa.foo, 1) + with self.assertRaises(AttributeError): + gsa.bar + with self.assertRaisesRegex(AttributeError, "Read-only attribute"): + gsa.foo = 2 + gsa.bar = 3 + self.assertEqual(gsa.bar, 3) + del sys.modules['test.test_module.good_setattr'] + + def test_module_setattr_errors(self): + import test.test_module.bad_setattr as bsa + from test.test_module import bad_setattr2 + self.assertEqual(bsa.foo, 1) + self.assertEqual(bad_setattr2.foo, 1) + with self.assertRaises(TypeError): + bsa.foo = 2 + with self.assertRaises(TypeError): + bad_setattr2.foo = 2 + del sys.modules['test.test_module.bad_setattr'] + if 'test.test_module.bad_setattr2' in sys.modules: + del sys.modules['test.test_module.bad_setattr2'] + + def test_module_delattr(self): + import test.test_module.good_delattr as gda + self.assertEqual(gda.foo, 1) + with self.assertRaises(AttributeError): + gda.bar + with self.assertRaisesRegex(AttributeError, "Read-only attribute"): + del gda.foo + gda.bar = 3 + self.assertEqual(gda.bar, 3) + del gda.bar + with self.assertRaises(AttributeError): + gda.bar + del sys.modules['test.test_module.good_delattr'] + + def test_module_delattr_errors(self): + import test.test_module.bad_delattr as bda + from test.test_module import bad_delattr2 + self.assertEqual(bda.foo, 1) + self.assertEqual(bad_delattr2.foo, 1) + with self.assertRaises(TypeError): + del bda.foo + with self.assertRaises(TypeError): + del bad_delattr2.foo + del sys.modules['test.test_module.bad_delattr'] + if 'test.test_module.bad_delattr2' in sys.modules: + del sys.modules['test.test_module.bad_delattr2'] + def test_module_dir(self): import test.test_module.good_getattr as gga self.assertEqual(dir(gga), ['a', 'b', 'c']) diff --git a/Lib/test/test_module/bad_delattr.py b/Lib/test/test_module/bad_delattr.py new file mode 100644 index 00000000000000..5919b7dda26003 --- /dev/null +++ b/Lib/test/test_module/bad_delattr.py @@ -0,0 +1,2 @@ +foo = 1 +__delattr__ = "Oops" diff --git a/Lib/test/test_module/bad_delattr2.py b/Lib/test/test_module/bad_delattr2.py new file mode 100644 index 00000000000000..93fc7a21774f8c --- /dev/null +++ b/Lib/test/test_module/bad_delattr2.py @@ -0,0 +1,4 @@ +foo = 1 + +def __delattr__(): + "Bad function signature" diff --git a/Lib/test/test_module/bad_setattr.py b/Lib/test/test_module/bad_setattr.py new file mode 100644 index 00000000000000..6b30f22f7715e6 --- /dev/null +++ b/Lib/test/test_module/bad_setattr.py @@ -0,0 +1,2 @@ +foo = 1 +__setattr__ = "Oops" diff --git a/Lib/test/test_module/bad_setattr2.py b/Lib/test/test_module/bad_setattr2.py new file mode 100644 index 00000000000000..efa28456b5e9c2 --- /dev/null +++ b/Lib/test/test_module/bad_setattr2.py @@ -0,0 +1,4 @@ +foo = 1 + +def __setattr__(): + "Bad function signature" diff --git a/Lib/test/test_module/good_delattr.py b/Lib/test/test_module/good_delattr.py new file mode 100644 index 00000000000000..97b29d68956c93 --- /dev/null +++ b/Lib/test/test_module/good_delattr.py @@ -0,0 +1,6 @@ +foo = 1 + +def __delattr__(name): + if name == 'foo': + raise AttributeError("Read-only attribute") + del globals()[name] diff --git a/Lib/test/test_module/good_setattr.py b/Lib/test/test_module/good_setattr.py new file mode 100644 index 00000000000000..4c28d05bbbdaf0 --- /dev/null +++ b/Lib/test/test_module/good_setattr.py @@ -0,0 +1,6 @@ +foo = 1 + +def __setattr__(name, value): + if name == 'foo': + raise AttributeError("Read-only attribute") + globals()[name] = value diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-08-22-09-57-59.gh-issue-106016.lpbUq8.rst b/Misc/NEWS.d/next/Core and Builtins/2023-08-22-09-57-59.gh-issue-106016.lpbUq8.rst new file mode 100644 index 00000000000000..17a126a5bbd592 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-08-22-09-57-59.gh-issue-106016.lpbUq8.rst @@ -0,0 +1,2 @@ +Add support for module ``__setattr__`` and ``__delattr__``. Patch by Sergey +B Kirpichev. diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 3a1c516658dce7..6a0b6ba1fbb312 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -891,6 +891,43 @@ _Py_module_getattro(PyModuleObject *m, PyObject *name) return _Py_module_getattro_impl(m, name, 0); } +static int +module_setattro(PyModuleObject *mod, PyObject *name, PyObject *value) +{ + assert(mod->md_dict != NULL); + if (value) { + PyObject *setattr; + if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__setattr__), &setattr) < 0) { + return -1; + } + if (setattr) { + PyObject *res = PyObject_CallFunctionObjArgs(setattr, name, value, NULL); + Py_DECREF(setattr); + if (res == NULL) { + return -1; + } + Py_DECREF(res); + return 0; + } + } + else { + PyObject *delattr; + if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__delattr__), &delattr) < 0) { + return -1; + } + if (delattr) { + PyObject *res = PyObject_CallFunctionObjArgs(delattr, name, NULL); + Py_DECREF(delattr); + if (res == NULL) { + return -1; + } + Py_DECREF(res); + return 0; + } + } + return PyObject_GenericSetAttr((PyObject *)mod, name, value); +} + static int module_traverse(PyModuleObject *m, visitproc visit, void *arg) { @@ -1039,7 +1076,7 @@ PyTypeObject PyModule_Type = { 0, /* tp_call */ 0, /* tp_str */ (getattrofunc)_Py_module_getattro, /* tp_getattro */ - PyObject_GenericSetAttr, /* tp_setattro */ + (setattrofunc)module_setattro, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, /* tp_flags */