1

Starting Async Grpc Server and wait_for_termination will blocking the tkinter mainloop and vice versa, the tkinter mainloop will not allow the running of the Async Grpc Server wait_for_termination. It seems that tkinter is taking control of the main event loop and will let anything asynchronously outsdide this loop. Running insde a console will work obviously without run no let anything.

class PearlApp(tk.Tk):
     def start(self):
            await grpcserver.start()
            await grpcserver.wait_for_termination()
            self.mainloop()
M A
  • 11
  • 1

1 Answers1

2

Tkinter has been created way before Python featured async capabilities, and it won't work by default along async features in the same context - that is a given.

BTW, the snippet you posted will actually be a syntax error: one can't use await inside a non-async function, as it is in your code.

There are two ways to go there: move all grpc related code into code running in another thread, with its own async loop - this should work unless grpc for some reason requires being run on the main thread.

The other way is to handle the tkinter loop manually inside an async callback , and call root.update() on your root tkinter object.

Another thing to notice is that, contrary to most tkinter examples online, which were written over 10 years ago, inheriting your app class from tkinter.Tk is a *terrible idea. Tk has over 260 attributes and methods and any of those may colide with methods and attributes you need to add to your app. A lot of those are native methods, and won't work well being simply overriden by other attributes or methods, even if you know what you are doing.

The correct thing to do is to have your app setup in a class (or even in a plain function), and have the root window associated to it in a plain attribute, like in:

class PearlApp:
    def __init__(self, ...):
        self.root = tkinter.Tk()

In short, you can use that and call self.root.update() instead of tkinter's mainloop in order to have a responsive app in parallel with code running in an async loop. something along this:

import tkinter as Tk
import grpx


class PearlApp:
     def __init__(self):
        self.tasks = []
        self.setup_grpc()
        self.setup_tkwindow()
        self.command_queue = []
        
        
    def setup_grpc(self):
        self.grpcserver = ...
        
    def setup_tkwindow(self):
        window = self.root = tk.Tk()
        self.stop_condition = False
        window.protocol("WM_DELETE_WINDOW", self.close)
        # code to setup all widgets and controls in the tkinter window
        # ex. run_button = tk.Button(window, command=self.start_grpc_callback, text="start server")
        
    def close_callback(self):
        self.stop_condition = True
        
    def close(self):
        for task in self.tasks:
            try:
                task.result()
            except asyncio.InvalidStateError:
                print("task not finished")
        
    def start_grpc_callback(self):
        self.command_queue.append(self.start_grpc)
        
    async def start_grpc(self):
        await self.grpcserver.start()
        # this can't be called with "await" because it would block the thread - 
        # adding it as a task, instead, will run it on the background
        # at each loop run. 
        # ATTENTIOM: IT MAY BE THAT THIS FUNCTION IS NOT COLABORATIVE AT ALL WITH
        # OTHER ASYNC CODE, and is blocking itself - consult grpc docs for that:
        self.tasks.append(asyncio.create_task(grpcserver.wait_for_termination()))  
        
        
    async def run(self):
        while True:
            if self.command_queue:
                command = self.command_queue.pop(0)
                await command()
            self.root.update()  # synchronously updates the Tk app and respond to input events
            await asyncio.sleep(0)  # handles control to one pass of asyncio loop
            if self.stop_condition:
                break
        self.close()
        self.root.destroy()
        self.root.update()
        
...

if __name__ == "__main__":
    app = PearlApp()
    asyncio.run(app.run())
                
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • While I do agree that subclassing the instance of `Tk` can be tricky. I don't think it is a terrible idea, when necessarily add functionality to your root window (e.g. Drag and Drop). Before I set those attributes I just print out if this name is already taken. I would have preferred seeing you stick to the thread suggestion, since `tkinter.update` is [ill considered](https://stackoverflow.com/a/66781785/13629335) – Thingamabobs Aug 24 '22 at 16:50