diff --git a/procrunner/__init__.py b/procrunner/__init__.py index 8940ea7..267150b 100644 --- a/procrunner/__init__.py +++ b/procrunner/__init__.py @@ -8,6 +8,7 @@ import sys import time import timeit +import warnings from multiprocessing import Pipe from threading import Thread @@ -301,7 +302,7 @@ return command -class ReturnObject(dict, subprocess.CompletedProcess): +class ReturnObject(subprocess.CompletedProcess): """ A subprocess.CompletedProcess-like object containing the executed command, stdout and stderr (both as bytestrings), and the exitcode. @@ -311,12 +312,28 @@ exited with a non-zero exit code. """ - def __init__(self, *arg, **kw): - super().__init__(*arg, **kw) - self.args = self["command"] - self.returncode = self["exitcode"] - self.stdout = self["stdout"] - self.stderr = self["stderr"] + def __init__(self, exitcode=None, command=None, stdout=None, stderr=None, **kw): + super().__init__( + args=command, returncode=exitcode, stdout=stdout, stderr=stderr + ) + self._extras = { + "timeout": kw.get("timeout"), + "runtime": kw.get("runtime"), + "time_start": kw.get("time_start"), + "time_end": kw.get("time_end"), + } + + def __getitem__(self, key): + warnings.warn( + "dictionary access to a procrunner return object is deprecated", + DeprecationWarning, + stacklevel=2, + ) + if key in self._extras: + return self._extras[key] + if not hasattr(self, key): + raise KeyError(f"Unknown attribute {key}") + return getattr(self, key) def __eq__(self, other): """Override equality operator to account for added fields""" @@ -328,9 +345,71 @@ """This object is not immutable, so mark it as unhashable""" return None - def __ne__(self, other): - """Overrides the default implementation (unnecessary in Python 3)""" - return not self.__eq__(other) + @property + def cmd(self): + warnings.warn( + "procrunner return object .cmd is deprecated, use .args", + DeprecationWarning, + stacklevel=2, + ) + return self.args + + @property + def command(self): + warnings.warn( + "procrunner return object .command is deprecated, use .args", + DeprecationWarning, + stacklevel=2, + ) + return self.args + + @property + def exitcode(self): + warnings.warn( + "procrunner return object .exitcode is deprecated, use .returncode", + DeprecationWarning, + stacklevel=2, + ) + return self.returncode + + @property + def timeout(self): + warnings.warn( + "procrunner return object .timeout is deprecated", + DeprecationWarning, + stacklevel=2, + ) + return self._extras["timeout"] + + @property + def runtime(self): + warnings.warn( + "procrunner return object .runtime is deprecated", + DeprecationWarning, + stacklevel=2, + ) + return self._extras["runtime"] + + @property + def time_start(self): + warnings.warn( + "procrunner return object .time_start is deprecated", + DeprecationWarning, + stacklevel=2, + ) + return self._extras["time_start"] + + @property + def time_end(self): + warnings.warn( + "procrunner return object .time_end is deprecated", + DeprecationWarning, + stacklevel=2, + ) + return self._extras["time_end"] + + def update(self, dictionary): + self._extras.update(dictionary) def run( @@ -522,16 +601,14 @@ time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) result = ReturnObject( - { - "exitcode": p.returncode, - "command": command, - "stdout": stdout, - "stderr": stderr, - "timeout": timeout_encountered, - "runtime": runtime, - "time_start": time_start, - "time_end": time_end, - } + exitcode=p.returncode, + command=command, + stdout=stdout, + stderr=stderr, + timeout=timeout_encountered, + runtime=runtime, + time_start=time_start, + time_end=time_end, ) if stdin is not None: result.update( diff --git a/tests/test_procrunner.py b/tests/test_procrunner.py index b174d1d..dd0314e 100644 --- a/tests/test_procrunner.py +++ b/tests/test_procrunner.py @@ -93,7 +93,8 @@ assert not mock_process.terminate.called assert not mock_process.kill.called for key in expected: - assert actual[key] == expected[key] + with pytest.warns(DeprecationWarning): + assert actual[key] == expected[key] assert actual.args == tuple(command) assert actual.returncode == mock_process.returncode assert actual.stdout == mock.sentinel.proc_stdout @@ -260,48 +261,48 @@ def test_return_object_semantics(): ro = procrunner.ReturnObject( - { - "command": mock.sentinel.command, - "exitcode": 0, - "stdout": mock.sentinel.stdout, - "stderr": mock.sentinel.stderr, - } - ) - assert ro["command"] == mock.sentinel.command + command=mock.sentinel.command, + exitcode=0, + stdout=mock.sentinel.stdout, + stderr=mock.sentinel.stderr, + ) + with pytest.warns(DeprecationWarning): + assert ro["command"] == mock.sentinel.command assert ro.args == mock.sentinel.command - assert ro["exitcode"] == 0 + with pytest.warns(DeprecationWarning): + assert ro["exitcode"] == 0 assert ro.returncode == 0 - assert ro["stdout"] == mock.sentinel.stdout + with pytest.warns(DeprecationWarning): + assert ro["stdout"] == mock.sentinel.stdout assert ro.stdout == mock.sentinel.stdout - assert ro["stderr"] == mock.sentinel.stderr + with pytest.warns(DeprecationWarning): + assert ro["stderr"] == mock.sentinel.stderr assert ro.stderr == mock.sentinel.stderr with pytest.raises(KeyError): - ro["unknownkey"] + with pytest.warns(DeprecationWarning): + ro["unknownkey"] ro.update({"unknownkey": mock.sentinel.key}) - assert ro["unknownkey"] == mock.sentinel.key + with pytest.warns(DeprecationWarning): + assert ro["unknownkey"] == mock.sentinel.key def test_return_object_check_function_passes_on_success(): ro = procrunner.ReturnObject( - { - "command": mock.sentinel.command, - "exitcode": 0, - "stdout": mock.sentinel.stdout, - "stderr": mock.sentinel.stderr, - } + command=mock.sentinel.command, + exitcode=0, + stdout=mock.sentinel.stdout, + stderr=mock.sentinel.stderr, ) ro.check_returncode() def test_return_object_check_function_raises_on_error(): ro = procrunner.ReturnObject( - { - "command": mock.sentinel.command, - "exitcode": 1, - "stdout": mock.sentinel.stdout, - "stderr": mock.sentinel.stderr, - } + command=mock.sentinel.command, + exitcode=1, + stdout=mock.sentinel.stdout, + stderr=mock.sentinel.stderr, ) with pytest.raises(Exception) as e: ro.check_returncode()