Skip to content

Commit 26c22fd

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

File tree

5 files changed

+202
-66
lines changed

5 files changed

+202
-66
lines changed

Doc/library/timeit.rst

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,42 @@ 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)
65+
.. function:: timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None,
66+
*,
67+
global_setup='pass')
6668
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.
69+
Create a :class:`Timer` instance with the given statement, *setup* code,
70+
*global_setup* global code and *timer* function and run its :meth:`.timeit`
71+
method with the given *number* executions.
6972
The optional *globals* argument specifies a namespace in which to execute the
7073
code.
7174

7275
.. versionchanged:: 3.5
7376
The optional *globals* parameter was added.
7477

78+
.. versionchanged:: next
79+
The optional *global_setup* parameter was added.
7580

76-
.. function:: repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None)
7781

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.
82+
.. function:: repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None
83+
*,
84+
global_setup='pass')
85+
86+
Create a :class:`Timer` instance with the given statement, *setup* code,
87+
*global_setup* global code and *timer* function and run its :meth:`.repeat`
88+
method with the given *repeat* count and *number* executions.
89+
The optional *globals* argument specifies a namespace in which to execute the
90+
code.
8291

8392
.. versionchanged:: 3.5
8493
The optional *globals* parameter was added.
8594

8695
.. versionchanged:: 3.7
8796
Default value of *repeat* changed from 3 to 5.
8897

98+
.. versionchanged:: next
99+
The optional *global_setup* parameter was added.
100+
89101

90102
.. function:: default_timer()
91103

@@ -96,36 +108,53 @@ The module defines three convenience functions and a public class:
96108
:func:`time.perf_counter` is now the default timer.
97109

98110

99-
.. class:: Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None)
111+
.. class:: Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None,
112+
*,
113+
global_setup='pass')
100114
101115
Class for timing execution speed of small code snippets.
102116

103117
The constructor takes a statement to be timed, an additional statement used
104-
for setup, and a timer function. Both statements default to ``'pass'``;
118+
for setup and global setup, and a timer function.
119+
Statements default to ``'pass'``;
105120
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
121+
122+
*stmt*, *setup* and *global_setup* may also contain multiple statements
123+
separated by ``;`` or newlines, as long as they don't contain multi-line
124+
string literals.
125+
126+
The statement will by default be executed within timeit's namespace; this behavior
109127
can be controlled by passing a namespace to *globals*.
110128

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

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

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

122147
.. versionchanged:: 3.5
123148
The optional *globals* parameter was added.
124149

150+
.. versionchanged:: next
151+
The optional *global_setup* parameter was added.
152+
125153
.. method:: Timer.timeit(number=1000000)
126154

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
155+
Time *number* executions of the main statement.
156+
157+
This executes the setup statement once, and then returns the time it takes to execute the main
129158
statement a number of times. The default timer returns seconds as a float.
130159
The argument is the number of times through the loop, defaulting to one
131160
million. The main statement, the setup statement and the timer function
@@ -208,7 +237,7 @@ Command-Line Interface
208237

209238
When called as a program from the command line, the following form is used::
210239

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

213242
Where the following options are understood:
214243

@@ -224,8 +253,14 @@ Where the following options are understood:
224253

225254
.. option:: -s S, --setup=S
226255

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

262+
.. versionadded:: next
263+
229264
.. option:: -p, --process
230265

231266
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)