Skip to content

Instantly share code, notes, and snippets.

@1st1
Last active September 6, 2018 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1st1/fd13f74f43fba0f6cc7b2d19f58772fd to your computer and use it in GitHub Desktop.
Save 1st1/fd13f74f43fba0f6cc7b2d19f58772fd to your computer and use it in GitHub Desktop.
Backport PEP 562 module-level __getattr__ to Python 2.7
diff --git a/Lib/test/bad_getattr.py b/Lib/test/bad_getattr.py
new file mode 100644
index 0000000000..b4f790f3bf
--- /dev/null
+++ b/Lib/test/bad_getattr.py
@@ -0,0 +1,3 @@
+x = 1
+
+__getattr__ = "Surprise!"
diff --git a/Lib/test/bad_getattr2.py b/Lib/test/bad_getattr2.py
new file mode 100644
index 0000000000..21fa74f0ba
--- /dev/null
+++ b/Lib/test/bad_getattr2.py
@@ -0,0 +1,4 @@
+def __getattr__():
+ "Bad one"
+
+x = 1
diff --git a/Lib/test/good_getattr.py b/Lib/test/good_getattr.py
new file mode 100644
index 0000000000..1b8bb5305a
--- /dev/null
+++ b/Lib/test/good_getattr.py
@@ -0,0 +1,8 @@
+x = 1
+
+def __getattr__(name):
+ if name == "yolo" or name == "__warningregistry__":
+ raise AttributeError("Deprecated, use whatever instead")
+ return "There is {name}".format(name=name)
+
+y = 2
diff --git a/Lib/test/test_module.py b/Lib/test/test_module.py
index 21eaf3ed72..acab944d1f 100644
--- a/Lib/test/test_module.py
+++ b/Lib/test/test_module.py
@@ -6,6 +6,26 @@ import sys
ModuleType = type(sys)
class ModuleTests(unittest.TestCase):
+ def test_module_getattr(self):
+ import test.good_getattr as gga
+ from test.good_getattr import test
+ self.assertEqual(test, "There is test")
+ self.assertEqual(gga.x, 1)
+ self.assertEqual(gga.y, 2)
+ with self.assertRaises(AttributeError):
+ gga.yolo
+ self.assertEqual(gga.whatever, "There is whatever")
+
+ def test_module_getattr_errors(self):
+ import test.bad_getattr as bga
+ from test import bad_getattr2
+ self.assertEqual(bga.x, 1)
+ self.assertEqual(bad_getattr2.x, 1)
+ with self.assertRaises(TypeError):
+ bga.nope
+ with self.assertRaises(TypeError):
+ bad_getattr2.nope
+
def test_uninitialized(self):
# An uninitialized module has no __dict__ or __name__,
# and __doc__ is None
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index f2fed30e90..d5c68d1e51 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -202,6 +202,33 @@ module_repr(PyModuleObject *m)
return PyString_FromFormat("<module '%s' from '%s'>", name, filename);
}
+static PyObject*
+module_getattro(PyModuleObject *m, PyObject *name)
+{
+ PyObject *attr;
+
+ attr = PyObject_GenericGetAttr((PyObject *)m, name);
+ if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError)) {
+ return attr;
+ }
+
+ PyErr_Clear();
+ if (m->md_dict) {
+ PyObject *getattr = PyDict_GetItemString(m->md_dict, "__getattr__");
+ if (getattr) {
+ Py_INCREF(getattr);
+ attr = PyObject_CallFunctionObjArgs(getattr, name, NULL);
+ Py_DECREF(getattr);
+ return attr;
+ }
+ }
+
+ PyErr_Format(PyExc_AttributeError,
+ "'module' object has no attribute '%.400s'",
+ PyString_AS_STRING(name));
+ return NULL;
+}
+
/* We only need a traverse function, no clear function: If the module
is in a cycle, md_dict will be cleared as well, which will break
the cycle. */
@@ -235,7 +262,7 @@ PyTypeObject PyModule_Type = {
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
- PyObject_GenericGetAttr, /* tp_getattro */
+ (getattrofunc)module_getattro, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment