From 9599348f9b668feafd0a05db012e8806cda8389f Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 12 May 2020 12:48:23 -0700 Subject: [PATCH] make trio.Process.returncode automatically update itself as needed --- newsfragments/1315.feature.rst | 4 +++ trio/_subprocess.py | 47 +++++++++++++++++++--------------- trio/tests/test_subprocess.py | 11 ++++++++ 3 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 newsfragments/1315.feature.rst diff --git a/newsfragments/1315.feature.rst b/newsfragments/1315.feature.rst new file mode 100644 index 0000000000..824a793e74 --- /dev/null +++ b/newsfragments/1315.feature.rst @@ -0,0 +1,4 @@ +The `trio.Process.returncode` attribute is now automatically updated +as needed, instead of only when you call `~trio.Process.poll` or +`~trio.Process.wait`. Also, ``repr(process_object)`` now always +contains up-to-date information about the process status. diff --git a/trio/_subprocess.py b/trio/_subprocess.py index 6983c839c7..ab6ab76a7e 100644 --- a/trio/_subprocess.py +++ b/trio/_subprocess.py @@ -133,19 +133,20 @@ def __init__(self, popen, stdin, stdout, stderr): self.pid = self._proc.pid def __repr__(self): - if self.returncode is None: + returncode = self.returncode + if returncode is None: status = "running with PID {}".format(self.pid) else: - if self.returncode < 0: - status = "exited with signal {}".format(-self.returncode) + if returncode < 0: + status = "exited with signal {}".format(-returncode) else: - status = "exited with status {}".format(self.returncode) + status = "exited with status {}".format(returncode) return "".format(self.args, status) @property def returncode(self): - """The exit status of the process (an integer), or ``None`` if it is - not yet known to have exited. + """The exit status of the process (an integer), or ``None`` if it's + still running. By convention, a return code of zero indicates success. On UNIX, negative values indicate termination due to a signal, @@ -153,10 +154,16 @@ def returncode(self): Windows, a process that exits due to a call to :meth:`Process.terminate` will have an exit status of 1. - Accessing this attribute does not check for termination; - use :meth:`poll` or :meth:`wait` for that. + Unlike the standard library `subprocess.Popen.returncode`, you don't + have to call `poll` or `wait` to update this attribute; it's + automatically updated as needed, and will always give you the latest + information. + """ - return self._proc.returncode + result = self._proc.poll() + if result is not None: + self._close_pidfd() + return result async def aclose(self): """Close any pipes we have to the process (both input and output) @@ -175,7 +182,7 @@ async def aclose(self): try: await self.wait() finally: - if self.returncode is None: + if self._proc.returncode is None: self.kill() with trio.CancelScope(shield=True): await self.wait() @@ -205,20 +212,20 @@ async def wait(self): # actually block for a tiny fraction of a second. self._proc.wait() self._close_pidfd() - assert self.returncode is not None - return self.returncode + assert self._proc.returncode is not None + return self._proc.returncode def poll(self): - """Check if the process has exited yet. + """Returns the exit status of the process (an integer), or ``None`` if + it's still running. + + Note that on Trio (unlike the standard library `subprocess.Popen`), + ``process.poll()`` and ``process.returncode`` always give the same + result. See `returncode` for more details. This method is only + included to make it easier to port code from `subprocess`. - Returns: - The exit status of the process, or ``None`` if it is still - running; see :attr:`returncode`. """ - result = self._proc.poll() - if result is not None: - self._close_pidfd() - return result + return self.returncode def send_signal(self, sig): """Send signal ``sig`` to the process. diff --git a/trio/tests/test_subprocess.py b/trio/tests/test_subprocess.py index b9290f0401..ebc838b0ed 100644 --- a/trio/tests/test_subprocess.py +++ b/trio/tests/test_subprocess.py @@ -47,6 +47,7 @@ async def test_basic(): assert repr(proc) == repr_template.format( "running with PID {}".format(proc.pid) ) + assert proc._pidfd is None assert proc.returncode == 0 assert repr(proc) == repr_template.format("exited with status 0") @@ -58,6 +59,16 @@ async def test_basic(): ) +async def test_auto_update_returncode(): + p = await open_process(SLEEP(9999)) + assert p.returncode is None + p.kill() + p._proc.wait() + assert p.returncode is not None + assert p._pidfd is None + assert p.returncode is not None + + async def test_multi_wait(): async with await open_process(SLEEP(10)) as proc: # Check that wait (including multi-wait) tolerates being cancelled