Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 65a796e

Browse files
authoredApr 1, 2020
bpo-40094: Add os.waitstatus_to_exitcode() (GH-19201)
Add os.waitstatus_to_exitcode() function to convert a wait status to an exitcode. Suggest waitstatus_to_exitcode() usage in the documentation when appropriate. Use waitstatus_to_exitcode() in: * multiprocessing, os, subprocess and _bootsubprocess modules; * test.support.wait_process(); * setup.py: run_command(); * and many tests.
1 parent 5dd8360 commit 65a796e

18 files changed

+258
-61
lines changed
 

‎Doc/library/os.rst

+56
Original file line numberDiff line numberDiff line change
@@ -3665,6 +3665,11 @@ written in Python, such as a mail server's external command delivery program.
36653665
subprocess was killed.) On Windows systems, the return value
36663666
contains the signed integer return code from the child process.
36673667

3668+
On Unix, :func:`waitstatus_to_exitcode` can be used to convert the ``close``
3669+
method result (exit status) into an exit code if it is not ``None``. On
3670+
Windows, the ``close`` method result is directly the exit code
3671+
(or ``None``).
3672+
36683673
This is implemented using :class:`subprocess.Popen`; see that class's
36693674
documentation for more powerful ways to manage and communicate with
36703675
subprocesses.
@@ -3968,6 +3973,10 @@ written in Python, such as a mail server's external command delivery program.
39683973
to using this function. See the :ref:`subprocess-replacements` section in
39693974
the :mod:`subprocess` documentation for some helpful recipes.
39703975

3976+
On Unix, :func:`waitstatus_to_exitcode` can be used to convert the result
3977+
(exit status) into an exit code. On Windows, the result is directly the exit
3978+
code.
3979+
39713980
.. audit-event:: os.system command os.system
39723981

39733982
.. availability:: Unix, Windows.
@@ -4008,8 +4017,16 @@ written in Python, such as a mail server's external command delivery program.
40084017
number is zero); the high bit of the low byte is set if a core file was
40094018
produced.
40104019

4020+
:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
4021+
exit code.
4022+
40114023
.. availability:: Unix.
40124024

4025+
.. seealso::
4026+
4027+
:func:`waitpid` can be used to wait for the completion of a specific
4028+
child process and has more options.
4029+
40134030
.. function:: waitid(idtype, id, options)
40144031

40154032
Wait for the completion of one or more child processes.
@@ -4105,6 +4122,9 @@ written in Python, such as a mail server's external command delivery program.
41054122
id is known, not necessarily a child process. The :func:`spawn\* <spawnl>`
41064123
functions called with :const:`P_NOWAIT` return suitable process handles.
41074124

4125+
:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
4126+
exit code.
4127+
41084128
.. versionchanged:: 3.5
41094129
If the system call is interrupted and the signal handler does not raise an
41104130
exception, the function now retries the system call instead of raising an
@@ -4120,6 +4140,9 @@ written in Python, such as a mail server's external command delivery program.
41204140
information. The option argument is the same as that provided to
41214141
:func:`waitpid` and :func:`wait4`.
41224142

4143+
:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
4144+
exitcode.
4145+
41234146
.. availability:: Unix.
41244147

41254148

@@ -4131,9 +4154,42 @@ written in Python, such as a mail server's external command delivery program.
41314154
resource usage information. The arguments to :func:`wait4` are the same
41324155
as those provided to :func:`waitpid`.
41334156

4157+
:func:`waitstatus_to_exitcode` can be used to convert the exit status into an
4158+
exitcode.
4159+
41344160
.. availability:: Unix.
41354161

41364162

4163+
.. function:: waitstatus_to_exitcode(status)
4164+
4165+
Convert a wait status to an exit code.
4166+
4167+
On Unix:
4168+
4169+
* If the process exited normally (if ``WIFEXITED(status)`` is true),
4170+
return the process exit status (return ``WEXITSTATUS(status)``):
4171+
result greater than or equal to 0.
4172+
* If the process was terminated by a signal (if ``WIFSIGNALED(status)`` is
4173+
true), return ``-signum`` where *signum* is the number of the signal that
4174+
caused the process to terminate (return ``-WTERMSIG(status)``):
4175+
result less than 0.
4176+
* Otherwise, raise a :exc:`ValueError`.
4177+
4178+
On Windows, return *status* shifted right by 8 bits.
4179+
4180+
On Unix, if the process is being traced or if :func:`waitpid` was called
4181+
with :data:`WUNTRACED` option, the caller must first check if
4182+
``WIFSTOPPED(status)`` is true. This function must not be called if
4183+
``WIFSTOPPED(status)`` is true.
4184+
4185+
.. seealso::
4186+
4187+
:func:`WIFEXITED`, :func:`WEXITSTATUS`, :func:`WIFSIGNALED`,
4188+
:func:`WTERMSIG`, :func:`WIFSTOPPED`, :func:`WSTOPSIG` functions.
4189+
4190+
.. versionadded:: 3.9
4191+
4192+
41374193
.. data:: WNOHANG
41384194

41394195
The option for :func:`waitpid` to return immediately if no child process status

‎Doc/library/pty.rst

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ The :mod:`pty` module defines the following functions:
6969
*select* throws an error on your platform when passed three empty lists. This
7070
is a bug, documented in `issue 26228 <https://bugs.python.org/issue26228>`_.
7171

72+
Return the exit status value from :func:`os.waitpid` on the child process.
73+
74+
:func:`waitstatus_to_exitcode` can be used to convert the exit status into
75+
an exit code.
76+
7277
.. audit-event:: pty.spawn argv pty.spawn
7378

7479
.. versionchanged:: 3.4

‎Doc/whatsnew/3.9.rst

+4
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ The :func:`os.putenv` and :func:`os.unsetenv` functions are now always
322322
available.
323323
(Contributed by Victor Stinner in :issue:`39395`.)
324324

325+
Add :func:`os.waitstatus_to_exitcode` function:
326+
convert a wait status to an exit code.
327+
(Contributed by Victor Stinner in :issue:`40094`.)
328+
325329
pathlib
326330
-------
327331

‎Lib/_bootsubprocess.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@
66
import os
77

88

9-
def _waitstatus_to_exitcode(status):
10-
if os.WIFEXITED(status):
11-
return os.WEXITSTATUS(status)
12-
elif os.WIFSIGNALED(status):
13-
return -os.WTERMSIG(status)
14-
else:
15-
raise ValueError(f"invalid wait status: {status!r}")
16-
17-
189
# distutils.spawn used by distutils.command.build_ext
1910
# calls subprocess.Popen().wait()
2011
class Popen:
@@ -37,7 +28,7 @@ def wait(self):
3728
else:
3829
# Parent process
3930
_, status = os.waitpid(pid, 0)
40-
self.returncode = _waitstatus_to_exitcode(status)
31+
self.returncode = os.waitstatus_to_exitcode(status)
4132

4233
return self.returncode
4334

@@ -87,7 +78,7 @@ def check_output(cmd, **kwargs):
8778
try:
8879
# system() spawns a shell
8980
status = os.system(cmd)
90-
exitcode = _waitstatus_to_exitcode(status)
81+
exitcode = os.waitstatus_to_exitcode(status)
9182
if exitcode:
9283
raise ValueError(f"Command {cmd!r} returned non-zero "
9384
f"exit status {exitcode!r}")

‎Lib/multiprocessing/forkserver.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,8 @@ def sigchld_handler(*_unused):
237237
break
238238
child_w = pid_to_fd.pop(pid, None)
239239
if child_w is not None:
240-
if os.WIFSIGNALED(sts):
241-
returncode = -os.WTERMSIG(sts)
242-
else:
243-
if not os.WIFEXITED(sts):
244-
raise AssertionError(
245-
"Child {0:n} status is {1:n}".format(
246-
pid,sts))
247-
returncode = os.WEXITSTATUS(sts)
240+
returncode = os.waitstatus_to_exitcode(sts)
241+
248242
# Send exit code to client process
249243
try:
250244
write_signed(child_w, returncode)

‎Lib/multiprocessing/popen_fork.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ def poll(self, flag=os.WNOHANG):
3030
# e.errno == errno.ECHILD == 10
3131
return None
3232
if pid == self.pid:
33-
if os.WIFSIGNALED(sts):
34-
self.returncode = -os.WTERMSIG(sts)
35-
else:
36-
assert os.WIFEXITED(sts), "Status is {:n}".format(sts)
37-
self.returncode = os.WEXITSTATUS(sts)
33+
self.returncode = os.waitstatus_to_exitcode(sts)
3834
return self.returncode
3935

4036
def wait(self, timeout=None):

‎Lib/os.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -864,12 +864,8 @@ def _spawnvef(mode, file, args, env, func):
864864
wpid, sts = waitpid(pid, 0)
865865
if WIFSTOPPED(sts):
866866
continue
867-
elif WIFSIGNALED(sts):
868-
return -WTERMSIG(sts)
869-
elif WIFEXITED(sts):
870-
return WEXITSTATUS(sts)
871-
else:
872-
raise OSError("Not stopped, signaled or exited???")
867+
868+
return waitstatus_to_exitcode(sts)
873869

874870
def spawnv(mode, file, args):
875871
"""spawnv(mode, file, args) -> integer

‎Lib/subprocess.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -1838,23 +1838,17 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
18381838
raise child_exception_type(err_msg)
18391839

18401840

1841-
def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
1842-
_WTERMSIG=os.WTERMSIG, _WIFEXITED=os.WIFEXITED,
1843-
_WEXITSTATUS=os.WEXITSTATUS, _WIFSTOPPED=os.WIFSTOPPED,
1844-
_WSTOPSIG=os.WSTOPSIG):
1841+
def _handle_exitstatus(self, sts,
1842+
waitstatus_to_exitcode=os.waitstatus_to_exitcode,
1843+
_WIFSTOPPED=os.WIFSTOPPED,
1844+
_WSTOPSIG=os.WSTOPSIG):
18451845
"""All callers to this function MUST hold self._waitpid_lock."""
18461846
# This method is called (indirectly) by __del__, so it cannot
18471847
# refer to anything outside of its local scope.
1848-
if _WIFSIGNALED(sts):
1849-
self.returncode = -_WTERMSIG(sts)
1850-
elif _WIFEXITED(sts):
1851-
self.returncode = _WEXITSTATUS(sts)
1852-
elif _WIFSTOPPED(sts):
1848+
if _WIFSTOPPED(sts):
18531849
self.returncode = -_WSTOPSIG(sts)
18541850
else:
1855-
# Should never happen
1856-
raise SubprocessError("Unknown child exit status!")
1857-
1851+
self.returncode = waitstatus_to_exitcode(sts)
18581852

18591853
def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
18601854
_WNOHANG=os.WNOHANG, _ECHILD=errno.ECHILD):

‎Lib/test/support/__init__.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -3442,18 +3442,11 @@ def wait_process(pid, *, exitcode, timeout=None):
34423442

34433443
sleep = min(sleep * 2, max_sleep)
34443444
time.sleep(sleep)
3445-
3446-
if os.WIFEXITED(status):
3447-
exitcode2 = os.WEXITSTATUS(status)
3448-
elif os.WIFSIGNALED(status):
3449-
exitcode2 = -os.WTERMSIG(status)
3450-
else:
3451-
raise ValueError(f"invalid wait status: {status!r}")
34523445
else:
34533446
# Windows implementation
34543447
pid2, status = os.waitpid(pid, 0)
3455-
exitcode2 = (status >> 8)
34563448

3449+
exitcode2 = os.waitstatus_to_exitcode(status)
34573450
if exitcode2 != exitcode:
34583451
raise AssertionError(f"process {pid} exited with code {exitcode2}, "
34593452
f"but exit code {exitcode} is expected")

‎Lib/test/test_os.py

+29
Original file line numberDiff line numberDiff line change
@@ -2794,6 +2794,35 @@ def test_waitpid(self):
27942794
pid = os.spawnv(os.P_NOWAIT, FakePath(args[0]), args)
27952795
support.wait_process(pid, exitcode=0)
27962796

2797+
def test_waitstatus_to_exitcode(self):
2798+
exitcode = 23
2799+
filename = support.TESTFN
2800+
self.addCleanup(support.unlink, filename)
2801+
2802+
with open(filename, "w") as fp:
2803+
print(f'import sys; sys.exit({exitcode})', file=fp)
2804+
fp.flush()
2805+
args = [sys.executable, filename]
2806+
pid = os.spawnv(os.P_NOWAIT, args[0], args)
2807+
2808+
pid2, status = os.waitpid(pid, 0)
2809+
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
2810+
self.assertEqual(pid2, pid)
2811+
2812+
# Skip the test on Windows
2813+
@unittest.skipUnless(hasattr(signal, 'SIGKILL'), 'need signal.SIGKILL')
2814+
def test_waitstatus_to_exitcode_kill(self):
2815+
signum = signal.SIGKILL
2816+
args = [sys.executable, '-c',
2817+
f'import time; time.sleep({support.LONG_TIMEOUT})']
2818+
pid = os.spawnv(os.P_NOWAIT, args[0], args)
2819+
2820+
os.kill(pid, signum)
2821+
2822+
pid2, status = os.waitpid(pid, 0)
2823+
self.assertEqual(os.waitstatus_to_exitcode(status), -signum)
2824+
self.assertEqual(pid2, pid)
2825+
27972826

27982827
class SpawnTests(unittest.TestCase):
27992828
def create_args(self, *, with_env=False, use_bytes=False):

‎Lib/test/test_popen.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ def test_popen(self):
4444

4545
def test_return_code(self):
4646
self.assertEqual(os.popen("exit 0").close(), None)
47+
status = os.popen("exit 42").close()
4748
if os.name == 'nt':
48-
self.assertEqual(os.popen("exit 42").close(), 42)
49+
self.assertEqual(status, 42)
4950
else:
50-
self.assertEqual(os.popen("exit 42").close(), 42 << 8)
51+
self.assertEqual(os.waitstatus_to_exitcode(status), 42)
5152

5253
def test_contextmanager(self):
5354
with os.popen("echo hello") as f:

‎Lib/test/test_pty.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ def test_fork(self):
200200
## raise TestFailed("Unexpected output from child: %r" % line)
201201

202202
(pid, status) = os.waitpid(pid, 0)
203-
res = status >> 8
204-
debug("Child (%d) exited with status %d (%d)." % (pid, res, status))
203+
res = os.waitstatus_to_exitcode(status)
204+
debug("Child (%d) exited with code %d (status %d)." % (pid, res, status))
205205
if res == 1:
206206
self.fail("Child raised an unexpected exception in os.setsid()")
207207
elif res == 2:

‎Lib/test/test_wait3.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ def wait_impl(self, cpid, *, exitcode):
3030
time.sleep(0.1)
3131

3232
self.assertEqual(spid, cpid)
33-
self.assertEqual(status, exitcode << 8,
34-
"cause = %d, exit = %d" % (status&0xff, status>>8))
33+
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
3534
self.assertTrue(rusage)
3635

3736
def test_wait3_rusage_initialized(self):

‎Lib/test/test_wait4.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ def wait_impl(self, cpid, *, exitcode):
2929
break
3030
time.sleep(0.1)
3131
self.assertEqual(spid, cpid)
32-
self.assertEqual(status, exitcode << 8,
33-
"cause = %d, exit = %d" % (status&0xff, status>>8))
32+
self.assertEqual(os.waitstatus_to_exitcode(status), exitcode)
3433
self.assertTrue(rusage)
3534

3635
def tearDownModule():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`os.waitstatus_to_exitcode` function:
2+
convert a wait status to an exit code.

0 commit comments

Comments
 (0)
Please sign in to comment.