From 9b88d72e5f7869f2c5b80871d367ea88e3712d82 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 16:33:37 -0700 Subject: [PATCH 01/11] rename pkgname to mod_name --- Python/ceval.c | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 0ebd5bb58c859c..4f61e08a6edbbf 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2763,7 +2763,7 @@ PyObject * _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) { PyObject *x; - PyObject *fullmodname, *pkgname, *pkgpath, *pkgname_or_unknown, *errmsg; + PyObject *fullmodname, *mod_name, *pkgpath, *mod_name_or_unknown, *errmsg; if (PyObject_GetOptionalAttr(v, name, &x) != 0) { return x; @@ -2771,16 +2771,16 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) /* Issue #17636: in case this failed because of a circular relative import, try to fallback on reading the module directly from sys.modules. */ - if (PyObject_GetOptionalAttr(v, &_Py_ID(__name__), &pkgname) < 0) { + if (PyObject_GetOptionalAttr(v, &_Py_ID(__name__), &mod_name) < 0) { return NULL; } - if (pkgname == NULL || !PyUnicode_Check(pkgname)) { - Py_CLEAR(pkgname); + if (mod_name == NULL || !PyUnicode_Check(mod_name)) { + Py_CLEAR(mod_name); goto error; } - fullmodname = PyUnicode_FromFormat("%U.%U", pkgname, name); + fullmodname = PyUnicode_FromFormat("%U.%U", mod_name, name); if (fullmodname == NULL) { - Py_DECREF(pkgname); + Py_DECREF(mod_name); return NULL; } x = PyImport_GetModule(fullmodname); @@ -2788,16 +2788,16 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) if (x == NULL && !_PyErr_Occurred(tstate)) { goto error; } - Py_DECREF(pkgname); + Py_DECREF(mod_name); return x; error: - if (pkgname == NULL) { - pkgname_or_unknown = PyUnicode_FromString(""); - if (pkgname_or_unknown == NULL) { + if (mod_name == NULL) { + mod_name_or_unknown = PyUnicode_FromString(""); + if (mod_name_or_unknown == NULL) { return NULL; } } else { - pkgname_or_unknown = pkgname; + mod_name_or_unknown = mod_name; } pkgpath = NULL; @@ -2805,7 +2805,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) pkgpath = PyModule_GetFilenameObject(v); if (pkgpath == NULL) { if (!PyErr_ExceptionMatches(PyExc_SystemError)) { - Py_DECREF(pkgname_or_unknown); + Py_DECREF(mod_name_or_unknown); return NULL; } // module filename missing @@ -2816,7 +2816,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) Py_CLEAR(pkgpath); errmsg = PyUnicode_FromFormat( "cannot import name %R from %R (unknown location)", - name, pkgname_or_unknown + name, mod_name_or_unknown ); } else { @@ -2827,7 +2827,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) Py_DECREF(spec); } if (rc < 0) { - Py_DECREF(pkgname_or_unknown); + Py_DECREF(mod_name_or_unknown); Py_DECREF(pkgpath); return NULL; } @@ -2837,13 +2837,13 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) "(most likely due to a circular import) (%S)" : "cannot import name %R from %R (%S)"; - errmsg = PyUnicode_FromFormat(fmt, name, pkgname_or_unknown, pkgpath); + errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, pkgpath); } - /* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */ - _PyErr_SetImportErrorWithNameFrom(errmsg, pkgname, pkgpath, name); + /* NULL checks for errmsg and mod_name done by PyErr_SetImportError. */ + _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, pkgpath, name); Py_XDECREF(errmsg); - Py_DECREF(pkgname_or_unknown); + Py_DECREF(mod_name_or_unknown); Py_XDECREF(pkgpath); return NULL; } @@ -3202,5 +3202,3 @@ _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *na } return value; } - - From d0de86e43793e9cd081738cd5cf212d9105085cc Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 16:54:11 -0700 Subject: [PATCH 02/11] rename pkgpath to origin (this isn't semantically true yet) --- Python/ceval.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 4f61e08a6edbbf..fc3e404ffd1922 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2763,7 +2763,7 @@ PyObject * _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) { PyObject *x; - PyObject *fullmodname, *mod_name, *pkgpath, *mod_name_or_unknown, *errmsg; + PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg; if (PyObject_GetOptionalAttr(v, name, &x) != 0) { return x; @@ -2800,10 +2800,10 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) mod_name_or_unknown = mod_name; } - pkgpath = NULL; + origin = NULL; if (PyModule_Check(v)) { - pkgpath = PyModule_GetFilenameObject(v); - if (pkgpath == NULL) { + origin = PyModule_GetFilenameObject(v); + if (origin == NULL) { if (!PyErr_ExceptionMatches(PyExc_SystemError)) { Py_DECREF(mod_name_or_unknown); return NULL; @@ -2812,8 +2812,8 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) _PyErr_Clear(tstate); } } - if (pkgpath == NULL || !PyUnicode_Check(pkgpath)) { - Py_CLEAR(pkgpath); + if (origin == NULL || !PyUnicode_Check(origin)) { + Py_CLEAR(origin); errmsg = PyUnicode_FromFormat( "cannot import name %R from %R (unknown location)", name, mod_name_or_unknown @@ -2828,7 +2828,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) } if (rc < 0) { Py_DECREF(mod_name_or_unknown); - Py_DECREF(pkgpath); + Py_DECREF(origin); return NULL; } const char *fmt = @@ -2837,14 +2837,14 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) "(most likely due to a circular import) (%S)" : "cannot import name %R from %R (%S)"; - errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, pkgpath); + errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, origin); } /* NULL checks for errmsg and mod_name done by PyErr_SetImportError. */ - _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, pkgpath, name); + _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, origin, name); Py_XDECREF(errmsg); Py_DECREF(mod_name_or_unknown); - Py_XDECREF(pkgpath); + Py_XDECREF(origin); return NULL; } From 6ce3a67af03361c34d4e9dc1bb44919c2bf71188 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 17:24:23 -0700 Subject: [PATCH 03/11] switch logic to actually use origin, matching other code path --- Include/internal/pycore_moduleobject.h | 1 + Objects/moduleobject.c | 8 +-- Python/ceval.c | 70 ++++++++++++++------------ 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 049677b292e235..1cd4745b23cd29 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -11,6 +11,7 @@ extern "C" { extern void _PyModule_Clear(PyObject *); extern void _PyModule_ClearDict(PyObject *); extern int _PyModuleSpec_IsInitializing(PyObject *); +extern int _PyModuleSpec_GetFileOrigin(PyObject *, PyObject **); extern int _PyModule_IsExtension(PyObject *obj); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index efc74dafb5fc73..4f9226bdb711ba 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -824,15 +824,15 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name) return rc; } -static int -_get_file_origin_from_spec(PyObject *spec, PyObject **p_origin) +int +_PyModuleSpec_GetFileOrigin(PyObject *spec, PyObject **p_origin) { PyObject *has_location = NULL; int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(has_location), &has_location); if (rc <= 0) { return rc; } - // If origin is not a location, or doesn't exist, or is not a str), we could consider falling + // 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); @@ -981,7 +981,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } PyObject *origin = NULL; - if (_get_file_origin_from_spec(spec, &origin) < 0) { + if (_PyModuleSpec_GetFileOrigin(spec, &origin) < 0) { goto done; } diff --git a/Python/ceval.c b/Python/ceval.c index fc3e404ffd1922..3a85bd6b06c5e3 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2763,7 +2763,7 @@ PyObject * _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) { PyObject *x; - PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg; + PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec; if (PyObject_GetOptionalAttr(v, name, &x) != 0) { return x; @@ -2790,6 +2790,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) } Py_DECREF(mod_name); return x; + error: if (mod_name == NULL) { mod_name_or_unknown = PyUnicode_FromString(""); @@ -2799,52 +2800,55 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) } else { mod_name_or_unknown = mod_name; } + // mod_name is no longer an owned reference + assert(mod_name == NULL || mod_name == mod_name_or_unknown); origin = NULL; - if (PyModule_Check(v)) { - origin = PyModule_GetFilenameObject(v); - if (origin == NULL) { - if (!PyErr_ExceptionMatches(PyExc_SystemError)) { - Py_DECREF(mod_name_or_unknown); - return NULL; - } - // module filename missing - _PyErr_Clear(tstate); - } + if (PyObject_GetOptionalAttr(v, &_Py_ID(__spec__), &spec) < 0) { + Py_DECREF(mod_name_or_unknown); + return NULL; } - if (origin == NULL || !PyUnicode_Check(origin)) { - Py_CLEAR(origin); + if (spec == NULL) { errmsg = PyUnicode_FromFormat( "cannot import name %R from %R (unknown location)", name, mod_name_or_unknown ); + goto done_with_errmsg; + } + if (_PyModuleSpec_GetFileOrigin(spec, &origin) < 0) { + goto done; + } + if (origin == NULL) { + errmsg = PyUnicode_FromFormat( + "cannot import name %R from %R (unknown location)", + name, mod_name_or_unknown + ); + goto done_with_errmsg; } - else { - PyObject *spec; - int rc = PyObject_GetOptionalAttr(v, &_Py_ID(__spec__), &spec); - if (rc > 0) { - rc = _PyModuleSpec_IsInitializing(spec); - Py_DECREF(spec); - } - if (rc < 0) { - Py_DECREF(mod_name_or_unknown); - Py_DECREF(origin); - return NULL; - } - const char *fmt = - rc ? - "cannot import name %R from partially initialized module %R " - "(most likely due to a circular import) (%S)" : - "cannot import name %R from %R (%S)"; - errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, origin); + int rc = _PyModuleSpec_IsInitializing(spec); + if (rc < 0) { + Py_DECREF(mod_name_or_unknown); + Py_DECREF(origin); + return NULL; } + const char *fmt = + rc ? + "cannot import name %R from partially initialized module %R " + "(most likely due to a circular import) (%S)" : + "cannot import name %R from %R (%S)"; + + errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, origin); + +done_with_errmsg: /* NULL checks for errmsg and mod_name done by PyErr_SetImportError. */ _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, origin, name); + Py_DECREF(errmsg); - Py_XDECREF(errmsg); - Py_DECREF(mod_name_or_unknown); +done: Py_XDECREF(origin); + Py_XDECREF(spec); + Py_DECREF(mod_name_or_unknown); return NULL; } From aa2e667dc69ad333d5e1126a19f1f9be488a3d8f Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 17:57:55 -0700 Subject: [PATCH 04/11] add error for shadowing --- Include/internal/pycore_moduleobject.h | 1 + Lib/test/test_import/__init__.py | 315 ++++++++++++++++--------- Objects/moduleobject.c | 14 +- Python/ceval.c | 88 +++++-- 4 files changed, 287 insertions(+), 131 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 1cd4745b23cd29..dacc00dba54495 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -12,6 +12,7 @@ extern void _PyModule_Clear(PyObject *); extern void _PyModule_ClearDict(PyObject *); extern int _PyModuleSpec_IsInitializing(PyObject *); extern int _PyModuleSpec_GetFileOrigin(PyObject *, PyObject **); +extern int _PyModule_IsPossiblyShadowing(PyObject *); extern int _PyModule_IsExtension(PyObject *obj); diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 3d89d69955bb07..872a2b8328c866 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -786,104 +786,133 @@ def test_issue105979(self): str(cm.exception)) def test_script_shadowing_stdlib(self): - with os_helper.temp_dir() as tmp: - 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 '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 the import system gives it precedence\)" + script_errors = [ + ( + "import fractions\nfractions.Fraction", + rb"AttributeError: module 'fractions' has no attribute 'Fraction'" + ), + ( + "from fractions import Fraction", + rb"ImportError: cannot import name 'Fraction' from 'fractions'" ) + ] + for script, error in script_errors: + with os_helper.temp_dir() as tmp: + with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: + f.write(script) + + expected_error = error + ( + rb" \(consider renaming '.*fractions.py' since it has the " + rb"same name as the standard library module named 'fractions' " + rb"and the import system gives it precedence\)" + ) - popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) - stdout, stderr = popen.communicate() - self.assertRegex(stdout, expected_error) + popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) - popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp) - stdout, stderr = popen.communicate() - self.assertRegex(stdout, expected_error) + 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 fractions', cwd=tmp) - stdout, stderr = popen.communicate() - self.assertRegex(stdout, expected_error) + 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', 'fractions.py', cwd=tmp) - stdout, stderr = popen.communicate() - self.assertEqual(stdout, b'') + # and there's no error at all when using -P + popen = script_helper.spawn_python('-P', 'fractions.py', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual(stdout, b'') - tmp_child = os.path.join(tmp, "child") - os.mkdir(tmp_child) + tmp_child = os.path.join(tmp, "child") + os.mkdir(tmp_child) - # 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) + # 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) - 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('-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 + popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp_child) + 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 the import system gives it precedence\)" + script_errors = [ + ( + "fractions.Fraction", + rb"AttributeError: module 'fractions' has no attribute 'Fraction'" + ), + ( + "from fractions import Fraction", + rb"ImportError: cannot import name 'Fraction' from 'fractions'" ) + ] + for script, error in script_errors: + 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\n") + f.write(script) + + expected_error = error + ( + rb" \(consider renaming '.*fractions.__init__.py' since it has the " + rb"same name as the standard library module named 'fractions' " + rb"and the import system gives it precedence\)" + ) - 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(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) + 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'") + # 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): + script_errors = [ + ( + "import numpy\nnumpy.array", + rb"AttributeError: module 'numpy' has no attribute 'array'" + ), + ( + "from numpy import array", + rb"ImportError: cannot import name 'array' from 'numpy'" + ) + ] 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") + for script, error in script_errors: + with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: + f.write(script) - 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\)\s+\Z" - ) + expected_error = error + ( + rb" \(consider renaming '.*numpy.py' if it has the " + 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")) - stdout, stderr = popen.communicate() - self.assertRegex(stdout, expected_error) + popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) + 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('-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) + popen = script_helper.spawn_python('-c', 'import numpy', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) def test_script_maybe_not_shadowing_third_party(self): with os_helper.temp_dir() as tmp: @@ -893,15 +922,23 @@ def test_script_maybe_not_shadowing_third_party(self): expected_error = ( rb"AttributeError: module 'numpy' has no attribute 'attr'\s+\Z" ) - popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp) stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error) + expected_error = ( + rb"ImportError: cannot import name 'attr' from 'numpy' \(.*\)\s+\Z" + ) + popen = script_helper.spawn_python('-c', 'from numpy import attr', cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) + def test_script_shadowing_stdlib_edge_cases(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") + + # Unhashable str subclass with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" import fractions @@ -914,11 +951,28 @@ class substr(str): except TypeError as e: print(str(e)) """) + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") + + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import fractions +fractions.shadowing_module +class substr(str): + __hash__ = None +fractions.__name__ = substr('fractions') +try: + from fractions import Fraction +except TypeError as e: + print(str(e)) +""") popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") + # Various issues with sys module with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" import fractions @@ -943,18 +997,41 @@ class substr(str): except AttributeError as e: print(str(e)) """) + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + for line in stdout.splitlines(): + self.assertEqual(line, b"module 'fractions' has no attribute 'Fraction'") + with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: + f.write(""" +import fractions +fractions.shadowing_module + +import sys +sys.stdlib_module_names = None +try: + from fractions import Fraction +except ImportError as e: + print(str(e)) + +del sys.stdlib_module_names +try: + from fractions import Fraction +except ImportError as e: + print(str(e)) + +sys.path = [0] +try: + from fractions import Fraction +except ImportError 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'", - b"module 'fractions' has no attribute 'Fraction'", - b"module 'fractions' has no attribute 'Fraction'", - ], - ) + for line in stdout.splitlines(): + self.assertRegex(line, rb"cannot import name 'Fraction' from 'fractions' \(.*\)") + # Various issues with origin with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" import fractions @@ -974,37 +1051,57 @@ class substr(str): popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - self.assertEqual( - stdout.splitlines(), - [ - 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' " - rb"and the import system gives it precedence\)" - ) + for line in stdout.splitlines(): + self.assertEqual(line, b"module 'fractions' has no attribute 'Fraction'") 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 -""") +fractions.shadowing_module +del fractions.__spec__.origin +try: + from fractions import Fraction +except ImportError as e: + print(str(e)) +fractions.__spec__.origin = 0 +try: + from fractions import Fraction +except ImportError as e: + print(str(e)) +""") popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - self.assertRegex(stdout, expected_error) + for line in stdout.splitlines(): + self.assertRegex(line, rb"cannot import name 'Fraction' from 'fractions' \(.*\)") + + def test_script_shadowing_stdlib_sys_path_modification(self): + script_errors = [ + ( + "import fractions\nfractions.Fraction", + rb"AttributeError: module 'fractions' has no attribute 'Fraction'" + ), + ( + "from fractions import Fraction", + rb"ImportError: cannot import name 'Fraction' from 'fractions'" + ) + ] + for script, error in script_errors: + 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") + 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")\n') + f.write(script) + expected_error = error + ( + rb" \(consider renaming '.*fractions.py' since it has the " + rb"same name as the standard library module named 'fractions' " + rb"and the import system gives it precedence\)" + ) + + popen = script_helper.spawn_python("main.py", cwd=tmp) + stdout, stderr = popen.communicate() + self.assertRegex(stdout, expected_error) @skip_if_dont_write_bytecode diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 4f9226bdb711ba..98acd90972a2a7 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -855,8 +855,8 @@ _PyModuleSpec_GetFileOrigin(PyObject *spec, PyObject **p_origin) return 1; } -static int -_is_module_possibly_shadowing(PyObject *origin) +int +_PyModule_IsPossiblyShadowing(PyObject *origin) { // origin must be a unicode subtype // Returns 1 if the module at origin could be shadowing a module of the @@ -985,7 +985,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) goto done; } - int is_possibly_shadowing = _is_module_possibly_shadowing(origin); + int is_possibly_shadowing = _PyModule_IsPossiblyShadowing(origin); if (is_possibly_shadowing < 0) { goto done; } @@ -1011,7 +1011,10 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } else { int rc = _PyModuleSpec_IsInitializing(spec); - if (rc > 0) { + if (rc < 0) { + goto done; + } + else if (rc > 0) { if (is_possibly_shadowing) { assert(origin); // For third-party modules, only mention the possibility of @@ -1037,7 +1040,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) mod_name, name); } } - else if (rc == 0) { + else { + assert(rc == 0); rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); if (rc > 0) { PyErr_Format(PyExc_AttributeError, diff --git a/Python/ceval.c b/Python/ceval.c index 3a85bd6b06c5e3..90855e88e0adb9 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2801,6 +2801,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) mod_name_or_unknown = mod_name; } // mod_name is no longer an owned reference + assert(mod_name_or_unknown); assert(mod_name == NULL || mod_name == mod_name_or_unknown); origin = NULL; @@ -2818,27 +2819,80 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) if (_PyModuleSpec_GetFileOrigin(spec, &origin) < 0) { goto done; } - if (origin == NULL) { + + int is_possibly_shadowing = _PyModule_IsPossiblyShadowing(origin); + if (is_possibly_shadowing < 0) { + goto done; + } + int is_possibly_shadowing_stdlib = 0; + 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_or_unknown); + if (is_possibly_shadowing_stdlib < 0) { + goto done; + } + } + } + + if (is_possibly_shadowing_stdlib) { + assert(origin); errmsg = PyUnicode_FromFormat( - "cannot import name %R from %R (unknown location)", - name, mod_name_or_unknown + "cannot import name %R from %R " + "(consider renaming %R since it has the same " + "name as the standard library module named %R " + "and the import system gives it precedence)", + name, mod_name_or_unknown, origin, mod_name_or_unknown ); - goto done_with_errmsg; } - - int rc = _PyModuleSpec_IsInitializing(spec); - if (rc < 0) { - Py_DECREF(mod_name_or_unknown); - Py_DECREF(origin); - return NULL; + else { + int rc = _PyModuleSpec_IsInitializing(spec); + if (rc < 0) { + goto done; + } + else if (rc > 0) { + if (is_possibly_shadowing) { + assert(origin); + // For third-party modules, only mention the possibility of + // shadowing if the module is being initialized. + errmsg = PyUnicode_FromFormat( + "cannot import name %R from %R " + "(consider renaming %R if it has the same name " + "as a third-party module you intended to import)", + name, mod_name_or_unknown, origin + ); + } + else if (origin) { + errmsg = PyUnicode_FromFormat( + "cannot import name %R from partially initialized module %R " + "(most likely due to a circular import) (%S)", + name, mod_name_or_unknown, origin + ); + } + else { + errmsg = PyUnicode_FromFormat( + "cannot import name %R from partially initialized module %R " + "(most likely due to a circular import)", + name, mod_name_or_unknown + ); + } + } + else { + assert(rc == 0); + if (origin) { + errmsg = PyUnicode_FromFormat( + "cannot import name %R from %R (%S)", + name, mod_name_or_unknown, origin + ); + } + else { + errmsg = PyUnicode_FromFormat( + "cannot import name %R from %R (unknown location)", + name, mod_name_or_unknown + ); + } + } } - const char *fmt = - rc ? - "cannot import name %R from partially initialized module %R " - "(most likely due to a circular import) (%S)" : - "cannot import name %R from %R (%S)"; - - errmsg = PyUnicode_FromFormat(fmt, name, mod_name_or_unknown, origin); done_with_errmsg: /* NULL checks for errmsg and mod_name done by PyErr_SetImportError. */ From cac393bd4becd3fbac17c34165b7d93158af227f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 01:32:11 +0000 Subject: [PATCH 05/11] =?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-09-11-01-32-07.gh-issue-123930.BkPfB6.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-09-11-01-32-07.gh-issue-123930.BkPfB6.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-09-11-01-32-07.gh-issue-123930.BkPfB6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-11-01-32-07.gh-issue-123930.BkPfB6.rst new file mode 100644 index 00000000000000..3c8eb02b2dc2d6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-11-01-32-07.gh-issue-123930.BkPfB6.rst @@ -0,0 +1,4 @@ +Improve the error message when a script shadowing a module from the standard +library causes :exc:`ImportError` to be raised during a "from" import. +Similarly, improve the error message when a script shadowing a third party module +attempts to "from" import an attribute from that third party module while still initialising. From 53e69318ea3c8992762f9989784f01ee0a728005 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 18:36:52 -0700 Subject: [PATCH 06/11] check number of lines, now that we're looping --- Lib/test/test_import/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 872a2b8328c866..5972deee41703c 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -999,7 +999,9 @@ class substr(str): """) popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - for line in stdout.splitlines(): + lines = stdout.splitlines() + self.assertEqual(len(lines), 3) + for line in lines: self.assertEqual(line, b"module 'fractions' has no attribute 'Fraction'") with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: @@ -1028,7 +1030,9 @@ class substr(str): """) popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - for line in stdout.splitlines(): + lines = stdout.splitlines() + self.assertEqual(len(lines), 3) + for line in lines: self.assertRegex(line, rb"cannot import name 'Fraction' from 'fractions' \(.*\)") # Various issues with origin @@ -1051,7 +1055,9 @@ class substr(str): popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - for line in stdout.splitlines(): + lines = stdout.splitlines() + self.assertEqual(len(lines), 2) + for line in lines: self.assertEqual(line, b"module 'fractions' has no attribute 'Fraction'") with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: @@ -1072,7 +1078,9 @@ class substr(str): """) popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - for line in stdout.splitlines(): + lines = stdout.splitlines() + self.assertEqual(len(lines), 2) + for line in lines: self.assertRegex(line, rb"cannot import name 'Fraction' from 'fractions' \(.*\)") def test_script_shadowing_stdlib_sys_path_modification(self): From bd038b30138f13c46bc801dff69d6c45c7e4ce6e Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Tue, 10 Sep 2024 18:42:36 -0700 Subject: [PATCH 07/11] update comment --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 90855e88e0adb9..77f13ccdcf7659 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2895,7 +2895,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) } done_with_errmsg: - /* NULL checks for errmsg and mod_name done by PyErr_SetImportError. */ + /* NULL checks for errmsg, mod_name, origin done by PyErr_SetImportError. */ _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, origin, name); Py_DECREF(errmsg); From 49871c8040081d61fb007a6cb2f1a8d6ec249694 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 10 Sep 2024 19:21:21 -0700 Subject: [PATCH 08/11] fix test on windows --- Lib/test/test_import/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 5972deee41703c..20693b365a6273 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -863,7 +863,7 @@ def test_package_shadowing_stdlib_module(self): f.write(script) expected_error = error + ( - rb" \(consider renaming '.*fractions.__init__.py' since it has the " + rb" \(consider renaming '.*[\\/]fractions[\\/]+__init__.py' since it has the " rb"same name as the standard library module named 'fractions' " rb"and the import system gives it precedence\)" ) From cd227cc2d36180d41f59b06912c1d2745d98dc73 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 19 Oct 2024 18:56:09 -0700 Subject: [PATCH 09/11] change wording based on pablo feedback --- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_import/__init__.py | 2 +- Objects/moduleobject.c | 4 ++-- Python/ceval.c | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index c60def5ad9623c..4524c4855e8988 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -291,7 +291,7 @@ Improved error messages File "/home/me/numpy.py", line 3, in np.array([1, 2, 3]) ^^^^^^^^ - AttributeError: module 'numpy' has no attribute 'array' (consider renaming '/home/me/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/me/numpy.py' if it has the same name as a library 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 20693b365a6273..59f39a1943005e 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -899,7 +899,7 @@ def test_script_shadowing_third_party(self): expected_error = error + ( rb" \(consider renaming '.*numpy.py' if it has the " - rb"same name as a third-party module you intended to import\)\s+\Z" + rb"same name as a library you intended to import\)\s+\Z" ) popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 98acd90972a2a7..6ff1b1d21d8482 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1017,12 +1017,12 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) else if (rc > 0) { if (is_possibly_shadowing) { assert(origin); - // For third-party modules, only mention the possibility of + // For non-stdlib 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 library you intended to import)", mod_name, name, origin); } else if (origin) { diff --git a/Python/ceval.c b/Python/ceval.c index 77f13ccdcf7659..218cfb1cdfb365 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2853,12 +2853,12 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) else if (rc > 0) { if (is_possibly_shadowing) { assert(origin); - // For third-party modules, only mention the possibility of + // For non-stdlib modules, only mention the possibility of // shadowing if the module is being initialized. errmsg = PyUnicode_FromFormat( "cannot import name %R from %R " "(consider renaming %R if it has the same name " - "as a third-party module you intended to import)", + "as a library you intended to import)", name, mod_name_or_unknown, origin ); } From 336a72256bc3ea31ee64779adda3532acfb87e18 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 23 Oct 2024 13:00:39 -0700 Subject: [PATCH 10/11] alyssa review --- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_import/__init__.py | 6 +++--- Objects/moduleobject.c | 2 +- Python/ceval.c | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 0e72dc9eb4cdcb..de4c7fd4c0486b 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -274,7 +274,7 @@ Improved error messages File "/home/me/random.py", line 3, in print(random.randint(5)) ^^^^^^^^^^^^^^ - AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/me/random.py' since it has the same name as the standard library module named 'random' and the import system gives it precedence) + AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/me/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module) Similarly, if a script has the same name as a third-party module that 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 5796cf8bd4ed31..f38f3729318ac6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -822,7 +822,7 @@ def test_script_shadowing_stdlib(self): expected_error = error + ( rb" \(consider renaming '.*fractions.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and the import system gives it precedence\)" + rb"and prevents importing that standard library module\)" ) popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) @@ -883,7 +883,7 @@ def test_package_shadowing_stdlib_module(self): expected_error = error + ( rb" \(consider renaming '.*[\\/]fractions[\\/]+__init__.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and the import system gives it precedence\)" + rb"and prevents importing that standard library module\)" ) popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp) @@ -1122,7 +1122,7 @@ def test_script_shadowing_stdlib_sys_path_modification(self): expected_error = error + ( rb" \(consider renaming '.*fractions.py' since it has the " rb"same name as the standard library module named 'fractions' " - rb"and the import system gives it precedence\)" + rb"and prevents importing that standard library module\)" ) popen = script_helper.spawn_python("main.py", cwd=tmp) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f3d5c200f92bc1..91e3693eb84667 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1017,7 +1017,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 the import system gives it precedence)", + "and prevents importing that standard library module)", mod_name, name, origin, mod_name); } else { diff --git a/Python/ceval.c b/Python/ceval.c index efed4167651285..626f1bba38f822 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2873,7 +2873,7 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) "cannot import name %R from %R " "(consider renaming %R since it has the same " "name as the standard library module named %R " - "and the import system gives it precedence)", + "and prevents importing that standard library module)", name, mod_name_or_unknown, origin, mod_name_or_unknown ); } From 0cea35be9dd97a2685a44c2f8a267e61e24c0038 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 23 Oct 2024 15:53:33 -0700 Subject: [PATCH 11/11] subtest from alex --- 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 f38f3729318ac6..5b7ba90b2cc7c6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -815,7 +815,7 @@ def test_script_shadowing_stdlib(self): ) ] for script, error in script_errors: - with os_helper.temp_dir() as tmp: + with self.subTest(script=script), os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: f.write(script) @@ -870,7 +870,7 @@ def test_package_shadowing_stdlib_module(self): ) ] for script, error in script_errors: - with os_helper.temp_dir() as tmp: + with self.subTest(script=script), 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' @@ -910,8 +910,8 @@ def test_script_shadowing_third_party(self): rb"ImportError: cannot import name 'array' from 'numpy'" ) ] - with os_helper.temp_dir() as tmp: - for script, error in script_errors: + for script, error in script_errors: + with self.subTest(script=script), os_helper.temp_dir() as tmp: with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: f.write(script) @@ -1113,7 +1113,7 @@ def test_script_shadowing_stdlib_sys_path_modification(self): ) ] for script, error in script_errors: - with os_helper.temp_dir() as tmp: + with self.subTest(script=script), 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") with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: