Skip to content

Commit 8e86579

Browse files
authored
gh-95754: Better error when script shadows a standard library or third party module (#113769)
1 parent c9829ee commit 8e86579

File tree

8 files changed

+456
-53
lines changed

8 files changed

+456
-53
lines changed

Doc/whatsnew/3.13.rst

+34
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,40 @@ Improved Error Messages
104104
variables. See also :ref:`using-on-controlling-color`.
105105
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)
106106

107+
* A common mistake is to write a script with the same name as a
108+
standard library module. When this results in errors, we now
109+
display a more helpful error message:
110+
111+
.. code-block:: shell-session
112+
113+
$ python random.py
114+
Traceback (most recent call last):
115+
File "/home/random.py", line 1, in <module>
116+
import random; print(random.randint(5))
117+
^^^^^^^^^^^^^
118+
File "/home/random.py", line 1, in <module>
119+
import random; print(random.randint(5))
120+
^^^^^^^^^^^^^^
121+
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)
122+
123+
Similarly, if a script has the same name as a third-party
124+
module it attempts to import, and this results in errors,
125+
we also display a more helpful error message:
126+
127+
.. code-block:: shell-session
128+
129+
$ python numpy.py
130+
Traceback (most recent call last):
131+
File "/home/numpy.py", line 1, in <module>
132+
import numpy as np; np.array([1,2,3])
133+
^^^^^^^^^^^^^^^^^^
134+
File "/home/numpy.py", line 1, in <module>
135+
import numpy as np; np.array([1,2,3])
136+
^^^^^^^^
137+
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)
138+
139+
(Contributed by Shantanu Jain in :gh:`95754`.)
140+
107141
* When an incorrect keyword argument is passed to a function, the error message
108142
now potentially suggests the correct keyword argument.
109143
(Contributed by Pablo Galindo Salgado and Shantanu Jain in :gh:`107944`.)

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ struct _Py_global_strings {
470470
STRUCT_FOR_ID(h)
471471
STRUCT_FOR_ID(handle)
472472
STRUCT_FOR_ID(handle_seq)
473+
STRUCT_FOR_ID(has_location)
473474
STRUCT_FOR_ID(hash_name)
474475
STRUCT_FOR_ID(header)
475476
STRUCT_FOR_ID(headers)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_import/__init__.py

+221
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,227 @@ def test_issue105979(self):
804804
self.assertIn("Frozen object named 'x' is invalid",
805805
str(cm.exception))
806806

807+
def test_script_shadowing_stdlib(self):
808+
with os_helper.temp_dir() as tmp:
809+
with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f:
810+
f.write("import fractions\nfractions.Fraction")
811+
812+
expected_error = (
813+
rb"AttributeError: module 'fractions' has no attribute 'Fraction' "
814+
rb"\(consider renaming '.*fractions.py' since it has the "
815+
rb"same name as the standard library module named 'fractions' "
816+
rb"and the import system gives it precedence\)"
817+
)
818+
819+
popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp)
820+
stdout, stderr = popen.communicate()
821+
self.assertRegex(stdout, expected_error)
822+
823+
popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp)
824+
stdout, stderr = popen.communicate()
825+
self.assertRegex(stdout, expected_error)
826+
827+
popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp)
828+
stdout, stderr = popen.communicate()
829+
self.assertRegex(stdout, expected_error)
830+
831+
# and there's no error at all when using -P
832+
popen = script_helper.spawn_python('-P', 'fractions.py', cwd=tmp)
833+
stdout, stderr = popen.communicate()
834+
self.assertEqual(stdout, b'')
835+
836+
tmp_child = os.path.join(tmp, "child")
837+
os.mkdir(tmp_child)
838+
839+
# test the logic with different cwd
840+
popen = script_helper.spawn_python(os.path.join(tmp, "fractions.py"), cwd=tmp_child)
841+
stdout, stderr = popen.communicate()
842+
self.assertRegex(stdout, expected_error)
843+
844+
popen = script_helper.spawn_python('-m', 'fractions', cwd=tmp_child)
845+
stdout, stderr = popen.communicate()
846+
self.assertEqual(stdout, b'') # no error
847+
848+
popen = script_helper.spawn_python('-c', 'import fractions', cwd=tmp_child)
849+
stdout, stderr = popen.communicate()
850+
self.assertEqual(stdout, b'') # no error
851+
852+
def test_package_shadowing_stdlib_module(self):
853+
with os_helper.temp_dir() as tmp:
854+
os.mkdir(os.path.join(tmp, "fractions"))
855+
with open(os.path.join(tmp, "fractions", "__init__.py"), "w", encoding='utf-8') as f:
856+
f.write("shadowing_module = True")
857+
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
858+
f.write("""
859+
import fractions
860+
fractions.shadowing_module
861+
fractions.Fraction
862+
""")
863+
864+
expected_error = (
865+
rb"AttributeError: module 'fractions' has no attribute 'Fraction' "
866+
rb"\(consider renaming '.*fractions.__init__.py' since it has the "
867+
rb"same name as the standard library module named 'fractions' "
868+
rb"and the import system gives it precedence\)"
869+
)
870+
871+
popen = script_helper.spawn_python(os.path.join(tmp, "main.py"), cwd=tmp)
872+
stdout, stderr = popen.communicate()
873+
self.assertRegex(stdout, expected_error)
874+
875+
popen = script_helper.spawn_python('-m', 'main', cwd=tmp)
876+
stdout, stderr = popen.communicate()
877+
self.assertRegex(stdout, expected_error)
878+
879+
# and there's no shadowing at all when using -P
880+
popen = script_helper.spawn_python('-P', 'main.py', cwd=tmp)
881+
stdout, stderr = popen.communicate()
882+
self.assertRegex(stdout, b"module 'fractions' has no attribute 'shadowing_module'")
883+
884+
def test_script_shadowing_third_party(self):
885+
with os_helper.temp_dir() as tmp:
886+
with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f:
887+
f.write("import numpy\nnumpy.array")
888+
889+
expected_error = (
890+
rb"AttributeError: module 'numpy' has no attribute 'array' "
891+
rb"\(consider renaming '.*numpy.py' if it has the "
892+
rb"same name as a third-party module you intended to import\)\s+\Z"
893+
)
894+
895+
popen = script_helper.spawn_python(os.path.join(tmp, "numpy.py"))
896+
stdout, stderr = popen.communicate()
897+
self.assertRegex(stdout, expected_error)
898+
899+
popen = script_helper.spawn_python('-m', 'numpy', cwd=tmp)
900+
stdout, stderr = popen.communicate()
901+
self.assertRegex(stdout, expected_error)
902+
903+
popen = script_helper.spawn_python('-c', 'import numpy', cwd=tmp)
904+
stdout, stderr = popen.communicate()
905+
self.assertRegex(stdout, expected_error)
906+
907+
def test_script_maybe_not_shadowing_third_party(self):
908+
with os_helper.temp_dir() as tmp:
909+
with open(os.path.join(tmp, "numpy.py"), "w", encoding='utf-8') as f:
910+
f.write("this_script_does_not_attempt_to_import_numpy = True")
911+
912+
expected_error = (
913+
rb"AttributeError: module 'numpy' has no attribute 'attr'\s+\Z"
914+
)
915+
916+
popen = script_helper.spawn_python('-c', 'import numpy; numpy.attr', cwd=tmp)
917+
stdout, stderr = popen.communicate()
918+
self.assertRegex(stdout, expected_error)
919+
920+
def test_script_shadowing_stdlib_edge_cases(self):
921+
with os_helper.temp_dir() as tmp:
922+
with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f:
923+
f.write("shadowing_module = True")
924+
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
925+
f.write("""
926+
import fractions
927+
fractions.shadowing_module
928+
class substr(str):
929+
__hash__ = None
930+
fractions.__name__ = substr('fractions')
931+
try:
932+
fractions.Fraction
933+
except TypeError as e:
934+
print(str(e))
935+
""")
936+
937+
popen = script_helper.spawn_python("main.py", cwd=tmp)
938+
stdout, stderr = popen.communicate()
939+
self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'")
940+
941+
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
942+
f.write("""
943+
import fractions
944+
fractions.shadowing_module
945+
946+
import sys
947+
sys.stdlib_module_names = None
948+
try:
949+
fractions.Fraction
950+
except AttributeError as e:
951+
print(str(e))
952+
953+
del sys.stdlib_module_names
954+
try:
955+
fractions.Fraction
956+
except AttributeError as e:
957+
print(str(e))
958+
959+
sys.path = [0]
960+
try:
961+
fractions.Fraction
962+
except AttributeError as e:
963+
print(str(e))
964+
""")
965+
966+
popen = script_helper.spawn_python("main.py", cwd=tmp)
967+
stdout, stderr = popen.communicate()
968+
self.assertEqual(
969+
stdout.splitlines(),
970+
[
971+
b"module 'fractions' has no attribute 'Fraction'",
972+
b"module 'fractions' has no attribute 'Fraction'",
973+
b"module 'fractions' has no attribute 'Fraction'",
974+
],
975+
)
976+
977+
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
978+
f.write("""
979+
import fractions
980+
fractions.shadowing_module
981+
del fractions.__spec__.origin
982+
try:
983+
fractions.Fraction
984+
except AttributeError as e:
985+
print(str(e))
986+
987+
fractions.__spec__.origin = 0
988+
try:
989+
fractions.Fraction
990+
except AttributeError as e:
991+
print(str(e))
992+
""")
993+
994+
popen = script_helper.spawn_python("main.py", cwd=tmp)
995+
stdout, stderr = popen.communicate()
996+
self.assertEqual(
997+
stdout.splitlines(),
998+
[
999+
b"module 'fractions' has no attribute 'Fraction'",
1000+
b"module 'fractions' has no attribute 'Fraction'"
1001+
],
1002+
)
1003+
1004+
def test_script_shadowing_stdlib_sys_path_modification(self):
1005+
with os_helper.temp_dir() as tmp:
1006+
with open(os.path.join(tmp, "fractions.py"), "w", encoding='utf-8') as f:
1007+
f.write("shadowing_module = True")
1008+
1009+
expected_error = (
1010+
rb"AttributeError: module 'fractions' has no attribute 'Fraction' "
1011+
rb"\(consider renaming '.*fractions.py' since it has the "
1012+
rb"same name as the standard library module named 'fractions' "
1013+
rb"and the import system gives it precedence\)"
1014+
)
1015+
1016+
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
1017+
f.write("""
1018+
import sys
1019+
sys.path.insert(0, "this_folder_does_not_exist")
1020+
import fractions
1021+
fractions.Fraction
1022+
""")
1023+
1024+
popen = script_helper.spawn_python("main.py", cwd=tmp)
1025+
stdout, stderr = popen.communicate()
1026+
self.assertRegex(stdout, expected_error)
1027+
8071028

8081029
@skip_if_dont_write_bytecode
8091030
class FilePermissionTests(unittest.TestCase):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve the error message when a script shadowing a module from the standard
2+
library causes :exc:`AttributeError` to be raised. Similarly, improve the error
3+
message when a script shadowing a third party module attempts to access an
4+
attribute from that third party module while still initialising.

0 commit comments

Comments
 (0)