https://github.com/python/cpython/commit/ff35fe4633cc6d3a30f6af8281dfa641783c1d07
commit: ff35fe4633cc6d3a30f6af8281dfa641783c1d07
branch: main
author: W. H. Wang <[email protected]>
committer: ncoghlan <[email protected]>
date: 2026-05-02T12:27:23+10:00
summary:
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.
files:
A Misc/NEWS.d/next/Library/2026-04-29-16-11-27.gh-issue-149117.yEeTYd.rst
M Lib/runpy.py
M Lib/test/test_runpy.py
diff --git a/Lib/runpy.py b/Lib/runpy.py
index 9f62d20e9a2322..a535b4f651a5ae 100644
--- a/Lib/runpy.py
+++ b/Lib/runpy.py
@@ -102,8 +102,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
@@ -136,12 +138,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)
@@ -149,17 +152,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):
@@ -232,6 +237,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:
@@ -239,7 +245,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
diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py
index 9f3bc8973eb8ac..55b9673ef6c91c 100644
--- a/Lib/test/test_runpy.py
+++ b/Lib/test/test_runpy.py
@@ -217,6 +217,25 @@ def test_invalid_names(self):
# 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 @@ def test_directory_error(self):
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
index 00000000000000..41223e90ed0b0e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-29-16-11-27.gh-issue-149117.yEeTYd.rst
@@ -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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]