Skip to content

Commit a63c5f1

Browse files
committed
support top-level setup statements in Timer objects
1 parent 3964f97 commit a63c5f1

File tree

5 files changed

+204
-67
lines changed

5 files changed

+204
-67
lines changed

Doc/library/timeit.rst

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,44 @@ Python Interface
6262
The module defines three convenience functions and a public class:
6363

6464

65-
.. function:: timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)
66-
67-
Create a :class:`Timer` instance with the given statement, *setup* code and
68-
*timer* function and run its :meth:`.timeit` method with *number* executions.
65+
.. function:: timeit(stmt='pass', setup='pass', timer=<default timer>,\
66+
number=1000000, globals=None,\
67+
*,\
68+
global_setup='pass')
69+
70+
Create a :class:`Timer` instance with the given statement, *setup* code,
71+
*global_setup* global code and *timer* function and run its :meth:`.timeit`
72+
method with the given *number* executions.
6973
The optional *globals* argument specifies a namespace in which to execute the
7074
code.
7175

7276
.. versionchanged:: 3.5
7377
The optional *globals* parameter was added.
7478

79+
.. versionchanged:: next
80+
The optional *global_setup* parameter was added.
81+
7582

76-
.. function:: repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None)
83+
.. function:: repeat(stmt='pass', setup='pass', timer=<default timer>,\
84+
repeat=5, number=1000000, globals=None,\
85+
*,\
86+
global_setup='pass')
7787
78-
Create a :class:`Timer` instance with the given statement, *setup* code and
79-
*timer* function and run its :meth:`.repeat` method with the given *repeat*
80-
count and *number* executions. The optional *globals* argument specifies a
81-
namespace in which to execute the code.
88+
Create a :class:`Timer` instance with the given statement, *setup* code,
89+
*global_setup* global code and *timer* function and run its :meth:`.repeat`
90+
method with the given *repeat* count and *number* executions.
91+
The optional *globals* argument specifies a namespace in which to execute the
92+
code.
8293

8394
.. versionchanged:: 3.5
8495
The optional *globals* parameter was added.
8596

8697
.. versionchanged:: 3.7
8798
Default value of *repeat* changed from 3 to 5.
8899

100+
.. versionchanged:: next
101+
The optional *global_setup* parameter was added.
102+
89103

90104
.. function:: default_timer()
91105

@@ -96,36 +110,52 @@ The module defines three convenience functions and a public class:
96110
:func:`time.perf_counter` is now the default timer.
97111

98112

99-
.. class:: Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None)
113+
.. class:: Timer(stmt='pass', setup='pass', timer=<timer function>,\
114+
globals=None, *, global_setup='pass')
100115
101116
Class for timing execution speed of small code snippets.
102117

103118
The constructor takes a statement to be timed, an additional statement used
104-
for setup, and a timer function. Both statements default to ``'pass'``;
119+
for setup and global setup, and a timer function.
120+
Statements default to ``'pass'``;
105121
the timer function is platform-dependent (see the module doc string).
106-
*stmt* and *setup* may also contain multiple statements separated by ``;``
107-
or newlines, as long as they don't contain multi-line string literals. The
108-
statement will by default be executed within timeit's namespace; this behavior
122+
123+
*stmt*, *setup* and *global_setup* may also contain multiple statements
124+
separated by ``;`` or newlines, as long as they don't contain multi-line
125+
string literals.
126+
127+
The statement will by default be executed within timeit's namespace; this behavior
109128
can be controlled by passing a namespace to *globals*.
110129

130+
The distinction between *setup* and *global_setup* is that *setup* is
131+
executed once per :meth:`.timeit` call while *global_setup* is executed
132+
only at construction time. *global_setup* is typically useful to execute
133+
top-level-only statements such as :ref:`future statements <future>` and
134+
:ref:`wildcard imports <import>`.
135+
111136
To measure the execution time of the first statement, use the :meth:`.timeit`
112137
method. The :meth:`.repeat` and :meth:`.autorange` methods are convenience
113138
methods to call :meth:`.timeit` multiple times.
114139

115-
The execution time of *setup* is excluded from the overall timed execution run.
140+
The execution time of *setup* or *global_setup* is excluded from the
141+
overall timed execution run.
116142

117-
The *stmt* and *setup* parameters can also take objects that are callable
143+
The *stmt*, *setup* and *global_setup* parameters can also take objects that are callable
118144
without arguments. This will embed calls to them in a timer function that
119145
will then be executed by :meth:`.timeit`. Note that the timing overhead is a
120146
little larger in this case because of the extra function calls.
121147

122148
.. versionchanged:: 3.5
123149
The optional *globals* parameter was added.
124150

151+
.. versionchanged:: next
152+
The optional *global_setup* parameter was added.
153+
125154
.. method:: Timer.timeit(number=1000000)
126155

127-
Time *number* executions of the main statement. This executes the setup
128-
statement once, and then returns the time it takes to execute the main
156+
Time *number* executions of the main statement.
157+
158+
This executes the setup statement once, and then returns the time it takes to execute the main
129159
statement a number of times. The default timer returns seconds as a float.
130160
The argument is the number of times through the loop, defaulting to one
131161
million. The main statement, the setup statement and the timer function
@@ -208,7 +238,7 @@ Command-Line Interface
208238

209239
When called as a program from the command line, the following form is used::
210240

211-
python -m timeit [-n N] [-r N] [-u U] [-s S] [-p] [-v] [-h] [statement ...]
241+
python -m timeit [-n N] [-r N] [-u U] [-s S] [-g S] [-p] [-v] [-h] [statement ...]
212242

213243
Where the following options are understood:
214244

@@ -224,8 +254,14 @@ Where the following options are understood:
224254

225255
.. option:: -s S, --setup=S
226256

257+
statement to be executed once per timer repetition (default ``pass``)
258+
259+
.. option:: -g S, --global-setup=S
260+
227261
statement to be executed once initially (default ``pass``)
228262

263+
.. versionadded:: next
264+
229265
.. option:: -p, --process
230266

231267
measure process time, not wallclock time, using :func:`time.process_time`

Doc/whatsnew/3.15.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,21 @@ tarfile
355355
and :cve:`2025-4435`.)
356356

357357

358+
timeit
359+
------
360+
361+
* :class:`~timeit.Timer` now support global setup statements which
362+
are typically useful for top-level-only statements such as future
363+
statements or wildcard imports.
364+
365+
Contrary to normal setup statements which are executed every time
366+
a timer is repeated, global setup statements are executed exactly
367+
once when constructing the ``Timer`` object and their side-effects
368+
remain active for the entire object lifetime.
369+
370+
(Contributed by Bénédikt Tran in :gh:`137578`.)
371+
372+
358373
types
359374
------
360375

Lib/test/test_timeit.py

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import itertools
12
import timeit
23
import unittest
34
import sys
@@ -23,8 +24,9 @@ class FakeTimer:
2324
BASE_TIME = 42.0
2425
def __init__(self, seconds_per_increment=1.0):
2526
self.count = 0
27+
self.global_setup_calls = 0
2628
self.setup_calls = 0
27-
self.seconds_per_increment=seconds_per_increment
29+
self.seconds_per_increment = seconds_per_increment
2830
timeit._fake_timer = self
2931

3032
def __call__(self):
@@ -33,6 +35,9 @@ def __call__(self):
3335
def inc(self):
3436
self.count += 1
3537

38+
def global_setup(self):
39+
self.global_setup_calls += 1
40+
3641
def setup(self):
3742
self.setup_calls += 1
3843

@@ -81,6 +86,15 @@ def test_timer_invalid_stmt(self):
8186
self.assertRaises(SyntaxError, timeit.Timer,
8287
setup='while False:\n pass', stmt=' break')
8388

89+
def test_timer_invalid_global_setup(self):
90+
self.assertRaises(ValueError, timeit.Timer, global_setup=None)
91+
self.assertRaises(SyntaxError, timeit.Timer, global_setup='return')
92+
self.assertRaises(SyntaxError, timeit.Timer, global_setup='yield')
93+
self.assertRaises(SyntaxError, timeit.Timer, global_setup='yield from ()')
94+
self.assertRaises(SyntaxError, timeit.Timer, global_setup='break')
95+
self.assertRaises(SyntaxError, timeit.Timer, global_setup='continue')
96+
self.assertRaises(SyntaxError, timeit.Timer, global_setup=' pass')
97+
8498
def test_timer_invalid_setup(self):
8599
self.assertRaises(ValueError, timeit.Timer, setup=None)
86100
self.assertRaises(SyntaxError, timeit.Timer, setup='return')
@@ -91,30 +105,62 @@ def test_timer_invalid_setup(self):
91105
self.assertRaises(SyntaxError, timeit.Timer, setup='from timeit import *')
92106
self.assertRaises(SyntaxError, timeit.Timer, setup=' pass')
93107

108+
def test_timer_empty_global_setup(self):
109+
timeit.Timer(global_setup='')
110+
timeit.Timer(global_setup=' \n\t\f')
111+
timeit.Timer(global_setup='# comment')
112+
94113
def test_timer_empty_stmt(self):
95114
timeit.Timer(stmt='')
96115
timeit.Timer(stmt=' \n\t\f')
97116
timeit.Timer(stmt='# comment')
98117

118+
def test_timer_global_setup_future(self):
119+
timer = timeit.Timer(stmt='print(f.__annotations__)',
120+
setup='def f(x: int): pass',
121+
global_setup='from __future__ import annotations')
122+
with captured_stdout() as stdout:
123+
timer.timeit(1)
124+
self.assertEqual(stdout.getvalue().strip(), str({'x': 'int'}))
125+
126+
def test_timer_global_setup_side_effect(self):
127+
timer = timeit.Timer(stmt='print(s)', global_setup='s = 1')
128+
with captured_stdout() as stdout:
129+
timer.timeit(5)
130+
self.assertEqual(stdout.getvalue(), '1\n' * 5)
131+
132+
def test_timer_callable_global_setup_side_effect(self):
133+
self.fake_timer = FakeTimer()
134+
timer = timeit.Timer(global_setup=self.fake_callable_global_setup)
135+
timer.timeit(5)
136+
self.assertEqual(self.fake_timer.count, 0)
137+
self.assertEqual(self.fake_timer.setup_calls, 0)
138+
self.assertEqual(self.fake_timer.global_setup_calls, 1)
139+
140+
fake_global_setup = "import timeit\ntimeit._fake_timer.global_setup()"
99141
fake_setup = "import timeit\ntimeit._fake_timer.setup()"
100142
fake_stmt = "import timeit\ntimeit._fake_timer.inc()"
101143

144+
def fake_callable_global_setup(self):
145+
self.fake_timer.global_setup()
146+
102147
def fake_callable_setup(self):
103148
self.fake_timer.setup()
104149

105150
def fake_callable_stmt(self):
106151
self.fake_timer.inc()
107152

108-
def timeit(self, stmt, setup, number=None, globals=None):
153+
def timeit(self, stmt, setup, global_setup, number=None, globals=None):
109154
self.fake_timer = FakeTimer()
110155
t = timeit.Timer(stmt=stmt, setup=setup, timer=self.fake_timer,
111-
globals=globals)
156+
globals=globals, global_setup=global_setup)
112157
kwargs = {}
113158
if number is None:
114159
number = DEFAULT_NUMBER
115160
else:
116161
kwargs['number'] = number
117162
delta_time = t.timeit(**kwargs)
163+
self.assertEqual(self.fake_timer.global_setup_calls, 1)
118164
self.assertEqual(self.fake_timer.setup_calls, 1)
119165
self.assertEqual(self.fake_timer.count, number)
120166
self.assertEqual(delta_time, number)
@@ -123,21 +169,20 @@ def timeit(self, stmt, setup, number=None, globals=None):
123169
#def test_timeit_default_iters(self):
124170
# self.timeit(self.fake_stmt, self.fake_setup)
125171

126-
def test_timeit_zero_iters(self):
127-
self.timeit(self.fake_stmt, self.fake_setup, number=0)
128-
129-
def test_timeit_few_iters(self):
130-
self.timeit(self.fake_stmt, self.fake_setup, number=3)
131-
132-
def test_timeit_callable_stmt(self):
133-
self.timeit(self.fake_callable_stmt, self.fake_setup, number=3)
134-
135-
def test_timeit_callable_setup(self):
136-
self.timeit(self.fake_stmt, self.fake_callable_setup, number=3)
137-
138-
def test_timeit_callable_stmt_and_setup(self):
139-
self.timeit(self.fake_callable_stmt,
140-
self.fake_callable_setup, number=3)
172+
def test_timeit_simple(self):
173+
for number, (callable_stmt, callable_setup, callable_global_setup) in (
174+
itertools.product([0, 3], itertools.product([True, False], repeat=3))
175+
):
176+
with self.subTest(
177+
number=number,
178+
callable_stmt=callable_stmt,
179+
callable_setup=callable_setup,
180+
callable_global_setup=callable_global_setup,
181+
):
182+
stmt = self.fake_stmt if callable_stmt else self.fake_callable_stmt
183+
setup = self.fake_setup if callable_setup else self.fake_callable_setup
184+
global_setup = self.fake_global_setup if callable_global_setup else self.fake_callable_global_setup
185+
self.timeit(stmt, setup, global_setup, number=number)
141186

142187
# Takes too long to run in debug build.
143188
#def test_timeit_function(self):
@@ -161,9 +206,10 @@ def test_timeit_globals_args(self):
161206
timeit.timeit(stmt='local_timer.inc()', timer=local_timer,
162207
globals=locals(), number=3)
163208

164-
def repeat(self, stmt, setup, repeat=None, number=None):
209+
def repeat(self, stmt, setup, global_setup, repeat=None, number=None):
165210
self.fake_timer = FakeTimer()
166-
t = timeit.Timer(stmt=stmt, setup=setup, timer=self.fake_timer)
211+
t = timeit.Timer(stmt=stmt, setup=setup, timer=self.fake_timer,
212+
global_setup=global_setup)
167213
kwargs = {}
168214
if repeat is None:
169215
repeat = DEFAULT_REPEAT
@@ -174,6 +220,7 @@ def repeat(self, stmt, setup, repeat=None, number=None):
174220
else:
175221
kwargs['number'] = number
176222
delta_times = t.repeat(**kwargs)
223+
self.assertEqual(self.fake_timer.global_setup_calls, 1)
177224
self.assertEqual(self.fake_timer.setup_calls, repeat)
178225
self.assertEqual(self.fake_timer.count, repeat * number)
179226
self.assertEqual(delta_times, repeat * [float(number)])
@@ -182,26 +229,23 @@ def repeat(self, stmt, setup, repeat=None, number=None):
182229
#def test_repeat_default(self):
183230
# self.repeat(self.fake_stmt, self.fake_setup)
184231

185-
def test_repeat_zero_reps(self):
186-
self.repeat(self.fake_stmt, self.fake_setup, repeat=0)
187-
188-
def test_repeat_zero_iters(self):
189-
self.repeat(self.fake_stmt, self.fake_setup, number=0)
190-
191-
def test_repeat_few_reps_and_iters(self):
192-
self.repeat(self.fake_stmt, self.fake_setup, repeat=3, number=5)
193-
194-
def test_repeat_callable_stmt(self):
195-
self.repeat(self.fake_callable_stmt, self.fake_setup,
196-
repeat=3, number=5)
197-
198-
def test_repeat_callable_setup(self):
199-
self.repeat(self.fake_stmt, self.fake_callable_setup,
200-
repeat=3, number=5)
201-
202-
def test_repeat_callable_stmt_and_setup(self):
203-
self.repeat(self.fake_callable_stmt, self.fake_callable_setup,
204-
repeat=3, number=5)
232+
def test_repeat_simple(self):
233+
for repeat, (callable_stmt, callable_setup, callable_global_setup) in (
234+
itertools.product([0, 3], itertools.product([True, False], repeat=3))
235+
):
236+
# do not use number = None for repeat > 0 as it would be too large
237+
number = 5 if repeat > 0 else None
238+
with self.subTest(
239+
repeat=repeat,
240+
number=number,
241+
callable_stmt=callable_stmt,
242+
callable_setup=callable_setup,
243+
callable_global_setup=callable_global_setup,
244+
):
245+
stmt = self.fake_stmt if callable_stmt else self.fake_callable_stmt
246+
setup = self.fake_setup if callable_setup else self.fake_callable_setup
247+
global_setup = self.fake_global_setup if callable_global_setup else self.fake_callable_global_setup
248+
self.repeat(stmt, setup, global_setup, repeat=repeat, number=number)
205249

206250
# Takes too long to run in debug build.
207251
#def test_repeat_function(self):
@@ -274,6 +318,18 @@ def test_main_fixed_iters(self):
274318
s = self.run_main(seconds_per_increment=2.0, switches=['-n35'])
275319
self.assertEqual(s, "35 loops, best of 5: 2 sec per loop\n")
276320

321+
def test_main_global_setup(self):
322+
s = self.run_main(seconds_per_increment=2.0,
323+
switches=['-n35', '-g', 'print("CustomSetup")'])
324+
self.assertEqual(s, "CustomSetup\n" +
325+
"35 loops, best of 5: 2 sec per loop\n")
326+
327+
def test_main_multiple_global_setups(self):
328+
s = self.run_main(seconds_per_increment=2.0,
329+
switches=['-n35', '-g', 'a = "CustomSetup"', '-g', 'print(a)'])
330+
self.assertEqual(s, "CustomSetup\n" +
331+
"35 loops, best of 5: 2 sec per loop\n")
332+
277333
def test_main_setup(self):
278334
s = self.run_main(seconds_per_increment=2.0,
279335
switches=['-n35', '-s', 'print("CustomSetup")'])

0 commit comments

Comments
 (0)