From afe03daed019e4c28be9a4019e5081d46f961a86 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 00:08:59 -0800 Subject: [PATCH 01/48] Refactor to reduce nesting --- Objects/moduleobject.c | 106 ++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 3a1c516658dce7..e2cd42105406d3 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -819,68 +819,68 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) Py_DECREF(getattr); return result; } + + if (suppress == 1) { + return NULL; + } if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__name__), &mod_name) < 0) { return NULL; } - if (mod_name && PyUnicode_Check(mod_name)) { - PyObject *spec; - if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__spec__), &spec) < 0) { + if (!mod_name || !PyUnicode_Check(mod_name)) { + Py_XDECREF(mod_name); + PyErr_Format(PyExc_AttributeError, + "module has no attribute '%U'", name); + return NULL; + } + PyObject *spec; + if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__spec__), &spec) < 0) { + Py_DECREF(mod_name); + return NULL; + } + int rc = _PyModuleSpec_IsInitializing(spec); + if (rc > 0) { + int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + if (valid_spec == -1) { + Py_XDECREF(spec); Py_DECREF(mod_name); return NULL; } - if (suppress != 1) { - int rc = _PyModuleSpec_IsInitializing(spec); - if (rc > 0) { - 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); - if (rc > 0) { - PyErr_Format(PyExc_AttributeError, - "cannot access submodule '%U' of module '%U' " - "(most likely due to a circular import)", - name, mod_name); - } - else if (rc == 0) { - PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U'", - mod_name, name); - } - } + 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); } - Py_XDECREF(spec); - Py_DECREF(mod_name); - return NULL; } - Py_XDECREF(mod_name); - if (suppress != 1) { - PyErr_Format(PyExc_AttributeError, - "module has no attribute '%U'", name); + else if (rc == 0) { + rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); + if (rc > 0) { + PyErr_Format(PyExc_AttributeError, + "cannot access submodule '%U' of module '%U' " + "(most likely due to a circular import)", + name, mod_name); + } + else if (rc == 0) { + PyErr_Format(PyExc_AttributeError, + "module '%U' has no attribute '%U'", + mod_name, name); + } } + Py_XDECREF(spec); + Py_DECREF(mod_name); return NULL; } From ae438a4f81358a455cc38d6e20aa689d1a4188cc Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 00:20:10 -0800 Subject: [PATCH 02/48] Check for script shadowing stdlib --- Lib/test/test_import/__init__.py | 42 ++++++++++ Objects/moduleobject.c | 139 ++++++++++++++++++++++++------- 2 files changed, 150 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 7b0126226c4aba..56e2c68ade7d28 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -790,6 +790,48 @@ def test_issue105979(self): self.assertIn("Frozen object named 'x' is invalid", str(cm.exception)) + def test_cwd_script_shadowing_stdlib(self): + with CleanImport('collections'): + import collections + collections.__spec__ = types.SimpleNamespace() + collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist' \(most " + r"likely due to '.*collections.py' shadowing the standard " + r"library module named 'collections'\)" + ): + collections.does_not_exist + + def test_shadowing_stdlib_edge_cases(self): + with CleanImport('collections'): + import collections + collections.__spec__ = types.SimpleNamespace() + collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + with CleanImport('sys'): + import sys + sys.stdlib_module_names = None + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'" + ): + collections.does_not_exist + + del sys.stdlib_module_names + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'" + ): + collections.does_not_exist + with CleanImport("os"), CleanImport('os.path'): + import os as clean_os + del clean_os.path.dirname + with self.assertRaisesRegex( + AttributeError, + r"module '.*path' has no attribute 'dirname'" + ): + collections.does_not_exist + @skip_if_dont_write_bytecode class FilePermissionTests(unittest.TestCase): diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e2cd42105406d3..38c6ba33641a73 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -788,7 +788,7 @@ PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { // When suppress=1, this function suppresses AttributeError. - PyObject *attr, *mod_name, *getattr, *origin; + PyObject *attr, *mod_name, *getattr; attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress); if (attr) { return attr; @@ -837,48 +837,125 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) Py_DECREF(mod_name); return NULL; } - int rc = _PyModuleSpec_IsInitializing(spec); - if (rc > 0) { - int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); - if (valid_spec == -1) { - Py_XDECREF(spec); + PyObject *origin = NULL; + if (spec) { + int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + if (rc == -1) { + Py_DECREF(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); + if (rc == 1 && !PyUnicode_Check(origin)) { Py_DECREF(origin); + origin = NULL; } - else { - PyErr_Format(PyExc_AttributeError, - "partially initialized " - "module '%U' has no attribute '%U' " - "(most likely due to a circular import)", - mod_name, name); + } + + int is_script_shadowing_stdlib = 0; + // Check mod.__name__ in sys.stdlib_module_names + // and os.path.dirname(mod.__spec__.origin) == os.getcwd() + PyObject *stdlib = NULL; + if (origin) { + // Checks against mod_name are to avoid bad recursion + if ( + PyUnicode_CompareWithASCIIString(mod_name, "sys") != 0 + && PyUnicode_CompareWithASCIIString(mod_name, "builtins") != 0 + ) { + stdlib = _PyImport_GetModuleAttrString("sys", "stdlib_module_names"); + if (!stdlib) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + } else { + goto done; + } + } + if (stdlib && PyFrozenSet_Check(stdlib) && PySet_Contains(stdlib, mod_name)) { + if ( + PyUnicode_CompareWithASCIIString(mod_name, "os") != 0 + && PyUnicode_CompareWithASCIIString(mod_name, "posixpath") != 0 + && PyUnicode_CompareWithASCIIString(mod_name, "ntpath") != 0 + ) { + PyObject *os_path = _PyImport_GetModuleAttrString("os", "path"); + if (!os_path) { + goto done; + } + PyObject *dirname = PyObject_GetAttrString(os_path, "dirname"); + Py_DECREF(os_path); + if (!dirname) { + goto done; + } + PyObject *origin_dir = _PyObject_CallOneArg(dirname, origin); + Py_DECREF(dirname); + if (!origin_dir) { + goto done; + } + + PyObject *getcwd = _PyImport_GetModuleAttrString("os", "getcwd"); + if (!getcwd) { + Py_DECREF(origin_dir); + goto done; + } + PyObject *cwd = _PyObject_CallNoArgs(getcwd); + Py_DECREF(getcwd); + if (!cwd) { + Py_DECREF(origin_dir); + goto done; + } + + is_script_shadowing_stdlib = PyObject_RichCompareBool(origin_dir, cwd, Py_EQ); + Py_DECREF(origin_dir); + Py_DECREF(cwd); + if (is_script_shadowing_stdlib < 0) { + goto done; + } + } + } } } - else if (rc == 0) { - rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); + + if (is_script_shadowing_stdlib == 1) { + PyErr_Format(PyExc_AttributeError, + "module '%U' has no attribute '%U' " + "(most likely due to '%U' shadowing the standard library " + "module named '%U')", + mod_name, name, origin, mod_name); + } else { + int rc = _PyModuleSpec_IsInitializing(spec); if (rc > 0) { - PyErr_Format(PyExc_AttributeError, - "cannot access submodule '%U' of module '%U' " - "(most likely due to a circular import)", - name, mod_name); + if (origin) { + 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); + } + 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) { - PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U'", - mod_name, name); + rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); + if (rc > 0) { + PyErr_Format(PyExc_AttributeError, + "cannot access submodule '%U' of module '%U' " + "(most likely due to a circular import)", + name, mod_name); + } + else if (rc == 0) { + PyErr_Format(PyExc_AttributeError, + "module '%U' has no attribute '%U'", + mod_name, name); + } } } + +done: + Py_XDECREF(stdlib); + Py_XDECREF(origin); Py_XDECREF(spec); Py_DECREF(mod_name); return NULL; From 0933591b580efd8cbb58463d061d5fb784424f67 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 01:58:51 -0800 Subject: [PATCH 03/48] Add What's New --- Doc/whatsnew/3.13.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 3ab6d1ddc6ef21..622741202a4d0c 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -94,6 +94,24 @@ Improved Error Messages variables. See also :ref:`using-on-controlling-color`. (Contributed by Pablo Galindo Salgado in :gh:`112730`.) +* A common mistake is to write a script with the same name as a + standard library module. The interpreter now detects this and + displays a more helpful error message. + (Contributed by Shantanu Jain in :gh:`95754`.) + + .. code-block:: shell-session + + $ python random.py + Traceback (most recent call last): + File "/home/random.py", line 1, in + import random; print(random.randint(5)) + ^^^^^^^^^^^^^ + File "/home/random.py", line 1, in + import random; print(random.randint(5)) + ^^^^^^^^^^^^^^ + AttributeError: module 'random' has no attribute 'randint' (most likely due to '/home/random.py' shadowing the standard library module named 'random') + + Other Language Changes ====================== From 7f24b99b14452997cc90235c6cd364f393a0fe96 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 19:37:10 -0800 Subject: [PATCH 04/48] Avoid calling into Python --- Lib/test/test_import/__init__.py | 38 ++++--------- Objects/moduleobject.c | 98 ++++++++++++++------------------ 2 files changed, 55 insertions(+), 81 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 56e2c68ade7d28..11af07a4e7782b 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -803,34 +803,20 @@ def test_cwd_script_shadowing_stdlib(self): ): collections.does_not_exist - def test_shadowing_stdlib_edge_cases(self): with CleanImport('collections'): import collections - collections.__spec__ = types.SimpleNamespace() - collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') - with CleanImport('sys'): - import sys - sys.stdlib_module_names = None - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'" - ): - collections.does_not_exist - - del sys.stdlib_module_names - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'" - ): - collections.does_not_exist - with CleanImport("os"), CleanImport('os.path'): - import os as clean_os - del clean_os.path.dirname - with self.assertRaisesRegex( - AttributeError, - r"module '.*path' has no attribute 'dirname'" - ): - collections.does_not_exist + del collections.__spec__.origin + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'$" + ): + collections.does_not_exist + del collections.__spec__ + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'$" + ): + collections.does_not_exist @skip_if_dont_write_bytecode diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 38c6ba33641a73..ac28b7f4373f6c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_call.h" // _PyObject_CallNoArgs() +#include "pycore_fileutils.h" // _Py_wgetcwd #include "pycore_interp.h" // PyInterpreterState.importlib #include "pycore_modsupport.h" // _PyModule_CreateInitialized() #include "pycore_moduleobject.h" // _PyModule_GetDef() @@ -10,6 +11,8 @@ #include "pycore_pyerrors.h" // _PyErr_FormatFromCause() #include "pycore_pystate.h" // _PyInterpreterState_GET() +#include "osdefs.h" // MAXPATHLEN +#include "Python/stdlib_module_names.h" // _Py_stdlib_module_names static PyMemberDef module_members[] = { @@ -784,6 +787,20 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } +// TODO: deduplicate with suggestions.c. Where should this go? +static bool +is_name_stdlib_module(PyObject* name) +{ + const char* the_name = PyUnicode_AsUTF8(name); + Py_ssize_t len = Py_ARRAY_LENGTH(_Py_stdlib_module_names); + for (Py_ssize_t i = 0; i < len; i++) { + if (strcmp(the_name, _Py_stdlib_module_names[i]) == 0) { + return 1; + } + } + return 0; +} + PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { @@ -854,62 +871,34 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) int is_script_shadowing_stdlib = 0; // Check mod.__name__ in sys.stdlib_module_names // and os.path.dirname(mod.__spec__.origin) == os.getcwd() - PyObject *stdlib = NULL; - if (origin) { - // Checks against mod_name are to avoid bad recursion - if ( - PyUnicode_CompareWithASCIIString(mod_name, "sys") != 0 - && PyUnicode_CompareWithASCIIString(mod_name, "builtins") != 0 - ) { - stdlib = _PyImport_GetModuleAttrString("sys", "stdlib_module_names"); - if (!stdlib) { - if (PyErr_ExceptionMatches(PyExc_AttributeError)) { - PyErr_Clear(); - } else { - goto done; - } + if (origin && is_name_stdlib_module(mod_name)) { + wchar_t cwdbuf[MAXPATHLEN]; + if(_Py_wgetcwd(cwdbuf, MAXPATHLEN)) { + PyObject *cwd = PyUnicode_FromWideChar(cwdbuf, wcslen(cwdbuf)); + if (!cwd) { + goto done; } - if (stdlib && PyFrozenSet_Check(stdlib) && PySet_Contains(stdlib, mod_name)) { - if ( - PyUnicode_CompareWithASCIIString(mod_name, "os") != 0 - && PyUnicode_CompareWithASCIIString(mod_name, "posixpath") != 0 - && PyUnicode_CompareWithASCIIString(mod_name, "ntpath") != 0 - ) { - PyObject *os_path = _PyImport_GetModuleAttrString("os", "path"); - if (!os_path) { - goto done; - } - PyObject *dirname = PyObject_GetAttrString(os_path, "dirname"); - Py_DECREF(os_path); - if (!dirname) { - goto done; - } - PyObject *origin_dir = _PyObject_CallOneArg(dirname, origin); - Py_DECREF(dirname); - if (!origin_dir) { - goto done; - } - - PyObject *getcwd = _PyImport_GetModuleAttrString("os", "getcwd"); - if (!getcwd) { - Py_DECREF(origin_dir); - goto done; - } - PyObject *cwd = _PyObject_CallNoArgs(getcwd); - Py_DECREF(getcwd); - if (!cwd) { - Py_DECREF(origin_dir); - goto done; - } - - is_script_shadowing_stdlib = PyObject_RichCompareBool(origin_dir, cwd, Py_EQ); - Py_DECREF(origin_dir); - Py_DECREF(cwd); - if (is_script_shadowing_stdlib < 0) { - goto done; - } - } + const char sep_char = SEP; + PyObject *sep = PyUnicode_FromStringAndSize(&sep_char, 1); + if (!sep) { + Py_DECREF(cwd); + goto done; + } + PyObject *parts = PyUnicode_RPartition(origin, sep); + Py_DECREF(sep); + if (!parts) { + Py_DECREF(cwd); + goto done; + } + int rc = PyUnicode_Compare(cwd, PyTuple_GET_ITEM(parts, 0)); + if (rc == -1 && PyErr_Occurred()) { + Py_DECREF(parts); + Py_DECREF(cwd); + goto done; } + is_script_shadowing_stdlib = rc == 0; + Py_DECREF(parts); + Py_DECREF(cwd); } } @@ -954,7 +943,6 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } done: - Py_XDECREF(stdlib); Py_XDECREF(origin); Py_XDECREF(spec); Py_DECREF(mod_name); From f073672f96271f24690b4ac45b4a7dce40662ddb Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 7 Jan 2024 03:38:38 +0000 Subject: [PATCH 05/48] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst b/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst new file mode 100644 index 00000000000000..30800cb94418c1 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst @@ -0,0 +1 @@ +Improve the error message when a script in the current directory shadows a module from the standard library. From 5e229685c9ec39ce20aeaa7921e4f6d5ea954cd3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 19:55:35 -0800 Subject: [PATCH 06/48] just use wchar everywhere --- Objects/moduleobject.c | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index ac28b7f4373f6c..b1d8c108056d78 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -12,7 +12,7 @@ #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "osdefs.h" // MAXPATHLEN -#include "Python/stdlib_module_names.h" // _Py_stdlib_module_names +#include "../Python/stdlib_module_names.h" // _Py_stdlib_module_names static PyMemberDef module_members[] = { @@ -872,33 +872,19 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) // Check mod.__name__ in sys.stdlib_module_names // and os.path.dirname(mod.__spec__.origin) == os.getcwd() if (origin && is_name_stdlib_module(mod_name)) { - wchar_t cwdbuf[MAXPATHLEN]; - if(_Py_wgetcwd(cwdbuf, MAXPATHLEN)) { - PyObject *cwd = PyUnicode_FromWideChar(cwdbuf, wcslen(cwdbuf)); - if (!cwd) { + wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; + if(_Py_wgetcwd(cwd, MAXPATHLEN)) { + int rc = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); + if (rc < 0) { goto done; } - const char sep_char = SEP; - PyObject *sep = PyUnicode_FromStringAndSize(&sep_char, 1); - if (!sep) { - Py_DECREF(cwd); - goto done; - } - PyObject *parts = PyUnicode_RPartition(origin, sep); - Py_DECREF(sep); - if (!parts) { - Py_DECREF(cwd); - goto done; - } - int rc = PyUnicode_Compare(cwd, PyTuple_GET_ITEM(parts, 0)); - if (rc == -1 && PyErr_Occurred()) { - Py_DECREF(parts); - Py_DECREF(cwd); - goto done; + wchar_t *sep = wcsrchr(origin_dirname, SEP); + if (sep) { + *sep = L'\0'; + if (wcscmp(cwd, origin_dirname) == 0) { + is_script_shadowing_stdlib = 1; + } } - is_script_shadowing_stdlib = rc == 0; - Py_DECREF(parts); - Py_DECREF(cwd); } } From 4db6e6cb52abac624c1b8f826ebf3a3767928763 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 6 Jan 2024 21:10:08 -0800 Subject: [PATCH 07/48] fix warning --- Objects/moduleobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b1d8c108056d78..d6aed358ae0133 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -874,8 +874,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (origin && is_name_stdlib_module(mod_name)) { wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; if(_Py_wgetcwd(cwd, MAXPATHLEN)) { - int rc = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); - if (rc < 0) { + if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { goto done; } wchar_t *sep = wcsrchr(origin_dirname, SEP); From 2150dc538ce54a8cb5a02acdff80b19c9f28a4c4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 21 Jan 2024 15:41:01 -0800 Subject: [PATCH 08/48] do stdlib module name check via PySys --- Objects/moduleobject.c | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index d6aed358ae0133..7e9691dc487162 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -12,7 +12,6 @@ #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "osdefs.h" // MAXPATHLEN -#include "../Python/stdlib_module_names.h" // _Py_stdlib_module_names static PyMemberDef module_members[] = { @@ -787,20 +786,6 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } -// TODO: deduplicate with suggestions.c. Where should this go? -static bool -is_name_stdlib_module(PyObject* name) -{ - const char* the_name = PyUnicode_AsUTF8(name); - Py_ssize_t len = Py_ARRAY_LENGTH(_Py_stdlib_module_names); - for (Py_ssize_t i = 0; i < len; i++) { - if (strcmp(the_name, _Py_stdlib_module_names[i]) == 0) { - return 1; - } - } - return 0; -} - PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { @@ -871,17 +856,20 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) int is_script_shadowing_stdlib = 0; // Check mod.__name__ in sys.stdlib_module_names // and os.path.dirname(mod.__spec__.origin) == os.getcwd() - if (origin && is_name_stdlib_module(mod_name)) { - wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; - if(_Py_wgetcwd(cwd, MAXPATHLEN)) { - if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { - goto done; - } - wchar_t *sep = wcsrchr(origin_dirname, SEP); - if (sep) { - *sep = L'\0'; - if (wcscmp(cwd, origin_dirname) == 0) { - is_script_shadowing_stdlib = 1; + if (origin) { + PyObject *stdlib = PySys_GetObject("stdlib_module_names"); + if (stdlib && PyAnySet_Check(stdlib) && PySet_Contains(stdlib, mod_name)) { + wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; + if(_Py_wgetcwd(cwd, MAXPATHLEN)) { + if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { + goto done; + } + wchar_t *sep = wcsrchr(origin_dirname, SEP); + if (sep) { + *sep = L'\0'; + if (wcscmp(cwd, origin_dirname) == 0) { + is_script_shadowing_stdlib = 1; + } } } } From 0e1e3d8fd4225c356abd641353702bca166a60c9 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 21 Jan 2024 16:05:13 -0800 Subject: [PATCH 09/48] add back test case --- Lib/test/test_import/__init__.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 11af07a4e7782b..735ad47fb4917f 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -795,6 +795,7 @@ def test_cwd_script_shadowing_stdlib(self): import collections collections.__spec__ = types.SimpleNamespace() collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + with self.assertRaisesRegex( AttributeError, r"module 'collections' has no attribute 'does_not_exist' \(most " @@ -803,6 +804,28 @@ def test_cwd_script_shadowing_stdlib(self): ): collections.does_not_exist + def test_shadowing_stdlib_edge_cases(self): + with CleanImport('collections'): + import collections + collections.__spec__ = types.SimpleNamespace() + collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + + with CleanImport('sys'): + import sys + sys.stdlib_module_names = None + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'" + ): + collections.does_not_exist + + del sys.stdlib_module_names + with self.assertRaisesRegex( + AttributeError, + r"module 'collections' has no attribute 'does_not_exist'" + ): + collections.does_not_exist + with CleanImport('collections'): import collections del collections.__spec__.origin From 2486d002332e1ee557b2639f65427ef219c20ae1 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 21 Jan 2024 16:44:21 -0800 Subject: [PATCH 10/48] CleanImport sys is not enough --- Lib/test/test_import/__init__.py | 51 +++++++++++++++++--------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 735ad47fb4917f..49392ea6cbeffe 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -804,30 +804,6 @@ def test_cwd_script_shadowing_stdlib(self): ): collections.does_not_exist - def test_shadowing_stdlib_edge_cases(self): - with CleanImport('collections'): - import collections - collections.__spec__ = types.SimpleNamespace() - collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') - - with CleanImport('sys'): - import sys - sys.stdlib_module_names = None - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'" - ): - collections.does_not_exist - - del sys.stdlib_module_names - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'" - ): - collections.does_not_exist - - with CleanImport('collections'): - import collections del collections.__spec__.origin with self.assertRaisesRegex( AttributeError, @@ -841,6 +817,33 @@ def test_shadowing_stdlib_edge_cases(self): ): collections.does_not_exist + def test_shadowing_stdlib_sys_edge_cases(self): + program = textwrap.dedent(''' +import collections +import os +import types +collections.__spec__ = types.SimpleNamespace() +collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') +import sys +sys.stdlib_module_names = None +try: + collections.does_not_exist +except AttributeError as e: + print(str(e)) + +del sys.stdlib_module_names +try: + collections.does_not_exist +except AttributeError as e: + print(str(e)) +''') + popen = script_helper.spawn_python('-c', program) + stdout, stderr = popen.communicate() + self.assertEqual(stdout.splitlines(), [ + b"module 'collections' has no attribute 'does_not_exist'", + b"module 'collections' has no attribute 'does_not_exist'", + ]) + @skip_if_dont_write_bytecode class FilePermissionTests(unittest.TestCase): From 9f0955d8262f9203b9012834b7ff8e278d2b0a20 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 22 Jan 2024 00:45:46 -0800 Subject: [PATCH 11/48] handle set error --- Objects/moduleobject.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 7e9691dc487162..2f32f30232cb6a 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -858,20 +858,26 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) // and os.path.dirname(mod.__spec__.origin) == os.getcwd() if (origin) { PyObject *stdlib = PySys_GetObject("stdlib_module_names"); - if (stdlib && PyAnySet_Check(stdlib) && PySet_Contains(stdlib, mod_name)) { - wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; - if(_Py_wgetcwd(cwd, MAXPATHLEN)) { - if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { - goto done; - } - wchar_t *sep = wcsrchr(origin_dirname, SEP); - if (sep) { - *sep = L'\0'; - if (wcscmp(cwd, origin_dirname) == 0) { - is_script_shadowing_stdlib = 1; + if (stdlib && PyAnySet_Check(stdlib)) { + int rc = PySet_Contains(stdlib, mod_name); + if (rc == 1) { + wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; + if(_Py_wgetcwd(cwd, MAXPATHLEN)) { + if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { + goto done; + } + wchar_t *sep = wcsrchr(origin_dirname, SEP); + if (sep) { + *sep = L'\0'; + if (wcscmp(cwd, origin_dirname) == 0) { + is_script_shadowing_stdlib = 1; + } } } } + else if (rc == -1) { + goto done; + } } } From 24ccd8aa536f7262e34fbf0cd64c92060446aec0 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 22 Jan 2024 14:54:45 -0800 Subject: [PATCH 12/48] add substr test --- Lib/test/test_import/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 49392ea6cbeffe..28b7ea01a060c5 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -824,6 +824,15 @@ def test_shadowing_stdlib_sys_edge_cases(self): import types collections.__spec__ = types.SimpleNamespace() collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + +class substr(str): + __hash__ = None +collections.__name__ = substr('collections') +try: + collections.does_not_exist +except TypeError as e: + print(str(e)) + import sys sys.stdlib_module_names = None try: @@ -840,6 +849,7 @@ def test_shadowing_stdlib_sys_edge_cases(self): popen = script_helper.spawn_python('-c', program) stdout, stderr = popen.communicate() self.assertEqual(stdout.splitlines(), [ + b"unhashable type: 'substr'", b"module 'collections' has no attribute 'does_not_exist'", b"module 'collections' has no attribute 'does_not_exist'", ]) From 47d7f6c23b4c3083e9122b6abdc7c3195cbf9325 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 28 Jan 2024 02:38:17 -0800 Subject: [PATCH 13/48] remove dedent, fix whitespace, use bool --- Lib/test/test_import/__init__.py | 4 ++-- Objects/moduleobject.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 28b7ea01a060c5..773bb037eb597c 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -818,7 +818,7 @@ def test_cwd_script_shadowing_stdlib(self): collections.does_not_exist def test_shadowing_stdlib_sys_edge_cases(self): - program = textwrap.dedent(''' + program = ''' import collections import os import types @@ -845,7 +845,7 @@ class substr(str): collections.does_not_exist except AttributeError as e: print(str(e)) -''') +''' popen = script_helper.spawn_python('-c', program) stdout, stderr = popen.communicate() self.assertEqual(stdout.splitlines(), [ diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2f32f30232cb6a..45e0639799cd68 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -853,7 +853,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } } - int is_script_shadowing_stdlib = 0; + bool is_script_shadowing_stdlib = 0; // Check mod.__name__ in sys.stdlib_module_names // and os.path.dirname(mod.__spec__.origin) == os.getcwd() if (origin) { @@ -862,7 +862,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) int rc = PySet_Contains(stdlib, mod_name); if (rc == 1) { wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; - if(_Py_wgetcwd(cwd, MAXPATHLEN)) { + if (_Py_wgetcwd(cwd, MAXPATHLEN)) { if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { goto done; } From 5c0991db16eea14f8066d7730e9a5f818523733c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 28 Jan 2024 23:28:24 -0800 Subject: [PATCH 14/48] Rework to handle third party shadowing --- Objects/moduleobject.c | 106 ++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 45e0639799cd68..17f6cf42e00284 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -786,6 +786,60 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } +int +_is_module_possibly_shadowing(PyObject *origin) +{ + // origin must be a unicode subtype + // Returns 1 if the module at origin could be shadowing a module of the + // same name. The exact condition we check is: + // not sys.flags.safe_path and os.path.dirname(origin) == sys.path[0] + // Returns 0 otherwise (or if we aren't sure) + // Returns -1 if an error occurred that should be propagated + if (origin == NULL) { + return 0; + } + const PyConfig *config = _Py_GetConfig(); + if (config->safe_path) { + return 0; + } + + PyObject *sys_path = PySys_GetObject(&_Py_ID(path)); + if (sys_path == NULL || !PyList_Check(sys_path) || PyList_GET_SIZE(sys_path) == 0) { + return 0; + } + wchar_t sys_path_0[MAXPATHLEN]; + int rc = PyUnicode_AsWideChar(PyList_GET_ITEM(sys_path, 0), sys_path_0, MAXPATHLEN - 1); + if (rc < 0) { + return -1; + } + assert(rc < MAXPATHLEN); + sys_path_0[rc] = L'\0'; + if (rc == 0) { + // if sys.path[0] == "", treat it as if it were the current directory + if (!_Py_wgetcwd(sys_path_0, MAXPATHLEN)) { + return -1; + } + } + + wchar_t origin_dirname[MAXPATHLEN]; + rc = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN - 1); + if (rc < 0) { + return -1; + } + assert(rc < MAXPATHLEN); + origin_dirname[rc] = L'\0'; + + wchar_t *sep = wcsrchr(origin_dirname, SEP); + if (!sep) { + return 0; + } + *sep = L'\0'; + if (wcscmp(sys_path_0, origin_dirname) == 0) { + return 1; + } + return 0; +} + PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { @@ -853,44 +907,42 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } } - bool is_script_shadowing_stdlib = 0; - // Check mod.__name__ in sys.stdlib_module_names - // and os.path.dirname(mod.__spec__.origin) == os.getcwd() - if (origin) { - PyObject *stdlib = PySys_GetObject("stdlib_module_names"); - if (stdlib && PyAnySet_Check(stdlib)) { - int rc = PySet_Contains(stdlib, mod_name); - if (rc == 1) { - wchar_t cwd[MAXPATHLEN], origin_dirname[MAXPATHLEN]; - if (_Py_wgetcwd(cwd, MAXPATHLEN)) { - if (PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN) < 0) { - goto done; - } - wchar_t *sep = wcsrchr(origin_dirname, SEP); - if (sep) { - *sep = L'\0'; - if (wcscmp(cwd, origin_dirname) == 0) { - is_script_shadowing_stdlib = 1; - } - } - } - } - else if (rc == -1) { + int is_possibly_shadowing = _is_module_possibly_shadowing(origin); + if (is_possibly_shadowing < 0) { + goto done; + } + int is_possibly_shadowing_stdlib = 0; + if (is_possibly_shadowing == 1) { + PyObject *stdlib_modules = PySys_GetObject("stdlib_module_names"); + if (stdlib_modules && PyAnySet_Check(stdlib_modules)) { + is_possibly_shadowing_stdlib = PySet_Contains(stdlib_modules, mod_name); + if (is_possibly_shadowing_stdlib == -1) { goto done; } } } - if (is_script_shadowing_stdlib == 1) { + if (is_possibly_shadowing_stdlib) { + assert(origin); PyErr_Format(PyExc_AttributeError, "module '%U' has no attribute '%U' " - "(most likely due to '%U' shadowing the standard library " - "module named '%U')", + "(consider renaming '%U' since it has the same " + "name as the standard library module named '%U')", mod_name, name, origin, mod_name); } else { int rc = _PyModuleSpec_IsInitializing(spec); if (rc > 0) { - if (origin) { + if (is_possibly_shadowing) { + assert(origin); + // For third party modules, only mention the possibility of + // shadowing if the module is being initialized. + PyErr_Format(PyExc_AttributeError, + "module '%U' has no attribute '%U' " + "(consider renaming '%U' if it has the same name " + "as a third party module you intended to import)", + mod_name, name, origin); + } + else if (origin) { PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' from '%U' has no attribute '%U' " From dec51f3e1f1f8d4b9f074c8cc62220e6e3f8225f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:10:01 -0800 Subject: [PATCH 15/48] Update test cases --- Lib/test/test_import/__init__.py | 127 ++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 38 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 773bb037eb597c..d02eea73154f12 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -790,41 +790,79 @@ def test_issue105979(self): self.assertIn("Frozen object named 'x' is invalid", str(cm.exception)) - def test_cwd_script_shadowing_stdlib(self): - with CleanImport('collections'): - import collections - collections.__spec__ = types.SimpleNamespace() - collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') + def test_script_shadowing_stdlib(self): + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "collections.py"), "w", encoding='utf-8') as f: + f.write("import collections\ncollections.defaultdict") - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist' \(most " - r"likely due to '.*collections.py' shadowing the standard " - r"library module named 'collections'\)" - ): - collections.does_not_exist + expected_error = ( + rb"AttributeError: module 'collections' has no attribute 'defaultdict' " + rb"\(consider renaming '.*collections.py' since it has the " + rb"same name as the standard library module named 'collections'\)" + ) + + popen = script_helper.spawn_python('collections.py', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-m', 'collections', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-c', 'import collections', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + # and there's no error at all when using -P + popen = script_helper.spawn_python('-P', 'collections.py', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual(stdout, b'') + + def test_script_shadowing_third_party(self): + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: + f.write("import numpy\nnumpy.array") + + expected_error = ( + rb"AttributeError: module 'numpy' has no attribute 'array' " + rb"\(consider renaming '.*numpy.py' if it has the " + rb"same name as a third party module you intended to import\)\n\Z" + ) + + popen = script_helper.spawn_python('numpy.py', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-m', 'numpy', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-c', 'import numpy', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + def test_script_possibly_shadowing(self): + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: + f.write("this_script_does_not_attempt_to_import_numpy = True") + + expected_error = ( + rb"AttributeError: module 'numpy' has no attribute 'attr'\n\Z" + ) + + popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) - del collections.__spec__.origin - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'$" - ): - collections.does_not_exist - del collections.__spec__ - with self.assertRaisesRegex( - AttributeError, - r"module 'collections' has no attribute 'does_not_exist'$" - ): - collections.does_not_exist def test_shadowing_stdlib_sys_edge_cases(self): - program = ''' + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "collections.py"), "w", encoding='utf-8') as f: + f.write("shadowing_module = True") + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" import collections -import os -import types -collections.__spec__ = types.SimpleNamespace() -collections.__spec__.origin = os.path.join(os.getcwd(), 'collections.py') - +collections.shadowing_module class substr(str): __hash__ = None collections.__name__ = substr('collections') @@ -832,6 +870,16 @@ class substr(str): collections.does_not_exist except TypeError as e: print(str(e)) +""") + + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual(stdout, b"unhashable type: 'substr'\n") + + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import collections +collections.shadowing_module import sys sys.stdlib_module_names = None @@ -845,14 +893,17 @@ class substr(str): collections.does_not_exist except AttributeError as e: print(str(e)) -''' - popen = script_helper.spawn_python('-c', program) - stdout, stderr = popen.communicate() - self.assertEqual(stdout.splitlines(), [ - b"unhashable type: 'substr'", - b"module 'collections' has no attribute 'does_not_exist'", - b"module 'collections' has no attribute 'does_not_exist'", - ]) +""") + + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual( + stdout.splitlines(), + [ + b"module 'collections' has no attribute 'does_not_exist'", + b"module 'collections' has no attribute 'does_not_exist'", + ], + ) @skip_if_dont_write_bytecode From a32411b6c2955f3ce1c83c73a4dbeab549470d65 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:12:31 -0800 Subject: [PATCH 16/48] Update What's New --- Doc/whatsnew/3.13.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index b9b4b0bcfd8e1a..8e299c465aee17 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -95,9 +95,8 @@ Improved Error Messages (Contributed by Pablo Galindo Salgado in :gh:`112730`.) * A common mistake is to write a script with the same name as a - standard library module. The interpreter now detects this and - displays a more helpful error message. - (Contributed by Shantanu Jain in :gh:`95754`.) + standard library module. When this results in errors, we now + display a more helpful error message: .. code-block:: shell-session @@ -109,7 +108,25 @@ Improved Error Messages File "/home/random.py", line 1, in import random; print(random.randint(5)) ^^^^^^^^^^^^^^ - AttributeError: module 'random' has no attribute 'randint' (most likely due to '/home/random.py' shadowing the standard library module named 'random') + AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random') + + Similarly, if a script has the same name as a third party + module it attempts to import, we also display a more helpful + error message: + + .. code-block:: shell-session + + $ python numpy.py + Traceback (most recent call last): + File "/home/numpy.py", line 1, in + import numpy as np; np.array([1,2,3]) + ^^^^^^^^^^^^^^^^^^ + File "/home/numpy.py", line 1, in + import numpy as np; np.array([1,2,3]) + ^^^^^^^^ + AttributeError: module 'numpy' has no attribute 'array' (consider renaming '/home/numpy.py' if it has the same name as a third party module you intended to import) + + (Contributed by Shantanu Jain in :gh:`95754`.) Other Language Changes From c1b79fa301a442bf71a8b2194a4615024bdf3348 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:21:06 -0800 Subject: [PATCH 17/48] Tweak test --- Lib/test/test_import/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index d02eea73154f12..5b6034ace3b233 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -801,7 +801,7 @@ def test_script_shadowing_stdlib(self): rb"same name as the standard library module named 'collections'\)" ) - popen = script_helper.spawn_python('collections.py', cwd=tmp) + popen = script_helper.spawn_python(os.path.join(tmp, "collections.py")) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) @@ -829,7 +829,7 @@ def test_script_shadowing_third_party(self): rb"same name as a third party module you intended to import\)\n\Z" ) - popen = script_helper.spawn_python('numpy.py', cwd=tmp) + popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) From 1feabb04b738fc778c40aa1999bf4208cf3545f9 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:39:37 -0800 Subject: [PATCH 18/48] Update news entry --- .../2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst b/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst index 30800cb94418c1..588be2d28cd76e 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2024-01-07-03-38-34.gh-issue-95754.aPjEBG.rst @@ -1 +1,4 @@ -Improve the error message when a script in the current directory shadows a module from the standard library. +Improve the error message when a script shadowing a module from the standard +library causes :exc:`AttributeError` to be raised. Similarly, improve the error +message when a script shadowing a third party module attempts to access an +attribute from that third party module while still initialising. From eaf08ac3048cec59ad4336c2df9dfe7c863722d4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:47:02 -0800 Subject: [PATCH 19/48] Tweak test names --- Lib/test/test_import/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 5b6034ace3b233..619beb996448c4 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -841,7 +841,7 @@ def test_script_shadowing_third_party(self): stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) - def test_script_possibly_shadowing(self): + def test_script_maybe_not_shadowing_third_party(self): with os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: f.write("this_script_does_not_attempt_to_import_numpy = True") @@ -854,8 +854,7 @@ def test_script_possibly_shadowing(self): stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) - - def test_shadowing_stdlib_sys_edge_cases(self): + def test_script_shadowing_stdlib_edge_cases(self): with os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "collections.py"), "w", encoding='utf-8') as f: f.write("shadowing_module = True") From d9eb4244569e145185fc5425c52da68299770c6f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:51:09 -0800 Subject: [PATCH 20/48] Tweak What's New language --- Doc/whatsnew/3.13.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 8e299c465aee17..ee22a6bb277801 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -111,8 +111,8 @@ Improved Error Messages AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random') Similarly, if a script has the same name as a third party - module it attempts to import, we also display a more helpful - error message: + module it attempts to import, and this results in errors, + we also display a more helpful error message: .. code-block:: shell-session From 69df558c991e155e9ae5e1ed449423558489f7f2 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 00:57:20 -0800 Subject: [PATCH 21/48] Tweak docstring, fix accidental commit --- Objects/moduleobject.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 17f6cf42e00284..4117e6b941dcb6 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -791,8 +791,8 @@ _is_module_possibly_shadowing(PyObject *origin) { // origin must be a unicode subtype // Returns 1 if the module at origin could be shadowing a module of the - // same name. The exact condition we check is: - // not sys.flags.safe_path and os.path.dirname(origin) == sys.path[0] + // same name later in the module search path. The condition we check is basically: + // not sys.flags.safe_path and os.path.dirname(origin) == (sys.path[0] or os.getcwd()) // Returns 0 otherwise (or if we aren't sure) // Returns -1 if an error occurred that should be propagated if (origin == NULL) { @@ -803,7 +803,7 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } - PyObject *sys_path = PySys_GetObject(&_Py_ID(path)); + PyObject *sys_path = PySys_GetObject("path"); if (sys_path == NULL || !PyList_Check(sys_path) || PyList_GET_SIZE(sys_path) == 0) { return 0; } From ef30d332b37f9c69779e8374c7f4f1b244b40bee Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 01:02:02 -0800 Subject: [PATCH 22/48] Add hyphen in third-party --- Doc/whatsnew/3.13.rst | 4 ++-- Lib/test/test_import/__init__.py | 2 +- Objects/moduleobject.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index ee22a6bb277801..eaf658348f565c 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -110,7 +110,7 @@ Improved Error Messages ^^^^^^^^^^^^^^ AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random') - Similarly, if a script has the same name as a third party + Similarly, if a script has the same name as a third-party module it attempts to import, and this results in errors, we also display a more helpful error message: @@ -124,7 +124,7 @@ Improved Error Messages File "/home/numpy.py", line 1, in import numpy as np; np.array([1,2,3]) ^^^^^^^^ - AttributeError: module 'numpy' has no attribute 'array' (consider renaming '/home/numpy.py' if it has the same name as a third party module you intended to import) + AttributeError: module 'numpy' has no attribute 'array' (consider renaming '/home/numpy.py' if it has the same name as a third-party module you intended to import) (Contributed by Shantanu Jain in :gh:`95754`.) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 619beb996448c4..b1b2a8a3c7d9be 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -826,7 +826,7 @@ def test_script_shadowing_third_party(self): expected_error = ( rb"AttributeError: module 'numpy' has no attribute 'array' " rb"\(consider renaming '.*numpy.py' if it has the " - rb"same name as a third party module you intended to import\)\n\Z" + rb"same name as a third-party module you intended to import\)\n\Z" ) popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 4117e6b941dcb6..dad5bb449ab376 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -934,12 +934,12 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (rc > 0) { if (is_possibly_shadowing) { assert(origin); - // For third party modules, only mention the possibility of + // For third-party modules, only mention the possibility of // shadowing if the module is being initialized. PyErr_Format(PyExc_AttributeError, "module '%U' has no attribute '%U' " "(consider renaming '%U' if it has the same name " - "as a third party module you intended to import)", + "as a third-party module you intended to import)", mod_name, name, origin); } else if (origin) { From f1c9ee9a12c40346d499caac9e4e1f94851c3c5f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 01:10:35 -0800 Subject: [PATCH 23/48] Tweak test attribute --- Lib/test/test_import/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index b1b2a8a3c7d9be..ad8d223a4f46a6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -866,7 +866,7 @@ class substr(str): __hash__ = None collections.__name__ = substr('collections') try: - collections.does_not_exist + collections.defaultdict except TypeError as e: print(str(e)) """) @@ -883,13 +883,13 @@ class substr(str): import sys sys.stdlib_module_names = None try: - collections.does_not_exist + collections.defaultdict except AttributeError as e: print(str(e)) del sys.stdlib_module_names try: - collections.does_not_exist + collections.defaultdict except AttributeError as e: print(str(e)) """) @@ -899,8 +899,8 @@ class substr(str): self.assertEqual( stdout.splitlines(), [ - b"module 'collections' has no attribute 'does_not_exist'", - b"module 'collections' has no attribute 'does_not_exist'", + b"module 'collections' has no attribute 'defaultdict'", + b"module 'collections' has no attribute 'defaultdict'", ], ) From a588c15177ad7c3ababc9f2b74abdc8d3cbbd853 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 01:15:57 -0800 Subject: [PATCH 24/48] Fix nit --- Objects/moduleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index dad5bb449ab376..60f9d47053b77e 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -908,7 +908,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } int is_possibly_shadowing = _is_module_possibly_shadowing(origin); - if (is_possibly_shadowing < 0) { + if (is_possibly_shadowing == -1) { goto done; } int is_possibly_shadowing_stdlib = 0; From ea0d2ad3743165a140a4fc0f7a6ca829ce3f15a4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 01:21:55 -0800 Subject: [PATCH 25/48] Add missing static --- Objects/moduleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 60f9d47053b77e..fe8508af50a7aa 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -786,7 +786,7 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } -int +static int _is_module_possibly_shadowing(PyObject *origin) { // origin must be a unicode subtype From 61b55175e2decbefb69c1c37f055e6f2a4455964 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 01:39:20 -0800 Subject: [PATCH 26/48] Fix test failures on Windows --- Lib/test/test_import/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index ad8d223a4f46a6..b47c2d0f751c5b 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -826,7 +826,7 @@ def test_script_shadowing_third_party(self): expected_error = ( rb"AttributeError: module 'numpy' has no attribute 'array' " rb"\(consider renaming '.*numpy.py' if it has the " - rb"same name as a third-party module you intended to import\)\n\Z" + rb"same name as a third-party module you intended to import\)\s+\Z" ) popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) @@ -847,7 +847,7 @@ def test_script_maybe_not_shadowing_third_party(self): f.write("this_script_does_not_attempt_to_import_numpy = True") expected_error = ( - rb"AttributeError: module 'numpy' has no attribute 'attr'\n\Z" + rb"AttributeError: module 'numpy' has no attribute 'attr'\s+\Z" ) popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp) @@ -873,7 +873,7 @@ class substr(str): popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - self.assertEqual(stdout, b"unhashable type: 'substr'\n") + self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" From a6b3d83012437c0dba2bea36fda878152d9dfd38 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 02:18:18 -0800 Subject: [PATCH 27/48] Use Py_ssize_t --- Objects/moduleobject.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index fe8508af50a7aa..2a138a5a072843 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -808,13 +808,13 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } wchar_t sys_path_0[MAXPATHLEN]; - int rc = PyUnicode_AsWideChar(PyList_GET_ITEM(sys_path, 0), sys_path_0, MAXPATHLEN - 1); - if (rc < 0) { + Py_ssize_t size = PyUnicode_AsWideChar(PyList_GET_ITEM(sys_path, 0), sys_path_0, MAXPATHLEN - 1); + if (size < 0) { return -1; } - assert(rc < MAXPATHLEN); - sys_path_0[rc] = L'\0'; - if (rc == 0) { + assert(size < MAXPATHLEN); + sys_path_0[size] = L'\0'; + if (size == 0) { // if sys.path[0] == "", treat it as if it were the current directory if (!_Py_wgetcwd(sys_path_0, MAXPATHLEN)) { return -1; @@ -822,12 +822,12 @@ _is_module_possibly_shadowing(PyObject *origin) } wchar_t origin_dirname[MAXPATHLEN]; - rc = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN - 1); - if (rc < 0) { + size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN - 1); + if (size < 0) { return -1; } - assert(rc < MAXPATHLEN); - origin_dirname[rc] = L'\0'; + assert(size < MAXPATHLEN); + origin_dirname[size] = L'\0'; wchar_t *sep = wcsrchr(origin_dirname, SEP); if (!sep) { From fe2506835fcb5c11a4e6db51fcab932a4076ce19 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 02:24:53 -0800 Subject: [PATCH 28/48] Add test case for bad sys.path --- Lib/test/test_import/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index b47c2d0f751c5b..6d05a4c5ad6ac4 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -892,6 +892,12 @@ class substr(str): collections.defaultdict except AttributeError as e: print(str(e)) + +sys.path = [0] +try: + collections.defaultdict +except TypeError as e: + print(str(e)) """) popen = script_helper.spawn_python("main.py", cwd=tmp) @@ -901,6 +907,7 @@ class substr(str): [ b"module 'collections' has no attribute 'defaultdict'", b"module 'collections' has no attribute 'defaultdict'", + b"bad argument type for built-in operation", ], ) From 09813021ce5b336174c0a762220ba501705608a8 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 02:29:52 -0800 Subject: [PATCH 29/48] Further tweaks --- Lib/test/test_import/__init__.py | 4 ++-- Objects/moduleobject.c | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 6d05a4c5ad6ac4..22621b66bbcac6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -896,7 +896,7 @@ class substr(str): sys.path = [0] try: collections.defaultdict -except TypeError as e: +except AttributeError as e: print(str(e)) """) @@ -907,7 +907,7 @@ class substr(str): [ b"module 'collections' has no attribute 'defaultdict'", b"module 'collections' has no attribute 'defaultdict'", - b"bad argument type for built-in operation", + b"module 'collections' has no attribute 'defaultdict'", ], ) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2a138a5a072843..8b0010f52e1c30 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -807,8 +807,12 @@ _is_module_possibly_shadowing(PyObject *origin) if (sys_path == NULL || !PyList_Check(sys_path) || PyList_GET_SIZE(sys_path) == 0) { return 0; } - wchar_t sys_path_0[MAXPATHLEN]; - Py_ssize_t size = PyUnicode_AsWideChar(PyList_GET_ITEM(sys_path, 0), sys_path_0, MAXPATHLEN - 1); + PyObject *py_sys_path_0 = PyList_GET_ITEM(sys_path, 0); + if (!PyUnicode_Check(py_sys_path_0)) { + return 0; + } + wchar_t sys_path_0[MAXPATHLEN + 1]; + Py_ssize_t size = PyUnicode_AsWideChar(py_sys_path_0, sys_path_0, MAXPATHLEN); if (size < 0) { return -1; } @@ -821,8 +825,8 @@ _is_module_possibly_shadowing(PyObject *origin) } } - wchar_t origin_dirname[MAXPATHLEN]; - size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN - 1); + wchar_t origin_dirname[MAXPATHLEN + 1]; + size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); if (size < 0) { return -1; } From 5069b988f6f67dd454802119f8bd4580a7bda2ba Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 29 Jan 2024 11:23:59 -0800 Subject: [PATCH 30/48] Tweak assert for +1 logic --- Objects/moduleobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 8b0010f52e1c30..7fb0febcd8fb4c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -816,7 +816,7 @@ _is_module_possibly_shadowing(PyObject *origin) if (size < 0) { return -1; } - assert(size < MAXPATHLEN); + assert(size <= MAXPATHLEN); sys_path_0[size] = L'\0'; if (size == 0) { // if sys.path[0] == "", treat it as if it were the current directory @@ -830,7 +830,7 @@ _is_module_possibly_shadowing(PyObject *origin) if (size < 0) { return -1; } - assert(size < MAXPATHLEN); + assert(size <= MAXPATHLEN); origin_dirname[size] = L'\0'; wchar_t *sep = wcsrchr(origin_dirname, SEP); From 9977cea2066be41bdfd758392c0390518a7a907b Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 2 Feb 2024 01:00:33 -0800 Subject: [PATCH 31/48] Use config->sys_path_0 / config->module_search_path + add test for sys.path editing + avoid collections warning from traceback.py --- Lib/test/test_import/__init__.py | 67 +++++++++++++++++++++----------- Objects/moduleobject.c | 56 +++++++++++++------------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 22621b66bbcac6..4129a0bf1c09e6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -792,29 +792,29 @@ def test_issue105979(self): def test_script_shadowing_stdlib(self): with os_helper.temp_dir() as tmp: - with open(os.path.join(tmp, "collections.py"), "w", encoding='utf-8') as f: - f.write("import collections\ncollections.defaultdict") + with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: + f.write("import fractions\nfractions.Fraction") expected_error = ( - rb"AttributeError: module 'collections' has no attribute 'defaultdict' " - rb"\(consider renaming '.*collections.py' since it has the " - rb"same name as the standard library module named 'collections'\)" + rb"AttributeError: module 'fractions' has no attribute 'Fraction' " + rb"\(consider renaming '.*fractions.py' since it has the " + rb"same name as the standard library module named 'fractions'\)" ) - popen = script_helper.spawn_python(os.path.join(tmp, "collections.py")) + popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py")) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) - popen = script_helper.spawn_python('-m', 'collections', cwd=tmp) + popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) - popen = script_helper.spawn_python('-c', 'import collections', cwd=tmp) + popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) # and there's no error at all when using -P - popen = script_helper.spawn_python('-P', 'collections.py', cwd=tmp) + popen = script_helper.spawn_python('-P', 'fractions.py', cwd=tmp) stdout, stderr = popen.communicate() self.assertEqual(stdout, b'') @@ -856,17 +856,17 @@ def test_script_maybe_not_shadowing_third_party(self): def test_script_shadowing_stdlib_edge_cases(self): with os_helper.temp_dir() as tmp: - with open(os.path.join(tmp, "collections.py"), "w", encoding='utf-8') as f: + with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: f.write("shadowing_module = True") with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" -import collections -collections.shadowing_module +import fractions +fractions.shadowing_module class substr(str): __hash__ = None -collections.__name__ = substr('collections') +fractions.__name__ = substr('fractions') try: - collections.defaultdict + fractions.Fraction except TypeError as e: print(str(e)) """) @@ -877,25 +877,25 @@ class substr(str): with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" -import collections -collections.shadowing_module +import fractions +fractions.shadowing_module import sys sys.stdlib_module_names = None try: - collections.defaultdict + fractions.Fraction except AttributeError as e: print(str(e)) del sys.stdlib_module_names try: - collections.defaultdict + fractions.Fraction except AttributeError as e: print(str(e)) sys.path = [0] try: - collections.defaultdict + fractions.Fraction except AttributeError as e: print(str(e)) """) @@ -905,12 +905,35 @@ class substr(str): self.assertEqual( stdout.splitlines(), [ - b"module 'collections' has no attribute 'defaultdict'", - b"module 'collections' has no attribute 'defaultdict'", - b"module 'collections' has no attribute 'defaultdict'", + b"module 'fractions' has no attribute 'Fraction'", + b"module 'fractions' has no attribute 'Fraction'", + b"module 'fractions' has no attribute 'Fraction'", ], ) + def test_script_shadowing_stdlib_sys_path_modification(self): + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: + f.write("shadowing_module = True") + + expected_error = ( + rb"AttributeError: module 'fractions' has no attribute 'Fraction' " + rb"\(consider renaming '.*fractions.py' since it has the " + rb"same name as the standard library module named 'fractions'\)" + ) + + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import sys +sys.path.insert(0, "this_folder_does_not_exist") +import fractions +fractions.Fraction +""") + + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + @skip_if_dont_write_bytecode class FilePermissionTests(unittest.TestCase): diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 7fb0febcd8fb4c..e5b35efa8b5552 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -803,30 +803,9 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } - PyObject *sys_path = PySys_GetObject("path"); - if (sys_path == NULL || !PyList_Check(sys_path) || PyList_GET_SIZE(sys_path) == 0) { - return 0; - } - PyObject *py_sys_path_0 = PyList_GET_ITEM(sys_path, 0); - if (!PyUnicode_Check(py_sys_path_0)) { - return 0; - } - wchar_t sys_path_0[MAXPATHLEN + 1]; - Py_ssize_t size = PyUnicode_AsWideChar(py_sys_path_0, sys_path_0, MAXPATHLEN); - if (size < 0) { - return -1; - } - assert(size <= MAXPATHLEN); - sys_path_0[size] = L'\0'; - if (size == 0) { - // if sys.path[0] == "", treat it as if it were the current directory - if (!_Py_wgetcwd(sys_path_0, MAXPATHLEN)) { - return -1; - } - } - + // os.path.dirname(origin) wchar_t origin_dirname[MAXPATHLEN + 1]; - size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); + Py_ssize_t size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); if (size < 0) { return -1; } @@ -838,10 +817,35 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } *sep = L'\0'; - if (wcscmp(sys_path_0, origin_dirname) == 0) { - return 1; + + // sys.path[0] or os.getcwd() + wchar_t *sys_path_0 = config->sys_path_0; + if (!sys_path_0) { + if (!config->module_search_paths_set || config->module_search_paths.length == 0) { + return 0; + } + sys_path_0 = config->module_search_paths.items[0]; + assert(sys_path_0 != NULL); } - return 0; + bool sys_path_allocated = false; + if (sys_path_0[0] == L'\0') { + // if sys.path[0] == "", treat it as if it were the current directory + sys_path_0 = PyMem_RawMalloc(MAXPATHLEN * sizeof(wchar_t)); + if (!sys_path_0) { + return -1; + } + sys_path_allocated = true; + if (!_Py_wgetcwd(sys_path_0, MAXPATHLEN)) { + PyMem_RawFree(sys_path_0); + return -1; + } + } + + int result = wcscmp(sys_path_0, origin_dirname) == 0; + if (sys_path_allocated) { + PyMem_RawFree(sys_path_0); + } + return result; } PyObject* From f74226187ad3ba6f29d5513abd734d263367302c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 3 Feb 2024 23:03:48 -0800 Subject: [PATCH 32/48] stack allocate --- Objects/moduleobject.c | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e5b35efa8b5552..a7332f9b5ce7b8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -821,30 +821,19 @@ _is_module_possibly_shadowing(PyObject *origin) // sys.path[0] or os.getcwd() wchar_t *sys_path_0 = config->sys_path_0; if (!sys_path_0) { - if (!config->module_search_paths_set || config->module_search_paths.length == 0) { - return 0; - } - sys_path_0 = config->module_search_paths.items[0]; - assert(sys_path_0 != NULL); + return 0; } - bool sys_path_allocated = false; + + wchar_t sys_path_0_buf[MAXPATHLEN]; if (sys_path_0[0] == L'\0') { // if sys.path[0] == "", treat it as if it were the current directory - sys_path_0 = PyMem_RawMalloc(MAXPATHLEN * sizeof(wchar_t)); - if (!sys_path_0) { - return -1; - } - sys_path_allocated = true; - if (!_Py_wgetcwd(sys_path_0, MAXPATHLEN)) { - PyMem_RawFree(sys_path_0); + if (!_Py_wgetcwd(sys_path_0_buf, MAXPATHLEN)) { return -1; } + sys_path_0 = sys_path_0_buf; } int result = wcscmp(sys_path_0, origin_dirname) == 0; - if (sys_path_allocated) { - PyMem_RawFree(sys_path_0); - } return result; } From 3d205f3e8f638181fc5219f88360288d9ce8f0e6 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 3 Feb 2024 23:08:31 -0800 Subject: [PATCH 33/48] more cwd tests --- Lib/test/test_import/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 4129a0bf1c09e6..997f91f7ee0927 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -801,7 +801,7 @@ def test_script_shadowing_stdlib(self): rb"same name as the standard library module named 'fractions'\)" ) - popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py")) + popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) @@ -818,6 +818,22 @@ def test_script_shadowing_stdlib(self): stdout, stderr = popen.communicate() self.assertEqual(stdout, b'') + tmp_child = os.path.join(tmp, "child") + os.mkdir(tmp_child) + + # test the logic in with different cwd + popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp_child) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp_child) + stdout, stderr = popen.communicate() + self.assertEqual(stdout, b'') # no error + + popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp_child) + stdout, stderr = popen.communicate() + self.assertEqual(stdout, b'') # no error + def test_script_shadowing_third_party(self): with os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: From 146d39488f81d81940f44fc92cb4a97f766cf25e Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 6 Mar 2024 14:51:53 -0800 Subject: [PATCH 34/48] further tweak error message --- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_import/__init__.py | 6 ++++-- Objects/moduleobject.c | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index c59cef56fed248..d05d6e32ba7f9e 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -115,7 +115,7 @@ Improved Error Messages File "/home/random.py", line 1, in import random; print(random.randint(5)) ^^^^^^^^^^^^^^ - AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random') + AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random'and takes precedence over it on sys.path) Similarly, if a script has the same name as a third-party module it attempts to import, and this results in errors, diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 997f91f7ee0927..ba574c87347893 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -798,7 +798,8 @@ def test_script_shadowing_stdlib(self): expected_error = ( rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " - rb"same name as the standard library module named 'fractions'\)" + rb"same name as the standard library module named 'fractions'" + rb"and takes precedence over it on sys.path\)" ) popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) @@ -935,7 +936,8 @@ def test_script_shadowing_stdlib_sys_path_modification(self): expected_error = ( rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " - rb"same name as the standard library module named 'fractions'\)" + rb"same name as the standard library module named 'fractions'" + rb"and takes precedence over it on sys.path\)" ) with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index a7332f9b5ce7b8..e949f657b827f3 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -924,7 +924,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) PyErr_Format(PyExc_AttributeError, "module '%U' has no attribute '%U' " "(consider renaming '%U' since it has the same " - "name as the standard library module named '%U')", + "name as the standard library module named '%U'" + "and takes precedence over it on sys.path)", mod_name, name, origin, mod_name); } else { int rc = _PyModuleSpec_IsInitializing(spec); From a38bad541151cf1ea9fc3c056576d4ada3e91d69 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 7 Mar 2024 23:32:44 -0800 Subject: [PATCH 35/48] control flow feedback --- Objects/moduleobject.c | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e949f657b827f3..07b2ec54a27db3 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -873,6 +873,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) return result; } + // The attribute was not found. We make a best effort attempt at a useful error message, + // but only if we're not suppressing AttributeError. if (suppress == 1) { return NULL; } @@ -890,18 +892,22 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) Py_DECREF(mod_name); return NULL; } + if (spec == NULL) { + PyErr_Format(PyExc_AttributeError, + "module '%U' has no attribute '%U'", + mod_name, name); + Py_DECREF(mod_name); + return NULL; + } + PyObject *origin = NULL; - if (spec) { - int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); - if (rc == -1) { - Py_DECREF(spec); - Py_DECREF(mod_name); - return NULL; - } - if (rc == 1 && !PyUnicode_Check(origin)) { - Py_DECREF(origin); - origin = NULL; - } + int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + if (rc == -1) { + goto done; + } + if (rc == 1 && !PyUnicode_Check(origin)) { + Py_DECREF(origin); + origin = NULL; } int is_possibly_shadowing = _is_module_possibly_shadowing(origin); @@ -973,7 +979,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) done: Py_XDECREF(origin); - Py_XDECREF(spec); + Py_DECREF(spec); Py_DECREF(mod_name); return NULL; } From 822d8eb37ae783b9b05bb0520791eb7a735ca030 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 00:11:00 -0800 Subject: [PATCH 36/48] handle has_location correctly --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 3 ++ Objects/moduleobject.c | 33 +++++++++++++++---- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 932738c3049882..03e57ac84643e7 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -973,6 +973,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(groups)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(h)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(handle)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(has_location)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hash_name)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(header)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(headers)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index da62b4f0a951ff..2acaf02a0bcc8c 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -462,6 +462,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(groups) STRUCT_FOR_ID(h) STRUCT_FOR_ID(handle) + STRUCT_FOR_ID(has_location) STRUCT_FOR_ID(hash_name) STRUCT_FOR_ID(header) STRUCT_FOR_ID(headers) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 68fbbcb4378e17..8fbeae7e67ec21 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -971,6 +971,7 @@ extern "C" { INIT_ID(groups), \ INIT_ID(h), \ INIT_ID(handle), \ + INIT_ID(has_location), \ INIT_ID(hash_name), \ INIT_ID(header), \ INIT_ID(headers), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index c8458b4e36ccc9..72da512e42a9e4 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1227,6 +1227,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(handle); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(has_location); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(hash_name); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 07b2ec54a27db3..90311a92667477 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -786,6 +786,32 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } +static int +_get_file_origin_from_spec(PyObject *spec, PyObject **origin) +{ + PyObject *has_location = NULL; + int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(has_location), &has_location); + if (rc <= 0) { + return rc; + } + rc = PyObject_IsTrue(has_location); + Py_DECREF(has_location); + if (rc <= 0) { + return rc; + } + // has_location is true, so origin is a location + rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), origin); + if (rc <= 0) { + return rc; + } + if (!PyUnicode_Check(*origin)) { + Py_DECREF(*origin); + *origin = NULL; + return 0; + } + return 1; +} + static int _is_module_possibly_shadowing(PyObject *origin) { @@ -901,14 +927,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } PyObject *origin = NULL; - int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); - if (rc == -1) { + if (_get_file_origin_from_spec(spec, &origin) == -1) { goto done; } - if (rc == 1 && !PyUnicode_Check(origin)) { - Py_DECREF(origin); - origin = NULL; - } int is_possibly_shadowing = _is_module_possibly_shadowing(origin); if (is_possibly_shadowing == -1) { From 3ec37d19b2b80aa093f1358254e895668981e2c2 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 00:22:06 -0800 Subject: [PATCH 37/48] remove explicit safe path check --- Objects/moduleobject.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 90311a92667477..d870af69ac15b5 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -825,9 +825,12 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } const PyConfig *config = _Py_GetConfig(); - if (config->safe_path) { + // sys.path[0] or os.getcwd() + wchar_t *sys_path_0 = config->sys_path_0; + if (sys_path_0 == NULL) { return 0; } + assert(!config->safe_path); // os.path.dirname(origin) wchar_t origin_dirname[MAXPATHLEN + 1]; @@ -844,12 +847,6 @@ _is_module_possibly_shadowing(PyObject *origin) } *sep = L'\0'; - // sys.path[0] or os.getcwd() - wchar_t *sys_path_0 = config->sys_path_0; - if (!sys_path_0) { - return 0; - } - wchar_t sys_path_0_buf[MAXPATHLEN]; if (sys_path_0[0] == L'\0') { // if sys.path[0] == "", treat it as if it were the current directory From 64bb4fd9756397b352fd66e3c13274bc31eb07cf Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 00:25:30 -0800 Subject: [PATCH 38/48] make regen-global-objects --- Include/internal/pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + Include/internal/pycore_runtime_init_generated.h | 1 + Include/internal/pycore_unicodeobject_generated.h | 3 +++ 4 files changed, 6 insertions(+) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e8b62bd0dcd369..afba88501f08b8 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -977,6 +977,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(h)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(handle)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(handle_seq)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(has_location)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hash_name)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(header)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(headers)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index b41f5c8952021e..38b3f11191ac02 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -466,6 +466,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(h) STRUCT_FOR_ID(handle) STRUCT_FOR_ID(handle_seq) + STRUCT_FOR_ID(has_location) STRUCT_FOR_ID(hash_name) STRUCT_FOR_ID(header) STRUCT_FOR_ID(headers) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 016eae02a2103d..7b898bc91aba0d 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -975,6 +975,7 @@ extern "C" { INIT_ID(h), \ INIT_ID(handle), \ INIT_ID(handle_seq), \ + INIT_ID(has_location), \ INIT_ID(hash_name), \ INIT_ID(header), \ INIT_ID(headers), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 64c4cf8c077056..518207596e3578 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1239,6 +1239,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { string = &_Py_ID(handle_seq); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); + string = &_Py_ID(has_location); + assert(_PyUnicode_CheckConsistency(string, 1)); + _PyUnicode_InternInPlace(interp, &string); string = &_Py_ID(hash_name); assert(_PyUnicode_CheckConsistency(string, 1)); _PyUnicode_InternInPlace(interp, &string); From 786b74e170e33c8a6bad61b6028d07338c3b674d Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 00:25:40 -0800 Subject: [PATCH 39/48] fix spacing --- Lib/test/test_import/__init__.py | 4 ++-- Objects/moduleobject.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index ba574c87347893..d9f73794e51333 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -798,7 +798,7 @@ def test_script_shadowing_stdlib(self): expected_error = ( rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " - rb"same name as the standard library module named 'fractions'" + rb"same name as the standard library module named 'fractions' " rb"and takes precedence over it on sys.path\)" ) @@ -936,7 +936,7 @@ def test_script_shadowing_stdlib_sys_path_modification(self): expected_error = ( rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " - rb"same name as the standard library module named 'fractions'" + rb"same name as the standard library module named 'fractions' " rb"and takes precedence over it on sys.path\)" ) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 85bd4c336f8f51..02d0aded51b82b 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -948,7 +948,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) PyErr_Format(PyExc_AttributeError, "module '%U' has no attribute '%U' " "(consider renaming '%U' since it has the same " - "name as the standard library module named '%U'" + "name as the standard library module named '%U' " "and takes precedence over it on sys.path)", mod_name, name, origin, mod_name); } else { From d7f1bc089b222f4993c206d9369e86cff8bc0215 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 00:51:54 -0800 Subject: [PATCH 40/48] Revert "remove explicit safe path check" This reverts commit 3ec37d19b2b80aa093f1358254e895668981e2c2. --- Objects/moduleobject.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 02d0aded51b82b..6895df01b16ca9 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -825,12 +825,9 @@ _is_module_possibly_shadowing(PyObject *origin) return 0; } const PyConfig *config = _Py_GetConfig(); - // sys.path[0] or os.getcwd() - wchar_t *sys_path_0 = config->sys_path_0; - if (sys_path_0 == NULL) { + if (config->safe_path) { return 0; } - assert(!config->safe_path); // os.path.dirname(origin) wchar_t origin_dirname[MAXPATHLEN + 1]; @@ -847,6 +844,12 @@ _is_module_possibly_shadowing(PyObject *origin) } *sep = L'\0'; + // sys.path[0] or os.getcwd() + wchar_t *sys_path_0 = config->sys_path_0; + if (!sys_path_0) { + return 0; + } + wchar_t sys_path_0_buf[MAXPATHLEN]; if (sys_path_0[0] == L'\0') { // if sys.path[0] == "", treat it as if it were the current directory From e55576dd0bbbcad2c65a0370e6749d1eb530fe21 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 11:30:38 -0800 Subject: [PATCH 41/48] test the no origin case --- Lib/test/test_import/__init__.py | 18 ++++++++++++++++++ Objects/moduleobject.c | 1 + 2 files changed, 19 insertions(+) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index d9f73794e51333..bf4de6b3c3e6db 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -928,6 +928,24 @@ class substr(str): ], ) + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import fractions +fractions.shadowing_module +del fractions.__spec__.origin +try: + fractions.Fraction +except AttributeError as e: + print(str(e)) +""") + + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual( + stdout.splitlines(), + [b"module 'fractions' has no attribute 'Fraction'"], + ) + def test_script_shadowing_stdlib_sys_path_modification(self): with os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 6895df01b16ca9..e3d3601e498bd0 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -804,6 +804,7 @@ _get_file_origin_from_spec(PyObject *spec, PyObject **origin) if (rc <= 0) { return rc; } + assert(origin != NULL); if (!PyUnicode_Check(*origin)) { Py_DECREF(*origin); *origin = NULL; From a388887041ac90bcc405714f9f6ee8366d6e0b53 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 11:36:42 -0800 Subject: [PATCH 42/48] output param feedback --- Objects/moduleobject.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e3d3601e498bd0..c987b72cc52d41 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -787,7 +787,7 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) } static int -_get_file_origin_from_spec(PyObject *spec, PyObject **origin) +_get_file_origin_from_spec(PyObject *spec, PyObject **p_origin) { PyObject *has_location = NULL; int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(has_location), &has_location); @@ -800,16 +800,18 @@ _get_file_origin_from_spec(PyObject *spec, PyObject **origin) return rc; } // has_location is true, so origin is a location - rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), origin); + PyObject *origin = NULL; + rc = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); if (rc <= 0) { return rc; } assert(origin != NULL); - if (!PyUnicode_Check(*origin)) { - Py_DECREF(*origin); - *origin = NULL; + if (!PyUnicode_Check(origin)) { + Py_DECREF(origin); + *p_origin = NULL; return 0; } + *p_origin = origin; return 1; } From 3d360324f082bb48fea8b456b34982f7989d90e6 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 11:38:50 -0800 Subject: [PATCH 43/48] test non-str origin --- Lib/test/test_import/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index bf4de6b3c3e6db..9fe9b918392f50 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -933,6 +933,12 @@ class substr(str): import fractions fractions.shadowing_module del fractions.__spec__.origin +try: + fractions.Fraction +except AttributeError as e: + print(str(e)) + +fractions.__spec__.origin = 0 try: fractions.Fraction except AttributeError as e: @@ -943,7 +949,10 @@ class substr(str): stdout, stderr = popen.communicate() self.assertEqual( stdout.splitlines(), - [b"module 'fractions' has no attribute 'Fraction'"], + [ + b"module 'fractions' has no attribute 'Fraction'", + b"module 'fractions' has no attribute 'Fraction'" + ], ) def test_script_shadowing_stdlib_sys_path_modification(self): From 616ced27256b0616b40495a89fed56d68136d40c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 11:42:58 -0800 Subject: [PATCH 44/48] add comment about module.__file__ --- Objects/moduleobject.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index c987b72cc52d41..2f6ff9ea38ac80 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -794,6 +794,9 @@ _get_file_origin_from_spec(PyObject *spec, PyObject **p_origin) if (rc <= 0) { return rc; } + // If origin is not a location, or doesn't exist, or is not a str), we could consider falling + // back to module.__file__. But the cases in which module.__file__ is not __spec__.origin + // are cases in which we probably shouldn't be guessing. rc = PyObject_IsTrue(has_location); Py_DECREF(has_location); if (rc <= 0) { From afbcd403f56ac47e120895e7f848c5a5db0e1c61 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 12:31:22 -0800 Subject: [PATCH 45/48] test package shadowing module --- Lib/test/test_import/__init__.py | 32 ++++++++++++++++++++++++++++++++ Objects/moduleobject.c | 26 ++++++++++++++++++-------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 9fe9b918392f50..eb9ce1007d2e9f 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -835,6 +835,38 @@ def test_script_shadowing_stdlib(self): stdout, stderr = popen.communicate() self.assertEqual(stdout, b'') # no error + def test_package_shadowing_stdlib_module(self): + with os_helper.temp_dir() as tmp: + os.mkdir(os.path.join(tmp, "fractions")) + with open(os.path.join(tmp, "fractions", "__init__.py"), "w", encoding='utf-8') as f: + f.write("shadowing_module = True") + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import fractions +fractions.shadowing_module +fractions.Fraction +""") + + expected_error = ( + rb"AttributeError: module 'fractions' has no attribute 'Fraction' " + rb"\(consider renaming '.*fractions.__init__.py' since it has the " + rb"same name as the standard library module named 'fractions' " + rb"and takes precedence over it on sys.path\)" + ) + + popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + popen = script_helper.spawn_python('-m', 'main', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + + # and there's no shadowing at all when using -P + popen = script_helper.spawn_python('-P', 'main.py', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, b"module 'fractions' has no attribute 'shadowing_module'") + def test_script_shadowing_third_party(self): with os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2f6ff9ea38ac80..da8ca9344f72d6 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -824,30 +824,40 @@ _is_module_possibly_shadowing(PyObject *origin) // origin must be a unicode subtype // Returns 1 if the module at origin could be shadowing a module of the // same name later in the module search path. The condition we check is basically: - // not sys.flags.safe_path and os.path.dirname(origin) == (sys.path[0] or os.getcwd()) + // root = os.path.dirname(origin.removesuffix(os.sep + "__init__.py")) + // return not sys.flags.safe_path and root == (sys.path[0] or os.getcwd()) // Returns 0 otherwise (or if we aren't sure) // Returns -1 if an error occurred that should be propagated if (origin == NULL) { return 0; } + + // not sys.flags.safe_path const PyConfig *config = _Py_GetConfig(); if (config->safe_path) { return 0; } - // os.path.dirname(origin) - wchar_t origin_dirname[MAXPATHLEN + 1]; - Py_ssize_t size = PyUnicode_AsWideChar(origin, origin_dirname, MAXPATHLEN); + // root = os.path.dirname(origin.removesuffix(os.sep + "__init__.py")) + wchar_t root[MAXPATHLEN + 1]; + Py_ssize_t size = PyUnicode_AsWideChar(origin, root, MAXPATHLEN); if (size < 0) { return -1; } assert(size <= MAXPATHLEN); - origin_dirname[size] = L'\0'; + root[size] = L'\0'; - wchar_t *sep = wcsrchr(origin_dirname, SEP); - if (!sep) { + wchar_t *sep = wcsrchr(root, SEP); + if (sep == NULL) { return 0; } + if (wcscmp(sep + 1, L"__init__.py") == 0) { + *sep = L'\0'; + sep = wcsrchr(root, SEP); + if (sep == NULL) { + return 0; + } + } *sep = L'\0'; // sys.path[0] or os.getcwd() @@ -865,7 +875,7 @@ _is_module_possibly_shadowing(PyObject *origin) sys_path_0 = sys_path_0_buf; } - int result = wcscmp(sys_path_0, origin_dirname) == 0; + int result = wcscmp(sys_path_0, root) == 0; return result; } From 2890b6b18da4aae64d67d50292d24278d6847287 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 8 Mar 2024 14:09:12 -0800 Subject: [PATCH 46/48] feedback --- Objects/moduleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index da8ca9344f72d6..56c2b94a97f607 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -811,7 +811,6 @@ _get_file_origin_from_spec(PyObject *spec, PyObject **p_origin) assert(origin != NULL); if (!PyUnicode_Check(origin)) { Py_DECREF(origin); - *p_origin = NULL; return 0; } *p_origin = origin; @@ -851,6 +850,7 @@ _is_module_possibly_shadowing(PyObject *origin) if (sep == NULL) { return 0; } + // If it's a package then we need to look one directory further up if (wcscmp(sep + 1, L"__init__.py") == 0) { *sep = L'\0'; sep = wcsrchr(root, SEP); From 1d65725147d996a88904d238f895d1c919846251 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 1 Apr 2024 11:41:12 -0700 Subject: [PATCH 47/48] try another error message variation --- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_import/__init__.py | 8 ++++---- Objects/moduleobject.c | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e0b0447ecf27e1..901cc98838ad45 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -114,7 +114,7 @@ Improved Error Messages File "/home/random.py", line 1, in import random; print(random.randint(5)) ^^^^^^^^^^^^^^ - AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random'and takes precedence over it on sys.path) + AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/random.py' since it has the same name as the standard library module named 'random' and the import system gives it precedence) Similarly, if a script has the same name as a third-party module it attempts to import, and this results in errors, diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index eb9ce1007d2e9f..28eaaf881010e7 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -799,7 +799,7 @@ def test_script_shadowing_stdlib(self): rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and takes precedence over it on sys.path\)" + rb"and the import system gives it precedence\)" ) popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) @@ -822,7 +822,7 @@ def test_script_shadowing_stdlib(self): tmp_child = os.path.join(tmp, "child") os.mkdir(tmp_child) - # test the logic in with different cwd + # test the logic with different cwd popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp_child) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) @@ -851,7 +851,7 @@ def test_package_shadowing_stdlib_module(self): rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.__init__.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and takes precedence over it on sys.path\)" + rb"and the import system gives it precedence\)" ) popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp) @@ -996,7 +996,7 @@ def test_script_shadowing_stdlib_sys_path_modification(self): rb"AttributeError: module 'fractions' has no attribute 'Fraction' " rb"\(consider renaming '.*fractions.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and takes precedence over it on sys.path\)" + rb"and the import system gives it precedence\)" ) with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 56c2b94a97f607..195d4d9bfff8f8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -968,7 +968,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) "module '%U' has no attribute '%U' " "(consider renaming '%U' since it has the same " "name as the standard library module named '%U' " - "and takes precedence over it on sys.path)", + "and the import system gives it precedence)", mod_name, name, origin, mod_name); } else { int rc = _PyModuleSpec_IsInitializing(spec); From 300c557d969ef946157a6cb226fa00854b8d73c6 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 22 Apr 2024 15:42:35 -0700 Subject: [PATCH 48/48] style nits --- Objects/moduleobject.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 195d4d9bfff8f8..eba708946215f6 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -943,20 +943,20 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } PyObject *origin = NULL; - if (_get_file_origin_from_spec(spec, &origin) == -1) { + if (_get_file_origin_from_spec(spec, &origin) < 0) { goto done; } int is_possibly_shadowing = _is_module_possibly_shadowing(origin); - if (is_possibly_shadowing == -1) { + if (is_possibly_shadowing < 0) { goto done; } int is_possibly_shadowing_stdlib = 0; - if (is_possibly_shadowing == 1) { + if (is_possibly_shadowing) { PyObject *stdlib_modules = PySys_GetObject("stdlib_module_names"); if (stdlib_modules && PyAnySet_Check(stdlib_modules)) { is_possibly_shadowing_stdlib = PySet_Contains(stdlib_modules, mod_name); - if (is_possibly_shadowing_stdlib == -1) { + if (is_possibly_shadowing_stdlib < 0) { goto done; } } @@ -970,7 +970,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) "name as the standard library module named '%U' " "and the import system gives it precedence)", mod_name, name, origin, mod_name); - } else { + } + else { int rc = _PyModuleSpec_IsInitializing(spec); if (rc > 0) { if (is_possibly_shadowing) {