Asynchronous Countdowns in Python - python

using: python 3.8 on windows 10
I am working on a script that runs in a while loop.
At the end of the loop I want it to wait for 5 seconds for the user to give input and then restart, or exit if they do not give input.
I've never actually used it before but I assumed asyncio would be useful because it allows for the definition of awaitables. However bringing things together is proving more difficult than I'd anticipated.
import keyboard, asyncio, time as t
async def get():
keyboard.record('enter', True)
return True
async def countdown(time):
while time > 0:
print(f'This program will exit in {int(time)} seconds. Press enter to start over.', end='\r')
await asyncio.sleep(1)
# t.sleep(1)
time -= 1
print(f'This program will exit in 0 seconds. Press enter to start over.', end='\r')
return False
async def main():
running = True
while running:
# other code
clock = asyncio.create_task(countdown(5))
check = asyncio.create_task(get())
done, pending = await asyncio.wait({clock, check}, return_when=asyncio.FIRST_COMPLETED)
running = next(iter(done)).result()
print(running, end='\r')
print('bye')
asyncio.run(main())
As it stands, the process doesn't end if I wait for five seconds. It also doesn't visibly count down (my best, and most ridiculous, guess is that it may be looping too fast in main? Try holding down the "enter" key - with and without printing running).
Also, when I switch to time.sleep, the display works fine, but it doesn't seem as though the countdown function ever returns.
Previously I'd tried using an input statement instead of keyboard.record; that blocks. I had also tried using asyncio.wait_for; but the timeout never came, though again, the "enter" key was registered (that said, it wouldn't have printed the countdown even if it had worked). I also tried asyncio.as_completed but I was unable to parse anything useful from the iterable. Happy to be wrong there though!
Also, bonus points if you can generalize the countdown to non-integer time spans <3
The wait_for approach:
async def main():
running = True
while running:
try:
running = await asyncio.wait_for(get(), 5)
except asyncio.TimeoutError:
running = False
print(running)
print('bye')
asyncio.run(main())
as_completed
async def main():
running = True
while running:
## other code
clock = asyncio.create_task(countdown(5))
check = asyncio.create_task(get())
for f in asyncio.as_completed({check, clock}):
# print(f.cr_frame)
# print(f.cr_await, f.cr_running, f.cr_origin, f.cr_frame, f.cr_code)
print(f.cr_await, f.cr_running, f.cr_origin)
print('bye')
Also, bonus points if you can generalize the countdown to non-integer time :P
cheers!

If you look in the docs for keyboard.record it says:
Note: this is a blocking function.
This is why your process didn't end after 5 seconds. It was blocking at keyboard.record('enter', True). If you are going to stick with the keyboard module, what you need to do is create a hook on the 'enter' key. I put a quick demo together with your code:
import asyncio
import keyboard
class Program:
def __init__(self):
self.enter_pressed = asyncio.Event()
self._loop = asyncio.get_event_loop()
async def get(self):
await self.enter_pressed.wait()
return True
#staticmethod
async def countdown(time):
while time > 0:
print(f'This program will exit in {int(time)} seconds. Press enter to start over.')
await asyncio.sleep(1)
# t.sleep(1)
time -= 1
print(f'This program will exit in 0 seconds. Press enter to start over.')
return False
def notify_enter(self, *args, **kwargs):
self._loop.call_soon_threadsafe(self.enter_pressed.set)
async def main(self):
running = True
while running:
# other code
keyboard.on_press_key('enter', self.notify_enter)
self.enter_pressed.clear()
clock = asyncio.create_task(self.countdown(5))
check = asyncio.create_task(self.get())
done, pending = await asyncio.wait({clock, check}, return_when=asyncio.FIRST_COMPLETED)
keyboard.unhook('enter')
for task in pending:
task.cancel()
running = next(iter(done)).result()
print(running)
print('bye')
async def main():
program = Program()
await program.main()
asyncio.run(main())
There is callback created notify_enter, that sets an asyncio.Event whenever it's fired. The get() task waits for this event to trigger before it exits. Since I didn't know what your other code is doing, we don't bind a hook to the enter key's key_down event until right before you await both tasks and we unbind it right after one of the tasks completes. I wrapped everything up in a class so the event is accessible in the callback, since there isn't a way to pass parameters in.

Related

How to stop a loop with keyboard.add_hotkey, while this loop is started by keyboard.add_hotkey?

The following code works perfectly, the loop can be stop by pressing esc:
import time
import keyboard
run = 1
def end():
global run
run = 0
print(run)
def do_stuff():
while run:
print('running')
time.sleep(0.5)
keyboard.add_hotkey('esc', end)
do_stuff()
But if I start this loop with another add_hotkey, I cannot stop it with esc anymore.
import time
import keyboard
run = 1
def end():
global run
run = 0
print(run)
def do_stuff():
while run:
print('running')
time.sleep(0.5)
keyboard.add_hotkey('esc', end)
# do_stuff()
keyboard.add_hotkey('enter', do_stuff)
keyboard.wait()
What should I do to stop this loop? I tried to replace the while run: with while not keyboard.is_pressed('esc'):. It can stop the loop if I hold the esc for a while. But it doesn't seem like a good solution.
=======================
updates:
the following works:
import keyboard
import threading
run = 1
def end():
global run
run = 0
print(run)
def do_stuff():
while run:
print('running')
time.sleep(0.5)
def new_do_stuff():
t = threading.Thread(target=do_stuff, name='LoopThread')
t.start()
keyboard.add_hotkey('esc', end)
keyboard.add_hotkey('enter', new_do_stuff)
keyboard.wait('esc')
Since in the second example you enter the do_stuff() loop through the hotkey and never leave the do_stuff() loop, the system is still captured in the hotkey command and is not listening for hotkeys anymore. You would have to find a way to leave the loop after the keyboard.add_hotkey('enter', do_stuff) command and enter it externally through another way, so the system listens for hotkey-entries again.
I'm not aware of the context you're using this in, but using some sort of a main-loop, that does nothing but wait for a flag to be set (it should be set when you get the hotkey interrupt) and then enters the do_stuff() loop seems like a way to solve it.

How to stop this running threading.Thread?

I found this non blocking code on stack overflow which is using threads to provide functionality of nonblocking setInterval function in JavaScript. But when I try to stop the process it doesn't even the Ctrl + C is not stopping it, I have tried some more methods too stop the process but they are not working.
Can someone please tell a right way to stop the process, thank you in advance.
here is the code
import threading
class ThreadJob(threading.Thread):
def __init__(self,callback,event,interval):
'''runs the callback function after interval seconds
:param callback: callback function to invoke
:param event: external event for controlling the update operation
:param interval: time in seconds after which are required to fire the callback
:type callback: function
:type interval: int
'''
self.callback = callback
self.event = event
self.interval = interval
super(ThreadJob,self).__init__()
def run(self):
while not self.event.wait(self.interval):
self.callback()
event = threading.Event()
def foo():
print ("hello")
def boo():
print ("fello")
def run():
try:
k = ThreadJob(foo,event,2)
d = ThreadJob(boo,event,6)
k.start()
d.start()
while 1:
falg = input("Press q to quit")
if(falg == 'q'):
quit()
return
except KeyboardInterrupt:
print('Stoping the script...')
except Exception as e:
print(e)
run()
print( "It is non-blocking")
All you need to do is to replace this line:
quit()
with this one:
event.set()
A Python program won't exit if there are threads still running, so the quit() function wasn't doing anything. The threads will exit their while loops once the event's internal flag is set, so a call to event.set() will cause the termination of both extra threads you created. Then the program will exit.
Note: technically you could set the threads to be "daemon" and then they will not keep the program alive. But that's not the right solution here, I think.

await asyncio.sleep(1) not working in python

My code execution does not reach the print statement: print("I want to display after MyClass has started")
Why is this? I thought the purpose of await asyncio.sleep() is to unblock execution of code so that subsequent lines of code can run. Is that not the case?
import asyncio
class MyClass:
def __init__(self):
self.input = False
asyncio.run(self.start())
print("I want to display after MyClass has started") #This line is never reached.
async def start(self):
while True:
print("Changing state...")
if self.input:
print("I am on.")
break
await asyncio.sleep(1)
m = MyClass()
m.input = True #This line is never reached! Why?
print("I want to display after MyClass is started")
When I execute, it keeps printing "Changing state...". Even when I ctrl+c to quit, the execution continues as shown below. How can I properly terminate the execution? Sorry, I am new to python.
EDIT:
I appreciate the common use of asyncio is for running two or more separate functions asynchronously. However, my class is one which will be responding to changes in its state. For example, I intend to write code in the setters to do stuff when the class objects attributes change -WHILE still having a while True event loop running in the background. Is there not any way to permit this? I have tried running the event loop in it's own thread. However, that thread then dominates and the class objects response times run into several seconds. This may be due to the GIL (Global Interpreter Lock) which we can do nothing about. I have also tried using multiprocessing, but then I lose access to the properties and methods of the object as parallel process run in their own memory spaces.
In the init method of MyClass you invoke asyncio.run() - this method will execute the required routine until that routine terminates. In your case, since the main method includes a while True loop, it will never terminate.
Here is a slight modification of your code that perhaps shows the concurrency effect you're after -
import asyncio
class MyClass:
def __init__(self):
self.input = False
asyncio.run(self.main())
print("I want to display after MyClass has been initialized.") # This line is never reached.
async def main(self):
work1 = self.work1()
work2 = self.work2()
await asyncio.gather(work1, work2)
async def work1(self):
for idx in range(5):
print('doing some work 1...')
await asyncio.sleep(1)
async def work2(self):
for idx in range(5):
print('doing some work 2...')
await asyncio.sleep(1)
m = MyClass()
print("I want to display after MyClass is terminated")

Having trouble quitting sched.scheduler module?

I'm using Selenium Webdriver in my program in order to try and automate something. I am then parsing th resulting page, and checking for a specific element in the page. If the page doesn't have the specific element, then I use sched.scheduler to re-automate the task, by having the user click a button (in the Tkinter GUI). The button runs a function, which schedules a task for sched.scheduler, and has the task be sent to a function in which I created a new process from the multiprocessing module.
This is basically what it is:
import time
import sched
from multiprocessing import Process
#the function needs to run for the first time, then waits for user input if an error shows up
#if it's the second time around, the worker function runs the scheduler
global first_time_happening
first_time_happening = True
terminate = False
scheduler = sched.scheduler(time.time, time.sleep)
def worker():
#insert some working process here using selenium webdriver
print("Worker happened!")
global first_time_happening
if first_time_happening:
first_time_happening = False
elif not first_time_happening:
global relay_to_timer
relay_to_timer = scheduler.enter(5, 2, timer)
scheduler.run()
def process():
p = Process(target=worker)
#p.daemon = True
p.start()
def timer():
if not terminate:
global relay_to_process
relay_to_process = scheduler.enter(5, 2, process)
scheduler.run()
if terminate:
scheduler.cancel(relay_to_process)
scheduler.cancel(relay_to_timer)
def quit_button():
global terminate
terminate = True
if scheduler.empty:
print("The line is empty")
elif not scheduler.empty:
print("Something in the queue!")
while not scheduler.empty:
scheduler.cancel(relay_to_process)
scheduler.cancel(relay_to_timer)
worker()
#simulating where the GUI asks a question, person presses a button, and the button redirects them
#to function worker()
worker()
#simulating a user press the quit button
quit_button()
It keeps running even after I "hit" quit (or call the quit function in this case). I keep getting the queue is empty, but I'm not sure why it isn't working? Any help is appreciated, thanks!!
The scheduler keeps running even with an empty queue just in case somebody (presumably another thread) entered something again. I believe the way to make it end is to raise an exception (whether from the action or delay function) -- .run will propagate it and you can catch it.
To wit...
class AllDoneException(Exception): pass
def worker():
#insert some working process here using selenium webdriver
print("Worker happened!")
global first_time_happening
if first_time_happening:
first_time_happening = False
elif not first_time_happening:
global relay_to_timer
relay_to_timer = scheduler.enter(5, 2, timer)
try:
scheduler.run()
except AllDoneException:
pass
and in function timer
if terminate:
raise AllDoneException

python thread weird behavior

I have a timer function which I am calling it in another function like this
import time
import threading
def f():
while(True):
print "hello"
time.sleep(5)
def execute():
t = threading.Timer(5,f)
t.start()
command = ''
while command != 'exit':
command = raw_input()
if command == 'exit':
t.cancel()
Even if after entering "exit" command, the function is printing "hello"
I am not able to figure out Whats wrong with the code
class threading.Timer - cancel() - Doc-Link
Stop the timer, and cancel the execution of the timer’s action. This will only work if the timer is still in its waiting stage.
A very simple Version of what you are trying to accomplish could look like this.
import threading
_f_got_killed = threading.Event()
def f():
while(True):
print "hello"
_f_got_killed.wait(5)
if _f_got_killed.is_set():
break
def execute():
t = threading.Timer(5,f)
t.start()
command = ''
while command != 'exit':
command = raw_input()
if command == 'exit':
_f_got_killed.set()
t.cancel()
execute()
For forcefully killing a thread look at this:
Is there any way to kill a Thread in Python?
You are using cancel wrong. In http://docs.python.org/2/library/threading.html, it states: "Timers are started, as with threads, by calling their start() method. The timer can be stopped (before its action has begun) by calling the cancel() method. The interval the timer will wait before executing its action may not be exactly the same as the interval specified by the user."
In your code, if you try to use cancel after the timed thread has already begun its execution (it will in 5 seconds), cancel accomplishes nothing. The thread will remain in the while loop in f forever until you give it some sort of forced interrupt. So typing "exit" in the first 5 seconds after you run execute works. It will successfully stop the timer before the thread even begins. But after your timer stops and your thread starts executing the code in f, there will be no way to stop it through cancel.

Categories

Resources