From beed64ff0d4933597c9f88593430c37f846edb92 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 27 Sep 2025 08:26:50 +0800 Subject: [PATCH 01/17] fix: make timeit traceback can with color Signed-off-by: yihong0618 --- Lib/timeit.py | 5 +++-- .../Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst diff --git a/Lib/timeit.py b/Lib/timeit.py index e767f0187826df..ddf7a9952aaea0 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -150,7 +150,7 @@ def print_exc(self, file=None): The optional file argument directs where the traceback is sent; it defaults to sys.stderr. """ - import linecache, traceback + import linecache, traceback, _colorize, sys if self.src is not None: linecache.cache[dummy_src_name] = (len(self.src), None, @@ -158,7 +158,8 @@ def print_exc(self, file=None): dummy_src_name) # else the source is already stored somewhere else - traceback.print_exc(file=file) + traceback.print_exception(sys.exception(), file=file, + colorize=_colorize.can_colorize(file=file)) def timeit(self, number=default_number): """Time 'number' executions of the main statement. diff --git a/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst new file mode 100644 index 00000000000000..86703463e9f193 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst @@ -0,0 +1 @@ +Make timeit error traceback can with corlor From 0a90dc80fc48383ad5959336aee1c54e4a9c7651 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 27 Sep 2025 08:41:36 +0800 Subject: [PATCH 02/17] fix: test with color Signed-off-by: yihong0618 --- Lib/test/test_timeit.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 2aeebea9f93d43..00553c68bbc262 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -223,7 +223,12 @@ def assert_exc_string(self, exc_string, expected_exc_name): exc_lines = exc_string.splitlines() self.assertGreater(len(exc_lines), 2) self.assertStartsWith(exc_lines[0], 'Traceback') - self.assertStartsWith(exc_lines[-1], expected_exc_name) + # Remove ANSI color codes from the last line before checking + import re + last_line = exc_lines[-1] + # Remove ANSI escape sequences + clean_last_line = re.sub(r'\x1b\[[0-9;]*m', '', last_line) + self.assertStartsWith(clean_last_line, expected_exc_name) def test_print_exc(self): s = io.StringIO() From 74ae16969c892607fc8be9a477b67e79b5fb6c43 Mon Sep 17 00:00:00 2001 From: yihong Date: Sat, 27 Sep 2025 16:39:05 +0800 Subject: [PATCH 03/17] Update Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- .../next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst index 86703463e9f193..b81f94fc535689 100644 --- a/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst +++ b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst @@ -1 +1 @@ -Make timeit error traceback can with corlor +:mod:`timeit`: colorize error tracebacks. From f798c4ed1c10ab278f9bdb50df0ee0f22362704d Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 27 Sep 2025 16:43:49 +0800 Subject: [PATCH 04/17] fix: address comments Signed-off-by: yihong0618 --- Lib/test/test_timeit.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 00553c68bbc262..35fec0a52a9214 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -4,7 +4,7 @@ import io from textwrap import dedent -from test.support import captured_stdout +from test.support import captured_stdout, force_not_colorized from test.support import captured_stderr # timeit's default number of iterations. @@ -219,16 +219,12 @@ def test_repeat_function_zero_iters(self): timer=FakeTimer()) self.assertEqual(delta_times, DEFAULT_REPEAT * [0.0]) + @force_not_colorized def assert_exc_string(self, exc_string, expected_exc_name): exc_lines = exc_string.splitlines() self.assertGreater(len(exc_lines), 2) self.assertStartsWith(exc_lines[0], 'Traceback') - # Remove ANSI color codes from the last line before checking - import re - last_line = exc_lines[-1] - # Remove ANSI escape sequences - clean_last_line = re.sub(r'\x1b\[[0-9;]*m', '', last_line) - self.assertStartsWith(clean_last_line, expected_exc_name) + self.assertStartsWith(exc_lines[-1], expected_exc_name) def test_print_exc(self): s = io.StringIO() From 85539691aaeedc371391d610d444867dabe91861 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 27 Sep 2025 17:01:53 +0800 Subject: [PATCH 05/17] fix: make force color the right place Signed-off-by: yihong0618 --- Lib/test/test_timeit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 35fec0a52a9214..a5760d8af85bf7 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -219,13 +219,13 @@ def test_repeat_function_zero_iters(self): timer=FakeTimer()) self.assertEqual(delta_times, DEFAULT_REPEAT * [0.0]) - @force_not_colorized def assert_exc_string(self, exc_string, expected_exc_name): exc_lines = exc_string.splitlines() self.assertGreater(len(exc_lines), 2) self.assertStartsWith(exc_lines[0], 'Traceback') self.assertStartsWith(exc_lines[-1], expected_exc_name) + @force_not_colorized def test_print_exc(self): s = io.StringIO() t = timeit.Timer("1/0") @@ -352,11 +352,13 @@ def test_main_with_time_unit(self): self.assertEqual(error_stringio.getvalue(), "Unrecognized unit. Please select nsec, usec, msec, or sec.\n") + @force_not_colorized def test_main_exception(self): with captured_stderr() as error_stringio: s = self.run_main(switches=['1/0']) self.assert_exc_string(error_stringio.getvalue(), 'ZeroDivisionError') + @force_not_colorized def test_main_exception_fixed_reps(self): with captured_stderr() as error_stringio: s = self.run_main(switches=['-n1', '1/0']) From b6703460abf09e7d79d00924b3c2d55aee144143 Mon Sep 17 00:00:00 2001 From: yihong Date: Sat, 27 Sep 2025 18:01:16 +0800 Subject: [PATCH 06/17] Update Lib/timeit.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/timeit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index ddf7a9952aaea0..4b5d00d0083a3a 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -150,7 +150,7 @@ def print_exc(self, file=None): The optional file argument directs where the traceback is sent; it defaults to sys.stderr. """ - import linecache, traceback, _colorize, sys + import _colorize, linecache, traceback, sys if self.src is not None: linecache.cache[dummy_src_name] = (len(self.src), None, From 80097e8e3d6fd15908b7718a06b3a2fdf4ab11d1 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 09:00:32 +0800 Subject: [PATCH 07/17] Update Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst Co-authored-by: Peter Bierma --- .../next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst index b81f94fc535689..6504a1904102dd 100644 --- a/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst +++ b/Misc/NEWS.d/next/Library/2025-09-27-08-26-31.gh-issue-139374.hfh-dl.rst @@ -1 +1 @@ -:mod:`timeit`: colorize error tracebacks. +:mod:`timeit`: Add color to error tracebacks. From ad7dfe172019afea94c5ede3055aeeee967b2f1f Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 16:20:26 +0800 Subject: [PATCH 08/17] fix: address comments Signed-off-by: yihong0618 --- Lib/timeit.py | 8 +++++--- Lib/traceback.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index 4b5d00d0083a3a..274278c2a46e12 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -133,7 +133,7 @@ def __init__(self, stmt="pass", setup="pass", timer=default_timer, exec(code, global_ns, local_ns) self.inner = local_ns["inner"] - def print_exc(self, file=None): + def print_exc(self, file=None, **kwargs): """Helper to print a traceback from the timed code. Typical use: @@ -158,8 +158,10 @@ def print_exc(self, file=None): dummy_src_name) # else the source is already stored somewhere else - traceback.print_exception(sys.exception(), file=file, - colorize=_colorize.can_colorize(file=file)) + if 'colorize' not in kwargs: + kwargs['colorize'] = _colorize.can_colorize(file=file) + + traceback.print_exc(file=file, **kwargs) def timeit(self, number=default_number): """Time 'number' executions of the main statement. diff --git a/Lib/traceback.py b/Lib/traceback.py index 8e2d8d72a0a32d..692d44837936ee 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -206,9 +206,9 @@ def _safe_string(value, what, func=str): # -- -def print_exc(limit=None, file=None, chain=True): +def print_exc(limit=None, file=None, chain=True, **kwargs): """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'.""" - print_exception(sys.exception(), limit=limit, file=file, chain=chain) + print_exception(sys.exception(), limit=limit, file=file, chain=chain, **kwargs) def format_exc(limit=None, chain=True): """Like print_exc() but return a string.""" From 0123a6a90145b83ebbc94da8d994e0ac356a740d Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 17:22:58 +0800 Subject: [PATCH 09/17] fix: address comments Signed-off-by: yihong0618 --- Doc/whatsnew/3.15.rst | 8 ++++++++ Lib/timeit.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d5d387d9a0aaa7..ce5b0722c2e193 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -528,6 +528,14 @@ tarfile (Contributed by Christoph Walcher in :gh:`57911`.) +timeit +------ + +* Error tracebacks are now colourised by default. This can be controlled by + :ref:`environment variables `. + (Contributed by Yi Hong in :gh:`139374`.) + + types ------ diff --git a/Lib/timeit.py b/Lib/timeit.py index 274278c2a46e12..57d57ed2035240 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -149,8 +149,13 @@ def print_exc(self, file=None, **kwargs): The optional file argument directs where the traceback is sent; it defaults to sys.stderr. + + The optional colorize keyword argument controls whether the + traceback is colorized; it defaults to False for programmatic + usage. When used from the command line, this is automatically + set based on terminal capabilities. """ - import _colorize, linecache, traceback, sys + import linecache, traceback, sys if self.src is not None: linecache.cache[dummy_src_name] = (len(self.src), None, @@ -158,8 +163,7 @@ def print_exc(self, file=None, **kwargs): dummy_src_name) # else the source is already stored somewhere else - if 'colorize' not in kwargs: - kwargs['colorize'] = _colorize.can_colorize(file=file) + kwargs['colorize'] = kwargs.get('colorize', False) traceback.print_exc(file=file, **kwargs) @@ -263,6 +267,7 @@ def main(args=None, *, _wrap_timer=None): if args is None: args = sys.argv[1:] import getopt + import _colorize try: opts, args = getopt.getopt(args, "n:u:s:r:pvh", ["number=", "setup=", "repeat=", @@ -329,7 +334,8 @@ def callback(number, time_taken): try: number, _ = t.autorange(callback) except: - t.print_exc() + colorize = _colorize.can_colorize() + t.print_exc(colorize=colorize) return 1 if verbose: @@ -338,7 +344,8 @@ def callback(number, time_taken): try: raw_timings = t.repeat(repeat, number) except: - t.print_exc() + colorize = _colorize.can_colorize() + t.print_exc(colorize=colorize) return 1 def format_time(dt): From ee2732f260b8b600ef441213ef64d8648e2577c8 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 18:47:45 +0800 Subject: [PATCH 10/17] Update Lib/timeit.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/timeit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index 57d57ed2035240..7c0f04b8647a5b 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -164,7 +164,6 @@ def print_exc(self, file=None, **kwargs): # else the source is already stored somewhere else kwargs['colorize'] = kwargs.get('colorize', False) - traceback.print_exc(file=file, **kwargs) def timeit(self, number=default_number): From b76c3ff9b8515ea4e74617b2247e6e7717f1839c Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 18:59:40 +0800 Subject: [PATCH 11/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 3 ++- Lib/timeit.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index ce5b0722c2e193..ca313ab15205bf 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -531,7 +531,8 @@ tarfile timeit ------ -* Error tracebacks are now colourised by default. This can be controlled by +* The command-line interface now colorize error tracebacks + by default. This can be controlled by :ref:`environment variables `. (Contributed by Yi Hong in :gh:`139374`.) diff --git a/Lib/timeit.py b/Lib/timeit.py index 7c0f04b8647a5b..c7cb7e6493eab1 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -267,6 +267,8 @@ def main(args=None, *, _wrap_timer=None): args = sys.argv[1:] import getopt import _colorize + colorize_errors = _colorize.can_colorize() + try: opts, args = getopt.getopt(args, "n:u:s:r:pvh", ["number=", "setup=", "repeat=", From 89fed1109dc57ceb76baf69dbb8a110167c08757 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 19:02:26 +0800 Subject: [PATCH 12/17] fix: apply suggestions Signed-off-by: yihong0618 --- Lib/timeit.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index c7cb7e6493eab1..5046bbb3c1ddfc 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -335,8 +335,7 @@ def callback(number, time_taken): try: number, _ = t.autorange(callback) except: - colorize = _colorize.can_colorize() - t.print_exc(colorize=colorize) + t.print_exc(colorize=colorize_errors) return 1 if verbose: @@ -345,8 +344,7 @@ def callback(number, time_taken): try: raw_timings = t.repeat(repeat, number) except: - colorize = _colorize.can_colorize() - t.print_exc(colorize=colorize) + t.print_exc(colorize=colorize_errors) return 1 def format_time(dt): From b797dbbadcdeaa84ea22fe4b179cf088c45d5f41 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 19:03:11 +0800 Subject: [PATCH 13/17] fix: drop useless code Signed-off-by: yihong0618 --- Lib/timeit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index 5046bbb3c1ddfc..bf40a1976e201b 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -155,7 +155,7 @@ def print_exc(self, file=None, **kwargs): usage. When used from the command line, this is automatically set based on terminal capabilities. """ - import linecache, traceback, sys + import linecache, traceback if self.src is not None: linecache.cache[dummy_src_name] = (len(self.src), None, From 7409fc4f90354d09492eeb2916bece5902f56fd8 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 19:13:58 +0800 Subject: [PATCH 14/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/timeit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index bf40a1976e201b..9d8f8761ec2f93 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -265,9 +265,9 @@ def main(args=None, *, _wrap_timer=None): """ if args is None: args = sys.argv[1:] - import getopt import _colorize - colorize_errors = _colorize.can_colorize() + import getopt + colorize = _colorize.can_colorize() try: opts, args = getopt.getopt(args, "n:u:s:r:pvh", @@ -335,7 +335,7 @@ def callback(number, time_taken): try: number, _ = t.autorange(callback) except: - t.print_exc(colorize=colorize_errors) + t.print_exc(colorize=colorize) return 1 if verbose: @@ -344,7 +344,7 @@ def callback(number, time_taken): try: raw_timings = t.repeat(repeat, number) except: - t.print_exc(colorize=colorize_errors) + t.print_exc(colorize=colorize) return 1 def format_time(dt): From ec2ceadfbdff9735224f10f28659b0f792d10a93 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 19:18:37 +0800 Subject: [PATCH 15/17] fix: move import Signed-off-by: yihong0618 --- Lib/timeit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/timeit.py b/Lib/timeit.py index 9d8f8761ec2f93..80791acdeca23f 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -263,10 +263,10 @@ def main(args=None, *, _wrap_timer=None): is not None, it must be a callable that accepts a timer function and returns another timer function (used for unit testing). """ + import getopt if args is None: args = sys.argv[1:] import _colorize - import getopt colorize = _colorize.can_colorize() try: From 7d2d2dac02c6837ddb37b31d35795ba64728d4e2 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 19:19:18 +0800 Subject: [PATCH 16/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 4 ++-- Lib/test/test_timeit.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index ca313ab15205bf..1e4777a8f1162b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -531,8 +531,8 @@ tarfile timeit ------ -* The command-line interface now colorize error tracebacks - by default. This can be controlled by +* The command-line interface now colorizes error tracebacks + by default. This can be controlled with :ref:`environment variables `. (Contributed by Yi Hong in :gh:`139374`.) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index a5760d8af85bf7..931762ddbedc21 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -225,7 +225,6 @@ def assert_exc_string(self, exc_string, expected_exc_name): self.assertStartsWith(exc_lines[0], 'Traceback') self.assertStartsWith(exc_lines[-1], expected_exc_name) - @force_not_colorized def test_print_exc(self): s = io.StringIO() t = timeit.Timer("1/0") From f6d9da2595011dea74f20c72d4483d713a339c16 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 19:19:53 +0800 Subject: [PATCH 17/17] Update Lib/test/test_timeit.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_timeit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 931762ddbedc21..f8bc306b455a5d 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -4,8 +4,9 @@ import io from textwrap import dedent -from test.support import captured_stdout, force_not_colorized -from test.support import captured_stderr +from test.support import ( + captured_stdout, captured_stderr, force_not_colorized, +) # timeit's default number of iterations. DEFAULT_NUMBER = 1000000