This is a Python 3.8+ (although it can be adapted to Python 3.6+) cross-platform approach that only uses threading
(so no multiprocessing
or calls to shell utilities). It is intended for running scripts from the command-line and isn't very suited for dynamical use.
You can wrap the builtin input
function as follows. In this case I'm redefining the built-in name input
as the wrapper, since this implementation requires all calls to input
to be routed through this. (Disclaimer: that's why it's probably not a very good idea, just a different one, for fun.)
import atexit
import builtins
import queue
import threading
def _make_input_func():
prompt_queue = queue.Queue(maxsize=1)
input_queue = queue.Queue(maxsize=1)
def get_input():
while (prompt := prompt_queue.get()) != GeneratorExit:
inp = builtins.input(prompt)
input_queue.put(inp)
prompt_queue.task_done()
input_thread = threading.Thread(target=get_input, daemon=True)
last_call_timed_out = False
def input_func(prompt=None, timeout=None):
"""Mimics :function:`builtins.input`, with an optional timeout
:param prompt: string to pass to builtins.input
:param timeout: how long to wait for input in seconds; None means indefinitely
:return: the received input if not timed out, otherwise None
"""
nonlocal last_call_timed_out
if not last_call_timed_out:
prompt_queue.put(prompt, block=False)
else:
print(prompt, end='', flush=True)
try:
result = input_queue.get(timeout=timeout)
last_call_timed_out = False
return result
except queue.Empty:
print(flush=True) # optional: end prompt line if no input received
last_call_timed_out = True
return None
input_thread.start()
return input_func
input = _make_input_func()
del _make_input_func
(I've defined the setup in the one-use-only _make_input_func
to hide input
's "static" variables in its closure, in order to avoid polluting the global namespace.)
The idea here is to make a separate thread which handles any and all calls to builtins.input
, and make the input
wrapper manage the timeout. Since a call to builtins.input
always blocks until there is input, when the timeout is over, the special thread is still waiting for input, but the input
wrapper returns (with None
). At the next call, if the last call timed out, it doesn't need to call builtins.input
again (since the input thread has already been waiting for input), it just prints the prompt, and then waits for said thread to return some input, as always.
Having defined the above, try running the following script:
import time
if __name__ == '__main__':
timeout = 2
start_t = time.monotonic()
if (inp := input(f"Enter something (you have {timeout} seconds): ", timeout)) is not None:
print("Received some input:", repr(inp))
else:
end_t = time.monotonic()
print(f"Timed out after {end_t - start_t} seconds")
inp = input("Enter something else (I'll wait this time): ")
print("Received some input:", repr(inp))
input(f"Last chance to say something (you have {timeout} seconds): ", timeout)