6

I'm using Python 3 and I wanted to code a program that asks for multiple user inputs for a certain amount of time. Here is my attempt at that:

from threading import Timer
##
def timeup():
    global your_time
    your_time = False
    return your_time
##
timeout = 5
your_Time = True
t = Timer(timeout, timeup)
t.start()
##
while your_time == True:
    input()
t.cancel()
print('Stop typing!')

The problem is, the code still waits for an input even if the time is up. I would like the loop to stop exactly when the time runs out. How do I do this? Thank you!

Darkonaut
  • 20,186
  • 7
  • 54
  • 65
  • Since the answers are locked I will answer it here... This will ask for an input and after 5 seconds automatically close stating that the timer has ended. Code: import time import threading timer = 5 timeout = time.time() + 1 * timer def do_input(): while True: message = input("Message: ") def do_timer(): while time.time() < timeout: continue print("\nTimer has ended!") exit() for i in range(1): thread = threading.Thread(target=do_input) thread.daemon = True thread.start() thread1 = threading.Thread(target=do_timer).start() – Feitan Portor Nov 07 '18 at 13:41
  • @Feitan Portor That's essantialy how I started up writing my answer below. The issue with this is, that your deamon-prompt-thread lives until the whole process ends. Usually you would assume that the program should keep going on doing something _after_ the timeout happens and then you don't want the prompting thread alive taking in further inputs. Some specific avoidable issues with your implementation are that it uses busy waiting within the timer (add some `time.sleep` to prevent hammering the cpu ) and `exit()` will work in terminal but not in IDE consoles (change to `sys.exit()`) – Darkonaut Nov 08 '18 at 13:26

3 Answers3

6

This solution is platform-independent and immediately interrupts typing to inform about an existing timeout. It doesn't have to wait until the user hits ENTER to find out a timeout occured. Besides informing the user just-in-time this ensures no input after the timeout stepped in is further processed.

Features

  • Platform independent (Unix / Windows).
  • StdLib only, no external dependencies.
  • Threads only, no Subprocesses.
  • Immediate interrupt at timeout.
  • Clean shutdown of prompter at timeout.
  • Unlimited inputs possible during time span.
  • Easy expandable PromptManager class.
  • Program may resume after timeout, multiple runs of prompter instances possible without program restart.

This answer uses a threaded manager instance, which mediates between a separate prompting thread and the MainThread. The manager-thread checks for timeout and forwards inputs from the prompt-thread to the parent-thread. This design enables easy modification in case MainThread would need to be non-blocking (changes in _poll to replace blocking queue.get()).

On timeout the manager thread asks for ENTER to continue and uses an threading.Event instance to assure the prompt-thread shuts down before continuing. See further details in the doc-texts of the specific methods:

from threading import Thread, Event
from queue import Queue, Empty
import time


SENTINEL = object()


class PromptManager(Thread):

    def __init__(self, timeout):
        super().__init__()
        self.timeout = timeout
        self._in_queue = Queue()
        self._out_queue = Queue()
        self.prompter = Thread(target=self._prompter, daemon=True)
        self.start_time = None
        self._prompter_exit = Event()  # synchronization for shutdown
        self._echoed = Event()  # synchronization for terminal output

    def run(self):
        """Run in worker-thread. Start prompt-thread, fetch passed
        inputs from in_queue and check for timeout. Forward inputs for
        `_poll` in parent. If timeout occurs, enqueue SENTINEL to
        break the for-loop in `_poll()`.
        """
        self.start_time = time.time()
        self.prompter.start()

        while self.time_left > 0:
            try:
                txt = self._in_queue.get(timeout=self.time_left)
            except Empty:
                self._out_queue.put(SENTINEL)
            else:
                self._out_queue.put(txt)
        print("\nTime is out! Press ENTER to continue.")
        self._prompter_exit.wait()

    @property
    def time_left(self):
        return self.timeout - (time.time() - self.start_time)

    def start(self):
        """Start manager-thread."""
        super().start()
        self._poll()

    def _prompter(self):
        """Prompting target function for execution in prompter-thread."""
        while self.time_left > 0:
            self._in_queue.put(input('>$ '))
            self._echoed.wait()  # prevent intermixed display
            self._echoed.clear()

        self._prompter_exit.set()

    def _poll(self):
        """Get forwarded inputs from the manager-thread executing `run()`
        and process them in the parent-thread.
        """
        for msg in iter(self._out_queue.get, SENTINEL):
            print(f'you typed: {msg}')
            self._echoed.set()
        # finalize
        self._echoed.set()
        self._prompter_exit.wait()
        self.join()


if __name__ == '__main__':

    pm = PromptManager(timeout=5)
    pm.start()

Example Output:

>$ Hello
you typed: Hello
>$ Wor
Time is out! Press ENTER to continue.

Process finished with exit code 0

Note the timeout-message here popped up during the attempt of typing "World".

Darkonaut
  • 20,186
  • 7
  • 54
  • 65
2

You can use the poll() method (tested on Linux):

import select,sys

def timed_input(sec):

    po= select.poll()   # creating a poll object
    # register the standard input for polling with the file number 
    po.register(sys.stdin.fileno(), select.POLLIN)  

    while True:
        # start the poll
        events= po.poll(sec*1000)   # timeout: milliseconds
        if not events:
            print("\n Sorry, it's too late...")
            return ""

        for fno,ev in events:     #  check the events and the corresponding fno  
            if fno == sys.stdin.fileno():  # in our case this is the only one
                return(input())


s=timed_input(10)
print("From keyboard:",s)  

The stdin buffers the pressed keys, and the input() function read that buffer at once.

kantal
  • 2,331
  • 2
  • 8
  • 15
1

Here's a short way of doing that, Without using Signals, NOTE: While loop will be blocked until the user has inputted something and then check for condition.

from datetime import datetime, timedelta
t = 5  # You can type for 5 seconds
def timeup():
    final_time = datetime.now() + timedelta(seconds=t)
    print("You can enter now for" + str(t) + " seconds")
    while datetime.now() < final_time:
        input()

    print("STOP TYPING")

timeup()
Vineeth Sai
  • 3,389
  • 7
  • 23
  • 34
  • This will not work. the loop condition will be evaluated once and then wait for the loop body to terminate before evaluating it again, allowing the loop body to run for any amount of time – sam-pyt Nov 06 '18 at 07:46
  • Thank you for pointing it out. Updated my answer with it. – Vineeth Sai Nov 06 '18 at 08:22