From 8dff6abb814a5238e85129d254bef2610dd58241 Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Sun, 22 Feb 2026 12:38:23 +0800 Subject: [PATCH 1/7] let runpy suggest the possible module --- Lib/runpy.py | 21 ++++++++++++++++++++- Lib/traceback.py | 21 +++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/Lib/runpy.py b/Lib/runpy.py index 9f62d20e9a2322..e6e06c6360d82d 100644 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -100,6 +100,20 @@ 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("."): @@ -138,7 +152,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 + 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") diff --git a/Lib/traceback.py b/Lib/traceback.py index b16cd8646e43f1..d3c450de3a5fa4 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1778,6 +1778,19 @@ 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: @@ -1810,14 +1823,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 From bf9c13bffec515f0bf732af338b2bf2950c7636b Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Sun, 22 Feb 2026 12:39:46 +0800 Subject: [PATCH 2/7] add news --- .../next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst new file mode 100644 index 00000000000000..2d3fcf1a226aaa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst @@ -0,0 +1 @@ +suggest possible module in `python -m module` if the module name is wrong From 7bf07b84a674a1a126ab28cb30c29f9c818952f6 Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Sun, 22 Feb 2026 12:44:23 +0800 Subject: [PATCH 3/7] Fix formatting for module suggestion in documentation --- .../next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst index 2d3fcf1a226aaa..c533077cad4a15 100644 --- a/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst +++ b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst @@ -1 +1 @@ -suggest possible module in `python -m module` if the module name is wrong +suggest possible module in ``python -m module`` if the module name is wrong From e6ba9b1a1af1dde29ceebe7ecdff623e4cf016b8 Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Sun, 22 Feb 2026 13:18:11 +0800 Subject: [PATCH 4/7] add some test --- Lib/runpy.py | 2 +- Lib/test/test_cmd_line_script.py | 7 ++++++- Lib/test/test_pdb.py | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Lib/runpy.py b/Lib/runpy.py index e6e06c6360d82d..756798514cbaef 100644 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -156,7 +156,7 @@ def _get_module_details(mod_name, error=ImportError): if (d := _get_possible_name_list(mod_name)): from traceback import _calculate_closed_name if (suggestion := _calculate_closed_name(mod_name, d)): - message += ". Did you mean: %r" % suggestion + 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__"): diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 73b1f671c58555..6895a38ffbf44e 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -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 @@ -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() diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0e23cd6604379c..4fb97008954597 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -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' @@ -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: From 2b77183f1144357ddac8f687491015368f49a9ae Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Mon, 23 Feb 2026 01:11:08 +0800 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: AN Long --- Lib/traceback.py | 1 + .../next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index d3c450de3a5fa4..4314c2baac529e 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1790,6 +1790,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): return suggestion + def _calculate_closed_name(wrong_name, d): try: import _suggestions diff --git a/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst index c533077cad4a15..abf75287c70990 100644 --- a/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst +++ b/Misc/NEWS.d/next/Library/2026-02-22-12-39-31.gh-issue-145031.-4yezd.rst @@ -1 +1 @@ -suggest possible module in ``python -m module`` if the module name is wrong +Suggest a possible module name in ``python -m module`` if the module name is wrong. From 222d00ee7b7e241c247f3a619954007b77885a13 Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Mon, 23 Feb 2026 01:14:36 +0800 Subject: [PATCH 6/7] remove a space --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 4314c2baac529e..d15e207f2c8ebf 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1790,7 +1790,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): return suggestion - + def _calculate_closed_name(wrong_name, d): try: import _suggestions From 3a5d757459be744a28179679b27dcb8e03483419 Mon Sep 17 00:00:00 2001 From: Locked-chess-official <13140752715@163.com> Date: Mon, 23 Feb 2026 01:15:53 +0800 Subject: [PATCH 7/7] Follow PEP8 --- Lib/runpy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/runpy.py b/Lib/runpy.py index 756798514cbaef..e7aa819b0d4281 100644 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -100,6 +100,7 @@ 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]: @@ -114,6 +115,7 @@ def _get_possible_name_list(wrong_name): 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("."):