From 3e339aed2bc84e97b038aa07b1a2bdf5e06c5cc3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 1 Dec 2023 00:08:44 -0800 Subject: [PATCH 1/6] gh-95754: Better AttributeError on partially initialised module --- Lib/test/test_import/__init__.py | 8 ++++++++ .../data/circular_imports/import_cycle.py | 3 +++ Objects/moduleobject.c | 15 +++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 Lib/test/test_import/data/circular_imports/import_cycle.py diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 1ecac4f37fe1c1..9a8d6f2fd9082f 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -1629,6 +1629,14 @@ def test_circular_from_import(self): str(cm.exception), ) + def test_circular_import(self): + with self.assertRaisesRegex( + AttributeError, + r"partially initialized module 'test.test_import.data.circular_imports.import_cycle' " + r"from '.*' has no attribute 'some_attribute' \(most likely due to a circular import\)" + ): + import test.test_import.data.circular_imports.import_cycle + def test_absolute_circular_submodule(self): with self.assertRaises(AttributeError) as cm: import test.test_import.data.circular_imports.subpkg2.parent diff --git a/Lib/test/test_import/data/circular_imports/import_cycle.py b/Lib/test/test_import/data/circular_imports/import_cycle.py new file mode 100644 index 00000000000000..cd9507b5f69e25 --- /dev/null +++ b/Lib/test/test_import/data/circular_imports/import_cycle.py @@ -0,0 +1,3 @@ +import test.test_import.data.circular_imports.import_cycle as m + +m.some_attribute diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index bba77ce8ab7e7b..b7931b7b3eb853 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -799,7 +799,7 @@ PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { // When suppress=1, this function suppresses AttributeError. - PyObject *attr, *mod_name, *getattr; + PyObject *attr, *mod_name, *getattr, *origin; attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress); if (attr) { return attr; @@ -841,11 +841,22 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } if (suppress != 1) { if (_PyModuleSpec_IsInitializing(spec)) { - PyErr_Format(PyExc_AttributeError, + origin = PyObject_GetAttr(spec, &_Py_ID(origin)); + if (origin == NULL || origin == Py_None) { + PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' has no attribute '%U' " "(most likely due to a circular import)", mod_name, name); + } + else { + PyErr_Format(PyExc_AttributeError, + "partially initialized " + "module '%U' from '%U' has no attribute '%U' " + "(most likely due to a circular import)", + mod_name, origin, name); + } + Py_XDECREF(origin); } else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) { PyErr_Format(PyExc_AttributeError, From 722265320650fcee48d5c4880173602c33e88131 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:16:15 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst b/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst new file mode 100644 index 00000000000000..0884bc4a4be726 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst @@ -0,0 +1 @@ +Provide a better error message when accessing invalid attributes on partially initialized modules. The origin of the module being accessed is now included in the message to help with the common issue of shadowing other modules. From a1c7198e4dee1e07357a42ccdebf45deefc5c4ac Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 1 Dec 2023 12:49:32 -0800 Subject: [PATCH 3/6] code review --- Objects/moduleobject.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b7931b7b3eb853..e25418508edb5c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -841,22 +841,26 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } if (suppress != 1) { if (_PyModuleSpec_IsInitializing(spec)) { - origin = PyObject_GetAttr(spec, &_Py_ID(origin)); - if (origin == NULL || origin == Py_None) { - PyErr_Format(PyExc_AttributeError, - "partially initialized " - "module '%U' has no attribute '%U' " - "(most likely due to a circular import)", - mod_name, name); + int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + // check if origin is a str object + if (valid_spec == 1 && !PyUnicode_Check(origin)) { + valid_spec = 0; + Py_DECREF(origin); } - else { + if (valid_spec == 1) { PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' from '%U' has no attribute '%U' " "(most likely due to a circular import)", mod_name, origin, name); + Py_DECREF(origin); + } else { + PyErr_Format(PyExc_AttributeError, + "partially initialized " + "module '%U' has no attribute '%U' " + "(most likely due to a circular import)", + mod_name, name); } - Py_XDECREF(origin); } else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) { PyErr_Format(PyExc_AttributeError, From 3bac475f7dbc83c410fe3839ddf860ae8235fb01 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 4 Dec 2023 02:05:59 -0800 Subject: [PATCH 4/6] return null --- Objects/moduleobject.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e25418508edb5c..c24e909ae1dae5 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -842,7 +842,11 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (suppress != 1) { if (_PyModuleSpec_IsInitializing(spec)) { int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); - // check if origin is a str object + if (valid_spec == -1) { + Py_XDECREF(spec); + Py_DECREF(mod_name); + return NULL; + } if (valid_spec == 1 && !PyUnicode_Check(origin)) { valid_spec = 0; Py_DECREF(origin); From e043b30ed0df319066909571b21dc6b0f6c9a8c1 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 18 Dec 2023 01:01:52 -0600 Subject: [PATCH 5/6] rebase changes --- Objects/moduleobject.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 5cce5af95fe3ff..f751bff7900068 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -831,11 +831,30 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (suppress != 1) { int rc = _PyModuleSpec_IsInitializing(spec); if (rc > 0) { - PyErr_Format(PyExc_AttributeError, + int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + if (valid_spec == -1) { + Py_XDECREF(spec); + Py_DECREF(mod_name); + return NULL; + } + if (valid_spec == 1 && !PyUnicode_Check(origin)) { + valid_spec = 0; + Py_DECREF(origin); + } + if (valid_spec == 1) { + PyErr_Format(PyExc_AttributeError, + "partially initialized " + "module '%U' from '%U' has no attribute '%U' " + "(most likely due to a circular import)", + mod_name, origin, name); + Py_DECREF(origin); + } else { + PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' has no attribute '%U' " "(most likely due to a circular import)", mod_name, name); + } } else if (rc == 0) { rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); From 5e27a8f7f170a0ec9753bbd099637aef508b8dfc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 18 Dec 2023 10:54:34 +0200 Subject: [PATCH 6/6] Update Objects/moduleobject.c --- Objects/moduleobject.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f751bff7900068..3a1c516658dce7 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -848,7 +848,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) "(most likely due to a circular import)", mod_name, origin, name); Py_DECREF(origin); - } else { + } + else { PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' has no attribute '%U' "