Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion Lib/runpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ def _run_module_code(code, init_globals=None,
# may be cleared when the temporary module goes away
return mod_globals.copy()


def _get_possible_name_list(wrong_name):
try:
if parent_name := wrong_name.rpartition('.')[0]:
parent = importlib.util.find_spec(parent_name)
else:
parent = None
d = []
for finder in sys.meta_path:
if discover := getattr(finder, 'discover', None):
d += [spec.name for spec in discover(parent)]
return d
except Exception:
return None


# Helper to get the full name, spec and code for a module
def _get_module_details(mod_name, error=ImportError):
if mod_name.startswith("."):
Expand Down Expand Up @@ -138,7 +154,12 @@ def _get_module_details(mod_name, error=ImportError):
f"'{mod_name}' as the module name.")
raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex
if spec is None:
raise error("No module named %s" % mod_name)
message = "No module named %r" % mod_name
if (d := _get_possible_name_list(mod_name)):
from traceback import _calculate_closed_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we don't need to use local imports to defer imports, we can just use lazy imports.

if (suggestion := _calculate_closed_name(mod_name, d)):
message += ". Did you mean: %r?" % suggestion
raise error(message)
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")
Expand Down
7 changes: 6 additions & 1 deletion Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ def test_consistent_sys_path_for_module_execution(self):
"-Im", "script_pkg", cwd=work_dir
)
traceback_lines = stderr.decode().splitlines()
self.assertIn("No module named script_pkg", traceback_lines[-1])
self.assertIn("No module named 'script_pkg'", traceback_lines[-1])

def test_nonexisting_script(self):
# bpo-34783: "./python script.py" must not crash
Expand Down Expand Up @@ -829,6 +829,11 @@ def test_zipfile_run_filter_syntax_warnings_by_module(self):
)
self.assertEqual(err.count(b': SyntaxWarning: '), 12)

def test_typo_in_module_name_suggests_similar_module(self):
# python -m randon -> expect suggestion for 'random'
rc, out, err = assert_python_failure("-m", "randon")
self.assertIn(b"No module named 'randon'", err)
self.assertIn(b"Did you mean: 'random'?", err)

def tearDownModule():
support.reap_children()
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4210,7 +4210,7 @@ def test_module_without_a_main(self):
stdout, stderr = self._run_pdb(
['-m', module_name], "", expected_returncode=1
)
self.assertIn("ImportError: No module named t_main.__main__;", stdout)
self.assertIn("ImportError: No module named 't_main.__main__'", stdout)

def test_package_without_a_main(self):
pkg_name = 't_pkg'
Expand All @@ -4231,7 +4231,7 @@ def test_package_without_a_main(self):
def test_nonexistent_module(self):
assert not os.path.exists(os_helper.TESTFN)
stdout, stderr = self._run_pdb(["-m", os_helper.TESTFN], "", expected_returncode=1)
self.assertIn(f"ImportError: No module named {os_helper.TESTFN}", stdout)
self.assertIn(f"ImportError: No module named {os_helper.TESTFN!r}", stdout)

def test_dir_as_script(self):
with os_helper.temp_dir() as temp_dir:
Expand Down
22 changes: 14 additions & 8 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,20 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):

if not_normalized and wrong_name in d:
return wrong_name

suggestion = _calculate_closed_name(wrong_name, d)

# If no direct attribute match found, check for nested attributes
if not suggestion and isinstance(exc_value, AttributeError):
with suppress(Exception):
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
if nested_suggestion:
return nested_suggestion

return suggestion


def _calculate_closed_name(wrong_name, d):
try:
import _suggestions
except ImportError:
Expand Down Expand Up @@ -1810,14 +1824,6 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance

# If no direct attribute match found, check for nested attributes
if not suggestion and isinstance(exc_value, AttributeError):
with suppress(Exception):
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
if nested_suggestion:
return nested_suggestion

return suggestion


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Suggest a possible module name in ``python -m module`` if the module name is wrong.
Loading