From b3055abae2d56f37ac82edce709eea6c581f408a Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:51:28 -0300 Subject: [PATCH 1/4] Avoid exiting the new REPL when there are non-string candidates for suggestions. --- Lib/_pyrepl/console.py | 13 ++++++++----- Lib/test/test_pyrepl/test_pyrepl.py | 9 +++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index db911b3e1f0b91..2583bc86854d85 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -169,11 +169,14 @@ def showsyntaxerror(self, filename=None, **kwargs): def _excepthook(self, typ, value, tb): import traceback - lines = traceback.format_exception( - typ, value, tb, - colorize=self.can_colorize, - limit=traceback.BUILTIN_EXCEPTION_LIMIT) - self.write(''.join(lines)) + try: + lines = traceback.format_exception( + typ, value, tb, + colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) + self.write(''.join(lines)) + except TypeError as e: + self.write(repr(e) + '\n') def runcode(self, code): try: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 3540d2a5a41662..3fdc21ac4d6db1 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1318,6 +1318,15 @@ def test_null_byte(self): self.assertEqual(exit_code, 0) self.assertNotIn("TypeError", output) + def test_non_string_suggestion_candidates(self): + commands = ("import runpy\n" + "runpy._run_module_code('blech', {0: '', 'bluch': ''}, '')\n" + "exit()\n") + + output, exit_code = self.run_repl(commands) + self.assertEqual(exit_code, 0) + self.assertIn("all elements in 'candidates' must be strings", output) + def test_readline_history_file(self): # skip, if readline module is not available readline = import_module('readline') From f4773e9e4a84d2efaf85ae1f41d84d17c1344a8a Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 03:13:44 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst diff --git a/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst b/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst new file mode 100644 index 00000000000000..b31f54a230f89d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst @@ -0,0 +1 @@ +Avoid exiting the new REPL when there are non-string candidates for suggestions when errors occur. From 3b9afd9ce08878575126b9f2e0636c723cff1e83 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 1 Jun 2025 19:59:11 -0300 Subject: [PATCH 3/4] Make suggestions available even in the presence of non-string candidates. --- Lib/_pyrepl/console.py | 13 +++++-------- Lib/traceback.py | 3 +++ .../2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 63a46d6e58c5f5..8956fb1242e52a 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -170,14 +170,11 @@ def showsyntaxerror(self, filename=None, **kwargs): def _excepthook(self, typ, value, tb): import traceback - try: - lines = traceback.format_exception( - typ, value, tb, - colorize=self.can_colorize, - limit=traceback.BUILTIN_EXCEPTION_LIMIT) - self.write(''.join(lines)) - except TypeError as e: - self.write(repr(e) + '\n') + lines = traceback.format_exception( + typ, value, tb, + colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) + self.write(''.join(lines)) def runcode(self, code): try: diff --git a/Lib/traceback.py b/Lib/traceback.py index 17b082eced6f05..ab2ff023c308e6 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1603,6 +1603,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): frame = tb.tb_frame if 'self' in frame.f_locals and frame.f_locals['self'] is obj: hide_underscored = False + d = [x for x in d if isinstance(x, str)] if hide_underscored: d = [x for x in d if x[:1] != '_'] except Exception: @@ -1611,6 +1612,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): try: mod = __import__(exc_value.name) d = dir(mod) + d = [x for x in d if isinstance(x, str)] if wrong_name[:1] != '_': d = [x for x in d if x[:1] != '_'] except Exception: @@ -1628,6 +1630,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): + list(frame.f_globals) + list(frame.f_builtins) ) + d = [x for x in d if isinstance(x, str)] # Check first if we are in a method and the instance # has the wrong name as attribute diff --git a/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst b/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst index b31f54a230f89d..157522f9aab1b6 100644 --- a/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst +++ b/Misc/NEWS.d/next/Library/2025-03-09-03-13-41.gh-issue-130999.tBRBVB.rst @@ -1 +1,2 @@ -Avoid exiting the new REPL when there are non-string candidates for suggestions when errors occur. +Avoid exiting the new REPL and offer suggestions even if there are non-string +candidates when errors occur. From c81143deb67320cc8d3f137828027b9dbb2bd08a Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 2 Jun 2025 06:50:14 -0300 Subject: [PATCH 4/4] Guard against dir(obj) raising, add tests. --- Lib/test/test_pyrepl/test_pyrepl.py | 4 +++- Lib/test/test_traceback.py | 24 ++++++++++++++++++++++++ Lib/traceback.py | 14 ++++++++++---- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 962ad6b45001cf..98bae7dd703fd9 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1672,6 +1672,7 @@ def test_null_byte(self): self.assertEqual(exit_code, 0) self.assertNotIn("TypeError", output) + @force_not_colorized def test_non_string_suggestion_candidates(self): commands = ("import runpy\n" "runpy._run_module_code('blech', {0: '', 'bluch': ''}, '')\n" @@ -1679,7 +1680,8 @@ def test_non_string_suggestion_candidates(self): output, exit_code = self.run_repl(commands) self.assertEqual(exit_code, 0) - self.assertIn("all elements in 'candidates' must be strings", output) + self.assertNotIn("all elements in 'candidates' must be strings", output) + self.assertIn("bluch", output) def test_readline_history_file(self): # skip, if readline module is not available diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b9be87f357ffdd..6b2271f5d5ba8d 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4188,6 +4188,15 @@ def __dir__(self): self.assertNotIn("blech", actual) self.assertNotIn("oh no!", actual) + def test_attribute_error_with_non_string_candidates(self): + class T: + bluch = 1 + + instance = T() + instance.__dict__[0] = 1 + actual = self.get_suggestion(instance, 'blich') + self.assertIn("bluch", actual) + def test_attribute_error_with_bad_name(self): def raise_attribute_error_with_bad_name(): raise AttributeError(name=12, obj=23) @@ -4301,6 +4310,13 @@ def test_import_from_suggestions_underscored(self): self.assertIn("'_bluch'", self.get_import_from_suggestion(code, '_luch')) self.assertNotIn("'_bluch'", self.get_import_from_suggestion(code, 'bluch')) + def test_import_from_suggestions_non_string(self): + modWithNonStringAttr = textwrap.dedent("""\ + globals()[0] = 1 + bluch = 1 + """) + self.assertIn("'bluch'", self.get_import_from_suggestion(modWithNonStringAttr, 'blech')) + def test_import_from_suggestions_do_not_trigger_for_long_attributes(self): code = "blech = None" @@ -4397,6 +4413,14 @@ def func(): actual = self.get_suggestion(func) self.assertIn("'ZeroDivisionError'?", actual) + def test_name_error_suggestions_with_non_string_candidates(self): + def func(): + abc = 1 + globals()[0] = 1 + abv + actual = self.get_suggestion(func) + self.assertIn("abc", actual) + def test_name_error_suggestions_do_not_trigger_for_long_names(self): def func(): somethingverywronghehehehehehe = None diff --git a/Lib/traceback.py b/Lib/traceback.py index ab2ff023c308e6..a1f175dbbaa421 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1595,7 +1595,11 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): if isinstance(exc_value, AttributeError): obj = exc_value.obj try: - d = dir(obj) + try: + d = dir(obj) + except TypeError: # Attributes are unsortable, e.g. int and str + d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys()) + d = sorted([x for x in d if isinstance(x, str)]) hide_underscored = (wrong_name[:1] != '_') if hide_underscored and tb is not None: while tb.tb_next is not None: @@ -1603,7 +1607,6 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): frame = tb.tb_frame if 'self' in frame.f_locals and frame.f_locals['self'] is obj: hide_underscored = False - d = [x for x in d if isinstance(x, str)] if hide_underscored: d = [x for x in d if x[:1] != '_'] except Exception: @@ -1611,8 +1614,11 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): elif isinstance(exc_value, ImportError): try: mod = __import__(exc_value.name) - d = dir(mod) - d = [x for x in d if isinstance(x, str)] + try: + d = dir(mod) + except TypeError: # Attributes are unsortable, e.g. int and str + d = list(mod.__dict__.keys()) + d = sorted([x for x in d if isinstance(x, str)]) if wrong_name[:1] != '_': d = [x for x in d if x[:1] != '_'] except Exception: