Codebase list python-procrunner / c13aeeb
Merge pull request #21 from DiamondLightSource/SubprocessCompleted Change the procrunner return object to a compatible derivate of the Python 3.5 subprocess.CompletedProcess object Markus Gerstel authored 5 years ago GitHub committed 5 years ago
4 changed file(s) with 134 addition(s) and 39 deletion(s). Raw diff Collapse all Expand all
55 ------------------
66
77 * Support file system path objects (PEP-519) in arguments
8 * Change the return object to make it similar to
9 subprocess.CompletedProcess, introduced with Python 3.5+
810
911 0.9.1 (2019-02-22)
1012 ------------------
330330 return command
331331
332332
333 if sys.version_info < (3, 5):
334
335 class _ReturnObjectParent(object):
336 def check_returncode(self):
337 if self.returncode:
338 raise Exception(
339 "Call %r resulted in non-zero exit code %r"
340 % (self.args, self.returncode)
341 )
342
343
344 else:
345 _ReturnObjectParent = subprocess.CompletedProcess
346
347
348 class ReturnObject(dict, _ReturnObjectParent):
349 """
350 A subprocess.CompletedProcess-like object containing the executed
351 command, stdout and stderr (both as bytestrings), and the exitcode.
352 Further values such as process runtime can be accessed as dictionary
353 values.
354 The check_returncode() function raises an exception if the process
355 exited with a non-zero exit code.
356 """
357
358 def __init__(self, *arg, **kw):
359 super(ReturnObject, self).__init__(*arg, **kw)
360 self.args = self["command"]
361 self.returncode = self["exitcode"]
362 self.stdout = self["stdout"]
363 self.stderr = self["stderr"]
364
365
333366 def run(
334367 command,
335368 timeout=None,
369402 extension.
370403 :param string working_directory: If specified, run the executable from
371404 within this working directory.
372 :return: A dictionary containing stdout, stderr (both as bytestrings),
373 runtime, exitcode, and more.
405 :return: A ReturnObject() containing the executed command, stdout and stderr
406 (both as bytestrings), and the exitcode. Further values such as
407 process runtime can be accessed as dictionary values.
374408 """
375409
376410 time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
518552 stderr = stderr.get_output()
519553 time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
520554
521 result = {
522 "exitcode": p.returncode,
523 "command": command,
524 "stdout": stdout,
525 "stderr": stderr,
526 "timeout": timeout_encountered,
527 "runtime": runtime,
528 "time_start": time_start,
529 "time_end": time_end,
530 }
555 result = ReturnObject(
556 {
557 "exitcode": p.returncode,
558 "command": command,
559 "stdout": stdout,
560 "stderr": stderr,
561 "timeout": timeout_encountered,
562 "runtime": runtime,
563 "time_start": time_start,
564 "time_end": time_end,
565 }
566 )
531567 if stdin is not None:
532568 result.update(
533569 {
551587 time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
552588 logger.info("run_process is disabled. Requested command: %s", command)
553589
554 result = {
555 "exitcode": 0,
556 "command": command,
557 "stdout": "",
558 "stderr": "",
559 "timeout": False,
560 "runtime": 0,
561 "time_start": time_start,
562 "time_end": time_start,
563 }
590 result = ReturnObject(
591 {
592 "exitcode": 0,
593 "command": command,
594 "stdout": "",
595 "stderr": "",
596 "timeout": False,
597 "runtime": 0,
598 "time_start": time_start,
599 "time_end": time_start,
600 }
601 )
564602 if kwargs.get("stdin") is not None:
565603 result.update(
566604 {"stdin_bytes_sent": len(kwargs["stdin"]), "stdin_bytes_remain": 0}
9393 )
9494 assert not mock_process.terminate.called
9595 assert not mock_process.kill.called
96 assert actual == expected
96 for key in expected:
97 assert actual[key] == expected[key]
98 assert actual.args == tuple(command)
99 assert actual.returncode == mock_process.returncode
100 assert actual.stdout == mock.sentinel.proc_stdout
101 assert actual.stderr == mock.sentinel.proc_stderr
97102
98103
99104 @mock.patch("procrunner.subprocess")
253258 callback.assert_not_called()
254259 aggregator.flush()
255260 callback.assert_called_once_with("morestuff")
261
262
263 def test_return_object_semantics():
264 ro = procrunner.ReturnObject(
265 {
266 "command": mock.sentinel.command,
267 "exitcode": 0,
268 "stdout": mock.sentinel.stdout,
269 "stderr": mock.sentinel.stderr,
270 }
271 )
272 assert ro["command"] == mock.sentinel.command
273 assert ro.args == mock.sentinel.command
274 assert ro["exitcode"] == 0
275 assert ro.returncode == 0
276 assert ro["stdout"] == mock.sentinel.stdout
277 assert ro.stdout == mock.sentinel.stdout
278 assert ro["stderr"] == mock.sentinel.stderr
279 assert ro.stderr == mock.sentinel.stderr
280
281 with pytest.raises(KeyError):
282 ro["unknownkey"]
283 ro.update({"unknownkey": mock.sentinel.key})
284 assert ro["unknownkey"] == mock.sentinel.key
285
286
287 def test_return_object_check_function_passes_on_success():
288 ro = procrunner.ReturnObject(
289 {
290 "command": mock.sentinel.command,
291 "exitcode": 0,
292 "stdout": mock.sentinel.stdout,
293 "stderr": mock.sentinel.stderr,
294 }
295 )
296 ro.check_returncode()
297
298
299 def test_return_object_check_function_raises_on_error():
300 ro = procrunner.ReturnObject(
301 {
302 "command": mock.sentinel.command,
303 "exitcode": 1,
304 "stdout": mock.sentinel.stdout,
305 "stderr": mock.sentinel.stderr,
306 }
307 )
308 with pytest.raises(Exception) as e:
309 ro.check_returncode()
310 assert repr(mock.sentinel.command) in str(e.value)
311 assert "1" in str(e.value)
1414
1515 result = procrunner.run(command)
1616
17 assert result["exitcode"] == 0
18 assert result["stdout"] == b"hello" + os.linesep.encode("utf-8")
19 assert result["stderr"] == b""
17 assert result.returncode == 0
18 assert result.stdout == b"hello" + os.linesep.encode("utf-8")
19 assert result.stderr == b""
2020
2121
2222 def test_decode_invalid_utf8_input(capsys):
2727 else:
2828 command = ["cat"]
2929 result = procrunner.run(command, stdin=test_string)
30 assert result["exitcode"] == 0
31 assert not result["stderr"]
30 assert result.returncode == 0
31 assert not result.stderr
3232 if os.name == "nt":
3333 # Windows modifies line endings
34 assert result["stdout"] == test_string[:-1] + b"\r\n"
34 assert result.stdout == test_string[:-1] + b"\r\n"
3535 else:
36 assert result["stdout"] == test_string
36 assert result.stdout == test_string
3737 out, err = capsys.readouterr()
3838 assert out == u"test\ufffdstring\n"
3939 assert err == u""
4848 if e.errno == 2:
4949 pytest.skip("wget not available")
5050 raise
51 assert result["exitcode"] == 0
52 assert b"http" in result["stderr"]
53 assert b"google" in result["stdout"]
51 assert result.returncode == 0
52 assert b"http" in result.stderr
53 assert b"google" in result.stdout
5454
5555
5656 def test_path_object_resolution(tmpdir):
57 sentinel_value = "sentinel"
57 sentinel_value = b"sentinel"
5858 tmpdir.join("tempfile").write(sentinel_value)
5959 tmpdir.join("reader.py").write("print(open('tempfile').read())")
60 command = [sys.executable, tmpdir.join("reader.py")]
6160 result = procrunner.run(
62 command,
61 [sys.executable, tmpdir.join("reader.py")],
6362 environment_override={"PYTHONHASHSEED": "random"},
6463 working_directory=tmpdir,
6564 )
66 assert result["exitcode"] == 0
67 assert not result["stderr"]
68 assert sentinel_value == result["stdout"].strip().decode()
65 assert result.returncode == 0
66 assert not result.stderr
67 assert sentinel_value == result.stdout.strip()