Skip to content

Commit 2d4380c

Browse files
committed
Add colour to timeit CLI output
1 parent 578d726 commit 2d4380c

File tree

3 files changed

+85
-17
lines changed

3 files changed

+85
-17
lines changed

Lib/_colorize.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,20 @@ class Syntax(ThemeSection):
323323
reset: str = ANSIColors.RESET
324324

325325

326+
@dataclass(frozen=True, kw_only=True)
327+
class Timeit(ThemeSection):
328+
timing: str = ANSIColors.CYAN
329+
best: str = ANSIColors.BOLD_GREEN
330+
per_loop: str = ANSIColors.GREEN
331+
arrow: str = ANSIColors.GREY
332+
warning: str = ANSIColors.YELLOW
333+
warning_worst: str = ANSIColors.MAGENTA
334+
warning_worst_timing: str = ANSIColors.BOLD_MAGENTA
335+
warning_best: str = ANSIColors.GREEN
336+
warning_best_timing: str = ANSIColors.BOLD_GREEN
337+
reset: str = ANSIColors.RESET
338+
339+
326340
@dataclass(frozen=True, kw_only=True)
327341
class Traceback(ThemeSection):
328342
type: str = ANSIColors.BOLD_MAGENTA
@@ -356,6 +370,7 @@ class Theme:
356370
difflib: Difflib = field(default_factory=Difflib)
357371
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
358372
syntax: Syntax = field(default_factory=Syntax)
373+
timeit: Timeit = field(default_factory=Timeit)
359374
traceback: Traceback = field(default_factory=Traceback)
360375
unittest: Unittest = field(default_factory=Unittest)
361376

@@ -366,6 +381,7 @@ def copy_with(
366381
difflib: Difflib | None = None,
367382
live_profiler: LiveProfiler | None = None,
368383
syntax: Syntax | None = None,
384+
timeit: Timeit | None = None,
369385
traceback: Traceback | None = None,
370386
unittest: Unittest | None = None,
371387
) -> Self:
@@ -379,6 +395,7 @@ def copy_with(
379395
difflib=difflib or self.difflib,
380396
live_profiler=live_profiler or self.live_profiler,
381397
syntax=syntax or self.syntax,
398+
timeit=timeit or self.timeit,
382399
traceback=traceback or self.traceback,
383400
unittest=unittest or self.unittest,
384401
)
@@ -396,6 +413,7 @@ def no_colors(cls) -> Self:
396413
difflib=Difflib.no_colors(),
397414
live_profiler=LiveProfiler.no_colors(),
398415
syntax=Syntax.no_colors(),
416+
timeit=Timeit.no_colors(),
399417
traceback=Traceback.no_colors(),
400418
unittest=Unittest.no_colors(),
401419
)

Lib/test/test_timeit.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
from textwrap import dedent
66

77
from test.support import (
8-
captured_stdout, captured_stderr, force_not_colorized,
8+
captured_stderr,
9+
captured_stdout,
10+
force_colorized,
11+
force_not_colorized_test_class,
912
)
1013

14+
from _colorize import get_theme
15+
1116
# timeit's default number of iterations.
1217
DEFAULT_NUMBER = 1000000
1318

@@ -42,6 +47,7 @@ def wrap_timer(self, timer):
4247
self.saved_timer = timer
4348
return self
4449

50+
@force_not_colorized_test_class
4551
class TestTimeit(unittest.TestCase):
4652

4753
def tearDown(self):
@@ -352,13 +358,11 @@ def test_main_with_time_unit(self):
352358
self.assertEqual(error_stringio.getvalue(),
353359
"Unrecognized unit. Please select nsec, usec, msec, or sec.\n")
354360

355-
@force_not_colorized
356361
def test_main_exception(self):
357362
with captured_stderr() as error_stringio:
358363
s = self.run_main(switches=['1/0'])
359364
self.assert_exc_string(error_stringio.getvalue(), 'ZeroDivisionError')
360365

361-
@force_not_colorized
362366
def test_main_exception_fixed_reps(self):
363367
with captured_stderr() as error_stringio:
364368
s = self.run_main(switches=['-n1', '1/0'])
@@ -398,5 +402,32 @@ def callback(a, b):
398402
self.assertEqual(s.getvalue(), expected)
399403

400404

401-
if __name__ == '__main__':
405+
class TestTimeitColor(TestTimeit):
406+
407+
@force_colorized
408+
def test_main_colorized(self):
409+
t = get_theme(force_color=True).timeit
410+
s = self.run_main(seconds_per_increment=5.5)
411+
self.assertEqual(
412+
s,
413+
"1 loop, best of 5: "
414+
f"{t.best}5.5 sec {t.reset}"
415+
f"{t.per_loop}per loop{t.reset}\n",
416+
)
417+
418+
@force_colorized
419+
def test_main_verbose_colorized(self):
420+
t = get_theme(force_color=True).timeit
421+
s = self.run_main(switches=["-v"])
422+
self.assertEqual(
423+
s,
424+
f"1 loop {t.arrow}-> {t.timing}1 secs{t.reset}\n\n"
425+
"raw times: "
426+
f"{t.timing}1 sec, 1 sec, 1 sec, 1 sec, 1 sec{t.reset}\n\n"
427+
f"1 loop, best of 5: {t.best}1 sec {t.reset}"
428+
f"{t.per_loop}per loop{t.reset}\n",
429+
)
430+
431+
432+
if __name__ == "__main__":
402433
unittest.main()

Lib/timeit.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def main(args=None, *, _wrap_timer=None):
268268
args = sys.argv[1:]
269269
import _colorize
270270
colorize = _colorize.can_colorize()
271+
theme = _colorize.get_theme(force_color=colorize).timeit
272+
reset = theme.reset
271273

272274
try:
273275
opts, args = getopt.getopt(args, "n:u:s:r:pvh",
@@ -328,10 +330,13 @@ def main(args=None, *, _wrap_timer=None):
328330
callback = None
329331
if verbose:
330332
def callback(number, time_taken):
331-
msg = "{num} loop{s} -> {secs:.{prec}g} secs"
332-
plural = (number != 1)
333-
print(msg.format(num=number, s='s' if plural else '',
334-
secs=time_taken, prec=precision))
333+
s = "" if number == 1 else "s"
334+
print(
335+
f"{number} loop{s} "
336+
f"{theme.arrow}-> "
337+
f"{theme.timing}{time_taken:.{precision}g} secs{reset}"
338+
)
339+
335340
try:
336341
number, _ = t.autorange(callback)
337342
except:
@@ -362,24 +367,38 @@ def format_time(dt):
362367
return "%.*g %s" % (precision, dt / scale, unit)
363368

364369
if verbose:
365-
print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
370+
raw = ", ".join(map(format_time, raw_timings))
371+
print(f"raw times: {theme.timing}{raw}{reset}")
366372
print()
367373
timings = [dt / number for dt in raw_timings]
368374

369375
best = min(timings)
370-
print("%d loop%s, best of %d: %s per loop"
371-
% (number, 's' if number != 1 else '',
372-
repeat, format_time(best)))
376+
s = "" if number == 1 else "s"
377+
print(
378+
f"{number} loop{s}, best of {repeat}: "
379+
f"{theme.best}{format_time(best)} {reset}"
380+
f"{theme.per_loop}per loop{reset}"
381+
)
373382

374383
best = min(timings)
375384
worst = max(timings)
376385
if worst >= best * 4:
377386
import warnings
378-
warnings.warn_explicit("The test results are likely unreliable. "
379-
"The worst time (%s) was more than four times "
380-
"slower than the best time (%s)."
381-
% (format_time(worst), format_time(best)),
382-
UserWarning, '', 0)
387+
388+
print(file=sys.stderr)
389+
warnings.warn_explicit(
390+
f"{theme.warning}The test results are likely unreliable. "
391+
f"The {theme.warning_worst}worst time ("
392+
f"{theme.warning_worst_timing}{format_time(worst)}{reset}"
393+
f"{theme.warning_worst})"
394+
f"{theme.warning} was more than "
395+
f"{theme.warning_worst}four times slower"
396+
f"{theme.warning} than the "
397+
f"{theme.warning_best}best time ("
398+
f"{theme.warning_best_timing}{format_time(best)}{reset}"
399+
f"{theme.warning_best}){theme.warning}.{reset}",
400+
UserWarning, "", 0,
401+
)
383402
return None
384403

385404

0 commit comments

Comments
 (0)