0

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()
Shmack
  • 1,933
  • 2
  • 18
  • 23
  • Using [tag:async] is a little bit difficult in [tag:tkinter] and often isn't really needed. Would you be ok with having the same effect but not using the syntax of async? – Thingamabobs May 07 '23 at 08:44
  • @Thingamabobs, if you want to post an answer, I will let you know if it would be compatible with what I am going for - without down voting :) . The longer description of the problem is that I have a custom widget that is a console, using the logging library, it writes it out to a text box on the tkinter app. Though I pass the logger to the function, the multiple logging statements in the function do NOT print in real time. Rather, after the function has finished running, it dumps all of the logging statements to the console. I think I am going to have to use threading for this. – Shmack May 07 '23 at 15:24
  • so you need async functionality for you logger, like for a server ? Just to show text in portions seems not the specific problem then, am I wrong ? – Thingamabobs May 07 '23 at 15:32
  • @Thingamabobs, Yes, I am using it in conjunction with a server. The more I've spent thinking on this question the more apparent it's becoming that this is a multithreading question and I am contemplating just deleting this question, and looking more into multithreading. Thoughts? I also don't understand your last question, can you rephrase? – Shmack May 07 '23 at 15:36
  • I have an idea, but this assumes that you already have solved running tkinter asynchronous. If not it might be easier to have both loops in separated threads and share the data through a queue that both `put` or `get` on regular basis. – Thingamabobs May 07 '23 at 15:39
  • @Thingamabobs, I fixed all the bugs with the example. It should be reproduceable as to what I am trying to do now :) – Shmack May 07 '23 at 16:03

2 Answers2

0

I won't pretend I know anything about asyncio, I just read some articles online to try and put something together for you that will work and make sense:

Apparently calling asyncio functions has to be done through asyncio.run().

First of all, I want to make it a bit simpler by removing the rl input from the function because it's redundant. You can configure the labels without having to pass them into the function

Second of all, I added root.update_idletasks() in 2 places in the function order to get the labels to update after each change.

Here is the working code:

import asyncio
import functools
import tkinter as tk

root = tk.Tk()


async def async_function():
    root.update_idletasks()
    await asyncio.sleep(1)
    result_label.configure(text="some text")
    root.update_idletasks()
    await asyncio.sleep(1)
    result_label.configure(text="new text")

    return print("async_function ended successfully")

result_label = tk.Label(root, text="original")
result_label.pack()

button = tk.Button(master=root,
                   text='Run Async Function',
                   command=functools.partial(asyncio.run, async_function()))
button.pack()

root.mainloop()

So basically you have to pass your function as an input to asyncio.run()

  • While running this command the window will be unresponsive and you cannot use the button twice. I don't think this is a suitable answer as it stands. – Thingamabobs May 07 '23 at 15:22
  • I see, @Thingamabobs comment is accurate. It's a good attempt at it. If the function only had to be called once, it'd be a reasonable answer. Please do not delete this, it could be helpful for some one else, but feel free to post another answer. – Shmack May 07 '23 at 15:30
  • @Shmack I see what you mean, I was focused only on the labels based on the info I had. I see you posted more code so I'll try to have another go at it :) – Robert Salamon May 08 '23 at 05:01
0

First thing I see is that you are calling the function at the time you're creating the button.

button = tk.Button(root, text='Run Async Function', command=functools.partial(async_function, result_label))

If you need to pass arguments to a button command, you'll need to set it as a lambda

button = tk.Button(root, text='Run Async Function', command=lambda: functools.partial(async_function, result_label))
Pragmatic_Lee
  • 473
  • 1
  • 4
  • 10
  • 1
    `functools.partial` returns a function, so I shouldn't have to use a lambda here. If you try `from functools import; partial(max, [5, 3])()`, this is the same as max([5, 3]). – Shmack May 08 '23 at 20:49