So I have this code, which is just a very simple example of something more complicated that I am trying to achieve:
import asyncio
import functools
import tkinter as tk
root = tk.Tk()
async def async_function(rl):
await asyncio.sleep(1)
rl.configure(text="some text")
await asyncio.sleep(1)
rl.configure(text="new text")
result_label = tk.Label(root, text="original")
result_label.pack()
button = tk.Button(root, text='Run Async Function', command=functools.partial(async_function, result_label))
button.pack()
root.mainloop()
This loads the GUI, puts a label with some text on it, and a button. My expectation is that when I click the button, the async_function would fire, causing the label to cycle the text from "original" to "some text" to "new text", waiting a second between each cycle of text. Instead, I get this in my console:
text.py:18: DeprecationWarning: There is no current event loop
loop = asyncio.get_event_loop()
C:\Program Files\Python311\Lib\tkinter\__init__.py:1485: RuntimeWarning: coroutine 'async_function' was never awaited
self.tk.mainloop(n)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
I don't really have enough experience to take anything meaningful out of this. All I know is that the text doesn't cycle. Some different configurations can cause the function to wait 2 seconds, then change the label to the very last text sequence ("new text"), but nothing past that. Any idea on how to achieve this?
I think it also might be worthwhile to mention this thread is kind of close to my idea. My code is a little more interconnected than the OP's, apparently, because I can't separate my process out from a long one and a short one. My question may give off that "vibe", but I promise my function is way more interconnected, so the answer from this thread wouldn't answer my question.
EDIT
This question is getting a little more complicated than what I'd like, but in effort to help you reproduce it, here is some more code:
I have a file called "TkinterLoggerWidget.py":
import logging
import tkinter as tk
from functools import partial
class TkinterLogger(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
self.text_widget.tag_config("error", foreground="red")
def emit(self, record):
msg = self.format(record)
if record.levelname == "ERROR":
self.text_widget.insert(tk.END, f"{msg}\n", "error")
else:
self.text_widget.insert(tk.END, f"{msg}\n")
self.text_widget.see(tk.END)
class ConsoleWidget(tk.Frame):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.master = master
# Initialize the container
self.container = tk.Frame()
self.container.pack()
# Initialize the label
self.label = tk.Label(self.container, text="Console")
self.label.pack()
# Initialize the Text widget as the console
self.text_widget = tk.Text(self.container)
self.text_widget.pack(fill="both", expand=True)
self.text_widget.tag_config("error", foreground="red")
self.log = logging.getLogger()
self.handler = TkinterLogger(self.text_widget)
self.handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s:\n\t%(message)s')
self.handler.setFormatter(formatter)
self.log.addHandler(self.handler)
self.log.setLevel(logging.INFO)
# self.pack()
if __name__ == "__main__":
# Example usage:
# Create GUI
root = tk.Tk()
console = ConsoleWidget(master=root)
# console.pack()
def my_function():
v = int("t")
def main(l):
# Call function that will write logs to the main tkinter script's logger
try:
l.info("Trying to run function!")
my_function()
except Exception as e:
l.error(str(e))
# Continuously update the GUI with new logs
root.after(1000, partial(main, l))
# Start main loop
root.after(1000, partial(main, console.log))
root.mainloop()
I have another file called "TkinterApp.py"
import tkinter as tk
import logging
from functools import partial
import asyncio
import TkinterLoggerWidget as TLW
import platform # For getting the operating system name
import subprocess # For executing a shell command
class App:
def __init__(self, window, window_title, func=None):
self.window = window
self.window.title(window_title)
self.function = func
self.console = TLW.ConsoleWidget(master=self.window)
def handle_event(this, event):
if this.function is not None:
this.function(["www.google.com", "www.example.com", "www.purple.com"], logger=this.console.log)
self.button = tk.Button(self.window, text='Run Function')
self.button.pack()
self.window.bind("<Button-1>", partial(handle_event, self))
self.window.mainloop()
def ping(hosts, logger=None):
# Option for the number of packets as a function of
param = '-n' if platform.system().lower()=='windows' else '-c'
# Building the command. Ex: "ping -c 1 google.com"
for host in hosts:
command = ['ping', param, '1', host]
if logger is not None:
logger.info(f"Pinged {host} and exited with status {subprocess.call(command)}")
if __name__ == '__main__':
App(tk.Tk(), "Log Functions", func=ping)
handle_event
is fired when the window fires the event, which is when the button is clicked. The function ping
gets fired inside of handle_event
. Feel free, if need be, to change ping to an async function. The goal is to just to get the logger to log the messages real time and not after ping has finished.
Update
This isn't technically an answer to this problem I originally posted, so I am not making it "the answer", but I was able to use it as a solution for the bigger problem I was facing. Widgets have an update method, which causes the main loop for tkinter to prioritize updating a particular widget in the update queue. So I simply added
self.text_widget.update()
to the end of the emit method in the TkinterLoggerWidget.py's TkinterLogger Class, so it looks like:
def emit(self, record):
msg = self.format(record)
if record.levelname == "ERROR":
self.text_widget.insert(tk.END, f"{msg}\n", "error")
else:
self.text_widget.insert(tk.END, f"{msg}\n")
self.text_widget.see(tk.END)
self.text_widget.update()