Skip to content

Commit 092f9dd

Browse files
E-Paineterryjreedy
andauthored
bpo-44026: Idle - display interpreter's 'did you mean' hints (GH-25912)
A C function accessible by the default exception handler, but not by python code, finds the existing name closest to the name causing a name or attribute error. For such errors, call the default handler after capturing stderr and retrieve its message line. Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
1 parent b2ec37a commit 092f9dd

File tree

4 files changed

+63
-5
lines changed

4 files changed

+63
-5
lines changed

Lib/idlelib/NEWS.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Released on 2021-10-04?
44
=========================
55

66

7+
bpo-44026: Include interpreter's typo fix suggestions in message line
8+
for NameErrors and AttributeErrors. Patch by E. Paine.
9+
710
bpo-37903: Add mouse actions to the shell sidebar. Left click and
811
optional drag selects one or more lines of text, as with the
912
editor line number sidebar. Right click after selecting text lines

Lib/idlelib/idle_test/test_run.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"Test run, coverage 49%."
1+
"Test run, coverage 54%."
22

33
from idlelib import run
44
import io
@@ -12,7 +12,7 @@
1212
idlelib.testing = True # Use {} for executing test user code.
1313

1414

15-
class PrintExceptionTest(unittest.TestCase):
15+
class ExceptionTest(unittest.TestCase):
1616

1717
def test_print_exception_unhashable(self):
1818
class UnhashableException(Exception):
@@ -28,8 +28,7 @@ def __eq__(self, other):
2828
raise ex1
2929
except UnhashableException:
3030
with captured_stderr() as output:
31-
with mock.patch.object(run,
32-
'cleanup_traceback') as ct:
31+
with mock.patch.object(run, 'cleanup_traceback') as ct:
3332
ct.side_effect = lambda t, e: t
3433
run.print_exception()
3534

@@ -38,6 +37,46 @@ def __eq__(self, other):
3837
self.assertIn('UnhashableException: ex2', tb[3])
3938
self.assertIn('UnhashableException: ex1', tb[10])
4039

40+
data = (('1/0', ZeroDivisionError, "division by zero\n"),
41+
('abc', NameError, "name 'abc' is not defined. "
42+
"Did you mean: 'abs'?\n"),
43+
('int.reel', AttributeError,
44+
"type object 'int' has no attribute 'reel'. "
45+
"Did you mean: 'real'?\n"),
46+
)
47+
48+
def test_get_message(self):
49+
for code, exc, msg in self.data:
50+
with self.subTest(code=code):
51+
try:
52+
eval(compile(code, '', 'eval'))
53+
except exc:
54+
typ, val, tb = sys.exc_info()
55+
actual = run.get_message_lines(typ, val, tb)[0]
56+
expect = f'{exc.__name__}: {msg}'
57+
self.assertEqual(actual, expect)
58+
59+
@mock.patch.object(run, 'cleanup_traceback',
60+
new_callable=lambda: (lambda t, e: None))
61+
def test_get_multiple_message(self, mock):
62+
d = self.data
63+
data2 = ((d[0], d[1]), (d[1], d[2]), (d[2], d[0]))
64+
subtests = 0
65+
for (code1, exc1, msg1), (code2, exc2, msg2) in data2:
66+
with self.subTest(codes=(code1,code2)):
67+
try:
68+
eval(compile(code1, '', 'eval'))
69+
except exc1:
70+
try:
71+
eval(compile(code2, '', 'eval'))
72+
except exc2:
73+
with captured_stderr() as output:
74+
run.print_exception()
75+
actual = output.getvalue()
76+
self.assertIn(msg1, actual)
77+
self.assertIn(msg2, actual)
78+
subtests += 1
79+
self.assertEqual(subtests, len(data2)) # All subtests ran?
4180

4281
# StdioFile tests.
4382

Lib/idlelib/run.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
55
'.run' is needed because __import__ returns idlelib, not idlelib.run.
66
"""
7+
import contextlib
78
import functools
89
import io
910
import linecache
@@ -211,6 +212,19 @@ def show_socket_error(err, address):
211212
parent=root)
212213
root.destroy()
213214

215+
216+
def get_message_lines(typ, exc, tb):
217+
"Return line composing the exception message."
218+
if typ in (AttributeError, NameError):
219+
# 3.10+ hints are not directly accessible from python (#44026).
220+
err = io.StringIO()
221+
with contextlib.redirect_stderr(err):
222+
sys.__excepthook__(typ, exc, tb)
223+
return [err.getvalue().split("\n")[-2] + "\n"]
224+
else:
225+
return traceback.format_exception_only(typ, exc)
226+
227+
214228
def print_exception():
215229
import linecache
216230
linecache.checkcache()
@@ -241,7 +255,7 @@ def print_exc(typ, exc, tb):
241255
"debugger_r.py", "bdb.py")
242256
cleanup_traceback(tbe, exclude)
243257
traceback.print_list(tbe, file=efile)
244-
lines = traceback.format_exception_only(typ, exc)
258+
lines = get_message_lines(typ, exc, tb)
245259
for line in lines:
246260
print(line, end='', file=efile)
247261

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Include interpreter's typo fix suggestions in message line for
2+
NameErrors and AttributeErrors. Patch by E. Paine.

0 commit comments

Comments
 (0)