3

I am creating a cryptocurrency exchange API client using Python3.5 and Tkinter. I have several displays that I want to update asynchronously every 10 seconds. I am able to update the displays every 10 seconds using Tk.after() like in this example

def updateLoans():
    offers = dd.loanOffers()
    demands = dd.loanDemands()
    w.LoanOfferView.delete(1.0, END)
    w.LoanDemandView.delete(1.0, END)
    w.LoanOfferView.insert(END, offers)
    w.LoanDemandView.insert(END, demands)
    print('loans refreshed')

    root.after(10000, updateLoans)

In order for the after method to continue to update continuously every 10 seconds the function updateLoans() needs to be passed as a callable into after() inside of the function.

Now the part that is stumping me, when I make this function asynchronous with python's new async and await keywords

async def updateLoans():
    offers = await dd.loanOffers()
    demands = await dd.loanDemands()
    w.LoanOfferView.delete(1.0, END)
    w.LoanDemandView.delete(1.0, END)
    w.LoanOfferView.insert(END, offers)
    w.LoanDemandView.insert(END, demands)
    print('loans refreshed')

    root.after(10000, updateLoans)

The problem here is that I can not await a callable inside of the parameters for the after method. So I get a runtime warning. RuntimeWarning: coroutine 'updateLoans' was never awaited.

My initial function call IS placed inside of an event loop.

loop = asyncio.get_event_loop()
loop.run_until_complete(updateLoans())
loop.close()

The display populates just fine initially but never updates.

How can I use Tk.after to continuously update a tkinter display asynchronously?

Artemis
  • 2,553
  • 7
  • 21
  • 36
Riley Hughes
  • 1,344
  • 2
  • 12
  • 22

1 Answers1

11

tk.after accepts a normal function, not a coroutine. To run the coroutine to completion, you can use run_until_complete, just as you did the first time:

loop = asyncio.get_event_loop()
root.after(10000, lambda: loop.run_until_complete(updateLoans()))

Also, don't call loop.close(), since you'll need the loop again.


The above quick fix will work fine for many use cases. The fact is, however, that it will render the GUI completely unresponsive if updateLoans() takes a long time due to slow network or a problem with the remote service. A good GUI app will want to avoid this.

While Tkinter and asyncio cannot share an event loop yet, it is perfectly possible to run the asyncio event loop in a separate thread. The main thread then runs the GUI, while a dedicated asyncio thread runs all asyncio coroutines. When the event loop needs to notify the GUI to refresh something, it can use a queue as shown here. On the other hand, if the GUI needs to tell the event loop to do something, it can call call_soon_threadsafe or run_coroutine_threadsafe.

Example code (untested):

gui_queue = queue.Queue()

async def updateLoans():
    while True:
        offers = await dd.loanOffers()
        demands = await dd.loanDemands()
        print('loans obtained')
        gui_queue.put(lambda: updateLoansGui(offers, demands))
        await asyncio.sleep(10)

def updateLoansGui(offers, demands):
    w.LoanOfferView.delete(1.0, END)
    w.LoanDemandView.delete(1.0, END)
    w.LoanOfferView.insert(END, offers)
    w.LoanDemandView.insert(END, demands)
    print('loans GUI refreshed')

# http://effbot.org/zone/tkinter-threads.htm
def periodicGuiUpdate():
    while True:
        try:
            fn = gui_queue.get_nowait()
        except queue.Empty:
            break
        fn()
    root.after(100, periodicGuiUpdate)

# Run the asyncio event loop in a worker thread.
def start_loop():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.create_task(updateLoans())
    loop.run_forever()
threading.Thread(target=start_loop).start()

# Run the GUI main loop in the main thread.
periodicGuiUpdate()
root.mainloop()

# To stop the event loop, call loop.call_soon_threadsafe(loop.stop).
# To start a coroutine from the GUI, call asyncio.run_coroutine_threadsafe.
user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    One of the most clear and thorough answers I've received on stack overflow. Thanks this helped a lot – Riley Hughes Apr 22 '18 at 14:31
  • 1
    So trying to implement your second solution, I keep getting this error. `RuntimeError: There is no current event loop in thread 'Thread-1'` – Riley Hughes Apr 22 '18 at 20:16
  • @RileyHughes Ok, in Python 3.5 you will need to additionally set the event loop for the new thread. I've now amended the answer. – user4815162342 Apr 23 '18 at 05:09