Skip to content

Commit a8abce8

Browse files
authored
Merge branch 'main' into gh-148211/decompose-pop-top
2 parents 81d854d + 8687b9d commit a8abce8

File tree

5 files changed

+93
-19
lines changed

5 files changed

+93
-19
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,10 @@ tarfile
11081108
timeit
11091109
------
11101110

1111+
* The output of the :mod:`timeit` command-line interface is colored by default.
1112+
This can be controlled with
1113+
:ref:`environment variables <using-on-controlling-color>`.
1114+
(Contributed by Hugo van Kemenade in :gh:`146609`.)
11111115
* The command-line interface now colorizes error tracebacks
11121116
by default. This can be controlled with
11131117
:ref:`environment variables <using-on-controlling-color>`.

Lib/_colorize.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ class Syntax(ThemeSection):
362362
reset: str = ANSIColors.RESET
363363

364364

365+
@dataclass(frozen=True, kw_only=True)
366+
class Timeit(ThemeSection):
367+
timing: str = ANSIColors.CYAN
368+
best: str = ANSIColors.BOLD_GREEN
369+
per_loop: str = ANSIColors.GREEN
370+
punctuation: str = ANSIColors.GREY
371+
warning: str = ANSIColors.YELLOW
372+
warning_worst: str = ANSIColors.MAGENTA
373+
warning_best: str = ANSIColors.GREEN
374+
reset: str = ANSIColors.RESET
375+
376+
365377
@dataclass(frozen=True, kw_only=True)
366378
class Traceback(ThemeSection):
367379
type: str = ANSIColors.BOLD_MAGENTA
@@ -397,6 +409,7 @@ class Theme:
397409
http_server: HttpServer = field(default_factory=HttpServer)
398410
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
399411
syntax: Syntax = field(default_factory=Syntax)
412+
timeit: Timeit = field(default_factory=Timeit)
400413
traceback: Traceback = field(default_factory=Traceback)
401414
unittest: Unittest = field(default_factory=Unittest)
402415

@@ -409,6 +422,7 @@ def copy_with(
409422
http_server: HttpServer | None = None,
410423
live_profiler: LiveProfiler | None = None,
411424
syntax: Syntax | None = None,
425+
timeit: Timeit | None = None,
412426
traceback: Traceback | None = None,
413427
unittest: Unittest | None = None,
414428
) -> Self:
@@ -424,6 +438,7 @@ def copy_with(
424438
http_server=http_server or self.http_server,
425439
live_profiler=live_profiler or self.live_profiler,
426440
syntax=syntax or self.syntax,
441+
timeit=timeit or self.timeit,
427442
traceback=traceback or self.traceback,
428443
unittest=unittest or self.unittest,
429444
)
@@ -443,6 +458,7 @@ def no_colors(cls) -> Self:
443458
http_server=HttpServer.no_colors(),
444459
live_profiler=LiveProfiler.no_colors(),
445460
syntax=Syntax.no_colors(),
461+
timeit=Timeit.no_colors(),
446462
traceback=Traceback.no_colors(),
447463
unittest=Unittest.no_colors(),
448464
)

Lib/test/test_timeit.py

Lines changed: 42 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'])
@@ -403,5 +407,39 @@ def callback(a, b):
403407
self.assertEqual(s.getvalue(), expected)
404408

405409

406-
if __name__ == '__main__':
410+
class TestTimeitColor(unittest.TestCase):
411+
412+
fake_stmt = TestTimeit.fake_stmt
413+
run_main = TestTimeit.run_main
414+
415+
@force_colorized
416+
def test_main_colorized(self):
417+
t = get_theme(force_color=True).timeit
418+
s = self.run_main(seconds_per_increment=5.5)
419+
self.assertEqual(
420+
s,
421+
"1 loop, best of 5: "
422+
f"{t.best}5.5 sec{t.reset} "
423+
f"{t.per_loop}per loop{t.reset}\n",
424+
)
425+
426+
@force_colorized
427+
def test_main_verbose_colorized(self):
428+
t = get_theme(force_color=True).timeit
429+
s = self.run_main(switches=["-v"])
430+
self.assertEqual(
431+
s,
432+
f"1 loop {t.punctuation}-> {t.timing}1 secs{t.reset}\n\n"
433+
"raw times: "
434+
f"{t.timing}1 sec{t.punctuation}, "
435+
f"{t.timing}1 sec{t.punctuation}, "
436+
f"{t.timing}1 sec{t.punctuation}, "
437+
f"{t.timing}1 sec{t.punctuation}, "
438+
f"{t.timing}1 sec{t.reset}\n\n"
439+
f"1 loop, best of 5: {t.best}1 sec{t.reset} "
440+
f"{t.per_loop}per loop{t.reset}\n",
441+
)
442+
443+
444+
if __name__ == "__main__":
407445
unittest.main()

Lib/timeit.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ def main(args=None, *, _wrap_timer=None):
273273
args = sys.argv[1:]
274274
import _colorize
275275
colorize = _colorize.can_colorize()
276+
theme = _colorize.get_theme(force_color=colorize).timeit
277+
reset = theme.reset
276278

277279
try:
278280
opts, args = getopt.getopt(args, "n:u:s:r:pt:vh",
@@ -337,10 +339,13 @@ def main(args=None, *, _wrap_timer=None):
337339
callback = None
338340
if verbose:
339341
def callback(number, time_taken):
340-
msg = "{num} loop{s} -> {secs:.{prec}g} secs"
341-
plural = (number != 1)
342-
print(msg.format(num=number, s='s' if plural else '',
343-
secs=time_taken, prec=precision))
342+
s = "" if number == 1 else "s"
343+
print(
344+
f"{number} loop{s} "
345+
f"{theme.punctuation}-> "
346+
f"{theme.timing}{time_taken:.{precision}g} secs{reset}"
347+
)
348+
344349
try:
345350
number, _ = t.autorange(callback, target_time)
346351
except:
@@ -371,24 +376,34 @@ def format_time(dt):
371376
return "%.*g %s" % (precision, dt / scale, unit)
372377

373378
if verbose:
374-
print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
379+
raw = f"{theme.punctuation}, ".join(
380+
f"{theme.timing}{t}" for t in map(format_time, raw_timings)
381+
)
382+
print(f"raw times: {raw}{reset}")
375383
print()
376384
timings = [dt / number for dt in raw_timings]
377385

378-
best = min(timings)
379-
print("%d loop%s, best of %d: %s per loop"
380-
% (number, 's' if number != 1 else '',
381-
repeat, format_time(best)))
382-
383386
best = min(timings)
384387
worst = max(timings)
388+
s = "" if number == 1 else "s"
389+
print(
390+
f"{number} loop{s}, best of {repeat}: "
391+
f"{theme.best}{format_time(best)}{reset} "
392+
f"{theme.per_loop}per loop{reset}"
393+
)
394+
385395
if worst >= best * 4:
386396
import warnings
387-
warnings.warn_explicit("The test results are likely unreliable. "
388-
"The worst time (%s) was more than four times "
389-
"slower than the best time (%s)."
390-
% (format_time(worst), format_time(best)),
391-
UserWarning, '', 0)
397+
398+
print(file=sys.stderr)
399+
warnings.warn_explicit(
400+
f"{theme.warning}The test results are likely unreliable. "
401+
f"The {theme.warning_worst}worst time ({format_time(worst)})"
402+
f"{theme.warning} was more than four times slower than the "
403+
f"{theme.warning_best}best time ({format_time(best)})"
404+
f"{theme.warning}.{reset}",
405+
UserWarning, "", 0,
406+
)
392407
return None
393408

394409

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add colour to :mod:`timeit` CLI output. Patch by Hugo van Kemenade.

0 commit comments

Comments
 (0)