From 99835c7191dca3790d221f4a15e69551c20aa697 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:09:09 +0200 Subject: [PATCH] Use argparse for colour help timeit CLI --- Lib/test/test_timeit.py | 26 +-- Lib/timeit.py | 178 ++++++++++-------- ...-05-03-23-47-59.gh-issue-146609.V9jqYf.rst | 1 + 3 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index f41a8a41834..81f1a9c9739 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -259,11 +259,12 @@ def run_main(self, seconds_per_increment=1.0, switches=None, timer=None): return s.getvalue() def test_main_bad_switch(self): - s = self.run_main(switches=['--bad-switch']) - self.assertEqual(s, dedent("""\ - option --bad-switch not recognized - use -h/--help for command line help - """)) + with captured_stderr() as error_stringio: + s = self.run_main(switches=["--bad-switch"]) + self.assertEqual(s, "") + self.assertIn( + "unrecognized arguments: --bad-switch", error_stringio.getvalue() + ) def test_main_seconds(self): s = self.run_main(seconds_per_increment=5.5) @@ -301,10 +302,11 @@ def test_main_negative_reps(self): s = self.run_main(seconds_per_increment=60.0, switches=['-r-5']) self.assertEqual(s, "1 loop, best of 1: 60 sec per loop\n") - @unittest.skipIf(sys.flags.optimize >= 2, "need __doc__") def test_main_help(self): s = self.run_main(switches=['-h']) - self.assertEqual(s, timeit.__doc__) + self.assertIn("Tool for measuring execution time", s) + self.assertIn("-n", s) + self.assertIn("--number", s) def test_main_verbose(self): s = self.run_main(switches=['-v']) @@ -353,10 +355,12 @@ def test_main_with_time_unit(self): "100 loops, best of 5: 3e+03 usec per loop\n") # Test invalid unit input with captured_stderr() as error_stringio: - invalid = self.run_main(seconds_per_increment=0.003, - switches=['-u', 'parsec']) - self.assertEqual(error_stringio.getvalue(), - "Unrecognized unit. Please select nsec, usec, msec, or sec.\n") + invalid = self.run_main( + seconds_per_increment=0.003, switches=["-u", "parsec"] + ) + self.assertIn( + "choose from nsec, usec, msec, sec", error_stringio.getvalue() + ) def test_main_exception(self): with captured_stderr() as error_stringio: diff --git a/Lib/timeit.py b/Lib/timeit.py index f09ef43400e..04b1e8f6fb4 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -6,38 +6,6 @@ Library usage: see the Timer class. -Command line usage: - python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [-t T] [--] [statement] - -Options: - -n/--number N: how many times to execute 'statement' (default: see below) - -r/--repeat N: how many times to repeat the timer (default 5) - -s/--setup S: statement to be executed once initially (default 'pass'). - Execution time of this setup statement is NOT timed. - -p/--process: use time.process_time() (default is time.perf_counter()) - -v/--verbose: print raw timing results; repeat for more digits precision - -u/--unit: set the output time unit (nsec, usec, msec, or sec) - -t/--target-time T: if --number is 0 the code will run until it - takes *at least* this many seconds - (default: 0.2) - -h/--help: print this usage message and exit - --: separate options from statement, use when statement starts with - - statement: statement to be timed (default 'pass') - -A multi-line statement may be given by specifying each line as a -separate argument; indented lines are possible by enclosing an -argument in quotes and using leading spaces. Multiple -s options are -treated similarly. - -If -n is not given, a suitable number of loops is calculated by trying -increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the -total time is at least --target-time seconds. - -Note: there is a certain baseline overhead associated with executing a -pass statement. It differs between versions. The code here doesn't try -to hide it, but you should be aware of it. The baseline overhead can be -measured by invoking the program without arguments. - Classes: Timer @@ -268,7 +236,7 @@ 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 + import argparse if args is None: args = sys.argv[1:] import _colorize @@ -276,54 +244,106 @@ def main(args=None, *, _wrap_timer=None): theme = _colorize.get_theme(force_color=colorize).timeit reset = theme.reset - try: - opts, args = getopt.getopt(args, "n:u:s:r:pt:vh", - ["number=", "setup=", "repeat=", - "process", "target-time=", - "verbose", "unit=", "help"]) - except getopt.error as err: - print(err) - print("use -h/--help for command line help") - return 2 + epilog = """\ +A multi-line statement may be given by specifying each line as a +separate argument; indented lines are possible by enclosing an +argument in quotes and using leading spaces. Multiple `-s` options are +treated similarly. - timer = default_timer - stmt = "\n".join(args) or "pass" - number = 0 # auto-determine - target_time = default_target_time - setup = [] - repeat = default_repeat - verbose = 0 - time_unit = None +If `-n` is not given, a suitable number of loops is calculated by trying +increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the +total time is at least `--target-time` seconds. + +Note: there is a certain baseline overhead associated with executing a +pass statement. It differs between versions. The code here doesn't try +to hide it, but you should be aware of it. The baseline overhead can be +measured by invoking the program without arguments.""" + + parser = argparse.ArgumentParser( + prog="python -m timeit", + description="""\ +Tool for measuring execution time of small code snippets. + +This module avoids a number of common traps for measuring execution +times. See also Tim Peters' introduction to the Algorithms chapter in +the Python Cookbook, published by O'Reilly. + +Library usage: see the Timer class.""", + epilog=epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-n", + "--number", + type=int, + default=0, + help="how many times to execute 'statement' (default: see below)", + ) + parser.add_argument( + "-r", + "--repeat", + type=int, + default=default_repeat, + help="how many times to repeat the timer (default %(default)s)", + ) + parser.add_argument( + "-s", + "--setup", + action="append", + default=[], + help="statement to be executed once initially " + "(default 'pass'). Execution time of this " + "setup statement is NOT timed.", + ) + parser.add_argument( + "-p", + "--process", + action="store_true", + help="use time.process_time() (default is time.perf_counter())", + ) + parser.add_argument( + "-t", + "--target-time", + type=float, + default=default_target_time, + help="if --number is 0 the code will run until it takes " + "at least this many seconds (default %(default)s)", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="print raw timing results; repeat for more digits precision", + ) + parser.add_argument( + "-u", + "--unit", + default=None, + choices=["nsec", "usec", "msec", "sec"], + help="set the output time unit", + ) + parser.add_argument( + "statement", + nargs="*", + default=["pass"], + help="statement to be timed (default 'pass')", + ) + try: + ns = parser.parse_args(args) + except SystemExit as e: + return e.code + + timer = time.process_time if ns.process else default_timer + stmt = "\n".join(ns.statement) or "pass" + number = ns.number + target_time = ns.target_time + setup = "\n".join(ns.setup) or "pass" + repeat = max(ns.repeat, 1) + verbose = ns.verbose + time_unit = ns.unit units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} - precision = 3 - for o, a in opts: - if o in ("-n", "--number"): - number = int(a) - if o in ("-s", "--setup"): - setup.append(a) - if o in ("-u", "--unit"): - if a in units: - time_unit = a - else: - print("Unrecognized unit. Please select nsec, usec, msec, or sec.", - file=sys.stderr) - return 2 - if o in ("-r", "--repeat"): - repeat = int(a) - if repeat <= 0: - repeat = 1 - if o in ("-p", "--process"): - timer = time.process_time - if o in ("-t", "--target-time"): - target_time = float(a) - if o in ("-v", "--verbose"): - if verbose: - precision += 1 - verbose += 1 - if o in ("-h", "--help"): - print(__doc__, end="") - return 0 - setup = "\n".join(setup) or "pass" + precision = 3 + max(verbose - 1, 0) # Include the current directory, so that local imports work (sys.path # contains the directory of this script, rather than the current diff --git a/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst new file mode 100644 index 00000000000..3171b70bcaa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst @@ -0,0 +1 @@ +Use argparse for colour help timeit CLI. Patch by Hugo van Kemenade.