diff --git a/Lib/idlelib/__main__.py b/Lib/idlelib/__main__.py index ec3915b265f665e..4dde0d6a5acbb51 100644 --- a/Lib/idlelib/__main__.py +++ b/Lib/idlelib/__main__.py @@ -3,5 +3,12 @@ Run IDLE as python -m idlelib """ +import sys + +if not sys.flags.safe_path: + # Remove the current directory, prepended by "python -m", so that + # user files do not shadow IDLE's imports (gh-70331). + del sys.path[0] + import idlelib.pyshell idlelib.pyshell.main() diff --git a/Lib/idlelib/idle.py b/Lib/idlelib/idle.py index 485d5a75a29c1ad..d9faa12cc2ba45e 100644 --- a/Lib/idlelib/idle.py +++ b/Lib/idlelib/idle.py @@ -1,6 +1,12 @@ -import os.path import sys +if __spec__ is not None and not sys.flags.safe_path: + # Remove the current directory, prepended by "python -m", so that + # user files do not shadow IDLE's imports (gh-70331). + del sys.path[0] + +import os.path + # Enable running IDLE with idlelib in a non-standard location. # This was once used to run development versions of IDLE. diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index dec81bdbbccd67e..36dc2590aef9181 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -3,8 +3,11 @@ from idlelib import pyshell import os +import sys import unittest -from test.support import requires +from unittest import mock +from test.support import os_helper, requires +from test.support.script_helper import assert_python_ok from tkinter import Tk @@ -37,6 +40,24 @@ def test_fix_user_path(self): eq(pyshell.fix_user_path(['/a', '/b']), ['/a', '/b']) eq(pyshell.fix_user_path([idlelib_dir]), []) + def test_shadowed_stdlib(self): + # gh-70331: user files in the current directory must not shadow + # the stdlib modules imported by IDLE. + with os_helper.temp_dir() as cwd: + for name in ('os', 'random', 'tkinter'): + os_helper.create_empty_file(os.path.join(cwd, f'{name}.py')) + for module in 'idlelib', 'idlelib.idle', 'idlelib.pyshell': + with self.subTest(module=module): + assert_python_ok('-m', module, '-h', + __isolated=False, __cwd=cwd) + + def test_build_subprocess_arglist(self): + interp = mock.Mock(port=1234) + args = pyshell.ModifiedInterpreter.build_subprocess_arglist(interp) + # gh-70331: -P keeps the current directory out of sys.path. + self.assertEqual(args[:2], [sys.executable, '-P']) + self.assertEqual(args[-1], '1234') + class PyShellFileListTest(unittest.TestCase): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 51568d2a9e950e3..93fff7424fc4ef7 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -3,6 +3,10 @@ import sys if __name__ == "__main__": sys.modules['idlelib.pyshell'] = sys.modules['__main__'] + if __spec__ is not None and not sys.flags.safe_path: + # Remove the current directory, prepended by "python -m", so that + # user files do not shadow IDLE's imports (gh-70331). + del sys.path[0] try: from tkinter import * @@ -451,7 +455,10 @@ def build_subprocess_arglist(self): del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc', default=False, type='bool') command = f"__import__('idlelib.run').run.main({del_exitf!r})" - return [sys.executable] + w + ["-c", command, str(self.port)] + # -P keeps the current directory off sys.path, so that user files + # do not shadow run's imports (gh-70331). transfer_path() sets + # sys.path later. + return [sys.executable, '-P'] + w + ["-c", command, str(self.port)] def start_subprocess(self): addr = (HOST, self.port) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-16-22-00-00.gh-issue-70331.shadow.rst b/Misc/NEWS.d/next/IDLE/2026-09-16-22-00-00.gh-issue-70331.shadow.rst new file mode 100644 index 000000000000000..1a7124e76cd49f8 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-16-22-00-00.gh-issue-70331.shadow.rst @@ -0,0 +1,4 @@ +IDLE no longer fails to start with ``python -m idlelib``, and its user process +no longer fails to start, when the current directory contains user files with +the same names as standard library modules that IDLE imports, such as +``random.py`` or ``tkinter.py``.