Skip to content

Commit bb2c05b

Browse files
[3.13] gh-149117: Set ImportError.name on errors from runpy.run_module/run_path (gh-149159) (#149258)
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 ff35fe4) Co-authored-by: W. H. Wang <mattwang44@gmail.com>
1 parent bed659f commit bb2c05b

3 files changed

Lines changed: 49 additions & 9 deletions

File tree

Lib/runpy.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ def _run_module_code(code, init_globals=None,
103103

104104
# Helper to get the full name, spec and code for a module
105105
def _get_module_details(mod_name, error=ImportError):
106+
# name= is only accepted by ImportError and its subclasses.
107+
kwargs = {"name": mod_name} if issubclass(error, ImportError) else {}
106108
if mod_name.startswith("."):
107-
raise error("Relative module names not supported")
109+
raise error("Relative module names not supported", **kwargs)
108110
pkg_name, _, _ = mod_name.rpartition(".")
109111
if pkg_name:
110112
# Try importing the parent to avoid catching initialization errors
@@ -137,30 +139,33 @@ def _get_module_details(mod_name, error=ImportError):
137139
if mod_name.endswith(".py"):
138140
msg += (f". Try using '{mod_name[:-3]}' instead of "
139141
f"'{mod_name}' as the module name.")
140-
raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex
142+
raise error(msg.format(mod_name, type(ex).__name__, ex),
143+
**kwargs) from ex
141144
if spec is None:
142-
raise error("No module named %s" % mod_name)
145+
raise error("No module named %s" % mod_name, **kwargs)
143146
if spec.submodule_search_locations is not None:
144147
if mod_name == "__main__" or mod_name.endswith(".__main__"):
145-
raise error("Cannot use package as __main__ module")
148+
raise error("Cannot use package as __main__ module", **kwargs)
146149
try:
147150
pkg_main_name = mod_name + ".__main__"
148151
return _get_module_details(pkg_main_name, error)
149152
except error as e:
150153
if mod_name not in sys.modules:
151154
raise # No module loaded; being a package is irrelevant
152155
raise error(("%s; %r is a package and cannot " +
153-
"be directly executed") %(e, mod_name))
156+
"be directly executed") %(e, mod_name),
157+
**kwargs)
154158
loader = spec.loader
155159
if loader is None:
156160
raise error("%r is a namespace package and cannot be executed"
157-
% mod_name)
161+
% mod_name,
162+
**kwargs)
158163
try:
159164
code = loader.get_code(mod_name)
160165
except ImportError as e:
161-
raise error(format(e)) from e
166+
raise error(format(e), **kwargs) from e
162167
if code is None:
163-
raise error("No code object available for %s" % mod_name)
168+
raise error("No code object available for %s" % mod_name, **kwargs)
164169
return mod_name, spec, code
165170

166171
class _Error(Exception):
@@ -234,14 +239,16 @@ def _get_main_module_details(error=ImportError):
234239
# Also moves the standard __main__ out of the way so that the
235240
# preexisting __loader__ entry doesn't cause issues
236241
main_name = "__main__"
242+
kwargs = {"name": main_name} if issubclass(error, ImportError) else {}
237243
saved_main = sys.modules[main_name]
238244
del sys.modules[main_name]
239245
try:
240246
return _get_module_details(main_name)
241247
except ImportError as exc:
242248
if main_name in str(exc):
243249
raise error("can't find %r module in %r" %
244-
(main_name, sys.path[0])) from exc
250+
(main_name, sys.path[0]),
251+
**kwargs) from exc
245252
raise
246253
finally:
247254
sys.modules[main_name] = saved_main

Lib/test/test_runpy.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ def test_invalid_names(self):
216216
# Package without __main__.py
217217
self.expect_import_error("multiprocessing")
218218

219+
def test_invalid_names_set_name_attribute(self):
220+
cases = [
221+
# (mod_name, expected_name) -- comment indicates raise site
222+
("nonexistent_runpy_test_module",
223+
"nonexistent_runpy_test_module"), # spec is None
224+
("sys.imp.eric", "sys.imp.eric"), # find_spec error
225+
(".relative_name", ".relative_name"), # relative name rejected
226+
("sys", "sys"), # builtin: no code object
227+
("multiprocessing", "multiprocessing"), # package without __main__
228+
]
229+
for mod_name, expected_name in cases:
230+
with self.subTest(mod_name=mod_name):
231+
try:
232+
run_module(mod_name)
233+
except ImportError as exc:
234+
self.assertEqual(exc.name, expected_name)
235+
else:
236+
self.fail("Expected ImportError for %r" % mod_name)
237+
219238
def test_library_module(self):
220239
self.assertEqual(run_module("runpy")["__name__"], "runpy")
221240

@@ -714,6 +733,17 @@ def test_directory_error(self):
714733
msg = "can't find '__main__' module in %r" % script_dir
715734
self._check_import_error(script_dir, msg)
716735

736+
def test_directory_error_sets_name_attribute(self):
737+
with temp_dir() as script_dir:
738+
self._make_test_script(script_dir, 'not_main')
739+
try:
740+
run_path(script_dir)
741+
except ImportError as exc:
742+
self.assertEqual(exc.name, '__main__')
743+
else:
744+
self.fail("Expected ImportError for directory without "
745+
"__main__.py")
746+
717747
def test_zipfile(self):
718748
with temp_dir() as script_dir:
719749
mod_name = '__main__'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`runpy.run_module` and :func:`runpy.run_path` to set the
2+
:attr:`~ImportError.name` attribute on the :exc:`ImportError` they
3+
raise.

0 commit comments

Comments
 (0)