From 8ab0abbb83e6a26b5839e1cdc69d0b49840cd878 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 5 Aug 2026 19:47:20 +0300 Subject: [PATCH] gh-75221: Search __main__ only in the executed directory or zipfile runpy.run_path() and the execution of a directory or a zipfile searched the __main__ module in the whole sys.path, so an existing __main__ module located elsewhere could be executed instead of failing. As a result, running Lib/test/test_runpy.py directly ran the whole test suite. Co-Authored-By: Claude Opus 5 (1M context) --- Doc/library/runpy.rst | 10 ++++--- Lib/runpy.py | 28 +++++++++++-------- Lib/test/test_runpy.py | 13 ++++----- ...6-08-05-20-30-00.gh-issue-75221.runpyM.rst | 4 +++ 4 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-05-20-30-00.gh-issue-75221.runpyM.rst diff --git a/Doc/library/runpy.rst b/Doc/library/runpy.rst index d764a98c8419ed3..cb9a2614cb2e15f 100644 --- a/Doc/library/runpy.rst +++ b/Doc/library/runpy.rst @@ -114,10 +114,9 @@ The :mod:`!runpy` module provides two functions: For a simple script, the specified code is simply executed in a fresh module namespace. For a valid :data:`sys.path` entry (typically a zipfile or directory), the entry is first added to the beginning of ``sys.path``. The - function then looks for and executes a :mod:`__main__` module using the - updated path. Note that there is no special protection against invoking - an existing ``__main__`` entry located elsewhere on ``sys.path`` if - there is no such module at the specified location. + function then looks for and executes a :mod:`__main__` module in that + entry. :exc:`ImportError` is raised if there is no such module at the + specified location. The optional dictionary argument *init_globals* may be used to pre-populate the module's globals dictionary before the code is executed. @@ -177,6 +176,9 @@ The :mod:`!runpy` module provides two functions: .. versionchanged:: 3.15 ``__cached__`` is no longer set. + .. versionchanged:: next + The ``__main__`` module is now searched only in *path_name*. + .. seealso:: :pep:`338` -- Executing modules as scripts diff --git a/Lib/runpy.py b/Lib/runpy.py index a535b4f651a5ae8..1c416470c1ef7b0 100644 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -234,22 +234,26 @@ def run_module(mod_name, init_globals=None, def _get_main_module_details(error=ImportError): # Helper that gives a nicer error message when attempting to # execute a zipfile or directory by invoking __main__.py - # Also moves the standard __main__ out of the way so that the - # preexisting __loader__ entry doesn't cause issues + # The module is searched only in sys.path[0] -- the executed directory + # or zipfile -- not in the whole sys.path. main_name = "__main__" kwargs = {"name": main_name} if issubclass(error, ImportError) else {} - saved_main = sys.modules[main_name] - del sys.modules[main_name] + from pkgutil import get_importer + path_name = sys.path[0] + importer = get_importer(path_name) + spec = importer.find_spec(main_name) if importer is not None else None + if spec is None or spec.loader is None: + raise error("can't find %r module in %r" % (main_name, path_name), + **kwargs) + if spec.submodule_search_locations is not None: + raise error("Cannot use package as __main__ module", **kwargs) try: - return _get_module_details(main_name) + code = spec.loader.get_code(main_name) except ImportError as exc: - if main_name in str(exc): - raise error("can't find %r module in %r" % - (main_name, sys.path[0]), - **kwargs) from exc - raise - finally: - sys.modules[main_name] = saved_main + raise error(format(exc), **kwargs) from exc + if code is None: + raise error("No code object available for %s" % main_name, **kwargs) + return main_name, spec, code def _get_code_from_file(fname, module): diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index 55b9673ef6c91c0..163651ad153057e 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -14,9 +14,6 @@ import warnings from test.support import ( force_not_colorized_test_class, - infinite_recursion, - no_tracing, - requires_resource, requires_subprocess, verbose, ) @@ -771,17 +768,17 @@ def test_zipfile_error(self): msg = "can't find '__main__' module in %r" % zip_name self._check_import_error(zip_name, msg) - @no_tracing - @requires_resource('cpu') - def test_main_recursion_error(self): + def test_main_no_recursion(self): + # gh-75221: __main__ is searched only in the executed directory or + # zipfile, so running a directory without __main__ from a zipfile's + # __main__ fails instead of recursing. with temp_dir() as script_dir, temp_dir() as dummy_dir: mod_name = '__main__' source = ("import runpy\n" "runpy.run_path(%r)\n") % dummy_dir script_name = self._make_test_script(script_dir, mod_name, source) zip_name, fname = make_zip_script(script_dir, 'test_zip', script_name) - with infinite_recursion(25): - self.assertRaises(RecursionError, run_path, zip_name) + self.assertRaises(ImportError, run_path, zip_name) def test_encoding(self): with temp_dir() as script_dir: diff --git a/Misc/NEWS.d/next/Library/2026-08-05-20-30-00.gh-issue-75221.runpyM.rst b/Misc/NEWS.d/next/Library/2026-08-05-20-30-00.gh-issue-75221.runpyM.rst new file mode 100644 index 000000000000000..77326f3f98bf94a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-05-20-30-00.gh-issue-75221.runpyM.rst @@ -0,0 +1,4 @@ +:func:`runpy.run_path` and the execution of a directory or a zipfile now +search the ``__main__`` module only in that directory or zipfile. Previously +an existing ``__main__`` module located elsewhere on :data:`sys.path` could be +executed instead.