gh-123930: Better error for "from imports" when script shadows module (#123929)

This commit is contained in:
Shantanu 2024-10-24 12:11:12 -07:00 committed by GitHub
parent 3f24bde0b6
commit 500f5338a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 344 additions and 173 deletions

View File

@ -274,7 +274,7 @@ Improved error messages
File "/home/me/random.py", line 3, in <module> File "/home/me/random.py", line 3, in <module>
print(random.randint(5)) 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 Similarly, if a script has the same name as a third-party
module that it attempts to import and this results in errors, module that it attempts to import and this results in errors,
@ -289,7 +289,7 @@ Improved error messages
File "/home/me/numpy.py", line 3, in <module> File "/home/me/numpy.py", line 3, in <module>
np.array([1, 2, 3]) 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`.) (Contributed by Shantanu Jain in :gh:`95754`.)

View File

@ -11,6 +11,8 @@ extern "C" {
extern void _PyModule_Clear(PyObject *); extern void _PyModule_Clear(PyObject *);
extern void _PyModule_ClearDict(PyObject *); extern void _PyModule_ClearDict(PyObject *);
extern int _PyModuleSpec_IsInitializing(PyObject *); extern int _PyModuleSpec_IsInitializing(PyObject *);
extern int _PyModuleSpec_GetFileOrigin(PyObject *, PyObject **);
extern int _PyModule_IsPossiblyShadowing(PyObject *);
extern int _PyModule_IsExtension(PyObject *obj); extern int _PyModule_IsExtension(PyObject *obj);

View File

@ -804,104 +804,133 @@ class ImportTests(unittest.TestCase):
str(cm.exception)) str(cm.exception))
def test_script_shadowing_stdlib(self): def test_script_shadowing_stdlib(self):
with os_helper.temp_dir() as tmp: script_errors = [
with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f: (
f.write("import fractions\nfractions.Fraction") "import fractions\nfractions.Fraction",
rb"AttributeError: module 'fractions' has no attribute 'Fraction'"
expected_error = ( ),
rb"AttributeError: module 'fractions' has no attribute 'Fraction' " (
rb"\(consider renaming '.*fractions.py' since it has the " "from fractions import Fraction",
rb"same name as the standard library module named 'fractions' " rb"ImportError: cannot import name 'Fraction' from 'fractions'"
rb"and the import system gives it precedence\)"
) )
]
for script, error in script_errors:
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)
popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp) expected_error = error + (
stdout, stderr = popen.communicate() rb" \(consider renaming '.*fractions.py' since it has the "
self.assertRegex(stdout, expected_error) rb"same name as the standard library module named 'fractions' "
rb"and prevents importing that standard library module\)"
)
popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp) popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) self.assertRegex(stdout, expected_error)
popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp) popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) self.assertRegex(stdout, expected_error)
# and there's no error at all when using -P popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp)
popen = script_helper.spawn_python('-P', 'fractions.py', cwd=tmp) stdout, stderr = popen.communicate()
stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error)
self.assertEqual(stdout, b'')
tmp_child = os.path.join(tmp, "child") # and there's no error at all when using -P
os.mkdir(tmp_child) popen = script_helper.spawn_python('-P', 'fractions.py', cwd=tmp)
stdout, stderr = popen.communicate()
self.assertEqual(stdout, b'')
# test the logic with different cwd tmp_child = os.path.join(tmp, "child")
popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp_child) os.mkdir(tmp_child)
stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error)
popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp_child) # test the logic with different cwd
stdout, stderr = popen.communicate() popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp_child)
self.assertEqual(stdout, b'') # no error stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error)
popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp_child) popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp_child)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertEqual(stdout, b'') # no error 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): def test_package_shadowing_stdlib_module(self):
with os_helper.temp_dir() as tmp: script_errors = [
os.mkdir(os.path.join(tmp, "fractions")) (
with open(os.path.join(tmp, "fractions", "__init__.py"), "w", encoding='utf-8') as f: "fractions.Fraction",
f.write("shadowing_module = True") rb"AttributeError: module 'fractions' has no attribute 'Fraction'"
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: ),
f.write(""" (
import fractions "from fractions import Fraction",
fractions.shadowing_module rb"ImportError: cannot import name 'Fraction' from 'fractions'"
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\)"
) )
]
for script, error in script_errors:
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'
) 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)
popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp) expected_error = error + (
stdout, stderr = popen.communicate() rb" \(consider renaming '.*[\\/]fractions[\\/]+__init__.py' since it has the "
self.assertRegex(stdout, expected_error) rb"same name as the standard library module named 'fractions' "
rb"and prevents importing that standard library module\)"
)
popen = script_helper.spawn_python('-m', 'main', cwd=tmp) popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) self.assertRegex(stdout, expected_error)
# and there's no shadowing at all when using -P popen = script_helper.spawn_python('-m', 'main', cwd=tmp)
popen = script_helper.spawn_python('-P', 'main.py', cwd=tmp) stdout, stderr = popen.communicate()
stdout, stderr = popen.communicate() self.assertRegex(stdout, expected_error)
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): def test_script_shadowing_third_party(self):
with os_helper.temp_dir() as tmp: script_errors = [
with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f: (
f.write("import numpy\nnumpy.array") "import numpy\nnumpy.array",
rb"AttributeError: module 'numpy' has no attribute 'array'"
expected_error = ( ),
rb"AttributeError: module 'numpy' has no attribute 'array' " (
rb"\(consider renaming '.*numpy.py' if it has the " "from numpy import array",
rb"same name as a third-party module you intended to import\)\s+\Z" rb"ImportError: cannot import name 'array' from 'numpy'"
) )
]
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)
popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py")) expected_error = error + (
stdout, stderr = popen.communicate() rb" \(consider renaming '.*numpy.py' if it has the "
self.assertRegex(stdout, expected_error) rb"same name as a library you intended to import\)\s+\Z"
)
popen = script_helper.spawn_python('-m', 'numpy', cwd=tmp) popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py"))
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) self.assertRegex(stdout, expected_error)
popen = script_helper.spawn_python('-c', 'import numpy', cwd=tmp) popen = script_helper.spawn_python('-m', 'numpy', cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) 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): def test_script_maybe_not_shadowing_third_party(self):
with os_helper.temp_dir() as tmp: with os_helper.temp_dir() as tmp:
@ -911,15 +940,23 @@ fractions.Fraction
expected_error = ( expected_error = (
rb"AttributeError: module 'numpy' has no attribute 'attr'\s+\Z" rb"AttributeError: module 'numpy' has no attribute 'attr'\s+\Z"
) )
popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp) popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) 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): def test_script_shadowing_stdlib_edge_cases(self):
with os_helper.temp_dir() as tmp: with os_helper.temp_dir() as tmp:
with open(os.path.join(tmp, "fractions.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") f.write("shadowing_module = True")
# Unhashable str subclass
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
f.write(""" f.write("""
import fractions import fractions
@ -932,11 +969,28 @@ try:
except TypeError as e: except TypeError as e:
print(str(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) popen = script_helper.spawn_python("main.py", cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") 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: with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
f.write(""" f.write("""
import fractions import fractions
@ -961,22 +1015,49 @@ try:
except AttributeError as e: except AttributeError as e:
print(str(e)) print(str(e))
""") """)
popen = script_helper.spawn_python("main.py", cwd=tmp) popen = script_helper.spawn_python("main.py", cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertEqual( lines = stdout.splitlines()
stdout.splitlines(), self.assertEqual(len(lines), 3)
[ for line in lines:
b"module 'fractions' has no attribute 'Fraction'", self.assertEqual(line, b"module 'fractions' has no attribute 'Fraction'")
b"module 'fractions' has no attribute 'Fraction'",
b"module 'fractions' has no attribute 'Fraction'",
],
)
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
f.write(""" f.write("""
import fractions import fractions
fractions.shadowing_module 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()
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
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 del fractions.__spec__.origin
try: try:
fractions.Fraction fractions.Fraction
@ -992,37 +1073,61 @@ except AttributeError as e:
popen = script_helper.spawn_python("main.py", cwd=tmp) popen = script_helper.spawn_python("main.py", cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertEqual( lines = stdout.splitlines()
stdout.splitlines(), self.assertEqual(len(lines), 2)
[ for line in lines:
b"module 'fractions' has no attribute 'Fraction'", self.assertEqual(line, 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\)"
)
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
f.write(""" f.write("""
import sys
sys.path.insert(0, "this_folder_does_not_exist")
import fractions 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) popen = script_helper.spawn_python("main.py", cwd=tmp)
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error) 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):
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 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:
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 prevents importing that standard library module\)"
)
popen = script_helper.spawn_python("main.py", cwd=tmp)
stdout, stderr = popen.communicate()
self.assertRegex(stdout, expected_error)
@skip_if_dont_write_bytecode @skip_if_dont_write_bytecode

View File

@ -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.

View File

@ -836,15 +836,15 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name)
return rc; return rc;
} }
static int int
_get_file_origin_from_spec(PyObject *spec, PyObject **p_origin) _PyModuleSpec_GetFileOrigin(PyObject *spec, PyObject **p_origin)
{ {
PyObject *has_location = NULL; PyObject *has_location = NULL;
int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(has_location), &has_location); int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(has_location), &has_location);
if (rc <= 0) { if (rc <= 0) {
return rc; 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 // 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. // are cases in which we probably shouldn't be guessing.
rc = PyObject_IsTrue(has_location); rc = PyObject_IsTrue(has_location);
@ -867,8 +867,8 @@ _get_file_origin_from_spec(PyObject *spec, PyObject **p_origin)
return 1; return 1;
} }
static int int
_is_module_possibly_shadowing(PyObject *origin) _PyModule_IsPossiblyShadowing(PyObject *origin)
{ {
// origin must be a unicode subtype // origin must be a unicode subtype
// Returns 1 if the module at origin could be shadowing a module of the // Returns 1 if the module at origin could be shadowing a module of the
@ -993,11 +993,11 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
} }
PyObject *origin = NULL; PyObject *origin = NULL;
if (_get_file_origin_from_spec(spec, &origin) < 0) { if (_PyModuleSpec_GetFileOrigin(spec, &origin) < 0) {
goto done; goto done;
} }
int is_possibly_shadowing = _is_module_possibly_shadowing(origin); int is_possibly_shadowing = _PyModule_IsPossiblyShadowing(origin);
if (is_possibly_shadowing < 0) { if (is_possibly_shadowing < 0) {
goto done; goto done;
} }
@ -1018,20 +1018,23 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
"module '%U' has no attribute '%U' " "module '%U' has no attribute '%U' "
"(consider renaming '%U' since it has the same " "(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 the import system gives it precedence)", "and prevents importing that standard library module)",
mod_name, name, origin, mod_name); mod_name, name, origin, mod_name);
} }
else { else {
int rc = _PyModuleSpec_IsInitializing(spec); int rc = _PyModuleSpec_IsInitializing(spec);
if (rc > 0) { if (rc < 0) {
goto done;
}
else if (rc > 0) {
if (is_possibly_shadowing) { if (is_possibly_shadowing) {
assert(origin); 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. // shadowing if the module is being initialized.
PyErr_Format(PyExc_AttributeError, PyErr_Format(PyExc_AttributeError,
"module '%U' has no attribute '%U' " "module '%U' has no attribute '%U' "
"(consider renaming '%U' if it has the same name " "(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); mod_name, name, origin);
} }
else if (origin) { else if (origin) {
@ -1049,7 +1052,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
mod_name, name); mod_name, name);
} }
} }
else if (rc == 0) { else {
assert(rc == 0);
rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name); rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name);
if (rc > 0) { if (rc > 0) {
PyErr_Format(PyExc_AttributeError, PyErr_Format(PyExc_AttributeError,

View File

@ -2802,7 +2802,7 @@ PyObject *
_PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
{ {
PyObject *x; PyObject *x;
PyObject *fullmodname, *pkgname, *pkgpath, *pkgname_or_unknown, *errmsg; PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec;
if (PyObject_GetOptionalAttr(v, name, &x) != 0) { if (PyObject_GetOptionalAttr(v, name, &x) != 0) {
return x; return x;
@ -2810,16 +2810,16 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
/* Issue #17636: in case this failed because of a circular relative /* Issue #17636: in case this failed because of a circular relative
import, try to fallback on reading the module directly from import, try to fallback on reading the module directly from
sys.modules. */ sys.modules. */
if (PyObject_GetOptionalAttr(v, &_Py_ID(__name__), &pkgname) < 0) { if (PyObject_GetOptionalAttr(v, &_Py_ID(__name__), &mod_name) < 0) {
return NULL; return NULL;
} }
if (pkgname == NULL || !PyUnicode_Check(pkgname)) { if (mod_name == NULL || !PyUnicode_Check(mod_name)) {
Py_CLEAR(pkgname); Py_CLEAR(mod_name);
goto error; goto error;
} }
fullmodname = PyUnicode_FromFormat("%U.%U", pkgname, name); fullmodname = PyUnicode_FromFormat("%U.%U", mod_name, name);
if (fullmodname == NULL) { if (fullmodname == NULL) {
Py_DECREF(pkgname); Py_DECREF(mod_name);
return NULL; return NULL;
} }
x = PyImport_GetModule(fullmodname); x = PyImport_GetModule(fullmodname);
@ -2827,63 +2827,121 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
if (x == NULL && !_PyErr_Occurred(tstate)) { if (x == NULL && !_PyErr_Occurred(tstate)) {
goto error; goto error;
} }
Py_DECREF(pkgname); Py_DECREF(mod_name);
return x; return x;
error: error:
if (pkgname == NULL) { if (mod_name == NULL) {
pkgname_or_unknown = PyUnicode_FromString("<unknown module name>"); mod_name_or_unknown = PyUnicode_FromString("<unknown module name>");
if (pkgname_or_unknown == NULL) { if (mod_name_or_unknown == NULL) {
return NULL; return NULL;
} }
} else { } else {
pkgname_or_unknown = pkgname; 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);
pkgpath = NULL; origin = NULL;
if (PyModule_Check(v)) { if (PyObject_GetOptionalAttr(v, &_Py_ID(__spec__), &spec) < 0) {
pkgpath = PyModule_GetFilenameObject(v); Py_DECREF(mod_name_or_unknown);
if (pkgpath == NULL) { return NULL;
if (!PyErr_ExceptionMatches(PyExc_SystemError)) {
Py_DECREF(pkgname_or_unknown);
return NULL;
}
// module filename missing
_PyErr_Clear(tstate);
}
} }
if (pkgpath == NULL || !PyUnicode_Check(pkgpath)) { if (spec == NULL) {
Py_CLEAR(pkgpath);
errmsg = PyUnicode_FromFormat( errmsg = PyUnicode_FromFormat(
"cannot import name %R from %R (unknown location)", "cannot import name %R from %R (unknown location)",
name, pkgname_or_unknown name, mod_name_or_unknown
);
goto done_with_errmsg;
}
if (_PyModuleSpec_GetFileOrigin(spec, &origin) < 0) {
goto done;
}
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 "
"(consider renaming %R since it has the same "
"name as the standard library module named %R "
"and prevents importing that standard library module)",
name, mod_name_or_unknown, origin, mod_name_or_unknown
); );
} }
else { else {
PyObject *spec; int rc = _PyModuleSpec_IsInitializing(spec);
int rc = PyObject_GetOptionalAttr(v, &_Py_ID(__spec__), &spec);
if (rc > 0) {
rc = _PyModuleSpec_IsInitializing(spec);
Py_DECREF(spec);
}
if (rc < 0) { if (rc < 0) {
Py_DECREF(pkgname_or_unknown); goto done;
Py_DECREF(pkgpath); }
return NULL; else if (rc > 0) {
if (is_possibly_shadowing) {
assert(origin);
// 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 library 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, pkgname_or_unknown, pkgpath);
} }
/* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */
_PyErr_SetImportErrorWithNameFrom(errmsg, pkgname, pkgpath, name);
Py_XDECREF(errmsg); done_with_errmsg:
Py_DECREF(pkgname_or_unknown); /* NULL checks for errmsg, mod_name, origin done by PyErr_SetImportError. */
Py_XDECREF(pkgpath); _PyErr_SetImportErrorWithNameFrom(errmsg, mod_name, origin, name);
Py_DECREF(errmsg);
done:
Py_XDECREF(origin);
Py_XDECREF(spec);
Py_DECREF(mod_name_or_unknown);
return NULL; return NULL;
} }
@ -3243,5 +3301,3 @@ _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *na
} }
return value; return value;
} }