1818import pytest
1919
2020
21+ __all__ = ("is_debugging" , "Settings" )
22+
23+
2124HAVE_SIGALRM = hasattr (signal , "SIGALRM" )
2225if HAVE_SIGALRM :
2326 DEFAULT_METHOD = "signal"
@@ -70,6 +73,35 @@ def pytest_addoption(parser):
7073 parser .addini ("timeout_func_only" , FUNC_ONLY_DESC , type = "bool" )
7174
7275
76+ class TimeoutHooks :
77+ """Timeout specific hooks."""
78+
79+ @pytest .hookspec (firstresult = True )
80+ def pytest_timeout_set_timer (item , settings ):
81+ """Called at timeout setup.
82+
83+ 'item' is a pytest node to setup timeout for.
84+
85+ Can be overridden by plugins for alternative timeout implementation strategies.
86+
87+ """
88+
89+ @pytest .hookspec (firstresult = True )
90+ def pytest_timeout_cancel_timer (item ):
91+ """Called at timeout teardown.
92+
93+ 'item' is a pytest node which was used for timeout setup.
94+
95+ Can be overridden by plugins for alternative timeout implementation strategies.
96+
97+ """
98+
99+
100+ def pytest_addhooks (pluginmanager ):
101+ """Register timeout-specific hooks."""
102+ pluginmanager .add_hookspecs (TimeoutHooks )
103+
104+
73105@pytest .hookimpl
74106def pytest_configure (config ):
75107 """Register the marker so it shows up in --markers output."""
@@ -98,12 +130,14 @@ def pytest_runtest_protocol(item):
98130 teardown, then this hook installs the timeout. Otherwise
99131 pytest_runtest_call is used.
100132 """
101- func_only = get_func_only_setting (item )
102- if func_only is False :
103- timeout_setup (item )
133+ hooks = item .config .pluginmanager .hook
134+ settings = _get_item_settings (item )
135+ is_timeout = settings .timeout is not None and settings .timeout > 0
136+ if is_timeout and settings .func_only is False :
137+ hooks .pytest_timeout_set_timer (item = item , settings = settings )
104138 yield
105- if func_only is False :
106- timeout_teardown ( item )
139+ if is_timeout and settings . func_only is False :
140+ hooks . pytest_timeout_cancel_timer ( item = item )
107141
108142
109143@pytest .hookimpl (hookwrapper = True )
@@ -113,12 +147,14 @@ def pytest_runtest_call(item):
113147 If the timeout is set on only the test function this hook installs
114148 the timeout, otherwise pytest_runtest_protocol is used.
115149 """
116- func_only = get_func_only_setting (item )
117- if func_only is True :
118- timeout_setup (item )
150+ hooks = item .config .pluginmanager .hook
151+ settings = _get_item_settings (item )
152+ is_timeout = settings .timeout is not None and settings .timeout > 0
153+ if is_timeout and settings .func_only is True :
154+ hooks .pytest_timeout_set_timer (item = item , settings = settings )
119155 yield
120- if func_only is True :
121- timeout_teardown ( item )
156+ if is_timeout and settings . func_only is True :
157+ hooks . pytest_timeout_cancel_timer ( item = item )
122158
123159
124160@pytest .hookimpl (tryfirst = True )
@@ -138,7 +174,8 @@ def pytest_report_header(config):
138174@pytest .hookimpl (tryfirst = True )
139175def pytest_exception_interact (node ):
140176 """Stop the timeout when pytest enters pdb in post-mortem mode."""
141- timeout_teardown (node )
177+ hooks = node .config .pluginmanager .hook
178+ hooks .pytest_timeout_cancel_timer (item = node )
142179
143180
144181@pytest .hookimpl
@@ -187,13 +224,10 @@ def is_debugging(trace_func=None):
187224SUPPRESS_TIMEOUT = False
188225
189226
190- def timeout_setup (item ):
227+ @pytest .hookimpl (trylast = True )
228+ def pytest_timeout_set_timer (item , settings ):
191229 """Setup up a timeout trigger and handler."""
192- params = get_params (item )
193- if params .timeout is None or params .timeout <= 0 :
194- return
195-
196- timeout_method = params .method
230+ timeout_method = settings .method
197231 if (
198232 timeout_method == "signal"
199233 and threading .current_thread () is not threading .main_thread ()
@@ -204,17 +238,19 @@ def timeout_setup(item):
204238
205239 def handler (signum , frame ):
206240 __tracebackhide__ = True
207- timeout_sigalrm (item , params .timeout )
241+ timeout_sigalrm (item , settings .timeout )
208242
209243 def cancel ():
210244 signal .setitimer (signal .ITIMER_REAL , 0 )
211245 signal .signal (signal .SIGALRM , signal .SIG_DFL )
212246
213247 item .cancel_timeout = cancel
214248 signal .signal (signal .SIGALRM , handler )
215- signal .setitimer (signal .ITIMER_REAL , params .timeout )
249+ signal .setitimer (signal .ITIMER_REAL , settings .timeout )
216250 elif timeout_method == "thread" :
217- timer = threading .Timer (params .timeout , timeout_timer , (item , params .timeout ))
251+ timer = threading .Timer (
252+ settings .timeout , timeout_timer , (item , settings .timeout )
253+ )
218254 timer .name = "%s %s" % (__name__ , item .nodeid )
219255
220256 def cancel ():
@@ -223,9 +259,11 @@ def cancel():
223259
224260 item .cancel_timeout = cancel
225261 timer .start ()
262+ return True
226263
227264
228- def timeout_teardown (item ):
265+ @pytest .hookimpl (trylast = True )
266+ def pytest_timeout_cancel_timer (item ):
229267 """Cancel the timeout trigger if it was set."""
230268 # When skipping is raised from a pytest_runtest_setup function
231269 # (as is the case when using the pytest.mark.skipif marker) we
@@ -234,6 +272,7 @@ def timeout_teardown(item):
234272 cancel = getattr (item , "cancel_timeout" , None )
235273 if cancel :
236274 cancel ()
275+ return True
237276
238277
239278def get_env_settings (config ):
@@ -268,21 +307,7 @@ def get_env_settings(config):
268307 return Settings (timeout , method , func_only or False )
269308
270309
271- def get_func_only_setting (item ):
272- """Return the func_only setting for an item."""
273- func_only = None
274- marker = item .get_closest_marker ("timeout" )
275- if marker :
276- settings = get_params (item , marker = marker )
277- func_only = _validate_func_only (settings .func_only , "marker" )
278- if func_only is None :
279- func_only = item .config ._env_timeout_func_only
280- if func_only is None :
281- func_only = False
282- return func_only
283-
284-
285- def get_params (item , marker = None ):
310+ def _get_item_settings (item , marker = None ):
286311 """Return (timeout, method) for an item."""
287312 timeout = method = func_only = None
288313 if not marker :
@@ -298,6 +323,8 @@ def get_params(item, marker=None):
298323 method = item .config ._env_timeout_method
299324 if func_only is None :
300325 func_only = item .config ._env_timeout_func_only
326+ if func_only is None :
327+ func_only = False
301328 return Settings (timeout , method , func_only )
302329
303330
0 commit comments