Skip to content

Commit 63f10c2

Browse files
committed
Only sleep when input stream is waiting
This change means that we don't delay reading the input stream when there is still data available to read from it. This provides a significant speed improvement to scripts which are passing a populated stream of data, rather than awaiting user input from stdin. See #774
1 parent 506bf4e commit 63f10c2

File tree

2 files changed

+43
-6
lines changed

2 files changed

+43
-6
lines changed

invoke/runners.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,8 @@ def handle_stdin(
895895
if self.program_finished.is_set() and not data:
896896
break
897897
# Take a nap so we're not chewing CPU.
898-
time.sleep(self.input_sleep)
898+
if data is None:
899+
time.sleep(self.input_sleep)
899900

900901
def should_echo_stdin(self, input_: IO, output: IO) -> bool:
901902
"""

tests/runners.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import types
99
from io import StringIO
1010
from io import BytesIO
11+
from io import TextIOBase
1112
from itertools import chain, repeat
1213

1314
from pytest import raises, skip
@@ -1098,16 +1099,51 @@ def subclasses_can_override_input_sleep(self):
10981099
class MyRunner(_Dummy):
10991100
input_sleep = 0.007
11001101

1102+
def fake_stdin_stream():
1103+
# The value "foo" is eventually returned.
1104+
yield "f"
1105+
# None values simulate waiting for input on stdin.
1106+
yield None
1107+
yield "o"
1108+
yield None
1109+
yield "o"
1110+
yield None
1111+
# Once the stream is closed, stdin continues to return empty strings.
1112+
while True:
1113+
yield ''
1114+
1115+
class FakeStdin(TextIOBase):
1116+
def __init__(self, stdin):
1117+
self.stream = stdin
1118+
1119+
def read(self, size):
1120+
return next(self.stream)
1121+
11011122
with patch("invoke.runners.time") as mock_time:
11021123
MyRunner(Context()).run(
11031124
_,
1104-
in_stream=StringIO("foo"),
1125+
in_stream=FakeStdin(fake_stdin_stream()),
11051126
out_stream=StringIO(), # null output to not pollute tests
11061127
)
1107-
# Just make sure the first few sleeps all look good. Can't know
1108-
# exact length of list due to stdin worker hanging out til end of
1109-
# process. Still worth testing more than the first tho.
1110-
assert mock_time.sleep.call_args_list[:3] == [call(0.007)] * 3
1128+
# Just make sure the sleeps all look good.
1129+
# There are three here because we return three "None" in fake_stdin_stream.
1130+
assert mock_time.sleep.call_args_list == [call(0.007)] * 3
1131+
1132+
@mock_subprocess()
1133+
def populated_streams_do_not_sleep(self):
1134+
class MyRunner(_Dummy):
1135+
read_chunk_size = 1
1136+
1137+
runner = MyRunner(Context())
1138+
with patch("invoke.runners.time") as mock_time:
1139+
with patch.object(runner, "wait"):
1140+
runner.run(
1141+
_,
1142+
in_stream=StringIO("lots of bytes to read"),
1143+
out_stream=StringIO(), # null output to not pollute tests
1144+
)
1145+
# Sleep should not be called before we break.
1146+
assert len(mock_time.sleep.call_args_list) == 0
11111147

11121148
class stdin_mirroring:
11131149
def _test_mirroring(self, expect_mirroring, **kwargs):

0 commit comments

Comments
 (0)