]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-149117: Set `ImportError.name` on errors from `runpy.run_module`/`run_path...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 2 May 2026 02:51:06 +0000 (04:51 +0200)
committerGitHub <noreply@github.com>
Sat, 2 May 2026 02:51:06 +0000 (02:51 +0000)
gh-149117: Set `ImportError.name` on errors from `runpy.run_module`/`run_path` (gh-149159)

Set ImportError.name on errors from runpy.run_module/run_path

`runpy.run_module()` and `runpy.run_path()` now set the `name` attribute
of the `ImportError` they raise to the requested module name, matching
the behaviour of a regular import statement (previously `name` was
always `None`, which broke introspection).

The `name=` kwarg is gated on `issubclass(error, ImportError)` because
`_get_module_details()` is also used by `_run_module_as_main()` with
a private `_Error` sentinel class. `_Error` does not subclass
ImportError, and `BaseException.__init__` rejects unknown kwargs at
the C level, so passing `name=` unconditionally would break the
`python -m foo` codepath.
(cherry picked from commit ff35fe4633cc6d3a30f6af8281dfa641783c1d07)

Co-authored-by: W. H. Wang <mattwang44@gmail.com>
Lib/runpy.py
Lib/test/test_runpy.py
Misc/NEWS.d/next/Library/2026-04-29-16-11-27.gh-issue-149117.yEeTYd.rst [new file with mode: 0644]

index ef54d3282eee06d937aa1d9d1043cfb93bf01c31..1d5ecf0cf15bc0e75a6966cee94629f6e8681cd7 100644 (file)
@@ -103,8 +103,10 @@ def _run_module_code(code, init_globals=None,
 
 # Helper to get the full name, spec and code for a module
 def _get_module_details(mod_name, error=ImportError):
+    # name= is only accepted by ImportError and its subclasses.
+    kwargs = {"name": mod_name} if issubclass(error, ImportError) else {}
     if mod_name.startswith("."):
-        raise error("Relative module names not supported")
+        raise error("Relative module names not supported", **kwargs)
     pkg_name, _, _ = mod_name.rpartition(".")
     if pkg_name:
         # Try importing the parent to avoid catching initialization errors
@@ -137,12 +139,13 @@ def _get_module_details(mod_name, error=ImportError):
         if mod_name.endswith(".py"):
             msg += (f". Try using '{mod_name[:-3]}' instead of "
                     f"'{mod_name}' as the module name.")
-        raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex
+        raise error(msg.format(mod_name, type(ex).__name__, ex),
+                    **kwargs) from ex
     if spec is None:
-        raise error("No module named %s" % mod_name)
+        raise error("No module named %s" % mod_name, **kwargs)
     if spec.submodule_search_locations is not None:
         if mod_name == "__main__" or mod_name.endswith(".__main__"):
-            raise error("Cannot use package as __main__ module")
+            raise error("Cannot use package as __main__ module", **kwargs)
         try:
             pkg_main_name = mod_name + ".__main__"
             return _get_module_details(pkg_main_name, error)
@@ -150,17 +153,19 @@ def _get_module_details(mod_name, error=ImportError):
             if mod_name not in sys.modules:
                 raise  # No module loaded; being a package is irrelevant
             raise error(("%s; %r is a package and cannot " +
-                               "be directly executed") %(e, mod_name))
+                               "be directly executed") %(e, mod_name),
+                        **kwargs)
     loader = spec.loader
     if loader is None:
         raise error("%r is a namespace package and cannot be executed"
-                                                                 % mod_name)
+                                                                 % mod_name,
+                    **kwargs)
     try:
         code = loader.get_code(mod_name)
     except ImportError as e:
-        raise error(format(e)) from e
+        raise error(format(e), **kwargs) from e
     if code is None:
-        raise error("No code object available for %s" % mod_name)
+        raise error("No code object available for %s" % mod_name, **kwargs)
     return mod_name, spec, code
 
 class _Error(Exception):
@@ -234,6 +239,7 @@ def _get_main_module_details(error=ImportError):
     # Also moves the standard __main__ out of the way so that the
     # preexisting __loader__ entry doesn't cause issues
     main_name = "__main__"
+    kwargs = {"name": main_name} if issubclass(error, ImportError) else {}
     saved_main = sys.modules[main_name]
     del sys.modules[main_name]
     try:
@@ -241,7 +247,8 @@ def _get_main_module_details(error=ImportError):
     except ImportError as exc:
         if main_name in str(exc):
             raise error("can't find %r module in %r" %
-                              (main_name, sys.path[0])) from exc
+                              (main_name, sys.path[0]),
+                        **kwargs) from exc
         raise
     finally:
         sys.modules[main_name] = saved_main
index ada78ec8e6b0c757d83a9802c2c1cbc8b3c46a89..a4addbeaa2bd0a5cba1b524911308ab60330a055 100644 (file)
@@ -216,6 +216,25 @@ class RunModuleTestCase(unittest.TestCase, CodeExecutionMixin):
         # Package without __main__.py
         self.expect_import_error("multiprocessing")
 
+    def test_invalid_names_set_name_attribute(self):
+        cases = [
+            # (mod_name, expected_name)  -- comment indicates raise site
+            ("nonexistent_runpy_test_module",
+                "nonexistent_runpy_test_module"),    # spec is None
+            ("sys.imp.eric", "sys.imp.eric"),        # find_spec error
+            (".relative_name", ".relative_name"),    # relative name rejected
+            ("sys", "sys"),                          # builtin: no code object
+            ("multiprocessing", "multiprocessing"),  # package without __main__
+        ]
+        for mod_name, expected_name in cases:
+            with self.subTest(mod_name=mod_name):
+                try:
+                    run_module(mod_name)
+                except ImportError as exc:
+                    self.assertEqual(exc.name, expected_name)
+                else:
+                    self.fail("Expected ImportError for %r" % mod_name)
+
     def test_library_module(self):
         self.assertEqual(run_module("runpy")["__name__"], "runpy")
 
@@ -714,6 +733,17 @@ class RunPathTestCase(unittest.TestCase, CodeExecutionMixin):
             msg = "can't find '__main__' module in %r" % script_dir
             self._check_import_error(script_dir, msg)
 
+    def test_directory_error_sets_name_attribute(self):
+        with temp_dir() as script_dir:
+            self._make_test_script(script_dir, 'not_main')
+            try:
+                run_path(script_dir)
+            except ImportError as exc:
+                self.assertEqual(exc.name, '__main__')
+            else:
+                self.fail("Expected ImportError for directory without "
+                          "__main__.py")
+
     def test_zipfile(self):
         with temp_dir() as script_dir:
             mod_name = '__main__'
diff --git a/Misc/NEWS.d/next/Library/2026-04-29-16-11-27.gh-issue-149117.yEeTYd.rst b/Misc/NEWS.d/next/Library/2026-04-29-16-11-27.gh-issue-149117.yEeTYd.rst
new file mode 100644 (file)
index 0000000..41223e9
--- /dev/null
@@ -0,0 +1,3 @@
+Fix :func:`runpy.run_module` and :func:`runpy.run_path` to set the
+:attr:`~ImportError.name` attribute on the :exc:`ImportError` they
+raise.