Skip to content

Commit b47ecc7

Browse files
committed
Timeouts will now always raise TimeoutExpired
regardless of the raise_timeout_exception argument. This also deprecates the raise_timeout_exception argument. If that is still set to False a UserWarning is emitted. If it is set to True a DeprecationWarning is emitted.
1 parent 238e835 commit b47ecc7

File tree

4 files changed

+60
-32
lines changed

4 files changed

+60
-32
lines changed

HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ History
88
* The run() function now returns a subprocess.CompletedProcess object,
99
which no longer allows array access operations
1010
(those were deprecated in `#60 <https://github.com/DiamondLightSource/python-procrunner/pull/60>`_)
11+
* The run() argument 'raise_timeout_exception' is now set by default,
12+
a 'False' value will lead to a UserWarning and a behavioural change.
13+
The argument is now deprecated and will be removed in a future version.
14+
(previously introduced in `#61 <https://github.com/DiamondLightSource/python-procrunner/pull/61>`_)
1115
* Calling the run() function with multiple unnamed arguments is no longer supported
1216
(previously deprecated in `#62 <https://github.com/DiamondLightSource/python-procrunner/pull/62>`_)
1317
* The run() function no longer accepts a 'debug' argument

src/procrunner/__init__.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import warnings
1414
from multiprocessing import Pipe
1515
from threading import Thread
16-
from typing import Callable, Optional
16+
from typing import Any, Callable, Optional
1717

1818
#
1919
# run() - A function to synchronously run an external process, supporting
@@ -313,7 +313,7 @@ def run(
313313
environment_override: Optional[dict[str, str]] = None,
314314
win32resolve: bool = True,
315315
working_directory: Optional[str] = None,
316-
raise_timeout_exception: bool = False,
316+
raise_timeout_exception: Any = ...,
317317
) -> subprocess.CompletedProcess:
318318
"""
319319
Run an external process.
@@ -339,11 +339,7 @@ def run(
339339
extension.
340340
:param string working_directory: If specified, run the executable from
341341
within this working directory.
342-
:param boolean raise_timeout_exception: Forward compatibility flag. If set
343-
then a subprocess.TimeoutExpired exception is raised
344-
instead of returning an object that can be checked
345-
for a timeout condition. Defaults to False, will be
346-
changed to True in a future release.
342+
:param boolean raise_timeout_exception: Deprecated compatibility flag.
347343
:return: The exit code, stdout, stderr (separately, as byte strings)
348344
as a subprocess.CompletedProcess object.
349345
"""
@@ -359,12 +355,18 @@ def run(
359355
start_time = timeit.default_timer()
360356
if timeout is not None:
361357
max_time = start_time + timeout
362-
if not raise_timeout_exception:
363-
warnings.warn(
364-
"Using procrunner with timeout and without raise_timeout_exception set is deprecated",
365-
DeprecationWarning,
366-
stacklevel=3,
367-
)
358+
if not raise_timeout_exception:
359+
warnings.warn(
360+
"Using procrunner with raise_timeout_exception=False is no longer supported",
361+
UserWarning,
362+
stacklevel=3,
363+
)
364+
elif raise_timeout_exception is True:
365+
warnings.warn(
366+
"The raise_timeout_exception argument is deprecated and will be removed in a future release",
367+
DeprecationWarning,
368+
stacklevel=3,
369+
)
368370

369371
if environment is not None:
370372
env = {key: _path_resolve(environment[key]) for key in environment}
@@ -503,7 +505,7 @@ def run(
503505
output_stdout = stdout.get_output()
504506
output_stderr = stderr.get_output()
505507

506-
if timeout is not None and timeout_encountered and raise_timeout_exception:
508+
if timeout is not None and timeout_encountered:
507509
raise subprocess.TimeoutExpired(
508510
cmd=command, timeout=timeout, output=output_stdout, stderr=output_stderr
509511
)

tests/test_procrunner.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_run_command_aborts_after_timeout(
4747
task = ["___"]
4848

4949
with pytest.raises(RuntimeError):
50-
procrunner.run(task, timeout=-1, raise_timeout_exception=True)
50+
procrunner.run(task, timeout=-1)
5151

5252
assert mock_subprocess.Popen.called
5353
assert mock_process.terminate.called
@@ -81,7 +81,6 @@ def streamreader_processing(*args, **kwargs):
8181
callback_stdout=mock.sentinel.callback_stdout,
8282
callback_stderr=mock.sentinel.callback_stderr,
8383
working_directory=pathlib.Path("somecwd"),
84-
raise_timeout_exception=True,
8584
)
8685

8786
assert mock_subprocess.Popen.called
@@ -122,7 +121,7 @@ def streamreader_processing(*args, **kwargs):
122121
def test_default_process_environment_is_parent_environment(mock_subprocess):
123122
mock_subprocess.Popen.side_effect = NotImplementedError() # cut calls short
124123
with pytest.raises(NotImplementedError):
125-
procrunner.run([mock.Mock()], timeout=-1, raise_timeout_exception=True)
124+
procrunner.run([mock.Mock()], timeout=-1)
126125
assert mock_subprocess.Popen.call_args[1]["env"] == os.environ
127126

128127

@@ -136,7 +135,6 @@ def test_pass_custom_environment_to_process(mock_subprocess):
136135
[mock.Mock()],
137136
timeout=-1,
138137
environment=copy.copy(mock_env),
139-
raise_timeout_exception=True,
140138
)
141139
assert mock_subprocess.Popen.call_args[1]["env"] == mock_env
142140

@@ -153,7 +151,6 @@ def test_pass_custom_environment_to_process_and_add_another_value(mock_subproces
153151
timeout=-1,
154152
environment=copy.copy(mock_env1),
155153
environment_override=copy.copy(mock_env2),
156-
raise_timeout_exception=True,
157154
)
158155
mock_env_sum = copy.copy(mock_env1)
159156
mock_env_sum.update({key: str(mock_env2[key]) for key in mock_env2})
@@ -169,7 +166,6 @@ def test_use_default_process_environment_and_add_another_value(mock_subprocess):
169166
[mock.Mock()],
170167
timeout=-1,
171168
environment_override=copy.copy(mock_env2),
172-
raise_timeout_exception=True,
173169
)
174170
random_environment_variable = list(os.environ)[0]
175171
if random_environment_variable == list(mock_env2)[0]:
@@ -199,7 +195,6 @@ def test_use_default_process_environment_and_override_a_value(mock_subprocess):
199195
environment_override={
200196
random_environment_variable: "X" + random_environment_value
201197
},
202-
raise_timeout_exception=True,
203198
)
204199
assert (
205200
mock_subprocess.Popen.call_args[1]["env"][random_environment_variable]

tests/test_procrunner_system.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,54 @@ def test_path_object_resolution(tmp_path):
7676
), "overridden environment variable leaked into parent process"
7777

7878

79+
def test_timeout_behaviour_old_legacy(tmp_path):
80+
command = (sys.executable, "-c", "import time; time.sleep(5)")
81+
start = timeit.default_timer()
82+
try:
83+
with pytest.raises(subprocess.TimeoutExpired) as te:
84+
with pytest.warns(UserWarning, match="timeout"):
85+
procrunner.run(
86+
command,
87+
timeout=0.1,
88+
working_directory=tmp_path,
89+
raise_timeout_exception=False,
90+
)
91+
except RuntimeError:
92+
# This test sometimes fails with a RuntimeError.
93+
runtime = timeit.default_timer() - start
94+
assert runtime < 3
95+
return
96+
runtime = timeit.default_timer() - start
97+
assert runtime < 3
98+
assert te.value.stdout == b""
99+
assert te.value.stderr == b""
100+
assert te.value.timeout == 0.1
101+
assert te.value.cmd == command
102+
103+
79104
def test_timeout_behaviour_legacy(tmp_path):
105+
command = (sys.executable, "-c", "import time; time.sleep(5)")
80106
start = timeit.default_timer()
81107
try:
82-
with pytest.warns(DeprecationWarning, match="timeout"):
83-
result = procrunner.run(
84-
[sys.executable, "-c", "import time; time.sleep(5)"],
85-
timeout=0.1,
86-
working_directory=tmp_path,
87-
raise_timeout_exception=False,
88-
)
108+
with pytest.raises(subprocess.TimeoutExpired) as te:
109+
with pytest.warns(DeprecationWarning, match="timeout"):
110+
procrunner.run(
111+
command,
112+
timeout=0.1,
113+
working_directory=tmp_path,
114+
raise_timeout_exception=True,
115+
)
89116
except RuntimeError:
90117
# This test sometimes fails with a RuntimeError.
91118
runtime = timeit.default_timer() - start
92119
assert runtime < 3
93120
return
94121
runtime = timeit.default_timer() - start
95122
assert runtime < 3
96-
assert not result.stdout
97-
assert not result.stderr
98-
assert result.returncode
123+
assert te.value.stdout == b""
124+
assert te.value.stderr == b""
125+
assert te.value.timeout == 0.1
126+
assert te.value.cmd == command
99127

100128

101129
def test_timeout_behaviour(tmp_path):
@@ -107,7 +135,6 @@ def test_timeout_behaviour(tmp_path):
107135
command,
108136
timeout=0.1,
109137
working_directory=tmp_path,
110-
raise_timeout_exception=True,
111138
)
112139
except RuntimeError:
113140
# This test sometimes fails with a RuntimeError.

0 commit comments

Comments
 (0)