diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py index 83ecbffa2a197e..f95b8f01749d83 100644 --- a/Lib/idlelib/idle_test/test_run.py +++ b/Lib/idlelib/idle_test/test_run.py @@ -45,6 +45,7 @@ def __eq__(self, other): ('int.reel', AttributeError, "type object 'int' has no attribute 'reel'. " "Did you mean: 'real'?\n"), + (r'raise NameError("123\n456")', NameError, "123\n456\n"), ) @force_not_colorized @@ -52,7 +53,10 @@ def test_get_message(self): for code, exc, msg in self.data: with self.subTest(code=code): try: - eval(compile(code, '', 'eval')) + if "raise" not in code: + eval(compile(code, '', 'eval')) + else: + exec(compile(code, '', 'exec')) # code r"raise NameError("123\n456")" cannot run in "eval" mode: SyntaxError except exc: typ, val, tb = sys.exc_info() actual = run.get_message_lines(typ, val, tb)[0] @@ -64,15 +68,26 @@ def test_get_message(self): new_callable=lambda: (lambda t, e: None)) def test_get_multiple_message(self, mock): d = self.data - data2 = ((d[0], d[1]), (d[1], d[2]), (d[2], d[0])) + data2 = ((d[0], d[1]), + (d[1], d[2]), + (d[2], d[3]), + (d[3], d[0]), + (d[1], d[3]), + (d[0], d[2])) subtests = 0 for (code1, exc1, msg1), (code2, exc2, msg2) in data2: with self.subTest(codes=(code1,code2)): try: - eval(compile(code1, '', 'eval')) + if "raise" not in code1: + eval(compile(code1, '', 'eval')) + else: + exec(compile(code1, '', 'exec')) except exc1: try: - eval(compile(code2, '', 'eval')) + if "raise" not in code2: + eval(compile(code2, '', 'eval')) + else: + exec(compile(code2, '', 'exec')) except exc2: with captured_stderr() as output: run.print_exception() diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index a30db99a619a93..adead65a91b574 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -230,12 +230,20 @@ def show_socket_error(err, address): def get_message_lines(typ, exc, tb): "Return line composing the exception message." - if typ in (AttributeError, NameError): + if typ in (AttributeError, NameError, ImportError): # 3.10+ hints are not directly accessible from python (#44026). err = io.StringIO() with contextlib.redirect_stderr(err): sys.__excepthook__(typ, exc, tb) - return [err.getvalue().split("\n")[-2] + "\n"] + err_list = err.getvalue().split("\n")[1:] + + for i in range(len(err_list)): + if err_list[i].startswith(" "): + continue + else: + err_list = err_list[i:-1] + break + return ["\n".join(err_list) + "\n"] # The unmerged gh-135511 else: return traceback.format_exception_only(typ, exc) diff --git a/Lib/test/test_unittest/test_loader.py b/Lib/test/test_unittest/test_loader.py index cdff6d1a20c8df..972881b40e6967 100644 --- a/Lib/test/test_unittest/test_loader.py +++ b/Lib/test/test_unittest/test_loader.py @@ -335,7 +335,7 @@ def test_loadTestsFromName__unknown_attr_name_on_package(self): loader = unittest.TestLoader() suite = loader.loadTestsFromName('unittest.sdasfasfasdf') - expected = "No module named 'unittest.sdasfasfasdf'" + expected = "module 'unittest' has no child module 'sdasfasfasdf'" error, test = self.check_deferred_error(loader, suite) self.assertIn( expected, error, diff --git a/Lib/traceback.py b/Lib/traceback.py index 318ec13cf91121..1a3cb46992c553 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1107,11 +1107,19 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: self._str += f". Did you mean: '{suggestion}'?" - elif exc_type and issubclass(exc_type, ModuleNotFoundError) and \ - sys.flags.no_site and \ - getattr(exc_value, "name", None) not in sys.stdlib_module_names: - self._str += (". Site initialization is disabled, did you forget to " - + "add the site-packages directory to sys.path?") + elif exc_type and issubclass(exc_type, ModuleNotFoundError) and getattr(exc_value, "name", None): + wrong_name = getattr(exc_value, "name", None) + suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) + self._str = exc_value.msg + if suggestion: + self._str += f". Did you mean: '{suggestion}'?" + if sys.flags.no_site and getattr(exc_value, "name", None) not in sys.stdlib_module_names: + if not suggestion: + self._str += (". Site initialization is disabled, did you forget to " + + "add the site-packages directory to sys.path?") + else: + self._str += ("Or did you forget to add the site-packages directory to sys.path? The " + + "site initialization is disabled") elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \ getattr(exc_value, "name", None) is not None: wrong_name = getattr(exc_value, "name", None) @@ -1634,7 +1642,111 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): if wrong_name[:1] != '_': d = [x for x in d if x[:1] != '_'] except Exception: - return None + if not isinstance(exc_value, ModuleNotFoundError): + return None + scan_dir, find_all_packages = _find_all_packages() + import os + list_d = find_all_packages() + _module_name = exc_value.name + wrong_name_list = _module_name.split(".") + module_name = wrong_name_list[0] + if module_name not in sys.modules: + wrong_name = module_name + exc_value.msg = f"No module named '{module_name}'" + if len(wrong_name_list) == 1: + if (_closed_name := _calculate_closed_name(module_name, sorted(sys.stdlib_module_names))): + return _closed_name # stdlib first + _close_name_list = [] + for i in list_d: + module_result = _calculate_closed_name(wrong_name, i) + if module_result: + _close_name_list.append(module_result) + _close_name_list.sort() + if _close_name_list: + return _close_name_list[0] + else: + return None + else: + if wrong_name in sum(list_d, []): + path = "" + for i in sys.path: + if i and isinstance(i, str) and not i.endswith("idlelib"): + if wrong_name in scan_dir(i): + path = f"{i}/{wrong_name}" + break + else: + if (_closed_name := _calculate_closed_name(module_name, sorted(sys.stdlib_module_names))): + return _closed_name + _close_name_list = [] + for i in list_d: + module_result = _calculate_closed_name(wrong_name, i) + if module_result: + _close_name_list.append(module_result) + _close_name_list.sort() + if _close_name_list: + return _close_name_list[0] + else: + return None + else: + if (_closed_name := _calculate_closed_name(module_name, sorted(sys.stdlib_module_names))): + return _closed_name + _close_name_list = [] + for i in list_d: + module_result = _calculate_closed_name(wrong_name, i) + if module_result: + _close_name_list.append(module_result) + _close_name_list.sort() + if _close_name_list: + return _close_name_list[0] + else: + return None + + if not os.path.exists(path) or not os.path.isdir(path): + exc_value.msg = f"module '{module_name}' has no child module '{wrong_name_list[1]}'; '{module_name}' is not a package" + return None + index = 0 + for i in wrong_name_list[1:]: + index += 1 + _child_modules_d = scan_dir(path) + original_module_name = module_name + if wrong_name_list[index] not in _child_modules_d: + exc_value.msg = f"module '{module_name}' has no child module '{i}'" + wrong_name = i + d = _child_modules_d + break + path += f"/{i}" + if not os.path.exists(path) or not os.path.isdir(path) and len(wrong_name_list) > index + 1: + module_name += "." + i + exc_value.msg = f"module '{module_name}' has no child module '{wrong_name_list[index + 1]}'; '{module_name}' is not a package" + return None + module_name += "." + i + exc_value.args = (exc_value.msg,) + else: + if hasattr(sys.modules[module_name], '__path__') and len(wrong_name_list)>1: + index = 0 + for i in wrong_name_list[1:]: + index += 1 + original_module_name = module_name + exc_value.msg = f"module '{module_name}' has no child module '{i}'" + exc_value.args = (exc_value.msg,) + module_name += "." + i + if module_name not in sys.modules: + wrong_name = i + d = scan_dir(sys.modules[original_module_name].__path__[0]) + break + else: + if hasattr(sys.modules[module_name], '__path__'): + continue + else: + if len(wrong_name_list) > index + 1: + exc_value.msg = f"module '{module_name}' has no child module '{wrong_name_list[index+1]}'; '{module_name}' is not a package" + exc_value.args = (exc_value.msg,) + return None + else: + if len(wrong_name_list) > 1: + exc_value.msg = f"module '{module_name}' has no child module '{wrong_name_list[1]}'; '{module_name}' is not a package" + exc_value.args = (exc_value.msg,) + return None else: assert isinstance(exc_value, NameError) # find most recent frame @@ -1661,13 +1773,14 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): if has_wrong_name: return f"self.{wrong_name}" + return _calculate_closed_name(wrong_name, d) + +def _calculate_closed_name(wrong_name, d): try: import _suggestions + return _suggestions._generate_suggestions(d, wrong_name) except ImportError: pass - else: - return _suggestions._generate_suggestions(d, wrong_name) - # Compute closest match if len(d) > _MAX_CANDIDATE_ITEMS: @@ -1693,6 +1806,54 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): best_distance = current_distance return suggestion +def _find_all_packages(): + import os + import sys + from importlib import machinery + + def scan_dir(path): + """ + Return all of the packages in the path without import + contains: + - .py file + - directory with "__init__.py" + - the .pyd/so file that has right ABI + """ + if not os.path.isdir(path): + return [] + + suffixes = machinery.EXTENSION_SUFFIXES + result = [] + + for name in os.listdir(path): + full_path = os.path.join(path, name) + + # .py file + if name.endswith(".py") and os.path.isfile(full_path): + modname = name[:-3] + if modname.isidentifier(): + result.append(modname) + + # directory with "__init__.py" + elif os.path.isdir(full_path): + init_file = os.path.join(full_path, "__init__.py") + if os.path.isfile(init_file) and name.isidentifier(): + result.append(name) + + # the .pyd/so file that has right ABI + elif os.path.isfile(full_path): + for suf in suffixes: + if name.endswith(suf): + modname = name[:-len(suf)] + if modname.isidentifier(): + result.append(modname) + break + + return sorted(result) + + def find_all_packages(): + return [scan_dir(i) if i and isinstance(i, str) and not i.endswith("idlelib") else [] for i in sys.path] + [sorted(sys.builtin_module_names)] + return scan_dir, find_all_packages def _levenshtein_distance(a, b, max_cost): # A Python implementation of Python/suggestions.c:levenshtein_distance. diff --git a/Misc/NEWS.d/next/IDLE/2025-08-08-10-50-59.gh-issue-135511.9Rw5Zg.rst b/Misc/NEWS.d/next/IDLE/2025-08-08-10-50-59.gh-issue-135511.9Rw5Zg.rst new file mode 100644 index 00000000000000..f9526b760573ed --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2025-08-08-10-50-59.gh-issue-135511.9Rw5Zg.rst @@ -0,0 +1,2 @@ +Fix the missing error message in "NameError" and "AttributeError" in IDLE +when "\n" in message