[Python-checkins] bpo-44026: Idle - display interpreter's 'did you mean' hints (GH-25912)

terryjreedy webhook-mailer at python.org
Fri May 7 19:52:12 EDT 2021


https://github.com/python/cpython/commit/092f9ddb5e85665552c8207972cd393d492f764e
commit: 092f9ddb5e85665552c8207972cd393d492f764e
branch: main
author: E-Paine <63801254+E-Paine at users.noreply.github.com>
committer: terryjreedy <tjreedy at udel.edu>
date: 2021-05-07T19:52:01-04:00
summary:

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 at udel.edu>

files:
A Misc/NEWS.d/next/IDLE/2021-05-05-09-45-24.bpo-44026.m2Z0zR.rst
M Lib/idlelib/NEWS.txt
M Lib/idlelib/idle_test/test_run.py
M Lib/idlelib/run.py

diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt
index ed1142653d9534..396820e9117b53 100644
--- a/Lib/idlelib/NEWS.txt
+++ b/Lib/idlelib/NEWS.txt
@@ -4,6 +4,9 @@ Released on 2021-10-04?
 =========================
 
 
+bpo-44026: Include interpreter's typo fix suggestions in message line
+for NameErrors and AttributeErrors.  Patch by E. Paine.
+
 bpo-37903: Add mouse actions to the shell sidebar.  Left click and
 optional drag selects one or more lines of text, as with the
 editor line number sidebar.  Right click after selecting text lines
diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py
index a31671ee0485fa..ec4637c5ca617a 100644
--- a/Lib/idlelib/idle_test/test_run.py
+++ b/Lib/idlelib/idle_test/test_run.py
@@ -1,4 +1,4 @@
-"Test run, coverage 49%."
+"Test run, coverage 54%."
 
 from idlelib import run
 import io
@@ -12,7 +12,7 @@
 idlelib.testing = True  # Use {} for executing test user code.
 
 
-class PrintExceptionTest(unittest.TestCase):
+class ExceptionTest(unittest.TestCase):
 
     def test_print_exception_unhashable(self):
         class UnhashableException(Exception):
@@ -28,8 +28,7 @@ def __eq__(self, other):
                 raise ex1
             except UnhashableException:
                 with captured_stderr() as output:
-                    with mock.patch.object(run,
-                                           'cleanup_traceback') as ct:
+                    with mock.patch.object(run, 'cleanup_traceback') as ct:
                         ct.side_effect = lambda t, e: t
                         run.print_exception()
 
@@ -38,6 +37,46 @@ def __eq__(self, other):
         self.assertIn('UnhashableException: ex2', tb[3])
         self.assertIn('UnhashableException: ex1', tb[10])
 
+    data = (('1/0', ZeroDivisionError, "division by zero\n"),
+            ('abc', NameError, "name 'abc' is not defined. "
+                               "Did you mean: 'abs'?\n"),
+            ('int.reel', AttributeError,
+                 "type object 'int' has no attribute 'reel'. "
+                 "Did you mean: 'real'?\n"),
+            )
+
+    def test_get_message(self):
+        for code, exc, msg in self.data:
+            with self.subTest(code=code):
+                try:
+                    eval(compile(code, '', 'eval'))
+                except exc:
+                    typ, val, tb = sys.exc_info()
+                    actual = run.get_message_lines(typ, val, tb)[0]
+                    expect = f'{exc.__name__}: {msg}'
+                    self.assertEqual(actual, expect)
+
+    @mock.patch.object(run, 'cleanup_traceback',
+                       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]))
+        subtests = 0
+        for (code1, exc1, msg1), (code2, exc2, msg2) in data2:
+            with self.subTest(codes=(code1,code2)):
+                try:
+                    eval(compile(code1, '', 'eval'))
+                except exc1:
+                    try:
+                        eval(compile(code2, '', 'eval'))
+                    except exc2:
+                        with captured_stderr() as output:
+                            run.print_exception()
+                        actual = output.getvalue()
+                        self.assertIn(msg1, actual)
+                        self.assertIn(msg2, actual)
+                        subtests += 1
+        self.assertEqual(subtests, len(data2))  # All subtests ran?
 
 # StdioFile tests.
 
diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py
index 07e9a2bf9ceeae..3836727691229e 100644
--- a/Lib/idlelib/run.py
+++ b/Lib/idlelib/run.py
@@ -4,6 +4,7 @@
 f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
 '.run' is needed because __import__ returns idlelib, not idlelib.run.
 """
+import contextlib
 import functools
 import io
 import linecache
@@ -211,6 +212,19 @@ def show_socket_error(err, address):
             parent=root)
     root.destroy()
 
+
+def get_message_lines(typ, exc, tb):
+    "Return line composing the exception message."
+    if typ in (AttributeError, NameError):
+        # 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"]
+    else:
+        return traceback.format_exception_only(typ, exc)
+
+
 def print_exception():
     import linecache
     linecache.checkcache()
@@ -241,7 +255,7 @@ def print_exc(typ, exc, tb):
                        "debugger_r.py", "bdb.py")
             cleanup_traceback(tbe, exclude)
             traceback.print_list(tbe, file=efile)
-        lines = traceback.format_exception_only(typ, exc)
+        lines = get_message_lines(typ, exc, tb)
         for line in lines:
             print(line, end='', file=efile)
 
diff --git a/Misc/NEWS.d/next/IDLE/2021-05-05-09-45-24.bpo-44026.m2Z0zR.rst b/Misc/NEWS.d/next/IDLE/2021-05-05-09-45-24.bpo-44026.m2Z0zR.rst
new file mode 100644
index 00000000000000..bc4b680983a59a
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2021-05-05-09-45-24.bpo-44026.m2Z0zR.rst
@@ -0,0 +1,2 @@
+Include interpreter's typo fix suggestions in message line for
+NameErrors and AttributeErrors.  Patch by E. Paine.



More information about the Python-checkins mailing list