Skip to content
23 changes: 19 additions & 4 deletions Lib/idlelib/idle_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@ 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
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]
Expand All @@ -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()
Expand Down
12 changes: 10 additions & 2 deletions Lib/idlelib/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_unittest/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
179 changes: 170 additions & 9 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Comment on lines +1809 to +1812
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want to do this. I didn't see any support for your proposals on Discourse.

Choose a reason for hiding this comment

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

The code is in the issue. And I show the data on the code that there is no problem.


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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix the missing error message in "NameError" and "AttributeError" in IDLE
when "\n" in message
Comment on lines +1 to +2
Copy link
Member

Choose a reason for hiding this comment

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

I do not understand how the issue and this entry is related?

Choose a reason for hiding this comment

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

It is from the gh-135511. The branch is from there

Copy link
Member

Choose a reason for hiding this comment

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

Why are you using the same branch for a completely different PR?

Loading