Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8e86579

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

File tree

Expand file treeCollapse file tree

8 files changed

+456
-53
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+456
-53
lines changed

‎Doc/whatsnew/3.13.rst

Copy file name to clipboardExpand all lines: Doc/whatsnew/3.13.rst
+34Lines changed: 34 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Include/internal/pycore_global_objects_fini_generated.h
+1Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_global_strings.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_global_strings.h
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: Include/internal/pycore_runtime_init_generated.h
+1Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_unicodeobject_generated.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_unicodeobject_generated.h
+3Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Lib/test/test_import/__init__.py

Copy file name to clipboardExpand all lines: Lib/test/test_import/__init__.py
+221Lines changed: 221 additions & 0 deletions
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):
+4Lines changed: 4 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.