Skip to content
Merged
18 changes: 18 additions & 0 deletions Doc/library/subprocess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,24 @@ handling consistency are valid for these functions.
Notes
-----

.. _subprocess-timeout-behavior:

Timeout Behavior
^^^^^^^^^^^^^^^^

When using the ``timeout`` parameter in functions like :func:`run`,
:meth:`Popen.wait`, or :meth:`Popen.communicate`,
users should be aware of the following behaviors:

1. **Process Creation Delay**: The initial process creation itself cannot be interrupted
on many platform APIs. This means that even when specifying a timeout, you are not
guaranteed to see a timeout exception until at least after however long process
creation takes.

2. **Extremely Small Timeout Values**: Setting very small timeout values (such as a few
milliseconds) may result in almost immediate :exc:`TimeoutExpired` exceptions because
process creation and system scheduling inherently require time.

.. _converting-argument-sequence:

Converting an argument sequence to a string on Windows
Expand Down
14 changes: 10 additions & 4 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,8 +1235,11 @@ def communicate(self, input=None, timeout=None):

finally:
self._communication_started = True

sts = self.wait(timeout=self._remaining_time(endtime))
try:
sts = self.wait(timeout=self._remaining_time(endtime))
except TimeoutExpired as exc:
exc.timeout = timeout
raise

return (stdout, stderr)

Expand Down Expand Up @@ -2145,8 +2148,11 @@ def _communicate(self, input, endtime, orig_timeout):
selector.unregister(key.fileobj)
key.fileobj.close()
self._fileobj2output[key.fileobj].append(data)

self.wait(timeout=self._remaining_time(endtime))
try:
self.wait(timeout=self._remaining_time(endtime))
except TimeoutExpired as exc:
exc.timeout = orig_timeout
raise

# All data exchanged. Translate lists into strings.
if stdout is not None:
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ def test_call_timeout(self):
[sys.executable, "-c", "while True: pass"],
timeout=0.1)

def test_timeout_exception(self):
try:
subprocess.run(['echo', 'hi'], timeout = -1)
except subprocess.TimeoutExpired as e:
self.assertIn("-1 seconds", str(e))
else:
self.fail("Expected TimeoutExpired exception not raised")
try:
subprocess.run(['echo', 'hi'], timeout = 0)
except subprocess.TimeoutExpired as e:
self.assertIn("0 seconds", str(e))
else:
self.fail("Expected TimeoutExpired exception not raised")

def test_check_call_zero(self):
# check_call() function with zero return code
rc = subprocess.check_call(ZERO_RETURN_CMD)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Use original timeout value for :exc:`subprocess.TimeoutExpired`
when the func :meth:`subprocess.run` is called with a timeout
instead of sometimes a confusing partial remaining time out value
used internally on the final ``wait()``.
Loading