From 1d4f3217d77be950d933ea9c36b0b58603a372e2 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Tue, 30 Jul 2024 14:01:42 +0200 Subject: [PATCH 1/7] gh-82378: fix sys.tracebacklimit in pyrepl make sure that pyrepl uses the same logic for sys.tracebacklimit as both the basic repl and the standard sys.excepthook --- Lib/_pyrepl/console.py | 6 ++++-- Lib/code.py | 8 ++++++-- Lib/test/test_pyrepl/test_pyrepl.py | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index a8d3f520340dcf..3c9c45bb49440f 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -162,10 +162,12 @@ def __init__( self.can_colorize = _colorize.can_colorize() def showsyntaxerror(self, filename=None): - super().showsyntaxerror(colorize=self.can_colorize) + import traceback + super().showsyntaxerror(colorize=self.can_colorize, limit=traceback.BUILTIN_EXCEPTION_LIMIT) def showtraceback(self): - super().showtraceback(colorize=self.can_colorize) + import traceback + super().showtraceback(colorize=self.can_colorize, limit=traceback.BUILTIN_EXCEPTION_LIMIT) def runsource(self, source, filename="", symbol="single"): try: diff --git a/Lib/code.py b/Lib/code.py index a55fced0704b1d..19567a893146fd 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -107,6 +107,7 @@ def showsyntaxerror(self, filename=None, **kwargs): """ colorize = kwargs.pop('colorize', False) + limit = kwargs.pop('limit', None) type, value, tb = sys.exc_info() sys.last_exc = value sys.last_type = type @@ -124,7 +125,8 @@ def showsyntaxerror(self, filename=None, **kwargs): value = SyntaxError(msg, (filename, lineno, offset, line)) sys.last_exc = sys.last_value = value if sys.excepthook is sys.__excepthook__: - lines = traceback.format_exception_only(type, value, colorize=colorize) + lines = traceback.format_exception_only(type, value, colorize=colorize, + limit=limit) self.write(''.join(lines)) else: # If someone has set sys.excepthook, we let that take precedence @@ -140,11 +142,13 @@ def showtraceback(self, **kwargs): """ colorize = kwargs.pop('colorize', False) + limit = kwargs.pop('limit', None) sys.last_type, sys.last_value, last_tb = ei = sys.exc_info() sys.last_traceback = last_tb sys.last_exc = ei[1] try: - lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize) + lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize, + limit=limit) if sys.excepthook is sys.__excepthook__: self.write(''.join(lines)) else: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 3a1bacef8a1756..f0a77991e4de75 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1076,6 +1076,31 @@ def test_not_wiping_history_file(self): self.assertIn("spam", output) self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0) + @force_not_colorized + def test_proper_tracebacklimit(self): + env = os.environ.copy() + commands = ("import sys\n" + "sys.tracebacklimit = 1\n" + "def x1(): 1/0\n\n" + "def x2(): x1()\n\n" + "def x3(): x2()\n\n" + "x3()\n" + "exit()\n") + + env.pop("PYTHON_BASIC_REPL", None) + output, exit_code = self.run_repl(commands, env=env) + if "can\'t use pyrepl" in output: + self.skipTest("pyrepl not available") + self.assertIn("in x1", output) + self.assertNotIn("in x3", output) + self.assertNotIn("in ", output) + + env["PYTHON_BASIC_REPL"] = "1" + output, exit_code = self.run_repl(commands, env=env) + self.assertIn("in x1", output) + self.assertNotIn("in x3", output) + self.assertNotIn("in ", output) + def run_repl( self, repl_input: str | list[str], From 2e18e4c01ef454f9c7f843868d46f1257073c6b5 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Wed, 31 Jul 2024 14:55:43 +0200 Subject: [PATCH 2/7] add news entry --- .../next/Library/2024-07-31-14-55-41.gh-issue-82378.eZvYmR.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-07-31-14-55-41.gh-issue-82378.eZvYmR.rst diff --git a/Misc/NEWS.d/next/Library/2024-07-31-14-55-41.gh-issue-82378.eZvYmR.rst b/Misc/NEWS.d/next/Library/2024-07-31-14-55-41.gh-issue-82378.eZvYmR.rst new file mode 100644 index 00000000000000..8af016e7c82fcb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-31-14-55-41.gh-issue-82378.eZvYmR.rst @@ -0,0 +1,2 @@ +Make sure that the new :term:`REPL` interprets :data:`sys.tracebacklimit` in +the same way that the classic REPL did. From a9703a163340ac82d6ce96f4cadc3f1fbc9b031f Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Fri, 16 Aug 2024 10:07:06 +0200 Subject: [PATCH 3/7] simplify the code again to 3.12 remove the undocumented new keyword arguments colorize and limit that were added only for pyrepl --- Lib/_pyrepl/console.py | 10 ++++------ Lib/code.py | 17 ++++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 3c9c45bb49440f..43f3b5b1e5bdb5 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -161,13 +161,11 @@ def __init__( super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] self.can_colorize = _colorize.can_colorize() - def showsyntaxerror(self, filename=None): + def _showtraceback(self, typ, value, tb, colorize=False, limit=None): import traceback - super().showsyntaxerror(colorize=self.can_colorize, limit=traceback.BUILTIN_EXCEPTION_LIMIT) - - def showtraceback(self): - import traceback - super().showtraceback(colorize=self.can_colorize, limit=traceback.BUILTIN_EXCEPTION_LIMIT) + return super()._showtraceback( + typ, value, tb, colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) def runsource(self, source, filename="", symbol="single"): try: diff --git a/Lib/code.py b/Lib/code.py index 755a2f411c6aa5..ef62914c95b955 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -94,7 +94,7 @@ def runcode(self, code): except: self.showtraceback() - def showsyntaxerror(self, filename=None, **kwargs): + def showsyntaxerror(self, filename=None): """Display the syntax error that just occurred. This doesn't display a stack trace because there isn't one. @@ -106,8 +106,6 @@ def showsyntaxerror(self, filename=None, **kwargs): The output is written by self.write(), below. """ - colorize = kwargs.pop('colorize', False) - limit = kwargs.pop('limit', None) try: typ, value, tb = sys.exc_info() if filename and typ is SyntaxError: @@ -120,11 +118,11 @@ def showsyntaxerror(self, filename=None, **kwargs): else: # Stuff in the right filename value = SyntaxError(msg, (filename, lineno, offset, line)) - self._showtraceback(typ, value, None, colorize, limit) + self._showtraceback(typ, value, None) finally: typ = value = tb = None - def showtraceback(self, **kwargs): + def showtraceback(self): """Display the exception that just occurred. We remove the first stack item because it is our own code. @@ -132,15 +130,16 @@ def showtraceback(self, **kwargs): The output is written by self.write(), below. """ - colorize = kwargs.pop('colorize', False) - limit = kwargs.pop('limit', None) try: typ, value, tb = sys.exc_info() - self._showtraceback(typ, value, tb.tb_next, colorize, limit) + self._showtraceback(typ, value, tb.tb_next) finally: typ = value = tb = None - def _showtraceback(self, typ, value, tb, colorize, limit): + def _showtraceback(self, typ, value, tb, colorize=False, limit=None): + # This method is being overwritten in + # _pyrepl.console.InteractiveColoredConsole to pass different values of + # colorize and limit sys.last_type = typ sys.last_traceback = tb sys.last_exc = sys.last_value = value = value.with_traceback(tb) From e736e73cc9f300461478e9afa8c9e0af17fe7c64 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Fri, 16 Aug 2024 22:22:25 +0200 Subject: [PATCH 4/7] add another method to make things less weird from an OO perspective --- Lib/_pyrepl/console.py | 10 ++++++---- Lib/code.py | 16 ++++++++-------- Lib/test/test_pyrepl/test_pyrepl.py | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 43f3b5b1e5bdb5..2b6c6beab7b304 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -161,11 +161,13 @@ def __init__( super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] self.can_colorize = _colorize.can_colorize() - def _showtraceback(self, typ, value, tb, colorize=False, limit=None): + def _excepthook(self, typ, value, tb): import traceback - return super()._showtraceback( - typ, value, tb, colorize=self.can_colorize, - limit=traceback.BUILTIN_EXCEPTION_LIMIT) + lines = traceback.format_exception( + typ, value, tb, + colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) + self.write(''.join(lines)) def runsource(self, source, filename="", symbol="single"): try: diff --git a/Lib/code.py b/Lib/code.py index ef62914c95b955..6860b61a48df3f 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -136,18 +136,12 @@ def showtraceback(self): finally: typ = value = tb = None - def _showtraceback(self, typ, value, tb, colorize=False, limit=None): - # This method is being overwritten in - # _pyrepl.console.InteractiveColoredConsole to pass different values of - # colorize and limit + def _showtraceback(self, typ, value, tb): sys.last_type = typ sys.last_traceback = tb sys.last_exc = sys.last_value = value = value.with_traceback(tb) if sys.excepthook is sys.__excepthook__: - lines = traceback.format_exception(typ, value, tb, - colorize=colorize, - limit=limit) - self.write(''.join(lines)) + self._excepthook(typ, value, tb) else: # If someone has set sys.excepthook, we let that take precedence # over self.write @@ -164,6 +158,12 @@ def _showtraceback(self, typ, value, tb, colorize=False, limit=None): print('Original exception was:', file=sys.stderr) sys.__excepthook__(typ, value, tb) + def _excepthook(self, typ, value, tb): + # This method is being overwritten in + # _pyrepl.console.InteractiveColoredConsole + lines = traceback.format_exception(typ, value, tb) + self.write(''.join(lines)) + def write(self, data): """Write a string. diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index edb5c1bfc3488b..9fce735a203f8c 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1020,7 +1020,7 @@ def test_dumb_terminal_exits_cleanly(self): env.update({"TERM": "dumb"}) output, exit_code = self.run_repl("exit()\n", env=env) self.assertEqual(exit_code, 0) - self.assertIn("warning: can\'t use pyrepl", output) + self.assertIn("warning: can't use pyrepl", output) self.assertNotIn("Exception", output) self.assertNotIn("Traceback", output) From 482f74e386141c57e847f929982360b546ecaa6d Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Fri, 16 Aug 2024 22:26:38 +0200 Subject: [PATCH 5/7] test both presence and absence of tracebacklimit --- Lib/test/test_pyrepl/test_pyrepl.py | 55 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 9fce735a203f8c..c7e35e02675e62 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1103,27 +1103,40 @@ def test_not_wiping_history_file(self): @force_not_colorized def test_proper_tracebacklimit(self): env = os.environ.copy() - commands = ("import sys\n" - "sys.tracebacklimit = 1\n" - "def x1(): 1/0\n\n" - "def x2(): x1()\n\n" - "def x3(): x2()\n\n" - "x3()\n" - "exit()\n") - - env.pop("PYTHON_BASIC_REPL", None) - output, exit_code = self.run_repl(commands, env=env) - if "can\'t use pyrepl" in output: - self.skipTest("pyrepl not available") - self.assertIn("in x1", output) - self.assertNotIn("in x3", output) - self.assertNotIn("in ", output) - - env["PYTHON_BASIC_REPL"] = "1" - output, exit_code = self.run_repl(commands, env=env) - self.assertIn("in x1", output) - self.assertNotIn("in x3", output) - self.assertNotIn("in ", output) + for set_tracebacklimit in [True, False]: + commands = ("import sys\n" + + ("sys.tracebacklimit = 1\n" if set_tracebacklimit else "") + + "def x1(): 1/0\n\n" + "def x2(): x1()\n\n" + "def x3(): x2()\n\n" + "x3()\n" + "exit()\n") + + env.pop("PYTHON_BASIC_REPL", None) + output, exit_code = self.run_repl(commands, env=env) + if "can\'t use pyrepl" in output: + self.skipTest("pyrepl not available") + self.assertIn("in x1", output) + if set_tracebacklimit: + self.assertNotIn("in x2", output) + self.assertNotIn("in x3", output) + self.assertNotIn("in ", output) + else: + self.assertIn("in x2", output) + self.assertIn("in x3", output) + self.assertIn("in ", output) + + env["PYTHON_BASIC_REPL"] = "1" + output, exit_code = self.run_repl(commands, env=env) + self.assertIn("in x1", output) + if set_tracebacklimit: + self.assertNotIn("in x2", output) + self.assertNotIn("in x3", output) + self.assertNotIn("in ", output) + else: + self.assertIn("in x2", output) + self.assertIn("in x3", output) + self.assertIn("in ", output) def run_repl( self, From 7116f4198af508c9ab799139caeecd2ab79ddcf4 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Fri, 16 Aug 2024 22:52:57 +0200 Subject: [PATCH 6/7] remove duplication by adding another loop --- Lib/test/test_pyrepl/test_pyrepl.py | 44 +++++++++++++---------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index c7e35e02675e62..37bae82b6dca02 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1112,31 +1112,25 @@ def test_proper_tracebacklimit(self): "x3()\n" "exit()\n") - env.pop("PYTHON_BASIC_REPL", None) - output, exit_code = self.run_repl(commands, env=env) - if "can\'t use pyrepl" in output: - self.skipTest("pyrepl not available") - self.assertIn("in x1", output) - if set_tracebacklimit: - self.assertNotIn("in x2", output) - self.assertNotIn("in x3", output) - self.assertNotIn("in ", output) - else: - self.assertIn("in x2", output) - self.assertIn("in x3", output) - self.assertIn("in ", output) - - env["PYTHON_BASIC_REPL"] = "1" - output, exit_code = self.run_repl(commands, env=env) - self.assertIn("in x1", output) - if set_tracebacklimit: - self.assertNotIn("in x2", output) - self.assertNotIn("in x3", output) - self.assertNotIn("in ", output) - else: - self.assertIn("in x2", output) - self.assertIn("in x3", output) - self.assertIn("in ", output) + for basic_repl in [True, False]: + if basic_repl: + env["PYTHON_BASIC_REPL"] = "1" + else: + env.pop("PYTHON_BASIC_REPL", None) + with self.subTest(set_tracebacklimit=set_tracebacklimit, + basic_repl=basic_repl): + output, exit_code = self.run_repl(commands, env=env) + if "can\'t use pyrepl" in output: + self.skipTest("pyrepl not available") + self.assertIn("in x1", output) + if set_tracebacklimit: + self.assertNotIn("in x2", output) + self.assertNotIn("in x3", output) + self.assertNotIn("in ", output) + else: + self.assertIn("in x2", output) + self.assertIn("in x3", output) + self.assertIn("in ", output) def run_repl( self, From 5202d27f3cddd652ab178af0a4b8bbad73ad8511 Mon Sep 17 00:00:00 2001 From: CF Bolz-Tereick Date: Fri, 16 Aug 2024 23:49:01 +0200 Subject: [PATCH 7/7] remove \ --- Lib/test/test_pyrepl/test_pyrepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 37bae82b6dca02..e11ec74aa14058 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1120,7 +1120,7 @@ def test_proper_tracebacklimit(self): with self.subTest(set_tracebacklimit=set_tracebacklimit, basic_repl=basic_repl): output, exit_code = self.run_repl(commands, env=env) - if "can\'t use pyrepl" in output: + if "can't use pyrepl" in output: self.skipTest("pyrepl not available") self.assertIn("in x1", output) if set_tracebacklimit: