Skip to content

Commit 1774296

Browse files
committed
Remove ReturnObject(), completing #60
1 parent adcf6f1 commit 1774296

File tree

4 files changed

+18
-208
lines changed

4 files changed

+18
-208
lines changed

HISTORY.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ History
55
3.0.0 (2022-01-??)
66
------------------
77
* Drop Python 3.6 support
8+
* The run() function now returns a subprocess.CompletedProcess object,
9+
which no longer allows array access operations
10+
(those were deprecated in `#60 <https://github.com/DiamondLightSource/python-procrunner/pull/60>`_)
811

912
2.3.1 (2021-10-25)
1013
------------------

src/procrunner/__init__.py

Lines changed: 4 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,12 @@
3939
#
4040
# Returns:
4141
#
42-
# ReturnObject(
42+
# subprocess.CompletedProcess(
4343
# args=('/bin/ls', '/some/path/containing spaces'),
4444
# returncode=2,
4545
# stdout=b'',
4646
# stderr=b'/bin/ls: cannot access /some/path/containing spaces: No such file or directory\n'
4747
# )
48-
#
49-
# which also offers (albeit deprecated)
50-
#
51-
# result.runtime == 0.12990689277648926
52-
# result.time_end == '2017-11-12 19:54:49 GMT'
53-
# result.time_start == '2017-11-12 19:54:49 GMT'
54-
# result.timeout == False
5548

5649
__version__ = "2.3.1"
5750

@@ -307,114 +300,6 @@ def _windows_resolve(command, path=None):
307300
return command
308301

309302

310-
class ReturnObject(subprocess.CompletedProcess):
311-
"""
312-
A subprocess.CompletedProcess-like object containing the executed
313-
command, stdout and stderr (both as bytestrings), and the exitcode.
314-
The check_returncode() function raises an exception if the process
315-
exited with a non-zero exit code.
316-
"""
317-
318-
def __init__(self, exitcode=None, command=None, stdout=None, stderr=None, **kw):
319-
super().__init__(
320-
args=command, returncode=exitcode, stdout=stdout, stderr=stderr
321-
)
322-
self._extras = {
323-
"timeout": kw.get("timeout"),
324-
"runtime": kw.get("runtime"),
325-
"time_start": kw.get("time_start"),
326-
"time_end": kw.get("time_end"),
327-
}
328-
329-
def __getitem__(self, key):
330-
warnings.warn(
331-
"dictionary access to a procrunner return object is deprecated",
332-
DeprecationWarning,
333-
stacklevel=2,
334-
)
335-
if key in self._extras:
336-
return self._extras[key]
337-
if not hasattr(self, key):
338-
raise KeyError(f"Unknown attribute {key}")
339-
return getattr(self, key)
340-
341-
def __eq__(self, other):
342-
"""Override equality operator to account for added fields"""
343-
if type(other) is type(self):
344-
return self.__dict__ == other.__dict__
345-
return False
346-
347-
def __hash__(self):
348-
"""This object is not immutable, so mark it as unhashable"""
349-
return None
350-
351-
@property
352-
def cmd(self):
353-
warnings.warn(
354-
"procrunner return object .cmd is deprecated, use .args",
355-
DeprecationWarning,
356-
stacklevel=2,
357-
)
358-
return self.args
359-
360-
@property
361-
def command(self):
362-
warnings.warn(
363-
"procrunner return object .command is deprecated, use .args",
364-
DeprecationWarning,
365-
stacklevel=2,
366-
)
367-
return self.args
368-
369-
@property
370-
def exitcode(self):
371-
warnings.warn(
372-
"procrunner return object .exitcode is deprecated, use .returncode",
373-
DeprecationWarning,
374-
stacklevel=2,
375-
)
376-
return self.returncode
377-
378-
@property
379-
def timeout(self):
380-
warnings.warn(
381-
"procrunner return object .timeout is deprecated",
382-
DeprecationWarning,
383-
stacklevel=2,
384-
)
385-
return self._extras["timeout"]
386-
387-
@property
388-
def runtime(self):
389-
warnings.warn(
390-
"procrunner return object .runtime is deprecated",
391-
DeprecationWarning,
392-
stacklevel=2,
393-
)
394-
return self._extras["runtime"]
395-
396-
@property
397-
def time_start(self):
398-
warnings.warn(
399-
"procrunner return object .time_start is deprecated",
400-
DeprecationWarning,
401-
stacklevel=2,
402-
)
403-
return self._extras["time_start"]
404-
405-
@property
406-
def time_end(self):
407-
warnings.warn(
408-
"procrunner return object .time_end is deprecated",
409-
DeprecationWarning,
410-
stacklevel=2,
411-
)
412-
return self._extras["time_end"]
413-
414-
def update(self, dictionary):
415-
self._extras.update(dictionary)
416-
417-
418303
def _deprecate_argument_calling(f):
419304
@functools.wraps(f)
420305
def wrapper(*args, **kwargs):
@@ -445,7 +330,7 @@ def run(
445330
win32resolve=True,
446331
working_directory=None,
447332
raise_timeout_exception=False,
448-
):
333+
) -> subprocess.CompletedProcess:
449334
"""
450335
Run an external process.
451336
@@ -480,7 +365,6 @@ def run(
480365
as a subprocess.CompletedProcess object.
481366
"""
482367

483-
time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
484368
logger.debug("Starting external process: %s", command)
485369

486370
if stdin is None:
@@ -655,23 +539,6 @@ def run(
655539
cmd=command, timeout=timeout, output=stdout, stderr=stderr
656540
)
657541

658-
time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
659-
result = ReturnObject(
660-
exitcode=p.returncode,
661-
command=command,
662-
stdout=stdout,
663-
stderr=stderr,
664-
timeout=timeout_encountered,
665-
runtime=runtime,
666-
time_start=time_start,
667-
time_end=time_end,
542+
return subprocess.CompletedProcess(
543+
args=command, returncode=p.returncode, stdout=stdout, stderr=stderr
668544
)
669-
if stdin is not None:
670-
result.update(
671-
{
672-
"stdin_bytes_sent": stdin.bytes_sent(),
673-
"stdin_bytes_remain": stdin.bytes_remaining(),
674-
}
675-
)
676-
677-
return result

tests/test_procrunner.py

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import copy
24
import os
35
import pathlib
@@ -73,17 +75,6 @@ def streamreader_processing(*args, **kwargs):
7375
mock_streamreader.side_effect = streamreader_processing
7476
mock_subprocess.Popen.return_value = mock_process
7577

76-
expected = {
77-
"stderr": mock.sentinel.proc_stderr,
78-
"stdout": mock.sentinel.proc_stdout,
79-
"exitcode": mock_process.returncode,
80-
"command": tuple(command),
81-
"runtime": mock.ANY,
82-
"timeout": False,
83-
"time_start": mock.ANY,
84-
"time_end": mock.ANY,
85-
}
86-
8778
actual = procrunner.run(
8879
command,
8980
timeout=0.5,
@@ -120,13 +111,13 @@ def streamreader_processing(*args, **kwargs):
120111
)
121112
assert not mock_process.terminate.called
122113
assert not mock_process.kill.called
123-
for key in expected:
124-
with pytest.warns(DeprecationWarning):
125-
assert actual[key] == expected[key]
126-
assert actual.args == tuple(command)
127-
assert actual.returncode == mock_process.returncode
128-
assert actual.stdout == mock.sentinel.proc_stdout
129-
assert actual.stderr == mock.sentinel.proc_stderr
114+
assert actual == mock_subprocess.CompletedProcess.return_value
115+
mock_subprocess.CompletedProcess.assert_called_once_with(
116+
args=tuple(command),
117+
returncode=mock_process.returncode,
118+
stdout=mock.sentinel.proc_stdout,
119+
stderr=mock.sentinel.proc_stderr,
120+
)
130121

131122

132123
@mock.patch("procrunner.subprocess")
@@ -304,54 +295,3 @@ def test_lineaggregator_aggregates_data():
304295
callback.assert_not_called()
305296
aggregator.flush()
306297
callback.assert_called_once_with("morestuff")
307-
308-
309-
def test_return_object_semantics():
310-
ro = procrunner.ReturnObject(
311-
command=mock.sentinel.command,
312-
exitcode=0,
313-
stdout=mock.sentinel.stdout,
314-
stderr=mock.sentinel.stderr,
315-
)
316-
with pytest.warns(DeprecationWarning):
317-
assert ro["command"] == mock.sentinel.command
318-
assert ro.args == mock.sentinel.command
319-
with pytest.warns(DeprecationWarning):
320-
assert ro["exitcode"] == 0
321-
assert ro.returncode == 0
322-
with pytest.warns(DeprecationWarning):
323-
assert ro["stdout"] == mock.sentinel.stdout
324-
assert ro.stdout == mock.sentinel.stdout
325-
with pytest.warns(DeprecationWarning):
326-
assert ro["stderr"] == mock.sentinel.stderr
327-
assert ro.stderr == mock.sentinel.stderr
328-
329-
with pytest.raises(KeyError):
330-
with pytest.warns(DeprecationWarning):
331-
ro["unknownkey"]
332-
ro.update({"unknownkey": mock.sentinel.key})
333-
with pytest.warns(DeprecationWarning):
334-
assert ro["unknownkey"] == mock.sentinel.key
335-
336-
337-
def test_return_object_check_function_passes_on_success():
338-
ro = procrunner.ReturnObject(
339-
command=mock.sentinel.command,
340-
exitcode=0,
341-
stdout=mock.sentinel.stdout,
342-
stderr=mock.sentinel.stderr,
343-
)
344-
ro.check_returncode()
345-
346-
347-
def test_return_object_check_function_raises_on_error():
348-
ro = procrunner.ReturnObject(
349-
command=mock.sentinel.command,
350-
exitcode=1,
351-
stdout=mock.sentinel.stdout,
352-
stderr=mock.sentinel.stderr,
353-
)
354-
with pytest.raises(Exception) as e:
355-
ro.check_returncode()
356-
assert repr(mock.sentinel.command) in str(e.value)
357-
assert "1" in str(e.value)

tests/test_procrunner_system.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import os
24
import subprocess
35
import sys
@@ -90,8 +92,6 @@ def test_timeout_behaviour_legacy(tmp_path):
9092
assert runtime < 3
9193
return
9294
runtime = timeit.default_timer() - start
93-
with pytest.warns(DeprecationWarning, match="\\.timeout"):
94-
assert result.timeout
9595
assert runtime < 3
9696
assert not result.stdout
9797
assert not result.stderr

0 commit comments

Comments
 (0)