diff --git a/procrunner/__init__.py b/procrunner/__init__.py index eaa74ad..947f09e 100644 --- a/procrunner/__init__.py +++ b/procrunner/__init__.py @@ -347,6 +347,15 @@ class ReturnObject(dict, _ReturnObjectParent): + """ + A subprocess.CompletedProcess-like object containing the executed + command, stdout and stderr (both as bytestrings), and the exitcode. + Further values such as process runtime can be accessed as dictionary + values. + The check_returncode() function raises an exception if the process + exited with a non-zero exit code. + """ + def __init__(self, *arg, **kw): super(ReturnObject, self).__init__(*arg, **kw) self.args = self["command"] @@ -394,8 +403,9 @@ extension. :param string working_directory: If specified, run the executable from within this working directory. - :return: A dictionary containing stdout, stderr (both as bytestrings), - runtime, exitcode, and more. + :return: A ReturnObject() containing the executed command, stdout and stderr + (both as bytestrings), and the exitcode. Further values such as + process runtime can be accessed as dictionary values. """ time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) @@ -543,16 +553,18 @@ stderr = stderr.get_output() time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) - result = { - "exitcode": p.returncode, - "command": command, - "stdout": stdout, - "stderr": stderr, - "timeout": timeout_encountered, - "runtime": runtime, - "time_start": time_start, - "time_end": time_end, - } + result = ReturnObject( + { + "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( { @@ -576,16 +588,18 @@ time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) logger.info("run_process is disabled. Requested command: %s", command) - result = { - "exitcode": 0, - "command": command, - "stdout": "", - "stderr": "", - "timeout": False, - "runtime": 0, - "time_start": time_start, - "time_end": time_start, - } + result = ReturnObject( + { + "exitcode": 0, + "command": command, + "stdout": "", + "stderr": "", + "timeout": False, + "runtime": 0, + "time_start": time_start, + "time_end": time_start, + } + ) if kwargs.get("stdin") is not None: result.update( {"stdin_bytes_sent": len(kwargs["stdin"]), "stdin_bytes_remain": 0} diff --git a/tests/test_procrunner.py b/tests/test_procrunner.py index 9704c1c..c645968 100644 --- a/tests/test_procrunner.py +++ b/tests/test_procrunner.py @@ -94,7 +94,12 @@ ) assert not mock_process.terminate.called assert not mock_process.kill.called - assert actual == expected + for key in expected: + assert actual[key] == expected[key] + assert actual.args == tuple(command) + assert actual.returncode == mock_process.returncode + assert actual.stdout == mock.sentinel.proc_stdout + assert actual.stderr == mock.sentinel.proc_stderr @mock.patch("procrunner.subprocess") diff --git a/tests/test_procrunner_system.py b/tests/test_procrunner_system.py index ccd2b02..f4fd500 100644 --- a/tests/test_procrunner_system.py +++ b/tests/test_procrunner_system.py @@ -15,9 +15,9 @@ result = procrunner.run(command) - assert result["exitcode"] == 0 - assert result["stdout"] == b"hello" + os.linesep.encode("utf-8") - assert result["stderr"] == b"" + assert result.returncode == 0 + assert result.stdout == b"hello" + os.linesep.encode("utf-8") + assert result.stderr == b"" def test_decode_invalid_utf8_input(capsys): @@ -28,13 +28,13 @@ else: command = ["cat"] result = procrunner.run(command, stdin=test_string) - assert result["exitcode"] == 0 - assert not result["stderr"] + assert result.returncode == 0 + assert not result.stderr if os.name == "nt": # Windows modifies line endings - assert result["stdout"] == test_string[:-1] + b"\r\n" + assert result.stdout == test_string[:-1] + b"\r\n" else: - assert result["stdout"] == test_string + assert result.stdout == test_string out, err = capsys.readouterr() assert out == u"test\ufffdstring\n" assert err == u"" @@ -49,21 +49,20 @@ if e.errno == 2: pytest.skip("wget not available") raise - assert result["exitcode"] == 0 - assert b"http" in result["stderr"] - assert b"google" in result["stdout"] + assert result.returncode == 0 + assert b"http" in result.stderr + assert b"google" in result.stdout def test_path_object_resolution(tmpdir): - sentinel_value = "sentinel" + sentinel_value = b"sentinel" tmpdir.join("tempfile").write(sentinel_value) tmpdir.join("reader.py").write("print(open('tempfile').read())") - command = [sys.executable, tmpdir.join("reader.py")] result = procrunner.run( - command, + [sys.executable, tmpdir.join("reader.py")], environment_override={"PYTHONHASHSEED": "random"}, working_directory=tmpdir, ) - assert result["exitcode"] == 0 - assert not result["stderr"] - assert sentinel_value == result["stdout"].strip().decode() + assert result.returncode == 0 + assert not result.stderr + assert sentinel_value == result.stdout.strip()