Skip to content

Commit 5364b5c

Browse files
authored
bpo-32225: Implementation of PEP 562 (#4731)
Implement PEP 562: module __getattr__ and __dir__. The implementation simply updates module_getattro and module_dir.
1 parent 9e7c136 commit 5364b5c

File tree

9 files changed

+161
-4
lines changed

9 files changed

+161
-4
lines changed

Doc/reference/datamodel.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,51 @@ access (use of, assignment to, or deletion of ``x.name``) for class instances.
15121512
returned. :func:`dir` converts the returned sequence to a list and sorts it.
15131513

15141514

1515+
Customizing module attribute access
1516+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1517+
1518+
.. index::
1519+
single: __getattr__ (module attribute)
1520+
single: __dir__ (module attribute)
1521+
single: __class__ (module attribute)
1522+
1523+
Special names ``__getattr__`` and ``__dir__`` can be also used to customize
1524+
access to module attributes. The ``__getattr__`` function at the module level
1525+
should accept one argument which is the name of an attribute and return the
1526+
computed value or raise an :exc:`AttributeError`. If an attribute is
1527+
not found on a module object through the normal lookup, i.e.
1528+
:meth:`object.__getattribute__`, then ``__getattr__`` is searched in
1529+
the module ``__dict__`` before raising an :exc:`AttributeError`. If found,
1530+
it is called with the attribute name and the result is returned.
1531+
1532+
The ``__dir__`` function should accept no arguments, and return a list of
1533+
strings that represents the names accessible on module. If present, this
1534+
function overrides the standard :func:`dir` search on a module.
1535+
1536+
For a more fine grained customization of the module behavior (setting
1537+
attributes, properties, etc.), one can set the ``__class__`` attribute of
1538+
a module object to a subclass of :class:`types.ModuleType`. For example::
1539+
1540+
import sys
1541+
from types import ModuleType
1542+
1543+
class VerboseModule(ModuleType):
1544+
def __repr__(self):
1545+
return f'Verbose {self.__name__}'
1546+
1547+
def __setattr__(self, attr, value):
1548+
print(f'Setting {attr}...')
1549+
setattr(self, attr, value)
1550+
1551+
sys.modules[__name__].__class__ = VerboseModule
1552+
1553+
.. note::
1554+
Defining module ``__getattr__`` and setting module ``__class__`` only
1555+
affect lookups made using the attribute access syntax -- directly accessing
1556+
the module globals (whether by code within the module, or via a reference
1557+
to the module's globals dictionary) is unaffected.
1558+
1559+
15151560
.. _descriptors:
15161561

15171562
Implementing Descriptors

Doc/whatsnew/3.7.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,24 @@ effort will be made to add such support.
159159
PEP written by Erik M. Bray; implementation by Masayuki Yamamoto.
160160

161161

162+
PEP 562: Customization of access to module attributes
163+
-----------------------------------------------------
164+
165+
It is sometimes convenient to customize or otherwise have control over access
166+
to module attributes. A typical example is managing deprecation warnings.
167+
Typical workarounds are assigning ``__class__`` of a module object to
168+
a custom subclass of :class:`types.ModuleType` or replacing the ``sys.modules``
169+
item with a custom wrapper instance. This procedure is now simplified by
170+
recognizing ``__getattr__`` defined directly in a module that would act like
171+
a normal ``__getattr__`` method, except that it will be defined on module
172+
*instances*.
173+
174+
.. seealso::
175+
176+
:pep:`562` -- Module ``__getattr__`` and ``__dir__``
177+
PEP written and implemented by Ivan Levkivskyi
178+
179+
162180
PEP 564: Add new time functions with nanosecond resolution
163181
----------------------------------------------------------
164182

Lib/test/bad_getattr.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
x = 1
2+
3+
__getattr__ = "Surprise!"
4+
__dir__ = "Surprise again!"

Lib/test/bad_getattr2.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def __getattr__():
2+
"Bad one"
3+
4+
x = 1
5+
6+
def __dir__(bad_sig):
7+
return []

Lib/test/bad_getattr3.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def __getattr__(name):
2+
if name != 'delgetattr':
3+
raise AttributeError
4+
del globals()['__getattr__']
5+
raise AttributeError

Lib/test/good_getattr.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
x = 1
2+
3+
def __dir__():
4+
return ['a', 'b', 'c']
5+
6+
def __getattr__(name):
7+
if name == "yolo":
8+
raise AttributeError("Deprecated, use whatever instead")
9+
return f"There is {name}"
10+
11+
y = 2

Lib/test/test_module.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,57 @@ def test_weakref(self):
125125
gc_collect()
126126
self.assertIs(wr(), None)
127127

128+
def test_module_getattr(self):
129+
import test.good_getattr as gga
130+
from test.good_getattr import test
131+
self.assertEqual(test, "There is test")
132+
self.assertEqual(gga.x, 1)
133+
self.assertEqual(gga.y, 2)
134+
with self.assertRaisesRegex(AttributeError,
135+
"Deprecated, use whatever instead"):
136+
gga.yolo
137+
self.assertEqual(gga.whatever, "There is whatever")
138+
del sys.modules['test.good_getattr']
139+
140+
def test_module_getattr_errors(self):
141+
import test.bad_getattr as bga
142+
from test import bad_getattr2
143+
self.assertEqual(bga.x, 1)
144+
self.assertEqual(bad_getattr2.x, 1)
145+
with self.assertRaises(TypeError):
146+
bga.nope
147+
with self.assertRaises(TypeError):
148+
bad_getattr2.nope
149+
del sys.modules['test.bad_getattr']
150+
if 'test.bad_getattr2' in sys.modules:
151+
del sys.modules['test.bad_getattr2']
152+
153+
def test_module_dir(self):
154+
import test.good_getattr as gga
155+
self.assertEqual(dir(gga), ['a', 'b', 'c'])
156+
del sys.modules['test.good_getattr']
157+
158+
def test_module_dir_errors(self):
159+
import test.bad_getattr as bga
160+
from test import bad_getattr2
161+
with self.assertRaises(TypeError):
162+
dir(bga)
163+
with self.assertRaises(TypeError):
164+
dir(bad_getattr2)
165+
del sys.modules['test.bad_getattr']
166+
if 'test.bad_getattr2' in sys.modules:
167+
del sys.modules['test.bad_getattr2']
168+
169+
def test_module_getattr_tricky(self):
170+
from test import bad_getattr3
171+
# these lookups should not crash
172+
with self.assertRaises(AttributeError):
173+
bad_getattr3.one
174+
with self.assertRaises(AttributeError):
175+
bad_getattr3.delgetattr
176+
if 'test.bad_getattr3' in sys.modules:
177+
del sys.modules['test.bad_getattr3']
178+
128179
def test_module_repr_minimal(self):
129180
# reprs when modules have no __file__, __name__, or __loader__
130181
m = ModuleType('foo')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PEP 562: Add support for module ``__getattr__`` and ``__dir__``. Implemented by Ivan
2+
Levkivskyi.

Objects/moduleobject.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -679,12 +679,19 @@ module_repr(PyModuleObject *m)
679679
static PyObject*
680680
module_getattro(PyModuleObject *m, PyObject *name)
681681
{
682-
PyObject *attr, *mod_name;
682+
PyObject *attr, *mod_name, *getattr;
683683
attr = PyObject_GenericGetAttr((PyObject *)m, name);
684-
if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError))
684+
if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError)) {
685685
return attr;
686+
}
686687
PyErr_Clear();
687688
if (m->md_dict) {
689+
_Py_IDENTIFIER(__getattr__);
690+
getattr = _PyDict_GetItemId(m->md_dict, &PyId___getattr__);
691+
if (getattr) {
692+
PyObject* stack[1] = {name};
693+
return _PyObject_FastCall(getattr, stack, 1);
694+
}
688695
_Py_IDENTIFIER(__name__);
689696
mod_name = _PyDict_GetItemId(m->md_dict, &PyId___name__);
690697
if (mod_name && PyUnicode_Check(mod_name)) {
@@ -730,8 +737,15 @@ module_dir(PyObject *self, PyObject *args)
730737
PyObject *dict = _PyObject_GetAttrId(self, &PyId___dict__);
731738

732739
if (dict != NULL) {
733-
if (PyDict_Check(dict))
734-
result = PyDict_Keys(dict);
740+
if (PyDict_Check(dict)) {
741+
PyObject *dirfunc = PyDict_GetItemString(dict, "__dir__");
742+
if (dirfunc) {
743+
result = _PyObject_CallNoArg(dirfunc);
744+
}
745+
else {
746+
result = PyDict_Keys(dict);
747+
}
748+
}
735749
else {
736750
const char *name = PyModule_GetName(self);
737751
if (name)

0 commit comments

Comments
 (0)